Upgrade to Pro — share decks privately, control downloads, hide ads and more …

20250829_LambdaとStepFunctionsどちらを選ぶべき_コスト視点て...

Avatar for Hiroshi Kato Hiroshi Kato
August 29, 2025
320

 20250829_LambdaとStepFunctionsどちらを選ぶべき_コスト視点で考えてみる.pdf

Avatar for Hiroshi Kato

Hiroshi Kato

August 29, 2025
Tweet

Transcript

  1. Step Functions とは? サーバレスなワークフローオーケストレーション • ステートを定義して、ワークフローを構築 • Workflow Studio(GUI)を使用し、ワーク フローをドラッグ&ドロップで構築可能

    • ASL(JSON)を使用してワークフローを 記述することも可能 ASL:Amazon States Language • 分岐、並列、リトライ、タイムアウト、 エラーハンドリングなど、豊富な制御機能 • 200+ のAWSサービスと統合 Workflow Studio 特徴 ドラッグ&ドロップで構築 ASL記述 Step Functions DynamoDB S3 SNS SQS EventBridge Athena Bedrock Lambda 200+ のAWSサービス
  2. Step Functions の主なステートと制御機能 主な制御機能 主なステート ステート種別 説明 Task Lambdaなどの呼び出し Choice

    条件分岐(if/else 相当) Parallel 複数処理の同時並行実行 Map 配列要素の処理繰り返し Wait 一定時間/指定時刻の待機 Pass 入力データをそのまま出力、 または定数値を出力 Succeed/Fail ワークフローの成功/失敗終了 フィールド名 説明 Retry ステート実行失敗時の自動再試行 設定 Catch ステート失敗時の代替フロー指定 TimeoutSeconds ステートの最大実行時間制限 ASL (Amazon States Language) を用いて記述
  3. Step Functions をどこから呼び出す? 主な呼び出し元 主な用途・特徴 Lambda 関数 StartExecution APIを使ってどこからでも呼び出し可能 EventBridge

    イベントトリガーで自動実行(S3アップロード・スケジュール・ログ検出など) API Gateway REST API のバックエンドとして使える外部システムから直接呼び出し可能 AWS SDK / CLI Boto3 や SDK 経由で実行。バッチやWebアプリからも活用しやすい Lambda EventBridge API Gateway AWS Tools and SDKs Step Functions AWS 内のさまざまなサービスから呼び出し、連携することができる DynamoDB S3 SNS SQS EventBridge Athena Bedrock Lambda 200+ のAWSサービス
  4. データアクセスの進化 JSONataの登場で、劇的に実装が簡潔に! • 合計4ステート必要 • JSONPathは「読み取るだけ」で演算不可 • Step Functions用ロールが必要 さらに

    Lambda用ロールが必要 • 合計2ステートで完結 • Step Functions用ロールのみ 入力JSONから firstName と lastName を取得 それらを結合して フルネーム(姓名) を作成 名前に “加藤" が 含まれていたら "VIP" 判定 JSONPath (従来) JSONata(+ Assign) データ取得 データ結合 データ整形 条件分岐 データ取得 ・結合・整形 条件分岐 例
  5. データアクセスの進化 JSONataの利用は、多くのメリットを生み出す! 項目 JSONPath(従来) JSONata(+ Assign) データ取得方法 パス指定(InputPath 等) JSONata

    式(Arguments, Output) データ加工 Lambda or Pass JSONata 式1行で完結 条件分岐 Lambda or Choice Choice + JSONata 式 ステート数 多くなりがち(Pass・Lambda併用) 少ない(整形・演算を内部処理) ロール構成 StepFunctions用 + Lambda用ロール StepFunctions用ロールのみ コスト Lambda実行回数 × ステート数 ステート数削減でコスト最適 実装・保守性 煩雑・ステートが多くて見づらくなる シンプル・視認性がよい テストのしやすさ Lambda側でロジック分離の必要あり ASL内で完結、TestStateで確認可能 Lambdaを利用することで発生していた、実装規模、複雑さ、コストを大幅にカット可能
  6. JSONata(+ Assign)の真価 • QueryLanguage: “JSONata” を指定するだけ • 各ステートは Input を受け取り、Output

    を返す $states.input → ステートの入力全体 $states.output → ステートの出力 • Assign ステートで変数定義 → $変数名 で他ステートから参照可能 • 値の整形・演算・条件分岐・文字列操作・日付処理など、式で完結 • 構文: {% <JSONata式> %} • 演算:+, &, ==, $sum(), $contains() など • 文字列:{% $states.input.user.lastName & ' さん' %} • 条件式:{% $contains($fullName, ‘加藤') %} • グローバル変数: Assign ステート内の変数は、後続のすべての ステートから参照可能 • ローカル変数:Map / Parallel内で定義 → ブロック内のみ有効 { "QueryLanguage": "JSONata", "StartAt": "SetFullName", "States": { "SetFullName": { "Type": "Pass", "Assign": { "fullName": "{% $states.input.user.lastName & ' ' & $states.input.user.firstName %}" }, "Next": "CheckName" }, "CheckName": { "Type": "Choice", "Choices": [ { "Condition": "{% $contains($fullName, '加藤') %}", "Next": "VIP" } ], "Default": "Normal" }, "VIP": { "Type": "Succeed" }, "Normal": { "Type": "Succeed" } } } 基本仕様 JSONata式の書き方 スコープ仕様 JSONata(+Assign)はとても簡単!
  7. ワークフロータイプとコスト 2つのワークフロータイプの適切な利用がポイント ワークフロータイプ Standard Workflows Express Workflows 実行モデル Exactly-once(重複なし・冪等でない操作) At-least-once(重複の可能性あり・冪等な操作)

    保存期間 最大 1年(永続化) 実行後は非永続、ログのみ 実行時間上限 1年 5分 適用シーン 長期処理、業務ワークフロー 高頻度イベント、リアルタイム処理 料金体系 状態遷移数:$0.000025/遷移 無料枠:月間4,000状態遷移まで無料 リクエスト数:$0.000001/リクエスト 実行時間:$0.00001667 per GB-Second 料金目安 100万実行:25$ 100万実行:1$+メモリ量×実行時間
  8. DynamoDB S3 簡単なデータ取込処理でシミュレーション ? • S3にデータファイルを格納 • ファイル格納をトリガーにして、DynamoDBに格納 • データは一意、更新はなし、データ数は1000件

    [ { "userId": "user-10001", "name": "Taro Yamada", "score": 83, "timestamp": "2025-08-25T10:00:00Z" }, { "userId": "user-10002", "name": "Hanako Suzuki", "score": 91, "timestamp": "2025-08-25T10:01:00Z" } ・・・・・・・ ] 1000件
  9. import json import boto3 import logging frombotocore.exceptions import ClientError import

    itertools logger = logging.getLogger() logger.setLevel(logging.INFO) s3 = boto3.client("s3") dynamodb = boto3.client("dynamodb") TABLE_NAME = "h-test-stepfunctions-table" def lambda_handler(event, context): try: # --- 1. S3イベントからバケット名とキー取得 --- record = event["Records"][0] bucket = record["s3"]["bucket"]["name"] key = record["s3"]["object"]["key"] logger.info(f"Triggered by file: s3://{bucket}/{key}") # --- 2. ファイル取得 --- response = s3.get_object(Bucket=bucket, Key=key) body = response["Body"].read().decode("utf-8") records = json.loads(body) if not isinstance(records, list): raise ValueError("JSON file must contain an array of records") logger.info(f"Parsed {len(records)} records") success_count = 0 failure_count = 0 # --- 3. 100件ずつに分割してトランザクション書き込み --- for batch in chunked(records, 100): transact_items = [] for rec in batch: if "userId" not in rec or "score" not in rec: logger.warning(f"Skipping invalid record: {rec}") failure_count += 1 continue i# 同一トランザクション内に同じUserIdが含まれないように注意 if any(item["Put"]["Item"]["UserId"]["S"] == str(rec["userId"]) for item in transact_items): logger.warning(f"Duplicate userId in same transaction, skipping: {rec['userId']}") failure_count += 1 continue transact_items.append({ "Put": { "TableName": TABLE_NAME, "Item": { "UserId": {"S": str(rec["userId"])}, "Score": {"N": str(rec["score"])}, "Name": {"S": rec.get("name", "N/A")}, "Timestamp": {"S": rec.get("timestamp", "N/A")} }}}) if not transact_items: continue try: dynamodb.transact_write_items(TransactItems=transact_items) success_count += len(transact_items) except ClientError as e: logger.error(f"Transaction failed: {e}") failure_count += len(transact_items) logger.info(f"Inserted: {success_count}, Failed: {failure_count}") return { "statusCode": 200, "body": json.dumps({"inserted": success_count, "failed": failure_count}) } except Exception as e: logger.exception(f"Unhandled error: {e}") return { "statusCode": 500, "body": json.dumps({"error": str(e)}) } def chunked(iterable, n): """リストをn件ごとのサブリストに分割""" it = iter(iterable) while True: batch = list(itertools.islice(it, n)) if not batch: break yield batch DynamoDB S3 Lambdaで実装 • S3にデータファイルを格納 • ファイル格納をトリガーにして、DynamoDBに格納 • データは一意、更新はなし Lambda イベント通知 TransactWriteItems 1.S3イベント通知でLambda起動 2.LambdaがS3からデータ取得 3.データを100件づつに分割 4.TransactWriteItemsで100件づつ書き込み 94Line
  10. DynamoDB S3 Step Functionsで実装 • S3にデータファイルを格納 • ファイル格納をトリガーにして、DynamoDBに格納 • データは一意、更新はなし

    Step Functions イベント通知 TransactWriteItems 1.S3イベント通知でEventBridge経由し Step Functionsを起動 2.Step FunctionsがS3からデータ取得 3.データを100件づつに分割 4.TransactWriteItemsで100件づつ書き込み EventBridge { "QueryLanguage": "JSONata", "StartAt": "GetObject", "States": { "GetObject": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:s3:getObject", "Arguments": { "Bucket": "h-test-stepfunctions-bucket", "Key": "{% $states.input.detail.object.key %}" }, "Next": "ProcessLargeData" }, "ProcessLargeData": { "Type": "Pass", "Assign": { "records": "{% $parse($states.input.Body) %}", "totalRecords": "{% $count($parse($states.input.Body)) %}", "batches": "{% $partition($parse($states.input.Body), 100) %}" }, "Next": "ProcessBatches" }, "ProcessBatches": { "Type": "Map", "Items": "{% $batches %}", "MaxConcurrency": 5, "ItemProcessor": { "StartAt": "WriteBatch", "States": { "WriteBatch": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:dynamodb:transactWriteItems", "Arguments": { "TransactItems": "{% $map($states.input, function($item) { { 'Put': { 'TableName': 'h-test- stepfunctions-table', 'Item': { 'UserId': {'S': $item.userId}, 'Score': {'N': $string($item.score)}, 'Name': {'S': $item.name}, 'Timestamp': {'S': $item.timestamp}, 'ProcessedAt': {'S': $now()} } } } }) %}" }, "End": true } } }, "End": true } } } 43Line
  11. コスト算出方法 実測条件 • 実行時間・・・2,112ms = 2.112S • メモリ・・・128MB = 0.128GB

    単価 • GB-秒単価・・・$0.0000166667 / GB-秒 • リクエスト単価・・・$0.20 / 100万回 = $0.0000002 / 回 計算 1. 使用GB-秒 = 0.128GB × 2.112秒 = 0.270336GB-秒 2. 実行料金 = 0.270336 × 0.00001667 = $0.00000451 3. リクエスト料金 = $0.0000002 4. 合計 = $0.00000471 / 回 Lambda
  12. コスト算出方法 実測条件 • 実行時間・・・0.875S 単価 • GB-秒単価・・・$0.00001667 / GB-秒 •

    リクエスト単価・・・$0.000001 / 回 計算 1. 実行料金 = 0.875秒 × 0.0625GB(64MB) × $0.00001667 = $0.0000009121 1. リクエスト料金 = $0.000001 2. 合計 = $0.00000191 / 回 Step Functions Express Workflows
  13. コスト算出方法 実測条件 • 遷移数・・・13状態遷移 単価 • 遷移単価・・・$0.000025 計算 1. 料金

    = 13遷移 × $0.000025 = $0.000325 2. 合計 = $0.000325 / 回 Step Functions Standard Workflows
  14. コストランキング 実行規模 1位 2位 3位 月間1,000回 Express ($0.002) Lambda ($0.0047)

    Standard ($0.325) 月間10万回 Express ($0.2) Lambda ($0.47) Standard ($32.5) 月間100万回 Express ($2) Lambda ($4.7) Standard ($325) サービス 1回あたり料金 計算式 Lambda $0.0000047 リクエスト: $0.0000002 実行時間: $0.0000044 Express $0.000002 リクエスト: $0.000001 実行時間: 約$0.000001 Standard $0.000325 13遷移 × $0.000025
  15. コストランキング(無料枠を考慮) サービス 1回あたり料金 計算式 無料枠 Lambda $0.0000047 リクエスト: $0.0000002 実行時間:

    $0.0000044 100万リクエスト 40万GB-seconds Express $0.000002 リクエスト: $0.000001 実行時間: 約$0.000001 なし Standard $0.000325 13遷移 × $0.000025 4,000状態遷移 実行規模 1位 2位 3位 月間1,000回 Lambda / Standard Express ($0.002) 月間10万回 Lambda ($0) Express ($0.2) Standard ($32.5) 月間100万回 Lambda ($0) Express ($2) Standard ($325) 月間175万回を超えると、Expressの方が安くなる
  16. まとめ • APIGateway+StepFunctionsのワークロードの構築 • JSONataの構文、関数の深掘 • コスト最適化、ワークフロータイプのネスト検証 • ステート設計のベストプラクティス、アンチパターン •

    ローカルIDE統合 • サービスクォータ • ログ記録、モニタリング • テスト、デバッグ • バージョニング、デプロイ管理 • 単純にランニングコストを見ると、Lambdaの無料枠は強い ただし、メモリを多く使う重い処理はその限りではない • Step Functionsは、実装がシンプル、フローの可視性が強い • 稼働後の運用コスト(フローの改修・改善など)まで見据えて、 実装方法は選択しましょう さらに学びたいこと