CloudFormationとAWS CLIでFargateのBlue/Green Deployment環境を構築する #Fargate

同じVPC内に複数のFargateを複数構築するときに使えるCloudformationテンプレートをBlueGreen Deploymentできるように変更しました。
2020.04.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、コカコーラ大好きカジです。

以前、VPC構築済みの環境や、同じVPC内に複数のFargateを複数構築するときに使えるCloudFormationテンプレートを作成し公開しました。

上記のCloudFormationテンプレートで構築した環境をBlue/Green Deploymentへ変更しようとした際に、現在マネージメントコンソールから変更できず、解決した方法を記載しておきます。2020年3月時点、上記ブログのCloudFormationから作成したECSのサービス更新から、Blue/Green Deploymentの選択が表示されません。今後変更されると推測しています。(マネージメントコンソールから手動でFargateを構築した場合は影響を受けません。)

CloudFormationのテンプレートを一部追記することで、マネージメントコンソールのECS Serviceの更新からBlue/Green Deploymentの選択できるようになりましたが、全体の手順もまとめてご紹介します。

目次

前提条件

  • 上記CloudFormationテンプレートで構築できる準備が整っている必要があります。
  • 前回のStackからのUpdate Stackでは名前が重複しエラーとなり、更新できません。そのためStackを一度削除し再作成か、すべて別名での構築となります。

Blue/Green Deploymentとは

Blue/Green Deploymentはデプロイの一つのパターンです。

本番環境内に2つの環境を用意し、トラフィックを片方だけに向けておきます。新しいバージョンのデプロイは本番のトラフィックが流れていない方の環境に行い、その環境上で正しく動作していることを確認したら、トラフィックをその環境に切り替えることで新しいバージョンをリリースします。

今回のFargateに対してのCodeDeployでのBlue/Green Deploymentでは2つのターゲットグループを作成し、ALBからのトラフィックを流すターゲットグループを切り替えることでBlue/Green Deploymentするための変更を行います。

詳細は以下

前回のブログからの変更点(前回のブログを見てくれている人向け)

一番重要なポイントは、ECS Fargateは、以下の2行を追記となります。

# ------------------------------------------------------------#
#  ECS Service
# ------------------------------------------------------------#
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ALBListener
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: !Ref ECSTaskDesiredCount
      DeploymentController: # 追記
        Type: CODE_DEPLOY   # 追記

その他変更点

  • ALBのターゲットグループとリスナーをそれぞれ1つ追加
  • Blue/Green DeploymentのCodeDeployと接続する際に、CodeDeploy用のRoleの指定が必要となるため一緒にCloudFormationで構築
  • Blue/Green DeploymentのCodeDeployをAWS CLIで構築 (現時点で、CloudFormation非対応のため)
  • マネージメントコンソールのECS Serviceの更新からBlue/Green Deploymentの選択し設定 (現時点で、CloudFormation非対応のため)

作業手順

文末のCloudFormationテンプレートで、ALBとFargateを構築します。(構築方法は前回のブログと同じ)

構築したCloudFormationのリソース情報(赤い四角部分)から文末のJSONファイルを一部修正します。

文末のAWS CLIのスクリプト実行でCodeDeployのBlue/Green Deploymentを構築します。

上記まで完了したら、マネージメントコンソールからECS>作成したServiceの更新から、Blue/Green Deploymentの選択します。

新しいデプロイの強制にチェックを入れて、「次のステップ」をクリック

デプロイメントの設定で、AWS CLIで構築したCodeDeployのアプリケーション名と、デプロイメントグループが選択されていることを確認し「次のステップ」をクリック

次以降は、しばらく「次のステップ」を連打となります。

変更内容を確認し、サービスの更新を押します。

正常に行えると以下のようになり、Blue/Green Deploymentが設定されました。

Blue/Green Deploymentに必要なCode Deploy作成用スクリプトサンプル

AWS CLIがインストール済みの環境で行なってください。

CloudFromationで構築後にCodeDeploy GroupのJSONファイルを修正してから、スクリプトを実行します。

詳細は以下のブログが参考になります。

sample-fargate-deploy-group.json

Create Stackで作成後のリソースを確認し以下のJSONを修正します。ALBのターゲットグループ、リスナーや、Cluster/Service名、CodeDeployのRoleを修正します。

{
   "applicationName": "sample-fargate-app",
   "autoRollbackConfiguration": {
      "enabled": true,
      "events": [
         "DEPLOYMENT_FAILURE"
      ]
   },
   "blueGreenDeploymentConfiguration": {
      "deploymentReadyOption": {
         "actionOnTimeout": "CONTINUE_DEPLOYMENT",
         "waitTimeInMinutes": 0
      },
      "terminateBlueInstancesOnDeploymentSuccess": {
         "action": "TERMINATE",
         "terminationWaitTimeInMinutes": 5
      }
   },
   "deploymentGroupName": "sample-fargate-deploygroup",
   "deploymentStyle": {
      "deploymentOption": "WITH_TRAFFIC_CONTROL",
      "deploymentType": "BLUE_GREEN"
   },
   "loadBalancerInfo": {
      "targetGroupPairInfoList": [
         {
            "targetGroups": [
               {
                  "name": "sample-fargate-tg1"
               },
               {
                  "name": "sample-fargate-tg2"
               }
            ],
            "prodTrafficRoute": {
               "listenerArns": [
                  "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:listener/app/sample-fargate-alb/f064a3708a0a154d/95624f75b7087fb2"
               ]
            },
            "testTrafficRoute": {
               "listenerArns": [
                  "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:listener/app/sample-fargate-alb/f064a3708a0a154d/64defa76d77a59a5"
               ]
            }
         }
      ]
   },
   "serviceRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/sample-fargate-CodeDeployRole",
   "ecsServices": [
      {
         "serviceName": "sample-fargate-service",
         "clusterName": "sample-fargate-cluster"
      }
   ]
}

create-sample-codedeploy.sh

上記JSONファイルを修正してから実行します。

#!/bin/bash

aws deploy create-application \
     --application-name sample-fargate-app \
     --compute-platform ECS \
     --region ap-northeast-1

aws deploy create-deployment-group \
     --cli-input-json file://sample-fargate-deploy-group.json \
     --region ap-northeast-1

スクリプト成功時のログ

$ ./create-sample-codedeploy.sh
{
    "applicationId": "ebbd58ba-a9e2-4ec4-94dc-xxxxxxxxxxxx"
}
{
    "deploymentGroupId": "c5df86a3-75dd-4926-9ba3-xxxxxxxxxxxx"
}

CloudFormationテンプレート

構築方法は前回のブログと変わりません。

AWSTemplateFormatVersion: "2010-09-09"
Description: Fargate and ALB Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - ProjectName
      - Label:
          default: "InternetALB Configuration"
        Parameters:
          - InternetALBName
          - TargetGroupName1
          - TargetGroupName2
      - Label:
          default: "Fargate for ECS Configuration"
        Parameters:
          - ECSClusterName
          - ECSTaskName
          - ECSTaskCPUUnit
          - ECSTaskMemory
          - ECSContainerName
          - ECSImageName
          - ECSServiceName
          - ECSTaskDesiredCount
      - Label:
          default: "Netowork Configuration"
        Parameters:
          - VpcId
          - ALBSecurityGroupId
          - ALBSubnetId1
          - ALBSubnetId2
          - ECSSecurityGroupId
          - ECSSubnetId1
          - ECSSubnetId2
      - Label:
          default: "Scaling Configuration"
        Parameters:
          - ServiceScaleEvaluationPeriods
          - ServiceCpuScaleOutThreshold
          - ServiceCpuScaleInThreshold
          - TaskMinContainerCount
          - TaskMaxContainerCount

    ParameterLabels:
      InternetALBName:
        default: "InternetALBName"
      TargetGroupName1:
        default: "TargetGroupName1"
      TargetGroupName2:
        default: "TargetGroupName2"
      ECSClusterName:
        default: "ECSClusterName"
      ECSTaskName:
        default: "ECSTaskName"
      ECSTaskCPUUnit:
        default: "ECSTaskCPUUnit"
      ECSTaskMemory:
        default: "ECSTaskMemory"
      ECSContainerName:
        default: "ECSContainerName"
      ECSImageName:
        default: "ECSImageName"
      ECSServiceName:
        default: "ECSServiceName"
      ECSTaskDesiredCount:
        default: "ECSTaskDesiredCount"

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  ProjectName:
    Default: sample-fargate
    Type: String

  #VPCID
  VpcId:
    Description: "VPC ID"
    Type: AWS::EC2::VPC::Id

  #ALBSecurity Group
  ALBSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id

  #ALBSubnet1
  ALBSubnetId1:
    Description: "ALB Subnet 1st"
    Type: AWS::EC2::Subnet::Id

  #ALBSubnet2
  ALBSubnetId2:
    Description: "ALB Subnet 2st"
    Type: AWS::EC2::Subnet::Id

  #ECSSecurity Group
  ECSSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id

  #ECSSubnet1
  ECSSubnetId1:
    Description: "ECS Subnet 1st"
    Type: AWS::EC2::Subnet::Id

  #ECSSubnet2
  ECSSubnetId2:
    Description: "ECS Subnet 2st"
    Type: AWS::EC2::Subnet::Id

  #InternetALB
  InternetALBName:
    Type: String
    Default: "alb"

  #TargetGroupName1
  TargetGroupName1:
    Type: String
    Default: "tg1"

  #TargetGroupName2
  TargetGroupName2:
    Type: String
    Default: "tg2"

  #ECSClusterName
  ECSClusterName:
    Type: String
    Default: "cluster"

  #ECSTaskName
  ECSTaskName:
    Type: String
    Default: "task"

  #ECSTaskCPUUnit
  ECSTaskCPUUnit:
    AllowedValues: [256, 512, 1024, 2048, 4096]
    Type: String
    Default: "256"

  #ECSTaskMemory
  ECSTaskMemory:
    AllowedValues: [256, 512, 1024, 2048, 4096]
    Type: String
    Default: "512"

  #ECSContainerName
  ECSContainerName:
    Type: String
    Default: "container"

  #ECSImageName
  ECSImageName:
    Type: String
    Default: "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/kaji-test-ecr:latest"

  #ECSServiceName
  ECSServiceName:
    Type: String
    Default: "service"

  #ECSTaskDesiredCount
  ECSTaskDesiredCount:
    Type: Number
    Default: 1

  # Scaling params
  ServiceScaleEvaluationPeriods:
    Description: The number of periods over which data is compared to the specified threshold
    Type: Number
    Default: 2
    MinValue: 2

  ServiceCpuScaleOutThreshold:
    Type: Number
    Description: Average CPU value to trigger auto scaling out
    Default: 50
    MinValue: 0
    MaxValue: 100
    ConstraintDescription: Value must be between 0 and 100

  ServiceCpuScaleInThreshold:
    Type: Number
    Description: Average CPU value to trigger auto scaling in
    Default: 25
    MinValue: 0
    MaxValue: 100
    ConstraintDescription: Value must be between 0 and 100

  TaskMinContainerCount:
    Type: Number
    Description: Minimum number of containers to run for the service
    Default: 1
    MinValue: 1
    ConstraintDescription: Value must be at least one

  TaskMaxContainerCount:
    Type: Number
    Description: Maximum number of containers to run for the service when auto scaling out
    Default: 2
    MinValue: 1
    ConstraintDescription: Value must be at least one

Resources:
  # ------------------------------------------------------------#
  #  Target Group
  # ------------------------------------------------------------#
  TargetGroup1:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      VpcId: !Ref VpcId
      Name: !Sub "${ProjectName}-${TargetGroupName1}"
      Protocol: HTTP
      Port: 80
      TargetType: ip

  TargetGroup2:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      VpcId: !Ref VpcId
      Name: !Sub "${ProjectName}-${TargetGroupName2}"
      Protocol: HTTP
      Port: 80
      TargetType: ip

  # ------------------------------------------------------------#
  #  Internet ALB
  # ------------------------------------------------------------#
  InternetALB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name: !Sub "${ProjectName}-${InternetALBName}"
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${InternetALBName}"
      Scheme: "internet-facing"
      LoadBalancerAttributes:
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 60
        - Key: "access_logs.s3.enabled"
          Value: true
        - Key: "access_logs.s3.bucket"
          Value: !Sub "alb-log-${AWS::AccountId}"
      SecurityGroups:
        - !Ref ALBSecurityGroupId
      Subnets:
        - !Ref ALBSubnetId1
        - !Ref ALBSubnetId2

  ALBListener1:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup1
          Type: forward
      LoadBalancerArn: !Ref InternetALB
      Port: 80
      Protocol: HTTP

  ALBListener2:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup2
          Type: forward
      LoadBalancerArn: !Ref InternetALB
      Port: 8080
      Protocol: HTTP

  # ------------------------------------------------------------#
  # ECS Cluster
  # ------------------------------------------------------------#
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "${ProjectName}-${ECSClusterName}"

  # ------------------------------------------------------------#
  #  ECS LogGroup
  # ------------------------------------------------------------#
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/logs/${ProjectName}-ecs-group"

  # ------------------------------------------------------------#
  #  ECS Task Execution Role
  # ------------------------------------------------------------#
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${ProjectName}-ECSTaskExecutionRolePolicy"
      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

  # ------------------------------------------------------------#
  #  ECS TaskDefinition
  # ------------------------------------------------------------#
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Cpu: !Ref ECSTaskCPUUnit
      ExecutionRoleArn: !Ref ECSTaskExecutionRole
      Family: !Sub "${ProjectName}-${ECSTaskName}"
      Memory: !Ref ECSTaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE

      #ContainerDefinitions
      ContainerDefinitions:
        - Name: !Sub "${ProjectName}-${ECSContainerName}"
          Image: !Ref ECSImageName
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: !Ref ProjectName
          MemoryReservation: 128
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80

  # ------------------------------------------------------------#
  #  ECS Service
  # ------------------------------------------------------------#
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ALBListener1
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: !Ref ECSTaskDesiredCount
      DeploymentController:
        Type: CODE_DEPLOY
      LaunchType: FARGATE
      LoadBalancers:
        - TargetGroupArn: !Ref TargetGroup1
          ContainerPort: 80
          ContainerName: !Sub "${ProjectName}-${ECSContainerName}"
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref ECSSecurityGroupId
          Subnets:
            - !Ref ECSSubnetId1
            - !Ref ECSSubnetId2
      ServiceName: !Sub "${ProjectName}-${ECSServiceName}"
      TaskDefinition: !Ref ECSTaskDefinition

  # ------------------------------------------------------------#
  #  Auto Scaling Service
  # ------------------------------------------------------------#
  ServiceAutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: application-autoscaling.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub "${ProjectName}-${ECSContainerName}-autoscaling"
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - application-autoscaling:*
                  - cloudwatch:DescribeAlarms
                  - cloudwatch:PutMetricAlarm
                  - ecs:DescribeServices
                  - ecs:UpdateService
                Resource: "*"

  ServiceScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MinCapacity: !Ref TaskMinContainerCount
      MaxCapacity: !Ref TaskMaxContainerCount
      ResourceId: !Sub
        - service/${EcsClusterName}/${EcsDefaultServiceName}
        - EcsClusterName: !Ref ECSCluster
          EcsDefaultServiceName: !Sub "${ProjectName}-${ECSServiceName}"
      RoleARN: !GetAtt ServiceAutoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
    DependsOn:
      - ECSService
      - ServiceAutoScalingRole

  ServiceScaleOutPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub "${ProjectName}-${ECSServiceName}-ScaleOutPolicy"
      PolicyType: StepScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      StepScalingPolicyConfiguration:
        AdjustmentType: ChangeInCapacity
        Cooldown: 60
        MetricAggregationType: Average
        StepAdjustments:
          - ScalingAdjustment: 1
            MetricIntervalLowerBound: 0
    DependsOn: ServiceScalingTarget

  ServiceScaleInPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub "${ProjectName}-${ECSServiceName}-ScaleInPolicy"
      PolicyType: StepScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      StepScalingPolicyConfiguration:
        AdjustmentType: ChangeInCapacity
        Cooldown: 60
        MetricAggregationType: Average
        StepAdjustments:
          - ScalingAdjustment: -1
            MetricIntervalUpperBound: 0
    DependsOn: ServiceScalingTarget

  ServiceScaleOutAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${ProjectName}-${ECSServiceName}-ScaleOutAlarm"
      EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
      Statistic: Average
      TreatMissingData: notBreaching
      Threshold: !Ref ServiceCpuScaleOutThreshold
      AlarmDescription: Alarm to add capacity if CPU is high
      Period: 60
      AlarmActions:
        - !Ref ServiceScaleOutPolicy
      Namespace: AWS/ECS
      Dimensions:
        - Name: ClusterName
          Value: !Ref ECSCluster
        - Name: ServiceName
          Value: !Sub "${ProjectName}-${ECSServiceName}"
      ComparisonOperator: GreaterThanThreshold
      MetricName: CPUUtilization
    DependsOn:
      - ECSService
      - ServiceScaleOutPolicy

  ServiceScaleInAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${ProjectName}-${ECSServiceName}-ScaleInAlarm"
      EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
      Statistic: Average
      TreatMissingData: notBreaching
      Threshold: !Ref ServiceCpuScaleInThreshold
      AlarmDescription: Alarm to reduce capacity if container CPU is low
      Period: 300
      AlarmActions:
        - !Ref ServiceScaleInPolicy
      Namespace: AWS/ECS
      Dimensions:
        - Name: ClusterName
          Value: !Ref ECSCluster
        - Name: ServiceName
          Value: !Sub "${ProjectName}-${ECSServiceName}"
      ComparisonOperator: LessThanThreshold
      MetricName: CPUUtilization
    DependsOn:
      - ECSService
      - ServiceScaleInPolicy

  # ------------------------------------------------------------#
  #  BlueGreen CodeDeploy Role
  # ------------------------------------------------------------#
  CodeDeployRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${ProjectName}-CodeDeployRole"
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codedeploy.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS

感想

自分でハマった点を解決したメモを公開しました。どなたかのお役に立てれば光栄です。 また、今後、CloudFormationでFargateとCodeDeployのBlue/Green Deploymentもまとめて構築できるようになり、サンプルテンプレートが公開されることを期待しております。