#概要
AWS の公式ユーザーガイドにより、CloudFormation を使用して、CodeDeploy による ECS ブルー/グリーンデプロイを実行することができます。
しかし、条件としては「CloudFormation を通じて CodeDeploy を使用して ECS ブルー/グリーンデプロイを実行するには、テンプレートに Amazon ECS サービスとロードバランサーなど、デプロイをモデル化するリソースを含める必要があります。」
また、現用系(ブルー、Prod)から開発環境(グリーン、Test)への切り替えトリガーは、「次の ECS リソースの置き換えが必要なプロパティを更新するスタック更新を実行すると、CloudFormation はグリーンデプロイを開始します。」
AWS::ECS::TaskDefinition
AWS::ECS::TaskSet
ここで、Transform というAWSから提供されるマクロの機能とCodeDeploy::BlueGreenフックを使って ECS の Blue/Green の環境を作成して検証します。
以下は注意するべきこと。
1.ECSサービスやALB、タスク、コンテナに等は、事前に作成しておいたり、他のスタックで作成したものを使うことはできません。これらのすべてリソースは、Transformと同じテンプレート内で定義する必要がある。
2.CodeDeploy と連携させて実現していた ECS のServiceのDeploymentControllerタイプは、Externalなデプロイメントコントローラーとして使用する。
3.CodeDeployのところに、アプリケーションやデプロイグループが作成されません
#環境用意
ECRリポジトリ
VPC
Subnet1a
Subnet1c
SecurityGroup
Dockerfile
#検証方法
##1.現用系(ブルー)アプリを作成
Ubuntu18コンテナにイメージファイル。コンテナにが実行されたらWebBrowserに“Hello World!”を表示される。
FROM ubuntu:18.04
-Install dependencies
RUN apt-get update && \
apt-get -y install apache2
- Install apache and write hello world message
RUN echo 'Hello World!' > /var/www/html/index.html
- Configure apache
RUN echo '. /etc/apache2/envvars' > /root/run_apache.sh && \
echo 'mkdir -p /var/run/apache2' >> /root/run_apache.sh && \
echo 'mkdir -p /var/lock/apache2' >> /root/run_apache.sh && \
echo '/usr/sbin/apache2 -D FOREGROUND' >> /root/run_apache.sh && \
chmod 755 /root/run_apache.sh
EXPOSE 80
CMD /root/run_apache.sh
2.グリーン系:
Nginxコンテナにイメージファイル。コンテナにが実行されたらWebBrowserに“Welcome to nginx!...”を表示される。
デモテンプレート(demo-template.yml)
Fargate で Ununtu18 コンテナを動かして、ALB経由でHTTP 80番ポートでアクセスするの“HelloWorld!”メッセージを作成する。
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
AwsTokyoRegion:
Type: String
Default: xxxxxxxx
Service:
Type: String
Default: system
Env:
Type: String
Default: dev
App:
Type: String
Default: hello
ECRName:
Type: String
Default: xxxxxx
ContainerName:
Type: String
Default: xxxxxxxxxxx1
CodeCommitRepositoryName:
Type: String
Default: xxxxxxxxxxx
NameTagPrefix:
Type: String
Default: hook
Description: Prefix of Name tags.
ServiceName:
Type: String
Default: xxxx
Description: Prefix of Service tags.
Vpc:
Type: AWS::EC2::VPC::Id
Default: 'vpc-xxxxxxxxxxx'
Subnet1a:
Type: AWS::EC2::Subnet::Id
Default: 'subnet-xxxxxxxxxxxx'
Subnet1c:
Type: AWS::EC2::Subnet::Id
Default: 'subnet- xxxxxxxxxxxx '
LBSecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
Default: 'sg- xxxxxxxxxxxx'
S3LogBucketName:
Type: String
Default: xxxxxxxxxxxx
TaskExecutionRoleArn:
Type: String
Default: 'arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole'
FamilyName:
Type: String
Default: xxxxxxxxxxx1
ECSCluster:
Type: String
Default: 'arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/xxxxxxxxxxx-1'
# ------------------------------------------------------------#
# Transform
# ------------------------------------------------------------#
Transform:
- 'AWS::CodeDeployBlueGreen'
Hooks:
CodeDeployBlueGreenHook:
Properties:
TrafficRoutingConfig:
Type: TimeBasedLinear
TimeBasedLinear:
StepPercentage: 30
BakeTimeMins: 1
AdditionalOptions:
TerminationWaitTimeInMinutes: 5
Applications:
- Target:
Type: 'AWS::ECS::Service'
LogicalID: ECSService
ECSAttributes:
TaskDefinitions:
- BlueTaskDefinition
- GreenTaskDefinition
TaskSets:
- BlueTaskSet
- GreenTaskSet
TrafficRouting:
ProdTrafficRoute:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
LogicalID: listenerProdTraffic
# TestTrafficRoute:
# Type: 'AWS::ElasticLoadBalancingV2::Listener'
# LogicalID: listenerTestTraffic
TargetGroups:
- lbTargetGroupBlue
- lbTargetGroupGreen
Type: 'AWS::CodeDeploy::BlueGreen'
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
lbTargetGroupBlue:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: '80'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
Matcher:
HttpCode: '200'
Port: 80
Protocol: HTTP
TargetType: ip
UnhealthyThresholdCount: 5
VpcId: !Ref Vpc
lbTargetGroupGreen:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: '80'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
Matcher:
HttpCode: '200'
Port: 80
Protocol: HTTP
TargetType: ip
UnhealthyThresholdCount: 5
VpcId: !Ref Vpc
loadBalancer:
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
Properties:
Name: !Sub ${NameTagPrefix}-${Env}-${App}-alb
LoadBalancerAttributes:
- Key: deletion_protection.enabled
Value: 'false'
- Key: access_logs.s3.enabled
Value: 'true'
- Key: routing.http2.enabled
Value: 'true'
- Key: routing.http.drop_invalid_header_fields.enabled
Value: 'false'
- Key: idle_timeout.timeout_seconds
Value: '60'
- Key: access_logs.s3.bucket
Value: !Ref S3LogBucketName
- Key: access_logs.s3.prefix
Value: 'prod'
Scheme: internet-facing
SecurityGroups:
- !Ref LBSecurityGroup
Subnets:
- !Ref Subnet1a
- !Ref Subnet1c
Type: application
listenerProdTraffic:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref lbTargetGroupBlue
LoadBalancerArn: !Ref loadBalancer
Port: 80
Protocol: HTTP
listenerTestTraffic:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref lbTargetGroupGreen
LoadBalancerArn: !Ref loadBalancer
Port: 8080
Protocol: HTTP
BlueTaskDefinition:
Type: 'AWS::ECS::TaskDefinition'
Properties:
ExecutionRoleArn: !Ref TaskExecutionRoleArn
ContainerDefinitions:
- Name: !Ref ContainerName
Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/xxx-ubuntu18:latest
# Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/xxx-nginx:latest
Essential: true
PortMappings:
- HostPort: 80
Protocol: tcp
ContainerPort: 80
RequiresCompatibilities:
- FARGATE
NetworkMode: awsvpc
Cpu: '256'
Memory: '512'
Family: !Ref FamilyName
ECSService:
Type: 'AWS::ECS::Service'
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
DeploymentController:
Type: EXTERNAL
# Type: CODE_DEPLOY
BlueTaskSet:
Type: 'AWS::ECS::TaskSet'
Properties:
Cluster: !Ref ECSCluster
LaunchType: FARGATE
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref LBSecurityGroup
Subnets:
- !Ref Subnet1a
- !Ref Subnet1c
PlatformVersion: LATEST
Scale:
Unit: PERCENT
Value: 1
Service: !Ref ECSService
TaskDefinition: !Ref BlueTaskDefinition
LoadBalancers:
- ContainerName: !Ref ContainerName
ContainerPort: 80
TargetGroupArn: !Ref lbTargetGroupBlue
PrimaryTaskSet:
Type: 'AWS::ECS::PrimaryTaskSet'
Properties:
Cluster: !Ref ECSCluster
Service: !Ref ECSService
TaskSetId: !GetAtt
- BlueTaskSet
- Id
このテンプレートには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。また、テンプレートにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPANDの指定も必要です。
CloudFormationスタックを作成するコマンドは下記になる。
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode - create-stack..."
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-deploy
# テンプレートの実⾏
aws cloudformation create-stack --stack-name ${CFN_STACK_NAME} --template-body file://./${CFN_TEMPLATE} ${CHANGES
ET_OPTION} \
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
./dly.sh deploy xxxx-hook-deploy.yml
スタックが作成されたら、ECSのサービス及びタスクを調べて、作成されたコンテナまたはALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。
##3.開発系(グリーン)に切り替え
TaskDefinition に変更が加えられたらそれがフックされ、デプロイが走るので、開発系のコンテナに切り替えます。
TaskDefinitionを変更する。
BlueTaskDefinition:
Type: 'AWS::ECS::TaskDefinition'
Properties:
ExecutionRoleArn: !Ref TaskExecutionRoleArn
ContainerDefinitions:
- Name: !Ref ContainerName
# Image: xxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/xxx-ubuntu18:latest
Image: xxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/xxx-nginx:latest
Essential: true
PortMappings:
- HostPort: 80
Protocol: tcp
ContainerPort: 80
RequiresCompatibilities:
- FARGATE
NetworkMode: awsvpc
Cpu: '256'
Memory: '512'
Family: !Ref FamilyName
変更スタックを作成するコマンドは下記になる。
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode - create-stack..."
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-deploy
# テンプレートの実⾏
aws cloudformation update-stack --stack-name ${CFN_STACK_NAME} --template-body file://./${CFN_TEMPLATE} ${CHANGES
ET_OPTION} \
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
./dly-update.sh deploy xxxx-hook-deploy.yml
実行すると、CloudFormation のマクロが変更セットを作成して、CodeDeploy や グリーン系への切り替えなどを始めます。
##4.開発系(グリーン)へのアクセス
開発系のコンテナに切り替え後、再び。ALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。
##5.ブルー系の停止
5分間のCodeDeploy TerminationWaitTimeInMinutes待ってから、ブルー系コンテナが停止され、コンテナにアクセスすると返事がありません。
まとめ
今回、CodeDeploy::BlueGreenフックを使って、簡単なWebアプリ今回を経由してCloudFormationでECSのBlue/Greenデプロイを構築検証した。AWS公式ユーザーガイドとおり、ECSのBule/Green CodeDeployが動くことを確認できました。この検証では、最初に説明したようにECSサービスやALB、タスク、コンテナに等は、事前に作成しておいたり、他のスタックで作成したものを使うことはできない、これらのすべてリソースは、Transformと同じテンプレート内で定義する必要があるため、実際複雑な商用システムでは、柔軟性は十分ではないかと思います。