AWSリソースはCDKでの構築にしませんか？

## CFnテンプレートの修正って大変よね
CFn（CloudFormation）を触っててこんなことありませんか？
 - json/yamlの文法ミスってた...
 - プロパティ足りてなくてエラーになった...
 - 他にもIAMとか付随するサービスの定義も必要だった...
 - テンプレートだと自由度が低くて痒い所に手が届かない...

そんなときこそCDKです！

## CDKとは？
「CFnテンプレートをコードで書ける」って代物です！
だいぶざっくりとした説明なので詳しくって方は公式docをご参照ください。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

## さっそく使ってみるゾ
WorkShopを参考にしつつ触ってみたいと思います。
https://catalog.workshops.aws/typescript-and-cdk-for-beginner/ja-JP/40-cdk-introduction

お試し利用なので、デプロイリソースは単一のLambda関数にします。

### 環境セットアップ
作業前の環境は以下で、cli操作は`Git Bash`、cdkの実装は`typescript`にて行っていきます。
 - OS：`Win11`
 - AWS CLI：`2.13.37`
 - npm：`10.2.4`
 - git：`2.41.0.windows.3`

追加でCDK開発に必要なパッケージをインストールしておきます。
```
$ npm install -g aws-cdk
$ npm install -g typescript ts-node
```

### CDKプロジェクトの立ち上げ
適当にプロジェクト用のディレクトリを作ります。
```
$ mkdir cdk-workshop
$ cd cdk-workshop
```
新規でプロジェクトをセットアップします。
ディレクトリの雛形ができるので、後は実装していくだけです。
```
$ cdk init sample-app --language typescript
```

いろいろとファイルはあるのですが、重要なのは以下の2つ（`bin`, `lib`）です。
```
cdk-workshop/
├── bin/
│   └── cdk-workshop.ts
├── lib/
│   └── cdk-workshop-stack.ts
```
`bin`配下のファイル（`cdk-workshop.ts`）は実際にデプロイする際のエントリーポイントとなっており、このファイルから`lib`配下のファイルを呼び出します。
実際にデプロイするリソース（スタック）の定義は`lib`配下で記述します。

実際に`cdk-workshop.ts`を見ると、`CdkWorkshopStack`（`cdk-workshop-stack.ts`）をimportし、インスタンスを生成しているのがわかるかと思います。
```
[cdk-workshop.ts]

import * as cdk from 'aws-cdk-lib';
import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';

const app = new cdk.App();
new CdkWorkshopStack(app, 'CdkWorkshopStack');
```
### lambdaを定義してみる
`cdk-workshop-stack.ts`を修正し、lambdaの定義を記述してみます。
リソース毎に記述方法は異なるので、基本的には公式リファレンスを参考に定義を記述していきます。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html

今回はお試しなので適当なコードをインラインコードとして記述しちゃいました。
```
[cdk-workshop-stack.ts]

import { Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class CdkWorkshopStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const lambdaFunction = new lambda.Function(this, 'LambdaFunction', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: new lambda.InlineCode(`
        exports.handler = async (event) => {
          return {
            statusCode: 200,
            body: 'Hello, CDK Lambda!'
          };
        };
      `)
    });
  }
}

```


### デプロイしてみる
cdkをデプロイする前に、ブートストラップスタックのデプロイが必要になります。
この作業は1つのAWSアカウントで1度だけ実施すればOKの作業となります。
```
$ cdk bootstrap
```
cdkコマンド実行時に以下のエラーが出る場合は、プロファイルの設定が必要となるため、適宜実施してください。
```
Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment
```
参考までに自分のプロファイル設定を記載します。（旧AWS SSO環境です）
マスキングしておりますが、`~/.aws/config`は以下のような記述となっています。
```
~/.aws/config

[profile sso-profile]
sso_start_url = https://xxxxxxxx/start#/ ※SSOログインのURL
sso_region = [ログインするリージョン]
sso_account_id = [デプロイ先のアカウントID]
sso_role_name = [SSOログインする際のロール]

[profile deploy-profile]
role_arn = [ログイン後にスイッチするロール（実際にデプロイで使うロール）]
region = [ログインするリージョン]
source_profile = sso-profile
```
configファイルを用意した上で、cliからSSOログインします。
ブラウザで許可を求められるので許可します。
```
$ aws sso login --profile sso-profile
```
いちいち、プロファイル名を指定するのは面倒なのでスイッチするロール（デプロイに使うロール）のプロファイルを環境変数に登録しときます。
```
$ export AWS_PROFILE=deploy-profile
```

ちょっと逸れましたが、ブートストラップが完了したら、いよいよデプロイです。
```
$ cdk deploy
```
デプロイ対象にIAMが含まれていたりすると、続行してよいかを聞かれるので、`y`で続行します。
煩わしいなって場合は、デプロイ時に`--require-approval never`のオプションを付けてあげるとスルーしてくれます。
```
Do you wish to deploy these changes (y/n)?
```
コンソールの出力でも確認できますが、CFn側でもstackが作成されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1057132/02dc094e-00ab-a322-2e7e-eb82dfb162de.png)

これにて、デプロイ完です！
どうです？GUIでポチポチするよりはマシですよね？


なお、リソースを削除する場合は、CFnでスタックを削除するか、以下コマンドを実行します。
```
$ cdk destroy
```

### 複数Stackの場合は？
`lib`配下でexportするクラスがスタックとなるため、1ファイル内で複数クラス定義するか、1つのクラス内で複数クラスをexportするかの選択肢があります。

今回は、新たに`cdk-another-stack.ts`を`lib`配下に作成し、以下構成にします。
```
cdk-workshop/
├── bin/
│   └── cdk-workshop.ts
├── lib/
│   └── cdk-workshop-stack.ts
│   └── cdk-another-stack.ts
```

`lambda.Function`の第二引数にあたるIDを`CdkWorkshopStack`とは違う値としていますが、コンストラクト内で一意であればよいため、同じにしても大丈夫です。  
```
[cdk-another-stack.ts]

import { Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class CdkAnotherStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const lambdaFunction = new lambda.Function(this, 'LambdaAnotherFunction', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: new lambda.InlineCode(`
        exports.handler = async (event) => {
          return {
            statusCode: 200,
            body: 'Hello, CDK Lambda!'
          };
        };
      `)
    });
  }
}
```
`cdk-workshop.ts`から呼び出す必要があるので、こちらも呼び出しを追加しておきます。  
ここでは、第二引数のIDは異なるものを指定しないとダメです。
```
[cdk-workshop.ts]

import * as cdk from 'aws-cdk-lib';
import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';
import { CdkAnotherStack } from '../lib/cdk-another-stack';

const app = new cdk.App();
new CdkWorkshopStack(app, 'CdkWorkshopStack');
// スタックを追加
new CdkAnotherStack(app, 'CdkAnotherStack');
```
デプロイパターンはいくつかあるので、状況に応じて使い分けするとよいかと思います。
 - 全て直列でデプロイ
`CdkWorkshopStack`⇒`CdkAnotherStack`の順でデプロイする場合は以下で実施します。
```
$ cdk deploy --all
```

 - 並列でデプロイ
`concurrency`で指定した数の並列数で`CdkWorkshopStack`, `CdkAnotherStack`を並列デプロイします。
直列に比べ、早くデプロイすることが可能です。
承認が必要なスタックがあると失敗するので、スルーするようオプションを付けときます。

```
$ cdk deploy --all --concurrency [並列可能数] --require-approval never
（例）
$ cdk deploy --all --concurrency 2 --require-approval never
```

 - 個別デプロイ
`CdkWorkshopStack` or `CdkAnotherStack`を指定してデプロイします。
```
$ cdk deploy [スタックのクラス名]
（例）
$ cdk deploy CdkWorkshopStack
```

## CFnテンプレートとの比較
ここまででもテンプレートよりCDK！って気持ちになってるかもしれないですが、最後に実際のCFnテンプレートとの比較を行ってみます。

直接CFnのGUIからデプロイ後のテンプレートも確認できますが、事前にどのようなテンプレートが使われるのかを確認することも可能です。
```
$ cdk synth
```
synthを実行することで標準出力にテンプレートが出力され、`~/cdk.out`にもテンプレートファイルが置かれます。
```
[CdkWorkshopStack.template.json（IAMとLambda部分を抜粋）]


{
 "Resources": {
  "LambdaFunctionServiceRoleC555A460": {
   "Type": "AWS::IAM::Role",
   "Properties": {
    "AssumeRolePolicyDocument": {
     "Statement": [
      {
       "Action": "sts:AssumeRole",
       "Effect": "Allow",
       "Principal": {
        "Service": "lambda.amazonaws.com"
       }
      }
     ],
     "Version": "2012-10-17"
    },
    "ManagedPolicyArns": [
     {
      "Fn::Join": [
       "",
       [
        "arn:",
        {
         "Ref": "AWS::Partition"
        },
        ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
       ]
      ]
     }
    ]
   },
   "Metadata": {
    "aws:cdk:path": "CdkWorkshopStack/LambdaFunction/ServiceRole/Resource"
   }
  },
  "LambdaFunctionBF21E41F": {
   "Type": "AWS::Lambda::Function",
   "Properties": {
    "Code": {
     "ZipFile": "\n        exports.handler = async (event) => {\n          return {\n            statusCode: 200,\n            body: 'Hello, CDK Lambda!'\n          };\n        };\n      "
    },
    "Handler": "index.handler",
    "Role": {
     "Fn::GetAtt": [
      "LambdaFunctionServiceRoleC555A460",
      "Arn"
     ]
    },
    "Runtime": "nodejs20.x"
   },
   "DependsOn": [
    "LambdaFunctionServiceRoleC555A460"
   ],
   "Metadata": {
    "aws:cdk:path": "CdkWorkshopStack/LambdaFunction/Resource"
   }
  },
  ```
実際に`CdkWorkshopStack.template.json`を確認すると`AWS::IAM::Role`の定義が存在しているのを確認できるかと思います。

ですが、cdk側での実装はたった数行で済むんです！楽ですねぇ。

（CDK Pipelinesにも触れていこうと思ったんですが長くなるのでまたの機会に）

## おまけ（インラインでLambdaコードなんて書かねぇよ）
今回はお試しなので簡易にインラインコードを書きましたが、実際にそんなことはしないと思います。
自分のプロジェクトではCDKスクリプトとLambdaスクリプトを同じレポジトリで管理し、以下のようなディレクトリ構成を取っています。
```
project-root/
├── cdk/　・・・cdkスクリプト
│   └── bin
│   └── lib
│
├── lambda/　・・・lambdaスクリプト
```
その上で、stack側では以下のようにlambda構築を実装しています。
```
const sampleFunc = new NodejsFunction(this, 'sampleFunc', {
    runtime: Runtime.NODEJS_20_X,
    entry: path.join(__dirname, [lambdaハンドラースクリプトまでの相対パス]),
    handler: 'handle',
    functionName: 'sampleFunc',
    timeout: Duration.seconds(900),
    memorySize: 512,
});
```
レポジトリ分けるとめんどくさいので、Lambda+CDKの場合は基本的にこの構成を取ることになるんでは、と思ってます。