Amazon ECS on FargateのBlue/GreenデプロイをCloudFormaitionでIaC化できるのか-Part1

記事タイトルとURLをコピーする

こんにちは。最近神奈川県藤沢市に引っ越してQOLが上がりつつある、DS課の古川です。

はじめに

Infrastructure as Code(以下IaC)が当たり前の世の中になってきました。 この流れでAmazon ECS (以下ECS)もAWS CloudFormaition(以下CFn)でIaC化して構築する機会が増えました。

ECS on Fargateはデプロイタイプとして、ローリングアップデートとBlue/Geenデプロイを選択できます。

docs.aws.amazon.com docs.aws.amazon.com

今まで、ECSのローリングアップデートをCFnにてIaC化する機会はあったのですが、Blue/GeenデプロイをIaC化したことはなかったので、今回挑戦してみました。

結論

いきなり結論から述べますと、IaC化には以下の2パターンがあります。

  1. CloudFormaionからBlue/Greenデプロイを実行するなら可能

  2. 100%IaC化は無理で、一部手動によるリソースの用意が必要

本記事では1. CloudFormaionからBlue/Greenデプロイを実行するなら可能を実践してみました。

テンプレート

今回の検証は、AWS公式ページを参考にしております。

docs.aws.amazon.com

公式ページを参考に、自身で用意したyamlを以下に記載します。※ネットワーク関連のリソースは予め用意しております。


この中で気になった箇所をピックアップしてきます。

AWSTemplateFormatVersion: 2010-09-09
Description: ecs_poc_blue_green

Parameters:
  System:
    Type: String
    Default: ecs
  Stage:
    Type: String
    Default: poc
  Vpc:
    Type: AWS::EC2::VPC::Id
  ALBSubnet1a:
    Type: AWS::EC2::Subnet::Id
  ALBSubnet1c:
    Type: AWS::EC2::Subnet::Id
  ECSSubnet1a:
    Type: AWS::EC2::Subnet::Id
  ECSSubnet1c:
    Type: AWS::EC2::Subnet::Id


Transform:
  - AWS::CodeDeployBlueGreen

Hooks:
  CodeDeployBlueGreenHook:
    Type: AWS::CodeDeploy::BlueGreen
    Properties:
      TrafficRoutingConfig:
        Type: AllAtOnce
      Applications:
        - Target:
            Type: AWS::ECS::Service
            LogicalID: ECSPocService
          ECSAttributes:
            TaskDefinitions:
              - ECSPocBlueTaskDefinition
              - ECSPocGreenTaskDefinition
            TaskSets:
              - ECSPocBlueTaskSet
              - ECSPocGreenTaskSet
            TrafficRouting:
              ProdTrafficRoute:
                Type: AWS::ElasticLoadBalancingV2::Listener
                LogicalID: ECSPocALBListenerProdTraffic
              TestTrafficRoute:
                Type: AWS::ElasticLoadBalancingV2::Listener
                LogicalID: ECSPocALBListenerTestTraffic
              TargetGroups:
                - ECSPocALBTargetGroupBlue
                - ECSPocALBTargetGroupGreen

Resources:
  ECSPocALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ec2 access
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: 0.0.0.0/0

  ECSPocContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ecs
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0

  ECSPocALBTargetGroupBlue:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub "${System}-${Stage}-blue-tg"
      HealthCheckIntervalSeconds: 15
      HealthCheckPath: /healthcheck.php
      HealthCheckPort: 80
      HealthCheckProtocol: HTTP
      HealthyThresholdCount: 3
      UnhealthyThresholdCount: 2
      Matcher:
        HttpCode: 200
      Port: 80
      Protocol: HTTP
      Tags:
        - Key: System
          Value: !Ref System
        - Key: Environment
          Value: !Ref Stage
      TargetType: ip
      VpcId: !Ref Vpc

  ECSPocALBTargetGroupGreen:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub "${System}-${Stage}-green-tg"
      HealthCheckIntervalSeconds: 15
      HealthCheckPath: /healthcheck.php
      HealthCheckPort: 80
      HealthCheckProtocol: HTTP
      HealthyThresholdCount: 3
      UnhealthyThresholdCount: 2
      Matcher:
        HttpCode: 200
      Port: 80
      Protocol: HTTP
      Tags:
        - Key: System
          Value: !Ref System
        - Key: Environment
          Value: !Ref Stage
      TargetType: ip
      VpcId: !Ref Vpc

  ECSPocALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub "${System}-${Stage}-alb"
      Scheme: internet-facing
      SecurityGroups:
        - !Ref ECSPocALBSecurityGroup
      Subnets:
        - !Ref ALBSubnet1a
        - !Ref ALBSubnet1c
      Tags:
        - Key: System
          Value: !Ref System
        - Key: Environment
          Value: !Ref Stage
      Type: application
      IpAddressType: ipv4

  ECSPocALBListenerProdTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref ECSPocALBTargetGroupBlue
                Weight: 1
      LoadBalancerArn: !Ref ECSPocALB
      Port: 80
      Protocol: HTTP

  ECSPocALBListenerTestTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref ECSPocALBTargetGroupBlue
                Weight: 1
      LoadBalancerArn: !Ref ECSPocALB
      Port: 8080
      Protocol: HTTP

  ECSPocTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${System}-${Stage}-task-execution-role"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole

  ECSPocBlueTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ExecutionRoleArn: !GetAtt ECSPocTaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: !Sub "${System}-${Stage}-app"
          Image: ******.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-poc-ecr:v3
          Essential: true
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80
      RequiresCompatibilities:
        - FARGATE
      NetworkMode: awsvpc
      Cpu: 256
      Memory: 512
      Family: !Sub "${System}-${Stage}-task"

  ECSPocCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "${System}-${Stage}-cluster"
      Tags:
        - Key: System
          Value: !Ref System
        - Key: Environment
          Value: !Ref Stage

  ECSPocService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref ECSPocCluster
      DesiredCount: 1
      DeploymentController:
        Type: EXTERNAL

  ECSPocBlueTaskSet:
    Type: AWS::ECS::TaskSet
    Properties:
      Cluster: !Ref ECSPocCluster
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsVpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref ECSPocContainerSecurityGroup
          Subnets:
            - !Ref ECSSubnet1a
            - !Ref ECSSubnet1c
      PlatformVersion: 1.4.0
      Scale:
        Unit: PERCENT
        Value: 1
      Service: !Ref ECSPocService
      TaskDefinition: !Ref ECSPocBlueTaskDefinition
      LoadBalancers:
        - ContainerName: !Sub "${System}-${Stage}-app"
          ContainerPort: 80
          TargetGroupArn: !Ref ECSPocALBTargetGroupBlue
  PrimaryTaskSet:
    Type: AWS::ECS::PrimaryTaskSet
    Properties:
      Cluster: !Ref ECSPocCluster
      Service: !Ref ECSPocService
      TaskSetId: !GetAtt ECSPocBlueTaskSet.Id

TransformとHooks

Transformプロパティはいわゆるマクロで、AWS公式が用意しているAWS::CodeDeployBlueGreenを適用します。 AWS::CodeDeployBlueGreenによって、Hooksプロパティに設定しているCodeDeployBlueGreenHookが呼び出されます。

今回CodeDeployBlueGreenHookで定義するものは、Blue/Greenデプロイで使用する以下のリソースです。

プロパティ 説明
TrafficRoutingConfig 一括デプロイの場合はAllAtOnce、カナリアデプロイはTimeBasedCanary、線形デプロイはTimeBasedLinearを選択します。
Applications TaskDefinitions Blue用、Green用タスク定義のリソースの論理ID
TaskSets Blue用、Green用タスクセットのリソースの論理ID
TrafficRouting ALBのリスナーポートを選択します。公式ページのサンプルではProdTrafficRouteのみの記載ですが、テスト用ポートが必要な場合はTestTrafficRouteを用意します。

公式ページを見ると、ServiceRoleの設定が必須になっていますが、記載しなくてもデフォルトで用意してくれるようです。

他にもAdditionalOptionsプロパティやLifecycleEventHooksプロパティを記載することで、CodeDeployの設定ができます。

Resources

ECS関連のリソースは、Blue用を用意するだけで十分です。
Green用のリソースは?と思うかもしれませんが、Hooksで設定したECSPocGreenTaskDefinitionECSPocGreenTaskSetによりGreen用のリソースを用意してくれます。
リスナーやターゲットグループ等のALB関連のリソースは、Blue用とGreen用を両方用意する必要があります。

以下のDeploymentControllerにて、ローリングアップデートかCodeDeployによるBlue/Greenデプロイ、もしくはその他のデプロイタイプとしてEXTERNALが選択できます。
Type:code_deployを設定するかと思ったのですが、ここではType: EXTERNALを選択します。

  ECSPocService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref ECSPocCluster
      DesiredCount: 1
      DeploymentController:
        Type: EXTERNAL

まずはリソースをデプロイしてみる

今回はAWS CLIではなく、CFnのマネージメントコンソールからデプロイします。

f:id:swx-furukawa:20211228194706p:plain

問題なくデプロイが完了しました。

f:id:swx-furukawa:20211228194759p:plain

ECSサービスのステータス画面を確認してみると、ECSタスクは問題なく実行されています。
しかし画面上ではデプロイタイプはBlue/Greenデプロイメントと認識はされていないようです。

f:id:swx-furukawa:20211228194954p:plain

マネージメントコンソールからCodeDeployによるBlue/Greenデプロイを設定した場合、本来ならば以下の画像のように表示されます。

f:id:swx-furukawa:20211228195427p:plain

Greenをデプロイする

仕様として、ECSタスク定義もしくはECSタスクセットの変更を実施した場合、Blue/Greenデプロイが実行されるようです。 今回はコンテナイメージをv3からv4に変更します。

      ContainerDefinitions:
        - Name: !Sub "${System}-${Stage}-app"
          Image: ******.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-poc-ecr:v4

CFnのコンソール画面から、先ほど作成したスタックを更新します。

f:id:swx-furukawa:20211228200250p:plain

CodeDeployのコンソール画面を確認すると、デプロイの開始を確認できました。

f:id:swx-furukawa:20211228200314p:plain

Greenのデプロイが完了しました。切り替えも完了しております。

f:id:swx-furukawa:20211228200500p:plain

最後に

本記事では、CloudFormaitionからAmazon ECS on FargateのBlue/Greenデプロイを行いました。
ただこの方法はいくつか制約があるのと、Hooksプロパティの内容がブラックボックス化されているので、注意が必要です。
気になる制約としては以下が上げられます。

  • コンテナイメージのURIはParametersから参照できない
    • Parametersから動的に参照させようと試みたのですが、Resoursesに直接URIを記載しないといけないようです
  • ECSタスク定義もしくはECSタスクセットと同時に、他のリソースの変更を行い、スタックを更新するとエラーが出る
  • NoEcho 属性で定義されたパラメータは参照できない
  • Parameter StoreやSecrets Managerなどの外部リソースから取得する値は参照できない

他にも制約がありそうなので、用法・用量を守って正しくお使いください。

次回は2. 100%IaC化は無理で、一部手動によるリソースの用意が必要について検証してみます。

古川敏光 (執筆記事の一覧)

アプリケーションサービス部・ディべロップメント課

AWSによるサーバレス開発をメインに日々研鑽しております。 最近ハマっている趣味はサーフィンです。