cloudpack大阪の佐々木です。
今扱っているECS環境では、デプロイをCloudFormationでやっています。実際やってみると、アップデート途中で止まるってことが割と頻繁にあって、インプレイスでアップデートするのは怖いなということで、Blue/Greenデプロイ環境をつくってみました。
https://aws.amazon.com/jp/blogs/news/bluegreen-deployments-with-amazon-ecs/
元ネタはこれなんですが、このサンプルだと、CodePipelineとか使って ややこしい いい感じので、もう少し簡単に。
初期設定
まず、Blueを本番、Greenをステージングとして、下記のように設定します。
ALB設定
- ダイナミックポートマッピングで、Blue、Greenそれぞれのターゲットグループを作成
- Blue用ターゲットグループをTCP/80(本番用ポート)にマッピング
- Green用ターゲットグループをTCP/8080(ステージング用ポート)にマッピング
ECS設定
- Blue用のサービスをつくってBlue用のターゲットグループにマッピング
- Green用のサービスをつくってGreen用のターゲットグループにマッピング
CFnのテンプレートはこんな感じになります。
elb.yml
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環境を紐付け
blue.yml
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
green.yml
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します。
elb.yml
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が止まっても本番環境には影響なく安心ですね。