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が止まっても本番環境には影響なく安心ですね。   

元記事はこちら

ECSでBlue/Greenデプロイ【cloudpack大阪ブログ】