Amazon ECSでBlue/Green Deploymentを使用しTarget group切り替えの挙動を確認する
Amazon ECSは先日のリリースでBlue/Green Deploymentをネイティブサポートしました。私はこれまで恥ずかしながらECSのBlue/Green deploymentをちゃんと使ったことがなく、 Blue/Green環境を切り替えるためにTarget group/Listenerは修正しなくてもよいのか わかっておりませんでした。
結論として、 Blue/Green環境の切り替え時にTarget group/Listenerを修正する必要はなく、AWS側で自動的に切り替えてくれる ことを確認しました。
せっかくなので今回少し検証してみたので、こちらに供養しておきます。
検証方法
今回はAmazon ECSネイティブなBlue/Green deploymentを使えるようにリソースを用意し、AWSマネジメントコンソールとAWS CloudFormationの2つから、コンテナイメージの変更を複数回実施し、ECSがどんな挙動になるかを確認しました。
今回は以下のCloudFormationファイルで環境を用意しました。なおここではベイク時間を指定しておらず、作成後に確認すると 15分
と設定されていました。
コンテナのアップデートは nginx
の既存のイメージを使用しました。
AWSTemplateFormatVersion: "2010-09-09"
Description: ECS and ALB resources for ECS Native Blue/Green deployment
Parameters:
PJPrefix:
Type: String
Default: "ecs-test"
ALBSecurityGroupIngressIPAddress:
Type: String
InternetALBName:
Type: String
Default: "alb"
TargetGroupBlueName:
Type: String
Default: "tg-blue"
TargetGroupGreenName:
Type: String
Default: "tg-green"
ECSClusterName:
Type: String
Default: "cluster"
ECSTaskName:
Type: String
Default: "task"
ECSTaskCPUUnit:
AllowedValues: [ 256, 512, 1024, 2048, 4096 ]
Type: String
Default: "256"
ECSTaskMemory:
AllowedValues: [ 256, 512, 1024, 2048, 4096 ]
Type: String
Default: "512"
ECSContainerName:
Type: String
Default: "container"
ECSImageName:
Type: String
Default: "nginx:stable"
ECSServiceName:
Type: String
Default: "service"
ECSTaskDesiredCount:
Type: Number
Default: 1
ALBPortHTTP:
Type: Number
Default: 80
ALBPortTest:
Type: Number
Default: 8080
Resources:
#Security Group
ALBSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
GroupName: !Sub "${PJPrefix}-alb-sg"
GroupDescription: "-"
Tags:
- Key: "Name"
Value: !Sub "${PJPrefix}-alb-sg"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref ALBPortHTTP
ToPort: !Ref ALBPortHTTP
CidrIp: !Ref ALBSecurityGroupIngressIPAddress
- IpProtocol: tcp
FromPort: !Ref ALBPortTest
ToPort: !Ref ALBPortTest
CidrIp: !Ref ALBSecurityGroupIngressIPAddress
ECSSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
GroupName: !Sub "${PJPrefix}-ecs-sg"
GroupDescription: "-"
Tags:
- Key: "Name"
Value: !Sub "${PJPrefix}-ecs-sg"
# Security Group Rule
ECSSecurityGroupIngress:
Type: "AWS::EC2::SecurityGroupIngress"
Properties:
IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !GetAtt [ ALBSecurityGroup, GroupId ]
GroupId: !GetAtt [ ECSSecurityGroup, GroupId ]
# Target Group
TargetGroupBlue:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
Name: !Sub "${PJPrefix}-${TargetGroupBlueName}"
Protocol: HTTP
Port: 80
TargetType: ip
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
TargetGroupGreen:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
Name: !Sub "${PJPrefix}-${TargetGroupGreenName}"
Protocol: HTTP
Port: 80
TargetType: ip
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
# ALB
InternetALB:
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
Properties:
Name: !Sub "${PJPrefix}-${InternetALBName}"
Tags:
- Key: Name
Value: !Sub "${PJPrefix}-${InternetALBName}"
Scheme: "internet-facing"
LoadBalancerAttributes:
- Key: "deletion_protection.enabled"
Value: false
- Key: "idle_timeout.timeout_seconds"
Value: 60
SecurityGroups:
- !Ref ALBSecurityGroup
Subnets:
- { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" }
- { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-c" }
ALBListenerProd:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroupBlue
Type: forward
LoadBalancerArn: !Ref InternetALB
Port: !Ref ALBPortHTTP
Protocol: HTTP
ALBListenerTest:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroupGreen
Type: forward
LoadBalancerArn: !Ref InternetALB
Port: !Ref ALBPortTest
Protocol: HTTP
ALBProdListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref TargetGroupBlue
Conditions:
- Field: path-pattern
Values: ["/*"]
ListenerArn: !Ref ALBListenerProd
Priority: 1
ALBTestListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref TargetGroupGreen
Conditions:
- Field: path-pattern
Values: ["/*"]
ListenerArn: !Ref ALBListenerTest
Priority: 1
# ECS Cluster
ECSCluster:
Type: "AWS::ECS::Cluster"
Properties:
ClusterName: !Sub "${PJPrefix}-${ECSClusterName}"
# Loggroup
ECSLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/${PJPrefix}-ecs-group"
RetentionInDays: 7
# ECS Task Definition
ECSTaskDefinition:
Type: "AWS::ECS::TaskDefinition"
Properties:
Cpu: !Ref ECSTaskCPUUnit
ExecutionRoleArn: !Ref ECSTaskExecutionRole
Family: !Sub "${PJPrefix}-${ECSTaskName}"
Memory: !Ref ECSTaskMemory
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: !Sub "${PJPrefix}-${ECSContainerName}"
Image: !Ref ECSImageName
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref ECSLogGroup
awslogs-region: !Ref "AWS::Region"
awslogs-stream-prefix: !Ref PJPrefix
MemoryReservation: 128
PortMappings:
- HostPort: 80
Protocol: tcp
ContainerPort: 80
# ECS Service
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBListenerProd
- ALBListenerTest
Properties:
Cluster: !Ref ECSCluster
DesiredCount: !Ref ECSTaskDesiredCount
LaunchType: FARGATE
LoadBalancers:
- TargetGroupArn: !Ref TargetGroupBlue
ContainerPort: 80
ContainerName: !Sub "${PJPrefix}-${ECSContainerName}"
AdvancedConfiguration:
AlternateTargetGroupArn: !Ref TargetGroupGreen
ProductionListenerRule: !GetAtt ALBProdListenerRule.RuleArn
TestListenerRule: !GetAtt ALBTestListenerRule.RuleArn
RoleArn: !GetAtt ECSServiceRole.Arn
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref ECSSecurityGroup
Subnets:
- { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" }
- { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-c" }
ServiceName: !Sub "${PJPrefix}-${ECSServiceName}"
TaskDefinition: !Ref ECSTaskDefinition
DeploymentController:
Type: ECS
DeploymentConfiguration:
Strategy: BLUE_GREEN
MaximumPercent: 200
MinimumHealthyPercent: 100
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${PJPrefix}-task-execution-role"
Path: /
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
ECSServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${PJPrefix}-ecs-service-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ecs.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole
Policies:
- PolicyName: !Sub "${PJPrefix}-elb-access-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- elasticloadbalancing:DescribeListeners
- elasticloadbalancing:DescribeTargetGroups
- elasticloadbalancing:DescribeRules
- elasticloadbalancing:ModifyListener
- elasticloadbalancing:ModifyRule
- elasticloadbalancing:RegisterTargets
- elasticloadbalancing:DeregisterTargets
- elasticloadbalancing:DescribeLoadBalancers
Resource: "*"
Outputs:
ALBDNSName:
Value: !GetAtt InternetALB.DNSName
Export:
Name: !Sub "${PJPrefix}-${InternetALBName}-dnsname"
ProdALBListenerArn:
Value: !Ref ALBListenerProd
Export:
Name: !Sub "${PJPrefix}-prod-alb-listener-arn"
TestALBListenerArn:
Value: !Ref ALBListenerTest
Export:
Name: !Sub "${PJPrefix}-test-alb-listener-arn"
更新前の状態
更新前の状態はこちら。Target groupは ecs-test-tg-green
を向いています。
マネジメントコンソールから操作
1回目: Green -> Blue
マネジメントコンソールからタスク定義を更新しておきます。
ECSサービス画面からイメージを更新します。 新しいデプロイの強制
をチェックして、タスク定義のリビジョン
が更新対象を指していることを確認してから、画面下部の 更新
を選択します。LB周りの設定は一切変更していません。
更新中の画面がこちら。
サービスリビジョンを見ると、切り替え先のほうは ecs-test-tg-blue
Target groupを指しています。
Target groupの方を見ても、新規に作成されたコンテナをターゲットとして登録しています。
しばらく待つとデプロイが完了します。
古い方のTarget groupを見るとターゲットのドレインプロセスが進行中でした。
2回目: Blue -> Green
2回目の更新を行います。ここでは古いタスク定義リビジョンを指定して行います。
デプロイが開始されます。
しばらくするとデプロイが完了し、Target groupも切り替わっていることを確認しました。
CloudFormationから操作
※CloudFormationからリソースを作成・更新する際にドリフトをチェックすると、以下のドリフトが検知され続けます。
1回目: Green -> Blue
CloudFormationからの更新は、パラメータの ECSImageName
部分を書き換えるだけです。
変更セットの概要はこちら。
変更セットを適用し、デプロイが開始されます。
CloudFormationから更新するとタスク定義の新しいリビジョンが作成され、それを使用して更新します。
しばらくするとデプロイが完了します。
2回目: Blue -> Green
再びCloudFormationから更新します。ここでもパラメータの ECSImageName
部分に古いイメージを指定します。
変更セットを適用し、デプロイが開始されます。
しばらくするとデプロイが完了します。
Discussion