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.0Glue 5.0
Sparkバージョン3.34.0
Pythonバージョン3.103.12
DPUあたりvCPU44(変更なし)
FLEX実行対応対応(割引率改善)
Icebergネイティブ対応v1.2v1.6
Delta Lake対応v2.4v3.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との組み合わせで困った経験があれば教えてほしい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事