Lambda（Node.js）で Secrets Manager を使用する

[AWS Parameters and Secrets Lambda Extension](https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets_lambda.html)を使用して、Lambdaからシークレットマネージャーの値を取得します。
シークレットがキャッシュされるので、SDKで取得するより高速かつコスト削減になります。

## AWS CDK
```typescript:lib/sample-stack.ts
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNodeJs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as sm from 'aws-cdk-lib/aws-secretsmanager';

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

        // Secrets Manager
        const secret = this.createSampleSecret();

        // 既存のシークレットを取得する場合はfromSecretNameV2などを使用する
        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_secretsmanager.Secret.html#static-fromwbrsecretwbrnamewbrv2scope-id-secretname
        // const secret = sm.Secret.fromSecretNameV2(scope, 'Secrets',  '/sample/login');

        // Lambda
        const lambda = this.createFunc();
        secret.grantRead(lambda);
    }

    /**
     * @description シークレットを作成
     * {@link class Secret | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_secretsmanager.Secret.html}
     */
    private createSampleSecret(): sm.ISecret {
        return new sm.Secret(this, 'Secret', {
            secretName: '/sample/login',
            description: 'シークレットのサンプル',
            secretObjectValue: {
                clientID: cdk.SecretValue.unsafePlainText('dummy'),
                clientSecret: cdk.SecretValue.unsafePlainText('dummy'),
                publicKeyID: cdk.SecretValue.unsafePlainText('dummy'),
                privateKey: cdk.SecretValue.unsafePlainText('dummy'),
                passphrase: cdk.SecretValue.unsafePlainText('dummy'),
                enterpriseID: cdk.SecretValue.unsafePlainText('dummy'),
            },
        });
    }

    /**
     * @description Lambdaの作成
     * {@link ParamsAndSecretsLayerVersion | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.ParamsAndSecretsLayerVersion.html}
     * {@link NodejsFunction | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html}
     */
    private createFunc(): lambdaNodeJs.NodejsFunction {
        // AWS Parameters and Secrets Lambda Extension の作成
        const paramsAndSecrets = lambda.ParamsAndSecretsLayerVersion.fromVersion(
            lambda.ParamsAndSecretsVersions.V1_0_103,
            {
                cacheSize: 500,
                logLevel: lambda.ParamsAndSecretsLogLevel.DEBUG,
            }
        );

        return new lambdaNodeJs.NodejsFunction(this, 'SampleFunc', {
            entry: 'src/lambda/sample/index.ts',
            handler: 'handler',
            runtime: lambda.Runtime.NODEJS_18_X,
            timeout: cdk.Duration.minutes(5),
            paramsAndSecrets,
        });
    }
}
```

### 補足
LambdaにVPCを設定する場合は、シークレットにアクセスするためにVPCエンドポイントが必要です。

```typescript
// VPCエンドポイント
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.InterfaceVpcEndpoint.html
new ec2.InterfaceVpcEndpoint(this, 'VpcEndpointToSecretsManager', {
    service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
    vpc: vpc,
    subnets: vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_ISOLATED }),
});
```

## Lambda
Lambda関数でシークレットの値を取得するにはHTTP GET リクエストを行います。
エンドポイントのポート番号とセッショントークンは[AWS Parameters and Secrets Lambda Extensionの環境変数](https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets_lambda.html#retrieving-secrets_lambda_env-var)で取得します。
シークレットのキャッシュはデフォルトで300秒保持されます。環境変数の`SECRETS_MANAGER_TTL`で変更可能です。

```console
% npm install axios
```

```typescript:src/lambda/sample/index.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import axios from 'axios';

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

interface LoginSecret {
    clientID: string;
    clientSecret: string;
    publicKeyID: string;
    privateKey: string;
    passphrase: string;
}

/**
 * {@link AWS Lambda 関数で AWS Secrets Manager シークレットを使用する | https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets_lambda.html}
 * {@link axios | https://www.npmjs.com/package/axios}
 */
export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
    // シークレットから値を取得する
    const smUri = `http://localhost:${PARAMETERS_SECRETS_EXTENSION_HTTP_PORT}/secretsmanager/get`;
    const res = await axios.get(smUri, {
        params: { secretId: '/sample/login' },
        headers: { 'X-Aws-Parameters-Secrets-Token': AWS_SESSION_TOKEN },
    });
    const secret = JSON.parse(res.data.SecretString) as LoginSecret;

    console.debug(`clientID: ${secret.clientID}`);
    console.debug(`clientSecret: ${secret.clientSecret}`);
    console.debug(`publicKeyID: ${secret.publicKeyID}`);
    console.debug(`privateKey: ${secret.privateKey}`);

    return {
        statusCode: 200,
        body: '',
    };
}
```

## テスト
<details><summary>test/sample.test.ts</summary>

```typescript:test/sample.test.ts
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { SampleStack } from '../lib/sample-stack';

test('SM', () => {
    const app = new cdk.App();
    const stack = new SampleStack(app, 'SampleStack');
    const template = Template.fromStack(stack);
    console.debug(JSON.stringify(template));

    // Secrets Manager
    template.resourceCountIs('AWS::SecretsManager::Secret', 1);
    template.hasResourceProperties('AWS::SecretsManager::Secret', {
        Description: 'シークレットのサンプル',
        Name: '/sample/login',
        SecretString:
            '{"clientID":"client-id-dummy","clientSecret":"client-secret-dummy","publicKeyID":"public-key-id-dummy","privateKey":"private-key-dummy","passphrase":"passphrase-dummy"}',
    });
});

test('Lambda', () => {
    const app = new cdk.App();
    const stack = new SampleStack(app, 'SampleStack');
    const template = Template.fromStack(stack);
    console.debug(JSON.stringify(template));

    // Lambda
    template.resourceCountIs('AWS::Lambda::Function', 1);
    template.hasResourceProperties('AWS::Lambda::Function', {
        Runtime: 'nodejs18.x',
        Timeout: 300,
    });
});

```

</details>

## 参考
- AWS CDK
    - [AWS CDK Intro Workshop](https://catalog.us-east-1.prod.workshops.aws/workshops/99731164-1d19-4d2e-9319-727a130e2d57/ja-JP)
    - [API Reference · AWS CDK](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html)
- SecretｓManager
    - [class Secret (construct) · AWS CDK](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_secretsmanager.Secret.html)
    - [AWS Lambda 関数で AWS Secrets Manager シークレットを使用する - AWS Secrets Manager](https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets_lambda.html)