Slide 1

Slide 1 text

Lambda と Step Functions どちらを選ぶべき? コスト視点で考えてみる JAWS-UG名古屋 2025年8月29日(金)

Slide 2

Slide 2 text

自己紹介 加藤 寛士 (かとう ひろし) NECソリューションイノベータ株式会社 • JAWS-UG 名古屋 の運営をしています! • JAWS Festa 2025 参戦予定! (実行委員会にも入ってます)

Slide 3

Slide 3 text

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サービス

Slide 4

Slide 4 text

Step Functions の主なステートと制御機能 主な制御機能 主なステート ステート種別 説明 Task Lambdaなどの呼び出し Choice 条件分岐(if/else 相当) Parallel 複数処理の同時並行実行 Map 配列要素の処理繰り返し Wait 一定時間/指定時刻の待機 Pass 入力データをそのまま出力、 または定数値を出力 Succeed/Fail ワークフローの成功/失敗終了 フィールド名 説明 Retry ステート実行失敗時の自動再試行 設定 Catch ステート失敗時の代替フロー指定 TimeoutSeconds ステートの最大実行時間制限 ASL (Amazon States Language) を用いて記述

Slide 5

Slide 5 text

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サービス

Slide 6

Slide 6 text

データアクセスの進化 JSONataの登場で、劇的に実装が簡潔に! • 合計4ステート必要 • JSONPathは「読み取るだけ」で演算不可 • Step Functions用ロールが必要 さらに Lambda用ロールが必要 • 合計2ステートで完結 • Step Functions用ロールのみ 入力JSONから firstName と lastName を取得 それらを結合して フルネーム(姓名) を作成 名前に “加藤" が 含まれていたら "VIP" 判定 JSONPath (従来) JSONata(+ Assign) データ取得 データ結合 データ整形 条件分岐 データ取得 ・結合・整形 条件分岐 例

Slide 7

Slide 7 text

データアクセスの進化 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を利用することで発生していた、実装規模、複雑さ、コストを大幅にカット可能

Slide 8

Slide 8 text

JSONata(+ Assign)の真価 • QueryLanguage: “JSONata” を指定するだけ • 各ステートは Input を受け取り、Output を返す $states.input → ステートの入力全体 $states.output → ステートの出力 • Assign ステートで変数定義 → $変数名 で他ステートから参照可能 • 値の整形・演算・条件分岐・文字列操作・日付処理など、式で完結 • 構文: {% %} • 演算:+, &, ==, $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)はとても簡単!

Slide 9

Slide 9 text

ワークフロータイプとコスト 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$+メモリ量×実行時間

Slide 10

Slide 10 text

Step Functionsを使ったほうが コストメリットでるの?

Slide 11

Slide 11 text

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件

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

コスト算出方法 実測条件 • 実行時間・・・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

Slide 15

Slide 15 text

コスト算出方法 実測条件 • 実行時間・・・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

Slide 16

Slide 16 text

コスト算出方法 実測条件 • 遷移数・・・13状態遷移 単価 • 遷移単価・・・$0.000025 計算 1. 料金 = 13遷移 × $0.000025 = $0.000325 2. 合計 = $0.000325 / 回 Step Functions Standard Workflows

Slide 17

Slide 17 text

コストランキング 実行規模 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

Slide 18

Slide 18 text

コストランキング(無料枠を考慮) サービス 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の方が安くなる

Slide 19

Slide 19 text

まとめ • APIGateway+StepFunctionsのワークロードの構築 • JSONataの構文、関数の深掘 • コスト最適化、ワークフロータイプのネスト検証 • ステート設計のベストプラクティス、アンチパターン • ローカルIDE統合 • サービスクォータ • ログ記録、モニタリング • テスト、デバッグ • バージョニング、デプロイ管理 • 単純にランニングコストを見ると、Lambdaの無料枠は強い ただし、メモリを多く使う重い処理はその限りではない • Step Functionsは、実装がシンプル、フローの可視性が強い • 稼働後の運用コスト(フローの改修・改善など)まで見据えて、 実装方法は選択しましょう さらに学びたいこと