LoginSignup
13
3

More than 1 year has passed since last update.

AWS CDKでECS環境を構築する

Last updated at Posted at 2021-12-19

この記事はオープンロジアドベントカレンダーの19日目の記事です。

はじめに

AWS Cloud Development Kit (AWS CDK) v2 の一般提供開始
12月のre:InventでAWS CDK v2の一般提供が開始されました。
今回はCDK v2を使ってAWS ECS環境のアプリケーションからRDSへの接続までを構築していきたいと思います!

また、情報が古くなる可能性があるので実際のご利用の際は公式ドキュメントをご参照ください

前提環境

cdkコマンドをDockerコンテナから叩いています。言語はTypeScriptを利用しました。
Dockerfile
package.json

コマンドの実行

deploy.sh
$ docker run --rm -v $(pwd):/app --env-file=".env" --env ENV=Dev -it ecs-sample cdk synth
$ docker run --rm -v $(pwd):/app --env-file=".env" --env ENV=Dev -it ecs-sample cdk deploy

またリポジトリも公開しているのでご参照ください。

Step1. 単純なECSサービスの作成

step1のコード

ecs-sample.ts
import { Stack, StackProps, aws_ecs as ecs, aws_ecs_patterns as ecsp} from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    new ecsp.ApplicationLoadBalancedFargateService(this, 'SampleWebService', {
      taskImageOptions: {
        image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
      },
      publicLoadBalancer: true
    });
  }
}

AWS CDKを使用してAmazon ECS の開始方法とほとんど同じ方法で開始できます。

v2への移行の変更点は、

  • パッケージのimport元がaws-cdk-libになった。
  • 上記の影響でnpm install @aws-cdk/aws-ecs-patternsのインストール作業が不要になった。

の2点です。
チュートリアルではaws_ecs_patternsを利用しています。
aws_ecs_patternsは公開されているECSのテンプレートパターンを利用してECS環境を構築するモジュールになります。

Step2. VPCのカスタマイズ

step2のコード

チュートリアルのコードでは、VPCに単一サブネットだったのでMultiAZの3層Subnet構成(Public,Private,Protected)に変更したいと思います。

VPCの作成

vpc-sample-stack.ts
export class VPCSampleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    this.vpc = new ec2.Vpc(this, 'ECSSampleVPC', {
      cidr: "192.168.0.0/21",
      maxAzs: 2,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC
        },
        {
          cidrMask: 24,
          name: 'private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_NAT
        },
        {
          cidrMask: 24,
          name: 'protected',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED
        }
      ],
      vpnRoutePropagation: [
        {
          availabilityZones: [],
        }
      ]
    });
    Tags.of(this.vpc).add('Name', 'VPCSampleStack');
 }
}

VPCを作成します。今回はprivateにapplication, protectedにアプリケーションから参照されるデータリソース(RDS)を配置します

クロススタックの設定

VPCやアプリケーションのインフラとしてのライフサイクルが違うので、意図しないタイミングでの変更を防ぐためにクロススタック(スタック間でのリソース共有)での生成を行いたいと思います。

vpc-sample-stack.ts#L5
export class VPCSampleStack extends Stack {
  public readonly vpc : ec2.IVpc;

VPCのスタックのリードオンリーのメンバ変数としてvpcを登録します。

ecs-sample.ts#L9
new EcsSampleStack(app, 'EcsSampleStack', vpcStack.vpc, {});

ECSのスタックのコンストラクタを拡張してVPCを設定すれば、Fn::ImportValueでのデータ参照ができるようになります。

あとは、ECS Clusterを作成してFargateサービスに渡してあげれば、Step1のFargate ECSが今回生成したVPCに作成されます。

この時、PublicにLoadBalancer, PrivateにECS Clusterを自動的に配置してくれるようです。

Step3. RDSの作成

step3のコード

認証情報の管理はSecretManagerの方が管理などの面で便利だと思いますが、簡素化のためParameterStoreを使います。

SSM Parameter Storeへの認証情報の登録

ECSで利用するのでSSM Parameter Storeに認証情報の登録を行います。

store-ssm-parameter.sh
$ aws ssm put-parameter --name "/ECSSample/Dev/RDS/Password" --value "xxxxxxxxx" --type "SecureString"
{
    "Version": 1,
    "Tier": "Standard"
}
$ aws ssm put-parameter --name "/ECSSample/Dev/RDS/User" --value "user" --type "String"
{
    "Version": 1,
    "Tier": "Standard"
}
$ aws ssm put-parameter --name "/ECSSample/Dev/RDS/Rotation" --value "1" --type "String"
{
    "Version": 1,
    "Tier": "Standard"
}
$ aws ssm put-parameter --name "/ECSSample/Dev/RDS/Database" --value "testDB" --type "String"
{
    "Version": 1,
    "Tier": "Standard"
}

Aurora MySQL8.0でのインスタンス作成

rds-sample-stack.ts
export class RDSSampleStack extends Stack {
  public readonly rdsCluster: rds.IDatabaseCluster;
  constructor(scope: Construct, id: string, vpc: ec2.IVpc, props?: StackProps) {
    super(scope, id, props);
    const user = ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/User');
    const rotation = ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/Rotation'); // あまり良くないかも
    const password = SecretValue.ssmSecure('/ECSSample/Dev/RDS/Password', rotation);
    this.rdsCluster = new rds.DatabaseCluster(this, 'SampleAuroraCluster', {
      engine: rds.DatabaseClusterEngine.auroraMysql({ version: rds.AuroraMysqlEngineVersion.VER_3_01_0 }),
      credentials: rds.Credentials.fromPassword(user, password),

      instanceProps: {
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MEDIUM),
        vpcSubnets: {
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED
        },
        vpc,
      },
    });
  }
}

先程の認証情報をもとにAurora MySQLの作成をします。
インスタンスタイプはt4g.midiumインスタンス、MySQL Versionは8.0です。

Deployを行うとライターインスタンス,リーダインスタンス構成のAurora MySQLが生成されます。
スクリーンショット 2021-12-19 10.57.22.png

Step4. SecurityGroupの設定

step4のコード

現状ではRDSのSecurityGroupがフルオープンの状態となってしまっているので、Security系の設定をしていきます。

SecurityGroupの設定

今回は各スタックで生成したSecurityGroupを繋げていきます。

 export class RDSSampleStack extends Stack {
   public readonly rdsCluster: rds.IDatabaseCluster;
+  public readonly rdsSecurityGroup: ec2.ISecurityGroup;
   constructor(scope: Construct, id: string, vpc: ec2.IVpc, props?: StackProps) {
     super(scope, id, props);
+
     const user = ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/User');
     const rotation = ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/Rotation'); // あまり良くないかも
     const password = SecretValue.ssmSecure('/ECSSample/Dev/RDS/Password', rotation);
+
+    this.rdsSecurityGroup = new ec2.SecurityGroup(this,'RDSSecurityGroup', {vpc});
     this.rdsCluster = new rds.DatabaseCluster(this, 'SampleAuroraCluster', {
       engine: rds.DatabaseClusterEngine.auroraMysql({ version: rds.AuroraMysqlEngineVersion.VER_3_01_0 }),
       credentials: rds.Credentials.fromPassword(user, password),

RDSとECSのスタックにSecurityGroupの生成を記述します。
各スタックからExportし、AssignSecurityGroupクラスでルールの繋ぎ込みを行います。

assign-security-group.ts
import { aws_ec2 as ec2 } from 'aws-cdk-lib';

export class AssignSecurityGroup {
  private ecsSecurityGroup: ec2.ISecurityGroup;
  private rdsSecurityGroup: ec2.ISecurityGroup;
    constructor(ecsSecurityGroup: ec2.ISecurityGroup, rdsSecurityGroup: ec2.ISecurityGroup ) {
    this.ecsSecurityGroup = ecsSecurityGroup;
    this.rdsSecurityGroup = rdsSecurityGroup;
    }
  assign(){
    this.ecsSecurityGroup.connections.allowTo(
      this.rdsSecurityGroup,
      ec2.Port.tcp(3306),
      'Allow ECS Connection'
    );
  }
}

RDS => ECS 方向のみの依存にするためAllowToで後からIngressを作ってます。
デプロイし、SecurityGroupが紐づいていることを確認できました。

Step5. 動作確認

step5のコード

検証用のコンテナを作成します。 Dockerfile
コンテナの中身はGolangでRDSにPingを送った後にHttp HandlerでALBのヘルスチェックを80ポートで受け付けるようにしてます。

$ cd ./sample-app
$ docker build -f ./Dockerfile -t rds-connect-test . 
$ aws ecr create-repository --repository-name rds-connect-test 

コンテナをビルドして、ECRにリポジトリを作成しpushします。
イメージのpushについては下記をご参照ください。

Imageのオプションを変更し環境変数をコンテナに渡します。

diff --git a/lib/ecs-sample-stack.ts b/lib/ecs-sample-stack.ts
index 9b0d863..c84d689 100644
--- a/lib/ecs-sample-stack.ts
+++ b/lib/ecs-sample-stack.ts
@@ -3,7 +3,9 @@ import {
   StackProps,
   aws_ecs as ecs,
   aws_ec2 as ec2,
-  aws_ecs_patterns as ecsp
+  aws_ecs_patterns as ecsp,
+  aws_ssm as ssm,
+  aws_ecr as ecr
 } from 'aws-cdk-lib';
 import { IVpc } from 'aws-cdk-lib/aws-ec2';
 import { Construct } from 'constructs';
@@ -14,12 +16,27 @@ export class EcsSampleStack extends Stack {
     super(scope, id, props);
     const cluster = new ecs.Cluster(this, 'SampleEcsCluster', { vpc })
     this.ecsToRDSSecurityGroup = new ec2.SecurityGroup(this,'ECStoRDSSecurityGroup', {vpc});
+    const rotation = Number(ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/Rotation'));
+    const repo = ecr.Repository.fromRepositoryName(this, 'RDSConnectTestRepo', 'rds-connect-test')
+    

     new ecsp.ApplicationLoadBalancedFargateService(this, 'SampleWebService', {
       cluster,
       taskImageOptions: {
-        image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
-      },
+        image: ecs.ContainerImage.fromEcrRepository(repo),
+        environment: {
+          ENDPOINT: ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/Endpoint'),
+          USER: ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/User'),
+          DATABASE: ssm.StringParameter.valueForStringParameter(this, '/ECSSample/Dev/RDS/Database'),
+        },
+        secrets: {
+          PASS: ecs.Secret.fromSsmParameter(
+            ssm.StringParameter.fromSecureStringParameterAttributes(this, 'RDSPassWord',{
+              parameterName: '/ECSSample/Dev/RDS/Password',
+              version: rotation
+            }))
+        },
+    },
       publicLoadBalancer: true,
       securityGroups: [ this.ecsToRDSSecurityGroup ] ,
       deploymentController: {

ECRのエンドポイントを変更して、SSM ParameterをTask定義に渡しました。

またVPCにECRとSSMのVPCEndPointを作成しました。

スクリーンショット 2021-12-20 2.24.17.png

ここまででECS FargateからRDSへの疎通確認まで出来たと思います。

最後に

CloudFormationだとyamlやjsonのパラメータを細かく調べるコストがかかるところを、CDKがある程度自動的に作成してくれることで簡易に構築することができました。

チームで利用する際は意図しない構成で作成されてしまうことを避けるため、テストコードなどで動作を担保する必要があるのかもしれません。

業務ではインフラは触らないのですが、今後もキャッチアップしていきたいと思います。

参考

13
3
0

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
13
3