AWS ECS Blue/GreenデプロイのCI/CD環境構築をCloudFormationで自動化してみた
AWSの手頃なCI/CD環境が欲しかったので公式ハンズオンを組み合わせて構築してみた。
最初は構成理解のために手動で構築を始めたが、時間課金のリソース(主にALB)が財布に厳しかったのと目的に沿って拡張できるように再現性が欲しかったので、必要な時だけ簡単に作り直せるようCloudFormation化してみた。
CloudFormationテンプレートのみを残すとインプットや手順の再現性がなくなってしまうので、備忘として作成過程の記録を残してみる。
構成概要
基本的な構成は練習題材としてよく参照されていると思われる、以下2つのハンズオン(JP Contents Hubより)をベースとしている。
まずは手動で構築したかった経緯から、前者のECS環境構築手順に後付けする形で、後者に含まれるCode Deploy Blue/GreenとCodePipeline構築手順の部分を組み合わせた構成にしている。
組み合わせに伴って、両者のハンズオンから大きく変更したポイントは以下の通り。
- コンテナイメージは
Next.js
を使用する - ソースリポジトリはCode Commitではなく
GitHub
を使用する
最終的に作成した構成は以下のイメージになる。(CloudFormationテンプレートをLLMにMermaid出力させたものなので詳細は割愛)
ECS環境の構築
以下の2点を除き、ひとつ目のハンズオンの手順(Amazon Elastic Container Service 入門 コンテナイメージを作って動かしてみよう)に従ってECS環境の構築を行う。
-
02 コンテナイメージを作成するための、Cloud9環境を構築する
は省略してローカル端末でビルドする - コンテナイメージはNext.jsを使用する
前者についてはCloud9環境の取扱コストを避けたかった以外に内容はないが、後者については手順周りを補足する。
1. Next.jsのコンテナイメージ作成
リファレンスよりNode.jsのRuntimeに載せる形でビルドすれば良いことがわかる。やや手抜きではあるが、サンプルのDockerfileをそのまま利用する形で作成した。
-
create-next-app
コマンドでNext.jsアプリを作成する - 公式記載のClone our exampleより、リンク先リポジトリのDockerfileを、作成したアプリのルートに配置する
Dockerfile
# syntax=docker.io/docker/dockerfile:1
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
2. コンテナのポート番号周りの対応
Dockerfileより、作成したコンテナはポート番号3000
で起動するので、80
想定となっているハンズオンの手順を一部読み替える必要がある。(読み替える方が色々とわかることがあるという考え)
- 06 VPCの作成
- セキュリティグループのインバウンドルールにカスタムTCPでポート範囲3000を追加する
- 07 ECSクラスター・タスク定義の作成
- タスク定義のポートマッピングのコンテナポートに80番ではなく3000番を指定する
ECS環境のCloudFormation化
ハンズオンの06 VPCの作成
、07 ECSクラスター・タスク定義の作成
の単位でそれぞれに対応したCloudFormationテンプレートを作成する。(ECRは作成コストがかからず放置した際のストレージコストも月額USD0.10/GBとそれほど問題にならないため対象外とした)
作成対象のリソースを特定できれば、CloudFormationのIaCジェネレーターを利用することでテンプレートの雛形を取得できるので、基本的な流れとしてはリソースの特定
-> IaCジェネレーターによるテンプレート雛形の生成
-> テンプレートの整形
の手順とした。
Networkテンプレートの作成
リソースの特定はある程度機械的に行いたかったのでCloudTrailのイベント履歴
を活用することにした。イベント名CreateVpc
でリソース作成イベントが発生した時間を特定し、改めて前後数分を検索することでCreateX
されているリソースをリストアップした。
次に、IaCジェネレーターでリストアップしたリソースをチェックする。この時関連リソースが提示されるが、リストしていなかったDHCPOptions
やNetworkAcl
といったNW系の名称を持つリソースが数件存在していた。これらはVPC作成時にAWSがデフォルトで作成するリソースであるため、変更を加える意図がなければCloudFormationテンプレートに定義する必要はなかった。Security Group
はポート番号の読み替えの都合で変更していたのでテンプレートに含めた。
整形後のテンプレートは以下の通り。
network.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Network Stack
Parameters:
System:
Description: System Name
Type: String
Env:
Description: Environment
Type: String
Resources:
Vpc:
Type: AWS::EC2::VPC
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
InstanceTenancy: default
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-igw
VpcGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref Vpc
PublicSubnetAz1:
Type: AWS::EC2::Subnet
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
VpcId: !Ref Vpc
MapPublicIpOnLaunch: false
EnableDns64: false
AvailabilityZoneId: apne1-az1
PrivateDnsNameOptionsOnLaunch:
EnableResourceNameDnsARecord: false
HostnameType: ip-name
EnableResourceNameDnsAAAARecord: false
CidrBlock: 10.0.16.0/20
Ipv6Native: false
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-subnet-public-endpoint-az1
PublicSubnetAz4:
Type: AWS::EC2::Subnet
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
VpcId: !Ref Vpc
MapPublicIpOnLaunch: false
EnableDns64: false
AvailabilityZoneId: apne1-az4
PrivateDnsNameOptionsOnLaunch:
EnableResourceNameDnsARecord: false
HostnameType: ip-name
EnableResourceNameDnsAAAARecord: false
CidrBlock: 10.0.0.0/20
Ipv6Native: false
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-subnet-public-endpoint-az4
SecurityGroup:
Type: AWS::EC2::SecurityGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
GroupName: !Sub ${System}-${Env}-sg-web
GroupDescription: !Sub ${System}-${Env}-sg-web
VpcId: !Ref Vpc
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
IpProtocol: tcp
FromPort: 80
ToPort: 80
- CidrIp: 0.0.0.0/0
IpProtocol: tcp
FromPort: 3000
ToPort: 3000
- IpProtocol: "-1"
FromPort: -1
SourceSecurityGroupId: !GetAtt Vpc.DefaultSecurityGroup
ToPort: -1
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
IpProtocol: "-1"
FromPort: -1
ToPort: -1
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-sg-web
RouteTable:
Type: AWS::EC2::RouteTable
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-rtb-public
InternetGatewayRoute:
Type: AWS::EC2::Route
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
RouteTableId: !Ref RouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetAz1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
RouteTableId: !Ref RouteTable
SubnetId: !Ref PublicSubnetAz1
PublicSubnetAz4RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
RouteTableId: !Ref RouteTable
SubnetId: !Ref PublicSubnetAz4
# S3VpcEndpoint:
# Type: AWS::EC2::VPCEndpoint
# DeletionPolicy: Delete
# UpdateReplacePolicy: Delete
# Properties:
# PrivateDnsEnabled: false
# IpAddressType: not-specified
# DnsOptions:
# PrivateDnsOnlyForInboundResolverEndpoint: NotSpecified
# DnsRecordIpType: not-specified
# ResourceConfigurationArn: ""
# SecurityGroupIds: []
# SubnetIds: []
# ServiceNetworkArn: ""
# VpcId: !Ref Vpc
# RouteTableIds: []
# ServiceName: com.amazonaws.ap-northeast-1.s3
# PolicyDocument:
# Version: "2008-10-17"
# Statement:
# - Resource: "*"
# Action: "*"
# Effect: Allow
# Principal: "*"
# VpcEndpointType: Gateway
# Tags:
# - Key: Name
# Value: !Sub ${System}-${Env}-vpce-s3
Outputs:
VpcId:
Description: VPC ID
Value: !Ref Vpc
Export:
Name: !Sub ${AWS::StackName}-vpc-id
SecurityGroupId:
Description: Security Group ID
Value: !Ref SecurityGroup
Export:
Name: !Sub ${AWS::StackName}-sg-id
PublicSubnetIds:
Description: Public Subnet IDs
Value: !Join [",", [!Ref PublicSubnetAz1, !Ref PublicSubnetAz4]]
Export:
Name: !Sub ${AWS::StackName}-public-subnet-ids
ECSテンプレートの作成
ECSのリソースは構築時にCloudFormationスタックが自動で作成される。これらのテンプレートにはECS Cluster
やECS Service
が含まれているが、ECS タスク定義
は含まれていない。不足を補うためにIaCジェネレーターでECS タスク定義
をチェックし、関連リソースも提示されたものをすべて含める形でテンプレート生成を行った。
生成結果のなかで必要なものはECS タスク定義
とタスクロール
のみでAWSServiceRoleForECS
はAWSがデフォルトで作成するものであり、またその他はNetworkテンプレートでカバーできるものであった。
また、TargetGroup.Properties.HealthCheckPort
は指定しなければデフォルトでtraffic-port=コンテナポートを向いてくれるようであるのでそのままにした。
If the protocol is HTTP, HTTPS, TCP, TLS, UDP, or TCP_UDP, the default is traffic-port, which is the port on which each target receives traffic from the load balancer.
自動作成されたスタックのテンプレートとIaCジェネレーターで生成したテンプレートをマージして整形したテンプレートは以下の通り。
ecs.yml
AWSTemplateFormatVersion: 2010-09-09
Description: ECS Stack
Parameters:
System:
Description: System Name
Type: String
Env:
Description: Environment
Type: String
ImageUri:
Description: Image URI
Type: String
Resources:
EcsLogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
FieldIndexPolicies: []
LogGroupClass: STANDARD
LogGroupName: !Sub /ecs/${System}-${Env}-ecs-td
DataProtectionPolicy: {}
EcsCluster:
Type: AWS::ECS::Cluster
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ClusterName: !Sub ${System}-${Env}-ecs-cluster
CapacityProviders:
- FARGATE
- FARGATE_SPOT
ClusterSettings:
- Name: containerInsights
Value: disabled
ServiceConnectDefaults:
Namespace: !Sub ${System}-${Env}
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-ecs-cluster
EcsTaskDefinition:
Type: AWS::ECS::TaskDefinition
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ExecutionRoleArn:
Fn::GetAtt:
- "EcsTaskExecutionRole"
- "Arn"
RuntimePlatform:
OperatingSystemFamily: "LINUX"
CpuArchitecture: "X86_64"
Volumes: []
InferenceAccelerators: []
Memory: "1024"
PlacementConstraints: []
ContainerDefinitions:
- ExtraHosts: []
Secrets: []
VolumesFrom: []
Cpu: 0
EntryPoint: []
DnsServers: []
Image: !Ref ImageUri
Essential: true
LogConfiguration:
SecretOptions: []
Options:
awslogs-group: !Ref EcsLogGroup
mode: "non-blocking"
max-buffer-size: "25m"
awslogs-create-group: "true"
awslogs-region: "ap-northeast-1"
awslogs-stream-prefix: "ecs"
LogDriver: "awslogs"
ResourceRequirements: []
EnvironmentFiles: []
Name: !Sub ecs-container
MountPoints: []
DependsOn: []
DockerLabels: {}
PortMappings:
- ContainerPort: 3000
AppProtocol: "http"
Protocol: "tcp"
HostPort: 3000
Name: !Sub ${Env}--${System}--container--3000--tcp
DockerSecurityOptions: []
SystemControls: []
Command: []
DnsSearchDomains: []
Environment: []
Links: []
CredentialSpecs: []
Ulimits: []
Cpu: "512"
RequiresCompatibilities:
- "FARGATE"
Family: !Sub ${System}-${Env}-ecs-td
NetworkMode: "awsvpc"
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-ecs-td
EcsTaskExecutionRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Path: "/"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
MaxSessionDuration: 3600
RoleName: !Sub ${System}-${Env}-role-ecs-task-execution
AssumeRolePolicyDocument:
Version: "2008-10-17"
Statement:
- Action: "sts:AssumeRole"
Effect: "Allow"
Principal:
Service: "ecs-tasks.amazonaws.com"
Sid: ""
EcsService:
Type: AWS::ECS::Service
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Cluster: !Ref EcsCluster
CapacityProviderStrategy:
- CapacityProvider: FARGATE
Base: 0
Weight: 1
TaskDefinition: !Ref EcsTaskDefinition
ServiceName: !Sub ${System}-${Env}-ecs-sv
SchedulingStrategy: REPLICA
DesiredCount: 1
AvailabilityZoneRebalancing: ENABLED
LoadBalancers:
- ContainerName: !Sub ecs-container
ContainerPort: 3000
LoadBalancerName: !Ref AWS::NoValue
TargetGroupArn: !Ref TargetGroup
HealthCheckGracePeriodSeconds: "30"
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-sg-id
Subnets:
Fn::Split:
- ","
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-public-subnet-ids
PlatformVersion: LATEST
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DeploymentCircuitBreaker:
Enable: true
Rollback: true
DeploymentController:
Type: ECS
ServiceConnectConfiguration:
Enabled: false
Tags: []
EnableECSManagedTags: true
DependsOn:
- Listener
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Type: application
Name: !Sub ${System}-${Env}-alb-web
SecurityGroups:
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-sg-id
Subnets:
Fn::Split:
- ","
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-public-subnet-ids
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
HealthCheckPath: /
Name: !Sub ${System}-${Env}-elb-tg-web
Port: 3000
Protocol: HTTP
TargetType: ip
HealthCheckProtocol: HTTP
VpcId:
Fn::ImportValue: !Sub ${System}-${Env}-network-stack-vpc-id
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: "300"
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
CI/CDパイプラインの構築
以下の4点を除き、ふたつ目のハンズオンの手順(AWS CI/CD for Amazon ECS ハンズオン)のP63〜
を参照してECS ServiceをDeployType = Blue/Green
で作り直したうえで、Blue/Greenデプロイに対応したCI/CDパイプラインの構築を行なう。
1. CodeConnectionsでGitHubと接続
CodeCommitは残念ながら今後の発展が見込めないため、将来性の観点からSource ProviderはGitHubへの置き換えを試すことにした。
After careful consideration, we have made the decision to close new customer access to AWS CodeCommit, effective July 25, 2024. AWS CodeCommit existing customers can continue to use the service as normal. AWS continues to invest in security, availability, and performance improvements for AWS CodeCommit, but we do not plan to introduce new features.
こちらの記事を参考にしてGitHubと接続するためのCodeConnectionsのリソースを作成した。
CodePipelineのソースプロバイダーはGitHub (バージョン 2)
を指定して道なりに設定を行う。
The current GitHub (via GitHub App) action is the version 2 source action for GitHub.
2. buildspec.ymlの最新化
ハンズオンのbuildspec.ymlのままであると、こちらの記事で言及されている通りECRへのログインコマンドが旧いため実行に失敗してしまう。
記事の内容を念頭に、ECS Web Application ハンズオンで提供されているbuildspec.ymlをベースにして最新化を行なった。
一部の環境変数を外出しにしているので、CodeBuild側で設定を行う。
buildspec.yml
version: 0.2
phases:
pre_build:
commands:
- AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
- REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NAME}
- IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
- aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
- $(aws ecs describe-task-definition --task-definition ${ECS_TASK_DEFINITION_ARN} --query taskDefinition | jq '.containerDefinitions[0].image="<IMAGE1_NAME>"' > taskdef.json)
build:
commands:
- docker build -t ${REPOSITORY_URI}:latest .
- docker tag ${REPOSITORY_URI}:latest ${REPOSITORY_URI}:${IMAGE_TAG}
post_build:
commands:
- docker push ${REPOSITORY_URI}:${IMAGE_TAG}
- printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
files:
- imageDetail.json
- taskdef.json
3. taskdef.jsonをビルド時に生成
増築している背景もあるが、ハンズオンの構成だとECS タスク定義をECSテンプレートと静的に二重管理することになり忌避感があったので、こちらの記事を参考にしてtaskdef.jsonをビルド時に動的に生成することにした。(どちらで管理すべきかは別途検討が必要だと思うが暫定的に)
上述のbuildspec.ymlに反映している。
4. appspec.ymlのコンテナポート番号を変更
Next.jsのコンテナイメージに対応するため、コンテナポート番号を80 -> 3000に変更した。
appspec.yml
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "<TASK_DEFINITION>"
LoadBalancerInfo:
ContainerName: ecs-container
ContainerPort: 3000
CI/CDパイプラインのCloudFormation化
すでに作成したECSテンプレートのBlue/Greenデプロイ拡張版、CodeConnections、CI/CDの単位でテンプレートを作成する。基本的にはECSテンプレートの作成手順と同様になるが、CodeConnectionsやCodePipelineはIaCジェネレーターの対象外サービスとなっているので別途ベースを探す必要があった。
ECS Blue/Greenテンプレートの作成
ECSテンプレート作成時と同様に、ECS ServiceのDeployType = Blue/Green
への作り直しを行うと対応するCloudFormationスタックが新たに自動で作成される。作成済みのECSテンプレートとの差分リソースがシンプルにBlue/Greenデプロイに関連したものになるのでその分を更新すれば良い。
差分をマージして整形したテンプレートは以下の通り。
ecs-bluegreen.yml
AWSTemplateFormatVersion: 2010-09-09
Description: ECS Fargate with Blue/Green Deploy Stack
Parameters:
System:
Description: System Name
Type: String
Env:
Description: Environment
Type: String
ImageUri:
Description: Image URI
Type: String
Resources:
EcsLogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
FieldIndexPolicies: []
LogGroupClass: STANDARD
LogGroupName: !Sub /ecs/${System}-${Env}-ecs-td
DataProtectionPolicy: {}
EcsCluster:
Type: AWS::ECS::Cluster
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ClusterName: !Sub ${System}-${Env}-ecs-cluster
CapacityProviders:
- FARGATE
- FARGATE_SPOT
ClusterSettings:
- Name: containerInsights
Value: disabled
ServiceConnectDefaults:
Namespace: !Sub ${System}-${Env}
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-ecs-cluster
EcsTaskDefinition:
Type: AWS::ECS::TaskDefinition
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ExecutionRoleArn:
Fn::GetAtt:
- "EcsTaskExecutionRole"
- "Arn"
RuntimePlatform:
OperatingSystemFamily: "LINUX"
CpuArchitecture: "X86_64"
Volumes: []
InferenceAccelerators: []
Memory: "1024"
PlacementConstraints: []
ContainerDefinitions:
- ExtraHosts: []
Secrets: []
VolumesFrom: []
Cpu: 0
EntryPoint: []
DnsServers: []
Image: !Ref ImageUri
Essential: true
LogConfiguration:
SecretOptions: []
Options:
awslogs-group: !Ref EcsLogGroup
mode: "non-blocking"
max-buffer-size: "25m"
awslogs-create-group: "true"
awslogs-region: "ap-northeast-1"
awslogs-stream-prefix: "ecs"
LogDriver: "awslogs"
ResourceRequirements: []
EnvironmentFiles: []
Name: ecs-container
MountPoints: []
DependsOn: []
DockerLabels: {}
PortMappings:
- ContainerPort: 3000
AppProtocol: "http"
Protocol: "tcp"
HostPort: 3000
Name: ecs-container-3000-tcp
DockerSecurityOptions: []
SystemControls: []
Command: []
DnsSearchDomains: []
Environment: []
Links: []
CredentialSpecs: []
Ulimits: []
Cpu: "512"
RequiresCompatibilities:
- "FARGATE"
Family: !Sub ${System}-${Env}-ecs-td
NetworkMode: "awsvpc"
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-ecs-td
EcsTaskExecutionRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
MaxSessionDuration: 3600
RoleName: !Sub ${System}-${Env}-role-ecs-task-execution
AssumeRolePolicyDocument:
Version: 2008-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
EcsService:
Type: AWS::ECS::Service
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Cluster: !Ref EcsCluster
CapacityProviderStrategy:
- CapacityProvider: FARGATE
Base: 0
Weight: 1
TaskDefinition: !Ref EcsTaskDefinition
ServiceName: !Sub ${System}-${Env}-ecs-sv
SchedulingStrategy: REPLICA
DesiredCount: 1
AvailabilityZoneRebalancing: ENABLED
LoadBalancers:
- ContainerName: ecs-container
ContainerPort: 3000
LoadBalancerName: !Ref AWS::NoValue
TargetGroupArn: !Ref TargetGroup1
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-sg-id
Subnets:
Fn::Split:
- ","
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-public-subnet-ids
PlatformVersion: LATEST
DeploymentController:
Type: CODE_DEPLOY
ServiceConnectConfiguration:
Enabled: false
Tags: []
EnableECSManagedTags: true
DependsOn:
- Listener
TargetGroup1:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
HealthCheckPath: /
Name: !Sub ${System}-${Env}-elb-tg-web-1
Port: 3000
Protocol: HTTP
TargetType: ip
HealthCheckProtocol: HTTP
VpcId:
Fn::ImportValue: !Sub ${System}-${Env}-network-stack-vpc-id
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: "300"
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup1
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
TargetGroup2:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
HealthCheckPath: /
Name: !Sub ${System}-${Env}-elb-tg-web-2
Port: 3000
Protocol: HTTP
TargetType: ip
HealthCheckProtocol: HTTP
VpcId:
Fn::ImportValue: !Sub ${System}-${Env}-network-stack-vpc-id
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: "300"
CodeDeployRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
MaxSessionDuration: 3600
RoleName: !Sub ${System}-${Env}-role-cd
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codedeploy.amazonaws.com
CodeDeployDeploymentGroup:
Type: AWS::CodeDeploy::DeploymentGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ApplicationName: !Ref CodeDeployApplication
DeploymentGroupName: !Sub ${System}-${Env}-cd-group
DeploymentConfigName: CodeDeployDefault.ECSAllAtOnce
AutoRollbackConfiguration:
Enabled: true
Events:
- DEPLOYMENT_FAILURE
- DEPLOYMENT_STOP_ON_REQUEST
BlueGreenDeploymentConfiguration:
DeploymentReadyOption:
ActionOnTimeout: CONTINUE_DEPLOYMENT
WaitTimeInMinutes: 0
TerminateBlueInstancesOnDeploymentSuccess:
Action: TERMINATE
TerminationWaitTimeInMinutes: 60
DeploymentStyle:
DeploymentOption: WITH_TRAFFIC_CONTROL
DeploymentType: BLUE_GREEN
LoadBalancerInfo:
TargetGroupPairInfoList:
- ProdTrafficRoute:
ListenerArns:
- !GetAtt Listener.ListenerArn
TargetGroups:
- Name: !GetAtt TargetGroup1.TargetGroupName
- Name: !GetAtt TargetGroup2.TargetGroupName
ServiceRoleArn: !GetAtt CodeDeployRole.Arn
ECSServices:
- ClusterName: !Sub ${System}-${Env}-ecs-cluster
ServiceName: !Sub ${System}-${Env}-ecs-sv
Tags: []
DependsOn:
- CodeDeployApplication
CodeDeployApplication:
Type: AWS::CodeDeploy::Application
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ApplicationName: !Sub ${System}-${Env}-cd-app
ComputePlatform: ECS
Tags: []
DependsOn:
- EcsService
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Type: application
Name: !Sub ${System}-${Env}-alb-web
SecurityGroups:
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-sg-id
Subnets:
Fn::Split:
- ","
- Fn::ImportValue: !Sub ${System}-${Env}-network-stack-public-subnet-ids
Outputs:
EcsTaskDefinitionArn:
Description: ECS Task Definition Arn
Value: !Ref EcsTaskDefinition
Export:
Name: !Sub ${AWS::StackName}-ecs-td-arn
CodeDeployAppName:
Description: CodeDeploy Application Name
Value: !Ref CodeDeployApplication
Export:
Name: !Sub ${AWS::StackName}-cd-app-name
CodeDeployDGName:
Description: CodeDeploy Deployment Group Name
Value: !Ref CodeDeployDeploymentGroup
Export:
Name: !Sub ${AWS::StackName}-cd-group-name
CodeConnectionsテンプレートの作成
前述の通り、CodeConnectionsはIaCジェネレーターの対象外であるためテンプレートのベースを別途参照する必要があった。ぱっと見ではネット上に参考になりそうな記事がなかったため、LLMに生成してもらいつつ仕様を確認する形で作成した。
また、以下の引用にある通り、CloudFormationでリソースを作成した後に別途コンソールでステータスを更新する手動対応は残ってしまう。
Note: A connection created through AWS CloudFormation, the CLI, or the SDK is in
PENDING
status by default. You can make its statusAVAILABLE
by updating the connection in the console.
connections.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Code Connections Stack
Parameters:
System:
Description: System Name
Type: String
Env:
Description: Environment
Type: String
Resources:
CodeConnections:
Type: AWS::CodeConnections::Connection
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ConnectionName: !Sub ${System}-${Env}-connections
ProviderType: GitHub
Tags:
- Key: Name
Value: !Sub ${System}-${Env}-connections
Outputs:
CodeConnections:
Description: Code Connections
Value: !Ref CodeConnections
Export:
Name: !Sub ${AWS::StackName}-connections-arn
CI/CDテンプレートの作成
CodePipelineもIaCジェネレーターの対象外であるため、別途テンプレートのベースを探す必要があった。CodePipelineのパイプラインを作成する
-> Category = Deployment
-> Template = Deploy to ECS Fargate
を選択していくとサンプルのCloudFomationテンプレートが得られるが、これはBlue/Greenデプロイには対応していなかった。形式自体は公式になると思われるのでこちらを一定ベースとしつつも、中核部分に関してはこちらの記事のplaceholder-codepipeline.yml
を参考にさせてもらった。
こちらのテンプレートではソースプロバイダをCodeCommitとしており、CodeConnectionsへの読み替えが必要であったため、以下の変更を行なっている。
1. CloudWatchEvent(EventBridge)関連リソースの除去
CodePipelineの実行トリガーとしてEventBridgeを定義しているが、CodeConnectionsの場合は独自に変更を検知してトリガーしてくれるので不要であった。
The pipeline defaults to detect changes on code push to the connection source repository.
2. SourceActionの変更
リファレンスを参照し、ProviderをCodeCommitからCodeStarSourceConnection
へ変更した。
3. CodePipeline用のIAMロールにCodeConnectionsへのアクセス権限を付与
CodeCommitへのアクセス権限の代わりにCodeConnectionsへのアクセス権限が必要であった。CodePipeline用のIAMロールについては手動構築時に作成されたリソースの許可ポリシー・信頼関係をコンソールで確認し裏付けを行なった。
整形後のテンプレートは以下の通り。
cicd.yml
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate with Blue/Green Deploy
Parameters:
System:
Description: System Name
Type: String
Env:
Description: Environment
Type: String
EcrRepositoryName:
Description: ECR Repository Name used in buildspec.yml
Type: String
GithubFullRepositoryId:
Description: Github Full Repository Id (e.g. <owner_name>/<repository_name>)
Type: String
Resources:
CodeBuildServiceRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
RoleName: !Sub ${System}-${Env}-pipeline-role-cb
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
Policies:
- PolicyName: PipelineCodeBuildAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Resource: "*"
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Effect: Allow
Resource: !Sub arn:aws:s3:::${CodePipelineArtifactsBucket}/*
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
- Effect: Allow
Resource: "*"
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
- Effect: Allow
Resource: "*"
Action:
- ecs:DescribeTaskDefinition
CodePipelineServiceRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
RoleName: !Sub ${System}-${Env}-role-cp
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: PipelineAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- iam:PassRole
Resource: "*"
Effect: Allow
Condition:
StringEqualsIfExists:
iam:PassedToService:
- ecs-tasks.amazonaws.com
- Resource:
- !Sub arn:aws:s3:::${CodePipelineArtifactsBucket}/*
Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketVersioning
- Action:
- codedeploy:CreateDeployment
- codedeploy:GetApplication
- codedeploy:GetApplicationRevision
- codedeploy:GetDeployment
- codedeploy:GetDeploymentConfig
- codedeploy:RegisterApplicationRevision
- codedeploy:*
Resource: "*"
Effect: Allow
- Action:
- elasticbeanstalk:*
- ec2:*
- elasticloadbalancing:*
- autoscaling:*
- cloudwatch:*
- sns:*
- cloudformation:*
- rds:*
- sqs:*
- ecs:*
Resource: "*"
Effect: Allow
- Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
- codebuild:BatchGetBuildBatches
- codebuild:StartBuildBatch
Resource: "*"
Effect: Allow
- Action:
- codestar-connections:UseConnection
Resource:
- Fn::ImportValue: !Sub ${System}-${Env}-connections-stack-connections-arn
Effect: Allow
CodePipelineArtifactsBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
CodePipelineArtifactsBucketPolicy:
Type: AWS::S3::BucketPolicy
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Bucket: !Ref CodePipelineArtifactsBucket
PolicyDocument:
Statement:
- Action: s3:*
Condition:
Bool:
aws:SecureTransport: false
Effect: Deny
Principal:
AWS: "*"
Resource:
- !GetAtt CodePipelineArtifactsBucket.Arn
- !Join
- ""
- - !GetAtt CodePipelineArtifactsBucket.Arn
- /*
Version: 2012-10-17
CodeBuildProject:
Type: AWS::CodeBuild::Project
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
ServiceRole: !Ref CodeBuildServiceRole
Artifacts:
Type: CODEPIPELINE
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yml
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
ImagePullCredentialsType: CODEBUILD
PrivilegedMode: false
Type: LINUX_CONTAINER
EnvironmentVariables:
- Name: REPOSITORY_NAME
Value: !Ref EcrRepositoryName
- Name: ECS_TASK_DEFINITION_ARN
Value:
Fn::ImportValue: !Sub ${System}-${Env}-ecs-bluegreen-stack-ecs-td-arn
Name: !Sub ${System}-${Env}-cb-build
CodePipeline:
Type: AWS::CodePipeline::Pipeline
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
RoleArn: !GetAtt CodePipelineServiceRole.Arn
Name: !Sub ${System}-${Env}-cp
PipelineType: V2
ExecutionMode: QUEUED
ArtifactStore:
Type: S3
Location: !Ref CodePipelineArtifactsBucket
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Version: "1"
Provider: CodeStarSourceConnection
Configuration:
ConnectionArn:
Fn::ImportValue: !Sub ${System}-${Env}-connections-stack-connections-arn
FullRepositoryId: !Ref GithubFullRepositoryId
BranchName: main
OutputArtifactFormat: CODE_ZIP
RunOrder: 1
OutputArtifacts:
- Name: SourceArtifact
- Name: Build
Actions:
- Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Version: "1"
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: 1
InputArtifacts:
- Name: SourceArtifact
OutputArtifacts:
- Name: BuildArtifact
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Version: "1"
Provider: CodeDeployToECS
Configuration:
AppSpecTemplateArtifact: SourceArtifact
AppSpecTemplatePath: appspec.yml
TaskDefinitionTemplateArtifact: BuildArtifact
TaskDefinitionTemplatePath: taskdef.json
ApplicationName:
Fn::ImportValue: !Sub ${System}-${Env}-ecs-bluegreen-stack-cd-app-name
DeploymentGroupName:
Fn::ImportValue: !Sub ${System}-${Env}-ecs-bluegreen-stack-cd-group-name
Image1ArtifactName: BuildArtifact
Image1ContainerName: IMAGE1_NAME
RunOrder: 1
InputArtifacts:
- Name: SourceArtifact
- Name: BuildArtifact
Region: !Ref AWS::Region
その他特記事項
CloudFormationテンプレートに定義するリソース名の命名規則
こちらの記事を参考にさせてもらった。
CloudFormationスタック作成時に注意すること
- OutputsとImportValueが整合するようにスタック名とParameterの指定に気をつける
- スタック名は以下の指定を想定している
- ${System}-${Env}-network-stack
- ${System}-${Env}-ecs-stack
- ${System}-${Env}-ecs-bluegreen-stack
- ${System}-${Env}-connections-stack
- ${System}-${Env}-cicd-stack
CodePipeline実行時に気をつけること
すでに参照した記事でも言及されているが、根本的にCodeBuildの実行で落ちる可能性がある。回避策は特段取っていないので、エラーになった場合は気長に再実行する想定としている。
Discussion