AWS Lambda関数を連続して実行すると、生成されるユニークなはずの値が重複してハマった話

2021.11.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、AWS Lambda関数を連続して実行すると、生成されるユニークなはずの値が重複してハマった話についてです。

実装

Lambdaコード

dayjsuuidを使用して、現在日時とUUIDを生成するLambda関数です。

src/lambda/handlers/sampleHandler.ts

import dayjs = require('dayjs');
import timezone = require('dayjs/plugin/timezone');
import utc = require('dayjs/plugin/utc');
import { v4 as uuidv4 } from 'uuid';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Asia/Tokyo');

const now = dayjs().tz();
const uuid = uuidv4();

export const handler = async (): Promise<{
  now: string;
  uuid: string;
}> => {
  return {
    now: now.format(),
    uuid: uuid,
  };
};

dayjsuuidはそれぞれnpmから導入可能です。

CDKコード

lib/aws-cdk-app-stack.ts

import * as cdk from '@aws-cdk/core';
import * as lambdaNodejs from '@aws-cdk/aws-lambda-nodejs';

export class AwsCdkAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new lambdaNodejs.NodejsFunction(this, 'myFunc', {
      functionName: 'myFunc',
      entry: './src/lambda/handlers/sampleHandler.ts',
      handler: 'handler',
    });
  }
}

cdk deployでLambda関数をデプロイします。

期待動作

実行毎に異なる日時と重複のないUUIDが取得できることです。

Lambdaを連続して実行すると同じ結果となる。

作成したLambda関数を立て続けに10秒程度の間隔で実行すると、現在時刻(now)とID(uuid)がいずれも実行間で重複してしまいました。

//1回目
{
  "now": "2021-11-23T23:33:01+09:00",
  "uuid": "86e5b4ec-d0bc-4012-bff0-bb14ae578cc5"
}	

//2回目
{
  "now": "2021-11-23T23:33:01+09:00",
  "uuid": "86e5b4ec-d0bc-4012-bff0-bb14ae578cc5"
}

原因、解決

原因は、Lambda関数のコードで、dayjsによる現在日時の生成と、uuidによるUUIDの生成の処理をハンドラーの外で行っていたためでした。

よって生成処理をハンドラーの中で行うようにコードを修正します。

src/lambda/handlers/sampleHandler.ts

import dayjs = require('dayjs');
import timezone = require('dayjs/plugin/timezone');
import utc = require('dayjs/plugin/utc');
import { v4 as uuidv4 } from 'uuid';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Asia/Tokyo');

export const handler = async (): Promise<{
  now: string;
  uuid: string;
}> => {
  const now = dayjs().tz();
  const uuid = uuidv4();

  return {
    now: now.format(),
    uuid: uuid,
  };
};

修正したLambdaをまた連続して実行してみると、今度は現在時刻とUUIDのいずれも実行毎に異なる値が取得できるようになりました。

//1回目
{
  "now": "2021-11-23T23:44:31+09:00",
  "uuid": "f5417143-c1f8-47e9-bd56-8bd31a50756a"
}

//2回目
{
  "now": "2021-11-23T23:44:44+09:00",
  "uuid": "3050eddc-d9ea-4aaf-be26-0c37494516dd"
}

なぜこのようなことが起こるのか

Lambda 実行環境のライフサイクルによると、実行環境のライフサイクルは以下の3つのフェーズに大きく分かれています。

  • 初期化フェーズ(Init):
    • 関数コードとすべてのレイヤーのダウンロード、拡張機能とランタイムの初期化、関数のメインハンドラーの外部にあるコードの実行の処理を実施する。
  • 呼び出しフェーズ(Invoke):
    • Lambda関数ハンドラーの呼び出しを行う。
    • 呼び出し後にもしばらく実行環境を維持し、再呼び出しされたら実行環境を再利用する。
  • シャットダウンフェーズ(Shutdown):
    • Lambda関数が一定期間実行されない場合に実施される。
    • ランタイムをシャットダウンし、環境を削除する。

つまり、一定期間内に関数が再呼び出しされたら、ハンドラーの外部にあるコード実行は再利用される動作となるようです。これが今回ハマった事象の根本原因でした。

おわりに

AWS Lambda関数を連続して実行すると、生成されるユニークなはずの値が重複してハマった話についてでした。

Lambda関数のコードを実装する際は、再利用されて良い処理と良くない処理を意識しながら実装をするようにしなければ、再現の難しそうなバグを埋め込むことになるので、気をつけようと思いました。

以上