🍣

Amazon ECSでBlue/Green Deploymentを使用しTarget group切り替えの挙動を確認する

に公開

Amazon ECSは先日のリリースでBlue/Green Deploymentをネイティブサポートしました。私はこれまで恥ずかしながらECSのBlue/Green deploymentをちゃんと使ったことがなく、 Blue/Green環境を切り替えるためにTarget group/Listenerは修正しなくてもよいのか わかっておりませんでした。

https://aws.amazon.com/jp/blogs/aws/accelerate-safe-software-releases-with-new-built-in-blue-green-deployments-in-amazon-ecs/

結論として、 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