API Gatewayのカスタムエラーレスポンスを使ってLambdaの500エラーを制御してみた
こんにちは!製造ビジネステクノロジー部の小林です。
API Gatewayのエラーレスポンスをもっと分かりやすくしたいと思ったことはありませんか?
前回の記事では、API Gatewayが返す403エラーのカスタマイズ方法をご紹介しました。
今回はその続編として、バックエンドのLambdaが引き起こす500エラーの制御を行ってみます。具体的には、「メモリ超過」と「同時実行数制限」という2つのエラーを意図的に発生させ、API Gatewayでどのようにハンドリングできるのか見ていきます。
この記事で構築する構成
API Gateway (REST API)
Lambda Functions (TypeScript)
- 意図的にメモリを使い果たしてクラッシュする関数
- 実行数を1に制限し、スロットリングを誘発する関数
API Gateway Custom Response
上記のどちらのエラーが発生した場合も、HTTPステータス500で、以下のJSONを返す。
{
"returncode": 50000,
"message": "サーバー内部エラー"
}
前提
AWS CDKを利用してインフラを構築します。
ディレクトリ構造は以下のようになります。
error-handling/
├── bin/
│ └── error-handling.ts
├── lib/
│ └── error-handling-stack.ts
└── lambda-code/
├── memory-test
│ └── index.ts
└── concurrent-test.ts
└── index.ts
やってみた
AWS CDKスタックの定義
lib/ ディレクトリにあるスタック定義ファイルにAPI GatewayとLambdaリソースを定義します。
lib/error-handling-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as path from 'path';
export class ErrorHandlingStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// メモリ超過エラー用Lambdaの定義
const memoryErrorFunction = new NodejsFunction(this, 'MemoryErrorFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'handler',
entry: path.join(__dirname, 'lambda-code/memory-test/index.ts'),
memorySize: 128, // メモリサイズを最小の128MBに設定し、エラーを発生しやすくする
timeout: cdk.Duration.seconds(15),
bundling: {
minify: true,
externalModules: ['aws-sdk'],
},
});
// 同時実行数エラー用Lambdaの定義
const concurrencyErrorFunction = new NodejsFunction(this, 'ConcurrencyErrorFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'handler',
entry: path.join(__dirname, 'lambda-code/concurrency-test/index.ts'),
timeout: cdk.Duration.seconds(15),
/**
* Lambdaが同時に実行できるインスタンス数を1に制限する
* これにより、1つが実行中に別のリクエストが来るとスロットリングが発生する
*/
reservedConcurrentExecutions: 1,
});
// API Gatewayの定義
const api = new apigw.RestApi(this, 'MyApi', {
restApiName: 'ErrorHandlingApi',
});
/**
* Lambdaの実行時エラー(メモリ超過、タイムアウトなど)や、
* API Gatewayの統合エラー(Lambdaのスロットリングなど)をカスタムレスポンスで処理する
* DEFAULT_5XXは、それらのサーバー側エラー全般をキャッチする
*/
api.addGatewayResponse('Custom500Response', {
type: apigw.ResponseType.DEFAULT_5XX,
statusCode: '500',
responseHeaders: {
'Content-Type': "'application/json'",
'Access-Control-Allow-Origin': "'*'",
},
// レスポンスボディをここでカスタマイズする
templates: {
'application/json': JSON.stringify({
statusCode: 500,
message: 'サーバー内部エラー',
}),
},
});
const memoryErrorIntegration = new apigw.LambdaIntegration(memoryErrorFunction);
api.root.addResource('memory-test').addMethod('GET', memoryErrorIntegration);
const concurrencyErrorIntegration = new apigw.LambdaIntegration(concurrencyErrorFunction);
api.root.addResource('concurrency-test').addMethod('GET', concurrencyErrorIntegration);
}
}
メモリ超過エラーを発生させるLambda
この関数は、設定されたメモリ上限(今回は128MB)を超えるまで意図的にメモリを確保し続け、"Out of Memory" (OOM) エラーを発生させます。正常なレスポンスを返すことはありません。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
/**
* メモリ超過エラーを意図的に発生させるLambdaハンドラ
* 正常に終了することはない(常にタイムアウトかメモリ超過エラーになる)
*/
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const memory = [];
try {
// 無限ループでメモリを確保し続ける
// AWS Lambdaの実行環境がメモリ上限を超えた時点で強制終了される
while (true) {
// 1MBの文字列の塊を配列に追加していく
memory.push(Buffer.alloc(1024 * 1024, 'X'));
}
} catch (error) {
console.error('予期せぬエラー:', error);
return {
statusCode: 500,
body: JSON.stringify({ message: '予期せぬエラー' }),
};
}
};
同時実行数エラーを発生させるLambda
この関数は10秒間スリープするだけのシンプルな処理です。CDK側でこのLambdaの同時実行数を1に制限するため、最初の実行が完了する前に2回目のリクエストを送ると、Lambdaサービスがリクエストをスロットリングし、API Gatewayにエラーを返します。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
/**
* 10秒間スリープするだけのシンプルなLambdaハンドラ
* CDK側で同時実行数を1に制限することで、スロットリングエラーを誘発する
*/
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
// 10秒待機する
await new Promise(resolve => setTimeout(resolve, 10000));
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'successfully !' }),
};
};
以上で準備は完了です。デプロイ後、動作確認をしてみましょう!
動作確認
メモリ超過エラーの確認
curlコマンドで /memory-test エンドポイントにアクセスします。
# URLはご自身の環境のものに置き換えてください
curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/memory-test
結果
数秒後、CDKで定義したカスタムエラーレスポンスが返ってきます。
{
"returncode": 50002,
"message": "サーバー内部エラー"
}
CloudWatch LogsでMemoryErrorFunctionのログを見ると、以下のようなエラーが記録されており、計画通りにメモリ超過が発生したことが確認できます。
同時実行数エラーの確認
ターミナルを2つ使い、/concurrency-test エンドポイントにほぼ同時にリクエストを送ります。
ターミナル1: 1回目のリクエストを送ります。処理に10秒かかります。
curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/concurrency-test
ターミナル2: すぐに2回目のリクエストを送ります。
curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/concurrency-test
結果
ターミナル1(画面左)では、10秒後にLambdaからの正常なレスポンスが返ります。
ターミナル2(画面右)では、即座にカスタムエラーレスポンスが返ってきます。
まとめ
今回は、AWS CDKを用いてAPI Gatewayのカスタムエラーレスポンスを設定し、バックエンドLambdaで発生するエラーを一元的にハンドリングする方法を実践してみました!
この方法を使えば、Lambda関数側でtry-catchを多用しなくても、メモリ超過やスロットリングといった予期せぬエラーを一元管理できますね。
この記事が、API Gatewayのカスタマイズを検討されている方にとって参考になれば幸いです。