CDKを使用してECS Fargateにデプロイする

お久しぶりです、SREチームの成田です。 今回はCDKを使用してECS Fargate環境の作成、デプロイを行う方法をご紹介します。

ECS Fargateについて

ECSとは

ECSはフルマネージド型のコンテナオーケストレーションサービスです。 コンテナの実行、停止、スケーリング、デプロイ等の管理を行えます。

aws.amazon.com

類似サービスとしては、KubernetesとそのマネージドサービスであるEKSが挙げられますが、他のAWSサービスに慣れているのであればECSの方がとっつきやすいかなと思います。

Fargateとは

Fargateは、ECSやEKSにおいてコンテナをサーバレスで実行できるサービスです。

aws.amazon.com

Fargateが登場する前は、コンテナを実行するEC2インスタンスを用意して管理する必要がありましたが、Fargateを使用することによってサーバを気にすることなく必要な分だけコンテナを実行できるようになりました。

ECS Fargateのデプロイ

ECS Fargateのデプロイは数多くの方法があります。

  • IaC系
    • CloudFormation
    • Terraform
    • CDK
  • API
  • Docker Compose系
    • ECS CLI
    • Docker Compose ECS integration
  • AWSコンソール

それぞれ一長一短といった感じで未だ決定版が存在しない印象ですが、 ECSはVPCやELB等の他のAWSサービスと密に連携しながら動作するサービスなので、 個人的にはIaC系でECS以外にも使用するAWSサービスを作成しちゃうのがやりやすいかなと思います。

特にCDKは、CloudFormationよりも少ない記述で済み、ECSのデプロイに便利な機能もあるのでおすすめです。

こちらのスライドにより詳しい内容がありますので、一度目を通しておくと良いと思います。

ECS Fargate環境の作成、デプロイ

前置きはこれくらいにして、実際にECS Fargate環境の作成とデプロイを行ってみます。

Fargateで実行するアプリケーションの作成

ディレクトリを作成し、cdk initを実行してCDKプロジェクトを作成します。

$ mkdir fargate-cdk-demo
$ cd fargate-cdk-demo
$ npx cdk init app --language=typescript

次にECS Fargateで実行するアプリケーションを作成します。

$ mkdir docker
$ cd docker
$ npx create-next-app --example with-typescript sandbox-create-next-app-ts
$ cd sandbox-create-next-app-ts

今回はcreate-next-appを使用してアプリケーションを作成しました。 npm run devでローカルでの動作を確認できます。

ではこのアプリケーションのDocker環境を用意します。 以下の内容で.dockerignoreDockerfileを作成します。

.dockerignore

.git
node_modules

Dockerfile

FROM node:14-alpine

WORKDIR /app
COPY . /app
RUN npm install && \
    npm run build
ENV HOST 0.0.0.0
EXPOSE 3000
CMD ["npm", "run", "start"]

Dockerfileができたのでビルドして動作確認してみましょう。 ブラウザでhttp://localhost:3000/にアクセスてしみて正常にページが表示されていればOKです。

$ docker build -t sandbox-create-next-app-ts -f Dockerfile .
$ docker run --rm -p 3000:3000 sandbox-create-next-app-ts:latest

これでアプリケーションの作成が完了しました。 本来であればローカル開発環境もDockerにしたいので、docker-compose.ymlなども用意したいところですが今回は省略します。

ここまででディレクトリは以下のようになっています。

fargate-cdk-demo/
├── README.md
├── bin/
├── cdk.json
├── docker/
│   └── sandbox-create-next-app-ts/
│       ├── .dockerignore
│       ├── Dockerfile
│       ├── README.md
│       ├── components/
│       ├── interfaces/
│       ├── next-env.d.ts
│       ├── node_modules/
│       ├── package-lock.json
│       ├── package.json
│       ├── pages/
│       ├── tsconfig.json
│       └── utils/
├── jest.config.js
├── lib/
├── node_modules/
├── package-lock.json
├── package.json
├── test/
└── tsconfig.json

CDKの実装

作成したアプリケーション(sandbox-create-next-app-ts)をECS Fargateで実行するために必要なAWSリソースをCDKで定義していきます。

まずは必要なライブラリをインストールします。

$ npm install @aws-cdk/aws-ec2 @aws-cdk/aws-elasticloadbalancingv2 @aws-cdk/aws-ecs @aws-cdk/aws-logs

インストールが終わったらいよいよCDKの実装です。 fargate-cdk-demo-stack.tsをこのように実装します。

lib/fargate-cdk-demo-stack.ts

import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';
import * as ecs from '@aws-cdk/aws-ecs';
import * as log from '@aws-cdk/aws-logs';

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

    // VPC
    const vpc = new ec2.Vpc(this, 'Vpc', {
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC,
        }
      ]
    });

    // SecurityGroup
    const securityGroupELB = new ec2.SecurityGroup(this, 'SecurityGroupELB', {
      vpc
    });
    securityGroupELB.addIngressRule(ec2.Peer.ipv4('0.0.0.0/0'), ec2.Port.tcp(80));

    const securityGroupAPP = new ec2.SecurityGroup(this, 'SecurityGroupAPP', {
      vpc
    });

    // ALB
    const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
      vpc,
      securityGroup: securityGroupELB,
      internetFacing: true
    });
    const listenerHTTP = alb.addListener('ListenerHTTP', {
      port: 80,
    });

    // TargetGroup
    const targetGroup = new elbv2.ApplicationTargetGroup(this, "TG", {
      vpc: vpc,
      port: 3000,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targetType: elbv2.TargetType.IP,
      healthCheck: {
        path: '/',
        healthyHttpCodes: '200',
      },
    });

    listenerHTTP.addTargetGroups('DefaultHTTPSResponse', {
      targetGroups: [targetGroup]
    });

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

    // Fargate
    const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
      memoryLimitMiB: 1024,
      cpu: 512
    });
    const container = fargateTaskDefinition.addContainer("NextAppContainer", {
      image: ecs.ContainerImage.fromAsset("./docker/sandbox-create-next-app-ts/"),
      logging: ecs.LogDrivers.awsLogs({
        streamPrefix: 'next-app',
        logRetention: log.RetentionDays.ONE_MONTH,
      }),
    });
    container.addPortMappings({
      containerPort: 3000,
      hostPort: 3000
    })
    const service = new ecs.FargateService(this, 'Service', {
      cluster,
      taskDefinition: fargateTaskDefinition,
      desiredCount: 1,
      assignPublicIp: true,
      securityGroups: [
        securityGroupAPP
      ]
    });
    service.attachToApplicationTargetGroup(targetGroup);

  }
}

これにより、以下のイメージのような、ALB配下にECSサービス(Fargate)を起動する構成を作成することができます。

https://d1.awsstatic.com/icons/jp/cdp/renewal/diagram_ec-container_v2.85a0ad9ebf4bd95e18df84db4b274ba3b36f8586.png

引用: コンテナを利用した Web サービスでの Amazon ECS/AWS Fargate 利用構成と料金試算例 | AWS

さて、この実装のポイントですが、以下の箇所がECSタスク定義(Fargate)部分になります。 この数行で、これだけのことが行われます。

  • ECRレポジトリの作成
  • docker build
  • docker tag
  • docker push(ECRレポジトリへpush)
  • タスク定義へdocker uriの登録
  • タスク定義の更新
    const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
      memoryLimitMiB: 1024,
      cpu: 512
    });
    const container = fargateTaskDefinition.addContainer("NextAppContainer", {
      image: ecs.ContainerImage.fromAsset("./docker/sandbox-create-next-app-ts/"),
      logging: ecs.LogDrivers.awsLogs({
        streamPrefix: 'next-app',
        logRetention: log.RetentionDays.ONE_MONTH,
      }),
    });

つまりCDK側でアプリケーション(./docker/sandbox-create-next-app-ts/)の変更を検知して、変更がある場合docker buildからタスク定義の更新までよしなにやってくれるのです。(めちゃくちゃ便利)

ちなみにDockerイメージにつけられるタグは、アプリケーションディレクトリのチェックサムが使用されているようでした。

デプロイ

最後にデプロイを行います。

デプロイ実行の前に差分の確認をしてから

$ npx cdk diff

~省略~

Conditions
[+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"af-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]}

Resources
[+] AWS::EC2::VPC Vpc Vpc8378EB38
[+] AWS::EC2::Subnet Vpc/publicSubnet1/Subnet VpcpublicSubnet1Subnet2BB74ED7
[+] AWS::EC2::RouteTable Vpc/publicSubnet1/RouteTable VpcpublicSubnet1RouteTable15C15F8E
[+] AWS::EC2::SubnetRouteTableAssociation Vpc/publicSubnet1/RouteTableAssociation VpcpublicSubnet1RouteTableAssociation4E83B6E4
[+] AWS::EC2::Route Vpc/publicSubnet1/DefaultRoute VpcpublicSubnet1DefaultRouteB88F9E93
[+] AWS::EC2::Subnet Vpc/publicSubnet2/Subnet VpcpublicSubnet2SubnetE34B022A
[+] AWS::EC2::RouteTable Vpc/publicSubnet2/RouteTable VpcpublicSubnet2RouteTableC5A6DF77
[+] AWS::EC2::SubnetRouteTableAssociation Vpc/publicSubnet2/RouteTableAssociation VpcpublicSubnet2RouteTableAssociationCCE257FF
[+] AWS::EC2::Route Vpc/publicSubnet2/DefaultRoute VpcpublicSubnet2DefaultRoute732F0BEB
[+] AWS::EC2::InternetGateway Vpc/IGW VpcIGWD7BA715C
[+] AWS::EC2::VPCGatewayAttachment Vpc/VPCGW VpcVPCGWBF912B6E
[+] AWS::EC2::SecurityGroup SecurityGroupELB SecurityGroupELB44F0333E
[+] AWS::EC2::SecurityGroup SecurityGroupAPP SecurityGroupAPPF0E891C7
[+] AWS::EC2::SecurityGroupIngress SecurityGroupAPP/from FargateCdkDemoStackSecurityGroupELB1D4E6F6E:3000 SecurityGroupAPPfromFargateCdkDemoStackSecurityGroupELB1D4E6F6E3000FACD674D
[+] AWS::ElasticLoadBalancingV2::LoadBalancer ALB ALBAEE750D2
[+] AWS::ElasticLoadBalancingV2::Listener ALB/ListenerHTTP ALBListenerHTTPA7ECD76F
[+] AWS::ElasticLoadBalancingV2::TargetGroup TG TGB29B09E7
[+] AWS::ECS::Cluster Cluster ClusterEB0386A7
[+] AWS::IAM::Role TaskDef/TaskRole TaskDefTaskRole1EDB4A67
[+] AWS::ECS::TaskDefinition TaskDef TaskDef54694570
[+] AWS::Logs::LogGroup TaskDef/NextAppContainer/LogGroup TaskDefNextAppContainerLogGroupF97AD702
[+] AWS::IAM::Role TaskDef/ExecutionRole TaskDefExecutionRoleB4775C97
[+] AWS::IAM::Policy TaskDef/ExecutionRole/DefaultPolicy TaskDefExecutionRoleDefaultPolicy0DBB737A
[+] AWS::ECS::Service Service/Service ServiceD69D759B

デプロイ!

$ npx cdk deploy

~省略~

FargateCdkDemoStack: deploying...
[0%] start: Publishing 5d7ddf750c7f3831fb906b259a76a6789cdeb51d2fd831706922ccd9f09daec4:current
#1 [internal] load build definition from Dockerfile
#1 sha256:c68e97cccc8cef2a0df69b550cf1d46cb0f6c4c33c34335b6ee01809c1b637cb
#1 transferring dockerfile: 186B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 sha256:d4cfff45a929486b92b7a3536eb5e913ccef1efae98d1330fb1467ed90015084
#2 transferring context: 58B done
#2 DONE 0.0s

#3 [internal] load metadata for docker.io/library/node:14-alpine
#3 sha256:a4d7d3caeb58aee7272d38e41f54c299e593ee826f0d11b0b153dd03a81fefe1
#3 DONE 1.9s

#8 [1/4] FROM docker.io/library/node:14-alpine@sha256:ed51af876dd7932ce5c1e3b16c2e83a3f58419d824e366de1f7b00f40c848c40
#8 sha256:edbde4a12f5d8aa0b556381599d014a7c2b23c83d2581b2418ffb1490ad16abe
#8 DONE 0.0s

#5 [internal] load build context
#5 sha256:7313b6e4328604c032fcacf05406dcc55d548d9dd1bfaa8bb9c8f3fecf818889
#5 transferring context: 12.89MB 0.5s done
#5 DONE 0.5s

#4 [2/4] WORKDIR /app
#4 sha256:367e9797267c02fa2103312763927b3d9d90f0cbb2e7c94662053bfda17313c9
#4 CACHED

#6 [3/4] COPY . /app
#6 sha256:8742258e0d88720b9300aee4513f5afe6fe7b79331fa6dc7252e7c62ae47f6fc
#6 CACHED

#7 [4/4] RUN npm install &&     npm run build
#7 sha256:1531a061ab6c26e2132c559ebe51426c8408ddd636a72403cf033124420f28db
#7 CACHED

#9 exporting to image
#9 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
#9 exporting layers done
#9 writing image sha256:7c5fa1d4402ed9b635522923f784f1c91fe38c9e583cbd5a1a557486c184ef17 done
#9 naming to docker.io/library/cdkasset-5d7ddf750c7f3831fb906b259a76a6789cdeb51d2fd831706922ccd9f09daec4 done
#9 DONE 0.0s
The push refers to repository [xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/aws-cdk/assets]
023a63c4faf4: Preparing
74567fd067ea: Preparing
424acc639200: Preparing
47680fe39caf: Preparing
7454fd6f9639: Preparing
0213df4e2fdb: Preparing
9a5d14f9f550: Preparing
0213df4e2fdb: Waiting
9a5d14f9f550: Waiting
47680fe39caf: Layer already exists
74567fd067ea: Layer already exists
424acc639200: Layer already exists
023a63c4faf4: Layer already exists
7454fd6f9639: Layer already exists
0213df4e2fdb: Layer already exists
9a5d14f9f550: Layer already exists
5d7ddf750c7f3831fb906b259a76a6789cdeb51d2fd831706922ccd9f09daec4: digest: sha256:85b9880d8c4c31a21f5957c6d50fc4a080069c8a563b813b21a90e44e7c97673 size: 1788
[100%] success: Published 5d7ddf750c7f3831fb906b259a76a6789cdeb51d2fd831706922ccd9f09daec4:current
FargateCdkDemoStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (26/26)










 ✅  FargateCdkDemoStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxx:stack/FargateCdkDemoStack/bee04e00-a0e8-11eb-b9ae-064eebb86817

docker buildとpush が行われてからCloudFormationの更新が行われていることがわかると思います。

デプロイ完了後、作成されたALBにブラウザからアクセスしてみると、正常にページが表示できることが確認できました!

f:id:nrt-krmx:20210419174930p:plain

最後に

以上が、CDKを使用したECS Fargateのデプロイになります。 CDKを使用することでデプロイ時のDockerの扱いに悩んだり、ビルドスクリプトを作ったりする手間が省けます。

CDKは活発に開発されているプロジェクトなので、次々に実装される便利な機能を使いこなしていきたいですね。