AWS CDKについて学ぶ→TypeScriptでTDDを意識して実践→CDKプロジェクトの環境別のデプロイを考えてみた


# はじめに

[AWS CDK](https://aws.amazon.com/jp/cdk/)は、AWS リソース（インフラやサーバーなど）をコードから構築できる開発キットで、このAWS CDKを使ってAWS リソースを構築したくて勉強しました。

個人的にAWS公式サイトは迷子になるので、勉強したことを残しておきたくて記事にしました。
（ただのやってみた系の記事です）


## やったこと
- AWS CDKについて理解する
- AWS CDKを使った開発方法を理解して、実践する
    - テストコードも書く(TDDを意識)
- 環境(開発・検証など)が分かれている場合の実装方法を考えて実践する

※ 開発方法の理解が目的なので、記事内で出現するコードは簡単な内容で実装しています。
※ [CDK Pipelines](https://aws.amazon.com/jp/blogs/news/deploying-a-cdk-application-using-the-cdk-pipelines-modern-api/)を使った自動デプロイはやっていません。


## 前提知識

AWS CloudFormationの知識
AWS CloudFormationについては[こちら](https://qiita.com/marumeru/items/7f548ae2fe1b4193e662)の記事にまとめています。


## 筆者の動作環境

- **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環境にデプロイする前にテストできる

## 開発の流れ
1. CDK プロジェクトを作成(`cdk init`)
1. AWS リソースのコードを実装
1. CloudFormationのテンプレートに変換(`cdk synth`)
1. CloudFormationにデプロイ(`cdk deploy`)
   → デプロイするとCloudFormationのスタックが作成され、AWS環境にリソースを作る


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/2c2ca946-74d2-36d9-1ca5-b19e19d1fa44.png" width="850" />


## CDK プロジェクトの３つの要素

- **App(CDK アプリケーション)**
    - スタック同士の依存関係などスタックの管理をしている
 
- **Stack(スタック)**
  - AWS環境へのデプロイの単位で、CloudFormationのスタック(テンプレート)に相当する

- **Construct(コンストラクト)**
    - デプロイするAWS リソースの定義こと
    - スタック内に、**Construct Library** を使ってAWS リソースを定義していく


こちらの記事がとてもわかりやすかったです。

https://dev.classmethod.jp/articles/aws-cdk-construct-explanation/


### Construct Library(コンストラクト ライブラリ)とは？

AWS リソースを実装するためのライブラリで、AWS リソースごとに用意されている
(例)Lambda コンストラクト ライブラリ

- 使い方は[**AWS CDKのAPIリファレンス**](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html) に定義されている

- コンストラクト ライブラリは３種類ある

    - **L1 コンストラクト (Low Level Construct)**
        - AWS リソースのプロパティ値を明示的に設定する必要がある
        - コードが長くなるが細かい設定ができる
        - クラス名にCfnプレフィックスがついている
    
    - **L2 コンストラクト (High Level Construct)**
        - 便利なメソッドが提供されていて、直感的にコーディングしやすい
        - デフォルトの設定がされていて、細かい設定が省略できる
          → よしなにAWS リソースをベストプラクティスで作成してくれる
        - サポートされていないAWS リソースがある
    
    - **L3 コンストラクト (Patterns)**
        - 複数のAWS リソースを組み合わせてパターン化されたAWS環境を作れる

基本的にL2を使い、細かい設定をするならL1のライブラリを選ぶだったような・・（たしか）


## AWS CDKの自動テストについて

テストは２種類ある。

### アサーションテスト

実装したコードのAWS リソースの設定が、期待値と一致するかなどテストする。

詳細には「CDK スタック インスタンスのプロパティ値」と「変換されるCloudFormationテンプレートのプロパティ値」を比較しているようである。

テストの実装は**CDK assertionsモジュール**を使って行う。
AWS CDKのAPIリファレンスの[assertionsモジュールのページ](https://docs.aws.amazon.com/cdk/api/v1/docs/assertions-readme.html)に定義されている。

### スナップショットテスト
以前に作成したCloudFormationテンプレートと比較して差分があるかテストする。

### テストについての公式ドキュメント

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/testing.html

https://pages.awscloud.com/rs/112-TZM-766/images/CDK%E3%81%A7%E3%82%82%E3%83%86%E3%82%B9%E3%83%88%E3%81%8C%E3%81%97%E3%81%9F%E3%81%84.pdf


# AWS CDK コマンド

CDK コマンドを使ってCDKの実行を行う。
CDK コマンドは`cdk --help`で確認できる。



## よく使いそうなコマンド一覧

| CDK コマンド | 説明 |
|:---------|:-----|
| cdk init [TEMPLATE]             | CDK プロジェクトを作成(テンプレートを省略可) |
| cdk bootstrap [ENVIRONMENTS..]  | デプロイ設定(アカウント、リージョンごとに１回設定) |
| 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のスタックが１つの場合は、スタック名の省略が可能
- こちらの記事に全てのコマンドについて詳細に書かれていました

https://dev.classmethod.jp/articles/aws-cdk-command-line-interface/


<br/>



# [実践]AWS CDK のインストール

## 事前準備

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html

AWS CDKを使用するには、以下のインストールが必要(2022年12月時点)

- **Node.js 10.13.0 以降**
  長期サポートが有効な最新バージョンがおすすめとのこと
  ※バージョン 13.0.0 〜 13.6.0 は互換性の問題があるためサポートしていない

- [**AWS CLI**](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-welcome.html)
  設定手順は[こちらの記事](https://qiita.com/marumeru/items/ee61e90ac5d790679e27)にまとめています。
    


## AWS CDK をインストールする

```bash:インストール
$ npm install -g aws-cdk
```

```bash:バージョン確認
$ cdk --version
2.53.0 (build 7690f43)
```


### AWS CDK のバージョンについて

推奨のバージョンについては公式サイトで確認できる。
2022年12月時点では**v2を使うこと**を勧められている。

https://docs.aws.amazon.com/cdk/v2/guide/migrating-v2.html


```bash:インストール可能なバージョンの確認
$ 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スタック名などに使われていたので、適当すぎない名前が良いかも


```bash:プロジェクト作成
$ 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!

```


### 作成されたプロジェクト構成を詳しく見てみる

```bash:プロジェクト構成
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の設定ファイル

```

<details><summary>もっと詳細を見る！</summary><div>

- TypeScriptのインストールと設定がされていた
- gitの設定もされていて、パッケージの初期の状態でコミットされていた

    ```bash: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 リポジトリ作成は行われない

  ```bash: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クラスを追加した場合、このファイルにインスタンス化するコードを追加する
    - アプリケーション実行時に最初に呼びされる
      ```json:sample-app/cdk.json
       "app": "npx ts-node --prefer-ts-exts bin/sample-app.ts",
      ```
      :::note info
        ts-nodeを利用するとJavaScriptにコンパイルせずにTypeScriptのファイルのまま実行することができる。
      :::

</div></details>


### プロジェクト設定（好み） - Node.jsのバージョン固定

現時点で最新のバージョンの`18.12.1`を使うので、package.jsonに設定しておく。
Node.jsのバージョン管理ツールは`Volta`を使っているので以下で設定した。
    
```bash
$ volta pin node@v18.12.1
```
    
```json:package.json
 "volta": {
    "node": "18.12.1"
  }
```



## 2. デプロイ設定(`cdk bootstrap`)

AWS CDKでデプロイするときは、デプロイ用のS3などの準備が必要である。
AWSアカウントとリージョン毎に`cdk bootstrap`コマンドを実行して準備する。
(1回設定した次から設定は不要)

### 名前付きプロファイルを指定して設定する

#### (1)名前付きプロファイルを設定する

手順はこちら

https://qiita.com/marumeru/items/ee61e90ac5d790679e27


```config:設定したプロファイル
$ 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`コマンドを実行

```bash
$  cdk bootstrap --profile hoge-system_dev
```

:::note warn
プロファイルで[default]に設定しなかったので、
`cdk deploy`などのAWS環境と接続するコマンドは`--profile`で対象の環境を指定する
:::



## 3. 実装 (VPCを作成する)

TDDを意識してコーディングしました。

### (1)テストコードを書く

- APIリファレンスの[**CDK assertionsモジュールのページ**](https://docs.aws.amazon.com/cdk/api/v1/docs/assertions-readme.html)を見ながらテストを書く

- テストの期待値は、CloudFormationのテンプレートのプロパティ値を書いていくので、Cloud Fromationの[**公式リファレンス**](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html)を見るのが、個人的には効率よくかけた


- ひとまず、VPCにサブネットを２つを作るコードを最低限で書いてみる

```typescript:test/sample-app.test.ts
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のページ**](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html)を見ながらコードを書く


```typescript:lib/sample-app-stack.ts
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構成で２つ作成される)
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "SariVpc_PublicSubnet",
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    });
  }
}
```


### **(3)自動テスト実行**

```bash:テスト実行
$ 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)
```

デプロイする前に、間違いや想定との乖離に気がつけるので効率が良い！


:::note info 
**[補足]TypeScriptコンパイル**
TypeScriptで記述されたコードをJavaScriptにコンパイルする。

- リアルタイムでコンパイルする場合`npm run watch`
- コンパイルを都度実行したい場合 `npm run build`

(tscコマンドはプロジェクト作成時に自動でpackage.jsonに設定されている)

```bash:package.json
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
```
:::


### **(4)(1)〜(3)を繰り返す**

(1)〜(3)を繰り返して開発する。
[こちらのサイト](https://techblog.styleedge.co.jp/entry/2022/07/19/163825)のテストコードがとても分かりやすかったです。参考にさせていただきました。



## 4. CloudFormationのテンプレートに変換(`cdk synth`)

前章で貼り付けたコードを変換する


```bash
$ cdk synth
```

```bash:出力されたファイル
cdk.out/
├── SampleAppStack.assets.json
├── SampleAppStack.template.json
├── cdk.out
├── manifest.json
└── tree.json
```


## 5. CloudFormationにデプロイ(`cdk deploy`)

- プロファイル（AWS環境）を指定して、デプロイを実行する
- CDKのスタックが１つしかないので、`cdk deploy`コマンド実行時にスタック名の指定は省略した

```bash:デプロイ
$ 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
```

<br/>

## 6. デプロイされたリソースを確認

AWS マネジメントコンソールから確認する。

- CloudFormationのスタックが作成されていた！
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/7332faa3-0480-f8a4-a5b3-11f0ae6ca83d.png" width="800" />
    
- 作成されたリソース
    - 指定したリソースが作成されていた(VPC 1つとサブネット2つ)
    - 指定していないリソースも作成されていた（これがよしなに作成してくれるやつ）
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/9fd49484-f97f-1103-daa2-2fb98ddaf31d.png" width="700" />

**リソースの詳細確認**

- パブリックサブネット
  作るAZは指定してないが、それぞれ別のAZに作られマルチAZ構成になっていた！
  すごい！
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/54f2577c-8916-5111-1bca-dd40f897b0e1.png" width="700" />

- IGW
  IGWの作成は明示的に指定していないが自動で作ってくれていた！
  `subnetType: ec2.SubnetType.PUBLIC`の設定から判断してくれたと思う。
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/e7b23a41-92e8-bc1f-1994-ffb8bccc64b3.png" width="700" />

- ルート設定
  メインルートテーブル(1つ)とカスタムルートテーブル(２つ)が作られていた！
 
  カスタムルートテーブル(２つ)に、それぞれのサブネットとIGWのルート設定がされていた！
  ルートはメインルートテーブルに設定せず、カスタムルートテーブルに設定するのがベストプラクティス。指定しなくてもここまで設定してくれるとは・・・！

  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/72d7057a-1ad6-d552-1443-c275d6afd057.png" width="700" />



# [実践]CDK プロジェクト修正〜デプロイ　(Lambda)

**やること**
CDK プロジェクトを作成 → Lambdaを構築するコードを書く → AWS環境にリソースデプロイ(スタック作成) → Lambdaを構築したコードを修正 → AWS環境にリソースデプロイ(スタック更新)


## 1. CDK プロジェクトを作成(`cdk init`)

```bash:プロジェクト作成
$ mkdir sample-app-lambda

$ cd sample-app-lambda/

$ cdk init --language typescript
```

:::note warn
同じAWSアカウントとリージョンなので、`cdk bootstrap`　はしなかった
:::



## 2. 実装 (Lambdaを作成する)

Lambdaのコードも一緒にデプロイしたいので、プロジェクトに手動でLambdaのコードを追加する。
LambdaのコードはPythonで記載した。（特に理由なし）

```bash:プロジェクト構成
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のページ**](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html)
- APIリファレンスの[**Lambdaのページ**](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda-readme.html)


```typescript:test/sample-app-lambda.test.ts
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",
    });
  });
});

```

```typescript:lib/sample-app-lambda-stack.ts
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);

    // 第２引数はリソースの論理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 にアップロードされる)
    });
  }
}

```

```python:lib/lambda/hello.py
import json

def lambda_handler(event, context):

    return {
        "statusCode": 200,
        "body": json.dumps('Hello from Lambda!')
    }
```



## 3. CloudFormationのテンプレートに変換(`cdk synth`)

```bash
$ cdk synth
```


## 4. CloudFormationにデプロイ(`cdk deploy`)

Lambda用のIAMロール作るよって聞かれたのでYesにした。
(IAMロール必要だけどコードに書いてなかった)

```bash:デプロイ
$ 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 マネジメントコンソールから確認する。

- CloudFormationのスタックが作成されていた！
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/567886a3-7735-c4f0-f580-e00b24022a4a.png" width="700" />

**作成されたリソースの詳細を見る**
- Lambda用のIAMロール
  IAMロールにアタッチされたポリシーは「CloudWatch Logs」の書き込み権限のみで、Lambdaのログを「CloudWatch Logs」に書き込む用と思われる。
  必要最低限の権限が付与されたロールが自動で作られていた！
  すごい..

  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/4bfd0a73-2a02-8a68-547d-667724112278.png" width="700" />


- Lambda
  一緒にデプロイしたコードもアップされていた！
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/6de9c1ad-106d-5e36-48a1-145bd05963c8.png" width="700" />

  自動で作成されたIAMロールもアタッチされていた。。！
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/59f24a46-1c84-a720-1a6c-f20b82d80cd3.png" width="700" />



## 6. デプロイしたLambdaをテスト実行してみる
Lambdaを画面からテスト実行すると以下のエラーになった。
「lambda_functionがない」とのことで、これは単純なhandlerの設定ミス・・なので修正してデプロイし直す。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/0db09848-9429-96d8-e858-5e6a850846f0.png" width="700" />



## 7. コード修正

handlerの設定を「hello.lambda_handler」に修正

```typescript:lib/sample-app-lambda-stack.ts
handler: "hello.lambda_handler",
```

せっかくなので、Lambdaのレスポンスも変えてみる

```python:lib/lambda/hello.py
import json

def lambda_handler(event, context):

    return {
        "statusCode": 200,
        "body": json.dumps('Hello world!Hello world!') #ここを修正
    }
```

もちろんテストも修正済（テストコードは省略）



## 8. スタックの差分を確認(`cdk diff`)

- CloudFormationの変更セットにあたる確認
- 修正されるリソースの設定を確認することができる

```bash
$ 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のスタックを更新する。

```bash:デプロイ
 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`で、変更セットの名前を指定できる
  ※変更セットについては[「変更セットを使用したスタックの更新」](https://qiita.com/marumeru/items/7f548ae2fe1b4193e662#%E5%A4%89%E6%9B%B4%E3%82%BB%E3%83%83%E3%83%88%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E6%9B%B4%E6%96%B0)に記載している。
- **変更セットを作成した後、そのままスタックを更新していた**
  変更セットだけ作成のCDK コマンドはなさそうでした。あれば知りたい・・・



## 10. デプロイされたリソースを確認

AWS マネジメントコンソールから確認する。

- Lambdaのソースが更新されていた
     <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/1969979f-524f-489d-aa90-844d8e11d9d0.png" width="700" />

- 「handler」の設定も変更されていたので、Lambdaをテスト実行すると成功した
     <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/a008ad47-093c-01eb-1c77-f22e06bd0d2f.png" width="700" />



# [実践]スタックを削除(`cdk destroy`）

`cdk destroy`で、以下が全て削除される。
- CloudFormationのスタック
- スタックから作成されたAWS リソース

```bash
$ cdk destroy --profile hoge-system_dev
Are you sure you want to delete: SampleAppLambdaStack (y/n)? y
SampleAppLambdaStack: destroying...

 ✅  SampleAppLambdaStack: destroyed
```



# [実践]１つのCDKプロジェクトで開発・検証・本番の各環境にデプロイしたい

実案件では１つのCDK プロジェクトで開発環境 (dev)、ステージング環境 (stg)、本番環境 (prod)などの複数環境にデプロイすることになる。AWS リソースによっては、世界中で一意の名前にしないといけないものがあり、開発・検証・本番でそれぞれ名前を変えないといけない。

CloudFormationを使った時に「リソース名の一部をプレフィックスにして、デプロイ時に外部パラメータで受け取った値を埋め込む」などの対応していたので、同じようなことがしたい。


## まず方法を調べる

context変数を使って、環境を切切り替える記事を見つけた。

https://maku.blog/p/vx5ta85/#%E5%BF%9C%E7%94%A8-%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%83%90%E3%83%AA%E3%83%A5%E3%83%BC%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%AE%9F%E8%A1%8C%E7%92%B0%E5%A2%83-stagingproduction-%E3%82%92%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%82%8B


## context変数とは？

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/get_context_var.html

### ◇コマンドラインのcontext変数(`--context`オプション)

`--context`オプションで変数を渡すことができる。変数はキー＆バリューの形式。

```bash:例
$ cdk deploy --context Env=prod # 本番環境用スタックの生成
```

### ◇`cdk.json`に定義

`cdk.json`のcontextにcontext変数をキー＆バリューで記述するして、アプリオブジェクトからcontext変数にアクセスできる。

```json:cdk.json
{
  "context": {
    "bucket_name": "myotherbucket"
  }
}
```

```typescript:呼び出し側のコード(bin/*.ts)
const app = new cdk.App();
const bucket_name = app.node.tryGetContext('bucket_name')
```


## 実践

**やること**
- `--context`オプションで実行環境を指定して、環境毎の設定でリソースをデプロイする
- 各環境の設定は専用のクラスを作成して定義する(`cdk.json`の肥大化を避けるため）
- 手軽に試したいので、開発環境と検証環境の２つで行う
- AWS リソースはLambdaを作成する
  （全世界で名前を一意にする必要があり、前章でやったので）

```bash:ゴール(デプロイのコマンド)
$ cdk deploy --context Env=stg --profile hoge-system_stg # ステージング用
$ cdk deploy --context Env=dev --profile hoge-system_dev  # 開発環境用
```


調査の時に見つけた[サイト](https://maku.blog/p/vx5ta85/)のコードを参考にさせていただきました。


### 1. CDK プロジェクトを作成(`cdk init`)

手順などは省略

### 2. contextを扱うクラスを作成

`lib/context.ts`を手動で作成して、定義を記載する。

```typescript: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を作成)

実装したコードのみ貼り付け

#### ◇環境ごとにスタックを作成する

```typescript:bin/sample-app-lambda.ts
#!/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);

```
:::note
**[補足]CDKで作成するリソースに一括タグ付けできる**
「SCOPE」にapp、stack、constructを指定できる。

```typescript
Tags.of(SCOPE).add('key', 'value');
```

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/tagging.html

:::

#### ◇Lambdaを作成する

Lambdaの論理IDと名前も環境ごと変える

```typescript:lib/sample-app-lambda-stack.ts
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;

    // 第２引数はリソースの論理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),
    });
  }
}

```

#### ◇自動テストは全環境分を実行する

**実行結果**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/c4f479ac-0923-4cbb-e7a2-285371485191.png" width="700" />

**テストコード**

```typescript:test/sample-app-lambda.test.ts
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のテンプレートファイルに変換して、各環境にデプロイする。

#### 開発環境にデプロイ

```bash
$ cdk synth --context Env=dev

$ cdk deploy --context Env=dev --profile hoge-system_dev
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/0ebdacb2-9fc2-f1f7-db7c-6dad365486a2.png" width="700" />

- 実際にLambdaが開発環境にデプロイできた！
- Lambdaの名前は`--context`オプションで指定した環境の定義が埋め込まれて「Sari-getHellowWorld-dev」になった！
- テスト実行も成功！


#### 検証環境にデプロイ

```bash
$ cdk synth --context Env=stg

$ cdk deploy --context Env=stg --profile hoge-system_stg
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/340c6b09-6d76-fc50-21ee-a56e64cd7f60.png" width="700" />

- 実際にLambdaが検証環境にデプロイできた！
- Lambdaの名前は`--context`オプションで指定した環境の定義が埋め込まれて「Sari-getHellowWorld-stg」になった！
- テスト実行も成功！


#### 開発環境と検証環境が同じAWSアカウントでもうまくいくのか？

同じAWSアカウントでも、それぞれの環境用にスタックが作成されてデプロイできるのかを確認した。

```bash
$ 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
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728837/ba3f5fcb-9bc9-4d2b-d7f9-1ca605047060.png" width="700" />

問題なくデプロイできた！



#### スタック削除の確認

削除も指定した環境のスタックのみが消えていることも確認した！

```bash
$ 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) が生成される。
消したい場合は以下のコマンドを実行する。

```bash
cdk context --clear
```

<br/>

以上

---

# まとめ(ただの感想)


AWS構成を考える(図) → 詳細設計(テスト期待値で記載) → リソースを実装
の流れで効率的に開発だできそうな印象を持てました。


[ワークショップのページ](https://cdkworkshop.com/ja/20-typescript/30-hello-cdk/200-lambda.html)にLambdaがあったので試しにやってみましたが、サーバーレスアプリケーションは[AWS SAM](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html)を使うとローカルで実行もできるので、適宜使い分けがいるのかなと思いました。


## 今後やっていきたいこと

- 複雑な構成で練習する
- 自動テストの期待値をもっとかけるようにする(=設計力)
- [CDK Pipelines](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/cdk_pipeline.html)を使って自動的にビルド、テスト、デプロイする
- AWS SSOで認証情報を管理して、AWS CDKを使う
 
https://logmi.jp/tech/articles/326907



# 参考サイト

https://zenn.dev/marumarumeruru/articles/e4255f20042b9b

https://techblog.styleedge.co.jp/entry/2022/07/19/163825

https://dev.classmethod.jp/articles/aws-cdk-command-line-interface

https://dev.classmethod.jp/articles/aws-cdk-construct-explanation/

https://maku.blog/p/vx5ta85/