お久しぶりです、SREチームの成田です。 今回はCDKを使用してECS Fargate環境の作成、デプロイを行う方法をご紹介します。
ECS Fargateについて
ECSとは
ECSはフルマネージド型のコンテナオーケストレーションサービスです。 コンテナの実行、停止、スケーリング、デプロイ等の管理を行えます。
類似サービスとしては、KubernetesとそのマネージドサービスであるEKSが挙げられますが、他のAWSサービスに慣れているのであればECSの方がとっつきやすいかなと思います。
Fargateとは
Fargateは、ECSやEKSにおいてコンテナをサーバレスで実行できるサービスです。
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環境を用意します。
以下の内容で.dockerignore
とDockerfile
を作成します。
.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)を起動する構成を作成することができます。
引用: コンテナを利用した 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にブラウザからアクセスしてみると、正常にページが表示できることが確認できました!
最後に
以上が、CDKを使用したECS Fargateのデプロイになります。 CDKを使用することでデプロイ時のDockerの扱いに悩んだり、ビルドスクリプトを作ったりする手間が省けます。
CDKは活発に開発されているプロジェクトなので、次々に実装される便利な機能を使いこなしていきたいですね。