AWS CDKでECS/FargateとRDSを作成


## 1. 前書き 

### 1.1. CDKとは何か？
> AWS Cloud Development Kit (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースを定義するためのオープンソースのソフトウェア開発フレームワークです。
https://aws.amazon.com/jp/cdk/

> 使い慣れたプログラミング言語、ツール、ワークフローの使用
AWS CDK を使用すると、TypeScript、Python、Java、.NET、および Go (開発者プレビュー) を使用してアプリケーションインフラストラクチャをモデル化できます。CDK では、開発者は既存の IDE、テストツール、およびワークフローパターンを使用できます。自動入力ドキュメントやインラインドキュメントなどのツールを活用することで、AWS CDK では、サービスドキュメントとコードの切り替えにかかる時間を短縮できます。
https://aws.amazon.com/jp/cdk/features/

### 1.2. この記事の目的
この記事の目的は、AWS CDKでECS/FargateとRDSの環境を作成する手順を個人的なメモとして記すことです。

### 1.3. 試したかったこと

実は今回はCDKを試したのは、二次的な要素でした。 　
**コンテナ構築の効率化とセキュリティー向上をテーマに「CloudNative Buildpacks」と「コンテナイメージのスキャン」を試そうとしていました。**  
そのための検証環境が必要だったため、CDKで作るのが効率が良さそうと考えて、CDKも試してみたという背景があります。

今回は、以下リソースをCDKで作成することを意図しました。
* VPC関連のリソース（VPC, Subnet, Gateway）
* RDS
* ECS/Fargate関連のリソース（Cluster, Service, TaskDefinition）
* ApplicationLoadBalancer

また、関連して、以下のリソースも作成します。
* IAM Poclicy
* SecretsManager
* SecurityGroup

### 1.4. 構成図（概略）

概略とはいえ、雑すぎる図になってしまいました。

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/f8a55b94-8b78-e45f-2f48-be7a01fef3f2.png)


### 1.5. 前提事項
以下のインストールについては済んでいるものとします。
* CDK
* typescript

ちなみに、私の環境は以下のようになっています。(2022/2/20時点）
- CDK 2.12.0
- typescript 3.9.7
- VSCode 1.64.2
- macOS Monterey 12.1


## 2. CDKでリソースを作ってみる
### 2.1. CDK利用の戦略

CDKには、大きく分けると、
* L2 Library(AWS Construct Library)
* L1 Library(AWS CloudFormation Resource)
の２つがあります。
L2は、高水準な仕様となっており、最低限のパラメーターで、いい感じに直接指定のないリソースも自動生成するようになっています。（その分細かい調整が難しいです）。  
一方のL1は、生のCloudFormationを書くのとほぼ同等のパラメーター設定が必要で、細かい調整が可能ですが、記述が冗長になります。

状況にもよりますが、CDKのメリットを最大限享受するならば、なるべくL2を使うのが良いと思います。
今回は、L2を利用して必要な部分を都度オプションパラメーターで調整しています。

### 2.2. 実装を見てみる

以下が第一弾のソースコードです。基本的には最小限の定義にしており、関連するリソースは自動生成に委ねています。 　
ただし、このコードだとうまく生成されないリソースがあります。

```typescript:test_cdk-stack.ts
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns";
import { Construct } from "constructs";
import {
  DatabaseCluster,
  DatabaseClusterEngine,
} from "aws-cdk-lib/aws-rds";

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

    // VPC関連のリソース作成
    const vpc: ec2.Vpc = new ec2.Vpc(this, "AbeTestVpc", {
      cidr: "10.x.0.0/16",
      subnetConfiguration: [ // Optional（省略すると、PUBLICとPRIVATE_WITH_NATのみ生成される）
        {
          cidrMask: 24,
          name: "ingress",
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: "application",
          subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
        },
        {
          cidrMask: 28,
          name: "rds",
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
    });

    // Security Group(自動生成に任せる)

    // RDS(最低限の設定としてある)
    const rdsCluster = new DatabaseCluster(this, "AbeTestRds", {
      engine: DatabaseClusterEngine.AURORA_MYSQL,
      instanceProps: {
        vpc,
        vpcSubnets: {
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
        instanceType: ec2.InstanceType.of(
          ec2.InstanceClass.BURSTABLE2,
          ec2.InstanceSize.SMALL
        ),
      },
      defaultDatabaseName: "abeTest",
    });

    // ECS Cluster
    const cluster = new ecs.Cluster(this, "AbeTestCluster", {
      vpc: vpc,
    });

    // ALB, FargateService, TaskDefinition
    const loadBalancedFargateService =
      new ecsPatterns.ApplicationLoadBalancedFargateService(
        this,
        "AbeTestService",
        {
          cluster: cluster, // Required
          memoryLimitMiB: 512,
          cpu: 256,
          desiredCount: 1, // Optional(省略値は3)
          listenerPort: 80,
          taskImageOptions: {
            image: ecs.ContainerImage.fromRegistry(
              "my_account/spring-boot-docker"
            ),
            containerPort: 8080,
          },
          healthCheckGracePeriod: Duration.seconds(240),
        }
      );

    // HealthCheckの設定
    loadBalancedFargateService.targetGroup.configureHealthCheck({
      path: "/custom-health-path",
      healthyThresholdCount: 2, // Optional
      interval: Duration.seconds(15), // Optional
    });
  }
}

```

## 3. この時点での課題
1. RDS(DB)のSecurityGroupにInboundルールが作成されていない
2. ECSからSecretsManagerを参照するIAMポリシーが「タスク実行ロール」にアタッチされていない
3. ECSからSecretsManagerを参照する「環境変数」の定義がタスク定義に作成されていない



### 3.1. RDS(DB)のSecurityGroupにInboundルールが作成されていない

![01_security_group.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/48120337-baf9-1db7-8b73-90a797199072.png)


### 3.2. ECSからSecretsManagerを参照するIAMポリシーが「タスク実行ロール」にアタッチされていない

実は、RDSを作成した段階で自動的に、SecretsManagerはできている。  
しかし、CDKでそれをどこでどう使うか？を指定していないためにアタッチがされていない。
![02_iam_role.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/7b8e7fdc-9ce4-825a-38b0-ccaab43514a1.png)


ちなみに、ECSからのLog出力のIAMポリシーはアタッチされている。
![03_iam_policy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/94ed4719-8d8d-447c-e77d-3a5897eaffd4.png)


### 3.3. ECSからSecretsManagerを参照する「環境変数」の定義がタスク定義に作成されていない
![04_container_def.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/104fc14d-9998-ae81-0054-701c1873433d.png)



## 4. 課題の解決
### 課題1.の解決

現状は、CDKがSecurityGroupを自動的に生成してくれています。  
例えば、RDSのセキュリティーグループについては、API仕様をみると以下のように記載されています。 　

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rds.InstanceProps.html#securitygroups

> securityGroups?
> Type: ISecurityGroup[] (optional, default: a new security group is created.)
> Security group.

DatabaseClusterのコンストラクターに渡す、InstancePropsのsecurityGroupsは、省略可能で、省略した際には、新しいsecurity groupが（自動的に）作られるという記載があります。

従って、自前で、SecurityGroupを生成して、それを渡してあげれば良さそうです。

変更箇所は、以下のようになります。

```diff typescript:
+  // VPC定義の後で、RDS定義より前に追加
+  // RDS SecurityGroup設定
+    const ecsSG = new SecurityGroup(this, "AbeTestEcsSecurityGroup", {
+      vpc,
+    });
+
+    const rdsSG = new SecurityGroup(this, "AbeTestRdsSecurityGroup", {
+      vpc,
+      allowAllOutbound: true,
+    });
+    // ↓ ここがポイント
+    rdsSG.connections.allowFrom(ecsSG, Port.tcp(3306), "Ingress 3306 from ECS"); 
+
    // RDS
    const rdsCluster = new DatabaseCluster(this, "AbeTestRds", {
      engine: DatabaseClusterEngine.AURORA_MYSQL,
      instanceProps: {
        vpc,
        vpcSubnets: {
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
+        securityGroups: [rdsSG],
        instanceType: ec2.InstanceType.of(
          ec2.InstanceType.of(
          ec2.InstanceClass.BURSTABLE2,
          ec2.InstanceSize.SMALL
        ),
      },
      defaultDatabaseName: "abeTest",
    });

    // 〜 中略 〜

    // ALB, FargateService, TaskDefinition
    const loadBalancedFargateService =
      new ecsPatterns.ApplicationLoadBalancedFargateService(
        this,
        "AbeTestService",
        {
          cluster: cluster, // Required
          memoryLimitMiB: 512,
          cpu: 256,
          desiredCount: 1, // Optional(Default === 3 !!!)
          listenerPort: 80,
          taskImageOptions: {
            image: ecs.ContainerImage.fromRegistry(
              "akiraabe/spring-boot-docker"
            ),
            containerPort: 8080,
          },
+          securityGroups: [ecsSG],
          healthCheckGracePeriod: Duration.seconds(240),
        }
      );

```

### 課題2.の解決

今回は、「aws-cdk-lib.aws_ecs_patterns module」を用いて、ALBとECSを作成しています。  
一方で、RDSは個別に定義しているので、RDSとそこから生成されるSecretsManagerがいわば蚊帳の外の状態になっています。

従って、ECSのタスク実行ロールに、自前で生成したSecretsManager読み取りポリシーをアタッチすれば良さそうです。

コードの変更箇所は以下の通りです。

```diff typescript:
+    // RDS定義の後に追加
+    // SecretsManager(RDSにより自動設定)
+    const secretsmanager = rdsCluster.secret!;
+
+    // 最後に追加
+    // Add SecretsManager IAM policy to FargateTaskExecutionRole
+    const escExecutionRole = Role.fromRoleArn(
+      this,
+      "ecsExecutionRole",
+      loadBalancedFargateService.taskDefinition.executionRole!.roleArn,
+      {}
+    );
+    escExecutionRole.attachInlinePolicy(new Policy(this, 'abeTestSMGetPolicy', {
+      statements: [new PolicyStatement({
+        actions: ['secretsmanager:GetSecretValue'],
+        resources: [secretsmanager.secretArn],
+      })],
+    }));

```

### 課題3.の解決

課題2と原理は一緒です。SecretsManagerの値を、タスク定義に渡してあげればうまくいきます。

```diff typescript:
    // ALB, FargateService, TaskDefinition
    const loadBalancedFargateService =
      new ecsPatterns.ApplicationLoadBalancedFargateService(
        this,
        "AbeTestService",
        {
          cluster: cluster, // Required
          memoryLimitMiB: 512,
          cpu: 256,
          desiredCount: 1, // Optional(Default === 3 !!!)
          listenerPort: 80,
          taskImageOptions: {
            image: ecs.ContainerImage.fromRegistry(
              "akiraabe/spring-boot-docker"
            ),
            containerPort: 8080,
+            // Secretの設定
+            secrets: {
+              "dbname": ecs.Secret.fromSecretsManager(secretsmanager, 'dbname'),
+              "username": ecs.Secret.fromSecretsManager(secretsmanager, 'username'),
+              "host": ecs.Secret.fromSecretsManager(secretsmanager, 'host'),
+              "password": ecs.Secret.fromSecretsManager(secretsmanager, 'password'),
+            }
          },
          securityGroups: [ecsSG],
          healthCheckGracePeriod: Duration.seconds(240),
        }
      );


```

これにより、課題が解決され、ALB〜RDSまで一気通貫でつながります。

## 5. まとめ

### 5.1. 所感
今回は、手っ取り早くコンテナ環境とDBを作るということにフォーカスしています。 　
実験用なので、1セットのStackとしてあり、用が済んだら全て```cdk destroy```で消してしまうという思想です。

もう少し実用的なインフラを構築するためには、さらに、以下の課題があると思います。

* Stackを分割して、毎回RDSが消えないようにするなど、リソースのライフサイクルに合わせる
* CodePipelineを用いて、アプリケーションのリリースの自動化にも対応する
* Blue/Greenデプロイなどにも対応する

### 5.2. CDKの展望？

AmplifyやChaliceがCDKと連携する機能をリリースしているので、今後、AWSのサービス間での連携にCDKが使われていく可能性が高いと思います。

https://aws.amazon.com/jp/blogs/news/extend-amplify-backend-with-custom-aws-resource-using-aws-cdk-or-cloudformation/

https://aws.github.io/chalice/tutorials/cdk.html

### 5.3. 参考にしたサイト

以下のサイトを参考にしています。（特に、API仕様は必読です。）  

APIドキュメント  
* https://docs.aws.amazon.com/cdk/api/v2/

Construct Hub
* https://constructs.dev/

Example
* https://github.com/aws-samples/aws-cdk-examples

ClassmethodさんのじっせんCDK（L1の記述例として有用です）
* https://dev.classmethod.jp/articles/cdk-practice-1-introduction/

stackoverflow (SecurityGroupの設定の際に参考になりました)
* https://stackoverflow.com/questions/66979012/how-to-add-a-security-group-to-an-existing-rds-with-cdk-without-cyclic-dependenc

### 5.4. ソースコード全体

ソースコード全体を添付しておきます。  
Docker&VSCodeで動かす前提となっていますが、サブディレクトリのtestCdkだけ取り出して、ローカル環境にNodeやcdkをインストールすればDockerなしでも動かせます。  
https://github.com/akiraabe/cdk-fargate2