AWS CDKのTIPS集

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

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

## TIPS集

### AWS CDK の仕組みをザックリ

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

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


```json: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 する](https://qiita.com/masatomix/items/8fc3d52ee89489967c09) の記事で ``yarn cdk deploy`` したとき、下記の``bin/cdk-samples.ts`` が実行されたんですね。

![image-20230804222142014](https://imgur.com/IrXmibO.png)

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

```TypeScript: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 に複数のスタックが定義されている場合


```typescript: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]

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

```console
$ 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`` って書いてね、って怒られちゃいます。ってことで、

```console
$ 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を指定するやり方があります。

```yaml
  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です。


```typescript
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` }],
    })
  }
}

```

このように

```typescript
    new CfnSubnet(this, `MyPublicSubnet`, {
      vpcId: vpc.ref,
      ...
```

ってやるだけでOK。実際に下記のコマンドでCFnのテンプレートファイルを生成してみると、

```yaml
% 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
```

(不要な情報除去していますが) おお、ちゃんと

```yaml
      VpcId:
        Ref: MyVPC
```

が出力されました。なんだかとっても直感的ですね。



### 別のスタックの値を参照したい

さっきは同スタック内のVPCをどう参照するかでしたが、今度は別のスタックの場合です。CFnでもVPC関連を作った後にそこにEC2インスタンスを立てたいとき、別ファイルで作成されたVPCの情報が必要だったりします。

こんな「別のスタックで作ったリソースの情報を使いたい」場合は、CFnでは

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

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

```yaml: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]。

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

さて、これをAWS CDKでやるには以下のようにします。


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

```typescript: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です。

```typescript: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を実行してみます。

```console
% 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された情報も見てみます。

```console
$ 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文とかでプログラマブルに繰り返しができるのも、とっても好印象です。

参考:
- [CDKでほかのスタックのリソースを参照したいとき](https://zenn.dev/5hintaro/articles/e8a20c171bf8fd)
- [実践！AWS CDK #27 スタック分割](https://dev.classmethod.jp/articles/cdk-practice-27-split-stack/)



### 環境ごとに設定を分ける

開発環境(develop)とか、本番前環境(staging)とか本番環境(production)とか、環境によって設定を変えたいなんてことがあります。本番用VPCには Nameタグを``production-vpc``,ステージング用VPCは``staging-vpc`` ってつけたい、とかですね。

AWS CDKには コンテキスト変数って仕組みがあって(参考: [コンテキスト変数から値を取得](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/get_context_var.html)) 

``cdk deploy --context profile=stg``

とかオプションを渡すことで設定を切り替えることが可能です。
設定ファイル(``cdk.json`` ) には

```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プロパティの値を取り出す機能があります。

```typescript
const dev = this.node.tryGetContext('dev') // { "name": "develop" }
const stg = this.node.tryGetContext('stg') // { "name": "staging" }
```
こんな感じのシンプルな機能です。
一方で、最初に書いた ``--context profile=stg``  の情報も、プログラム上

```typescript
const profile = this.node.tryGetContext('profile') // stg
```

ってアクセスすることができます。``--context``は動的に ``cdk.json``のcontextプロパティに情報を追記する機能のようですね。

したがって、下記のコードで

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

```typescript: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の情報を取得できます。

```typescript
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
  }
}
```

一応実行してみます。

```console
$ 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`` になっていますね。成功です。

参考:

- [コンテキスト変数から値を取得](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/get_context_var.html)
- [AWS CDK で外部パラメーターを扱う（コンテキスト・バリューと環境変数）](https://maku.blog/p/vx5ta85/)
- [cdk コマンドの機能を 実際に叩いて理解する 【 AWS CDK Command Line Interface 】](https://dev.classmethod.jp/articles/aws-cdk-command-line-interface/#toc-13)


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

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

https://qiita.com/masatomix/items/895b335cc31165214562


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




## 関連リンク 

- [WSL環境にNode.js環境を構築する](https://qiita.com/masatomix/items/d69ac664a2fcbdb98f6c) 
- [WSL環境にAWS コマンドラインインターフェイス(AWS CLI)を構築する](https://qiita.com/masatomix/items/7a1af1f6fc28258927ae) 
- [AWS CDK で Infrastructure as Code する](https://qiita.com/masatomix/items/8fc3d52ee89489967c09)