AWS CDK v2.84.0 で AWS Parameter and Secrets Lambda extension を L2 Construct で構築可能になりました

2023.06.15

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

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

AWS CDK の v2.84.0 で、 AWS Parameter and Secrets Lambda extension の L2 Construct の提供がサポートされました。

今回は、実際に AWS CDK で AWS Parameter and Secrets Lambda extension を構築して動作を確認してみました。

AWS Parameter and Secrets Lambda extension とは

AWS Parameter and Secrets Lambda extension を構成すると、パラメーターストアおよびシークレットマネージャーの値を Lambda 関数から取得する際に、キャッシュレイヤーを利用することができるようになります。

これによって、値を取得する際の API コールを減らすことができ、コスト削減につながります。
Using the AWS Parameter and Secrets Lambda extension to cache parameters and secrets より

ソースコードの変更を見てみる

Pull Request で実装の変更を見ると、Parameters and Secrets Extension の layer version の実装が追加されています。

packages/aws-cdk-lib/aws-lambda/lib/params-and-secrets-layers.ts

/**
 * Parameters and Secrets Extension layer version
 */
export abstract class ParamsAndSecretsLayerVersion {
  /**
   * Use the Parameters and Secrets Extension associated with the provided ARN. Make sure the ARN is associated
   * with the same region and architecture as your function.
   *
   * @see https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html#retrieving-secrets_lambda_ARNs
   */
  public static fromVersionArn(arn: string, options: ParamsAndSecretsOptions = {}): ParamsAndSecretsLayerVersion {
    return new (class extends ParamsAndSecretsLayerVersion {
      public _bind(_scope: Construct, _fn: IFunction): ParamsAndSecretsBindConfig {
        return {
          arn,
          environmentVars: this.environmentVariablesFromOptions,
        };
      }
    })(options);
  }

  // 中略

実装

実際に実装を行います。Lambda 関数および AWS CDK スタックは TypeScript で実装します。

CDK ライブラリのアップグレード

CDK ライブラリを最新バージョンにアップグレードします。

npm install aws-cdk aws-cdk-lib

アップグレードされました。

package.json

{
  "devDependencies": {
-    "aws-cdk": "^2.77.0",
+    "aws-cdk": "^2.84.0",
  },
  "dependencies": {
-    "aws-cdk-lib": "^2.77.0",
+    "aws-cdk-lib": "^2.84.0",
  }
}

Lambda 関数

パラメーターストアおよびシークレットマネージャーから、値を取得する Lambda 関数を作成します。

lib/cdk-sample-stack.sampleFunc.ts

import axios from 'axios';

const AWS_SESSION_TOKEN = process.env['AWS_SESSION_TOKEN'] || '';

export const handler = async (): Promise<any> => {
  const getParameterResponse = await axios.get(
    'http://localhost:2773/systemsmanager/parameters/get',
    {
      params: {
        name: encodeURIComponent('/my/parameter'), // "/" の URI エンコードが必要
      },
      headers: {
        'X-Aws-Parameters-Secrets-Token': AWS_SESSION_TOKEN,
      },
    }
  );

  const getSecretResponse = await axios.get(
    'http://localhost:2773/secretsmanager/get',
    {
      params: {
        secretId: '/my/secret',
      },
      headers: {
        'X-Aws-Parameters-Secrets-Token': AWS_SESSION_TOKEN,
      },
    }
  );

  return {
    parameter: getParameterResponse.data.Parameter,
    secret: getSecretResponse.data,
  };
};

CDK スタック

AWS CDK で、AWS Parameters and Secrets Lambda Extension を設定した Lambda 関数を作成します。

lib/cdk-sample-stack.ts

import {
  aws_lambda_nodejs,
  aws_lambda,
  aws_ssm,
  aws_secretsmanager,
  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 parameter = new aws_ssm.StringParameter(this, 'Parameter', {
      parameterName: '/my/parameter',
      stringValue: 'mySsmParameterValue',
    });

    // シークレットの作成
    const secret = new aws_secretsmanager.Secret(this, 'Secret', {
      secretName: '/my/secret',
      generateSecretString: {
        secretStringTemplate: JSON.stringify({
          username: 'myUsername',
        }),
        generateStringKey: 'password',
      },
    });

    // AWS Parameters and Secrets Lambda Extension の作成
    const paramsAndSecrets =
      aws_lambda.ParamsAndSecretsLayerVersion.fromVersion(
        aws_lambda.ParamsAndSecretsVersions.V1_0_103,
        {
          cacheSize: 500,
          logLevel: aws_lambda.ParamsAndSecretsLogLevel.DEBUG,
        }
      );

    // Lambda 関数の作成
    const nodejsFunction = new aws_lambda_nodejs.NodejsFunction(
      this,
      'sampleFunc',
      {
        functionName: 'sampleFunc',
        runtime: aws_lambda.Runtime.NODEJS_18_X,
        architecture: aws_lambda.Architecture.ARM_64,
        paramsAndSecrets, // AWS Parameters and Secrets Lambda Extension を Lambda 関数に追加
      }
    );

    parameter.grantRead(nodejsFunction);
    secret.grantRead(nodejsFunction);
  }
}

動作確認

キャッシュヒットしない場合は現在のパラメーターおよびシークレットの値が取得され、キャッシュヒットした場合はキャッシュが使用されることを確認してみます。

Lambda 関数初回実行

まずはじめに、Lambda 関数を実行すると、パラメーターおよびシークレットの値が取得されています。

$ aws lambda invoke --function-name sampleFunc response.json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

$ cat response.json | jq .
{
  "parameter": {
    "ARN": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my/parameter",
    "DataType": "text",
    "LastModifiedDate": "2023-06-15T15:21:41.077Z",
    "Name": "/my/parameter",
    "Selector": null,
    "SourceResult": null,
    "Type": "String",
    "Value": "mySsmParameterValue",
    "Version": 1
  },
  "secret": {
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:XXXXXXXXXXXX:secret:/my/secret-v5FM84",
    "CreatedDate": "2023-06-15T15:28:32.671Z",
    "Name": "/my/secret",
    "SecretBinary": null,
    "SecretString": "{\"password\":\"yL214g'6RB7;<iu}e0k/*}n\\\\?y~1Ks{_\",\"username\":\"myUsername\"}",
    "VersionId": "c9a8da7d-1613-02f1-f5d9-b7ff1e366b5a",
    "VersionStages": ["AWSCURRENT"],
    "ResultMetadata": {}
  }
}

キャッシュヒットした場合

パラメーターおよびシークレットの値を更新します。

aws ssm put-parameter \
    --name "/my/parameter" \
    --value "mySsmParameterValue2" \
    --overwrite

aws secretsmanager put-secret-value \
    --secret-id "/my/secret" \
    --secret-string "{\"password\":\"myPassword\",\"username\":\"myUsername\"}"

再度 Lambda 関数を実行すると、キャッシュヒットが発生し、更新前の値が取得されています。

$ aws lambda invoke --function-name sampleFunc response.json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

$ cat response.json | jq .
{
  "parameter": {
    "ARN": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my/parameter",
    "DataType": "text",
    "LastModifiedDate": "2023-06-15T15:21:41.077Z",
    "Name": "/my/parameter",
    "Selector": null,
    "SourceResult": null,
    "Type": "String",
    "Value": "mySsmParameterValue",
    "Version": 1
  },
  "secret": {
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:XXXXXXXXXXXX:secret:/my/secret-v5FM84",
    "CreatedDate": "2023-06-15T15:28:32.671Z",
    "Name": "/my/secret",
    "SecretBinary": null,
    "SecretString": "{\"password\":\"yL214g'6RB7;<iu}e0k/*}n\\\\?y~1Ks{_\",\"username\":\"myUsername\"}",
    "VersionId": "c9a8da7d-1613-02f1-f5d9-b7ff1e366b5a",
    "VersionStages": [
      "AWSCURRENT"
    ],
    "ResultMetadata": {}
  }
}

キャッシュヒットしない場合

Lambda 関数を更新してキャッシュを削除します。

aws lambda update-function-configuration \
  --function-name sampleFunc \
  --description "hoge"

再度 Lambda 関数を実行すると、キャッシュヒットが発生せず、更新後の値が取得されています。

$ aws lambda invoke --function-name sampleFunc response.json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

$ cat response.json | jq .
{
  "parameter": {
    "ARN": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my/parameter",
    "DataType": "text",
    "LastModifiedDate": "2023-06-15T15:54:08.891Z",
    "Name": "/my/parameter",
    "Selector": null,
    "SourceResult": null,
    "Type": "String",
    "Value": "mySsmParameterValue2",
    "Version": 2
  },
  "secret": {
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:XXXXXXXXXXXX:secret:/my/secret-v5FM84",
    "CreatedDate": "2023-06-15T15:54:16.869Z",
    "Name": "/my/secret",
    "SecretBinary": null,
    "SecretString": "{\"password\":\"myPassword\",\"username\":\"myUsername\"}",
    "VersionId": "becaebc0-af98-4857-abc1-3735f04c868d",
    "VersionStages": [
      "AWSCURRENT"
    ],
    "ResultMetadata": {}
  }
}

Extension を利用して開発効率を向上させよう

次の記事の通り、パフォーマンス観点では本 Extension を使うよりも自前実装による Lambda 関数ランタイム本体のメモリ上へのキャッシュの方が優れている傾向にあります。

ただし、本 Extension を使うことでロジックのテストやメンテナンスは不要となるため、開発効率の向上は期待できます。また今回の L2 Construct で利用できるクラスの提供により、さらに利用が簡単になったことでしょう。是非、活用してみてください。

参考

以上