GitHub Actionsを使用したECSへのBlue/Greenデプロイを試してみた
前回のブログでGitHub Actionsを使用したローリングアップデートを試しました。
今回はBlue/Greenデプロイを試してみたのでブログに残しておこうと思います。
やること
前回同様GitHub Actionsでamazon-ecs-deploy-task-definitionを使用します。
前回との差分としてBlue/Greenデプロイを行うためにCodeDeployを使用します。
作成する構成としては以下のようになります。
設定
設定内容は前回のブログと殆ど同じなのでデプロイ方法などは割愛します。
GitHubとAWSの連携
前回との差分としてGitHub Actionsが使用するIAMロールのIAMポリシーが少し異なります。
CodeDeployを実行するために"GetDeployment"、"GetDeploymentConfig"、"RegisterApplicationRevision"を許可しています。
AWSTemplateFormatVersion: '2010-09-09'
Description: OIDC settings for Github Actions.
Parameters:
EnvName:
Type: String
Default: dev
AllowedValues:
- dev
Description: Environment name
OrgID:
Type: String
Description: Github Organazation ID
RepoName:
Type: String
Default: RepositoryName
Description: Repository name
Resources:
OIDCProvider:
Type: AWS::IAM::OIDCProvider
Properties:
ClientIdList:
- 'sts.amazonaws.com'
Url: https://token.actions.githubusercontent.com
GithubActionsPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- 'ecr:UploadLayerPart'
- 'ecr:PutImage'
- 'ecr:InitiateLayerUpload'
- 'ecr:CompleteLayerUpload'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetAuthorizationToken'
- 'iam:PassRole'
- 'ecs:DescribeServices'
- 'ecs:RegisterTaskDefinition'
- 'ecs:DescribeTaskDefinition'
- 'codedeploy:GetDeployment'
- 'codedeploy:GetDeploymentGroup'
- 'codedeploy:GetDeploymentConfig'
- 'codedeploy:RegisterApplicationRevision'
- 'codedeploy:CreateDeployment'
Resource: '*'
ManagedPolicyName: !Sub policy-${EnvName}-github-oidc-${RepoName}-001
OIDCProviderRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub 'role-${EnvName}-github-oidc-${RepoName}-001'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com'
Action: 'sts:AssumeRoleWithWebIdentity'
Condition:
StringLike:
'token.actions.githubusercontent.com:sub':
- !Sub 'repo:${OrgID}/${RepoName}:*'
ManagedPolicyArns:
- !Ref GithubActionsPolicy
ECSサービスなどの作成
ECRと初期デプロイ時に使用するコンテナイメージの作成は前回のブログを参照してください。
ECSクラスターなどの作成は以下のCloudFormationテンプレートで行います。
前回との差分としてCodeDeployとBlue/Greenで使用するALBのリスナールールなどが追加されています。
AWSTemplateFormatVersion: "2010-09-09"
Description: ECS
Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Parameters for env Name
Parameters:
- env
- Label:
default: Parameters for Network
Parameters:
- VPCCIDR
- PublicSubnet01CIDR
- PublicSubnet02CIDR
- PrivateSubnet01CIDR
- PrivateSubnet02CIDR
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
env:
Type: String
Default: dev
AllowedValues:
- dev
VPCCIDR:
Default: 192.168.0.0/16
Type: String
PublicSubnet01CIDR:
Default: 192.168.0.0/24
Type: String
PublicSubnet02CIDR:
Default: 192.168.1.0/24
Type: String
PrivateSubnet01CIDR:
Default: 192.168.2.0/24
Type: String
PrivateSubnet02CIDR:
Default: 192.168.3.0/24
Type: String
Resources:
# ------------------------------------------------------------#
# CloudWatch Logs
# ------------------------------------------------------------#
ECSLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/ecs-${env}-log"
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------#
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
RoleName: !Sub iam-${env}-ecs-tast-execution-role
CodeDeployRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- codedeploy.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
RoleName: !Sub iam-${env}-codedeploy-ecs-role
CodeDeployIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- codedeploy.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
RoleName: !Sub iam-${env}-codedeploy-role
Tags:
- Key: Name
Value: !Sub iam-${env}-codedeploy-role
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub vpc-${env}
# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------#
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub igw-${env}
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------#
PublicSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PublicSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub1
VpcId: !Ref VPC
PublicSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PublicSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub2
VpcId: !Ref VPC
PrivateSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PrivateSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv1
VpcId: !Ref VPC
PrivateSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PrivateSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv2
VpcId: !Ref VPC
# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------#
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-pub
PublicRouteTableRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet01
PublicRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet02
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-prv
PrivateRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet01
PrivateRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet02
# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------#
ALBSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for alb
GroupName: !Sub securitygroup-${env}-alb
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 80
- FromPort: 8080
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 8080
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-alb
VpcId: !Ref VPC
ECSSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for ecs
GroupName: !Sub securitygroup-${env}-ecs
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
SourceSecurityGroupId: !Ref ALBSG
ToPort: 80
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-ecs
VpcId: !Ref VPC
VPCEndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for vpc endpoint
GroupName: !Sub securitygroup-${env}-vpc-endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 443
IpProtocol: tcp
SourceSecurityGroupId: !Ref ECSSG
ToPort: 443
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-vpc-endpoint
VpcId: !Ref VPC
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
VpcId: !Ref VPC
ECRdkrEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
ECRapiEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
LogsEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
LoadBalancerAttributes:
- Key: deletion_protection.enabled
Value: false
Name: !Sub alb-${env}-ecs
Scheme: internet-facing
SecurityGroups:
- !Ref ALBSG
Subnets:
- !Ref PublicSubnet01
- !Ref PublicSubnet02
Tags:
- Key: Name
Value: !Sub alb-${env}-ecs
Type: application
TargetGroup1:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-01
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-01
TargetType: ip
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
TargetGroup2:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-02
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-02
TargetType: ip
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
ALBHTTPListener1:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup1
Type: forward
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
ALBHTTPListener2:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup2
Type: forward
LoadBalancerArn: !Ref ALB
Port: 8080
Protocol: HTTP
# ------------------------------------------------------------#
# ECS
# ------------------------------------------------------------#
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
CapacityProviders:
- FARGATE
ClusterName: !Sub ecs-${env}-cluster
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
ECSTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
- Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-${env}:latest
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref ECSLogGroup
awslogs-region: !Ref "AWS::Region"
awslogs-stream-prefix: !Sub ecs-${env}-log
Name: !Sub task-${env}
PortMappings:
- ContainerPort: 80
HostPort: 80
Cpu: 256
ExecutionRoleArn: !Ref TaskExecutionRole
Family: !Sub task-${env}
Memory: 512
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBHTTPListener1
- ALBHTTPListener2
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
LoadBalancers:
- ContainerName: !Sub task-${env}
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup1
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSG
Subnets:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
ServiceName: !Sub service-${env}
TaskDefinition: !Ref ECSTaskDef
PlatformVersion: LATEST
DeploymentController:
Type: CODE_DEPLOY
# ------------------------------------------------------------#
# CodeDeploy
# ------------------------------------------------------------#
CodeDeployApp:
Type: AWS::CodeDeploy::Application
DependsOn: ECSService
Properties:
ApplicationName: !Sub codedeploy-${env}-app
ComputePlatform: ECS
CodeDeployGroup:
Type: AWS::CodeDeploy::DeploymentGroup
Properties:
ApplicationName: !Ref CodeDeployApp
DeploymentGroupName: !Sub codedeploy-${env}-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: 5
DeploymentStyle:
DeploymentOption: WITH_TRAFFIC_CONTROL
DeploymentType: BLUE_GREEN
LoadBalancerInfo:
TargetGroupPairInfoList:
- ProdTrafficRoute:
ListenerArns:
- !Ref ALBHTTPListener1
TestTrafficRoute:
ListenerArns:
- !Ref ALBHTTPListener2
TargetGroups:
- Name: !GetAtt TargetGroup1.TargetGroupName
- Name: !GetAtt TargetGroup2.TargetGroupName
ServiceRoleArn: !GetAtt CodeDeployRole.Arn
ECSServices:
- ClusterName:
!Ref ECSCluster
ServiceName:
!GetAtt ECSService.Name
GitHub Actions設定
ECS周りのリソースが作成できたらGitHub ActionsのワークフローファイルとCodeDeployで使用するappspec.yamlの作成を行っていきます。
appspec.yamlは以下のものを使用します。
TaskDefinitionはGitHub Actions実行時に書き換えられるのでそのままで大丈夫です。
書き換えはindex.jsの399行目あたりで行われているようです。
findAppSpecValue関数でappspec.yaml内の'resources'を探して、さらに'properties'から'taskDefinition'を探して書き換えを行っています。
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "task-dev"
ContainerPort: 80
GitHub Actionsで使用するワークフローファイルは以下になります。
前回との差分としてはCodeDeployを使用するために"codedeploy-appspec"、"codedeploy-application"、"codedeploy-deployment-group"を追加しています。
こちらのパラメータはCodeDeployのアプリケーション、デプロイグループ、appspec.yamlファイル名を記載してください。
name: ECS deploy
on:
pull_request:
branches:
- main
types: [closed]
env:
AWS_ACCOUNT_ID: AWSアカウントID
TASK_DEF: "task-dev"
ECS_SERVICE: "service-dev"
ECS_CLUSTER: "ecs-dev-cluster"
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
defaults:
run:
working-directory: ./
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: "arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/role-dev-github-oidc-ecs-deploy-001"
aws-region: "ap-northeast-1"
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
id: login-ecr
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: "ecr-dev"
IMAGE_TAG: ${{ github.sha }}
run: |
docker build . --tag ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}
docker push ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}
echo "image=${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}" >> $GITHUB_OUTPUT
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.TASK_DEF }} --query taskDefinition > task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.TASK_DEF }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
codedeploy-appspec: appspec.yaml
codedeploy-application: codedeploy-dev-app
codedeploy-deployment-group: codedeploy-dev-group
ディレクトリ構成は以下のようにします。
.
├── .github
│ └── workflows
│ └── deploy.yaml
├── Dockerfile
├── appspec.yaml
└── html
└── index.html
動作確認
ファイルの作成などが完了したらGitHubリポジトリへプッシュ後、mainブランチへマージしてください。
マージ後、Actionsタブからデプロイが開始していることを確認できます。
また、CodeDeployのデプロイメントからデプロイが実行されていることが確認できます。
デプロイ中にALBのテストポートへアクセスすることで新しいWeb画面を確認することができます。
さいごに
GitHub ActionsでECSのCI/CDを試してみました。
CodePipelineを使用しない分、アーティファクト用のS3バケットやCodeBuildを作成しなくてもよいので設定がシンプルになります。
GitHub Actionsには慣れていてもAWSでのCI/CDに慣れていない方であれば今回の方法はAWS側の設定項目が少なくなるのでよいと思いました。