0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

軽いETL処理ならGlueよりもLambdaの方がコスパ良かった話

0
Posted at

はじめに

「 ETL処理 = Glue 」というイメージですよね。

AWSにおけるETL処理について調べるとGlueに関する情報がたくさん出てくるので、それだけ現場での採用率が高いのだとうかがえます。
一方、「Glueを使う必要ある?Lambdaのほうがよくない?」というように、場合によってはLambdaを推したくなることもあると思います。

本記事では、Glue (Python Shell) とLambdaを実際に動かしながら、それぞれの挙動を観察し、ETL処理における両者の違いについて感じたことを書き起こします。
なお、本記事は2026年3月時点の情報を基に記載しています。

データの準備

「動作検証をするためにいい感じの公開データを使いたい!」
ということで、データサイエンスの入門でよく扱われているタイタニック問題のトレーニングデータを使います。
以下リンクからKaggleにアクセスし、CSVファイルをダウンロードできます。
※ダウンロードにはKaggleの会員登録が必要になります。

こちらのデータですが、ファイルサイズが60KB程度しかありません。
せっかくなので、今回はもう少し重たいデータも使ってみます。

数百MBくらいのデータを探しましたがこれというのが見つからないので、タイタニック問題のトレーニングデータを加工し、拡張版トレーニングデータを作ります。
加工方法は後述します。

以降、元のトレーニングデータ/拡張版トレーニングデータの2つを使って動作検証をします。
また、元のトレーニングデータを扱う処理は「軽量ETL」、
拡張版トレーニングデータを扱う処理は「重量ETL」と表現します。

検証内容

概要

構成イメージは以下の通りです。

image.png

Extract
  S3バケットに格納されたCSVファイルを抽出
Transform
  GlueまたはLambdaでデータを変換
Load
  変換後のCSVファイルをS3バケットにロード

Glue・Lambdaでは、性別フラグが"男性"のレコードを抽出する処理を行います。

環境準備

本記事に掲載するソースコードは検証・学習用のサンプルです。
内容の正確性や動作は保証していません。

箇条書きになりますが、以下の通りリソースを構築します。

  • GlueはPython3.9、LambdaはPython3.14を選択
  • Glue Data connectionsを作成し、GlueをVPC内で利用できるように設定
  • セキュアな環境にも応用できるようにするため、Glue・LambdaはVPC内に作成
  • VPC内からS3バケットにアクセスするため、S3のゲートウェイ型VPCエンドポイントを作成
  • エラートレースのため、Glue・LambdaのログをCloudWatchLogsに格納
  • Glue・LambdaそれぞれのIAMロールを作成し、VPCアクセス・ログ格納・S3アクセスに必要なアクセス権限を設定
  • Lambdaの中でpandasを利用するため、AWSレイヤー「AWSSDKPandas-Python314」を設定

トレーニングデータは以下のファイル名でS3バケットにアップロードします。

  • 元のトレーニングデータ :titanic_train_light.csv
  • 拡張版トレーニングデータ:titanic_train_heavy.csv

image.png

少し脱線します。
これまで、Lambdaの中でpandasを利用する場合はLambdaレイヤーを自作する必要があると認識していました。
しかし、現在はAWS提供のLambdaレイヤーを利用できるようになっており、自作は不要となっていました。(だいぶ助かりました!)
詳細は以下の記事をご参照ください。

【軽量ETL】 元のトレーニングデータで検証

はじめに元のトレーニングデータ (titanic_train_light.csv) を利用し、GlueおよびLambdaでETL処理を実行してみます。
ここでは、1分以内に処理が完了するETL処理を前提とします。

Glue (Python Shell) でやってみる

まずはGlueです。
ソースコードは以下の通りです。S3バケット名はマスクしています。

Glueのソースコード.py
import boto3
import pandas as pd
from io import StringIO

# S3クライアントの初期化
s3_client = boto3.client('s3')


# S3のバケット名とキーを設定
input_bucket = '<S3バケット名>'
input_key = 'input/titanic_train_light.csv'
output_bucket = '<S3バケット名>'
output_key = 'output/titanic_train_light.csv'

try:
    # S3からインプットデータを取得
    print(f"Reading data from s3://{input_bucket}/{input_key}")
    response = s3_client.get_object(Bucket=input_bucket, Key=input_key)
    csv_content = response['Body'].read().decode('utf-8')
    
    # pandasでCSVを読み込む
    df = pd.read_csv(StringIO(csv_content))
    print(f"Loaded {len(df)} rows from input file")
    print(f"Columns: {list(df.columns)}")
    
    # 性別が男性の行を抽出
    df_sex = df[df['Sex'] == 'male']
    print(f"Filtered to {len(df_sex)} rows where Sex=male")
    
    # データフレームをCSV文字列に変換
    csv_buffer = StringIO()
    df_sex.to_csv(csv_buffer, index=False)
    csv_string = csv_buffer.getvalue()
    
    # S3にアウトプットデータを保存
    print(f"Writing data to s3://{output_bucket}/{output_key}")
    s3_client.put_object(
        Bucket=output_bucket,
        Key=output_key,
        Body=csv_string,
        ContentType='text/csv'
    )
    
    print("ETL処理が正常に完了しました")
    print(f"Input rows: {len(df)}")
    print(f"Output rows: {len(df_sex)}")
    print(f"Output location: s3://{output_bucket}/{output_key}")
    
except Exception as e:
    print(f"Error occurred: {str(e)}")
    raise e

Glue Jobを実行してみました。
無事に処理が完了しました。

image.png

ここで、Glueにおける合計処理時間は End time (Local) と Start time (Local) の差分とします。
そうすると、上記の結果より合計処理時間は 27秒 と読み取れます。

また脱線しますが、Data processing unitsは 1/16 DPU (Max capacity = 0.0625 DPUs) に設定していました。
1 DPU (Max capacity = 1 DPUs) にすると処理時間がどれくらい早くなるのか?
気になったので試してみました。結果は以下の通りです。

image.png

Start-up time と Execution time ともに短縮され、合計処理時間が 17秒 になりました。
参考値ではありますが、こんなに早くなると思ってなかったのでびっくりしました...

それと同時に、Execution time よりも Start-up time のほうが短縮されているという結果に違和感を覚えました。
DPUの差分なので Execution time が短縮されることは予想できましたが、 Start-up time も短縮(というか半減)したのは想定外でした。

明確な理由は分かりませんが、DPU が大きい場合にはリソースを確保しやすいことや、初期化処理が早く完了することなどが影響しているのかもしれません。

Lambdaでやってみる

次はLambdaです。
ソースコードは以下の通りです。S3バケット名はマスクしています。

Lambdaのソースコード.py
from io import StringIO
import boto3
import pandas as pd

# S3クライアントの初期化
s3_client = boto3.client('s3')


def lambda_handler(event, context):
    """AWS Lambda関数のエントリーポイント"""
    
    # S3のバケット名とキーを設定
    input_bucket = event.get('input_bucket', '<S3バケット名>')
    input_key = event.get('input_key', 'input/titanic_train_light.csv')
    output_bucket = event.get('output_bucket', '<S3バケット名>')
    output_key = event.get('output_key', 'output/titanic_train_light.csv')
    
    try:
        # S3からインプットデータを取得
        print(f"S3からデータ読み込み中: s3://{input_bucket}/{input_key}")
        response = s3_client.get_object(Bucket=input_bucket, Key=input_key)
        csv_content = response['Body'].read().decode('utf-8')
        
        # pandasでCSVを読み込む
        df = pd.read_csv(StringIO(csv_content))
        print(f"入力データ読み込み完了: {len(df)}")
        print(f"カラム: {list(df.columns)}")
        
        # 性別が男性の行を抽出
        df_sex = df[df['Sex'] == "male"]
        print(f"フィルタリング完了: {len(df_sex)}行 (性別=男性)")
        
        # データフレームをCSV文字列に変換
        csv_buffer = StringIO()
        df_sex.to_csv(csv_buffer, index=False)
        csv_string = csv_buffer.getvalue()
        
        # S3にアウトプットデータを保存
        print(f"S3にデータ保存中: s3://{output_bucket}/{output_key}")
        s3_client.put_object(
            Bucket=output_bucket,
            Key=output_key,
            Body=csv_string,
            ContentType='text/csv'
        )
        
        return {
            'statusCode': 200,
            'body': {
                'message': 'ETL処理が正常に完了しました',
                'input_rows': len(df),
                'output_rows': len(df_sex),
                'output_location': f's3://{output_bucket}/{output_key}'
            }
        }
        
    except Exception as e:
        error_msg = f"エラーが発生しました: {str(e)}"
        print(error_msg)
        return {
            'statusCode': 500,
            'body': {'error': error_msg}
        }

Lambdaを実行してみました。
無事に処理が完了しました。

image.png

ここで、Lambdaにおける合計処理時間は課金期間とします。
上記の結果より、合計処理時間は 5秒 と読み取れます。
(Lambdaはミリ秒単位で課金されるため正確に4.862秒として扱うべきですが、面倒なので丸めてます...)

GlueとLambdaを比較してみる

これまでの結果を踏まえ、GlueとLambdaを比較してみます。
比較観点はパフォーマンス・コストの2点です。

パフォーマンス

検証結果より、それぞれの処理時間は以下のとおりです。

Glue (PythonShell) Lambda
17秒 5秒

軽量ETLの検証では、「Lambdaの方が処理時間が短い」という結論となりました。

また、Lambdaは起動時間だけでなく、処理時間についてもGlueより短くなる結果となりました。
CPUやメモリといったスペックはGlueの方が高性能であるはずですが、なぜこのような結果になったのでしょうか?

明確な要因は特定できていませんが、VPC内で処理を実行している点が一因かもしれないと考えています。
検証後、LambdaのENIは残ったままでしたが、GlueのENIは削除されていました。
Glueはジョブの実行完了後にすぐENIを解放する挙動を取っているようなので、ジョブを実行するたびに新たなENIを用意する必要がありそうでした。

このことから、VPC内からAWSサービスへアクセスするための準備(ネットワーク確立など)は、LambdaよりもGlueの方が負荷が大きいのかもしれないと考えています。

コスト

GlueとLambdaのコスト算出方法が結構複雑なので、ここでは以下の前提を設けます。

  • 月200万リクエスト想定
  • 東京リージョンを利用
  • Lamdbaのランタイムはx86、メモリは128MB、エフェメラルストレージは512MB
  • Glueは 1/16 DPU

上記の前提より、以下の計算式でコストを見積ります。

  • Glue (PythonShell):27秒 × 200万リクエスト × 0.0625 DPUs x 0.44 = 413 USD
  • Lambda:20.83 USD + 0.4 USD = 21.23 USD
    • コンピュート:5秒 × 200万リクエスト × 0.125GB × 0.0000166667 = 20.83 USD
    • リクエスト:200万リクエスト × 0.0000002 = 0.4 USD
    • ストレージ:0 (無料枠)

算出したコストは以下の通りです。

Glue (PythonShell) Lambda
413 USD 21.23 USD

今回の試算では「GlueよりLambdaの方が安い」という結論になりました。
(GlueのコストはLambdaの約20倍になっています)

仮にLambdaの処理時間がGlueと同等の30秒だとしても、約130 USD程度となり、Glueよりも圧倒的に安上がりになります。

GlueとLambdaそれぞれのコストについて、詳細は以下の公式ドキュメントをご参照ください。

一言まとめ

軽量ETLであれば、GlueよりもLambdaの方がコスパが良さそう

【重量ETL】 拡張版トレーニングデータで検証

次は拡張版トレーニングデータ (titanic_train_heavy.csv) を利用し、GlueおよびLambdaでETL処理を実行してみます。
ここでは、10分以上かかるETL処理を前提とします。

ソースコードは使いまわしたかったんですが、Glue・LambdaともにOOMによるメモリ不足が起きたので、CSVファイルをチャンクに分割して処理するように改修します。
なお、検証では可能な限り処理時間を長く引っ張りたかったので、チャンクサイズは "1"、つまりレコードを1行ずつ処理するようにしました。

データの加工

元のトレーニングデータに対し、以下の加工を施して拡張版トレーニングデータを作りました。

  • レコードを 400,000 行に拡張
  • 英語テキストを格納する列を追加

これにより、元は 60 KB しかなかったデータが、215.7 MB になりました。

Glue (Python Shell) でやってみる

まずはGlueです。
ソースコードは以下の通りです。S3バケット名はマスクしています。

Glueのソースコード.py
import boto3
import pandas as pd
import tempfile
import os

# S3クライアントの初期化
s3_client = boto3.client('s3')

# S3のバケット名とキーを設定
input_bucket = '<S3バケット名>'
input_key = 'input/titanic_train_heavy.csv'
output_bucket = '<S3バケット名>'
output_key = 'output/titanic_train_heavy.csv'

# チャンクサイズ(1チャンクあたりのレコード数)
CHUNK_SIZE = 1

tmp_path = None

try:
    # S3からインプットデータをストリームで取得
    print(f"Reading data from s3://{input_bucket}/{input_key}")
    response = s3_client.get_object(Bucket=input_bucket, Key=input_key)

    total_input_rows = 0
    total_output_rows = 0
    header_written = False

    # 一時ファイルに男性レコードを書き出す
    with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp:
        tmp_path = tmp.name

        # CSVをチャンク読み込み
        for chunk in pd.read_csv(response['Body'], chunksize=CHUNK_SIZE):
            total_input_rows += len(chunk)

            # 男性レコードのみ抽出
            male_chunk = chunk[chunk['Sex'] == 'male']
            total_output_rows += len(male_chunk)

            if male_chunk.empty:
                continue

            # 一時ファイルに追記(最初のチャンクのみヘッダーを付与)
            male_chunk.to_csv(tmp, index=False, header=not header_written)
            header_written = True

    # 一時ファイルをS3にアップロード
    print(f"Writing data to s3://{output_bucket}/{output_key}")
    s3_client.upload_file(tmp_path, output_bucket, output_key)

    print("ETL処理が正常に完了しました")
    print(f"Input rows:  {total_input_rows}")
    print(f"Output rows: {total_output_rows}")
    print(f"Output location: s3://{output_bucket}/{output_key}")

except Exception as e:
    print(f"Error occurred: {str(e)}")
    raise e

finally:
    # 一時ファイルを削除
    if tmp_path and os.path.exists(tmp_path):
        os.remove(tmp_path)

Glue Jobを実行してみました。
無事に処理が完了しました。

image.png

合計処理時間は End time (Local) と Start time (Local) の差分とします。
よって、合計処理時間は 14分54秒 と読み取れます。

ここで軽量ETLの話に戻ります。
軽量ETLの検証結果では、GlueよりもLambdaの方が処理時間は短くなりました。
なので、今回の検証においても「GlueよりLambdaの方が処理時間が短い」と仮定します。

また、Lambdaには最大実行時間が15分という制約が存在します。

これらのことから、Glueの処理時間をあえて15分前後に設定することで、Lambdaにおける最大実行時間のギリギリを攻めることができるのでは?と思いました。
(実際そうはならないのですが...)

なお、重量ETLでは 1 DPU (Max capacity = 1 DPUs) の検証は実施しません。
コスト的な問題です。ご了承ください!

Lambdaでやってみる

次はLambdaです。
ソースコードは以下の通りです。S3バケット名はマスクしています。

Lambdaのソースコード.py
import boto3
import pandas as pd
import tempfile
import os

# S3クライアントの初期化
s3_client = boto3.client('s3')

# チャンクサイズ(1チャンクあたりのレコード数)
CHUNK_SIZE = 1


def lambda_handler(event, context):
    """AWS Lambda関数のエントリーポイント"""
    
    # S3のバケット名とキーを設定
    input_bucket = event.get('input_bucket', '<S3バケット名>')
    input_key = event.get('input_key', 'input/titanic_train_heavy.csv')
    output_bucket = event.get('output_bucket', '<S3バケット名>')
    output_key = event.get('output_key', 'output/titanic_train_heavy.csv')
    tmp_path = None
    try:
        # S3からインプットデータをストリームで取得
        print(f"S3からデータ読み込み中: s3://{input_bucket}/{input_key}")
        response = s3_client.get_object(Bucket=input_bucket, Key=input_key)
        total_input_rows = 0
        total_output_rows = 0
        header_written = False

        # 一時ファイルに男性レコードを書き出す(Lambda の /tmp に作成)
        with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, dir='/tmp') as tmp:
            tmp_path = tmp.name

            # CSVをチャンク読み込み
            for chunk in pd.read_csv(response['Body'], chunksize=CHUNK_SIZE):
                total_input_rows += len(chunk)

                # 男性レコードのみ抽出
                male_chunk = chunk[chunk['Sex'] == 'male']
                total_output_rows += len(male_chunk)
                if male_chunk.empty:
                    continue

                # 一時ファイルに追記(最初のチャンクのみヘッダーを付与)
                male_chunk.to_csv(tmp, index=False, header=not header_written)
                header_written = True

        # 一時ファイルをS3にアップロード
        print(f"S3にデータ保存中: s3://{output_bucket}/{output_key}")
        s3_client.upload_file(tmp_path, output_bucket, output_key)
        print("ETL処理が正常に完了しました")
        print(f"Input rows: {total_input_rows}")
        print(f"Output rows: {total_output_rows}")
        return {
            'statusCode': 200,
            'body': {
                'message': 'ETL処理が正常に完了しました',
                'input_rows': total_input_rows,
                'output_rows': total_output_rows,
                'output_location': f's3://{output_bucket}/{output_key}'
            }
        }

    except Exception as e:
        error_msg = f"エラーが発生しました: {str(e)}"
        print(error_msg)
        return {
            'statusCode': 500,
            'body': {'error': error_msg}
        }
    finally:
        # 一時ファイルを削除
        if tmp_path and os.path.exists(tmp_path):
            os.remove(tmp_path)

Lambdaを実行してみました。
なんと、処理が15分以上かかってしまったのでタイムアウトになりました!

image.png

この結果から、それなりに大きなデータを対象としてETL処理を行う場合、Lambdaでは実行できない処理があると分かりました。

ちなみに、100,000 行に減らしてもタイムアウトになりました。
10,000 行まで減らしてみると、5分 くらいで処理が完了しました。

GlueとLambdaを比較してみる

Lambdaの処理に限界があることが分かったので、拡張版トレーニングデータ(215.7 MB)を使った重量ETL処理における検証結果だけ以下のとおりまとめます。

Glue (PythonShell) Lambda
14分54秒 タイムアウトエラー

一言まとめ

重量ETLの場合、Lambdaでは実行できない処理であっても、Glueであれば大体できそう

総括

まずは箇条書きでまとめます。

  • 軽めのETL処理であれば、GlueよりもLambdaの方が処理が早くコストも安いので、Lambdaを使った方が良さそう
  • 重めのETL処理であれば、Lambdaはタイムアウトになりがちなので、基本はGlueを使わなければいけなさそう

Lambdaの最大処理時間が15分であるため、それを超える場合はGlueを使うことになります。
ただし、今回の検証結果より、処理が15分以内で完了するような軽量なETL処理であれば、GlueよりもLambdaの方がコストパフォーマンスに優れるケースが出てきます。
(おそらく、処理が軽ければ軽いほどLambdaの方が良さそうです。)

おわりに

ご一読いただき、誠にありがとうございました!
少しでも役に立つ情報を共有できていれば幸いです。

Glueについては他にも書きたい内容がいくつかあるので、改めて時間を取って記事を書きたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?