AWS CDK + TypeScript + Lambdaを使用してKMSで暗号化した値を取得し復号する

## はじめに
[AWS CDK](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html) + TypeScript + Lambdaを使用してKMSで暗号化された値を取得して復号する記事があまり見当たらなかったので、備忘録として残しておきます。

## 前提
こちらの記事は「AWS CLI」、「AWS CDK」のインストールを前提としており、私はそれぞれ以下のバージョンを使用しています。
- AWS CLI・・・2.7.21
- AWS CDK Toolkit・・・2.63.1

OSについてはM1 macOS Montereyです。

また、パラメータストアで任意のSecureStringパラメータが設定されているものとします。

## AWS CDKでLambda関数を作成する
まずディレクトリを作成し、プロジェクトを作成していきます。
```bash
$ mkdir kms && cd kms
```
```bash
$ cdk init app --language typescript
```
以下のファイルが作成されているのが確認できるかと思います。
```
.
├── README.md
├── bin
├── cdk.json
├── jest.config.js
├── lib
├── node_modules
├── package-lock.json
├── package.json
├── test
└── tsconfig.json
```

次に、lambdaディレクトリを作成し、lambda関数を作成していきます。

```bash
$ mkdir lambda && cd lambda
```
```
$ touch index.ts
```
サンプル関数として、`kms`という文字列を出力する処理を書いておきます。
```typescript:index.ts
exports.handler = async function () {
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/plain" },
    body: "kms",
  };
}
```
`lib/kms-stack.ts`に移り、既存のコードを削除したのち、今作成した`index.ts`を使用するコードに変更します。

```bash
$ cd ../lib/kms-stack.ts
```

```diff_typescript:kms-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
- // import * as sqs from 'aws-cdk-lib/aws-sqs';
+ import * as lambda from "aws-cdk-lib/aws-lambda";

export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
-    // The code that defines your stack goes here
-
-    // example resource
-    // const queue = new sqs.Queue(this, 'SampleQueue', {
-    //   visibilityTimeout: cdk.Duration.seconds(300)
-    // });
+    const lambdaFunction = new lambda.Function(this, "IndexHandler", {
+      runtime: lambda.Runtime.NODEJS_16_X, // ランタイム
+      code: lambda.Code.fromAsset("lambda"), // lambdaディレクトリからコードを読み込む
+      handler: "index.handler", // ハンドラ。ここではlambda/index.tsのファイル名を指す
+    });
  }
}

```

ここまで来たら、関数のデプロイをしていきます。
まずはcdk bootstrapコマンドで[AWSデプロイ中に使用される特定のリソースをプロビジョニング](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping.html)します。
```bash
$ cdk bootstrap
```

TSファイルをコンパイルしたのち、デプロイします。

```bash
$ npm run build

$ cdk deploy
```

試しに関数のテストをしてみます。
![スクリーンショット 0005-03-12 21.28.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/a64dbdba-8d3b-ef2e-7fea-9bf3976f6ded.png)


無事Lambda関数が実行されました🎉

## KMSで暗号化された値を取得する
KMSで暗号化された値を取得するにはいくつかやり方がありますが、今回は「[Lambda 拡張機能](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ps-integration-lambda-extensions.html)」を使用してみます。

まず`lib/kms-stack.ts`を編集します。

```diff_typescript:kms-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
+ import { PolicyStatement } from 'aws-cdk-lib/aws-iam';

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

+    const layer = lambda.LayerVersion.fromLayerVersionArn(
+      this,
+      'layer',
+      'arn:aws:lambda:ap-northeast-1:133490724326:layer:AWS-Parameters-and-Secrets-Lambda-Extension:2',
+    );

    const lambdaFunction = new lambda.Function(this, 'IndexHandler', {
      runtime: lambda.Runtime.NODEJS_16_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'index.handler',
+     layers: [layer],
    });

+    lambdaFunction.addToRolePolicy(
+      new PolicyStatement({
+        resources: [
+          `arn:aws:kms:{リージョン}:{アカウントID}:key/{KMSのキーID}`,
+          `arn:aws:ssm:{リージョン}:{アカウントID}:parameter/{パラメータストアのパラメータの名前}`,
+        ],
+        actions: ['kms:Decrypt', 'ssm:GetParametersByPath', 'ssm:GetParameter'],
+      })
+    );
  }
}
```

まず`layer`を定義します。
以下でAWS Parameters and Secrets Lambda ExtensionのARNを指定しています。
AWS Parameters and Secrets Lambda ExtensionのARNについては[こちら](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ps-integration-lambda-extensions.html)を参照。
```typescript:kms-stack.ts
const layer = lambda.LayerVersion.fromLayerVersionArn(
  this,
  'layer',
  'arn:aws:lambda:ap-northeast-1:133490724326:layer:AWS-Parameters-and-Secrets-Lambda-Extension:2',
);
```

次にLambda関数にレイヤーを追加します。
```typescript:kms-stack.ts
const lambdaFunction = new lambda.Function(this, 'IndexHandler', {
  runtime: lambda.Runtime.NODEJS_16_X,
  code: lambda.Code.fromAsset('lambda'),
  handler: 'index.handler',
  layers: [layer],
});
```

また、暗号化された値を取得し復号するためのポリシーをIAMロールにアタッチします。
```typescript:kms-stack.ts
lambdaFunction.addToRolePolicy(
  new PolicyStatement({
    resources: [
      `arn:aws:kms:{リージョン}:{アカウントID}:key/{KMSのキーID}`,
      `arn:aws:ssm:{リージョン}:{アカウントID}:parameter/{パラメータストアのパラメータの名前}`,
    ],
    actions: ['kms:Decrypt', 'ssm:GetParametersByPath', 'ssm:GetParameter'],
  })
);
```

:::note warn
簡単のためリソース等をハードコーディングしていますが本来は環境変数等で扱うべきです。
:::

`lambda/index.ts`に移ります。
暗号化された値を取得するのに`localhost`にリクエスト必要があるため、`lambda`ディレクトリで`axios`をインストールしておきます。
```bash:lambda/
$ npm init -y
$ npm i axios
```
`lambda/index.ts`で実際に暗号化された値を取得する処理を追加します。
```diff_typescript:index.ts
+ import axios from 'axios';

exports.handler = async function () {
-  return {
-    statusCode: 200,
-    headers: { "Content-Type": "text/plain" },
-    body: "kms",
-  };
+  const options = {
+    headers: {
+      'X-Aws-Parameters-Secrets-Token': process.env.AWS_SESSION_TOKEN,
+    },
+    method: 'GET',
+  }

+  const endpoint = encodeURI('http://localhost:2773/systemsmanager/parameters/get/?name={パラメータストアのパラメータの名前}&withDecryption=true');
+
+  try {
+    const res = await axios.get(endpoint, options);
+    
+     return {
+       statusCode: 200,
+       headers: { "Content-Type": "text/plain" },
+       body: res.data.Parameter.Value,
+     };
+  } catch (error) {
+    console.log(error);
+  }
}
```

基本的に[ドキュメント](基本的にドキュメントに記載されていますが、)に記載されていますが、いくつかポイントを抑えておきます。

- ヘッダー`にX-Aws-Parameters-Secrets-Token`を設定
> 拡張キャッシュからパラメータを取得するには、GET リクエストのヘッダーに X-Aws-Parameters-Secrets-Token 参照を含める必要があります。トークンを AWS_SESSION_TOKEN に設定します。
https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ps-integration-lambda-extensions.html

```diff_typescript:kms-stack.ts
+  const options = {
+    headers: {
+      'X-Aws-Parameters-Secrets-Token': process.env.AWS_SESSION_TOKEN,
+    },
+    method: 'GET',
+  }
```

- localhostの2773ポートにGETリクエスト
> GET リクエストに localhost を使用します。拡張機能は、localhost ポート 2773 に送信されます。
- パラメータストアのパラメータ名はurlエンコード
> GET 呼び出しを使用する場合、特殊文字を保存するために、パラメータ値を HTTP 用にエンコードする必要があります。
- **withDecryptionをtrueに設定**
:::note warn
withDecryptionをtrueに設定することに関しては明示的に設定する必要がある旨の記載がありませんが、こちらのパラメータがないと値を復号できません。
:::
```diff_typescript:kms-stack.ts
+  const endpoint = encodeURI('http://localhost:2773/systemsmanager/parameters/get/?name={パラメータストアのパラメータの名前}&withDecryption=true');
```

それではデプロイしてみます。

```bash
$ npm run build

$ cdk deploy
```

:::note info
今回SecureStringの値を「暗号化された値」という文字列にしています。
:::

関数のテストを実行

![スクリーンショット 0005-03-12 21.39.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/d96045fd-ccea-515c-163e-4fa295d2d60e.png)

無事暗号化された値が取得できました🎉

個人的にlocalhostにリクエストを送る際にwithDecryptionをtrueにしないといけないという点がハマりポイントでした（ドキュメントは隅々まで見ないといけないですね）

## 最後に
GoQSystemでは一緒に働いてくれる仲間を募集中です！
ご興味がある方は以下リンクよりご確認ください。

https://goqsystem.recruit.site?qiita=0e251485210d2d2577fb