CFnテンプレートの修正って大変よね
CFn(CloudFormation)を触っててこんなことありませんか?
- json/yamlの文法ミスってた...
- プロパティ足りてなくてエラーになった...
- 他にもIAMとか付随するサービスの定義も必要だった...
- テンプレートだと自由度が低くて痒い所に手が届かない...
そんなときこそCDKです!
CDKとは?
「CFnテンプレートをコードで書ける」って代物です!
だいぶざっくりとした説明なので詳しくって方は公式docをご参照ください。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html
さっそく使ってみるゾ
WorkShopを参考にしつつ触ってみたいと思います。
https://catalog.workshops.aws/typescript-and-cdk-for-beginner/ja-JP/40-cdk-introduction
お試し利用なので、デプロイリソースは単一のLambda関数にします。
環境セットアップ
作業前の環境は以下で、cli操作はGit Bash
、cdkの実装はtypescript
にて行っていきます。
- OS:
Win11
- AWS CLI:
2.13.37
- npm:
10.2.4
- git:
2.41.0.windows.3
追加でCDK開発に必要なパッケージをインストールしておきます。
$ npm install -g aws-cdk
$ npm install -g typescript ts-node
CDKプロジェクトの立ち上げ
適当にプロジェクト用のディレクトリを作ります。
$ mkdir cdk-workshop
$ cd cdk-workshop
新規でプロジェクトをセットアップします。
ディレクトリの雛形ができるので、後は実装していくだけです。
$ cdk init sample-app --language typescript
いろいろとファイルはあるのですが、重要なのは以下の2つ(bin
, lib
)です。
cdk-workshop/
├── bin/
│ └── cdk-workshop.ts
├── lib/
│ └── cdk-workshop-stack.ts
bin
配下のファイル(cdk-workshop.ts
)は実際にデプロイする際のエントリーポイントとなっており、このファイルからlib
配下のファイルを呼び出します。
実際にデプロイするリソース(スタック)の定義はlib
配下で記述します。
実際にcdk-workshop.ts
を見ると、CdkWorkshopStack
(cdk-workshop-stack.ts
)をimportし、インスタンスを生成しているのがわかるかと思います。
[cdk-workshop.ts]
import * as cdk from 'aws-cdk-lib';
import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';
const app = new cdk.App();
new CdkWorkshopStack(app, 'CdkWorkshopStack');
lambdaを定義してみる
cdk-workshop-stack.ts
を修正し、lambdaの定義を記述してみます。
リソース毎に記述方法は異なるので、基本的には公式リファレンスを参考に定義を記述していきます。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html
今回はお試しなので適当なコードをインラインコードとして記述しちゃいました。
[cdk-workshop-stack.ts]
import { Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class CdkWorkshopStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const lambdaFunction = new lambda.Function(this, 'LambdaFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: new lambda.InlineCode(`
exports.handler = async (event) => {
return {
statusCode: 200,
body: 'Hello, CDK Lambda!'
};
};
`)
});
}
}
デプロイしてみる
cdkをデプロイする前に、ブートストラップスタックのデプロイが必要になります。
この作業は1つのAWSアカウントで1度だけ実施すればOKの作業となります。
$ cdk bootstrap
cdkコマンド実行時に以下のエラーが出る場合は、プロファイルの設定が必要となるため、適宜実施してください。
Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment
参考までに自分のプロファイル設定を記載します。(旧AWS SSO環境です)
マスキングしておりますが、~/.aws/config
は以下のような記述となっています。
~/.aws/config
[profile sso-profile]
sso_start_url = https://xxxxxxxx/start#/ ※SSOログインのURL
sso_region = [ログインするリージョン]
sso_account_id = [デプロイ先のアカウントID]
sso_role_name = [SSOログインする際のロール]
[profile deploy-profile]
role_arn = [ログイン後にスイッチするロール(実際にデプロイで使うロール)]
region = [ログインするリージョン]
source_profile = sso-profile
configファイルを用意した上で、cliからSSOログインします。
ブラウザで許可を求められるので許可します。
$ aws sso login --profile sso-profile
いちいち、プロファイル名を指定するのは面倒なのでスイッチするロール(デプロイに使うロール)のプロファイルを環境変数に登録しときます。
$ export AWS_PROFILE=deploy-profile
ちょっと逸れましたが、ブートストラップが完了したら、いよいよデプロイです。
$ cdk deploy
デプロイ対象にIAMが含まれていたりすると、続行してよいかを聞かれるので、y
で続行します。
煩わしいなって場合は、デプロイ時に--require-approval never
のオプションを付けてあげるとスルーしてくれます。
Do you wish to deploy these changes (y/n)?
コンソールの出力でも確認できますが、CFn側でもstackが作成されていることが確認できます。
これにて、デプロイ完です!
どうです?GUIでポチポチするよりはマシですよね?
なお、リソースを削除する場合は、CFnでスタックを削除するか、以下コマンドを実行します。
$ cdk destroy
複数Stackの場合は?
lib
配下でexportするクラスがスタックとなるため、1ファイル内で複数クラス定義するか、1つのクラス内で複数クラスをexportするかの選択肢があります。
今回は、新たにcdk-another-stack.ts
をlib
配下に作成し、以下構成にします。
cdk-workshop/
├── bin/
│ └── cdk-workshop.ts
├── lib/
│ └── cdk-workshop-stack.ts
│ └── cdk-another-stack.ts
lambda.Function
の第二引数にあたるIDをCdkWorkshopStack
とは違う値としていますが、コンストラクト内で一意であればよいため、同じにしても大丈夫です。
[cdk-another-stack.ts]
import { Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class CdkAnotherStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const lambdaFunction = new lambda.Function(this, 'LambdaAnotherFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: new lambda.InlineCode(`
exports.handler = async (event) => {
return {
statusCode: 200,
body: 'Hello, CDK Lambda!'
};
};
`)
});
}
}
cdk-workshop.ts
から呼び出す必要があるので、こちらも呼び出しを追加しておきます。
ここでは、第二引数のIDは異なるものを指定しないとダメです。
[cdk-workshop.ts]
import * as cdk from 'aws-cdk-lib';
import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';
import { CdkAnotherStack } from '../lib/cdk-another-stack';
const app = new cdk.App();
new CdkWorkshopStack(app, 'CdkWorkshopStack');
// スタックを追加
new CdkAnotherStack(app, 'CdkAnotherStack');
デプロイパターンはいくつかあるので、状況に応じて使い分けするとよいかと思います。
- 全て直列でデプロイ
CdkWorkshopStack
⇒CdkAnotherStack
の順でデプロイする場合は以下で実施します。
$ cdk deploy --all
- 並列でデプロイ
concurrency
で指定した数の並列数でCdkWorkshopStack
,CdkAnotherStack
を並列デプロイします。
直列に比べ、早くデプロイすることが可能です。
承認が必要なスタックがあると失敗するので、スルーするようオプションを付けときます。
$ cdk deploy --all --concurrency [並列可能数] --require-approval never
(例)
$ cdk deploy --all --concurrency 2 --require-approval never
- 個別デプロイ
CdkWorkshopStack
orCdkAnotherStack
を指定してデプロイします。
$ cdk deploy [スタックのクラス名]
(例)
$ cdk deploy CdkWorkshopStack
CFnテンプレートとの比較
ここまででもテンプレートよりCDK!って気持ちになってるかもしれないですが、最後に実際のCFnテンプレートとの比較を行ってみます。
直接CFnのGUIからデプロイ後のテンプレートも確認できますが、事前にどのようなテンプレートが使われるのかを確認することも可能です。
$ cdk synth
synthを実行することで標準出力にテンプレートが出力され、~/cdk.out
にもテンプレートファイルが置かれます。
[CdkWorkshopStack.template.json(IAMとLambda部分を抜粋)]
{
"Resources": {
"LambdaFunctionServiceRoleC555A460": {
"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"
]
]
}
]
},
"Metadata": {
"aws:cdk:path": "CdkWorkshopStack/LambdaFunction/ServiceRole/Resource"
}
},
"LambdaFunctionBF21E41F": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "\n exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: 'Hello, CDK Lambda!'\n };\n };\n "
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"LambdaFunctionServiceRoleC555A460",
"Arn"
]
},
"Runtime": "nodejs20.x"
},
"DependsOn": [
"LambdaFunctionServiceRoleC555A460"
],
"Metadata": {
"aws:cdk:path": "CdkWorkshopStack/LambdaFunction/Resource"
}
},
実際にCdkWorkshopStack.template.json
を確認するとAWS::IAM::Role
の定義が存在しているのを確認できるかと思います。
ですが、cdk側での実装はたった数行で済むんです!楽ですねぇ。
(CDK Pipelinesにも触れていこうと思ったんですが長くなるのでまたの機会に)
おまけ(インラインでLambdaコードなんて書かねぇよ)
今回はお試しなので簡易にインラインコードを書きましたが、実際にそんなことはしないと思います。
自分のプロジェクトではCDKスクリプトとLambdaスクリプトを同じレポジトリで管理し、以下のようなディレクトリ構成を取っています。
project-root/
├── cdk/ ・・・cdkスクリプト
│ └── bin
│ └── lib
│
├── lambda/ ・・・lambdaスクリプト
その上で、stack側では以下のようにlambda構築を実装しています。
const sampleFunc = new NodejsFunction(this, 'sampleFunc', {
runtime: Runtime.NODEJS_20_X,
entry: path.join(__dirname, [lambdaハンドラースクリプトまでの相対パス]),
handler: 'handle',
functionName: 'sampleFunc',
timeout: Duration.seconds(900),
memorySize: 512,
});
レポジトリ分けるとめんどくさいので、Lambda+CDKの場合は基本的にこの構成を取ることになるんでは、と思ってます。