[AWS Lambda] Parameter Storeから取得したパラメータをグローバル変数でキャッシュする

2023.02.10

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

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

AWS Lambda関数でAWS Systems Manager Parameter Storeからパラメーターを取得して処理に使用するケースはよくあるのではないでしょうか。

しかしParameter Storeのパラメーター値が頻繁に変更されないにも関わらずLambda関数が頻繁に実行される場合、Parameter StoreのAPIを叩くコストが必要以上に大きくなってしまいます。(API インタラクション 1 万回ごとに 0.05 USD)またスループットにも気を配る必要も出てくるでしょう。(デフォルトで40 TPS, Transactions Per Second

そこで上記問題を解決するために、AWS Lambda関数がParameter Storeから取得したパラメータをグローバル変数でキャッシュして、呼び出し間で再利用する方法を確認してみました。

Lambda関数のウォームスタート

Lambda関数の実行環境の起動のされ方には「コールドスタート」と「ウォームスタート」の2つがあります。

ウォームスタートでは前回実行時の環境が再利用されるため、コールドスタートに比べて起動が迅速化されます。今回はそのウォームスタートのhandler外に記述されたグローバル変数は再利用される仕様を利用してキャッシュを行います。

ドキュメントには下記のような実装例があったため参考にしてみます。


記事より引用

やってみた

実装サンプル

次のような実装で実現をしました。

  • handler外でグローバル変数(HOGE)を定義
  • グローバル変数に値がキャッシュされている場合はリストアして使用
  • 値がキャッシュされていない場合はParameter Storeから取得
  • Parameter Storeから取得した値をグローバル変数にキャッシュ

lib/cdk-sample-app.nyaoFunc.ts

import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';

let HOGE: string | undefined;  // グローバル変数を定義

export const ssmClient = new SSMClient({
  region: 'ap-northeast-1',
});

export const handler = async (): Promise<string> => {
  if (HOGE !== undefined) {
    return HOGE; // キャッシュがある場合はリストア
  }

  const result = await ssmClient.send(
    new GetParameterCommand({ Name: 'hoge' })
  );

  if (result.Parameter !== undefined && result.Parameter!.Value !== undefined) {
    const hoge = result.Parameter.Value;

    HOGE = hoge; // Parameter Storeから取得した値をグローバル変数にキャッシュ

    return hoge;
  }

  HOGE = undefined;

  throw new Error();
};

AWS CDKを使用してLambda関数をデプロイします。

lib/cdk-sample-app.ts

import { Construct } from 'constructs';
import { Stack, StackProps, aws_lambda_nodejs, aws_ssm } from 'aws-cdk-lib';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const hogeParameter = aws_ssm.StringParameter.fromStringParameterName(
      this,
      'hogeParameter',
      'hoge'
    );

    const nyaoFunc = new aws_lambda_nodejs.NodejsFunction(this, 'nyaoFunc');

    hogeParameter.grantRead(nyaoFunc);
  }
}

動作確認

Jestのテストコードで動作確認をしてみます。

test/cdk-sample-app.nyaoFunc.test.ts

import {
  InvocationType,
  InvokeCommand,
  UpdateFunctionConfigurationCommand,
  LambdaClient,
} from '@aws-sdk/client-lambda';
import { PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm';

const FUNCTION_NAME = 'CdkSampleStack-nyaoFunc9AC2D57B-wCluRtORQwjY';
const PARAMETER_NAME = 'hoge';

const lambdaClient = new LambdaClient({
  region: 'ap-northeast-1',
});
const ssmClient = new SSMClient({ region: 'ap-northeast-1' });

beforeAll(async (): Promise<void> => {
  //コールドスタートを強制;
  await lambdaClient.send(
    new UpdateFunctionConfigurationCommand({
      FunctionName: FUNCTION_NAME,
      Description: new Date().toString(),
    })
  );
});

test('コールドスタート', async (): Promise<void> => {
  await ssmClient.send(
    new PutParameterCommand({
      Name: PARAMETER_NAME,
      Value: '1stVal',
      Overwrite: true,
    })
  );

  // Parameter Store反映待機
  await new Promise((r) => setTimeout(r, 3000));

  const lambdaResponse = await lambdaClient.send(
    new InvokeCommand({
      FunctionName: FUNCTION_NAME,
      InvocationType: InvocationType.RequestResponse,
    })
  );

  expect(lambdaResponse.Payload).not.toBeUndefined();

  if (lambdaResponse.Payload) {
    expect(JSON.parse(Buffer.from(lambdaResponse.Payload).toString())).toBe(
      '1stVal' // ParameterStoreに格納されている値が取得されている
    );
  }
});

test('ウォームスタート', async (): Promise<void> => {
  await ssmClient.send(
    new PutParameterCommand({
      Name: PARAMETER_NAME,
      Value: '2ndVal', //1回目のテスト(コールドスタート)時と異なる値をParameterStoreに格納
      Overwrite: true,
    })
  );

  // Parameter Store反映待機
  await new Promise((r) => setTimeout(r, 3000));

  const lambdaResponse = await lambdaClient.send(
    new InvokeCommand({
      FunctionName: FUNCTION_NAME,
      InvocationType: InvocationType.RequestResponse,
    })
  );

  expect(lambdaResponse.Payload).not.toBeUndefined();

  if (lambdaResponse.Payload) {
    expect(JSON.parse(Buffer.from(lambdaResponse.Payload).toString())).toBe(
      '1stVal' // ParameterStoreに格納されている値ではなく、キャッシュされた値が取得されている
    );
  }
});

実行すると、Lambda関数の呼び出し間でグローバル変数によるキャッシュが行われていることが確認できました。

$ npx jest
 PASS  test/cdk-sample-app.nyaoFunc.test.ts (10.534 s)
  ✓ コールドスタート (4172 ms)
  ✓ ウォームスタート (3130 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        10.591 s, estimated 11 s
Ran all test suites.

おわりに

AWS Lambda関数がParameter Storeから取得したパラメータをグローバル変数でキャッシュして、呼び出し間で再利用できるようにしてみました。

別解としてはLambda Extensionsを使った方法であれはTTLなど柔軟な設定ができます。しかし今回の方法の方が数行のコードを追加するだけでキャッシュを実現できるので比較的簡単です。

お好きな方をどうぞ。

以上