AWS

もう二度と 10日間で AWS Lambda 関数を 28億回 実行させない

内山浩佑

私はかつて以下のようなミスを犯しました。

10日間 で AWS Lambda 関数を 28億回 実行した話

無限に自分自身を呼び出すバグをLambda関数に入れ込んでしまい、10日間それに気づかずに放置し、最終的に28億回もLambda関数が実行される結果となってしまいました。数字のインパクトが大きいせいか、このミスは社内で何度も擦られ続けています。

現在のAWS Lambdaには「再帰ループ検出」という素晴らしい機能があります。まさにわたしが犯してしまったミスを検出できる機能になります。

Lambda 再帰ループ検出を使用した無限ループの防止

今回は「再帰ループ検出機能」がどの程度まで検出できるのかを検証してみました。具体的には、再帰ループが起こるアーキテクチャを3パターン用意し、再帰ループ検出機能で検出できるかどうかを検証しました。

サポートされているAWSのサービス

2024/11現在、再帰ループ検出機能がサポートされているAWSのサービスは、以下のとおりです。

  • Amazon SQS
  • Amazon S3
  • Amazon SNS

サポートされている AWS のサービス および SDK

再帰ループ検出機能は、これらの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請求アラームを設定するなどして、複数の対策を施しておいたほうが良いでしょう。少しでも再帰ループ発生の可能性を低くしていきましょう。

AUTHOR
uchiko
uchiko
記事URLをコピーしました