「Step Functionsのワークフローが途中で止まったとき、どこから再開するのかがわからない。」
サーバーレスアーキテクチャは「サーバーを管理しなくていい」というメリットがある一方、エラーが起きたときの挙動が見えづらく、冗長設計が後回しになりがちです。
この記事では、AWS Lambda・Step Functions・SQSを組み合わせたサーバーレス構成で、エラー処理と冗長設計をどう組み立てるかを解説します。サービス単体のコマンド解説ではなく、複数サービスを組み合わせたアーキテクチャ設計層の話として、冗長性の確保とリトライ設計の考え方を体系的に整理します。想定環境はAWS東京リージョン(ap-northeast-1)のVPC統合Lambdaを前提としています。
この記事のポイント
・Step FunctionsのRetryにJitterStrategy: FULLを加えると分散リトライになる
・Catchで全エラーを拾い、エラー内容をResultPathに保存して後続処理へ渡せる
・Lambda関数はべき等性設計(DynamoDB条件付き書き込み)で二重実行を防ぐ
・VPC Lambdaは複数AZのサブネットを指定することでマルチAZ冗長になる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
サーバーレスアーキテクチャで冗長設計が必要になる3つの理由
「Lambdaはマネージドだから、AWSが面倒を見てくれるはず」と思っている人は少なくありません。確かにインフラ自体の冗長性はAWSが担保していますが、アプリケーション層のエラー処理まで自動でやってくれるわけではありません。サーバーレス構成で冗長設計が必要になる主な原因は3つあります。
・サービス側の一時障害:Lambda・DynamoDB・外部API等、下流サービスが一時的に不安定になることがあります。リトライ設計がなければワークフロー全体が失敗します
・タイムアウトと同時実行制限:Lambda関数はデフォルト最大実行時間15分ですが、アカウント単位・関数単位の同時実行数上限(デフォルト1,000)に達すると新規呼び出しが絞られます
・部分失敗の見えにくさ:複数のLambda関数を直列・並列で組み合わせたとき、どのステップで何が起きたかをStep Functionsの実行履歴なしに追いにくくなります
これらを踏まえて、Step Functions・Lambda・SQSの3層でエラー処理と冗長設計を組み立てる方法を解説します。
Step FunctionsのRetryとCatchでエラー処理を設計する方法
Step Functionsのステートマシン定義(ASL: Amazon States Language)では、各Taskステートに`Retry`と`Catch`を設定できます。この2つを正しく組み合わせることが、サーバーレスの冗長設計の中心になります。1. Retry(リトライ)の設定と指数バックオフ
Lambdaが一時的なサービス例外で失敗したとき、Step Functionsに自動リトライさせるにはタスク定義の`Retry`配列に条件を書きます。Lambdaサービス例外に対してリトライを設定するASLの例です。
# Step Functionsステートマシン定義(ASL形式・一部抜粋) { "Comment": "Lambda失敗時のリトライ設定例", "StartAt": "ProcessData", "States": { "ProcessData": { "Type": "Task", "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:ProcessDataFn", "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException", "Lambda.TooManyRequestsException" ], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0, "JitterStrategy": "FULL" } ], "End": true } } }
・IntervalSeconds:初回リトライまでの待機秒数(上記では2秒)
・MaxAttempts:最大リトライ回数。0にするとリトライなし
・BackoffRate:待機秒数の乗数。2.0なら2秒→4秒→8秒と倍増
・JitterStrategy: FULL:バックオフ時間をランダムに分散させる機能。同じワークフローが多数同時にリトライする場合、一斉リトライによる下流サービスへの集中負荷を分散できます
`Lambda.TooManyRequestsException`はスロットリングエラーです。このエラーはリトライで解消できる場合が多いため、必ずリトライ対象に含めてください。
2. Catch(エラーキャッチ)でフォールバック処理を設計する
`MaxAttempts`を使い切っても失敗した場合、または`Retry`に含まれていないエラーが起きた場合は`Catch`で捕捉します。# CatchでエラーハンドリングStateへ遷移する設定例(ASL抜粋) "ProcessData": { "Type": "Task", "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:ProcessDataFn", "Retry": [ { "ErrorEquals": ["Lambda.TooManyRequestsException"], "IntervalSeconds": 3, "MaxAttempts": 5, "BackoffRate": 2.0, "JitterStrategy": "FULL" } ], "Catch": [ { "ErrorEquals": ["States.TaskFailed"], "Next": "NotifyFailure", "ResultPath": "$.error" }, { "ErrorEquals": ["States.ALL"], "Next": "NotifyFailure", "ResultPath": "$.error" } ], "Next": "SaveResult" }
`States.ALL`はすべてのエラーにマッチするキャッチオールです。`States.TaskFailed`はLambda等のタスクが失敗したことを示すStep Functions組み込みエラーです。特定エラーを先に書き、最後に`States.ALL`を置くのが基本パターンです。
3. Dead Letter Queue(DLQ)との組み合わせで保全を高める
Step Functionsのエラー処理と並行して、Lambda関数レベルのDLQ(デッドレタキュー)設定も重要です。特に非同期呼び出し(イベント駆動)を使う場合、Lambda側のリトライでも失敗したメッセージをSQSのDLQに保存しておくことで、後から再処理できます。DLQを設定するaws lambdaコマンドの例です。
# Lambda関数の失敗時送信先にSQS DLQを設定する aws lambda update-function-event-invoke-config \ --function-name ProcessDataFn \ --destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:ap-northeast-1:123456789012:ProcessDataDLQ"}}' \ --region ap-northeast-1 # DLQに蓄積されたメッセージ数を確認する aws sqs get-queue-attributes \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/ProcessDataDLQ \ --attribute-names ApproximateNumberOfMessages \ --region ap-northeast-1
{ "Attributes": { "ApproximateNumberOfMessages": "0" } }
Lambda関数のべき等性設計でリトライを安全にする
リトライ設計を機能させるためには、Lambda関数側が「べき等(同じリクエストを複数回処理しても1回と同じ結果になる)」であることが前提です。べき等でない関数は、リトライのたびに二重処理・重複レコードを生みます。よく使われる手法がDynamoDBの条件付き書き込みを使った重複排除です。
# DynamoDBに実行IDをキーとして書き込む(Python Lambda内での処理パターン) # PK に Step Functions の executionId を使い、attribute_not_exists でべき等性を確保する import boto3 from botocore.exceptions import ClientError dynamodb = boto3.resource('dynamodb') table = dynamodb.Table('ProcessedJobs') def lambda_handler(event, context): execution_id = event.get('executionId') try: # attribute_not_exists(id) の条件:idが存在しなければ書き込む(重複なら失敗) table.put_item( Item={ 'id': execution_id, 'status': 'processing', }, ConditionExpression='attribute_not_exists(id)' ) except ClientError as e: if e.response['Error']['Code'] == 'ConditionalCheckFailedException': # 同じexecutionIdで既に処理済み → スキップして正常終了 return {'status': 'already_processed', 'executionId': execution_id} raise # ここから実際のビジネスロジックを実行する result = do_business_logic(event) table.update_item( Key={'id': execution_id}, UpdateExpression='SET #s = :done', ExpressionAttributeNames={'#s': 'status'}, ExpressionAttributeValues={':done': 'completed'} ) return result
AWSのサーバーレスアーキテクチャとLinuxサーバー管理を体系的に学びたい場合は、AWS(Amazon Linux)入門からサーバー構築まで解説している記事もあわせて参考にしてください。
VPC LambdaのマルチAZ冗長設計
プライベートサブネット内のリソース(RDSやElastiCache等)にLambdaからアクセスするために、「VPC Lambda」(Lambda関数をVPCに統合する設定)を使うケースがあります。このとき、AZ障害への耐性を確保するには複数のAZのサブネットをLambdaの設定に含めることが重要です。VPC Lambdaで複数サブネットを指定するCLIコマンドの例です。
# Lambda関数のVPC設定に複数AZのサブネットを指定する(東京リージョン 1a/1b/1c) aws lambda update-function-configuration \ --function-name ProcessDataFn \ --vpc-config SubnetIds=subnet-aaaaaa01,subnet-bbbbbb02,subnet-cccccc03,SecurityGroupIds=sg-xxxxxxxx \ --region ap-northeast-1
・subnet-bbbbbb02:ap-northeast-1b のプライベートサブネット
・subnet-cccccc03:ap-northeast-1c のプライベートサブネット
サブネットを1つだけ指定した場合、そのAZが障害になるとLambda関数が起動できなくなります。東京リージョンでは3AZを指定しておくのが標準的です。また、各サブネットのNATゲートウェイや、エンドポイント経由の場合はVPCエンドポイントも各AZに配置しておく必要があります。
サーバーレス構成の全体像を整理すると、LambdaとStep Functions自体はリージョンスコープのマネージドサービスであり単一AZ障害の影響を受けません。マルチAZ設計が必要になるのは主に次の部分です。
・VPC Lambda(プライベートサブネット統合)のサブネット配置
・Lambda関数から接続するRDS/ElastiCacheの配置AZ
・SQS DLQのメッセージ保持設定(リージョンサービスのためAZ設定不要だが、保持期間の設計は必要)
トラブルシュート:よくあるエラーと対処
「Lambda.TooManyRequestsException」が頻発する
同時実行数の上限に達しているか、特定Lambdaにリクエストが集中しているケースです。対処は2つあります。・予約済み同時実行数(Reserved Concurrency)を関数に設定し、重要なワークフロー用のLambdaには一定の実行枠を確保する
・プロビジョニング済み同時実行数(Provisioned Concurrency)でコールドスタートを減らし、急増リクエストへの応答速度を上げる
# Lambdaの予約済み同時実行数を100に設定する aws lambda put-function-concurrency \ --function-name ProcessDataFn \ --reserved-concurrent-executions 100 \ --region ap-northeast-1
Step Functionsの実行が「RUNNING」のまま止まる
Step FunctionsのタスクがwaitForTaskTokenパターンを使っている場合、タスクトークンのコールバックが届かずに待ち続けることがあります。`HeartbeatSeconds`を設定してタイムアウトを強制させましょう。# waitForTaskTokenパターンに HeartbeatSeconds を設定する(ASL抜粋) "WaitForExternalSystem": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken", "Parameters": { "FunctionName": "ExternalCallFn", "Payload": { "taskToken.$": "$$.Task.Token", "input.$": "$" } }, "HeartbeatSeconds": 300, "TimeoutSeconds": 3600, "Retry": [ { "ErrorEquals": ["States.HeartbeatTimeout"], "IntervalSeconds": 10, "MaxAttempts": 2 } ], "Catch": [ { "ErrorEquals": ["States.HeartbeatTimeout", "States.Timeout"], "Next": "HandleTimeout" } ] }
DLQにメッセージが蓄積し続ける
DLQはあくまで「後で処理するための保全」です。蓄積し続ける場合はアプリケーション側に根本的な問題がある可能性が高いため、コードを修正してからメッセージを再処理(Dead-letter queue redrive)する流れになります。CloudWatchのメトリクス`ApproximateNumberOfMessagesNotVisible`も確認し、処理中で詰まっていないかも合わせて見てください。本記事のまとめ
| 設計要素 | 推奨設定・パターン |
|---|---|
| Step Functions Retry | JitterStrategy: FULL付き指数バックオフ、MaxAttempts 3~5 |
| Step Functions Catch | 特定エラーを先に書き、最後にStates.ALLで全捕捉、ResultPathでエラー情報を保持 |
| DLQ(Dead Letter Queue) | Lambda非同期呼び出しにSQS DLQを設定して再処理に備える |
| べき等性設計 | DynamoDB条件付き書き込み(attribute_not_exists)で二重処理を防ぐ |
| VPC LambdaのマルチAZ | 東京リージョンなら1a/1b/1cの3サブネットをSubnetIdsに指定する |
| 同時実行数対策 | Reserved Concurrencyで重要ワークフローの実行枠を確保する |
>> AWSでのLinuxサーバー構築を学ぶ
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:AWS RAGアーキテクチャ設計入門|BedrockとOpenSearch Serverlessを使ったVPC構成
- この記事の属するカテゴリ:AWS(Amazon Linux)へ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます