Step Functions エラーハンドリング完全ガイド — 注文処理パイプラインで学ぶRetry/Catch/Timeout

目次

Step Functions エラーハンドリング完全ガイド — 注文処理パイプラインで学ぶRetry/Catch/Timeout

公開日: 2026-04-12
難易度: 中級〜上級
所要時間: 約120分
シリーズ: [Step Functions シリーズ 第3回]

この記事で学ぶこと
– Step Functionsのエラーの種類とRetry/Catch/Timeoutの全パラメータ
– 注文処理パイプライン(バリデーション→在庫→決済→SNS通知)の段階的構築
– Step A(無対策)→B(Retry)→C(Catch+ロールバック)→D(最終形)の4段階実装
– 障害注入(FAIL_MODE環境変数)によるエラーハンドリングの動作確認
– Terraform版との完全一致ASL定義

シリーズ前回記事:
– 第1回: AWS Step Functions 入門 — コンソールとTerraformで学ぶハンズオン
– 第2回: ECS × Step Functions 入門 — CSVバッチをFargateタスクでジョブ化するハンズオン


1. Step Functionsのエラーモデル(概念編)

1-1. なぜエラーハンドリングが重要か

分散システムにおける障害の不可避性

マイクロサービスやサーバーレスアーキテクチャでは、複数のコンポーネントが連携して処理を進める。その過程で、ネットワーク遅延、Lambda関数のタイムアウト、外部APIの一時的な障害など、さまざまな理由で処理が失敗する可能性がある。

分散システムの原則として「障害は起きる前提で設計する」ことが不可欠だ。単一のLambda関数であれば例外をtry/exceptで捕捉すれば済むが、複数のステップが連鎖するワークフローでは話が異なる。あるステップの失敗がどのステップに影響し、どう回復するかを明示的に定義する必要がある。

Step Functionsが提供する組み込みエラーハンドリング機能の価値

AWS Step Functionsは、このような複雑なエラー処理をステートマシンのASL(Amazon States Language)定義に直接組み込める。アプリケーションコードにリトライロジックを実装するのではなく、インフラ層でエラーハンドリングを宣言的に定義できる。

主なメリット:
コードとインフラの分離: Lambda関数はビジネスロジックに集中できる
可視性: Step Functions コンソールで実行履歴・エラー詳細を確認できる
一貫性: すべてのステップに統一されたリトライ・フォールバック戦略を適用できる
べき等性との連携: リトライ安全なステップ設計と組み合わせることで、堅牢なパイプラインを実現できる

この記事で学ぶこと(4段階構築の予告)

本シリーズ「Step Functionsエラーハンドリング完全ガイド」では、注文処理パイプライン(ReserveInventoryProcessPaymentSendConfirmation + SNS通知)を例に取り、以下の4段階で学んでいく:

  1. Section 1(本稿): エラーモデルの理解 — エラーの種類、Retry/Catchの全パラメータ
  2. Section 2: 構成図と実装 — ハンズオンで実際に動かす
  3. Section 3: 応用パターン — 冪等性設計、デッドレターキュー連携
  4. Section 4: 監視・運用 — CloudWatch Metrics、アラート設定

1-2. Stepで発生するエラーの種類

Step Functionsにはシステム予約エラー名(States.* プレフィックス)が定義されており、各エラーに適した対処を選択できる。

エラー名発生条件典型的な対処
States.TaskFailedLambdaが例外を投げた(ワイルドカード: States.Timeout以外の全エラーにマッチ)Retry
States.TimeoutTimeoutSecondsを超過したCatch → 代替処理
States.HeartbeatTimeoutHeartbeatSecondsを超過した(タスクからのハートビートが途絶えた)Catch → 再試行
States.PermissionsIAMロールに必要な権限がないCatch → アラート
States.ResultPathMatchFailureResultPathで指定したパスがInputに存在しないCatch → デバッグ
States.BranchFailedParallelステート内の分岐で失敗が発生したCatch(親ステートに定義)
States.NoChoiceMatchedChoiceステートでどの条件にも合致しなかったDefault分岐を定義
States.ALL全エラー(ワイルドカード)Catch配列の最後に配置

重要: States.TaskFailedStates.Timeout除くすべての既知エラー名にマッチするワイルドカードとして機能する。States.ALLはすべてのエラー(States.Timeoutを含む)にマッチする完全なワイルドカードだ。


1-3. Retryの全パラメータ解説

RetryフィールドはRetrierオブジェクトの配列として定義する。各Retrierはエラーの種類と再試行ポリシーを紐付ける。

パラメータ一覧

パラメータデフォルト説明
ErrorEqualsstring[]必須対象エラー名の配列。States.ALLは単独で指定すること
IntervalSecondsinteger1初回リトライまでの待機秒数
MaxAttemptsinteger3最大リトライ回数。0を指定するとリトライなし
BackoffRatenumber2.0待機時間の乗数。2.0なら2秒→4秒→8秒と倍増する
JitterStrategystring"NONE"ジッター(ランダム揺らぎ)戦略。"FULL"または"NONE"
MaxDelaySecondsinteger未設定(上限なし)待機時間の上限秒数。1以上31622400以下で指定する

JitterStrategyの詳細

JitterStrategy: "FULL"を指定すると、各リトライの待機時間が0〜計算値の間でランダムになる。

例(IntervalSeconds: 2, BackoffRate: 2.0, MaxAttempts: 3の場合):

リトライJitterStrategy: NONEJitterStrategy: FULL
1回目2秒後0〜2秒のランダム
2回目4秒後0〜4秒のランダム
3回目8秒後0〜8秒のランダム

複数の処理が同時に失敗した際の同時リトライ(Thundering Herd問題)を防ぐためにジッターは有効だ。

ASLサンプル(ReserveInventoryに適用)

"ReserveInventory": {  "Type": "Task",  "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:ReserveInventory",  "Retry": [ {"ErrorEquals": ["States.Timeout"],"MaxAttempts": 0 }, {"ErrorEquals": ["States.TaskFailed"],"IntervalSeconds": 2,"MaxAttempts": 3,"BackoffRate": 2.0,"JitterStrategy": "FULL","MaxDelaySeconds": 30 }  ],  "Next": "ProcessPayment"}

ポイント: States.TimeoutMaxAttempts: 0でリトライを無効化している。タイムアウトはリトライしても解決しないケースが多く、むしろ後続のCatchでフォールバック処理に回す方が適切だ。


1-4. Catchの全パラメータ解説

CatchフィールドはCatcherオブジェクトの配列として定義する。Retryをすべて消費した後(またはRetryが未定義の場合)に評価される。

パラメータ一覧

パラメータ必須説明
ErrorEqualsstring[]必須キャッチ対象エラー名の配列。States.ALLは単独かつ最後に配置
Nextstring必須エラー時の遷移先ステート名(ステートマシン内の名前と完全一致)
ResultPathstring \| null任意エラー情報を格納するInputのパス(例: "$.error")。nullを指定すると元のInputをそのまま維持する

ResultPathの使い分け

元のInput: { "orderId": "ORD-001", "amount": 5000 }ResultPath: "$.error" の場合:→ { "orderId": "ORD-001", "amount": 5000, "error": { "Error": "States.Timeout", "Cause": "..." } }ResultPath: null の場合:→ { "orderId": "ORD-001", "amount": 5000 }  ← エラー情報は破棄、元のInputを維持

フォールバック先のステートでエラー詳細をログ・通知に使いたい場合は"$.error"を指定する。元のInputをそのまま渡してリトライステートに遷移させたい場合はnullが有効だ。

ASLサンプル(ProcessPaymentに適用)

"ProcessPayment": {  "Type": "Task",  "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:ProcessPayment",  "Retry": [ {"ErrorEquals": ["States.TaskFailed"],"IntervalSeconds": 1,"MaxAttempts": 2,"BackoffRate": 2.0 }  ],  "Catch": [ {"ErrorEquals": ["States.Timeout"],"ResultPath": "$.error","Next": "RollbackInventory" }, {"ErrorEquals": ["States.ALL"],"ResultPath": "$.error","Next": "NotifyFailure" }  ],  "Next": "SendConfirmation"}

1-5. Retry → Catch の評価順序

エラーが発生した際の内部評価フローを以下に示す。

[ステート実行]↓  エラー発生↓  Retry定義があるか?  ├─ YES → 対応するRetriuerを検索  │├─ マッチあり & MaxAttempts残りあり → 待機後リトライ  │└─ マッチなし or MaxAttempts消費済み → ↓  └─ NO  ──────────────────────────────────────────↓  Catchを評価  ├─ マッチするErrorEqualsあり  │└─ ResultPathにエラー情報格納  │ → Nextで指定したステートへ遷移  └─ マッチなし→ 実行失敗(FAILED状態)

重要ポイント

  1. States.ALLは必ず最後に配置する: Catch配列を先頭から順番に評価するため、States.ALLを途中に書くと後続のCatchが評価されない。
  2. リトライ不適切なエラーはRetry対象に含めない: 入力バリデーションエラー(例: InvalidInputException)は何度リトライしても失敗するため、Catchで直接フォールバック処理に回す。
  3. Retryが先、Catchは後: RetryとCatchが両方定義されている場合、Step FunctionsはまずRetryを試みる。Retryをすべて消費して初めてCatchが評価される。

1-6. ベストプラクティス

べき等性(Idempotency)の重要性

リトライが発生すると、同じLambda関数が複数回実行される可能性がある。そのため、同じリクエストを複数回受け取っても結果が変わらない設計(べき等性)が不可欠だ。

注文処理の例:
ReserveInventory: リクエストIDをDynamoDBで確認し、処理済みなら在庫確保をスキップして成功を返す
ProcessPayment: べき等キー(idempotency_key)を決済APIに渡し、重複課金を防ぐ
SendConfirmation: メール送信済みフラグをDBで管理し、二重送信を防ぐ

Timeoutは必ず設定する

TimeoutSecondsを未設定にすると、デフォルトで最大1年(31536000秒)待機し続ける。Lambdaのタイムアウト(最大15分)より長いStep Functionsのタイムアウトを設定してもほぼ意味がないため、現実的な値を必ず設定すること。

"ProcessPayment": {  "Type": "Task",  "Resource": "arn:aws:lambda:...",  "TimeoutSeconds": 30,  "HeartbeatSeconds": 10,  ...}

ResultPathの使い分け

状況推奨
フォールバック先でエラー内容をログ・通知に使いたい"$.error"
フォールバック先で元のInputをそのまま使いたいnull
エラー情報も元Inputも両方渡したい"$.error"(元Inputは保持されたまま$.errorに追記される)

エラー通知はSNSで一元管理

NotifyFailureステートでSNS Publishを呼び出し、すべてのエラー通知を一か所に集約する。エラーの種類に応じてSNSトピックやメッセージを変えることで、アラート疲れを防げる。

CloudWatch Metricsでエラー率を監視

Step Functionsは以下のメトリクスを自動的にCloudWatchに送信する:

メトリクス説明
ExecutionsFailed失敗した実行数
ExecutionsTimedOutタイムアウトした実行数
ExecutionThrottledスロットリングされた実行数

ExecutionsFailedにアラームを設定し、閾値超過でSNSへ通知する構成が基本だ。


概念理解を踏まえ、次のセクションではハンズオンで扱う注文処理パイプラインの全体像を構成図で把握していく。


2. アーキテクチャ解説

本セクションでは、Step Functionsエラーハンドリング完全ガイドで扱う注文処理ステートマシンの全体像を把握する。構成図3枚を使い、アーキテクチャ・ステート遷移・エラー伝播の時系列をそれぞれ解説する。


2-1. 全体アーキテクチャ

全体アーキテクチャ

各コンポーネントの役割

このハンズオンでは、注文処理パイプラインを題材にStep Functionsのエラーハンドリングを学ぶ。構成要素は以下の通りだ。

コンポーネント役割
ValidateOrder Lambda注文データのバリデーション(金額・在庫有無の事前確認)。入力不正時は OrderValidationError を投げる
ReserveInventory Lambda在庫予約処理。一時的な競合が発生しやすく、Retryの対象となる主要ステート
ProcessPayment Lambda決済API呼び出し。タイムアウトが発生した場合は在庫ロールバックが必要
SNS Topic(失敗通知)いずれかのステートが最終的に失敗した際、NotifyFailure ステートからPublishされる。メール/Slackへの通知を担う
CloudWatch LogsStep Functionsの実行ログを格納。エラー発生時の原因調査に使用する
CloudWatch MetricsExecutionsFailed などのメトリクスを自動収集。アラームを設定してエラー率を監視する
IAM Role(SF実行ロール)Step FunctionsがLambdaを呼び出し、SNSにPublishするための権限を持つロール

実行トリガー

ステートマシンは以下の2通りの方法で起動できる。

  • 手動実行(User / API): AWSコンソールまたはCLI(aws stepfunctions start-execution)から直接起動する。開発・テスト時に使用する
  • EventBridge(スケジュール): 定期バッチ処理として起動する。本番運用での自動化に適している

障害注入ポイント

本ハンズオンではLambda関数内でFAIL_MODE環境変数を設定することで、任意のエラーパターンを再現できるように実装する。

# ReserveInventory に一時的な失敗を注入する例aws lambda update-function-configuration \  --function-name ReserveInventory \  --environment "Variables={FAIL_MODE=always}"
FAIL_MODE挙動
never(デフォルト)常に成功を返す
once1回だけ失敗し、以降は成功する(Retryテスト用)
always常に失敗を返す(MaxAttempts消費テスト用)
timeout30秒スリープしてタイムアウトを模倣する

2-2. ステートマシンフロー

ステートマシンフロー

ステートの色分けと意味

構成図の配色は、ステートの役割を直感的に伝えるための規約だ。この記事を通じて一貫して使用する。

ステートタイプ該当ステート
オレンジ処理ステート(Task)ValidateOrder, ReserveInventory, ProcessPayment
正常終了ステートOrderCompleted
エラー回復・通知ステートRollbackInventory, NotifyFailure
グレー失敗終端ステート(Fail)OrderFailed

矢印の色も同様の意味を持つ。

矢印の色意味
正常系(成功時の遷移)
Catch(エラーパス)
Retry(自己ループ)

Step A → D の段階的構築

このハンズオンでは以下4ステップで段階的にステートマシンを構築していく。構成図は最終形(Step D)を示している。

ステップ追加要素学習ポイント
Step A基本3ステート + OrderCompleted正常フローの確認
Step BReserveInventoryへのRetry追加指数バックオフRetry + JitterStrategy
Step CValidateOrderへのCatch / ProcessPaymentへのTimeoutSeconds+Catch / RollbackInventory追加Catch+ロールバック
Step DReserveInventoryへのCatch / NotifyFailure(SNS通知) / OrderFailed追加全体エラーハンドラ+SNS通知

Step Dの完成形では、すべてのステートにエラーハンドリングが適用され、どの段階で障害が発生してもOrderFailedステートに収束する設計になっている。


2-3. エラー伝播の時系列

エラー伝播フロー

Retry消費からCatch発火、SNS通知までの流れ

この図はReserveInventoryで障害が発生し、Retryを3回消費した後にCatchが発火してSNS通知に至るまでの時系列を示している。

① 実行(1回目)
Step FunctionsがReserveInventory Lambdaを呼び出す。FAIL_MODE=alwaysのため即座にStates.TaskFailedが返される。

② 失敗(States.TaskFailed)
RetryのErrorEquals: ["States.TaskFailed"]にマッチ。MaxAttempts: 3のカウントが1に加算される。IntervalSeconds: 2なので2秒後にリトライを試みる。

③ Retry待機 2s
Step Functionsは2秒間待機する。この間、実行はRUNNING状態のまま停止しない(非同期待機)。

④ 実行(2回目)
2秒後に再びLambdaを呼び出す。同様に失敗する。

⑤ 失敗
カウントが2になる。BackoffRate: 2.0により、次の待機時間は 2秒 × 2.0 = 4秒 となる。

⑥ Retry待機 4s(BackoffRate 2.0)
4秒間待機する。JitterStrategy: "FULL"を設定している場合、この待機時間に0〜4秒のランダムな揺らぎが加わる。

⑦ 実行(3回目)
3回目の呼び出し。これがMaxAttempts: 3の最後の試みとなる。

⑧ MaxAttempts消費 → Catch発火
3回目も失敗。MaxAttemptsを消費したため、次にCatch配列が評価される。ErrorEquals: ["States.ALL"]にマッチし、Catcherが発火する。ResultPath: "$.error"に以下のエラー情報が格納される:

{  "orderId": "ORD-001",  "amount": 5000,  "error": { "Error": "States.TaskFailed", "Cause": "ReserveInventory failed: stock locked"  }}

⑨ Catch発火 → CloudWatch記録
Step Functionsが実行ログをCloudWatch Logsに記録する。エラーの原因(Cause)がログに含まれるため、後からトラブルシューティングが可能だ。

⑩ SNS通知(Publish)
NotifyFailureステートがSNS Publishを呼び出し、失敗メッセージをサブスクライバー(メール等)へ配信する。メッセージには$.errorに格納したエラー情報が含まれる。

⑪ FAILED終了
NotifyFailureOrderFailedFailタイプのステート)へ遷移し、ステートマシン実行がFAILED状態で終了する。AWSコンソールの実行履歴に失敗ステートと原因が記録される。

待機時間の計算式

バックオフ後の待機時間は以下の式で算出できる。

待機時間(n回目) = IntervalSeconds × BackoffRate^(n-1)

IntervalSeconds: 2, BackoffRate: 2.0の場合:

リトライ計算待機時間
1回目2 × 2.0^02秒
2回目2 × 2.0^14秒
3回目2 × 2.0^28秒

MaxDelaySecondsを設定すると上限を設けられる。例えばMaxDelaySeconds: 30とすれば、何回リトライしても30秒以上は待たない。


アーキテクチャの全体像を把握したところで、次はAWSコンソールを使って実際に4段階のステートマシンを構築し、FAIL_MODE環境変数でエラーを注入しながら動作を確認していく。


3. AWSコンソールでのハンズオン

このセクションでは、AWSコンソールを使ってStep Functionsのエラーハンドリングを実践的に学びます。「注文処理パイプライン」を題材に、エラーハンドリングなしの状態(Step A)からRetry・Catch・SNS通知を備えた最終形(Step D)まで段階的に構築することで、各機能の効果を肌で体感できます。


3-1. 前提条件

  • AWSアカウントと以下のサービスを操作できるIAM権限が必要です
  • AWS Lambda(関数の作成・編集・環境変数の変更)
  • AWS Step Functions(ステートマシンの作成・実行)
  • Amazon SNS(トピックの作成・サブスクリプションの設定)
  • AWS IAM(ロール・ポリシーの作成)
  • Amazon CloudWatch(ログ・メトリクスの参照)
  • Pythonランタイム (3.12) はAWSが管理するマネージドランタイムを使用します。Lambdaのインラインエディタで直接コードを貼り付けるため、ローカル環境へのPythonインストールは不要です。

3-2. Lambda 3関数のデプロイ(インラインPython・障害注入機能付き)

今回は以下の3つのLambda関数を作成します。各関数には環境変数 FAIL_MODE による障害注入機能が組み込まれており、後のStep A〜Dのテストで活用します。

関数名役割FAIL_MODE の効果
validate_order注文データのバリデーションalways: 必ず失敗 / random: 50%で失敗
reserve_inventory在庫の確保(ロールバック対応)always: 必ず失敗 / random: 50%で失敗
process_payment決済処理always: 必ず失敗 / random: 50%で失敗 / timeout: 15秒スリープしてSFタイムアウトを誘発

重要: デフォルトの FAIL_MODEnone(正常動作)です。テスト後は必ず none に戻してください。

関数の作成手順(3関数共通)

  1. Lambda コンソールを開き、「関数の作成」をクリック
  2. 一から作成」を選択
  3. 以下を入力:
  4. 関数名: 後述の各関数名を入力
  5. ランタイム: Python 3.12
  6. アーキテクチャ: x86_64(デフォルト)
  7. 実行ロール」→「基本的なLambdaアクセス権限で新しいロールを作成」を選択
  8. 関数の作成」をクリック
  9. 関数の詳細画面が開いたら、「コードソース」セクションのインラインエディタに後述のコードを貼り付け
  10. Deploy」ボタンをクリックしてデプロイ
  11. 設定」タブ → 「環境変数」 → 「編集」をクリック
  12. キー: FAIL_MODE、値: none を設定して「保存

図1: Lambda関数の作成


validate_order 関数 (Python 3.12)

関数名: validate_order

import jsonimport osimport randomclass OrderValidationError(Exception): passdef lambda_handler(event, context): fail_mode = os.environ.get("FAIL_MODE", "none") if fail_mode == "always":  raise OrderValidationError("必須フィールドが不足しています") elif fail_mode == "random" and random.random() < 0.5:  raise OrderValidationError("注文データの検証に失敗しました") order_id = event.get("order_id") if not order_id:  raise OrderValidationError("order_id は必須です") return {  "order_id": order_id,  "items": event.get("items", []),  "total_amount": event.get("total_amount", 0),  "validated": True }

ポイント: OrderValidationError はカスタム例外クラスです。Step C・D のCatchルールで "OrderValidationError" という名前で捕捉できます。Lambdaは例外クラス名をそのままStep Functionsに伝えます。


reserve_inventory 関数 (Python 3.12)

関数名: reserve_inventory

import jsonimport osimport randomdef lambda_handler(event, context): fail_mode = os.environ.get("FAIL_MODE", "none") action = event.get("action", "reserve") # ロールバックモード if action == "rollback":  print(f"在庫ロールバック実行: order_id={event.get('order_id')}")  return {"rollback": True, "order_id": event.get("order_id")} # 障害注入 if fail_mode == "always":  raise Exception("在庫システムへの接続に失敗しました (always)") elif fail_mode == "random" and random.random() < 0.5:  raise Exception("在庫システムへの接続に失敗しました (random)") return {  "order_id": event.get("order_id"),  "inventory_reserved": True,  "reservation_id": f"RES-{event.get('order_id')}-001" }

ポイント: この関数は「在庫確保」と「在庫ロールバック」の両方の役割を担います。Step C・D のRollbackInventoryステートから "action": "rollback" パラメータ付きで呼ばれると、ロールバック処理に切り替わります。


process_payment 関数 (Python 3.12)

関数名: process_payment

import jsonimport osimport randomimport timedef lambda_handler(event, context): fail_mode = os.environ.get("FAIL_MODE", "none") if fail_mode == "always":  raise Exception("決済処理に失敗しました (always)") elif fail_mode == "random" and random.random() < 0.5:  raise Exception("決済処理に失敗しました (random)") elif fail_mode == "timeout":  # SF側のTimeoutSeconds(10)を超過させる  time.sleep(15) return {  "order_id": event.get("order_id"),  "payment_completed": True,  "transaction_id": f"TXN-{event.get('order_id')}-001" }

ポイント: fail_mode == "timeout" のとき15秒スリープします。Step C・D では ProcessPayment ステートに "TimeoutSeconds": 10 を設定しているため、10秒でStep Functionsが States.Timeout エラーを発生させます。


Lambda ARNの確認

3関数を作成したら、各関数の詳細画面右上に表示される 関数ARN をメモしておきましょう。後述のASLの <LAMBDA_ARN_VALIDATE_ORDER> などのプレースホルダーに使用します。

ARNの形式例:arn:aws:lambda:ap-northeast-1:123456789012:function:validate_orderarn:aws:lambda:ap-northeast-1:123456789012:function:reserve_inventoryarn:aws:lambda:ap-northeast-1:123456789012:function:process_payment

3-3. SNSトピック作成 + メールサブスクリプション

Step Dの「全エラーハンドラ + SNS通知」で使用するSNSトピックを作成します。

  1. SNS コンソールを開き、「トピックの作成」をクリック
  2. 以下を設定:
  3. タイプ: スタンダード
  4. 名前: order-failure-notification
  5. トピックの作成」をクリック
  6. トピック詳細画面で「サブスクリプションの作成」をクリック
  7. 以下を設定:
  8. プロトコル: Eメール
  9. エンドポイント: 通知を受け取りたいメールアドレス
  10. サブスクリプションの作成」をクリック
  11. 入力したメールアドレスに確認メールが届くので、メール内のリンクをクリックしてサブスクリプションを確定

重要: トピックARNをメモしておいてください。Step DのASLの <SNS_TOPIC_ARN> プレースホルダーに使用します。
ARNの形式例:
arn:aws:sns:ap-northeast-1:123456789012:order-failure-notification

図2: SNSトピック作成


3-4. IAMロール作成(Step Functions実行ロール)

Step Functionsが Lambda を呼び出し、SNSを発行するための実行ロールを作成します。

ロールの作成

  1. IAM コンソールを開き、左メニューの「ロール」→「ロールの作成」をクリック
  2. 信頼されたエンティティのタイプ」→「AWSのサービス」を選択
  3. ユースケース」→「Step Functions」を選択して「次へ
  4. ポリシーのアタッチは現時点でスキップして「次へ
  5. ロール名: sf-order-execution-role を入力して「ロールの作成」をクリック

インラインポリシーの追加

作成したロールの詳細画面を開き、「許可」タブ→「許可を追加」→「インラインポリシーを作成」をクリックします。

JSON」タブに切り替えて以下のポリシーを貼り付けます(<ACCOUNT_ID><REGION> を実際の値に置き換えてください):

{  "Version": "2012-10-17",  "Statement": [ {"Effect": "Allow","Action": "lambda:InvokeFunction","Resource": [  "arn:aws:lambda:<REGION>:<ACCOUNT_ID>:function:validate_order",  "arn:aws:lambda:<REGION>:<ACCOUNT_ID>:function:reserve_inventory",  "arn:aws:lambda:<REGION>:<ACCOUNT_ID>:function:process_payment"] }, {"Effect": "Allow","Action": "sns:Publish","Resource": "arn:aws:sns:<REGION>:<ACCOUNT_ID>:order-failure-notification" }, {"Effect": "Allow","Action": [  "logs:CreateLogGroup",  "logs:CreateLogDelivery",  "logs:PutLogEvents",  "logs:DescribeLogGroups",  "logs:DescribeLogStreams"],"Resource": "*" }, {"Effect": "Allow","Action": [  "xray:PutTraceSegments",  "xray:GetSamplingRules",  "xray:GetSamplingTargets"],"Resource": "*" }  ]}

ポリシー名(例: sf-order-execution-policy)を入力して「ポリシーの作成」をクリックします。

図3: IAMロール作成


3-5. ステートマシンを段階的に構築(記事の核心)

💡 Step A→B→C→Dの順に構築・実行することで、エラーハンドリングの効果を体感できます

ここからがこの記事のメインパートです。同じ注文処理パイプラインを4段階に分けて強化していきます。

ステートマシン共通の作成手順

各Stepで共通する基本手順は以下のとおりです:

  1. Step Functions コンソールを開き、「ステートマシンの作成」をクリック
  2. コードでワークフローを記述する」を選択(ASLを直接入力するため)
  3. エディタにASLのJSONを貼り付け(<プレースホルダー> は実際のARNに置き換える)
  4. 次へ」をクリック
  5. ステートマシン名を入力(各Stepで指定)
  6. 実行ロール: 「既存のロールを選択」→ sf-order-execution-role を選択
  7. ステートマシンの作成」をクリック

Step A: エラーハンドリングなし — 失敗を体験する

目的: エラーハンドリングを一切設定していない素の状態を体験し、問題点を把握する

  1. 前述の共通手順に従い、以下のASLを使ってステートマシンを作成します。
  2. <プレースホルダー>を実際のLambda ARNに置き換えてください
{  "Comment": "注文処理パイプライン — Step A(エラーハンドリングなし)",  "StartAt": "ValidateOrder",  "States": { "ValidateOrder": {"Type": "Task","Resource": "<LAMBDA_ARN_VALIDATE_ORDER>","Next": "ReserveInventory" }, "ReserveInventory": {"Type": "Task","Resource": "<LAMBDA_ARN_RESERVE_INVENTORY>","Next": "ProcessPayment" }, "ProcessPayment": {"Type": "Task","Resource": "<LAMBDA_ARN_PROCESS_PAYMENT>","Next": "OrderCompleted" }, "OrderCompleted": {"Type": "Succeed" }  }}
  1. ステートマシン名: order-pipeline-step-a
  2. 実行ロール: sf-order-execution-role
実行テスト

テスト準備: reserve_inventory 関数の環境変数 FAIL_MODEalways に変更します。

入力JSON:

{  "order_id": "ORD-001",  "items": [{"id": "ITEM-A", "qty": 2}],  "total_amount": 5000}

結果確認:
– ビジュアルフロー上で ReserveInventory ステートが赤くなり、実行が FAILED になることを確認
– 「イベント」タブで TaskStateAborted イベントと例外メッセージを確認
何の回復機能もなく、即座に失敗する ことを体験する

これが何も対策しない状態です。 一時的なネットワーク障害でも、本番サービスが即座に停止します。次のStepでRetryを追加して改善していきます。

テスト後は reserve_inventoryFAIL_MODEnone に戻してください。

図4: Step A 実行失敗の様子


Step B: Retry追加 — 一時障害からの自動回復

目的: ReserveInventory にRetryルールを追加し、一時障害から自動回復できることを確認する

  1. 以下のASLで新しいステートマシンを作成します。
  2. <プレースホルダー>を実際のLambda ARNに置き換えてください
{  "Comment": "注文処理パイプライン — Step B(Retry追加)",  "StartAt": "ValidateOrder",  "States": { "ValidateOrder": {"Type": "Task","Resource": "<LAMBDA_ARN_VALIDATE_ORDER>","Next": "ReserveInventory" }, "ReserveInventory": {"Type": "Task","Resource": "<LAMBDA_ARN_RESERVE_INVENTORY>","Retry": [  { "ErrorEquals": ["States.TaskFailed"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0, "JitterStrategy": "FULL"  }],"Next": "ProcessPayment" }, "ProcessPayment": {"Type": "Task","Resource": "<LAMBDA_ARN_PROCESS_PAYMENT>","Next": "OrderCompleted" }, "OrderCompleted": {"Type": "Succeed" }  }}
  1. ステートマシン名: order-pipeline-step-b
  2. 実行ロール: sf-order-execution-role
実行テスト

テスト準備: reserve_inventory 関数の FAIL_MODErandom に変更します(50%の確率で失敗)。

入力JSON: Step Aと同じものを使用

結果確認:
– 「実行履歴」タブで TaskStateExited(失敗)→ TaskStateEntered(リトライ)を繰り返すイベントを確認
– リトライの待機時間が 2秒 → 4秒 → 8秒 と指数バックオフで増加していることを確認
JitterStrategy: FULL により、待機時間にランダムなゆらぎが加わっていることを確認(同時大量リトライの分散効果)
FAIL_MODE=random なので、数回のリトライ後に最終的に SUCCEEDED になることを確認

JitterStrategy の効果: 複数の実行が同時にリトライする場合、ジッターがなければ全実行が同じタイミングで再試行し、スパイク的な負荷が発生します。FULL ジッターは待機時間を [0, IntervalSeconds * BackoffRate^attempt] の範囲でランダム化し、負荷を分散します。

テスト後は reserve_inventoryFAIL_MODEnone に戻してください。

図5: Step B リトライ成功の様子
図6: 実行履歴でリトライ確認


Step C: Catch + Fallback — タイムアウト時のロールバック

目的: Catchルールを追加し、エラーの種類に応じた回復処理(バリデーションエラー通知・タイムアウト時ロールバック)を実装する

  1. 以下のASLで新しいステートマシンを作成します。
  2. <プレースホルダー>を実際のLambda ARNに置き換えてください
{  "Comment": "注文処理パイプライン — Step C(Catch + ロールバック)",  "StartAt": "ValidateOrder",  "States": { "ValidateOrder": {"Type": "Task","Resource": "<LAMBDA_ARN_VALIDATE_ORDER>","Catch": [  { "ErrorEquals": ["OrderValidationError"], "ResultPath": "$.error", "Next": "ValidationFailed"  }],"Next": "ReserveInventory" }, "ReserveInventory": {"Type": "Task","Resource": "<LAMBDA_ARN_RESERVE_INVENTORY>","Retry": [  { "ErrorEquals": ["States.TaskFailed"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0, "JitterStrategy": "FULL"  }],"Next": "ProcessPayment" }, "ProcessPayment": {"Type": "Task","Resource": "<LAMBDA_ARN_PROCESS_PAYMENT>","TimeoutSeconds": 10,"Catch": [  { "ErrorEquals": ["States.Timeout"], "ResultPath": "$.error", "Next": "RollbackInventory"  }],"Next": "OrderCompleted" }, "RollbackInventory": {"Type": "Task","Resource": "<LAMBDA_ARN_RESERVE_INVENTORY>","Parameters": {  "action": "rollback",  "order_id.$": "$.order_id"},"ResultPath": null,"Next": "PaymentFailed" }, "ValidationFailed": {"Type": "Fail","Error": "OrderValidationError","Cause": "注文バリデーション失敗" }, "PaymentFailed": {"Type": "Fail","Error": "PaymentTimeout","Cause": "決済処理タイムアウト。在庫をロールバックしました。" }, "OrderCompleted": {"Type": "Succeed" }  }}
  1. ステートマシン名: order-pipeline-step-c
  2. 実行ロール: sf-order-execution-role
実行テスト その1: バリデーションエラー

テスト準備: validate_order 関数の FAIL_MODEalways に変更します。

結果確認:
ValidateOrder ステートで OrderValidationError が発生
– Catchルールに捕捉され、ValidationFailed(Fail)ステートへ遷移することを確認
ResultPath: "$.error" により、元の入力JSON($.order_id など)にエラー情報が追記されていることを「出力」タブで確認

テスト後は validate_orderFAIL_MODEnone に戻してください。

実行テスト その2: タイムアウト → ロールバック

テスト準備: process_payment 関数の FAIL_MODEtimeout に変更します(15秒スリープで10秒タイムアウトを誘発)。

結果確認:
ProcessPayment ステートで TimeoutSeconds: 10 を超え、States.Timeout が発生
– Catchルールに捕捉され、RollbackInventoryPaymentFailed の流れを確認
RollbackInventoryreserve_inventory 関数を "action": "rollback" パラメータ付きで呼び出し、在庫をキャンセルすることを確認

ロールバックの重要性: 決済が失敗した場合、すでに確保していた在庫を解放しないと「在庫は押さえられているのに注文は存在しない」という不整合が発生します。RollbackInventory ステートがこの補償トランザクションを担います。

テスト後は process_paymentFAIL_MODEnone に戻してください。

図7: Step C タイムアウト→ロールバックの様子


Step D: 最終形 — 全エラーハンドラ + SNS通知

目的: 全てのエラーケースをカバーし、障害発生時にSNSメールで通知を受け取る本番想定の構成を完成させる

  1. 以下のASLで新しいステートマシンを作成します。
  2. <プレースホルダー>を実際のLambda ARNとSNSトピックARNに置き換えてください
{  "Comment": "注文処理パイプライン — Step D(最終形: Retry+Catch+SNS通知+全体エラーハンドラ)",  "StartAt": "ValidateOrder",  "States": { "ValidateOrder": {"Type": "Task","Resource": "<LAMBDA_ARN_VALIDATE_ORDER>","Catch": [  { "ErrorEquals": ["OrderValidationError"], "ResultPath": "$.error", "Next": "NotifyFailure"  }],"Next": "ReserveInventory" }, "ReserveInventory": {"Type": "Task","Resource": "<LAMBDA_ARN_RESERVE_INVENTORY>","Retry": [  { "ErrorEquals": ["States.TaskFailed"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0, "JitterStrategy": "FULL"  }],"Catch": [  { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "NotifyFailure"  }],"Next": "ProcessPayment" }, "ProcessPayment": {"Type": "Task","Resource": "<LAMBDA_ARN_PROCESS_PAYMENT>","TimeoutSeconds": 10,"Catch": [  { "ErrorEquals": ["States.Timeout"], "ResultPath": "$.error", "Next": "RollbackInventory"  },  { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "NotifyFailure"  }],"Next": "OrderCompleted" }, "RollbackInventory": {"Type": "Task","Resource": "<LAMBDA_ARN_RESERVE_INVENTORY>","Parameters": {  "action": "rollback",  "order_id.$": "$.order_id"},"ResultPath": null,"Next": "NotifyFailure" }, "NotifyFailure": {"Type": "Task","Resource": "arn:aws:states:::sns:publish","Parameters": {  "TopicArn": "<SNS_TOPIC_ARN>",  "Message.$": "States.Format('注文処理失敗 (order_id: {})', $.order_id)",  "Subject": "注文処理エラー通知"},"Next": "OrderFailed" }, "OrderCompleted": {"Type": "Succeed" }, "OrderFailed": {"Type": "Fail","Error": "OrderProcessingFailed","Cause": "注文処理が失敗しました。SNS通知を送信しました。" }  }}
  1. ステートマシン名: order-pipeline-final
  2. 実行ロール: sf-order-execution-role
4パターンのテスト実行

Step Dでは以下の4つのシナリオを試してみましょう。

テスト1: 正常系

  • 設定: 全Lambda FAIL_MODE=none
  • 期待結果: OrderCompleted(Succeed)で終了
  • 確認点: ビジュアルフローが全ステート緑色になることを確認

図8: Step D 正常系実行

テスト2: リトライ成功(一時障害からの回復)

  • 設定: reserve_inventoryFAIL_MODE=random
  • 期待結果: 数回のリトライ後に OrderCompleted で成功
  • 確認点: 実行履歴でリトライイベントを確認。最終的に成功することを確認
  • テスト後: FAIL_MODEnone に戻す

テスト3: タイムアウト → ロールバック → SNS通知

  • 設定: process_paymentFAIL_MODE=timeout
  • 期待結果: ProcessPayment タイムアウト → RollbackInventoryNotifyFailure(SNS発行)→ OrderFailed
  • 確認点:
  • ビジュアルフローで上記の遷移を確認
  • 登録したメールアドレスに「注文処理エラー通知」メールが届くことを確認
  • メール本文に 注文処理失敗 (order_id: ORD-001) が含まれることを確認
  • テスト後: FAIL_MODEnone に戻す

テスト4: バリデーション失敗 → SNS通知

  • 設定: validate_orderFAIL_MODE=always
  • 期待結果: ValidateOrderOrderValidationErrorNotifyFailure(SNS発行)→ OrderFailed
  • 確認点: SNSメール受信を確認
  • テスト後: FAIL_MODEnone に戻す

図9: Step D SNS通知メール受信


3-6. CloudWatch Logs/Metricsでのエラー監視

本番運用では、エラーハンドリングを実装するだけでなく、エラーの発生を素早く検知できる監視体制も不可欠です。

X-Ray / CloudWatch Logs 統合の確認

Step Functionsのステートマシン設定で、ログとトレースを有効化できます:

  1. order-pipeline-final の詳細画面を開き、「編集」をクリック
  2. ログ」セクションで「CloudWatch Logsへのログ送信を有効化」をオン
  3. ログレベル: ERROR(失敗したステートのみ記録)または ALL(全ステートを記録)
  4. X-Ray トレース」セクションで「X-Rayトレースを有効にする」をオン
  5. 保存」をクリック

CloudWatch Metricsで確認できる主要指標

メトリクス名説明監視の目的
ExecutionsFailed失敗した実行数異常終了の検知
ExecutionsTimedOutタイムアウトした実行数処理遅延の検知
ExecutionThrottledスロットリングされた実行数高負荷・クォータ超過の検知
ExecutionsStarted開始された実行数トラフィック量の把握
ExecutionsSucceeded成功した実行数成功率の計算に使用

CloudWatch Logsでの実行ログ確認

  1. CloudWatch コンソール → 「ロググループ」を開く
  2. /aws/states/order-pipeline-final のロググループを選択
  3. 最新のログストリームを開き、失敗した実行のイベント詳細を確認

アラームの設定(推奨)

推奨アラーム設定:- ExecutionsFailed > 0 → SNS通知(注文処理失敗の即時アラート)- ExecutionsTimedOut > 0 → SNS通知(タイムアウト発生の検知)- 評価期間: 1分- データポイント数: 1/1(1回でも発生したらアラート)

CloudWatch コンソール → 「アラーム」→「アラームの作成」から上記の設定でアラームを作成することを推奨します。

図10: CloudWatch Metricsの確認


ハンズオン完了後のクリーンアップ: 作成したリソース(Lambda 3関数、SNSトピック、Step Functions 4ステートマシン、IAMロール)は使用後に削除することをお勧めします。特にSNSサブスクリプションは意図しない課金を防ぐため、早めに削除してください。

コンソールでの動作確認が完了したら、次のセクションでは同一のASLをTerraformでIaC化し、再現性のある環境構築を実現する方法を解説する。


4. Terraformでの構築

Section 3(コンソールハンズオン)で構築した注文処理パイプラインを、TerraformでIaC化します。コンソール版と完全に同一のASL定義を使用するため、Step FunctionsのロジックはSection 3と一字一句一致しています。


4-1. 前提条件

項目要件
Terraform1.0以上
AWS CLI設定済み(aws configure
IAM権限Lambda / Step Functions / SNS / IAM / CloudWatch Logs の操作権限
Python3.12(Lambda ランタイムに対応)
# バージョン確認terraform versionaws sts get-caller-identity

4-2. ディレクトリ構成

sf-error-handling/├── main.tf# メインリソース定義├── variables.tf # 変数定義├── outputs.tf# 出力値定義├── lambda/│├── validate_order.py # 注文検証Lambda│├── reserve_inventory.py # 在庫確保Lambda│└── process_payment.py# 支払処理Lambda└── statemachine/ └── definition.json.tpl  # Step Functions ASL定義(テンプレート)

4-3. Lambda ソースコード

lambda/validate_order.py

注文データの検証を行うLambdaです。FAIL_MODE環境変数で障害注入が可能です。

import jsonimport osimport randomclass OrderValidationError(Exception): passdef lambda_handler(event, context): fail_mode = os.environ.get("FAIL_MODE", "none") if fail_mode == "always":  raise OrderValidationError("注文データが無効です(強制失敗)") elif fail_mode == "random" and random.random() < 0.5:  raise OrderValidationError("注文データが無効です(ランダム失敗)") # 注文検証ロジック order_id = event.get("order_id") items = event.get("items", []) total_amount = event.get("total_amount", 0) if not order_id:  raise OrderValidationError("order_id が指定されていません") if not items:  raise OrderValidationError("注文アイテムが空です") if total_amount <= 0:  raise OrderValidationError("合計金額が不正です") return {  "order_id": order_id,  "items": items,  "total_amount": total_amount,  "validated": True, }

lambda/reserve_inventory.py

在庫確保(およびロールバック)を行うLambdaです。rollbackアクションに対応しています。

import jsonimport osimport randomdef lambda_handler(event, context): fail_mode = os.environ.get("FAIL_MODE", "none") action = event.get("action", "reserve") # ロールバック処理 if action == "rollback":  order_id = event.get("order_id")  print(f"在庫ロールバック: order_id={order_id}")  return {"order_id": order_id, "action": "rollback", "status": "success"} # 在庫確保処理 if fail_mode == "always":  raise Exception("在庫確保に失敗しました(強制失敗)") elif fail_mode == "random" and random.random() < 0.5:  raise Exception("在庫確保に失敗しました(ランダム失敗)") order_id = event.get("order_id") items = event.get("items", []) print(f"在庫確保: order_id={order_id}, items={items}") return {  "order_id": order_id,  "items": items,  "total_amount": event.get("total_amount", 0),  "validated": event.get("validated", False),  "inventory_reserved": True, }

lambda/process_payment.py

支払処理を行うLambdaです。FAIL_MODE=timeoutでタイムアウトのシミュレーションが可能です(15秒スリープ)。

import jsonimport osimport randomimport timedef lambda_handler(event, context): fail_mode = os.environ.get("FAIL_MODE", "none") if fail_mode == "always":  raise Exception("支払処理に失敗しました(強制失敗)") elif fail_mode == "random" and random.random() < 0.5:  raise Exception("支払処理に失敗しました(ランダム失敗)") elif fail_mode == "timeout":  print("タイムアウトシミュレーション: 15秒スリープ")  time.sleep(15) order_id = event.get("order_id") total_amount = event.get("total_amount", 0) print(f"支払処理: order_id={order_id}, amount={total_amount}") return {  "order_id": order_id,  "total_amount": total_amount,  "payment_status": "completed",  "transaction_id": f"TXN-{order_id}", }

4-4. statemachine/definition.json.tpl

重要: 以下のASLは、Section 3(コンソール版)のStep D と同一のASL構造です。
Terraformのtemplatefile()関数を使用し、${...}変数を実際のARNに補間します。

{  "Comment": "注文処理パイプライン — Step D(最終形: Retry+Catch+SNS通知+全体エラーハンドラ)",  "StartAt": "ValidateOrder",  "States": { "ValidateOrder": {"Type": "Task","Resource": "${validate_order_arn}","Catch": [  { "ErrorEquals": ["OrderValidationError"], "ResultPath": "$.error", "Next": "NotifyFailure"  }],"Next": "ReserveInventory" }, "ReserveInventory": {"Type": "Task","Resource": "${reserve_inventory_arn}","Retry": [  { "ErrorEquals": ["States.TaskFailed"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0, "JitterStrategy": "FULL"  }],"Catch": [  { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "NotifyFailure"  }],"Next": "ProcessPayment" }, "ProcessPayment": {"Type": "Task","Resource": "${process_payment_arn}","TimeoutSeconds": 10,"Catch": [  { "ErrorEquals": ["States.Timeout"], "ResultPath": "$.error", "Next": "RollbackInventory"  },  { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "NotifyFailure"  }],"Next": "OrderCompleted" }, "RollbackInventory": {"Type": "Task","Resource": "${reserve_inventory_arn}","Parameters": {  "action": "rollback",  "order_id.$": "$.order_id"},"ResultPath": null,"Next": "NotifyFailure" }, "NotifyFailure": {"Type": "Task","Resource": "arn:aws:states:::sns:publish","Parameters": {  "TopicArn": "${sns_topic_arn}",  "Message.$": "States.Format('注文処理失敗 (order_id: {})', $.order_id)",  "Subject": "注文処理エラー通知"},"Next": "OrderFailed" }, "OrderCompleted": {"Type": "Succeed" }, "OrderFailed": {"Type": "Fail","Error": "OrderProcessingFailed","Cause": "注文処理が失敗しました。SNS通知を送信しました。" }  }}

templatefile() の変数一覧

変数名説明Terraform の参照先
${validate_order_arn}ValidateOrder Lambda の ARNaws_lambda_function.validate_order.arn
${reserve_inventory_arn}ReserveInventory Lambda の ARNaws_lambda_function.reserve_inventory.arn
${process_payment_arn}ProcessPayment Lambda の ARNaws_lambda_function.process_payment.arn
${sns_topic_arn}エラー通知 SNS トピックの ARNaws_sns_topic.order_failure.arn

4-5. variables.tf

variable "aws_region" {  default = "ap-northeast-1"}variable "project_name" {  default = "order-pipeline"}variable "notification_email" {  description = "エラー通知先メールアドレス"  type  = string}

4-6. main.tf(完全なコード)

# ============================================================# Provider# ============================================================terraform {  required_providers { aws = { source = "hashicorp/aws", version = ">= 5.0" }  }}provider "aws" { region = var.aws_region }data "aws_caller_identity" "current" {}data "aws_region" "current" {}# ============================================================# Lambda: ソースコードをZIPアーカイブ化# ============================================================data "archive_file" "validate_order" {  type  = "zip"  source_file = "${path.module}/lambda/validate_order.py"  output_path = "${path.module}/.terraform/lambda_zips/validate_order.zip"}data "archive_file" "reserve_inventory" {  type  = "zip"  source_file = "${path.module}/lambda/reserve_inventory.py"  output_path = "${path.module}/.terraform/lambda_zips/reserve_inventory.zip"}data "archive_file" "process_payment" {  type  = "zip"  source_file = "${path.module}/lambda/process_payment.py"  output_path = "${path.module}/.terraform/lambda_zips/process_payment.zip"}# ============================================================# IAM: Lambda 実行ロール# ============================================================resource "aws_iam_role" "lambda_role" {  name = "${var.project_name}-lambda-role"  assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{Effect = "Allow"Principal = { Service = "lambda.amazonaws.com" }Action = "sts:AssumeRole" }]  })}resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {  role = aws_iam_role.lambda_role.name  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"}# ============================================================# IAM: Step Functions 実行ロール# ============================================================resource "aws_iam_role" "sf_execution_role" {  name = "${var.project_name}-sf-execution-role"  assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{Effect = "Allow"Principal = { Service = "states.amazonaws.com" }Action = "sts:AssumeRole" }]  })}resource "aws_iam_role_policy" "sf_policy" {  name = "${var.project_name}-sf-policy"  role = aws_iam_role.sf_execution_role.id  policy = jsonencode({ Version = "2012-10-17" Statement = [{  Effect = "Allow"  Action = ["lambda:InvokeFunction"]  Resource = [ aws_lambda_function.validate_order.arn, aws_lambda_function.reserve_inventory.arn, aws_lambda_function.process_payment.arn,  ]},{  Effect= "Allow"  Action= ["sns:Publish"]  Resource = [aws_sns_topic.order_failure.arn]}, ]  })}# ============================================================# CloudWatch Log Groups(各 Lambda 用)# ============================================================resource "aws_cloudwatch_log_group" "validate_order" {  name  = "/aws/lambda/${var.project_name}-validate-order"  retention_in_days = 7}resource "aws_cloudwatch_log_group" "reserve_inventory" {  name  = "/aws/lambda/${var.project_name}-reserve-inventory"  retention_in_days = 7}resource "aws_cloudwatch_log_group" "process_payment" {  name  = "/aws/lambda/${var.project_name}-process-payment"  retention_in_days = 7}# ============================================================# Lambda Functions# ============================================================resource "aws_lambda_function" "validate_order" {  function_name = "${var.project_name}-validate-order"  role = aws_iam_role.lambda_role.arn  runtime = "python3.12"  handler = "validate_order.lambda_handler"  filename= data.archive_file.validate_order.output_path  source_code_hash = data.archive_file.validate_order.output_base64sha256  timeout = 30  environment { variables = {FAIL_MODE = "none" }  }  depends_on = [aws_cloudwatch_log_group.validate_order]}resource "aws_lambda_function" "reserve_inventory" {  function_name = "${var.project_name}-reserve-inventory"  role = aws_iam_role.lambda_role.arn  runtime = "python3.12"  handler = "reserve_inventory.lambda_handler"  filename= data.archive_file.reserve_inventory.output_path  source_code_hash = data.archive_file.reserve_inventory.output_base64sha256  timeout = 30  environment { variables = {FAIL_MODE = "none" }  }  depends_on = [aws_cloudwatch_log_group.reserve_inventory]}resource "aws_lambda_function" "process_payment" {  function_name = "${var.project_name}-process-payment"  role = aws_iam_role.lambda_role.arn  runtime = "python3.12"  handler = "process_payment.lambda_handler"  filename= data.archive_file.process_payment.output_path  source_code_hash = data.archive_file.process_payment.output_base64sha256  timeout = 30  environment { variables = {FAIL_MODE = "none" }  }  depends_on = [aws_cloudwatch_log_group.process_payment]}# ============================================================# SNS: エラー通知トピック + サブスクリプション# ============================================================resource "aws_sns_topic" "order_failure" {  name = "${var.project_name}-order-failure"}resource "aws_sns_topic_subscription" "email" {  topic_arn = aws_sns_topic.order_failure.arn  protocol  = "email"  endpoint  = var.notification_email}# ============================================================# Step Functions: ステートマシン# ============================================================resource "aws_sfn_state_machine" "order_pipeline" {  name  = "${var.project_name}-state-machine"  role_arn = aws_iam_role.sf_execution_role.arn  definition = templatefile("${path.module}/statemachine/definition.json.tpl", { validate_order_arn = aws_lambda_function.validate_order.arn reserve_inventory_arn = aws_lambda_function.reserve_inventory.arn process_payment_arn= aws_lambda_function.process_payment.arn sns_topic_arn= aws_sns_topic.order_failure.arn  })}

4-7. outputs.tf

output "state_machine_arn" {  description = "Step Functions ステートマシンの ARN"  value = aws_sfn_state_machine.order_pipeline.arn}output "state_machine_name" {  description = "Step Functions ステートマシン名"  value = aws_sfn_state_machine.order_pipeline.name}output "sns_topic_arn" {  description = "エラー通知 SNS トピックの ARN"  value = aws_sns_topic.order_failure.arn}output "validate_order_arn" {  description = "ValidateOrder Lambda の ARN"  value = aws_lambda_function.validate_order.arn}output "reserve_inventory_arn" {  description = "ReserveInventory Lambda の ARN"  value = aws_lambda_function.reserve_inventory.arn}output "process_payment_arn" {  description = "ProcessPayment Lambda の ARN"  value = aws_lambda_function.process_payment.arn}

4-8. デプロイ手順

ステップ 1: 初期化

cd sf-error-handlingterraform init

ステップ 2: 実行計画の確認

terraform plan -var="notification_email=your@email.com"

作成されるリソースを確認します。以下のリソースが生成される予定です:

  • aws_iam_role × 2(Lambda用 / SF用)
  • aws_iam_role_policy_attachment × 1
  • aws_iam_role_policy × 1
  • aws_cloudwatch_log_group × 3
  • aws_lambda_function × 3
  • aws_sns_topic × 1
  • aws_sns_topic_subscription × 1
  • aws_sfn_state_machine × 1

ステップ 3: 適用

terraform apply -var="notification_email=your@email.com"

yes と入力して適用します。

ステップ 4: SNS サブスクリプションの確認

notification_email に指定したメールアドレスに確認メールが届きます。
「Confirm subscription」リンクをクリックしてサブスクリプションを有効化してください。
これを忘れるとエラー通知メールが届きません。


4-9. 動作確認(CLIで実行)

正常系テスト

# ステートマシンARNを取得SM_ARN=$(terraform output -raw state_machine_arn)# 正常系実行aws stepfunctions start-execution \  --state-machine-arn "$SM_ARN" \  --input '{"order_id": "ORD-TF-001", "items": [{"id": "A", "qty": 1}], "total_amount": 3000}'

実行ARNを使って状態確認:

aws stepfunctions describe-execution \  --execution-arn <EXECUTION_ARN>

ステータスが SUCCEEDED になれば成功です。

異常系テスト: 注文検証エラー(OrderValidationError)

# ValidateOrder Lambda に FAIL_MODE=always を設定aws lambda update-function-configuration \  --function-name order-pipeline-validate-order \  --environment '{"Variables": {"FAIL_MODE": "always"}}'# 実行(NotifyFailure → OrderFailed へ遷移するはず)aws stepfunctions start-execution \  --state-machine-arn "$SM_ARN" \  --input '{"order_id": "ORD-TF-002", "items": [{"id": "A", "qty": 1}], "total_amount": 3000}'

異常系テスト: タイムアウト + 在庫ロールバック

# ProcessPayment Lambda に FAIL_MODE=timeout を設定aws lambda update-function-configuration \  --function-name order-pipeline-process-payment \  --environment '{"Variables": {"FAIL_MODE": "timeout"}}'# 実行(10秒でタイムアウト → RollbackInventory → NotifyFailure → OrderFailed)aws stepfunctions start-execution \  --state-machine-arn "$SM_ARN" \  --input '{"order_id": "ORD-TF-003", "items": [{"id": "A", "qty": 1}], "total_amount": 5000}'

ポイント: ASLの TimeoutSeconds: 10 がトリガーされ、States.Timeout エラーが発生します。
これにより RollbackInventory が実行され、在庫が元に戻された後、SNS通知が送信されます。

テスト後のクリーンアップ

# FAIL_MODE をリセットaws lambda update-function-configuration \  --function-name order-pipeline-validate-order \  --environment '{"Variables": {"FAIL_MODE": "none"}}'aws lambda update-function-configuration \  --function-name order-pipeline-process-payment \  --environment '{"Variables": {"FAIL_MODE": "none"}}'

リソースの削除

terraform destroy -var="notification_email=your@email.com"

コンソール版との対応: Section 3(コンソールハンズオン)のStep Dで手動作成したステートマシンと、本セクションのTerraformで作成したステートマシンは、ステート名・遷移・Retry/Catchパラメータ・TimeoutSecondsがすべて完全一致しています。IaCで再現性のある環境構築を実現しています。

コンソール・Terraformの両方で基本構成を習得した。次のセクションでは、本番環境でよく使われる応用パターンをASL例とともに解説する。


5. 実践パターン集(応用編)

この Section では、現場で使われる応用パターンを ASL 例とともに解説します。
ハンズオン手順は省略し、設計の考え方と実装例のみ紹介します。

5-1. サーキットブレーカーパターン

概念説明:

連続障害時に処理を自動停止し、依存サービスを守るパターンです。DynamoDB にエラーカウンターを持ち、閾値超過で全体を停止します。マイクロサービス間の障害伝播を防ぐために有効です。

実装方針:

  1. Choice ステート: DynamoDB からエラーカウントを読み取り閾値判定
  2. 閾値未満 → 通常処理継続
  3. 閾値以上 → 即座に CircuitOpen エラーで失敗

ASL例(概念):

{  "Comment": "サーキットブレーカーパターン(概念例)",  "StartAt": "CheckCircuitBreaker",  "States": { "CheckCircuitBreaker": {"Type": "Task","Resource": "<LAMBDA_ARN_CHECK_CIRCUIT>","Next": "RouteByCircuitState" }, "RouteByCircuitState": {"Type": "Choice","Choices": [  { "Variable": "$.circuit_open", "BooleanEquals": true, "Next": "CircuitOpenFail"  }],"Default": "ProcessRequest" }, "ProcessRequest": {"Type": "Task","Resource": "<LAMBDA_ARN_PROCESS>","Catch": [  { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "IncrementErrorCount"  }],"Next": "Success" }, "IncrementErrorCount": {"Type": "Task","Resource": "<LAMBDA_ARN_INCREMENT_ERROR>","Next": "ProcessFailed" }, "CircuitOpenFail": {"Type": "Fail","Error": "CircuitBreakerOpen","Cause": "サーキットブレーカーが開放状態です。しばらく待ってから再試行してください。" }, "Success": { "Type": "Succeed" }, "ProcessFailed": { "Type": "Fail", "Error": "ProcessingFailed" }  }}

ポイント:
CheckCircuitBreaker Lambda が DynamoDB のエラーカウントを読み取り $.circuit_open に設定する
– Circuit が Open の場合は即座に失敗させ、依存サービスへの余分なリクエストを遮断する
– エラー発生のたびに IncrementErrorCount で DynamoDB のカウンタをインクリメントする


5-2. Dead Letter Queue (DLQ) パターン

概念説明:

全リトライ消費後に失敗したメッセージを SQS DLQ に退避するパターンです。後からバッチ再処理や人手確認が可能になるため、重要なメッセージを取りこぼさずに運用できます。

実装方針:

  1. 全リトライ消費後に Catch(States.ALL) → SQS SendMessage ステートへ
  2. SQS SendMessage でエラー情報と元の入力データを DLQ に送信
  3. 別途 Lambda / 手動で DLQ からメッセージを取り出し再処理

ASL例(Catchと組み合わせ):

"ProcessTask": {  "Type": "Task",  "Resource": "<LAMBDA_ARN>",  "Retry": [ {"ErrorEquals": ["States.TaskFailed"],"IntervalSeconds": 5,"MaxAttempts": 3,"BackoffRate": 2.0 }  ],  "Catch": [ {"ErrorEquals": ["States.ALL"],"ResultPath": "$.error","Next": "SendToDLQ" }  ],  "Next": "Success"},"SendToDLQ": {  "Type": "Task",  "Resource": "arn:aws:states:::sqs:sendMessage",  "Parameters": { "QueueUrl": "<SQS_DLQ_URL>", "MessageBody.$": "States.JsonToString($)"  },  "Next": "MovedToDLQ"},"MovedToDLQ": {  "Type": "Fail",  "Error": "MovedToDLQ",  "Cause": "処理失敗。メッセージをDLQに退避しました。"}

ポイント:
States.JsonToString($) で現在の入力データ全体(エラー情報を含む)を文字列化して DLQ に送る
arn:aws:states:::sqs:sendMessage は SDK Integration — Lambda 不要で SQS を直接呼び出せる
– DLQ のメッセージを CloudWatch アラームで監視することで、異常を早期検知できる


5-3. Parallelステートのエラー伝播

概念説明:

Parallel ステート内の1つのブランチが失敗すると、他のブランチも中断されます。States.BranchFailed で Parallel 全体の失敗をキャッチできます。並列処理でのエラーハンドリングを設計する際に理解が必要な動作です。

ASL例:

"ParallelProcessing": {  "Type": "Parallel",  "Branches": [ {"StartAt": "TaskA","States": {  "TaskA": { "Type": "Task", "Resource": "<LAMBDA_ARN_A>", "End": true  }} }, {"StartAt": "TaskB","States": {  "TaskB": { "Type": "Task", "Resource": "<LAMBDA_ARN_B>", "End": true  }} }  ],  "Catch": [ {"ErrorEquals": ["States.BranchFailed"],"ResultPath": "$.error","Next": "HandleParallelFailure" }  ],  "Next": "AllSucceeded"}

重要ポイント:
– Parallel 内のブランチレベルで Retry/Catch を設定することも可能
– 全ブランチ成功時のみ Next ステートへ進む
– ブランチ単位でのエラーハンドリングを設計することで、1つのブランチ失敗が全体を止める問題を軽減できる


5-4. Map StateのToleratedFailurePercentage

概念説明:

Map ステートで大量アイテムを並列処理する際、一定割合の失敗を許容するパターンです。全アイテムの処理を完了させつつ、失敗率が閾値を超えた場合のみエラーとします。大規模バッチ処理で一部失敗を許容しながら全体を進めたい場合に有効です。

ASL例:

"ProcessItems": {  "Type": "Map",  "ItemsPath": "$.items",  "MaxConcurrency": 10,  "ToleratedFailurePercentage": 20,  "Iterator": { "StartAt": "ProcessSingleItem", "States": {"ProcessSingleItem": {  "Type": "Task",  "Resource": "<LAMBDA_ARN>",  "End": true} }  },  "Catch": [ {"ErrorEquals": ["States.ExceedToleratedFailureThreshold"],"ResultPath": "$.error","Next": "TooManyFailures" }  ],  "Next": "AllItemsProcessed"}

説明ポイント:

パラメータ説明
ToleratedFailurePercentage許容する失敗率(0〜100)。20 なら全アイテムの20%以内の失敗を許容
ToleratedFailureCount失敗許容数(絶対値で指定する場合に使用)
States.ExceedToleratedFailureThreshold閾値超過時に発生するエラー。Catch で捕捉可能

使いどころ: 数千件のデータを一括処理する際、ごく一部のアイテムが失敗しても処理全体を継続したい場合に適用します。失敗したアイテムは DLQ パターンと組み合わせて後処理することが多いです。


6. ハンズオン後の削除手順

6-1. コスト注意事項

⚠️ SNSトピックは月10万通まで無料。ただし、Lambda・CloudWatch Logsは課金が発生します。

リソース月額目安備考
Lambda無料枠100万リクエスト以内なら無料本ハンズオン程度では課金なし
SNS100万件まで無料本ハンズオン程度では課金なし
CloudWatch Logs~$0.50/GB(保存)+ ~$0.01/GB(取得)ロググループを削除で停止
Step Functions4,000回の状態遷移まで無料本ハンズオン程度では課金なし

本ハンズオンの範囲では課金がほぼ発生しませんが、CloudWatch Logs のロググループはログが蓄積し続けると保存料金が発生します。ハンズオン後は必ず削除してください。


6-2. Terraformで構築した場合

以下のコマンド1つで全リソースを削除できます。

# 全リソース削除terraform destroy -var="notification_email=your@email.com"

注意: terraform destroy の前に terraform plan -destroy を実行して削除対象を確認することを推奨します。

terraform destroy で削除されないもの(手動対応が必要):

  • CloudWatch Logs ロググループ(retain_on_delete を設定した場合)
  • Lambda 関数のバージョン/エイリアス(設定した場合)

手動削除が必要な場合:

# CloudWatch Logsロググループの手動削除aws logs delete-log-group --log-group-name /aws/lambda/validate_orderaws logs delete-log-group --log-group-name /aws/lambda/reserve_inventoryaws logs delete-log-group --log-group-name /aws/lambda/process_payment

6-3. コンソールで構築した場合の削除チェックリスト

コンソールで構築した場合は、以下の順番で各リソースを手動削除してください。

削除順序(依存関係に注意):

  • [ ] Step Functions ステートマシン(step-a / step-b / step-c / step-final の4つ)を削除
  • [ ] Lambda 関数(validate_order / reserve_inventory / process_payment の3つ)を削除
  • [ ] SNS トピックを削除(サブスクリプションも自動削除される)
  • [ ] IAM ロール(sf-order-execution-role)と付随するインラインポリシーを削除
  • [ ] CloudWatch Logs ロググループ(各Lambda名のもの: /aws/lambda/validate_order 等)を削除
  • [ ] 必要に応じて CloudWatch アラームも削除

ヒント: IAM ロールを先に削除すると Step Functions の削除時にエラーが出る場合があります。必ずステートマシン → Lambda → IAM ロールの順で削除してください。


7. まとめと次のステップ

7-1. この記事で学んだこと

本記事を通じて、以下のStep Functions エラーハンドリングの全体像を習得しました。

エラーの種類と識別:
States.TaskFailed — Lambda や ECS などのタスクが失敗した場合
States.Timeout — タスク全体のタイムアウト
States.HeartbeatTimeout — ハートビートタイムアウト(長時間タスク向け)
States.ALL — 全エラーを一括でキャッチするワイルドカード
States.BranchFailed — Parallel ステートのブランチ失敗
States.ExceedToleratedFailureThreshold — Map ステートの閾値超過

Retry の全パラメータ:
ErrorEquals — リトライ対象のエラー種別
IntervalSeconds — 初回リトライまでの待機秒数
MaxAttempts — 最大リトライ回数(0 でリトライ無効)
BackoffRate — リトライ間隔の倍増係数
JitterStrategy — 待機時間へのランダム揺らぎ(FULL / NONE

Catch の全パラメータ:
ErrorEquals — キャッチ対象のエラー種別
ResultPath — エラー情報の格納先($.error 等)
Next — エラー発生後の遷移先ステート

処理順序の理解:
– Retry → Catch の順で評価される(Retry を全消費してから Catch が評価される)
– Retry と Catch を組み合わせることで「自動回復 + 回復不能時の代替フロー」を実現できる

実践的なスキル:
– Step A → B → C → D の段階的構築でエラーハンドリングの効果を体感
FAIL_MODE 環境変数による障害注入テスト手法
– CloudWatch Logs / Metrics でのエラー監視(ログフィルターとアラーム設定)
– Terraform版とコンソール版のASLが完全に一致することの確認(インフラコードの信頼性担保)


7-2. 次のステップ

本シリーズで培った基礎をさらに発展させるため、以下のトピックを今後のシリーズで解説予定です。

トピック概要
Activity Task と .waitForTaskToken人間承認フローや外部システム連携を実現する非同期パターン
Express Workflow高頻度・短時間処理向けの実行モデル(Standard との使い分け)
EventBridge Pipes + Step Functionsイベント駆動アーキテクチャとのシームレスな連携
Step Functions Distributed Map数千〜数百万件の大規模並列バッチ処理
AWS SAM / CDK での Step Functions 定義IaC をより高レベルな抽象化で管理する方法

7-3. シリーズリンク

本記事は「AWS ハンズオン TechBlog」Step Functions シリーズの第3回です。

シリーズ一覧:
– 第1回: AWS Step Functions 入門 — コンソールとTerraformで学ぶハンズオン
– 第2回: ECS × Step Functions 入門 — CSVバッチをFargateタスクでジョブ化するハンズオン
– 第3回: 本記事(Step Functions エラーハンドリング完全ガイド)


7-4. 参考リンク

本記事の内容をより深く理解するために、以下の公式ドキュメントを参照してください。

  • Amazon States Language — エラー処理: ASL 仕様の Retry/Catch 定義の詳細
  • Step Functions Retry/Catch の設定: AWS 公式ドキュメントでの設定ガイド
  • Step Functions X-Ray との統合: 分散トレーシングによるエラー箇所の特定方法
  • AWS Lambda のエラー処理: Lambda 側でのエラーハンドリングと Step Functions との連携
  • CloudWatch アラームの設定: エラー検知のためのアラーム設定ガイド
最新情報をチェックしよう!