LoginSignup
2
2

AWS CDKのTIPS集

Last updated at Posted at 2023-08-08

こんにちは。最近AWS絶賛学習中です。

今回は、とくにCloudFormation(CFn)やAWS CDKを使っていて、備忘として整理しておいた方がいいものをTIPS集として記載していきます。

TIPS集

AWS CDK の仕組みをザックリ

AWS CDKは、コンソールから cdk deploy だったり、TypeScriptのプロジェクト上から yarn cdk deploy とかで実行されたりしますが、実行されるとまず、自分がいるディレクトリの cdk.json が読み込まれて、app にあるコマンドが実行されるっぽい。

TypeScriptのプロジェクトって例えば下記の通りになっていたりするので、

cdk.json
{
  "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 が実行されたんですね。

image-20230804222142014

そのなかで lib/cdk-samples-stack.ts にあるCdkSamplesStack がよびだされ、結果

lib/cdk-samples-stack.ts
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 に複数のスタックが定義されている場合

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', {})

こんな風にCdkSamplesStack1CdkSamplesStack2 などと複数のスタック名が定義されている場合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では

VPCStack.yaml
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に記述しておいて、

参照したい側では下記のように

SubnetStack.yaml
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でやるには以下のようにします。

lib/VPCStack.ts
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する側は次のように、コンストラクタでそのオブジェクトへの参照もらうように作ります。

lib/SubnetStack.ts
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です。

bin/cdk-samples.ts
#!/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 ) には

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を作っておくと便利かもしれませんね。

Utils.ts
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 になっていますね。成功です。

参考:

CFnやAWS CDKの構成情報をドキュメント化する

Pythonのライブラリがあったので使ってみたところを記事にしました。

今後も適宜TIPSを追記していきます。お疲れさまでした!

関連リンク

  1. (あ、その場合普通はCdkSamplesStack も別の名前の時が多いでしょうけど)

  2. 実際CFnでもCDKでも、VPCとSubnetを別スタックにすることはないでしょうから、ユースケースとしてはイマイチですが。。

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2