はじめに
AWS CDK + 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関数を作成する
まずディレクトリを作成し、プロジェクトを作成していきます。
$ mkdir kms && cd kms
$ 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関数を作成していきます。
$ mkdir lambda && cd lambda
$ touch index.ts
サンプル関数として、kms
という文字列を出力する処理を書いておきます。
exports.handler = async function () {
return {
statusCode: 200,
headers: { "Content-Type": "text/plain" },
body: "kms",
};
}
lib/kms-stack.ts
に移り、既存のコードを削除したのち、今作成したindex.ts
を使用するコードに変更します。
$ cd ../lib/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デプロイ中に使用される特定のリソースをプロビジョニングします。
$ cdk bootstrap
TSファイルをコンパイルしたのち、デプロイします。
$ npm run build
$ cdk deploy
無事Lambda関数が実行されました🎉
KMSで暗号化された値を取得する
KMSで暗号化された値を取得するにはいくつかやり方がありますが、今回は「Lambda 拡張機能」を使用してみます。
まずlib/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についてはこちらを参照。
const layer = lambda.LayerVersion.fromLayerVersionArn(
this,
'layer',
'arn:aws:lambda:ap-northeast-1:133490724326:layer:AWS-Parameters-and-Secrets-Lambda-Extension:2',
);
次にLambda関数にレイヤーを追加します。
const lambdaFunction = new lambda.Function(this, 'IndexHandler', {
runtime: lambda.Runtime.NODEJS_16_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'index.handler',
layers: [layer],
});
また、暗号化された値を取得し復号するためのポリシーをIAMロールにアタッチします。
lambdaFunction.addToRolePolicy(
new PolicyStatement({
resources: [
`arn:aws:kms:{リージョン}:{アカウントID}:key/{KMSのキーID}`,
`arn:aws:ssm:{リージョン}:{アカウントID}:parameter/{パラメータストアのパラメータの名前}`,
],
actions: ['kms:Decrypt', 'ssm:GetParametersByPath', 'ssm:GetParameter'],
})
);
簡単のためリソース等をハードコーディングしていますが本来は環境変数等で扱うべきです。
lambda/index.ts
に移ります。
暗号化された値を取得するのにlocalhost
にリクエスト必要があるため、lambda
ディレクトリでaxios
をインストールしておきます。
$ npm init -y
$ npm i axios
lambda/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
+ 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に設定
withDecryptionをtrueに設定することに関しては明示的に設定する必要がある旨の記載がありませんが、こちらのパラメータがないと値を復号できません。
+ const endpoint = encodeURI('http://localhost:2773/systemsmanager/parameters/get/?name={パラメータストアのパラメータの名前}&withDecryption=true');
それではデプロイしてみます。
$ npm run build
$ cdk deploy
今回SecureStringの値を「暗号化された値」という文字列にしています。
関数のテストを実行
無事暗号化された値が取得できました🎉
個人的にlocalhostにリクエストを送る際にwithDecryptionをtrueにしないといけないという点がハマりポイントでした(ドキュメントは隅々まで見ないといけないですね)
最後に
GoQSystemでは一緒に働いてくれる仲間を募集中です!
ご興味がある方は以下リンクよりご確認ください。