AWS Glue 5.0移行から6ヶ月、月42万円のコストと格闘した話
「動いてるものを触るな」と思っていたのに、月のGlueコストが42万円を超えた瞬間に決断。Spark 4.0統合・FLEX実行・Iceberg対応の実際の効果と、誰も教えてくれない落とし穴をまとめました。
Glue 4.0から5.0に移行した動機と最初の3週間
正直に言うと、最初は移行する気なかった。Glue 4.0で動いているパイプラインが20本以上あって、「動いてるものを触るな」という暗黙のルールがチームに根付いていたから。でもきっかけは2026年2月、大量の入力データが突然倍増したタイミングで月のGlueコストが42万円を超えたことだった。
その月のSlackで「またGlue高くね?」というメッセージから始まり、AWS Glue 5.0に移行して6ヶ月、コストと落とし穴の現実にも書いた経緯があるんだけど、今回はパイプライン設計にフォーカスして、もっと具体的な実装の話をしていく。
Glue 5.0の最大の変化はSpark 4.0の統合だ。Glue 4.0がSpark 3.3ベースだったのに対し、5.0ではSpark 4.0のAQE(Adaptive Query Execution)が全面的に使えるようになっている。Spark 3.5→4.0移行で半年ハマった話でも触れているように、AQEの挙動は「勝手に最適化してくれる」反面、想定外の動きもある。そのあたりを含めて今日は話していく。
Glue 5.0の主要変更点と実際の挙動
まずスペックを整理しておく。
| 項目 | Glue 4.0 | Glue 5.0 |
|---|---|---|
| Sparkバージョン | 3.3 | 4.0 |
| Pythonバージョン | 3.10 | 3.12 |
| DPUあたりvCPU | 4 | 4(変更なし) |
| FLEX実行 | 対応 | 対応(割引率改善) |
| Icebergネイティブ対応 | v1.2 | v1.6 |
| Delta Lake対応 | v2.4 | v3.1 |
| Job Run Insights | 基本のみ | AI推奨あり |
| Vector Search統合 | なし | あり(プレビュー) |
Job Run InsightsのAI推奨機能は地味に便利で、「このジョブはワーカー数が多すぎる」とか「メモリ使用率が15%しかないから減らせ」みたいなことを自動で教えてくれる。正直まだ完全には信用してないけど、目安として使える。
Spark 4.0統合の実際のパフォーマンス
実際に同じジョブを4.0と5.0で比較した。データ量は日次1.2億レコード、S3からIcebergテーブルへの集計ジョブで、結果はこんな感じだった。
xychart-beta
title "Glue 4.0 vs 5.0 ジョブ実行時間比較(分)"
x-axis ["S3読み込み", "データ変換", "集計処理", "Iceberg書き込み", "合計"]
y-axis "実行時間(分)" 0 --> 80
bar [12, 18, 25, 8, 63]
bar [9, 12, 14, 7, 42]
上がGlue 4.0、下がGlue 5.0。合計で約33%の短縮になった。AQEが集計処理でかなり効いていて、スキューが多いパーティションを自動で分割してくれたのが大きい。ただし、最初の数回は挙動が安定しなかった。個人的には「もう少し素直に動いてくれ」と思いながら調整していたくらい。
実際のジョブ設定コード
4.0と5.0で書き方が変わった部分もあるので、実際の設定を晒す。
# Glue 5.0 対応のジョブスクリプト
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
from pyspark.sql import functions as F
from pyspark.sql.window import Window
args = getResolvedOptions(sys.argv, [
'JOB_NAME',
'source_database',
'target_table',
'execution_date'
])
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
# Glue 5.0 推奨設定: AQEを明示的に有効化
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
# 5.0からIceberg書き込みの最適化設定が追加
spark.conf.set("spark.sql.iceberg.vectorization.enabled", "true")
# Iceberg v1.6 の新機能: ポジション削除ファイルの自動最適化
spark.conf.set(
"spark.sql.extensions",
"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions"
)
execution_date = args['execution_date']
# ソースデータ読み込み
df_source = glueContext.create_dynamic_frame.from_catalog(
database=args['source_database'],
table_name="raw_events",
push_down_predicate=f"event_date='{execution_date}'"
).toDF()
# 変換処理
df_transformed = df_source \
.filter(F.col("status").isin(["COMPLETED", "PARTIAL"])) \
.withColumn(
"revenue_category",
F.when(F.col("amount") >= 10000, "HIGH")
.when(F.col("amount") >= 1000, "MID")
.otherwise("LOW")
) \
.withColumn(
"cumulative_revenue",
F.sum("amount").over(
Window.partitionBy("user_id")
.orderBy("event_timestamp")
.rowsBetween(Window.unboundedPreceding, 0)
)
)
# Icebergへのマージ書き込み (5.0からの推奨パターン)
df_transformed.createOrReplaceTempView("staged_data")
spark.sql(f"""
MERGE INTO glue_catalog.analytics.{args['target_table']} AS target
USING staged_data AS source
ON target.event_id = source.event_id
AND target.event_date = '{execution_date}'
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
""")
job.commit()
4.0からの移行で特に変わったのは、Iceberg書き込みのパターンだ。4.0のときはGlueのgetSinkを使ったDynamicFrameベースの書き込みが多かったけど、5.0ではSpark SQLのMERGE構文を直接使うほうが安定している。パフォーマンスも体感で20%くらい改善した。
うちのチームで実装したパイプライン構成
アーキテクチャ全体を見てもらったほうが理解しやすいと思うので、実際の構成図を載せる。
graph TB
subgraph Internet["外部データソース"]
SaaS[SaaS API]
Webhook[Webhook]
end
subgraph AWS_Account["AWS Account(本番)"]
subgraph Ingestion["データ取り込み層"]
EB[EventBridge Scheduler]
Firehose[Kinesis Firehose]
Lambda_Ingest[Lambda\nAPI Connector]
end
subgraph Storage["ストレージ層"]
S3_Raw["S3 Raw\n(ランディングゾーン)"]
S3_Processed["S3 Processed\n(Icebergテーブル)"]
S3_Curated["S3 Curated\n(Analytics用)"]
end
subgraph Glue_Layer["AWS Glue 5.0 処理層"]
GlueCatalog[Glue Data Catalog]
GlueJob_ETL["Glue Job\nETL変換(Spark 4.0)"]
GlueJob_Agg["Glue Job\n集計処理(FLEX実行)"]
GlueCrawler[Glue Crawler]
GlueWorkflow[Glue Workflow]
end
subgraph Governance["データガバナンス"]
LakeFormation[Lake Formation]
DataZone[DataZone]
Macie[Macie]
end
subgraph Analytics["分析・可視化層"]
Athena[Athena]
QuickSight[QuickSight]
Redshift["Redshift Serverless"]
end
subgraph Monitoring["監視"]
CloudWatch[CloudWatch]
SNS[SNS Alert]
end
end
SaaS --> Lambda_Ingest
Webhook --> Firehose
Lambda_Ingest --> S3_Raw
Firehose --> S3_Raw
EB --> GlueWorkflow
GlueWorkflow --> GlueJob_ETL
GlueJob_ETL --> S3_Raw
GlueJob_ETL --> S3_Processed
GlueJob_ETL --> GlueCatalog
GlueCrawler --> GlueCatalog
GlueCatalog --> GlueJob_Agg
GlueJob_Agg --> S3_Curated
LakeFormation --> GlueCatalog
LakeFormation --> Athena
DataZone --> GlueCatalog
Macie --> S3_Raw
S3_Curated --> Athena
S3_Curated --> Redshift
Athena --> QuickSight
Redshift --> QuickSight
GlueWorkflow --> CloudWatch
CloudWatch --> SNS
この構成のポイントは、GlueジョブをETL変換と集計処理で分けていること。前者はOn-Demand DPUで実行時間を重視、後者はFLEX実行でコストを抑えている。集計処理は多少遅延しても許容できるバッチなので、FLEXで走らせると費用がだいたい40%安くなる。
Lake Formation本番導入で3時間ハマった話にも書いたけど、LakeFormationとGlueの権限まわりは本当に複雑で、最初の設定で丸一日潰れたこともあった。5.0になってもそこは変わっていない。
FLEX実行とジョブブックマークの落とし穴
FLEX実行の本当の挙動
FLEX実行は「余剰キャパシティで動かすから安い」という触れ込みなんだけど、実運用で気づいたことがいくつかある。
まず、FLEX実行のジョブが起動するまでの待機時間が読めない。普通のジョブは1〜2分で起動するのに対し、FLEXは最短2分から最長で20分以上かかることがあった。特に月末や年度末の混雑期は顕著で、SLAがシビアなパイプラインには向かない。この「起動待機が読めない」という特性、最初にちゃんと把握しておかないとSLA違反で痛い目を見る。
# Glue Workflow でFLEXとOn-Demandを使い分ける設定例
import boto3
glue_client = boto3.client('glue', region_name='ap-northeast-1')
# 時間クリティカルなETLジョブ(On-Demand)
glue_client.create_job(
Name='critical-etl-job',
Role='arn:aws:iam::ACCOUNT_ID:role/GlueJobRole',
Command={
'Name': 'glueetl',
'ScriptLocation': 's3://my-scripts/critical_etl.py',
'PythonVersion': '3'
},
GlueVersion='5.0',
WorkerType='G.2X',
NumberOfWorkers=10,
# FLEX実行しない(デフォルトはSTANDARD)
ExecutionClass='STANDARD',
DefaultArguments={
'--enable-job-insights': 'true',
'--enable-auto-scaling': 'true', # 5.0の新機能
'--conf': 'spark.sql.adaptive.enabled=true'
}
)
# 集計・レポートジョブ(FLEX実行)
glue_client.create_job(
Name='aggregation-flex-job',
Role='arn:aws:iam::ACCOUNT_ID:role/GlueJobRole',
Command={
'Name': 'glueetl',
'ScriptLocation': 's3://my-scripts/aggregation.py',
'PythonVersion': '3'
},
GlueVersion='5.0',
WorkerType='G.1X',
NumberOfWorkers=5,
ExecutionClass='FLEX', # コスト削減
DefaultArguments={
'--enable-job-insights': 'true',
'--job-bookmark-option': 'job-bookmark-enable',
'--conf': 'spark.sql.adaptive.enabled=true'
}
)
ジョブブックマークの予期しない動作
地味にハマったのがジョブブックマーク。Glue 5.0でIceberg書き込みを使うと、ブックマークの追跡がS3オブジェクトレベルではなくファイルメタデータレベルに変わっているケースがある。4.0のブックマーク設定をそのまま移行したら、同じデータを2回処理してしまって重複レコードが発生した。これは正直かなり焦った。
対策として、Icebergのタイムスタンプカラムをベースにした独自チェックポイント管理を実装した。
import boto3
import json
from datetime import datetime, timezone
def get_last_processed_timestamp(checkpoint_bucket: str, job_name: str) -> str:
"""独自チェックポイントから最終処理時刻を取得"""
s3 = boto3.client('s3')
checkpoint_key = f"checkpoints/{job_name}/last_processed.json"
try:
response = s3.get_object(Bucket=checkpoint_bucket, Key=checkpoint_key)
checkpoint = json.loads(response['Body'].read())
return checkpoint['last_processed_at']
except s3.exceptions.NoSuchKey:
# 初回実行は7日前から
return "2000-01-01T00:00:00Z"
def save_checkpoint(checkpoint_bucket: str, job_name: str, processed_at: str):
"""処理完了後にチェックポイントを保存"""
s3 = boto3.client('s3')
checkpoint_key = f"checkpoints/{job_name}/last_processed.json"
checkpoint_data = {
'last_processed_at': processed_at,
'updated_at': datetime.now(timezone.utc).isoformat()
}
s3.put_object(
Bucket=checkpoint_bucket,
Key=checkpoint_key,
Body=json.dumps(checkpoint_data),
ContentType='application/json'
)
# ジョブ内での使用例
last_ts = get_last_processed_timestamp('my-checkpoint-bucket', args['JOB_NAME'])
df_incremental = spark.sql(f"""
SELECT *
FROM glue_catalog.raw.events
WHERE updated_at > TIMESTAMP '{last_ts}'
""")
# 処理完了後
current_ts = datetime.now(timezone.utc).isoformat()
save_checkpoint('my-checkpoint-bucket', args['JOB_NAME'], current_ts)
これで重複処理の問題は解消した。正直、ブックマーク機能に全面依存するのは危うくて、重要なパイプラインは自前でチェックポイントを持ったほうがいいと今は思っている。
コスト変化の実測データ
移行前後のコスト推移を月次で追ったデータがある。
xychart-beta
title "月次Glueコスト推移(万円)"
x-axis ["2025-12", "2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07"]
y-axis "コスト(万円)" 0 --> 50
line [32, 35, 42, 38, 29, 24, 22, 21]
2026年2月がピークで42万円。3月から5.0への移行作業を始めて、4月に本番切り替え。7月時点で21万円まで落ちた。半分以下になっている。
主な削減要因は3つあって、それぞれ効果の大きさが違う。
- Auto Scaling導入:Glue 5.0のAuto Scalingが地味に効いていて、余剰DPUが自動的に解放されるようになった。4.0では固定ワーカー数で動かしていたので、処理が終わってもDPUの課金が続いていた。
- FLEX実行への切り替え:SLA不要な集計ジョブ10本をFLEXに変更。体感で35〜40%のコスト削減。
- ジョブ統合:細かくジョブを分割していたものをGlue Workflowで整理し、コールドスタートのオーバーヘッドを削減。
データパイプラインで2年間ハマり続けた話で書いたように、パイプラインのオーバーヘッド設計は最初から考えておかないと後で大変になる。うちもこのタイミングで整理し直した。
Glue 5.0で地味に助かっている新機能
Job Run InsightsのAI推奨
Glue 5.0から、ジョブ実行後に「次回はこうしたほうがいい」というAI推奨が出るようになった。最初は「どうせ役に立たないやつ」と思ってたんだけど、実際に見てみると精度が思ったより高かった。
【Job Run Insightsからの推奨例】
- ジョブID: aggregation-flex-job-2026-07-15
- メモリ使用率: 平均23%(最大41%)
- 推奨: G.1X × 5ワーカー → G.1X × 3ワーカーに削減可能
- 推定コスト削減: 月約18,000円
- データスキュー検出: パーティション 'user_segment=premium' が全体の67%を占有
→ Salting手法またはAQEのスキュー結合最適化の適用を推奨
メモリ使用率のアドバイスはかなり信頼できる。スキュー検出も自動で出してくれるのはマジで助かる。ただし「削減可能」の推定コストは実際より少なめに出る傾向があるので、そのまま鵜呑みにするのは禁物。
Iceberg v1.6のRow-level Delete最適化
4.0ではIcebergのDELETE操作でEQUALITY_DELETESファイルが大量生成されて、徐々にスキャン速度が落ちていく問題があった。5.0のIceberg v1.6では位置ベース削除が改善されていて、同じ削除操作でもファイル数が30〜40%減った感覚がある。
定期的なIcebergコンパクションジョブも実装している。
# Icebergコンパクションジョブ(週次実行)
spark.sql("""
CALL glue_catalog.system.rewrite_data_files(
table => 'analytics.user_events',
strategy => 'sort',
sort_order => 'user_id ASC, event_timestamp ASC',
options => map(
'target-file-size-bytes', '134217728',
'min-file-size-bytes', '67108864',
'max-concurrent-file-group-rewrites', '10'
)
)
""")
# 削除ファイルのクリーンアップ
spark.sql("""
CALL glue_catalog.system.rewrite_position_delete_files(
table => 'analytics.user_events',
options => map(
'target-file-size-bytes', '67108864'
)
)
""")
これを週次で回すようにしたら、日次クエリの平均実行時間が18%改善した。正直まだ最適なコンパクション頻度は検証中だけど、今のところこれで落ち着いている。
まとめ
6ヶ月運用してみた結論をまとめると、以下のとおりだ。
| ポイント | 結論 |
|---|---|
| コスト削減 | 移行 + Auto Scaling + FLEX実行の組み合わせで月コスト50%削減。ただし「移行するだけで安くなる」わけではなく、ジョブの見直しが必要 |
| Spark 4.0のAQE | 強力だが最初は挙動を確認すること。スキューが多いデータはパーティション数が予想外に変わる場合がある |
| ジョブブックマーク | Iceberg環境では要注意。独自チェックポイント管理に切り替えが安心。重複データが許容できないパイプラインでは必須 |
| Iceberg v1.6 + コンパクション | 週次コンパクションを実装することで読み込み速度の劣化を抑えられた |
| FLEX実行 | SLAのないバッチに限定すること。月末など混雑期は起動待機が20分を超えることがある |
次のアクションとして、まずJob Run InsightsのAI推奨を既存ジョブ全体に対して確認してほしい。うちのチームでは最初の棚卸しだけで月5万円分の無駄が見つかった。移行を検討しているなら、まず1〜2本の低リスクジョブで5.0を試して、FLEX実行とAuto Scalingの効果を計測するところから始めると失敗が少ない。
皆さんのチームはGlue 4.0からの移行をどうやって進めていますか?特にデータスキューの扱いやIcebergとの組み合わせで困った経験があれば教えてほしい。