もう二度と 10日間で AWS Lambda 関数を 28億回 実行させない
私はかつて以下のようなミスを犯しました。
10日間 で AWS Lambda 関数を 28億回 実行した話
無限に自分自身を呼び出すバグをLambda関数に入れ込んでしまい、10日間それに気づかずに放置し、最終的に28億回もLambda関数が実行される結果となってしまいました。数字のインパクトが大きいせいか、このミスは社内で何度も擦られ続けています。
現在のAWS Lambdaには「再帰ループ検出」という素晴らしい機能があります。まさにわたしが犯してしまったミスを検出できる機能になります。
今回は「再帰ループ検出機能」がどの程度まで検出できるのかを検証してみました。具体的には、再帰ループが起こるアーキテクチャを3パターン用意し、再帰ループ検出機能で検出できるかどうかを検証しました。
サポートされているAWSのサービス
2024/11現在、再帰ループ検出機能がサポートされているAWSのサービスは、以下のとおりです。
- Amazon SQS
- Amazon S3
- Amazon SNS
再帰ループ検出機能は、これらのAWSサービスを含んだ再帰ループを検出できるとしています。今回の検証で採用したアーキテクチャでは、これらのAWSサービスで構成しています。
再帰ループ通知の確認方法
再帰ループ検出機能により、Lambdaが再帰ループを停止すると、 AWS Health Dashboardやメールで通知が届きます。今回の検証では、AWS Health Dashboardの通知を確認しました。以下のように「ECLambda runaway termination notification」というイベントが通知されていることを確認しています。
検証パターン
検証パターンは、以下の3つです。
- パターン1: Lambda関数の再帰ループ
- パターン2: 複数のLambdaとバケットで構成されたループ
- パターン3: SNS/SQSとLambda並列実行で構成されたループ
それぞれのパターンで、再帰ループ検出機能がループを検出できるか検証しました。以下、それぞれのパターンの構成と検出結果を解説していきます。
パターン1: Lambda関数の再帰ループ
構成
このパターンでは、Lambda関数単体で構成されているアーキテクチャとなります。Lambda関数が自分自身を呼び出します。これは、私が犯したミスの状況とほぼ同じパターンとなっています。
構成図は、以下のとおりです。
Lambda関数のコードは、以下のとおりです。
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
const lambda = new LambdaClient({});
export const handler = async (event) => {
console.log("Lambda executed with event:", event);
// 自分自身を呼び出す
await lambda.send(new InvokeCommand({
FunctionName: "SelfCallingLambda",
InvocationType: "Event", // 非同期呼び出し
Payload: JSON.stringify({ triggeredBy: "self" }),
}));
console.log("Self-invocation triggered");
return { statusCode: 200, body: "Self-invoked!" };
};
これをAWS上にデプロイし、Lambda関数を実行しました。
検証結果
結果的に、再帰ループが検出され、自分自身を呼び出すLambda関数が停止することを確認できました。最も単純なパターンであるため、当然の結果となりました。あの時にこの機能があったらどんなに良かったでしょうか。
パターン2: 複数のLambdaとバケットで構成されたループ
構成
このパターンでは、複数のLambdaとバケットで処理を連鎖させていくアーキテクチャとなります。具体的には、10個のLambda関数と10個のバケットをつなげてループさせています。
構成図は、以下のとおりです。
10個目のバケットの発火先を1個目のLambda関数に指定することにより、ループさせています。このように、ループの中に多くのサービスが含まれている場合でも、検出できるかどうかを検証しました。
この構成のAWS CDK実装は、以下のとおりです。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3Notifications from 'aws-cdk-lib/aws-s3-notifications';
export class S3LoopDynamicStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// バケットとLambdaのペア数
const numBuckets = 10;
const buckets: s3.Bucket[] = [];
const lambdas: lambda.Function[] = [];
// バケットを作成
for (let i = 0; i < numBuckets; i++) {
const bucket = new s3.Bucket(this, `Bucket${i}`, {
bucketName: `s3-loop-bucket-${i}`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
buckets.push(bucket);
}
// Lambdaを作成
for (let i = 0; i < numBuckets; i++) {
const lambdaFn = new cdk.aws_lambda_nodejs.NodejsFunction(this, `Lambda${i}`, {
functionName: `LoopLambda${i}`,
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
entry: 'funcs/loop-s3/index.js',
environment: {
BUCKET_NAME: buckets[(i + 1) % numBuckets].bucketName,
}
});
lambdas.push(lambdaFn);
}
// イベント通知の設定
for (let i = 0; i < numBuckets; i++) {
buckets[i].addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3Notifications.LambdaDestination(lambdas[i])
);
buckets[i].grantReadWrite(lambdas[(i + numBuckets - 1) % numBuckets]);
}
}
}
Lambda関数の実装は、S3バケットにダミーのファイルをアップロードする処理になっています。1個目のLambda関数を実行し、ループを発生させます。
検証結果
結果的に、再帰ループが検出され、ループが停止することを確認できました。1つ目のバケットを確認してみると、22個のオブジェクトが作られていました。
ループの中に多くのサービスが含まれている場合でも、検出できることが確認できました。
パターン3: SNS/SQSとLambda並列実行で構成されたループ
構成
このパターンでは、サポートされているサービスをすべて含んでいるループになります。また、Lambdaを並列実行するようにしています。具体的には、SNSのサブスクライバーに2つのLambda関数を指定してます。
構成図は、以下のとおりです。
このように、ループの中に複数ルートがある場合でも、検出できるかどうかを検証しました。
この構成のAWS CDK実装は、以下のとおりです。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as s3Notifications from 'aws-cdk-lib/aws-s3-notifications';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { SqsEventSource, SnsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
export class LoopS3SnsSqsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// S3バケットの作成
const bucket = new s3.Bucket(this, 'BucketFirst', {
bucketName: 'loop-sqs-sns-bucket',
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// S3バケットにイベント通知を追加(S3 -> SNS)
const topic = new sns.Topic(this, 'TopicFirst', {
displayName: 'S3LoopTopic',
});
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3Notifications.SnsDestination(topic)
);
// SQSトピック
const queue = new sqs.Queue(this, 'QueueFirst', {
queueName: 'S3LoopQueue',
});
// Lambda -> SQS
for (let i = 0; i < 2; i++) {
const lambdaFn = new cdk.aws_lambda_nodejs.NodejsFunction(this, `Lambda${i}`, {
functionName: `WorkerLambda${i}`,
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
entry: 'funcs/sns-worker/index.js',
environment: {
BUCKET_NAME: bucket.bucketName,
TOPIC_ARN: topic.topicArn,
QUEUE_URL: queue.queueUrl,
},
});
queue.grantSendMessages(lambdaFn);
topic.grantPublish(lambdaFn);
lambdaFn.addEventSource(new SnsEventSource(topic));
}
// SQS -> Lambda
const consumeLambda = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'SubscriberLambda', {
functionName: 'ConsumeLambda',
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
entry: 'funcs/sqs-consume/index.js',
environment: {
BUCKET_NAME: bucket.bucketName,
QUEUE_URL: queue.queueUrl,
},
});
bucket.grantReadWrite(consumeLambda);
queue.grantConsumeMessages(consumeLambda);
consumeLambda.addEventSource(new SqsEventSource(queue));
}
}
S3バケットにダミーのファイルを置くLambda関数を実行し、ループを発生させます。
検証結果
結果的に、再帰ループが検出されず、ループが停止しませんでした。バケットを確認してみると、オブジェクトが作られ続けていってしまっていました。再帰ループが検出されずに停止しなかったので、手動でLambdaのトリガーを削除することによって、ループを停止させました。
サポートされているサービスのみで構成されている場合でも、再帰ループが検出されないパターンがあるということですね。
おわりに
今回は「再帰ループ検出機能」の検証を、3パターンのアーキテクチャを用いて検証してみました。結果としては、サポートされているサービスのみで構成されている場合でも、検出できない構成パターンがある、ということがわかりました。
今回見てきたとおり、検出できるサービスやパターンには限りがあるため、注意が必要です。再帰ループ検出機能を過信せず、従来通りにAWS請求アラームを設定するなどして、複数の対策を施しておいたほうが良いでしょう。少しでも再帰ループ発生の可能性を低くしていきましょう。