CloudFormation 一撃で Fargate の Blue/Green Deployment 環境を構築する

2021.03.10

はじめに

おはようございます、もきゅりんです。

皆さん、Blue/Green デプロイメントしてますか?

Blue/Green デプロイメントとは、はすでに理解されていることを前提として話を進めます。

本稿は、CloudFormationとAWS CLIでFargateのBlue/Green Deployment環境を構築する #Fargate | DevelopersIO の内容をカスタムリソースを利用して CloudFormation 一撃で構築するものになります。

構成図は以下です。

fargate_architecture

プレースホルダを用いて以下のBlue/Green デプロイメントを実現します。

bluegreen_image

プレースホルダとは?という方は、 チュートリアル: ソースと ECS と CodeDeploy 間のデプロイでパイプラインを作成するAmazon ECR - AWS CodePipeline をご参照下さい。

なお、本稿の別アプローチで AWS CloudFormation を使用して CodeDeploy による ECS ブルー/グリーンデプロイを実行する - AWS CloudFormation の方法でも CloudFormation 一撃で ECS の Blue/Green デプロイメント環境を構築することもできます。

これらの違いについては、[2パターン] CFn で Fargate の Blue/Green Deployment の CodePipeline を構築するで説明していますので、興味がある方はご参照下さい。

カスタムリソースとは

カスタムリソースとは?という人は カスタムリソース - AWS CloudFormation をご参照下さい。

カスタムリソースを使用すると、テンプレートにカスタムのプロビジョニングロジックを記述し、ユーザーがスタックを作成、更新(カスタムリソースを変更した場合)、削除するたびに AWS CloudFormation がそれを実行します。たとえば、AWS CloudFormation のリソースタイプとして使用できないリソースを含める必要があるとします。それらのリソースは、カスタム リソースを使用して含めることができます。この方法により、すべての関連リソースを 1 つのスタックで管理できます。

本稿では、AWS Lambda-backed カスタムリソース - AWS CloudFormation を使って、AWS CloudFormation とLambda 関数との組み合わせで環境を構築します。

なんでそんなことわざわざするの?

現状(2021/3/10)、CloudFormation による CodeDeploy の ECS Blue/Green Deployment Group 作成は非対応のためです。

CloudFormation と AWS CLI で作れますが、どうせなら CFn 一撃で作りたいよね、ということで作った次第です。

以下の図でいうと、赤枠の部分を Lambda のカスタムリソースが作成します。

custom_resource_target

前提条件

ベースは CloudformationでFargateを構築する | DevelopersIO のブログなので、こちらと同様です。

  • 利用予定のアカウントで、AWS CLIが利用可能
  • VPC、ALB Security Group、ECS Task Security Groupが構築済み
  • Fargateを配置するサブネットは、NATゲートウェイ等経由でインターネット通信可能であること

ついでだったのでALBログS3バケットはリソースに加えています。

Cloudformationテンプレート

注記

Lambda 関数は Python で記載しているので、必要に応じて、CodeDeploy — Boto3 Docs 1.17.23 documentation を確認の上、パラメータを微調整してご利用頂ければと思います。

CodeDeployのデプロイメントグループのデプロイ設定は下記です。

  • トラフィックを再ルーティングさせるのは5分後です。
  • デプロイ設定は CodeDeployDefault.ECSLinear10PercentEvery1Minutes です。
  • 元のリビジョンの終了は30分後です。
  • デプロイが失敗したらロールバックします。

sample.yml

AWSTemplateFormatVersion: '2010-09-09'
Description: Fargate and ALB and CodeDeploy for Blue/Green 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
      - Label:
          default: 'CodeDeploy Configuration'
        Parameters:
          - CodeDeployAppName
          - CodeDeployDeploymentGroupName

    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'
      CodeDeployAppName:
        default: 'CodeDeployAppName'
      CodeDeployDeploymentGroupName:
        default: 'CodeDeployDeploymentGroupName'
# ------------------------------------------------------------#
# 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: 'nginxdemos/hello:latest'

  #ECSServiceName
  ECSServiceName:
    Type: String
    Default: 'service'

  CodeDeployAppName:
    Type: String
    Default: 'app'

  CodeDeployDeploymentGroupName:
    Type: String
    Default: 'dg'

  #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: !GetAtt ECSTaskExecutionRole.Arn
      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

  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

  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

  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

  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
  # ------------------------------------------------------------#
  #  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
  # ------------------------------------------------------------#
  # Lambda
  # ------------------------------------------------------------#
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    DeletionPolicy: 'Delete'
    Properties:
      Code:
        ZipFile: |
          import boto3
          import json
          import logging
          import cfnresponse
          from botocore.exceptions import ClientError

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          client = boto3.client('codedeploy')

          def lambda_handler(event, context):

              appName = event['ResourceProperties']['appName']
              deploymentGroup = event['ResourceProperties']['deploymentGroup']
              clusterName = event['ResourceProperties']['ECSClusterName']
              serviceName = event['ResourceProperties']['ECSServiceName']

              print('REQUEST RECEIVED:\n' + json.dumps(event))
              responseData = {}
              try:
                res = client.create_application(
                    applicationName=appName,
                    computePlatform='ECS'
                )
                logger.info(res)
                logger.info("SUCCESS: CodeDeploy Application created.")
                res = client.create_deployment_group(
                    applicationName=appName,
                    deploymentGroupName=deploymentGroup,
                    deploymentConfigName='CodeDeployDefault.ECSLinear10PercentEvery1Minutes',
                    serviceRoleArn=event['ResourceProperties']['CodeDeployServiceRoleArn'],
                    autoRollbackConfiguration={
                        'enabled': True,
                        'events': [
                            'DEPLOYMENT_FAILURE',
                        ]
                    },
                    deploymentStyle={
                        'deploymentType': 'BLUE_GREEN',
                        'deploymentOption': 'WITH_TRAFFIC_CONTROL'
                    },
                    blueGreenDeploymentConfiguration={
                        'terminateBlueInstancesOnDeploymentSuccess': {
                            'action': 'TERMINATE',
                            'terminationWaitTimeInMinutes': 30
                        },
                        'deploymentReadyOption': {
                            'actionOnTimeout': 'STOP_DEPLOYMENT',
                            'waitTimeInMinutes': 5
                        }
                    },
                    loadBalancerInfo={
                        'targetGroupPairInfoList': [
                            {
                                'targetGroups': [
                                    {
                                        'name': event['ResourceProperties']['TargetGroup1']
                                    },
                                    {
                                        'name': event['ResourceProperties']['TargetGroup2']
                                    },
                                ],
                                'prodTrafficRoute': {
                                    'listenerArns': [
                                        event['ResourceProperties']['ALBListener1'],
                                    ]
                                },
                                'testTrafficRoute': {
                                    'listenerArns': [
                                        event['ResourceProperties']['ALBListener2'],
                                    ]
                                }
                            },
                        ]
                    },
                    ecsServices=[
                        {
                            'serviceName': serviceName,
                            'clusterName': clusterName
                        },
                    ]
                )
              except ClientError as e:
                  logger.error("ERROR: Something error!")
                  logger.error(e)
                  responseData = {'error': str(e)}
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
              else:
                  logger.info(res)
                  logger.info(
                      "SUCCESS: CodeDeploy Application and DeploymentGroup created.")
                  return cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
      Handler: index.lambda_handler
      Role: !GetAtt LambdaRole.Arn
      Runtime: python3.8
      Timeout: 10
  # ------------------------------------------------------------#
  # Custom Resource
  # ------------------------------------------------------------#
  CreateCodeDeploy:
    Type: Custom::CreateCodeDeploy
    DependsOn:
      - ECSService
    Properties:
      ServiceToken: !GetAtt LambdaFunction.Arn
      Region: !Ref AWS::Region
      ECSClusterName: !Sub '${ProjectName}-${ECSClusterName}'
      ECSServiceName: !Sub '${ProjectName}-${ECSServiceName}'
      CodeDeployServiceRoleArn: !GetAtt CodeDeployRole.Arn
      TargetGroup1: !Sub '${ProjectName}-${TargetGroupName1}'
      TargetGroup2: !Sub '${ProjectName}-${TargetGroupName2}'
      ALBListener1: !Ref ALBListener1
      ALBListener2: !Ref ALBListener2
      appName: !Sub '${ProjectName}-${ECSClusterName}-${ECSServiceName}-${CodeDeployAppName}'
      deploymentGroup: !Sub '${ProjectName}-${ECSClusterName}-${ECSServiceName}-${CodeDeployDeploymentGroupName}'
  # ------------------------------------------------------------#
  # IAMRole For CustomResource Lambda
  # ------------------------------------------------------------#
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSCodeDeployFullAccess
  LambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: LambdaPolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - ec2:*
              - logs:*
            Resource: '*'
          - Effect: Allow
            Resource: '*'
            Action:
              - iam:PassRole
            Condition:
              StringEqualsIfExists:
                iam:PassedToService:
                  - codedeploy.amazonaws.com
      Roles:
        - !Ref LambdaRole
  # ------------------------------------------------------------#
  # Log Bucket
  # ------------------------------------------------------------#
  logsBacket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'alb-log-${AWS::AccountId}'
      AccessControl: LogDeliveryWrite
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'AES256'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: TRUE
        BlockPublicPolicy: TRUE
        IgnorePublicAcls: TRUE
        RestrictPublicBuckets: TRUE
      LifecycleConfiguration:
        Rules:
          - Id: 'Delete-After-400days'
            Status: Enabled
            ExpirationInDays: 400
  logsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn: logsBacket
    Properties:
      Bucket: !Sub 'alb-log-${AWS::AccountId}'
      PolicyDocument:
        Statement:
          - Action:
              - 's3:PutObject'
            Effect: 'Allow'
            Resource:
              - Fn::Join:
                  - ''
                  - - 'arn:aws:s3:::'
                    - !Sub 'alb-log-${AWS::AccountId}'
                    - '/*'
            Principal:
              AWS: '582318560864'
          - Action:
              - 's3:PutObject'
            Effect: 'Allow'
            Resource:
              - Fn::Join:
                  - ''
                  - - 'arn:aws:s3:::'
                    - !Sub 'alb-log-${AWS::AccountId}'
                    - '/*'
            Principal:
              Service: 'delivery.logs.amazonaws.com'
            Condition:
              StringEquals:
                's3:x-amz-acl':
                  - 'bucket-owner-full-control'
          - Action:
              - 's3:GetBucketAcl'
            Effect: 'Allow'
            Resource:
              Fn::Join:
                - ''
                - - 'arn:aws:s3:::'
                  - !Sub 'alb-log-${AWS::AccountId}'
            Principal:
              Service: 'delivery.logs.amazonaws.com'

CodePipeline 構築を合わせてどうぞ

[2パターン] CFn で Fargate の Blue/Green Deployment の CodePipeline を構築する では、CodeCommit を利用した プレースホルダを利用した ECS Blue/Green の CodePipeline を CloudFormation で構築しています。

(Fargateを構築するパラメータと多少異なるので調整が必要です。)

CodeCommit に必要な Dockerfile(+その他) , appspec.yaml , taskdef.jsonを格納して実行します。

placeholder_pipeline

手動承認ステージはコメントアウトしていますので、利用する場合は必要な設定を確認の上、ご利用下さい。

codepipeline.yml

AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with PlaceHolder
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  NameTagPrefix:
    Type: String
    Default: test
    Description: Prefix of Name tags.
  ServiceName:
    Type: String
    Default: myapp
    Description: Prefix of Service tags.
  CodeCommitRepositoryName:
    Type: String
  CodeDeployAppName:
    Type: String
  CodeDeployDGName:
    Type: String
  ContainerName:
    Type: String
  ECRName:
    Type: String
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Resources:
  # ------------------------------------------------------------#
  # IAM Roles
  # ------------------------------------------------------------#
  # CodeWatchEventを実行できるIAMRole
  CloudwatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CloudWatchEventRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: CloudWatchEventsPipelineExecution
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: codepipeline:StartPipelineExecution
                Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}

  # CodeBuildに適用するIAMRole
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodeBuildServiceNameRole
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SampleCodeBuildAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource: '*'
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Effect: Allow
                Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
              - Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                  - codebuild:BatchPutCodeCoverages
                Resource: '*'
              - Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:GetRepositoryPolicy
                  - ecr:DescribeRepositories
                  - ecr:ListImages
                  - ecr:DescribeImages
                  - ecr:BatchGetImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload
                  - ecr:PutImage
                Resource: '*'

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodePipelineServiceRole
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SamplePipeline
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource: '*'
                Effect: Allow
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                  - codedeploy:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - elasticbeanstalk:*
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - sns:*
                  - cloudformation:*
                  - rds:*
                  - sqs:*
                  - ecs:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuildBatches
                  - codebuild:StartBuildBatch
                Resource: '*'
                Effect: Allow

  # S3Bucket
  ArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  # CloudWatchEventの実行ルール
  AmazonCloudWatchEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - CodeCommit Repository State Change
        resources:
          - Fn::Join:
              - ''
              - - 'arn:aws:codecommit:'
                - !Ref 'AWS::Region'
                - ':'
                - !Ref 'AWS::AccountId'
                - ':'
                - !Ref CodeCommitRepositoryName
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - master
      Targets:
        - Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
          RoleArn: !GetAtt CloudwatchEventRole.Arn
          Id: codepipeline-AppPipeline

  # CodeBuild
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      ServiceRole: !Ref CodeBuildServiceRole
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            pre_build:
              commands:
                - echo Logging in to Amazon ECR...
                - $(aws ecr get-login --no-include-email)
                - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
            build:
              commands:
                - echo Build started on `date`
                - echo Building the Docker image...
                - docker build -t $REPOSITORY_URI:$IMAGE_TAG .
                - docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images...
                - docker push $REPOSITORY_URI:$IMAGE_TAG
                - echo Writing imageDetail json...
                - echo "{\"name\":\"${ContainerName}\",\"ImageURI\":\"${REPOSITORY_URI}:${IMAGE_TAG}\"}" > imageDetail.json
          artifacts:
            files: imageDetail.json
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
          - Name: ContainerName
            Value: !Ref ContainerName
          - Name: DOCKER_BUILDKIT
            Value: '1'
      Name: !Ref AWS::StackName
  # ------------------------------------------------------------#
  # CodePipeline
  # ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub ${NameTagPrefix}-${ServiceName}-pipeline
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: '1'
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref CodeCommitRepositoryName
                PollForSourceChanges: false
                BranchName: master
              RunOrder: 1
              OutputArtifacts:
                - Name: App
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: App
              OutputArtifacts:
                - Name: BuildOutput
        # - Name: Approval
        #   Actions:
        #     - Name: Manual_Approval
        #       ActionTypeId:
        #         Category: Approval
        #         Owner: AWS
        #         Version: '1'
        #         Provider: Manual
        #       Configuration:
        #         CustomData: !Sub '${ServiceName} will be updated. Do you want to deploy it?'
        #         NotificationArn: arn:aws:sns:ap-northeast-1:xxxxxxxx:hogehoge
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: '1'
                Provider: CodeDeployToECS
              Configuration:
                AppSpecTemplateArtifact: App
                AppSpecTemplatePath: appspec.yaml
                TaskDefinitionTemplateArtifact: App
                TaskDefinitionTemplatePath: taskdef.json
                ApplicationName: !Ref CodeDeployAppName
                DeploymentGroupName: !Ref CodeDeployDGName
                Image1ArtifactName: BuildOutput
                Image1ContainerName: IMAGE1_NAME
              RunOrder: 1
              InputArtifacts:
                - Name: App
                - Name: BuildOutput
              Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PipelinelogicalID:
    Description: logical ID.
    Value: !Ref Pipeline

最後に

前回カスタムリソースを使ったのは、今は AWS のアップデートによって不要となったCloudFormation一撃でAWS Transfer for SFTP のパブリックアクセスを特定IP に限定する | DevelopersIO でした。

このテンプレートもそのうち無用になるかと思っています。

とはいえ、個人的には機会があればカスタムリソースを使ってみたいと思います。

以上です。

どなたかのどなたかのお役に立てば幸いです。

参考