#概要
AWS ECS(Fargate)のサービスをBlue/Greenにデプロイして、Code Pipelineからコンテナイメージやアプリを変更させ、CodeBuildでdocker buildし、CodeDeployにてBlueからGreenに切り替えさせる手順を紹介する。
AWS ECS(Fargate)のサービスのFargate Blue/Greenのデプロイは、プレースホルダを利用する方法とCodeDeploy::BlueGreen フックを利用する方法2パータンがありますが、今回はCodeDeploy::BlueGreen フックをCodePipelineにより実現する方法を検証する。
CodeDeploy::BlueGreen フックを利用して実現するやり方は、下記の記事をご参照ください。
プレースホルダを利用してECS(Fargate)サービスのBlue/Green CICDを実現する方は、下記の記事をご参照ください
やり方として、Transform というAWSのマクロ機能を利用し、CodeDeploy::BlueGreen フックにECSタスク、ECSサービス、ECSタスクセットを定義し、これらのリソースの更新は、CodePipelineに渡してBlue/Greenデプロイを実現する。
この考えで、二つのYmlファイルを作った。一つはCodeDeploy::BlueGreen フックとECSタスク、ECSサービス、ECSタスクセットを定義するhook-deploy.ymlファイル、もひとつはPipelineを定義するhook-pipeline.ymlである。
#環境用意
以下のリソースは事前に用意して置く。
S3パケット
ECRリポジトリ
VPC
Subnet1a
Subnet1c
SecurityGroup
Dockerfile
#実施プロシージャ
1. CodeDeploy::BlueGreen フックとECSリソースを作成
CodeDeploy::BlueGreen フックを作成にはAWSのTransformを使う。Transform含むテンプレートはaws cloudformation create-stackコマンドでは実行できないのでご注意ください。その代わり、aws cloudformation packageコマンドでパッケージを作る。また、作れたパッケージはS3のバケットにインクルードするファイルをアップロードする必要があるので、S3パケットの事前用意も必要である。
aws cloudformation packageコマンドで作成されたCloudFormationテンプレートファイルを基に、CloudFormationのスタックを作成するには、aws cloudformation create-stackコマンドもdeployコマンドもできます。
!Refなどの短縮形の組み込み関数の利用やテンプレートファイルにTransformを入れる位置によるLocationファイル名を変換される場合のテンプレートを実行にはdeployで行う必要があるようで、本文ではaws cloudformation create-stackコマンドによる無事実行できた。
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
AwsTokyoRegion:
Type: String
Default: xxxxxxxx
Service:
Type: String
Default: xxxx
Env:
Type: String
Default: dev
App:
Type: String
Default: hello
ECRName:
Type: String
Default: ubuntu18
ContainerName:
Type: String
Default: ununtu18-container1
CodeCommitRepositoryName:
Type: String
Default: codecommit-ubuntu18
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-xxxxxxxxxxxxxx'
Subnet1a:
Type: AWS::EC2::Subnet::Id
Default: 'subnet-xxxxxxxxxxxxxx'
Subnet1c:
Type: AWS::EC2::Subnet::Id
Default: 'subnet-xxxxxxxxxxxxxx'
LBSecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
Default: 'sg-xxxxxxxxxxxxxx'
S3LogBucketName:
Type: String
Default: xxx-xxxx-dev-hook-s3-xxxxx
TaskExecutionRoleArn:
Type: String
Default: 'arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole'
FamilyName:
Type: String
Default: fargate-cluster-task1
ECSCluster:
Type: String
Default: 'arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/fargate-cluster-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/ubuntu18:latest
# Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/docker-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
2. フックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成
CodeDeploy::BlueGreen フックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成すうには--output-template-fileで指定したファイルに変換後の内容が出力される(今回は出力後のテンプレートファイルをcf-deploy-template.ymlとする。)
#!/bin/bash
aws cloudformation package --template-file ./hook-deploy.yml --s3-bucket xxx-xxxx-dev-hook-s3-xxxx --output-template-file cf-deploy-template.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
Parameters:
AwsTokyoRegion:
Type: String
Default: xxxxxxxx
Service:
Type: String
Default: xxxx
Env:
Type: String
Default: dev
App:
Type: String
Default: hello
ECRName:
Type: String
Default: ubuntu18
ContainerName:
Type: String
Default: ununtu18-container1
CodeCommitRepositoryName:
Type: String
Default: codecommit-ubuntu18
CodeDeployAppName:
Type: String
Default: AppECS-fargate-cluster-1-fargate-cluster-service1
CodeDeployGrpName:
Type: String
Default: DgpECS-fargate-cluster-1-fargate-cluster-service1
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-xxxxxxxxxxxx
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: xxx-xxxx-dev-hook-s3-xxxxxx
TaskExecutionRoleArn:
Type: String
Default: arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole
FamilyName:
Type: String
Default: fargate-cluster-task1
ECSCluster:
Type: String
Default: arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/fargate-cluster-1
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
TargetGroups:
- lbTargetGroupBlue
- lbTargetGroupGreen
Type: AWS::CodeDeploy::BlueGreen
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:
Fn::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/ubuntu18: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
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:
Fn::GetAtt:
- BlueTaskSet
- Id
3. ECSタスク定義などのDeployスタックを作成
作成されたcf-tするにフックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成する。
このテンプレートファイルには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。さらにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPANDの指定も必要である。
#!/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} ${CHANGESET_OPTION} \
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
./dly.sh deploy cf-deploy-template.yml
4. CodePipelineスタックを作成
以下のYmlファイルとaws cliによるCloudFormationのCodePipelineを作成する。
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with CodeDeploy Hook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
AwsTokyoRegion:
Type: String
Default: xxxxxxxx
Service:
Type: String
Default: xxxx
Env:
Type: String
Default: dev
App:
Type: String
Default: hello
ECRName:
Type: String
Default: ubuntu18
TemplateName:
Type: String
Default: cf-template
ContainerName:
Type: String
Default: ununtu18-container1
CodeCommitRepositoryName:
Type: String
Default: codecommit-ubuntu18
ServiceName:
Type: String
Default: xxxx
Description: Prefix of Service tags.
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
# CodeWatchEventを実行できるIAMRole
CloudwatchEventRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub xxx-cwe-${Service}-${Env}-hook-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 xxx-codebuild-${Service}-${Env}-hook-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: '*'
# CloudFormationに適用するIAMRole
CFnServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub xxx-cfn-${Service}-${Env}-hook-role
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: SampleCloudFormationPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- iam:PassRole
Resource: '*'
Effect: Allow
Condition:
StringEqualsIfExists:
iam:PassedToService:
- ecs-tasks.amazonaws.com
- Action:
- codedeploy:CreateDeployment
- codedeploy:GetApplication
- codedeploy:GetApplicationRevision
- codedeploy:GetDeployment
- codedeploy:GetDeploymentConfig
- codedeploy:RegisterApplicationRevision
- codedeploy:*
Resource: '*'
Effect: Allow
- Action:
- ec2:*
- elasticloadbalancing:*
- autoscaling:*
- cloudwatch:*
- sns:*
- cloudformation:*
- ecs:*
Resource: '*'
Effect: Allow
# CodePipelineに適用するIAMRole
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub xxx-codepipeline-${Service}-${Env}-hook-role
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: xxx-CodePipelineHookPolicy
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
- Resource: '*'
Effect: Allow
Action:
- ecr:DescribeImages
- 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 xxx-codedeploy-${Service}-${Env}-hook-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 Updating CloudFormation Template...
- sed -i -e "s|$REPOSITORY_URI:.*$|$REPOSITORY_URI:$IMAGE_TAG|" $TEMPLATE_NAME.yml
artifacts:
files: $TEMPLATE_NAME.yml
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: TEMPLATE_NAME
Value: !Ref TemplateName
- Name: DOCKER_BUILDKIT
Value: '1'
Name: !Ref AWS::StackName
# ------------------------------------------------------------#
# CodePipeline
# ------------------------------------------------------------#
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
RoleArn: !GetAtt CodePipelineServiceRole.Arn
Name: !Sub xxx-${ServiceName}-hook-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: Approval
# Actions:
# - Name: Manual_Approval
# ActionTypeId:
# Category: Approval
# Owner: AWS
# Version: '1'
# Provider: Manual
# Configuration:
# CustomData: !Sub '${ServiceName} will be updated. Do you want to deploy it?'
# NotificationArn: arn:aws:sns:ap-xxxxxxxx:xxxxxxxx:hogehoge
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Version: '1'
Provider: CloudFormation
InputArtifacts:
# - Name: App
- Name: BuildOutput
Configuration:
ActionMode: CREATE_UPDATE
Capabilities: CAPABILITY_AUTO_EXPAND
RoleArn: !GetAtt CFnServiceRole.Arn
# StackName: hook-pipeline
StackName: hook-deploy
TemplatePath: !Sub 'BuildOutput::${TemplateName}.yml'
RunOrder: 1
Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
PipelineDev:
Description: Dev xxxx Pipeline
Value: !Ref Pipeline
Export:
Name: hook-DevxxxxAppPipeline
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-pipeline![hook-pipeline-hello-blue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/2916287f-ae7b-b582-9626-57ff64a3264b.jpeg)
# テンプレートの実⾏
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION} \
--capabilities CAPABILITY_NAMED_IAM --parameter-overrides `cat parameters.hook`
CloudFormationスタックを作成するコマンドは下記になる。
./pip.sh deploy hook-pipeline.yml
このテンプレートには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。また、テンプレートにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPANDの指定も必要です
#結果確認
- 現用系(ブルー)アプリへのアクセス
スタックが作成されたら、ECSのサービス及びタスクを調べて、作成されたコンテナまたはALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。
2.開発系(グリーン)アプリを作成(タスク変更)
TaskDefinition に変更が加えられたらそれがフックされ、デプロイが走るので、開発系のコンテナに切り替えます。
CodeCommitレポジトリのDockerfile中身を変更する。
RUN echo 'Hello World - BLUE!' > /var/www/html/index.htmlから
RUN echo 'Hello World - GREEN!' > /var/www/html/index.htmlへ変更。
3.CodeDeploymentの実施と開発系(グリーン)に切り替え
実行すると、CloudFormation のマクロが変更セットを作成して、CodeDeploy や グリーン系への切り替えなどを始めます。
4.開発系(グリーン)へのアクセス
開発系のコンテナに切り替え後、再び。ALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。
5.(オプション)update-stackによる開発系(グリーン)への切り替え
“2.開発系(グリーン)アプリを作成(タスク変更)”の他、deployスタックのアップデートによる開発系アプリの作成及びグリーン系への切り替えもできる。
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode - update-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} ${CHANGESET_OPTION} \
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
#所感
今回、CloudFormationの CodePipelineとCodeDeploy::BlueGreenフックをインテグレートにより、ECS FargateのBlue/Greenサービスデプロイを構築検証した。テスク定義の変更によるECSのBule/Green CodeDeployが動くことを確認できた。ただ、CodeDeploy::BlueGreenフックリソース定義、CloudFormationテンプレートファイルの作成及びCodePipelineの作成はちょっと手間をかかり、実際複雑な商用システムでは、CodeDeploy::BlueGreenフックとCodePipelineの連携する方法は柔軟性に十分ではないかと思います。