🙌

AWS CDKでECS/FargateとRDSを作成

2022/02/22に公開

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. 構成図(概略)

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

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)
    の2つがあります。
    L2は、高水準な仕様となっており、最低限のパラメーターで、いい感じに直接指定のないリソースも自動生成するようになっています。(その分細かい調整が難しいです)。
    一方のL1は、生のCloudFormationを書くのとほぼ同等のパラメーター設定が必要で、細かい調整が可能ですが、記述が冗長になります。

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

2.2. 実装を見てみる

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

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ルールが作成されていない

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

実は、RDSを作成した段階で自動的に、SecretsManagerはできている。
しかし、CDKでそれをどこでどう使うか?を指定していないためにアタッチがされていない。

ちなみに、ECSからのLog出力のIAMポリシーはアタッチされている。

3.3. ECSからSecretsManagerを参照する「環境変数」の定義がタスク定義に作成されていない

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を生成して、それを渡してあげれば良さそうです。

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

+  // 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読み取りポリシーをアタッチすれば良さそうです。

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

+    // 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の値を、タスク定義に渡してあげればうまくいきます。

    // 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ドキュメント

Construct Hub

Example

ClassmethodさんのじっせんCDK(L1の記述例として有用です)

stackoverflow (SecurityGroupの設定の際に参考になりました)

5.4. ソースコード全体

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

Discussion