いまだにマネコンからリソース作ってませんか？（AWS CDKのススメ）

## はじめに
AWSでWEBアプリを開発していて、以下のような心境になったことはないでしょうか？
* 手動でインフラの設定をするのが手間
* 開発/本番環境でインフラの設定を同期するのが大変
* セキュリティグループ、ロールの管理が面倒

これらの悩みを解決してくれるのが、IaC（Infrastructure as Code）という仕組みになります。
https://aws.amazon.com/jp/what-is/iac/

AWSでは、AWS Cloud Development Kit（CDK）というオープンソースのクラウド開発キットを用いて、IaCを実現することができます。
https://aws.amazon.com/jp/cdk/

今回は、私が開発しているWEBサービスのインフラ管理に`AWS CDK`を導入した経験から、
* CDKの概念・仕組み
* 導入手順・コマンド
* ベストプラクティス
* メリット
* 注意点
について紹介できればと思います。

## 概念
**IaC**とは、Infrastructure as Codeの略で、インフラ（サーバー、ネットワーク、ロードバランサーなど）の設定と管理を、手動でなくコードを通じて自動化する手法です。
代表的なIaCのツールとしては、
* [AWS Cloud Development Kit（CDK）](https://aws.amazon.com/jp/cdk/)
* [Terraform](https://www.terraform.io/)
* [Ansible](https://www.ansible.com/)
などが該当します。

**CDK**とは、AWSのリソースを構築、展開するためのオープンソースのソフトウェア開発キットです。
TypeScriptやPythonなどのプログラミング言語を使用して、AWSのリソースをコードとして定義することができます。
他のIaCツールとの違いとしては、AWSのサービスに特化しており、ベストプラクティスに沿って複数のリソースを束ねる仕組みが提供されていることで同じインフラでも少ないコードで記述することが可能です。

**CloudFormation**は、AWSのリソースをJSONやYAML形式で定義し、デプロイするためのサービスです。
CDKはプログラミング言語を用いて、CloudFormationのテンプレートを生成するための仕組みです。

CDKでインフラのコードを書く際に、StackとConstructという概念が登場します。
**Stack**は、AWSリソースの集合体を表す概念で、デプロイ可能な最小単位です。
後述するConstructを論理的な単位でグループ化し、関連するリソースを一緒に管理することが出来ます。
AWSリソース（例：Amazon S3バケット、Amazon EC2インスタンス）の論理的なコンテナとして機能し、これらのリソースのデプロイ、更新、破棄を一括で行うことができます。

どういう単位でStackを作成するかは自由ですが、一般的には機能単位などでまとめることが多いです。

* ネットワークStack：VPC、サブネット、NATゲートウェイ、セキュリティグループなどのネットワークリソースを含むStack。
* データベースStack：Amazon RDSインスタンスやDynamoDBテーブルなどのデータベースリソースを含むStack。
* アプリケーションStack：EC2インスタンスやECSクラスタ、Lambda関数などのアプリケーションリソースを含むStack。

それぞれのStackは1つのCloudFormationのテンプレートとして扱われます。

**Construct**とは、CDKアプリケーションの基本的な構築ブロックです。
ConstructはAWSリソースを抽象化し、より高いレベルでのインフラの定義を可能にします。
Constructは、一つ以上の低レベルAWSリソース（例：S3バケット、EC2インスタンス）や、他のConstructを組み合わせて、再利用可能なコンポーネントを形成します。

* L1（Low-level）Constructs：これらはAWS CloudFormationリソースに直接対応し、最も基本的なレベルのConstructです。
* L2（High-level）Constructs：より高レベルの抽象化を提供し、開発者がより簡単にインフラを構築できるようにするConstructです。
* L3（Pattern）Constructs：特定のアーキテクチャパターンを実装するためのConstructで、複数のL1およびL2 Constructsを組み合わせたものです。

## 仕組み
コードで記述したインフラがどのようにしてAWS上に反映されるのかを解説します。
主な流れは以下の通りです。
1. コードの記述
2. シンセサイズ
3. CloudFormationテンプレートのデプロイ
4. プロビジョニング
5. 結果の確認

### コードの記述
CDKでは、TypeScriptやPythonなどのプログラミング言語を使用して、AWSのリソースをコードとして定義します。
以下は、TypeScriptを使用してSQSキューとSNSトピックを作成するコードの例です。

```typescript
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subs from 'aws-cdk-lib/aws-sns-subscriptions';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';

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

    const queue = new sqs.Queue(this, 'CdkQueue', {
      visibilityTimeout: Duration.seconds(300)
    });

    const topic = new sns.Topic(this, 'CdkTopic');

    topic.addSubscription(new subs.SqsSubscription(queue));
  }
}

const app = new cdk.App();
new CdkStack(app, 'CdkStack');
```

CDKのコードでは、App→Stack→Constructという階層構造を持ちます。
Appは、CDKアプリケーションのルートで、Stackを含むConstructツリーを定義します。
Stackは、デプロイ可能な最小単位で、関連するリソースを一緒に管理することができます。
Constructは、AWSリソースの集合体を表す概念で、Stackに追加することで、Stackにリソースを追加することができます。

シンプルなJSON/YAMLと違い、CDKのコードはプログラミング言語で記述されているため、より複雑なロジックを記述することができます。
S3を作成して、IAMロールにそのS3へのアクセス権限を付与するというような、複数のリソースをまたがる処理も可能です。

### シンセサイズ
先ほど記述したコードを、CDKのCLIを使用してCloudFormationテンプレート（JSON/YAML形式）に変換します。

### デプロイ
先ほど作成したテンプレートをCloudFormationにデプロイします。

### プロビジョニング
CloudFormationはテンプレート内の指示に従って、各リソースを作成します。
依存関係に応じて、リソースの作成順序が決定されます。
リソースの作成が完了すると、CloudFormationはStackの作成を完了し、Stackの状態を「CREATE_COMPLETE」もしくは「UPDATE_COMPLETE」に設定されます。
この時何かしらのエラーが発生した場合は、ロールバックが行われ、Stackの状態が「ROLLBACK_COMPLETE」に設定されます。

## CDKの導入手順
続いてはCDKを導入する手順について紹介します。
CDKは様々な言語に対応していますが、ここではTypeScriptを使用した場合の手順を紹介します。

### 前提
* Node.jsがインストールされていること
* aws-cliがインストールされていること
* `aws configure`で認証情報が設定されていること

### 初期化
以下のコマンドからプロジェクトの初期化ができます。

```
npx aws-cdk init sample-app --language typescript
# もしくは
npm i -g aws-cdk
cdk init sample-app --language typescript
```

必要なライブラリがインストールされ、サンプルコードが生成された状態になります。
`lib`フォルダにStackクラスを記述し、`bin`フォルダの中身にAppクラスを記述します。

### 環境の準備
CDKを利用するにはまずAWS環境に対してCDKを利用するための準備が必要です。
具体的には、S3バケットの作成やIAMロールの作成などが必要です。
以下のコマンドで自動で作成することが出来ます。

```
npm run cdk bootstrap
```

Appに対して、一度だけ実行すれば問題ないです。

### コマンド

#### シンセサイズ
```bash
$ cdk synth
# もしくはnpm run cdk synth
```

CDKのコードをCloudFormationのテンプレートに変換します。
`./cdk.out`というディレクトリにJSONファイルが生成されます。
このフォルダはGit管理は不要なので、`.gitignore`に記述しておくとよいです。
テンプレートから作成した場合はデフォルトで設定済です。

#### デプロイ
```bash
$ cdk deploy
# もしくはnpm run cdk deploy
```

CDKのコードをCloudFormationのテンプレートに変換し、CloudFormationのStackを作成します。
シンセサイズの処理も行われるので、あえて`cdk synth`を実行する必要はありません。
複数Stackある場合は、`cdk deploy XXXStack`のようにStack名を指定します。
`--all`で全てのStackを対象にデプロイすることも可能です。

#### Stackの削除
```bash
$ cdk destroy
# もしくはnpm run cdk destroy
```

CloudFormationのStackを削除します。
Stackを削除することで、Stackに紐づくリソースも削除されます。

## （私が考える）ベストプラクティス
CDKを使って複数プロジェクト開発・運用した経験から私が考えるベストプラクティスについて紹介します。
あくまでも私の考えなので、参考程度に見てください。
少しCDKとは直接関係ない話も含みます。

### 使用言語
CDKはTypeScript、JavaScript、Python、Java、C#などのプログラミング言語をサポートしています。
書きやすさや型安全な点から私はTypeScriptを採用しています。
TypeScriptの場合は、`aws-cdk-lib`を使うようにしましょう。
* `aws-cdk-lib`: CDKの最新バージョン（2.x）に対応したライブラリ
* `@aws-cdk`: CDKの古いバージョン（1.x）に対応したライブラリ

たまに`@aws-cdk`で書かれているドキュメントがあるので、注意してください。

### プロジェクトツリーの構成
プロジェクトツリーの構成は、以下のようにしていました。

```bash
.
├── README.md
├── cdk
│   ├── bin
│   │   └── app.ts
│   ├── lib
│   │   ├── app-stack.ts
│   │   ├── batch-stack.ts
│   │   └── common-stack.ts
│   ├── package.json
│   ├── tsconfig.json
│   └── cdk.json
├── webapp
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   ├── tsconfig.json
├── batch
│   ├── package.json
│   ├── src
│   │   └── index.ts
└── └── tsconfig.json
```

プロジェクトに対して、CDKのAppは一つになるようにするのが良いでしょう。
上記の例では、WEBアプリとバッチ処理にインフラコードは同じディレクトリの別Stackとして管理しています。

### Stackの分け方
Stackはアプリケーションの機能単位（デプロイをしたい単位）で分けると良さそうに思いました。
私の開発するサービスでは、以下のようなStackに分けていました。
* CommonStack: Stack共通のリソース（VPCなど）
* WebAppStack: ECS, ALB, CodePipeline, CloudWatch
* LambdaXXXStack: Lambda, S3, CloudWatch
* LambdaYYYStack: Lambda, S3, CloudWatch

また以下のようなAWSリソースはCDK管理に向かないため、手動で作成していました。
* [AWS Secrets Manager](https://aws.amazon.com/jp/secrets-manager/)
* [AWS Certificate Manager](https://aws.amazon.com/jp/certificate-manager/)
* [AWS Code Star Connections](https://docs.aws.amazon.com/codestar-connections/latest/APIReference/Welcome.html)

### 環境管理
開発(dev)/本番(prd)などの環境管理は、`cdk.json`の`context`の値を変更することで行います。
以下のように、`dev`と`prd`の環境を用意しています。

```json:./cdk.json
{
    "app": "npx ts-node --prefer-ts-exts bin/app.ts",
    "context": {
        ...
        "dev":{
            "env": {
                "account": "000",
                "region": "ap-northeast-1"
            },
            "envKey": "dev",
            "vpcId": "vpc-xxx",
            ...
        },
        "prd":{
            "env": {
                "account": "111",
                "region": "ap-northeast-1"
            },
            "envKey": "prd",
            "vpcId": "vpc-xxx",
            ...
        }
    }
}
```

`./bin/app.ts`を次のように修正することで、環境毎の変数を取得しStackに渡すことが出来ます。

```typescript:./bin/app.ts
import * as cdk from 'aws-cdk-lib';
import 'source-map-support/register';
import { WebAppStack } from '../lib/web-stack';

export type EnvProps = {
    envKey: 'dev' | 'prd';
    vpcId: string;
};

const app = new cdk.App();

const argContext = 'environment';
const envKey = app.node.tryGetContext(argContext);
if (envKey == undefined)
    throw new Error(`Please specify environment with context option. ex) npm run cdk:dev deploy`);

const envVals = app.node.tryGetContext(envKey) as EnvProps;
if (envVals == undefined) throw new Error('Invalid environment.');

const webAppStack = new WebAppStack(app, `${envKey}-WebAppStack`, {
    ...envVals
});

app.synth();
```

`package.json`に以下のようにコマンドを追加しておくと便利です。
`npm run cdk:dev deploy`のように実行することで、`dev`環境にデプロイすることが出来ます。

```json:./package.json
{
    "scripts": {
        ...
        "cdk": "cdk",
        "cdk:dev": "cdk -c environment=dev",
        "cdk:dev:deployall": "cdk -c environment=dev deploy --all",
        "cdk:prd": "cdk -c environment=prd",
        "cdk:prd:deployall": "cdk -c environment=prd deploy --all"
    },
}
```

### アプリケーションのデプロイは別で考える
CDKのデプロイはあくまでもインフラのデプロイです。
アプリケーションのデプロイは**CodeDeploy**を使用して行います。
CodeDeploy（CodePipeline）の設定はCDKを通して、コードで管理することが出来ます。

以下のようにインフラとアプリケーションのデプロイは別物として考えるのが良さそうです。

インフラのデプロイが必要なシーン
  * ECSのタスク数の変更
  * Lambdaのメモリの変更

アプリケーションのデプロイなシーン
  * ソースコードの変更

### サーバーレスのサービスを使う
CDKで管理するからと言って、既存のインフラを変更する必要はないですが、変更の余地があるのであればサーバーレスのリソースを使う方がCDKとの相性が良いです。

* EC2→ECS
* RDS→Aurora Serverless
など

CDKはインフラの設定までしか管理ができないので、EC2のサーバーの中身までは制御できません。
また特に開発時などでインフラの設定を変更することがあると思いますが、変更内容によってはリソースの再作成が必要な場合があり、その都度EC2の中身が消えてしまう可能性があります。

よくあるユースケースとして踏み台サーバーをEC2で立てることもありますが、こちらもECS/Fargateを使うことを推奨します。

### 極力L3のConstructを使う
CDKのConstructには、L1、L2、L3の3種類があります。
中でもL3は、特定のアーキテクチャパターンを実装するためのConstructで、それ自体がAWSのベストプラクティスに沿った設計になっていることが多いです。
L3やL2では、セキュリティグループやIAMロールなど煩雑になりがちな設定が内包されており、全体的にコードがシンプルになります。
可能な限りL3のConstructを使うことで、インフラ管理のコードが綺麗になります。

### その他
* リソース名には極力名前を付ける
* インフラ設定が記述されたファイルを扱う際は[S3Assets](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_assets-readme.html)を利用する
  * ECSのfluentbit.confなど
* サブネットの指定はIDではなく、[SubnetSelection](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.SubnetSelection.html)を使う

## 良かった点

### コードによる管理
CDKを使うことによってこれまでどこにも管理していなかったインフラの設定内容もコードで管理することが出来るようになりました。
コードになったことでGit管理が可能になり、バージョンの追跡や共有などが容易になりました。
また各リソースの関係値などが整理されるようになりました。
WEBアプリと同じ開発言語（TypeScript）で記述できるため、少ない学習コストで導入が出来ました。

### 自動化
CDKを使うことによって、不毛な手作業がなくなりました。
特に開発から本番へインフラの設定を同期する手間がなくなったことは大きかったです。
開発と本番で意図しない設定の差異があると事故にも繋がるので、CDKでその安全性が担保できるようになりました。
変更があった際にも、差分を的確に検知して必要最低限のリソースのみに更新が掛けられるので開発で試行錯誤している際も役立ちました。
不要になったStackは削除することによって関連するリソースも併せて削除されるので、ゴミリソースが残らずに済むのも良い点です。

## 注意点

### 利用するAWSサービスの知識がないと危険
CDKはコードで簡単にリソースを作成できますが、それぞれのサービスが意味するところを理解していないと記述が難しいです。
例えばECSであれば、クラスター、サービス、タスクの関係値を理解していない状態でコードを書くのは難しいです。
また変更を加える際に、どの操作がダウンタイムを要するかを理解していないと、本番環境での変更が難しくなります。
例えば、RDSのインスタンスタイプの変更はダウンタイムが発生します。

### 対応していない操作もある
CDKは比較的新しい仕組みなので、まだ全ての操作に対応しているわけではありません。
CloudFrontであれば、OAIの設定がCDKではできません。

## さいごに
CDKを使い始めたことで、インフラのデプロイが簡略化され、開発に多くの時間を割くことが出来るようになりました。
またインフラの設定がコード化されたことで、どこにも管理されていなかったモヤモヤを解消することも出来ました。

非常に便利なのでぜひ使ってみてください。