概要
本文はAWS ECS(Fargate)のサービスをBlue/Greenにデプロイして、Code Pipelineからコンテナイメージやアプリを変更させ、CodeBuildでdocker buildし、CodeDeployにてBlueからGreenに切り替えさせる手順を紹介する。
AWS Code pipelineのFargate Blue/Greenのデプロイは、プレースホルダを利用するとCodeDeploy::BlueGreen フックを利用する2方法があるが、今回はプレースホルダを利用して実施する。CodeDeploy::BlueGreen フックを利用して実現するやり方は、前回の記事をご参照ください。
両方法の主な違いは、プレースホルダではCodeDeployの機能により、バージョン管理ツール (CodeCommitレポジトリ) に格納された taskdef.json のイメージURI と appspec.ymlのタスク定義のプレースホルダのみを更新してECS Blue/Greenデプロイを実行する。それ以外部分の更新は、Blue/Greenデプロイの管理外となる。
これに対して、CodeDeploy::BlueGreen フック方法ではTransform というAWSのマクロ機能を利用しECS の Blue/Green の環境を作成する。フック全体の広範な変換 (Transform) まで、テンプレートに対してカスタム処理を実行できるようになる。プレースホルダとは異なり、イメージURI以外のタスク定義の更新やサービスの設定更新も、Blue/Greenデプロイの管理範囲となる。
マクロの詳細は、下記のAWS CloudFormation マクロを使用例 - AWS CloudFormation をご参照下さい。
環境用意
以下のリソースは事前に用意して置く。
ECRリポジトリ
VPC
Subnet1a
Subnet1c
SecurityGroup
Dockerfile
実施手順
- ELB(ロードバランサー)、リスナー、ターゲットグループを作成して置く。ELBのリスナーとたーべっとグループは下記のように設定する。
80/TCPのリスナー
80/TCPのリスナーで利用されるターゲットグループ、ターゲット80|
8080/TCPのリスナー
8080/TCPのリスナーで利用されるターゲットグループ、ターゲット同じく80 -
ECS環境を用意します。具体的は、ECSクラスタ、ECSサービス、ECSタスクを作成して置く。また、ECSサービスを作成する際、必ず“Blue/green deployment(powered by AWS CodeDeploy)”を選択する。
-
ECS サービスを作成する際、作成されたCodeDeploy applicationとdeployment group名をメモしておく。このapplicationとdeployment groupを次にCodePipeline定義する時に必要である。applicationとdeployment groupを作れなかった、若しくは既存のサービスに追加するタスクまたはその他の原因でapplicationとdeployment groupがない場合は、“JSONからapplicationとdeployment groupを作成する”章をご参照ください。
ECS タスク定義から、作成されたタスクのJSONタブの全て内容を、taskdef.jsonファイルとしてローカルに保存する。また、保存する前、行"image": "xxxxxxxxxxxx.dkr.ecr.xxxxxx.amazonaws.com/xxxxxx:latest",を"image": ""に変更する。
作成されたtaskdef.jsonファイルをCodecommitレジストリにアップロードする。
appspec.yml、buildspec.ymlファイルをCodecommitレジストリにアップロードする。appspec.yml のContainName、と buildspec.ymlファイルを実提供するサービスアプリにより変更してください。
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "<TASK_DEFINITION>"
LoadBalancerInfo:
ContainerName: "xxxxxxxx-xxxxxxxx1"
ContainerPort: "80"
version: 0.2
phases:
pre_build:
commands:
- aws ecr get-login-password --region xxxxxx | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.xxxxxx.amazonaws.com
- REPOSITORY_URI=xxxxxxxxxxxx.dkr.ecr.xxxxxx.amazonaws.com/xxxxxx
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
build:
commands:
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
- printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
files: imageDetail.json
検証方法
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
開発環境(グリーン)系:
Nginxコンテナにイメージファイル。コンテナが実行されたらWebBrowserに“Welcome to nginx!...”を表示される。
- Codepipelineスタックを作成する(pholder-pipeline.yml)
Fargate で Ununtu18 コンテナを動かして、ALB経由でHTTP 80番ポートでアクセスするの“HelloWorld!”メッセージを作成する。
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with PlaceHolder
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
AwsTokyoRegion:
Type: String
Default: xxxxxx
Service:
Type: String
Default: xxxx
Env:
Type: String
Default: dev
ECRName:
Type: String
Default: xxxxxxxx
ContainerName:
Type: String
Default: xxxxxxxxxxxx
CodeCommitRepositoryName:
Type: String
Default: xxxxxxxx
CodeDeployAppName:
Type: String
Default: AppECS-xxxxxx-xxxxxx-service1
CodeDeployGrpName:
Type: String
Default: DgpECS-xxxxxx-xxxxxx-service1
NameTagPrefix:
Type: String
Default: xxxx
Description: Prefix of Name tags.
ServiceName:
Type: String
Default: xxxx
Description: Prefix of Service tags.
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
# IAM Roles
# ------------------------------------------------------------#
# CodeWatchEventを実行できるIAMRole
CloudwatchEventRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub cwe-${Service}-${Env}-xxxx-role
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: CloudWatchEventsPipelineExecution
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: codepipeline:StartPipelineExecution
Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
# CodeBuildに適用するIAMRole
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub codebuild-${Service}-${Env}-xxxx-role
Path: /
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: SampleCodeBuildAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Resource: '*'
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Effect: Allow
Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
- Effect: Allow
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
Resource: '*'
- Effect: Allow
Action:
- ecr:GetAuthorizationToken
- ecr:BatchCheckLayerAvailability
- ecr:GetDownloadUrlForLayer
- ecr:GetRepositoryPolicy
- ecr:DescribeRepositories
- ecr:ListImages
- ecr:DescribeImages
- ecr:BatchGetImage
- ecr:InitiateLayerUpload
- ecr:UploadLayerPart
- ecr:CompleteLayerUpload
- ecr:PutImage
Resource: '*'
# CodePipelineに適用するIAMRole
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub codepipeline-${Service}-${Env}-xxxx-role
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: SamplePipeline
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:::${ArtifactBucket}/*
Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketVersioning
- Action:
- codecommit:CancelUploadArchive
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:GetRepository
- codecommit:GetUploadArchiveStatus
- codecommit:UploadArchive
Resource: '*'
Effect: Allow
- 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
# CodeDeployに適用するIAMRole
CodeDeployRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: 'Allow'
Principal:
Service:
- 'codedeploy.amazonaws.com'
Action:
- 'sts:AssumeRole'
Path: '/'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
RoleName: !Sub codedeploy-${Service}-${Env}-xxxx-role
# S3Bucket
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
# CloudWatchEventの実行ルール
AmazonCloudWatchEventRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.codecommit
detail-type:
- CodeCommit Repository State Change
resources:
- Fn::Join:
- ''
- - 'arn:aws:codecommit:'
- !Ref 'AWS::Region'
- ':'
- !Ref 'AWS::AccountId'
- ':'
- !Ref CodeCommitRepositoryName
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- master
Targets:
- Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
RoleArn: !GetAtt CloudwatchEventRole.Arn
Id: codepipeline-AppPipeline
# CodeBuild
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
ServiceRole: !Ref CodeBuildServiceRole
Artifacts:
Type: CODEPIPELINE
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- $(aws ecr get-login --no-include-email)
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
- docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing imageDetail json...
- echo "{\"name\":\"${ContainerName}\",\"ImageURI\":\"${REPOSITORY_URI}:${IMAGE_TAG}\"}" > imageDetail.json
artifacts:
files: imageDetail.json
Environment:
PrivilegedMode: true
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:4.0
Type: LINUX_CONTAINER
EnvironmentVariables:
- Name: AWS_DEFAULT_REGION
Value: !Ref AWS::Region
- Name: REPOSITORY_URI
Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
- Name: ContainerName
Value: !Ref ContainerName
- Name: DOCKER_BUILDKIT
Value: '1'
Name: !Sub ${Service}-${Env}-xxxx-project1
# Name: !Ref AWS::StackName
# ------------------------------------------------------------#
# CodePipeline
# ------------------------------------------------------------#
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
RoleArn: !GetAtt CodePipelineServiceRole.Arn
Name: !Sub ${ServiceName}-xxxx-pipeline1
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Version: '1'
Provider: CodeCommit
Configuration:
RepositoryName: !Ref CodeCommitRepositoryName
PollForSourceChanges: false
BranchName: master
RunOrder: 1
OutputArtifacts:
- Name: App
- Name: Build
Actions:
- Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Version: '1'
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: 1
InputArtifacts:
- Name: App
OutputArtifacts:
- Name: BuildOutput
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Version: '1'
Provider: CodeDeployToECS
Configuration:
AppSpecTemplateArtifact: App
AppSpecTemplatePath: appspec.yml
TaskDefinitionTemplateArtifact: App
TaskDefinitionTemplatePath: taskdef.json
ApplicationName: !Ref CodeDeployAppName
DeploymentGroupName: !Ref CodeDeployGrpName
Image1ArtifactName: BuildOutput
Image1ContainerName: IMAGE1_NAME
RunOrder: 1
InputArtifacts:
- Name: App
- Name: BuildOutput
Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
PipelineDev:
Description: Dev xxxx Pipeline
Value: !Ref Pipeline
Export:
Name: xxxxPipeline
このテンプレートには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。
CloudFormationスタックを作成するコマンドは下記になる(phld.sh)。
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
CFN_TEMPLATE=$2
CFN_STACK_NAME=xxxx-pipeline
# テンプレートの実⾏
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION} \
--capabilities CAPABILITY_NAMED_IAM --parameter-overrides `cat parameters.phld`
./phld.sh deploy pholder-pipeline.yml
スタックが作成されたら、ECSのサービス及びタスクを調べて、作成されたコンテナまたはALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。
2. 開発系(グリーン)に切り替え
Dockfileerファイルを変更して、グリーン系のNginxコンテナを作り直す。CloudWatchでDocker変更を検知しデプロイが走る。その後、開発系のコンテナに切り替えます。
コンテナが実行されたらWebBrowserに“Welcome to nginx!...”を表示される。
CodeDeploy の グリーン系への切り替え過程は以下になる。
3.開発系(グリーン)へのアクセス
開発系のコンテナに切り替え後、再び。ALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。切り替え途中でもALBへのアクセスは一定の確率(例90%)で開発系にルートされたことも確認できる。
4.5分間のCodeDeploy TerminationWaitTimeInMinutes待ってから、ブルー系コンテナが停止され、コンテナにアクセスすると返事がありません。
(オプション)JSONからapplicationとdeployment groupを作成する
若しECS サービスが既に作成されて、CodeDeploy用のapplicationとdeployment groupが分からない場合は、aws cliからapplicationとdeployment groupを作れる。また、現在のCloudFormation(1.4.0バージョン)では、CodeDeployのapplicationも作れるが、deployment groupは作れません。
ECS Blue/Greenデプロイ用のアプリケーションを作成する。
$ aws deploy create-application \
--application-name AppECS-xxxxxx-service1 \
--compute-platform ECS
若しくは、CloudFormationからDeploy applicationを作る。
# Create Code deploy application
CodeDeployApplication:
Type: AWS::CodeDeploy::Application
Properties:
ApplicationName: !Ref CodeDeployAppName
ComputePlatform: ECS
上記で作ったアプリケーションに紐付けるデプロイグループを作成する。JSONフォーマットの定義ファイルから作成する。
定義ファイルdeploy-grp-create.json:
{
"applicationName": "xxxxxxxx-app1",
"deploymentGroupName": "xxxxxxxx-grp1",
"deploymentConfigName": "CodeDeployDefault.ECSAllAtOnce",
"serviceRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/xxxxxxxx-xxxxxxxx-role",
"alarmConfiguration": {
"enabled": false,
"ignorePollAlarmFailure": false,
"alarms": []
},
"autoRollbackConfiguration": {
"enabled": true,
"events": [
"DEPLOYMENT_STOP_ON_ALARM",
"DEPLOYMENT_FAILURE",
"DEPLOYMENT_STOP_ON_REQUEST"
]
},
"deploymentStyle": {
"deploymentType": "BLUE_GREEN",
"deploymentOption": "WITH_TRAFFIC_CONTROL"
},
"blueGreenDeploymentConfiguration": {
"terminateBlueInstancesOnDeploymentSuccess": {
"action": "TERMINATE",
"terminationWaitTimeInMinutes": 5
},
"deploymentReadyOption": {
"actionOnTimeout": "CONTINUE_DEPLOYMENT",
"waitTimeInMinutes": 0
}
},
"loadBalancerInfo": {
"targetGroupPairInfoList": [
{
"targetGroups": [
{
"name": "xxx-xxx-tg"
},
{
"name": "xxx-xxx-tg2"
}
],
"prodTrafficRoute": {
"listenerArns": [
"arn:aws:elasticloadbalancing:xxxxxx:xxxxxxxxxxxx:listener/net/xxx-xxx-nlb/0b7c7c53ede509d4/0a8e14e46fa40eae"
]
},
"testTrafficRoute": {
"listenerArns": [
"arn:aws:elasticloadbalancing:xxxxxx:xxxxxxxxxxxx:listener/net/xxx-xxx-nlb/0b7c7c53ede509d4/9ebf467664c0f617"
]
}
}
]
},
"ecsServices": [
{
"serviceName": "xxx-xxx-service",
"clusterName": "xxx-xxx-fargate"
}
],
"tags": [
{
"Key": "tag-key",
"Value": "tag-value"
}
]
}
--cli-input-json をオプション利用して定義ファイルを読み込み、デプロイグループを作成する。
$ create-grp.sh
#!/bin/bash
aws deploy create-deployment-group \
--application-name AppECS-xxxxxx \
--deployment-group-name DrpECS-xxxxxx \
--cli-input-json file://deploy-grp-create.json
所感
今回、CodeDeployのプレースホルダ方法を使って、簡単なWebアプリを作るによりCloudFormationのECS Blue/Greenデプロイを構築と検証した。ECS FargateのBule/Green CodeDeployが動くことを確認できました。CodeDeploy::BlueGreenフックの実現方法により、CodeDeployのプレースホルダ方法は、事前にECSサービスやALB、タスク、コンテナ等を作成しており、又は既存のECSサービスとの連携が可能で、実際の複雑な商用システムでは対応できると思います。