cloudpack大阪の佐々木です。
今扱っているECS環境では、デプロイをCloudFormationでやっています。実際やってみると、アップデート途中で止まるってことが割と頻繁にあって、インプレイスでアップデートするのは怖いなということで、Blue/Greenデプロイ環境をつくってみました。
元ネタはこれなんですが、このサンプルだと、CodePipelineとか使って ややこしい いい感じので、もう少し簡単に。
初期設定
まず、Blueを本番、Greenをステージングとして、下記のように設定します。
ALB設定
- ダイナミックポートマッピングで、Blue、Greenそれぞれのターゲットグループを作成
- Blue用ターゲットグループをTCP/80(本番用ポート)にマッピング
- Green用ターゲットグループをTCP/8080(ステージング用ポート)にマッピング
ECS設定
- Blue用のサービスをつくってBlue用のターゲットグループにマッピング
- Green用のサービスをつくってGreen用のターゲットグループにマッピング
CFnのテンプレートはこんな感じになります。
Parameters:
VpcId:
Type: String
Subnets:
Type: List<AWS::EC2::Subnet::Id>
Resources:
WebSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: web-sg
SecurityGroupIngress:
- CidrIp: "0.0.0.0/0"
IpProtocol: "TCP"
FromPort: 80
ToPort: 80
- CidrIp: "0.0.0.0/0"
IpProtocol: "TCP"
FromPort: 8080
ToPort: 8080
VpcId: !Ref VpcId
### ALBを作成 ###
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: ecstest-elb
Subnets: !Ref Subnets
SecurityGroups:
- !Ref WebSecurityGroup
### Blue環境用TargetGroup ###
BlueTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: LoadBalancer
Properties:
Name: target-blue
VpcId: !Ref VpcId
Port: 80
Protocol: HTTP
### Green環境用TargetGroup ###
GreenTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: LoadBalancer
Properties:
Name: target-green
VpcId: !Ref VpcId
Port: 80
Protocol: HTTP
### 本番環境用Listner(TCP/80)
ListenerProd:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref BlueTargetGroup # <- 本番にBlue環境を紐付け
### ステージング環境用Listner(TCP/8080)
ListenerStg:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 8080
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref GreenTargetGroup # <- ステージングにGreen環境を紐付け
Parameters:
Cluster:
Type: String
BlueTargetGroupARN:
Type: String
Resources:
### Role作成 ###
ECSServiceRole:
Type: AWS::IAM::Role
Properties:
Path: /
RoleName: role-blue
AssumeRolePolicyDocument: |
{
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": [ "ecs.amazonaws.com" ]},
"Action": [ "sts:AssumeRole" ]
}]
}
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole
### Blue用サービス ###
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: service-blue
Cluster: !Ref Cluster
Role: !Ref ECSServiceRole
DesiredCount: 1
TaskDefinition: !Ref TaskDefinition
LoadBalancers:
- ContainerName: nginx
ContainerPort: 80
TargetGroupArn: !Ref BlueTargetGroupARN # <- BlueのTargetGroupを指定
### Blue用タスク定義 ###
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: ecstest # ← Familyを同じ値にすることでRevisionの変更が可能
ContainerDefinitions:
- Name: nginx
Image: nginx
Memory: 128
PortMappings:
- ContainerPort: 80
Parameters:
Cluster:
Type: String
GreenTargetGroupARN:
Type: String
Resources:
### Role作成 ###
ECSServiceRole:
Type: AWS::IAM::Role
Properties:
Path: /
RoleName: role-green
AssumeRolePolicyDocument: |
{
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": [ "ecs.amazonaws.com" ]},
"Action": [ "sts:AssumeRole" ]
}]
}
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole
### Green用サービス ###
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: service-green
Cluster: !Ref Cluster
Role: !Ref ECSServiceRole
DesiredCount: 1
TaskDefinition: !Ref TaskDefinition
LoadBalancers:
- ContainerName: nginx
ContainerPort: 80
TargetGroupArn: !Ref GreenTargetGroupARN # <- BlueのTargetGroupを指定
### Green用タスク定義 ###
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: ecstest # ← Familyを同じ値にすることでRevisionの変更が可能
ContainerDefinitions:
- Name: nginx
Image: nginx
Memory: 128
PortMappings:
- ContainerPort: 80
ELBのリスナーを確認すると、BlueがTCP/80、GreenがTCP/8080になっています。
タスク定義のFamilyの値をBlue/Greenで同じにしておけば、同じタスク定義でRevisionの更新ができます。
デプロイ
デプロイの時は、こんな感じです。
ECS設定
- Green用のサービスをアップデートする
ALB設定
- ポートのマッピングをBlue/Greenで入れ替える
という手順になります。
CFnでアップデートする場合は、下記のようなテンプレートでUpdate Stackします。
Parameters:
VpcId:
Type: String
Subnets:
Type: List<AWS::EC2::Subnet::Id>
Resources:
WebSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: web-sg
SecurityGroupIngress:
- CidrIp: "0.0.0.0/0"
IpProtocol: "TCP"
FromPort: 80
ToPort: 80
- CidrIp: "0.0.0.0/0"
IpProtocol: "TCP"
FromPort: 8080
ToPort: 8080
VpcId: !Ref VpcId
### ALBを作成 ###
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: ecstest-elb
Subnets: !Ref Subnets
SecurityGroups:
- !Ref WebSecurityGroup
### Blue環境用TargetGroup ###
BlueTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: LoadBalancer
Properties:
Name: target-blue
VpcId: !Ref VpcId
Port: 80
Protocol: HTTP
### Green環境用TargetGroup ###
GreenTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: LoadBalancer
Properties:
Name: target-green
VpcId: !Ref VpcId
Port: 80
Protocol: HTTP
### 本番環境用Listner(TCP/80)
ListenerProd:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref GreenTargetGroup # <- 本番にGreen環境を紐付け
### ステージング環境用Listner(TCP/8080)
ListenerStg:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 8080
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref BlueTargetGroup # <- ステージングにBlue環境を紐付け
リスナーのターゲットグループを入れ替えます。
入れ替わってますね。
まとめ
ALB(or NLB)のターゲットグループを使えばBlue/Green環境が1つのECSクラスタでできるようになります。
これでCFnが止まっても本番環境には影響なく安心ですね。