TL; DR
社内でInfrastructure as Codeを推進しているのですが、元々はTerraformでやっていました。(実際既に運用ベースに乗っているものもあります。)
しかしある日、こんなワードを聞いてしまったのです。。
「インフラ強い会社は今だいたいCDKっすよ。」…ドキッとしてしまいました。
彼は勉強熱心で社外の色々な勉強会にも参加しており、雷に打たれた思いでした。
その日のうちにCDKに移行することを決め、翌日からちまちま書き始めた経緯です。
Advent Calendarの担当者通知
前職でAdvent Calenderに取り組んだときの記事です。
始まってからリマインダーなどの通知を仕込むことを失念していることに気付きさくっとやってみました。
通知してくれるLambdaをCDKでデプロイするまでやってみようと思います。
CDKの基本
生成されたREADME.md
には下記記載があります。
-
npm run build
compile typescript to js -
npm run watch
watch for changes and compile -
npm run test
perform the jest unit tests -
cdk deploy
deploy this stack to your default AWS account/region -
cdk diff
compare deployed stack with current state -
cdk synth
emits the synthesized CloudFormation template
まあ見ての通りですが、これ以外にも後述するcdk bootstrap
というデプロイ時に生成されるCloudFormation定義を格納しておくS3バケットなどを作ってくれるコマンドは必須なので覚えておきましょう。
おまじない
最初のあれですね。
CDKは割とTypeScriptが公式感あるのでTypeScriptで書いていきます。
ちなみにエディタはVSCodeだと特にプラグインなくてもめちゃくちゃ恩恵受けられるのでオススメです。(公式も力入れてるっぽい)
$ cdk --version
1.17.1 (build fa4cb1f)
$
$ cdk init app --language=typescript
`cdk init` cannot be run in a non-empty directory!
いきなりエラーw
はい、空じゃないと怒られてしまうのできれいきれいなディレクトリでやりましょう。
$ cdk init app --language typescript
Applying project template app for typescript
Initializing a new git repository...
Executing npm install...
npm WARN deprecated fsevents@1.2.9: One of your dependencies needs to upgrade to fsevents v2: 1) Proper nodejs v10+ support 2) No more fetching binaries from AWS, smaller package size
npm WARN deprecated left-pad@1.3.0: use String.prototype.padStart()
node-pre-gyp WARN Using request for node-pre-gyp https download
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN sub@0.1.0 No repository field.
npm WARN sub@0.1.0 No license field.
# Useful commands
* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
今度は問題なく初期化が実施されました。
色々なファイルやらなんやらが生成されています。
$ pwd
/Users/user/Documents/GitHub/cdk-lambda
$
$ ls -l
total 504
drwxr-xr-x 15 user staff 480 12 1 22:51 .
drwx------+ 33 user staff 1056 12 1 22:50 ..
drwxr-xr-x 12 user staff 384 12 1 22:51 .git
-rw-r--r-- 1 user staff 93 12 1 22:51 .gitignore
-rw-r--r-- 1 user staff 65 12 1 22:51 .npmignore
-rw-r--r-- 1 user staff 369 12 1 22:51 README.md
drwxr-xr-x 3 user staff 96 12 1 22:51 bin
-rw-r--r-- 1 user staff 112 12 1 22:51 cdk.json
-rw-r--r-- 1 user staff 159 12 1 22:51 jest.config.js
drwxr-xr-x 3 user staff 96 12 1 22:51 lib
drwxr-xr-x 467 user staff 14944 12 1 22:51 node_modules
-rw-r--r-- 1 user staff 227570 12 1 22:51 package-lock.json
-rw-r--r-- 1 user staff 525 12 1 22:51 package.json
drwxr-xr-x 3 user staff 96 12 1 22:51 test
-rw-r--r-- 1 user staff 596 12 1 22:51 tsconfig.json
$
$ tree -I node_modules
.
├── README.md
├── bin
│ └── cdk-lambda.ts
├── cdk.json
├── jest.config.js
├── lib
│ └── cdk-lambda-stack.ts
├── package-lock.json
├── package.json
├── test
│ └── cdk-lambda.test.ts
└── tsconfig.json
init
コマンドで生成されるいくつかのファイルにはデフォルトでディレクトリ名が振られます。
ファイルの中身はこんな感じ。
// CloudFormationのStackと同様にインフラを定義したファイル
import cdk = require('@aws-cdk/core');
export class CdkLambdaStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
}
}
// cdk build 実施時に呼び出されるファイル
#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { CdkLambdaStack } from '../lib/cdk-lambda-stack';
const app = new cdk.App();
new CdkLambdaStack(app, 'CdkLambdaStack');
// TypeScriptの設定ファイル
{
"compilerOptions": {
"target":"ES2018",
"module": "commonjs",
"lib": ["es2018"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization":false,
"typeRoots": ["./node_modules/@types"]
},
"exclude": ["cdk.out"]
}
// CDKの設定ファイル(環境変数などで使えるが、今回は使わない)
{
"app": "npx ts-node bin/sub.ts",
"context": {
"@aws-cdk/core:enableStackNameDuplicates": "true"
}
}
Stackを書いていく
さきほどinit
コマンドで生成されたlib/index.ts
がリソース定義に当たるので編集していきます。
が、その前にinit
打ったばっかりだと@aws-cdk/core
しかないのでエディタに怒られ続けるので先にインストールしておきましょう。
今回はLambda関数を作成するのとSSMを利用、IAMロールの作成、Cloudwatch eventでのトリガーを作成するため以下が必要になります。
$ npm install @aws-cdk/aws-lambda @aws-cdk/aws-ssm @aws-cdk/aws-iam @aws-cdk/aws-events @aws-cdk/aws-events-targets
npm WARN cdk-lambda@0.1.0 No repository field.
npm WARN cdk-lambda@0.1.0 No license field.
+ @aws-cdk/aws-events-targets@1.18.0
+ @aws-cdk/aws-lambda@1.18.0
+ @aws-cdk/aws-ssm@1.18.0
+ @aws-cdk/aws-events@1.18.0
+ @aws-cdk/aws-iam@1.18.0
added 40 packages from 4 contributors and audited 1764654 packages in 13.232s
8 packages are looking for funding.
Run "npm fund" to find out more.
Nodeのv8系はサポート切れてしまうのでv10系以上が推奨なので、RuntimeもNODEJS_10_X
に設定します。
またLambdaのソースを参照する部分ではAssetCode
クラスを使ってあげると、ローカルファイルをよきに計らって色々してくれるのでこっちにしましょう。ZIPにするのはめんどくさい。
const adventarNotifyFunction = new Function(this, 'SlackAdventar', {
code: AssetCode.fromAsset('src'), //ここで指定するとZIP化してUploadまでしてくれる
書きあがったコードはこちら
import cdk = require('@aws-cdk/core');
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam';
import { Rule, Schedule } from '@aws-cdk/aws-events';
import { LambdaFunction } from '@aws-cdk/aws-events-targets';
import { StringParameter } from '@aws-cdk/aws-ssm';
export class CdkLambdaStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// IAM Role for Lambda with SSM policy.
const executionLambdaRole = new Role(this, 'secureLambdaRole', {
roleName: 'lambdaSecureExecutionRole',
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMReadOnlyAccess'),
]
});
// Call Slack channel by plain text.
const slackChannel = StringParameter.fromStringParameterAttributes(this, 'slackChannel', {
parameterName: '/Lambda/production/slack-channel',
});
// Configure for Lambda function.
const adventarNotifyFunction = new Function(this, 'SlackAdventar', {
functionName: 'slack-adventar-notify',
runtime: Runtime.NODEJS_10_X,
code: AssetCode.fromAsset('src'),
handler: 'index.handler',
timeout: cdk.Duration.seconds(300),
role: executionLambdaRole,
environment: {
TZ: "Asia/Tokyo",
SLACK_CHANNEL: slackChannel.stringValue,
}
});
// Cloudwatch event for run notification at 1PM JST in Advent Calendar term.
const rule = new Rule(this, 'Rule', {
schedule: Schedule.expression('cron(0 4 1-25 12 ? 2019)')
});
rule.addTarget(new LambdaFunction(adventarNotifyFunction));
}
}
const app = new cdk.App();
new CdkLambdaStack(app, 'adventarNotification');
app.synth();
ざっとこんな感じです。
今回はリソース量が大した量ではないので、1ファイルにまとめてしまいました。
SSMを登録する
SlackチャンネルとWebhookはSSMのパラメータストアから呼び出すようにしたいので、下記コマンドで個別に登録していきます。
環境変数 | 属性 |
---|---|
SLACK_CHANNEL | String |
SLACK_MEMBERS | StringList |
SLACK_WEBHOOK | SecureString |
$ aws ssm put-parameter \
> --type 'String' \
> --name '/Lambda/production/slack-channel' \
> --value '$CHANNEL_NAME' #Slackのチャンネル名
{
"Version": 1,
"Tier": "Standard"
}
$
$
$ # https://github.com/aws/aws-cli/issues/3076
$ aws ssm put-parameter \
> --cli-input-json '{"Name": "/Lambda/production/slack-webhook", "Type": "SecureString", "Value": "$WEBHOOK_URL"}' #SlackのWebhook URL
{
"Version": 1,
"Tier": "Standard"
}
$
$
$ aws ssm put-parameter \
> --type 'StringList' \
> --name '/Lambda/production/slack-members' \
> --value "dummy,hogehoge,fugafuga,hogefuga,..." #SlackのユーザーID
{
"Version": 1,
"Tier": "Standard"
}
上記のように設定し、Webhook
などのクレデンシャルはお漏らししないように気をつけます。
Lambdaを書いていく
単純なSlack通知なので、特にこれといった面白みはないのですが…一応書いていきます。
ちょいハマりポイントとしては以下でした。
- Slackのメンションの指定方法が
<@username>
-><@user_id>
に変わってた。
-> A lingering farewell to the username
- SSMで設定したWebhookとチャンネル名の呼び出し方
src/index.js
を作成して、中に処理を記述していきます。
const request = require('request');
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();
exports.handler = async function(event, context) {
// Get WebHook URL from SSM parameter store.
const slackWebhook = await ssm.getParameter({
Name: '/Lambda/production/slack-webhook',
WithDecryption: true,
}).promise();
let SLACK_WEBHOOK = slackWebhook.Parameter.Value;
// Get user_id from SSM parameter store.
const slackMember = await ssm.getParameter({
Name: '/Lambda/production/slack-members',
WithDecryption: true,
}).promise();
let member = slackMember.Parameter.Value.split(',');
// Inject SLACK_CHANNEL value.
const SLACK_CHANNEL = process.env.SLACK_CHANNEL;
// create message
const now = new Date();
const Today = now.getDate();
const mention = "<@" + member[Today] + ">";
let message = mention + "\n";
message += '*' + "Advent Calendar " + Today + "日目 よろしくお願いします!" + '*' + "\n";
message += 'https://adventar.org/calendars/4548';
// Request configure
const options = {
url: SLACK_WEBHOOK,
headers: {
'Content-type': 'application/json'
},
body: {
"text": message,
"channel": SLACK_CHANNEL,
"username": "uluru",
"icon_emoji": "uluru",
},
json: true
};
// Sending message
request.post(options, function(error, response, body) {
if (!error && response.statusCode == 200) {
context.done(null, body);
} else {
console.log('error: ' + response.statusCode);
context.done(null, 'error');
}
});
}
ざっとこんな感じです。
特に面白みもなく。笑
注意すべき点はsrc
配下に必要なパッケージをインストールあげないといけないところですね。
今回はrequest
が必要なのでrootディレクトリで下記を実施しています。
$ npm install --prefix ./src request
npm WARN saveError ENOENT: no such file or directory, open '/Users/user/Documents/GitHub/cdk-lambda/src/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/Users/user/Documents/GitHub/cdk-lambda/src/package.json'
npm WARN src No description
npm WARN src No repository field.
npm WARN src No README data
npm WARN src No license field.
+ request@2.88.0
added 48 packages from 59 contributors and audited 63 packages in 2.997s
デプロイ
コードが書き終わったのでデプロイしていきたいと思います。
まずはcdk bootstrap
でS3バケットを作成していきます。
その後npm run build
でコンパイルを実施して、cdk deploy
でリソースを作成していきます。
cdk bootstrap
対象ディレクトリでcdk bootstrap
と打つだけです。
$ pwd
/Users/user/Documents/GitHub/cdk-lambda
# failed
$ cdk bootstrap
⨯ Unable to compile TypeScript:
lib/cdk-lambda-stack.ts:40:16 - error TS2304: Cannot find name 'lambda'.
40 runtime: lambda.Runtime.NODEJS_10_X,
~~~~~~
Subprocess exited with error 1
$ cdk bootstrap
ENOENT: no such file or directory, open '../src/index.js'
Subprocess exited with error 1
# Success
$ cdk bootstrap
⏳ Bootstrapping environment aws://608092561803/us-east-1...
CDKToolkit: creating CloudFormation changeset...
0/2 | 22:41:38 | CREATE_IN_PROGRESS | AWS::S3::Bucket | StagingBucket
0/2 | 22:41:39 | CREATE_IN_PROGRESS | AWS::S3::Bucket | StagingBucket Resource creation Initiated
1/2 | 22:42:01 | CREATE_COMPLETE | AWS::S3::Bucket | StagingBucket
2/2 | 22:42:03 | CREATE_COMPLETE | AWS::CloudFormation::Stack | CDKToolkit
✅ Environment aws://608092561803/us-east-1 bootstrapped.
この時点でエラーがあると教えてくれます。
問題なければS3バケットとCloudFormationのスタックが作成されます。
npm run build
では続いてコンパイルするためにnpm run build
を実施します。
コンパイラはTypeScriptなのでtsc
が走ります。
{
"app": "npx ts-node bin/cdk-lambda.ts"
}
$ npm run build
> cdk-lambda@0.1.0 build /Users/user/Documents/GitHub/cdk-lambda
> tsc
特に何か出力される訳ではないのですが、jsファイルに変換されたmonogalib
/ bin
配下にできます。
またcdk.out
というディレクトリが作成され、CloudFormationの定義のようなものが作成されます。
ファイルの中身はこんな感じ。
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("source-map-support/register");
const cdk = require("@aws-cdk/core");
const cdk_lambda_stack_1 = require("../lib/cdk-lambda-stack");
const app = new cdk.App();
new cdk_lambda_stack_1.CdkLambdaStack(app, 'CdkLambdaStack');
//# sourceMappingURL=data:application/json;base64,hogehogehogefugafugafugafugafuga
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const cdk = require("@aws-cdk/core");
const aws_lambda_1 = require("@aws-cdk/aws-lambda");
const aws_iam_1 = require("@aws-cdk/aws-iam");
const aws_events_1 = require("@aws-cdk/aws-events");
const aws_events_targets_1 = require("@aws-cdk/aws-events-targets");
const aws_ssm_1 = require("@aws-cdk/aws-ssm");
const fs = require("fs");
class CdkLambdaStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);
// The code that defines your stack goes here
// IAM Role for Lambda with SSM policy.
const executionLambdaRole = new aws_iam_1.Role(this, 'secureLambdaRole', {
roleName: 'lambdaSecureExecutionRole',
assumedBy: new aws_iam_1.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
aws_iam_1.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
aws_iam_1.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMReadOnlyAccess'),
]
});
// Call Slack channel by plain text.
const slackChannel = aws_ssm_1.StringParameter.fromStringParameterAttributes(this, 'slackChannel', {
parameterName: '/Lambda/production/slack-channel',
});
// Configure for Lambda function.
const adventarNotifyFunction = new aws_lambda_1.Function(this, 'SlackAdventar', {
functionName: 'slack-adventar-notify',
runtime: aws_lambda_1.Runtime.NODEJS_10_X,
code: new aws_lambda_1.InlineCode(fs.readFileSync('./src/index.js', { encoding: 'utf-8' })),
handler: 'index.handler',
timeout: cdk.Duration.seconds(300),
role: executionLambdaRole,
environment: {
TZ: "Asia/Tokyo",
SLACK_CHANNEL: slackChannel.stringValue,
}
});
// Cloudwatch event for run notification at 1PM JST in Advent Calendar term.
const rule = new aws_events_1.Rule(this, 'Rule', {
schedule: aws_events_1.Schedule.expression('cron(0 4 1-25 12 * 2019)')
});
rule.addTarget(new aws_events_targets_1.LambdaFunction(adventarNotifyFunction));
}
}
exports.CdkLambdaStack = CdkLambdaStack;
const app = new cdk.App();
new CdkLambdaStack(app, 'adventarNotification');
app.synth();
//# sourceMappingURL=data:application/json;base64,hogehogehogefugafugafuga
import cdk = require('@aws-cdk/core');
export declare class CdkLambdaStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps);
}
ls -l cdk.out
total 80
-rw-r--r-- 1 user staff 4902 12 1 22:41 CdkLambdaStack.template.json
-rw-r--r-- 1 user staff 4938 12 1 22:41 adventarNotification.template.json
-rw-r--r-- 1 user staff 20 12 1 22:41 cdk.out
-rw-r--r-- 1 user staff 11674 12 1 22:41 manifest.json
-rw-r--r-- 1 user staff 7352 12 1 22:41 tree.json
{
"Resources": {
"secureLambdaRole067C996A": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/AmazonSSMReadOnlyAccess"
]
]
}
],
"RoleName": "lambdaSecureExecutionRole"
},
"Metadata": {
"aws:cdk:path": "CdkLambdaStack/secureLambdaRole/Resource"
}
},
"SlackAdventar6AF5ACBE": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "const request = require('request');\nconst AWS = require('aws-sdk');\nconst ssm = new AWS.SSM();\n\nexports.handler = async function(event, context) {\n\n // Get WebHook URL from SSM parameter store.\n const slackWebhook = await ssm.getParameter({\n Name: '/Lambda/production/slack-webhook',\n WithDecryption: true,\n }).promise();\n let SLACK_WEBHOOK = slackWebhook.Parameter.Value;\n\n // Get user_id from SSM parameter store.\n const slackMember = await ssm.getParameter({\n Name: '/Lambda/production/slack-members',\n WithDecryption: true,\n }).promise();\n let member = slackMember.Parameter.Value.split(',');\n\n // Inject SLACK_CHANNEL value.\n const SLACK_CHANNEL = process.env.SLACK_CHANNEL;\n\n // create message\n const now = new Date();\n const Today = now.getDate();\n const mention = \"<@\" + member[Today] + \">\";\n let message = mention + \"\\n\";\n message += '*' + \"Advent Calendar \" + Today + \"日目 よろしくお願いします!\" + '*' + \"\\n\";\n message += 'https://adventar.org/calendars/4548';\n\n // Request configure\n const options = {\n url: SLACK_WEBHOOK,\n headers: {\n 'Content-type': 'application/json'\n },\n body: {\n \"text\": message,\n \"channel\": SLACK_CHANNEL,\n \"username\": \"uluru\",\n \"icon_emoji\": \"uluru\",\n },\n json: true\n };\n\n // Sending message\n request.post(options, function(error, response, body) {\n if (!error && response.statusCode == 200) {\n context.done(null, body);\n } else {\n console.log('error: ' + response.statusCode);\n context.done(null, 'error');\n }\n });\n}\n"
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"secureLambdaRole067C996A",
"Arn"
]
},
"Runtime": "nodejs10.x",
"Environment": {
"Variables": {
"TZ": "Asia/Tokyo",
"SLACK_CHANNEL": {
"Ref": "slackChannelParameter"
}
}
},
"FunctionName": "slack-adventar-notify",
"Timeout": 300
},
"DependsOn": [
"secureLambdaRole067C996A"
],
"Metadata": {
"aws:cdk:path": "CdkLambdaStack/SlackAdventar/Resource"
}
},
"SlackAdventarAllowEventRuleCdkLambdaStackRule69AC60575315E536": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"SlackAdventar6AF5ACBE",
"Arn"
]
},
"Principal": "events.amazonaws.com",
"SourceArn": {
"Fn::GetAtt": [
"Rule4C995B7F",
"Arn"
]
}
},
"Metadata": {
"aws:cdk:path": "CdkLambdaStack/SlackAdventar/AllowEventRuleCdkLambdaStackRule69AC6057"
}
},
"Rule4C995B7F": {
"Type": "AWS::Events::Rule",
"Properties": {
"ScheduleExpression": "cron(0 4 1-25 12 * 2019)",
"State": "ENABLED",
"Targets": [
{
"Arn": {
"Fn::GetAtt": [
"SlackAdventar6AF5ACBE",
"Arn"
]
},
"Id": "Target0"
}
]
},
"Metadata": {
"aws:cdk:path": "CdkLambdaStack/Rule/Resource"
}
}
},
"Parameters": {
"slackChannelParameter": {
"Type": "AWS::SSM::Parameter::Value<String>",
"Default": "/Lambda/production/slack-channel"
}
}
}
cdk deploy
コンパイルまで完了したのでいよいよcdk deploy
でリソースを作成していきます。
初回の場合いきなり作成されることはないのですが、2回目以降は普通に無視して作りにいくので注意してしてください。
ちなみに変更がかかるとこんな感じでUpdate
がかかります。
まとめ
Lambdaも楽にデプロイできて幸せになれますね!
ZIP化して…S3にあげて…パスコピーして…みたいなのがないので楽でいいですね!
弊社だと割とServerless Framework
が主流ですが、SAMとうまく掛け合わせてローカルテストまでできるようになれば完全に駆逐できそう…!!!
嫌いな訳じゃないんですが、ちょっと多機能すぎるかな…というのと、AWS環境しか利用していないという背景があり、私個人としてはAWS CDKに統合していきたいのが本音です。
今後はユーザー作成やPersonalizeなどもCDKでガンガン書いていくつもりなので、気が向けばまた記事書くかもしれません。