はじめに
AWS CDKは、AWS リソース(インフラやサーバーなど)をコードから構築できる開発キットで、このAWS CDKを使ってAWS リソースを構築したくて勉強しました。
個人的にAWS公式サイトは迷子になるので、勉強したことを残しておきたくて記事にしました。
(ただのやってみた系の記事です)
やったこと
- AWS CDKについて理解する
- AWS CDKを使った開発方法を理解して、実践する
- テストコードも書く(TDDを意識)
- 環境(開発・検証など)が分かれている場合の実装方法を考えて実践する
※ 開発方法の理解が目的なので、記事内で出現するコードは簡単な内容で実装しています。
※ CDK Pipelinesを使った自動デプロイはやっていません。
前提知識
AWS CloudFormationの知識
AWS CloudFormationについてはこちらの記事にまとめています。
筆者の動作環境
- Node.js 18.12.1
- AWS CDK 2.53.0
- AWS CLI 2.9.1
- MacBook Pro M1チップ Monterey(12.0.1)
AWS CDKについて学ぶ
- AWS CDKは、AWS リソースをコードから構築できる開発キット
- TypeScriptやPythonなどの言語を使って開発できる
- 実装したコードはAWS環境にデプロイする前にテストできる
開発の流れ
- CDK プロジェクトを作成(
cdk init
) - AWS リソースのコードを実装
- CloudFormationのテンプレートに変換(
cdk synth
) - CloudFormationにデプロイ(
cdk deploy
)
→ デプロイするとCloudFormationのスタックが作成され、AWS環境にリソースを作る
CDK プロジェクトの3つの要素
-
App(CDK アプリケーション)
- スタック同士の依存関係などスタックの管理をしている
-
Stack(スタック)
- AWS環境へのデプロイの単位で、CloudFormationのスタック(テンプレート)に相当する
-
Construct(コンストラクト)
- デプロイするAWS リソースの定義こと
- スタック内に、Construct Library を使ってAWS リソースを定義していく
こちらの記事がとてもわかりやすかったです。
Construct Library(コンストラクト ライブラリ)とは?
AWS リソースを実装するためのライブラリで、AWS リソースごとに用意されている
(例)Lambda コンストラクト ライブラリ
-
使い方はAWS CDKのAPIリファレンス に定義されている
-
コンストラクト ライブラリは3種類ある
-
L1 コンストラクト (Low Level Construct)
- AWS リソースのプロパティ値を明示的に設定する必要がある
- コードが長くなるが細かい設定ができる
- クラス名にCfnプレフィックスがついている
-
L2 コンストラクト (High Level Construct)
- 便利なメソッドが提供されていて、直感的にコーディングしやすい
- デフォルトの設定がされていて、細かい設定が省略できる
→ よしなにAWS リソースをベストプラクティスで作成してくれる - サポートされていないAWS リソースがある
-
L3 コンストラクト (Patterns)
- 複数のAWS リソースを組み合わせてパターン化されたAWS環境を作れる
-
基本的にL2を使い、細かい設定をするならL1のライブラリを選ぶだったような・・(たしか)
AWS CDKの自動テストについて
テストは2種類ある。
アサーションテスト
実装したコードのAWS リソースの設定が、期待値と一致するかなどテストする。
詳細には「CDK スタック インスタンスのプロパティ値」と「変換されるCloudFormationテンプレートのプロパティ値」を比較しているようである。
テストの実装はCDK assertionsモジュールを使って行う。
AWS CDKのAPIリファレンスのassertionsモジュールのページに定義されている。
スナップショットテスト
以前に作成したCloudFormationテンプレートと比較して差分があるかテストする。
テストについての公式ドキュメント
AWS CDK コマンド
CDK コマンドを使ってCDKの実行を行う。
CDK コマンドはcdk --help
で確認できる。
よく使いそうなコマンド一覧
CDK コマンド | 説明 |
---|---|
cdk init [TEMPLATE] | CDK プロジェクトを作成(テンプレートを省略可) |
cdk bootstrap [ENVIRONMENTS..] | デプロイ設定(アカウント、リージョンごとに1回設定) |
cdk list [STACKS..] | CDKのスタック一覧を表示 |
cdk synthesize [STACKS..] | CDKのスタックをCloudFormationのテンプレートファイルに変換(cdk synthに省略可) |
cdk deploy [STACKS..] | CDKのスタックをCloudFormationへデプロイ |
cdk destroy [STACKS..] | 指定したスタックをCloudFormationから削除 |
cdk diff [STACKS..] | CDKのスタックとCloudFormationのスタックの差分を表示 |
- CDKのスタックが1つの場合は、スタック名の省略が可能
- こちらの記事に全てのコマンドについて詳細に書かれていました
[実践]AWS CDK のインストール
事前準備
AWS CDKを使用するには、以下のインストールが必要(2022年12月時点)
-
Node.js 10.13.0 以降
長期サポートが有効な最新バージョンがおすすめとのこと
※バージョン 13.0.0 〜 13.6.0 は互換性の問題があるためサポートしていない
AWS CDK をインストールする
$ npm install -g aws-cdk
$ cdk --version
2.53.0 (build 7690f43)
AWS CDK のバージョンについて
推奨のバージョンについては公式サイトで確認できる。
2022年12月時点ではv2を使うことを勧められている。
$ npm info aws-cdk versions
[
'0.8.0', '0.8.1', '0.8.2', '0.9.0',
〜〜略〜〜
'2.51.0', '2.51.1', '2.52.0', '2.52.1',
'2.53.0'
]
[実践]CDK プロジェクト作成〜デプロイ (VPC作成)
やること
CDK プロジェクトを作成 → VPCを構築するコードを書く → AWS環境にリソースデプロイ
1. CDK プロジェクト作成(cdk init
)
- 任意のフォルダを作成し、
cdk init --language [言語]
を実行する - フォルダ名がデフォルトのCDKスタック名などに使われていたので、適当すぎない名前が良いかも
$ mkdir sample-app
$ cd sample-app/
$ cdk init --language typescript
Applying project template sample-app for typescript
# Welcome to your CDK TypeScript project
You should explore the contents of this project. It demonstrates a CDK app with an instance of a stack (`SampleAppStack`)
which contains an Amazon SQS queue that is subscribed to an Amazon SNS topic.
# →SQS キューを含み、SNS トピックにサブスクライブしているスタック をデモとして実装しているよ!
✅ All done!
作成されたプロジェクト構成を詳しく見てみる
sample-app/
├── README.md
├── bin
│ └── sample-app.ts # 【実装ファイル】Appクラス定義Stackクラスのインスタンスを作成している
├── cdk.json # [設定ファイル]CDKの設定
├── jest.config.js # [設定ファイル]JSのテストプラットフォーム「jest」の設定ファイル
├── lib
│ └── sample-app-stack.ts # 【実装ファイル】StackとConstructクラスを定義(リソースを実装)
│
├── node_modules # [設定ファイル]依存パッケージのファイル群
├── package-lock.json
├── package.json # [設定ファイル]プロジェクト設定ファイル
├── test
│ └── sample-app.test.ts # 【実装ファイル】自動テスト用ファイル
└── tsconfig.json # [設定ファイル]TypeScriptの設定ファイル
もっと詳細を見る!
-
TypeScriptのインストールと設定がされていた
-
gitの設定もされていて、パッケージの初期の状態でコミットされていた
gitコミットログ$ git log commit 51d266b634b626e28cb63b9a1957ed8683d4b9a9 (HEAD -> main) Author: sari <sari@xxxxxx.co.jp> Date: Tue Dec 6 23:55:14 2022 +0900 Initial commit
-
--generate-only
オプションをつけてCDK プロジェクトを作成すれば、パッケージのインストールやgit リポジトリ作成は行われないhelpでオプション確認$ cdk init --help --generate-only If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project [boolean] [default: false]
【実装ファイル】をもう少し詳細に説明
-
lib/sample-app-stack.ts
でStackクラスを定義- Stackクラスはデプロイの単位(CloudFormationのテンプレートになる)
- Stackクラスは複数作ることが可能
-
bin/sample-app.ts
でStackクラスのインスタンスを作成- Stackクラスを追加した場合、このファイルにインスタンス化するコードを追加する
- アプリケーション実行時に最初に呼びされる
sample-app/cdk.json
"app": "npx ts-node --prefer-ts-exts bin/sample-app.ts",
ts-nodeを利用するとJavaScriptにコンパイルせずにTypeScriptのファイルのまま実行することができる。
プロジェクト設定(好み) - Node.jsのバージョン固定
現時点で最新のバージョンの18.12.1
を使うので、package.jsonに設定しておく。
Node.jsのバージョン管理ツールはVolta
を使っているので以下で設定した。
$ volta pin node@v18.12.1
"volta": {
"node": "18.12.1"
}
2. デプロイ設定(cdk bootstrap
)
AWS CDKでデプロイするときは、デプロイ用のS3などの準備が必要である。
AWSアカウントとリージョン毎にcdk bootstrap
コマンドを実行して準備する。
(1回設定した次から設定は不要)
名前付きプロファイルを指定して設定する
(1)名前付きプロファイルを設定する
手順はこちら
$ cat ~/.aws/config
[profile hoge-system_dev]
region = ap-northeast-1
output = json
$ cat ~/.aws/credentials
[hoge-system_dev]
aws_access_key_id = XXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXX
(2)cdk bootstrap
コマンドを実行
設定したプロファイルを指定してcdk bootstrap
コマンドを実行
$ cdk bootstrap --profile hoge-system_dev
プロファイルで[default]に設定しなかったので、
cdk deploy
などのAWS環境と接続するコマンドは--profile
で対象の環境を指定する
3. 実装 (VPCを作成する)
TDDを意識してコーディングしました。
(1)テストコードを書く
-
APIリファレンスのCDK assertionsモジュールのページを見ながらテストを書く
-
テストの期待値は、CloudFormationのテンプレートのプロパティ値を書いていくので、Cloud Fromationの公式リファレンスを見るのが、個人的には効率よくかけた
-
ひとまず、VPCにサブネットを2つを作るコードを最低限で書いてみる
import * as cdk from "aws-cdk-lib";
import { Template, Match } from "aws-cdk-lib/assertions";
import * as SampleApp from "../lib/sample-app-stack";
// スタック作成〜テンプレート取得
const app = new cdk.App();
const stack = new SampleApp.SampleAppStack(app, "MyTestStack");
const template = Template.fromStack(stack);
// TEST
test("VPCがcidr192.168.0.0/16で1つ作成されること", () => {
template.resourceCountIs("AWS::EC2::VPC", 1);
template.hasResourceProperties("AWS::EC2::VPC", {
CidrBlock: "192.168.0.0/16",
});
});
test("サブネットが2つ作成されること", () => {
template.resourceCountIs("AWS::EC2::Subnet", 2);
});
(2)AWS リソースを構築するコードを書く
- APIリファレンスのVPCのページを見ながらコードを書く
import { Stack, StackProps } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";
export class SampleAppStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, "SariVpc", {
ipAddresses: ec2.IpAddresses.cidr("192.168.0.0/16"),
// サブネット構成(特に指定なければマルチAZ構成で2つ作成される)
subnetConfiguration: [
{
cidrMask: 24,
name: "SariVpc_PublicSubnet",
subnetType: ec2.SubnetType.PUBLIC,
},
],
});
}
}
(3)自動テスト実行
$ npm run build && npm run test
> sample-app@0.1.0 build
> tsc
> sample-app@0.1.0 test
> jest
PASS test/sample-app.test.ts (9.376 s)
✓ VPCがcidr192.168.0.0/16で1つ作成されること (1 ms)
✓ サブネットが2つ作成されること
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 9.628 s
Ran all test suites.
※(例)テスト失敗した時の結果
> sample-app@0.1.0 test
> jest
FAIL test/sample-app.test.ts
✓ VPCがcidr192.168.0.0/16で1つ作成されること (1 ms)
✕ サブネットが1つ作成されること
● サブネットが1つ作成されること
Expected 1 resources of type AWS::EC2::Subnet but found 2
17 |
18 | test("サブネットが1つ作成されること", () => {
> 19 | template.resourceCountIs("AWS::EC2::Subnet", 1);
| ^
20 | });
21 |
at Template.resourceCountIs (node_modules/aws-cdk-lib/assertions/lib/template.js:1:2087)
at Object.<anonymous> (test/sample-app.test.ts:19:12)
デプロイする前に、間違いや想定との乖離に気がつけるので効率が良い!
[補足]TypeScriptコンパイル
TypeScriptで記述されたコードをJavaScriptにコンパイルする。
- リアルタイムでコンパイルする場合
npm run watch
- コンパイルを都度実行したい場合
npm run build
(tscコマンドはプロジェクト作成時に自動でpackage.jsonに設定されている)
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk"
},
(4)(1)〜(3)を繰り返す
(1)〜(3)を繰り返して開発する。
こちらのサイトのテストコードがとても分かりやすかったです。参考にさせていただきました。
4. CloudFormationのテンプレートに変換(cdk synth
)
前章で貼り付けたコードを変換する
$ cdk synth
cdk.out/
├── SampleAppStack.assets.json
├── SampleAppStack.template.json
├── cdk.out
├── manifest.json
└── tree.json
5. CloudFormationにデプロイ(cdk deploy
)
- プロファイル(AWS環境)を指定して、デプロイを実行する
- CDKのスタックが1つしかないので、
cdk deploy
コマンド実行時にスタック名の指定は省略した
$ cdk deploy --profile hoge-system_dev
✨ Synthesis time: 3.8s
SampleAppStack: building assets...
SampleAppStack: assets built
SampleAppStack: deploying...
SampleAppStack: creating CloudFormation changeset...
✅ SampleAppStack
✨ Deployment time: 70s
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxx:stack/SampleAppStack/xxxxxxxxx
✨ Total time: 73.8s
6. デプロイされたリソースを確認
AWS マネジメントコンソールから確認する。
リソースの詳細確認
-
IGW
IGWの作成は明示的に指定していないが自動で作ってくれていた!
subnetType: ec2.SubnetType.PUBLIC
の設定から判断してくれたと思う。
-
ルート設定
メインルートテーブル(1つ)とカスタムルートテーブル(2つ)が作られていた!カスタムルートテーブル(2つ)に、それぞれのサブネットとIGWのルート設定がされていた!
ルートはメインルートテーブルに設定せず、カスタムルートテーブルに設定するのがベストプラクティス。指定しなくてもここまで設定してくれるとは・・・!
[実践]CDK プロジェクト修正〜デプロイ (Lambda)
やること
CDK プロジェクトを作成 → Lambdaを構築するコードを書く → AWS環境にリソースデプロイ(スタック作成) → Lambdaを構築したコードを修正 → AWS環境にリソースデプロイ(スタック更新)
1. CDK プロジェクトを作成(cdk init
)
$ mkdir sample-app-lambda
$ cd sample-app-lambda/
$ cdk init --language typescript
同じAWSアカウントとリージョンなので、cdk bootstrap
はしなかった
2. 実装 (Lambdaを作成する)
Lambdaのコードも一緒にデプロイしたいので、プロジェクトに手動でLambdaのコードを追加する。
LambdaのコードはPythonで記載した。(特に理由なし)
sample-app-lambda
├── README.md
├── bin
│ └── sample-app-lambda.ts
├── cdk.json
├── jest.config.js
├── lib
│ ├── lambda ←追加
│ │ └── hello.py ←追加
│ └── sample-app-lambda-stack.ts ←修正
├── node_modules
├── package-lock.json
├── package.json
├── test
│ └── sample-app-lambda.test.ts ←修正
└── tsconfig.json
こちらを参考に実装した
- CloudFormationのテンプレートLambdaのページ
- APIリファレンスのLambdaのページ
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import * as SampleApp from "../lib/sample-app-lambda-stack";
// スタック作成〜テンプレート取得
const app = new cdk.App();
const stack = new SampleApp.SampleAppLambdaStack(app, "TestStack");
const template = Template.fromStack(stack);
// TEST
test("作成するLambdaは1つであること", () => {
template.resourceCountIs("AWS::Lambda::Function", 1);
});
describe("「Sari-getHellowWorld」のテスト", (): void => {
test("Runtime(言語)の設定が「python3.9」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Runtime: "python3.9",
});
});
test("handler(実行コード)の設定が「lambda_function.lambda_handler」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "lambda_function.lambda_handler",
});
});
test("Description(説明)の設定が「HellowWorldを返却するAPI」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Description: "HellowWorldを返却するAPI",
});
});
test("Lambda名が「Sari-getHellowWorld」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
FunctionName: "Sari-getHellowWorld",
});
});
});
import { Stack, StackProps, aws_lambda } from "aws-cdk-lib";
import { Construct } from "constructs";
const path = require("path");
export class SampleAppLambdaStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// 第2引数はリソースの論理IDになる
new aws_lambda.Function(this, "Sari-getHellowWorld", {
functionName: "Sari-getHellowWorld",
description: "HellowWorldを返却するAPI",
runtime: aws_lambda.Runtime.PYTHON_3_9, // Lambda言語
handler: "lambda_function.lambda_handler", //実行する関数
code: aws_lambda.Code.fromAsset(path.join(__dirname, "lambda")), // Lambdaに設定するコード(展開前に圧縮されて S3 にアップロードされる)
});
}
}
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps('Hello from Lambda!')
}
3. CloudFormationのテンプレートに変換(cdk synth
)
$ cdk synth
4. CloudFormationにデプロイ(cdk deploy
)
Lambda用のIAMロール作るよって聞かれたのでYesにした。
(IAMロール必要だけどコードに書いてなかった)
$ cdk deploy --profile hoge-system_dev
SampleAppLambdaStack: assets built
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬────────────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${Sari-getHellowWorld/ServiceRole.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
└───┴────────────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Sari-getHellowWorld/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
SampleAppLambdaStack: deploying...
SampleAppLambdaStack: creating CloudFormation changeset...
✅ SampleAppLambdaStack
✨ Deployment time: 61.28s
Stack ARN:
arn:aws:cloudformation:ap-northeast-1
✨ Total time: 69.9s
5. デプロイされたリソースを確認
AWS マネジメントコンソールから確認する。
作成されたリソースの詳細を見る
-
Lambda用のIAMロール
IAMロールにアタッチされたポリシーは「CloudWatch Logs」の書き込み権限のみで、Lambdaのログを「CloudWatch Logs」に書き込む用と思われる。
必要最低限の権限が付与されたロールが自動で作られていた!
すごい..
6. デプロイしたLambdaをテスト実行してみる
Lambdaを画面からテスト実行すると以下のエラーになった。
「lambda_functionがない」とのことで、これは単純なhandlerの設定ミス・・なので修正してデプロイし直す。
7. コード修正
handlerの設定を「hello.lambda_handler」に修正
handler: "hello.lambda_handler",
せっかくなので、Lambdaのレスポンスも変えてみる
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps('Hello world!Hello world!') #ここを修正
}
もちろんテストも修正済(テストコードは省略)
8. スタックの差分を確認(cdk diff
)
- CloudFormationの変更セットにあたる確認
- 修正されるリソースの設定を確認することができる
$ cdk diff --profile hoge-system_dev
Stack SampleAppLambdaStack
Resources
[~] AWS::Lambda::Function Sari-getHellowWorld SarigetHellowWorldxxxxx
├─ [~] Code
│ └─ [~] .S3Key:
│ ├─ [-] 1c5e3cd55bb87bfa6xxxxxxxxxxx.zip
│ └─ [+] 2637a990f446715ddxxxxxxxxxxx.zip
├─ [~] Description
│ ├─ [-] HellowWorld?????API
│ └─ [+] HellowWorldを返却するAPI
├─ [~] Handler
│ ├─ [-] lambda_function.lambda_handler
│ └─ [+] hello.lambda_handler
└─ [~] Metadata
└─ [~] .aws:asset:path:
├─ [-] asset.1c5e3cd55bb87bfa6xxxxxxxxxxx
└─ [+] asset.2637a990f446715ddxxxxxxxxxxx
-
Description
変えてないけど、文字化けしているせい。日本語だめ? -
Metadata
Lambdaのコードの差分
(cdk.out
配下にアップロードするソースが出力されている)
9. CloudFormationにデプロイ(cdk deploy
)
CloudFormationのスタックを更新する。
cdk deploy --profile hoge-system_dev
✨ Synthesis time: 9.36s
SampleAppLambdaStack: building assets...
SampleAppLambdaStack: assets built
SampleAppLambdaStack: deploying...
SampleAppLambdaStack: creating CloudFormation changeset...
✅ SampleAppLambdaStack
✨ Deployment time: 56.07s
Stack ARN:
arn:aws:cloudformation:
✨ Total time: 65.43s
- デプロイの途中でCloudFormationの変更セットを作成している
-
cdk deploy --change-set-name
で、変更セットの名前を指定できる
※変更セットについては「変更セットを使用したスタックの更新」に記載している。 -
変更セットを作成した後、そのままスタックを更新していた
変更セットだけ作成のCDK コマンドはなさそうでした。あれば知りたい・・・
10. デプロイされたリソースを確認
AWS マネジメントコンソールから確認する。
[実践]スタックを削除(cdk destroy
)
cdk destroy
で、以下が全て削除される。
- CloudFormationのスタック
- スタックから作成されたAWS リソース
$ cdk destroy --profile hoge-system_dev
Are you sure you want to delete: SampleAppLambdaStack (y/n)? y
SampleAppLambdaStack: destroying...
✅ SampleAppLambdaStack: destroyed
[実践]1つのCDKプロジェクトで開発・検証・本番の各環境にデプロイしたい
実案件では1つのCDK プロジェクトで開発環境 (dev)、ステージング環境 (stg)、本番環境 (prod)などの複数環境にデプロイすることになる。AWS リソースによっては、世界中で一意の名前にしないといけないものがあり、開発・検証・本番でそれぞれ名前を変えないといけない。
CloudFormationを使った時に「リソース名の一部をプレフィックスにして、デプロイ時に外部パラメータで受け取った値を埋め込む」などの対応していたので、同じようなことがしたい。
まず方法を調べる
context変数を使って、環境を切切り替える記事を見つけた。
context変数とは?
◇コマンドラインのcontext変数(--context
オプション)
--context
オプションで変数を渡すことができる。変数はキー&バリューの形式。
$ cdk deploy --context Env=prod # 本番環境用スタックの生成
◇cdk.json
に定義
cdk.json
のcontextにcontext変数をキー&バリューで記述するして、アプリオブジェクトからcontext変数にアクセスできる。
{
"context": {
"bucket_name": "myotherbucket"
}
}
const app = new cdk.App();
const bucket_name = app.node.tryGetContext('bucket_name')
実践
やること
-
--context
オプションで実行環境を指定して、環境毎の設定でリソースをデプロイする - 各環境の設定は専用のクラスを作成して定義する(
cdk.json
の肥大化を避けるため) - 手軽に試したいので、開発環境と検証環境の2つで行う
- AWS リソースはLambdaを作成する
(全世界で名前を一意にする必要があり、前章でやったので)
$ cdk deploy --context Env=stg --profile hoge-system_stg # ステージング用
$ cdk deploy --context Env=dev --profile hoge-system_dev # 開発環境用
調査の時に見つけたサイトのコードを参考にさせていただきました。
1. CDK プロジェクトを作成(cdk init
)
手順などは省略
2. contextを扱うクラスを作成
lib/context.ts
を手動で作成して、定義を記載する。
export type EnvironmentType = "dev" | "stg";
export type Config = {
envSystemName: string;
envType: EnvironmentType;
envName: string;
};
export function getConfig(environmentType: EnvironmentType): Config {
const systemName = "SampleApp";
switch (environmentType) {
case "stg":
return {
envSystemName: `${systemName}-${environmentType}`,
envType: environmentType,
envName: "staging",
};
case "dev":
return {
envSystemName: `${systemName}-${environmentType}`,
envType: environmentType,
envName: "develop",
};
default:
throw new Error("Context value [Env] is invalid (use [stg] or [dev]).");
}
}
3. 実装(環境ごとにスタックを分けてLambdaを作成)
実装したコードのみ貼り付け
◇環境ごとにスタックを作成する
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { SampleAppLambdaStack } from "../lib/sample-app-lambda-stack";
import { EnvironmentType, getConfig } from "../lib/context";
const app = new cdk.App();
// 環境ごとの定義を取得してStackインスタンス作成
const env = app.node.tryGetContext("Env") as EnvironmentType;
const config = getConfig(env);
// id(=スタック名)を環境ごとに変えることでスタックを分ける
new SampleAppLambdaStack(app, config.envSystemName, { config });
// app単位で作成するリソース全てにタグをつける
cdk.Tags.of(app).add("ENV", config.envName);
cdk.Tags.of(app).add("name", config.envSystemName);
[補足]CDKで作成するリソースに一括タグ付けできる
「SCOPE」にapp、stack、constructを指定できる。
Tags.of(SCOPE).add('key', 'value');
◇Lambdaを作成する
Lambdaの論理IDと名前も環境ごと変える
import { Stack, StackProps, Duration, aws_lambda } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Config } from "../lib/context";
interface SampleAppLambdaStackProps extends StackProps {
config: Config;
}
const path = require("path");
export class SampleAppLambdaStack extends Stack {
constructor(scope: Construct, id: string, props: SampleAppLambdaStackProps) {
super(scope, id, props);
const { envType } = props.config;
// 第2引数はリソースの論理IDになる
new aws_lambda.Function(this, `Sari-getHellowWorld-${envType}`, {
functionName: `Sari-getHellowWorld-${envType}`,
description: "HellowWorldを返却するAPI",
runtime: aws_lambda.Runtime.PYTHON_3_9,
handler: "hello.lambda_handler",
code: aws_lambda.Code.fromAsset(path.join(__dirname, "lambda")),
timeout: Duration.seconds(30),
});
}
}
◇自動テストは全環境分を実行する
テストコード
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import * as SampleApp from "../lib/sample-app-lambda-stack";
import { EnvironmentType, getConfig } from "../lib/context";
// 開発スタックとテンプレート作成
const getTestTemplate = (env: EnvironmentType): cdk.assertions.Template => {
const app = new cdk.App();
const config = getConfig(env);
const stack = new SampleApp.SampleAppLambdaStack(app, `testStac${env}`, {
config,
});
return Template.fromStack(stack);
};
//全環境で同じテストを定義
const testSameInAllEnv = (template: cdk.assertions.Template) => {
test("作成するLambdaは1つであること", () => {
template.resourceCountIs("AWS::Lambda::Function", 1);
});
describe("「Sari-getHellowWorld-xxx」のテスト", (): void => {
test("Runtime(言語)の設定が「python3.9」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Runtime: "python3.9",
});
});
test("handler(実行コード)の設定が「hello.lambda_handler」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "hello.lambda_handler",
});
});
test("Description(説明)の設定が「HellowWorldを返却するAPI」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Description: "HellowWorldを返却するAPI",
});
});
test("Lambdaの関数実行タイムアウト時間が「30秒」であること", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Timeout: 30,
});
});
});
};
/** テスト実行 */
const templateDev = getTestTemplate("dev");
describe("開発環境のテスト", (): void => {
// 共通のテスト
testSameInAllEnv(templateDev);
// 環境ごとのテストを定義
test("開発環境はLambda名が「Sari-getHellowWorld-dev」であること", () => {
templateDev.hasResourceProperties("AWS::Lambda::Function", {
FunctionName: "Sari-getHellowWorld-dev",
});
});
});
const templateStg = getTestTemplate("stg");
describe("検証環境のテスト", (): void => {
// 共通のテスト
testSameInAllEnv(templateStg);
// 環境ごとのテストを定義
test("検証環境はLambda名が「Sari-getHellowWorld-stg」であること", () => {
templateStg.hasResourceProperties("AWS::Lambda::Function", {
FunctionName: "Sari-getHellowWorld-stg",
});
});
});
lib/context.ts
のコードは書けてないです・・・書かなきゃ・・・・
4. CloudFormationにデプロイ
CloudFormationのテンプレートファイルに変換して、各環境にデプロイする。
開発環境にデプロイ
$ cdk synth --context Env=dev
$ cdk deploy --context Env=dev --profile hoge-system_dev
- 実際にLambdaが開発環境にデプロイできた!
- Lambdaの名前は
--context
オプションで指定した環境の定義が埋め込まれて「Sari-getHellowWorld-dev」になった! - テスト実行も成功!
検証環境にデプロイ
$ cdk synth --context Env=stg
$ cdk deploy --context Env=stg --profile hoge-system_stg
- 実際にLambdaが検証環境にデプロイできた!
- Lambdaの名前は
--context
オプションで指定した環境の定義が埋め込まれて「Sari-getHellowWorld-stg」になった! - テスト実行も成功!
開発環境と検証環境が同じAWSアカウントでもうまくいくのか?
同じAWSアカウントでも、それぞれの環境用にスタックが作成されてデプロイできるのかを確認した。
$ cdk synth --context Env=dev
$ cdk deploy --context Env=dev --profile hoge-system
$ cdk synth --context Env=stg
$ cdk deploy --context Env=stg --profile hoge-system
問題なくデプロイできた!
スタック削除の確認
削除も指定した環境のスタックのみが消えていることも確認した!
$ cdk destroy cdk destroy --context Env=dev --profile hoge-system_dev
$ cdk destroy cdk destroy --context Env=stg --profile hoge-system_stg
[おまけ]コンテキストキャッシュクリア(cdk context --clear
)
cdk コマンド実行時にキャッシュファイル (cdk.context.json) が生成される。
消したい場合は以下のコマンドを実行する。
cdk context --clear
以上
まとめ(ただの感想)
AWS構成を考える(図) → 詳細設計(テスト期待値で記載) → リソースを実装
の流れで効率的に開発だできそうな印象を持てました。
ワークショップのページにLambdaがあったので試しにやってみましたが、サーバーレスアプリケーションはAWS SAMを使うとローカルで実行もできるので、適宜使い分けがいるのかなと思いました。
今後やっていきたいこと
- 複雑な構成で練習する
- 自動テストの期待値をもっとかけるようにする(=設計力)
- CDK Pipelinesを使って自動的にビルド、テスト、デプロイする
- AWS SSOで認証情報を管理して、AWS CDKを使う
参考サイト