Powertools for AWS Lambda (TypeScript) を使ってパラメーターの取得およびキャッシュの実装が簡単にできるようになりました

2023.06.30

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

Powertools for AWS Lambda (TypeScript) は、サーバーレス開発のベストプラクティスを容易に実装可能にするツールキットです。

先日リリースされたその v1.11.0 で、パラメーターユティリティが追加され、パラメーターおよびシークレットの取得およびキャッシュ(TTL)の実装が簡単にできるようになりました

対応しているのは次のサービスのパラメーターおよびシークレットです。

  • AWS Systems Manager Parameter Store
  • AWS Secrets Manager
  • AWS AppConfig
  • Amazon DynamoDB
  • your own parameter store

試してみた

今回は、AWS Systems Manager Parameter Store のパラメーターの取得およびキャッシュの実装を試してみます。

インストール

モジュールをインストールします。

npm install @aws-lambda-powertools/parameters @aws-sdk/client-ssm

検証環境の作成

検証環境として、Lambda 関数およびパラメーターストアを AWS CDK で作成します。Lambda 関数にはパラメーターストアの読み取り権限を付与します。

lib/cdk-sample-stack.ts

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

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

    // パラメーターストア
    const myParameter1 = new aws_ssm.StringParameter(this, 'MyParameter1', {
      parameterName: '/my/parameter1',
      stringValue: 'myValue',
    });
    const myParameter2 = new aws_ssm.StringParameter(this, 'MyParameter2', {
      parameterName: '/my/parameter2',
      stringValue: '{"key": "myValue"}',
    });
    const myParameter3 = new aws_ssm.StringParameter(this, 'MyParameter3', {
      parameterName: '/my/parameter3',
      stringValue: 'myValue',
    });
    const myParameter4 =
      aws_ssm.StringParameter.fromSecureStringParameterAttributes(
        this,
        'MyParameter4',
        {
          parameterName: '/my/parameter4',
        }
      );

    // Lambda 関数
    const myFunction = new aws_lambda_nodejs.NodejsFunction(
      this,
      'MyFunction',
      {
        functionName: 'myFunction',
        architecture: aws_lambda.Architecture.ARM_64,
        runtime: aws_lambda.Runtime.NODEJS_20_X,
      }
    );

    // 読み取り権限付与
    myParameter1.grantRead(myFunction);
    myParameter2.grantRead(myFunction);
    myParameter3.grantRead(myFunction);
    myParameter4.grantRead(myFunction);
  }
}

セキュアストリングのパラメーターは CDK で作成できないので、CLI で別途作成します。

aws ssm put-parameter \
    --name '/my/parameter4' \
    --value 'myValue' \
    --type SecureString

getParameter() による単一のパラメーター取得

getParameter() を使用すると、単一のパラメーターを取得できます。既定では 5 秒のキャッシュ期間(TTL)が設定されます。

Lambda 関数のコードは次のようになります。SSMClient の初期化などよしなにやってくれます。

lib/cdk-sample-stack.MyFunction.ts

import { getParameter } from '@aws-lambda-powertools/parameters/ssm';

export const handler = async (): Promise<string> => {
  // パラメーターの取得
  const parameter = await getParameter('/my/parameter1');
  return parameter || '';
};

関数を実行すると、パラメーターの値が取得できました。値のみを返すので取得結果のパースは不要です。

$ aws lambda invoke --function-name myFunction response.json
$ cat response.json
"myValue"

TTL をカスタマイズする場合は、maxAge オプションを指定します。下記では 10 秒の TTL を設定しています。

lib/cdk-sample-stack.MyFunction.ts

import { getParameter } from '@aws-lambda-powertools/parameters/ssm';

export const handler = async (): Promise<string> => {
  // パラメーターの取得
  const parameter = await getParameter('/my/parameter1', { maxAge: 10 });
  return parameter || '';
};

逆にキャッシュを使わずに常に最新の値を取得したい場合は、forceFetch オプションを指定します。

lib/cdk-sample-stack.MyFunction.ts

import { getParameter } from '@aws-lambda-powertools/parameters/ssm';

export const handler = async (): Promise<string> => {
  // パラメーターの取得
  const parameter = await getParameter('/my/parameter1', { forceFetch: true });
  return parameter || '';
};

このように getParameter を使用すると、AWS SDK を直接使う場合に比べ、パラメーターの取得およびキャッシュの実装が簡単にできます。

getParametersByName() による複数パラメーターの取得

getParametersByName() を使用すると、複数のパラメーターを 1 度に取得することが可能です。

Lambda 関数のコードは次のようになります。パラメーターごとに TTL を設定できます。下記では /my/parameter1 は 0 秒、/my/parameter2 は 300 秒、/my/parameter3 はデフォルト値の 60 秒の TTL を設定しています。

lib/cdk-sample-stack.MyFunction.ts

import { Transform } from '@aws-lambda-powertools/parameters';
import { getParametersByName } from '@aws-lambda-powertools/parameters/ssm';
import type { SSMGetParametersByNameOptions } from '@aws-lambda-powertools/parameters/ssm/types';

const props: Record<string, SSMGetParametersByNameOptions> = {
  '/my/parameter1': {
    maxAge: 0,
  },
  '/my/parameter2': {
    maxAge: 300,
    transform: Transform.JSON, // JSON パース
  },
  '/my/parameter3': {}, // undefined の場合はデフォルト値が使われる
};

export const handler = async (): Promise<any> => {
  // パラメーターの取得
  const parameters = await getParametersByName(props, { maxAge: 60 });
  return parameters;
};

ここでは実際にキャッシュの動作を確認してみます。

初回取得

関数を実行すると、パラメーターの値が取得できました。getParametersByName() では次のようにパラメーター名をキーとしたオブジェクトが返されます。

$ aws lambda invoke --function-name myFunction response.json && cat response.json | jq .
{
  "/my/parameter1": "myValue",
  "/my/parameter2": {
    "key": "myValue"
  },
  "/my/parameter3": "myValue"
}

キャッシュヒット

初回取得の直後にパラメーターストアの値を更新します。

aws ssm put-parameter \
    --name '/my/parameter1' \
    --value 'myValue_hoge' \
    --overwrite
aws ssm put-parameter \
    --name "/my/parameter2" \
    --value '{"Key": "myValue_hoge"}' \
    --overwrite
aws ssm put-parameter \
    --name '/my/parameter3' \
    --value 'myValue_hoge' \
    --overwrite

立て続けに関数を実行して、パラメーターを取得します。TTL が 0 秒の /my/parameter のみ更新後の値、その他の 2 つは更新前の値が取得されました。

$ aws lambda invoke --function-name myFunction response.json && cat response.json | jq .
{
  "/my/parameter2": {
    "key": "myValue"
  },
  "/my/parameter3": "myValue",
  "/my/parameter1": "myValue_hoge"
}

さらに 1 分ほど経過後に関数を実行すると、TTL が経過した /my/parameter も更新後の値が取得されました。

$ aws lambda invoke --function-name myFunction response.json && cat response.json | jq .
{
  "/my/parameter2": {
    "key": "myValue"
  },
  "/my/parameter1": "myValue_hoge",
  "/my/parameter3": "myValue_hoge"
}

キャッシュが働いていることを確認できました。

暗号化されたパラメーター取得

decrypt オプションを指定して暗号化されたパラメーターを取得することもできます。

getParameter を使う場合の Lambda 関数のコードは次のようになります。

lib/cdk-sample-stack.MyFunction.ts

import { getParameter } from '@aws-lambda-powertools/parameters/ssm';

export const handler = async (): Promise<string> => {
  // 暗号化されたパラメーターの取得
  const parameter = await getParameter('/my/parameter4', { decrypt: true });
  return parameter || '';
};

復号されたパラメーター値を取得できました。

$ aws lambda invoke --function-name myFunction response.json && cat response.json
"myValue"

getParametersByName を使う場合の Lambda 関数のコードは次のようになります。

lib/cdk-sample-stack.MyFunction.ts

import { getParametersByName } from '@aws-lambda-powertools/parameters/ssm';
import type { SSMGetParametersByNameOptions } from '@aws-lambda-powertools/parameters/ssm/types';

const props: Record<string, SSMGetParametersByNameOptions> = {
  '/my/parameter4': { maxAge: 300 },
};

export const handler = async (): Promise<any> => {
  // 暗号化されたパラメーターの取得
  const parameters = await getParametersByName(props, { decrypt: true });
  return parameters;
};

復号されたパラメーター値を取得できました。

$ aws lambda invoke --function-name myFunction response.json && cat response.json
{"/my/parameter4":"myValue"}

おわりに

Powertools for AWS Lambda (TypeScript) v1.11.0 で、パラメーターユティリティが追加され、パラメーターおよびシークレットの取得およびキャッシュの実装が簡単にできるようになったので試してみました。

今までは Lambda extension を使って HTTP サーバーを立てることによりキャッシュを実現する AWS Parameter and Secrets Lambda extension や、グローバル変数を使った自前実装をするという方法がありましたが、それらに比べてとてもシンプルな実装を実現することができました。

今後は TypeScript 環境においてはキャッシュ実装のファーストチョイスにしても良いのではないでしょうか。

参考

以上