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


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のテンプレートはこんな感じになります。

```yaml: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環境を紐付け
```

```yaml: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

```

```yaml: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になっています。

![Kobito.ZPP2rK.png](https://qiita-image-store.s3.amazonaws.com/0/3246/7d56cbb4-0d4e-87bb-ecf4-bfa78d3c22a3.png "Kobito.ZPP2rK.png")

タスク定義のFamilyの値をBlue/Greenで同じにしておけば、同じタスク定義でRevisionの更新ができます。

![Kobito.D5emlt.png](https://qiita-image-store.s3.amazonaws.com/0/3246/fd93d909-50c9-b25e-3757-0c9445f9d4cc.png "Kobito.D5emlt.png")


## デプロイ

デプロイの時は、こんな感じです。

### ECS設定
- Green用のサービスをアップデートする

### ALB設定
- ポートのマッピングをBlue/Greenで入れ替える

という手順になります。

CFnでアップデートする場合は、下記のようなテンプレートでUpdate Stackします。

```yaml: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環境を紐付け
```

リスナーのターゲットグループを入れ替えます。

![Kobito.LyimFg.png](https://qiita-image-store.s3.amazonaws.com/0/3246/c09ee6f0-1f9a-28ef-d69b-66afaf183f4d.png "Kobito.LyimFg.png")

入れ替わってますね。

## まとめ

ALB(or NLB)のターゲットグループを使えばBlue/Green環境が1つのECSクラスタでできるようになります。
これでCFnが止まっても本番環境には影響なく安心ですね。