7
1

More than 1 year has passed since last update.

AWS CDKでECS/FargateとRDSを作成

Last updated at Posted at 2022-02-23

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

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

01_security_group.png

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

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

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

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

04_container_def.png

4. 課題の解決

課題1.の解決

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

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が使われていく可能性が高いと思います。

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

7
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1