こんにちは。最近AWS絶賛学習中です。
今回は、とくにCloudFormation(CFn)やAWS CDKを使っていて、備忘として整理しておいた方がいいものをTIPS集として記載していきます。
TIPS集
AWS CDK の仕組みをザックリ
AWS CDKは、コンソールから cdk deploy
だったり、TypeScriptのプロジェクト上から yarn cdk deploy
とかで実行されたりしますが、実行されるとまず、自分がいるディレクトリの cdk.json
が読み込まれて、app
にあるコマンドが実行されるっぽい。
TypeScriptのプロジェクトって例えば下記の通りになっていたりするので、
{
"app": "npx ts-node --prefer-ts-exts bin/cdk-samples.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
...
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
... 省略
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true
}
}
なるほど、だから AWS CDK で Infrastructure as Code する の記事で yarn cdk deploy
したとき、下記のbin/cdk-samples.ts
が実行されたんですね。
そのなかで lib/cdk-samples-stack.ts
にあるCdkSamplesStack
がよびだされ、結果
import * as cdk from 'aws-cdk-lib'
import { CfnVPC } from 'aws-cdk-lib/aws-ec2'
import { Construct } from 'constructs'
export class CdkSamplesStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
new CfnVPC(this, 'MyVPC', {
cidrBlock: '192.168.0.0/16',
tags: [{ key: 'Name', value: `test-vpc` }],
})
}
}
が実行されるってことですね。なるほど。
bin/cdk-samples.ts に複数のスタックが定義されている場合
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { CdkSamplesStack } from '../lib/cdk-samples-stack'
const app = new cdk.App()
new CdkSamplesStack(app, 'CdkSamplesStack1', {})
new CdkSamplesStack(app, 'CdkSamplesStack2', {})
こんな風にCdkSamplesStack1
、CdkSamplesStack2
などと複数のスタック名が定義されている場合1
$ cdk deploy
Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: CdkSamplesStack1 · CdkSamplesStack2
$
アプリには1個以上スタックが定義されているからちゃんと指定してね、もしくは --all
って書いてね、って怒られちゃいます。ってことで、
$ cdk deploy CdkSamplesStack2
✨ Synthesis time: 2.72s
CdkSamplesStack2: deploying... [1/1]
CdkSamplesStack2: creating CloudFormation changeset...
✅ CdkSamplesStack2
✨ Deployment time: 26.54s
$
このように、明示的に指定してそのスタックを実行するようにしましょう。ちなみに--all
を指定したときは上から順番、になるっぽい?ですね。
実運用としては、メインのコード(bin/cdk-samples.ts
) で複数のスタックを定義しておいて、都度都度、実行したいスタック名を指定すればいいわけですね、なるほど。
同じスタックの値を参照したい
「CFnでつくったVPCに、Subnetを追加したい」とき、Subnetを作成するスタックでVPCを指定するやり方があります。
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/16
Tags:
- Key: Name
Value: test-vpc
MyPublicSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1
CidrBlock: 192.168.0.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: public-subnet
VpcId:
Ref: MyVPC
いわゆるRef のことですね。これをAWS CDKではどうやるかですが、シンプルにvpc.ref
って参照を指定してあげればOKです。
import { App, Stack, StackProps } from 'aws-cdk-lib'
import { CfnSubnet, CfnVPC } from 'aws-cdk-lib/aws-ec2'
export class VPCStack extends Stack {
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props)
const vpcCIDRs = { vpc: '192.168.0.0/16' }
const subnetCIDR = { public: '192.168.0.0/24', private: '192.168.1.0/24' }
const vpc = new CfnVPC(this, 'MyVPC', {
cidrBlock: vpcCIDRs.vpc,
tags: [{ key: 'Name', value: `test-vpc` }],
})
new CfnSubnet(this, `MyPublicSubnet`, {
vpcId: vpc.ref,
cidrBlock: subnetCIDR.public,
availabilityZone: 'ap-northeast-1',
mapPublicIpOnLaunch: true,
tags: [{ key: 'Name', value: `public-subnet` }],
})
new CfnSubnet(this, `MyPrivateSubnet`, {
vpcId: vpc.ref,
cidrBlock: subnetCIDR.private,
availabilityZone: 'ap-northeast-1',
mapPublicIpOnLaunch: false,
tags: [{ key: 'Name', value: `private-subnet` }],
})
}
}
このように
new CfnSubnet(this, `MyPublicSubnet`, {
vpcId: vpc.ref,
...
ってやるだけでOK。実際に下記のコマンドでCFnのテンプレートファイルを生成してみると、
% cdk synth VPCStack
Resources:
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/16
Tags:
- Key: Name
Value: test-vpc
MyPublicSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1
CidrBlock: 192.168.0.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: public-subnet
VpcId:
Ref: MyVPC
MyPrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1
CidrBlock: 192.168.1.0/24
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: private-subnet
VpcId:
Ref: MyVPC
(不要な情報除去していますが) おお、ちゃんと
VpcId:
Ref: MyVPC
が出力されました。なんだかとっても直感的ですね。
別のスタックの値を参照したい
さっきは同スタック内のVPCをどう参照するかでしたが、今度は別のスタックの場合です。CFnでもVPC関連を作った後にそこにEC2インスタンスを立てたいとき、別ファイルで作成されたVPCの情報が必要だったりします。
こんな「別のスタックで作ったリソースの情報を使いたい」場合は、CFnでは
Resources:
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/16
Tags:
- Key: Name
Value: test-vpc
Outputs:
ExportsOutputRefMyVPC3A1A60EC:
Value:
Ref: MyVPC
Export:
Name: VPCStack:ExportsOutputRefMyVPC3A1A60EC
このように他のスタックで参照してもらいたいリソース(MyVPC) を「VPCStack:ExportsOutputRefMyVPC3A1A60EC
」って名前でOutputsに記述しておいて、
参照したい側では下記のように
Resources:
MyPublicSubnet0:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: 192.168.0.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: public-subnet-0
VpcId:
Fn::ImportValue: VPCStack:ExportsOutputRefMyVPC3A1A60EC
MyPrivateSubnet0:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: 192.168.1.0/24
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: private-subnet-0
VpcId:
Fn::ImportValue: VPCStack:ExportsOutputRefMyVPC3A1A60EC
Fn::ImportValue: VPCStack:ExportsOutputRefMyVPC3A1A60EC
ってExport Nameを指定することで、別のスタックの値を参照することができます2。
さて、これをAWS CDKでやるには以下のようにします。
import { App, Stack, StackProps } from 'aws-cdk-lib'
import { CfnVPC } from 'aws-cdk-lib/aws-ec2'
export class VPCStack extends Stack {
public readonly vpc: CfnVPC // プロパティを準備
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props)
const vpcCIDRs = { vpc: '192.168.0.0/16' }
// VPC
const vpc = new CfnVPC(this, 'MyVPC', {
cidrBlock: vpcCIDRs.vpc,
tags: [{ key: 'Name', value: `test-vpc` }],
})
this.vpc = vpc // 作ったインスタンスをプロパティに保持
}
}
Exportする側はこのように、クラスのプロパティにExportしたいオブジェクトをセットし、
Importする側は次のように、コンストラクタでそのオブジェクトへの参照もらうように作ります。
import { App, CfnParameter, Stack, StackProps } from 'aws-cdk-lib'
import { CfnSubnet, CfnVPC } from 'aws-cdk-lib/aws-ec2'
export const region = 'ap-northeast-1'
export const availabilityZones = [`${region}a`, `${region}c`, `${region}d`]
const subnetCIDRs = [
{ public: '192.168.0.0/24', private: '192.168.1.0/24' },
{ public: '192.168.2.0/24', private: '192.168.3.0/24' },
{ public: '192.168.4.0/24', private: '192.168.5.0/24' },
]
export class SubnetStack extends Stack {
// コンストラクタでvpcの参照をもらうようにする
constructor(scope: App, id: string, vpc: CfnVPC, props?: StackProps) {
super(scope, id, props)
for (let index = 0; index < availabilityZones.length; index++) {
new CfnSubnet(this, `MyPublicSubnet${index}`, {
vpcId: vpc.ref, // ここは今までと同じ
cidrBlock: subnetCIDRs[index].public,
availabilityZone: availabilityZones[index],
mapPublicIpOnLaunch: true,
tags: [{ key: 'Name', value: `public-subnet-${index}` }],
})
new CfnSubnet(this, `MyPrivateSubnet${index}`, {
vpcId: vpc.ref,
cidrBlock: subnetCIDRs[index].private,
availabilityZone: availabilityZones[index],
mapPublicIpOnLaunch: false,
tags: [{ key: 'Name', value: `private-subnet-${index}` }],
})
}
}
}
で、最初に起動するbin/cdk-samples.ts
で、下記の通りオブジェクトを渡してあげればOKです。
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { VPCStack } from '../lib/VPCStack'
import { SubnetStack } from '../lib/SubnetStack'
const app = new cdk.App()
const vpcStack = new VPCStack(app, 'VPCStack')
new SubnetStack(app, 'SubnetStack', vpcStack.vpc) // vpcStackがもってるプロパティを渡している
さて AWS CDKを実行してみます。
% cdk deploy --all
VPCStack
VPCStack: deploying... [1/2]
Outputs:
VPCStack.ExportsOutputRefMyVPC3A1A60EC = vpc-09bb7adfd9ebfd23f
✨ Total time: 29.07s
SubnetStack
SubnetStack: deploying... [2/2]
✨ Total time: 23.96s
%
なんかOutputsのログが出つつ終わったようです。VPC/Subnetの作成状況も見てみます。
$ aws ec2 describe-vpcs --query "Vpcs[*].[VpcId,CidrBlock]" --output table
---------------------------------------------
| DescribeVpcs |
+------------------------+------------------+
| vpc-09bb7adfd9ebfd23f | 192.168.0.0/16 |
+------------------------+------------------+
$ aws ec2 describe-subnets \
--query "Subnets[*].[(Tags[?Key=='Name'])[0].Value,CidrBlock,SubnetId,VpcId]" \
--filters "Name=vpc-id, Values=vpc-09bb7adfd9ebfd23f" \
--output table
---------------------------------------------------------------------------------------------
| DescribeSubnets |
+------------------+-----------------+----------------------------+-------------------------+
| public-subnet-1 | 192.168.2.0/24 | subnet-0ce28a033a719cd88 | vpc-09bb7adfd9ebfd23f |
| private-subnet-2| 192.168.5.0/24 | subnet-0dd9dc90b3996c631 | vpc-09bb7adfd9ebfd23f |
| private-subnet-1| 192.168.3.0/24 | subnet-09e1c0e78ac56b3d4 | vpc-09bb7adfd9ebfd23f |
| public-subnet-0 | 192.168.0.0/24 | subnet-076fcbe27c949420d | vpc-09bb7adfd9ebfd23f |
| public-subnet-2 | 192.168.4.0/24 | subnet-000da27f7d732407a | vpc-09bb7adfd9ebfd23f |
| private-subnet-0| 192.168.1.0/24 | subnet-0ac7b541fd73a4ad3 | vpc-09bb7adfd9ebfd23f |
+------------------+-----------------+----------------------------+-------------------------+
$
ちゃんと別スタックで作成されたVPCに紐付いたサブネットが作成できていますね。
CFnで、Exportされた情報も見てみます。
$ aws cloudformation describe-stacks --stack-name VPCStack \
--query "Stacks[*].[Outputs]" \
--output table
------------------------------------------------------------------------------------------------------
| DescribeStacks |
+----------------------------------------+---------------------------------+-------------------------+
| ExportName | OutputKey | OutputValue |
+----------------------------------------+---------------------------------+-------------------------+
| VPCStack:ExportsOutputRefMyVPC3A1A60EC| ExportsOutputRefMyVPC3A1A60EC | vpc-09bb7adfd9ebfd23f |
+----------------------------------------+---------------------------------+-------------------------+
CFnでもOutputの情報が出力されているのがわかりました。
別スタックの値を参照する方法をまとめると、
- スタックを構成するクラスにプロパティをつけると、CFn的にリソースをExportできること
- コンストラクタでもらったオブジェクトを使用することで 、CFn的にリソースをImportできること
- 他のスタックがExportしたものと名前がかぶらないように「VPCStack:ExportsOutputRefMyVPC3A1A60EC」とかにしてくれること
などなど、AWS CDKの操作感はとっても直感的ですね。またExport/Importに限らず、今回サブネットを 3AZに計6つ作成しているのですが、for文とかでプログラマブルに繰り返しができるのも、とっても好印象です。
参考:
環境ごとに設定を分ける
開発環境(develop)とか、本番前環境(staging)とか本番環境(production)とか、環境によって設定を変えたいなんてことがあります。本番用VPCには Nameタグをproduction-vpc
,ステージング用VPCはstaging-vpc
ってつけたい、とかですね。
AWS CDKには コンテキスト変数って仕組みがあって(参考: コンテキスト変数から値を取得)
cdk deploy --context profile=stg
とかオプションを渡すことで設定を切り替えることが可能です。
設定ファイル(cdk.json
) には
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
...
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"dev": {
"name": "develop"
},
"stg": {
"name": "staging"
}
などと設定値を書くことができます。
プログラム上からその設定を取得/参照する方法を説明します。まずAWS CDKには、 cdk.json
上のcontextプロパティの値を取り出す機能があります。
const dev = this.node.tryGetContext('dev') // { "name": "develop" }
const stg = this.node.tryGetContext('stg') // { "name": "staging" }
こんな感じのシンプルな機能です。
一方で、最初に書いた --context profile=stg
の情報も、プログラム上
const profile = this.node.tryGetContext('profile') // stg
ってアクセスすることができます。--context
は動的に cdk.json
のcontextプロパティに情報を追記する機能のようですね。
したがって、下記のコードで
// --context profile=xxx が指定されてなかったらデフォルト値 dev にする
const profileStr = this.node.tryGetContext('profile') ?? 'dev'
// xxx もしくはdevとかで cdk.json から設定をとる
const profile = this.node.tryGetContext(profileStr)
console.log(profile.name) // stagingとかがとれる
設定を切り替えることができました。
下記みたいなUtilsを作っておくと便利かもしれませんね。
import { Stack } from 'aws-cdk-lib'
export const getProfile = (stack: Stack): any => {
// --context profile=xxx が指定されてなかったらデフォルト値 dev にする
const profileStr = stack.node.tryGetContext('profile') ?? 'dev'
// xxx もしくはdevとかで cdk.json から設定をとる
const profile = stack.node.tryGetContext(profileStr)
if (!profile) {
throw new Error(`profile=${profileStr} の環境設定が取得できず`)
}
return profile
}
こんな風にcontextの情報を取得できます。
import { App, Stack, StackProps } from 'aws-cdk-lib'
import { CfnVPC } from 'aws-cdk-lib/aws-ec2'
import { getProfile } from './Utils'
export class VPCStack extends Stack {
public readonly vpc: CfnVPC
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props)
const profile = getProfile(this) // ←ココ
const vpcCIDRs = { vpc: '192.168.0.0/16' }
// VPC
const vpc = new CfnVPC(this, 'MyVPC', {
cidrBlock: vpcCIDRs.vpc,
tags: [{ key: 'Name', value: `${profile.name}-test-vpc` }],
})
this.vpc = vpc
}
}
一応実行してみます。
$ yarn cdk deploy VPCStack --context profile=stg
Done in 46.45s.
$
$ aws ec2 describe-vpcs \
--query "Vpcs[*].[(Tags[?Key=='Name'])[0].Value, VpcId, CidrBlock]" \
--output table
-----------------------------------------------------------------
| DescribeVpcs |
+------------------+-------------------------+------------------+
| staging-test-vpc| vpc-02ee32f942d386c58 | 192.168.0.0/16 |
+------------------+-------------------------+------------------+
コード中の `${profile.name}-test-vpc`
が staging-test-vpc
になっていますね。成功です。
参考:
- コンテキスト変数から値を取得
- AWS CDK で外部パラメーターを扱う(コンテキスト・バリューと環境変数)
- cdk コマンドの機能を 実際に叩いて理解する 【 AWS CDK Command Line Interface 】
CFnやAWS CDKの構成情報をドキュメント化する
Pythonのライブラリがあったので使ってみたところを記事にしました。
今後も適宜TIPSを追記していきます。お疲れさまでした!