LoginSignup
0
0

More than 1 year has passed since last update.

CloudFormationでECSのBlue/Greenデプロイを構築(その3-CodeDeploy::BlueGreenフックをPipelineによる実現)

Posted at

概要

AWS ECS(Fargate)のサービスをBlue/Greenにデプロイして、Code Pipelineからコンテナイメージやアプリを変更させ、CodeBuildでdocker buildし、CodeDeployにてBlueからGreenに切り替えさせる手順を紹介する。
AWS ECS(Fargate)のサービスのFargate Blue/Greenのデプロイは、プレースホルダを利用する方法とCodeDeploy::BlueGreen フックを利用する方法2パータンがありますが、今回はCodeDeploy::BlueGreen フックをCodePipelineにより実現する方法を検証する。
CodeDeploy::BlueGreen フックを利用して実現するやり方は、下記の記事をご参照ください。

プレースホルダを利用してECS(Fargate)サービスのBlue/Green CICDを実現する方は、下記の記事をご参照ください

やり方として、Transform というAWSのマクロ機能を利用し、CodeDeploy::BlueGreen フックにECSタスク、ECSサービス、ECSタスクセットを定義し、これらのリソースの更新は、CodePipelineに渡してBlue/Greenデプロイを実現する。
この考えで、二つのYmlファイルを作った。一つはCodeDeploy::BlueGreen フックとECSタスク、ECSサービス、ECSタスクセットを定義するhook-deploy.ymlファイル、もひとつはPipelineを定義するhook-pipeline.ymlである。

環境用意

以下のリソースは事前に用意して置く。
S3パケット
ECRリポジトリ
VPC
Subnet1a
Subnet1c
SecurityGroup
Dockerfile

実施プロシージャ

1. CodeDeploy::BlueGreen フックとECSリソースを作成

CodeDeploy::BlueGreen フックを作成にはAWSのTransformを使う。Transform含むテンプレートはaws cloudformation create-stackコマンドでは実行できないのでご注意ください。その代わり、aws cloudformation packageコマンドでパッケージを作る。また、作れたパッケージはS3のバケットにインクルードするファイルをアップロードする必要があるので、S3パケットの事前用意も必要である。

aws cloudformation packageコマンドで作成されたCloudFormationテンプレートファイルを基に、CloudFormationのスタックを作成するには、aws cloudformation create-stackコマンドもdeployコマンドもできます。

!Refなどの短縮形の組み込み関数の利用やテンプレートファイルにTransformを入れる位置によるLocationファイル名を変換される場合のテンプレートを実行にはdeployで行う必要があるようで、本文ではaws cloudformation create-stackコマンドによる無事実行できた。

hook-deploy.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxxxx
  Service:
    Type: String
    Default: xxxx
  Env:
    Type: String
    Default: dev
  App:
    Type: String
    Default: hello

  ECRName:
    Type: String
    Default: ubuntu18
  ContainerName:
    Type: String
    Default: ununtu18-container1
  CodeCommitRepositoryName:
    Type: String
    Default: codecommit-ubuntu18

  NameTagPrefix:
    Type: String
    Default: hook
    Description: Prefix of Name tags.
  ServiceName:
    Type: String
    Default: xxxx
    Description: Prefix of Service tags.

  Vpc:
    Type: AWS::EC2::VPC::Id
    Default: 'vpc-xxxxxxxxxxxxxx'
  Subnet1a:
    Type: AWS::EC2::Subnet::Id
    Default: 'subnet-xxxxxxxxxxxxxx'
  Subnet1c:
    Type: AWS::EC2::Subnet::Id
    Default: 'subnet-xxxxxxxxxxxxxx'
  LBSecurityGroup:
    Type: AWS::EC2::SecurityGroup::Id
    Default: 'sg-xxxxxxxxxxxxxx'
  S3LogBucketName:
    Type: String
    Default: xxx-xxxx-dev-hook-s3-xxxxx

  TaskExecutionRoleArn:
    Type: String
    Default: 'arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole'
  FamilyName:
    Type: String
    Default: fargate-cluster-task1
  ECSCluster:
    Type: String
    Default: 'arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/fargate-cluster-1'

# ------------------------------------------------------------#
# Transform
# ------------------------------------------------------------#
Transform:
  - 'AWS::CodeDeployBlueGreen'
Hooks:
  CodeDeployBlueGreenHook:
    Properties:
      TrafficRoutingConfig:
        Type: TimeBasedLinear
        TimeBasedLinear:
          StepPercentage: 30
          BakeTimeMins: 1
        AdditionalOptions:
          TerminationWaitTimeInMinutes: 5
      Applications:
        - Target:
            Type: 'AWS::ECS::Service'
            LogicalID: ECSService
          ECSAttributes:
            TaskDefinitions:
              - BlueTaskDefinition
              - GreenTaskDefinition
            TaskSets:
              - BlueTaskSet
              - GreenTaskSet
            TrafficRouting:
              ProdTrafficRoute:
                Type: 'AWS::ElasticLoadBalancingV2::Listener'
                LogicalID: listenerProdTraffic
#              TestTrafficRoute:
#                Type: 'AWS::ElasticLoadBalancingV2::Listener'
#                LogicalID: listenerTestTraffic
              TargetGroups:
                - lbTargetGroupBlue
                - lbTargetGroupGreen
    Type: 'AWS::CodeDeploy::BlueGreen'

# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:

  lbTargetGroupBlue:
    Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: '80'
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher:
        HttpCode: '200'
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 5
      VpcId: !Ref Vpc

  lbTargetGroupGreen:
    Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: '80'
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher:
        HttpCode: '200'
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 5
      VpcId: !Ref Vpc

  loadBalancer:
    Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
    Properties:
      Name: !Sub ${NameTagPrefix}-${Env}-${App}-alb
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: 'false'
        - Key: access_logs.s3.enabled
          Value: 'true'
        - Key: routing.http2.enabled
          Value: 'true'
        - Key: routing.http.drop_invalid_header_fields.enabled
          Value: 'false'
        - Key: idle_timeout.timeout_seconds
          Value: '60'
        - Key: access_logs.s3.bucket
          Value: !Ref S3LogBucketName
        - Key: access_logs.s3.prefix
          Value: 'prod'
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LBSecurityGroup
      Subnets:
        - !Ref Subnet1a
        - !Ref Subnet1c
      Type: application

  listenerProdTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref lbTargetGroupBlue
      LoadBalancerArn: !Ref loadBalancer
      Port: 80
      Protocol: HTTP

  listenerTestTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref lbTargetGroupGreen
      LoadBalancerArn: !Ref loadBalancer
      Port: 8080
      Protocol: HTTP

  BlueTaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      ExecutionRoleArn: !Ref TaskExecutionRoleArn
      ContainerDefinitions:
        - Name: !Ref ContainerName
          Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/ubuntu18:latest
#          Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/docker-nginx:latest
          Essential: true
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80
      RequiresCompatibilities:
        - FARGATE
      NetworkMode: awsvpc
      Cpu: '256'
      Memory: '512'
      Family: !Ref FamilyName

  ECSService:
    Type: 'AWS::ECS::Service'
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: 1
      DeploymentController:
        Type: EXTERNAL
#        Type: CODE_DEPLOY

  BlueTaskSet:
    Type: 'AWS::ECS::TaskSet'
    Properties:
      Cluster: !Ref ECSCluster
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsVpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref LBSecurityGroup
          Subnets:
            - !Ref Subnet1a
            - !Ref Subnet1c
      PlatformVersion: LATEST
      Scale:
        Unit: PERCENT
        Value: 1
      Service: !Ref ECSService
      TaskDefinition: !Ref BlueTaskDefinition
      LoadBalancers:
        - ContainerName: !Ref ContainerName
          ContainerPort: 80
          TargetGroupArn: !Ref lbTargetGroupBlue

  PrimaryTaskSet:
    Type: 'AWS::ECS::PrimaryTaskSet'
    Properties:
      Cluster: !Ref ECSCluster
      Service: !Ref ECSService
      TaskSetId: !GetAtt
        - BlueTaskSet
        - Id

2. フックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成
CodeDeploy::BlueGreen フックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成すうには--output-template-fileで指定したファイルに変換後の内容が出力される(今回は出力後のテンプレートファイルをcf-deploy-template.ymlとする。)

hook-deploy.sh
#!/bin/bash
aws cloudformation package --template-file ./hook-deploy.yml --s3-bucket xxx-xxxx-dev-hook-s3-xxxx --output-template-file cf-deploy-template.yml
cf-deploy-template.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxxxx
  Service:
    Type: String
    Default: xxxx
  Env:
    Type: String
    Default: dev
  App:
    Type: String
    Default: hello
  ECRName:
    Type: String
    Default: ubuntu18
  ContainerName:
    Type: String
    Default: ununtu18-container1
  CodeCommitRepositoryName:
    Type: String
    Default: codecommit-ubuntu18
  CodeDeployAppName:
    Type: String
    Default: AppECS-fargate-cluster-1-fargate-cluster-service1
  CodeDeployGrpName:
    Type: String
    Default: DgpECS-fargate-cluster-1-fargate-cluster-service1
  NameTagPrefix:
    Type: String
    Default: hook
    Description: Prefix of Name tags.
  ServiceName:
    Type: String
    Default: xxxx
    Description: Prefix of Service tags.
  Vpc:
    Type: AWS::EC2::VPC::Id
    Default: vpc-xxxxxxxxxxxx
  Subnet1a:
    Type: AWS::EC2::Subnet::Id
    Default: subnet-xxxxxxxxxxxx
  Subnet1c:
    Type: AWS::EC2::Subnet::Id
    Default: subnet-xxxxxxxxxxxx
  LBSecurityGroup:
    Type: AWS::EC2::SecurityGroup::Id
    Default: sg-xxxxxxxxxxxx
  S3LogBucketName:
    Type: String
    Default: xxx-xxxx-dev-hook-s3-xxxxxx
  TaskExecutionRoleArn:
    Type: String
    Default: arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole
  FamilyName:
    Type: String
    Default: fargate-cluster-task1
  ECSCluster:
    Type: String
    Default: arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/fargate-cluster-1
Transform:
- AWS::CodeDeployBlueGreen
Hooks:
  CodeDeployBlueGreenHook:
    Properties:
      TrafficRoutingConfig:
        Type: TimeBasedLinear
        TimeBasedLinear:
          StepPercentage: 30
          BakeTimeMins: 1
        AdditionalOptions:
          TerminationWaitTimeInMinutes: 5
      Applications:
      - Target:
          Type: AWS::ECS::Service
          LogicalID: ECSService
        ECSAttributes:
          TaskDefinitions:
          - BlueTaskDefinition
          - GreenTaskDefinition
          TaskSets:
          - BlueTaskSet
          - GreenTaskSet
          TrafficRouting:
            ProdTrafficRoute:
              Type: AWS::ElasticLoadBalancingV2::Listener
              LogicalID: listenerProdTraffic
            TargetGroups:
            - lbTargetGroupBlue
            - lbTargetGroupGreen
    Type: AWS::CodeDeploy::BlueGreen
Resources:
  lbTargetGroupBlue:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: '80'
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher:
        HttpCode: '200'
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 5
      VpcId:
        Ref: Vpc
  lbTargetGroupGreen:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: '80'
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher:
        HttpCode: '200'
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 5
      VpcId:
        Ref: Vpc
  loadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name:
        Fn::Sub: ${NameTagPrefix}-${Env}-${App}-alb
      LoadBalancerAttributes:
      - Key: deletion_protection.enabled
        Value: 'false'
      - Key: access_logs.s3.enabled
        Value: 'true'
      - Key: routing.http2.enabled
        Value: 'true'
      - Key: routing.http.drop_invalid_header_fields.enabled
        Value: 'false'
      - Key: idle_timeout.timeout_seconds
        Value: '60'
      - Key: access_logs.s3.bucket
        Value:
          Ref: S3LogBucketName
      - Key: access_logs.s3.prefix
        Value: prod
      Scheme: internet-facing
      SecurityGroups:
      - Ref: LBSecurityGroup
      Subnets:
      - Ref: Subnet1a
      - Ref: Subnet1c
      Type: application
  listenerProdTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
      - Type: forward
        TargetGroupArn:
          Ref: lbTargetGroupBlue
      LoadBalancerArn:
        Ref: loadBalancer
      Port: 80
      Protocol: HTTP
  listenerTestTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
      - Type: forward
        TargetGroupArn:
          Ref: lbTargetGroupGreen
      LoadBalancerArn:
        Ref: loadBalancer
      Port: 8080
      Protocol: HTTP
  BlueTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ExecutionRoleArn:
        Ref: TaskExecutionRoleArn
      ContainerDefinitions:
      - Name:
          Ref: ContainerName
        Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/ubuntu18:latest
        Essential: true
        PortMappings:
        - HostPort: 80
          Protocol: tcp
          ContainerPort: 80
      RequiresCompatibilities:
      - FARGATE
      NetworkMode: awsvpc
      Cpu: '256'
      Memory: '512'
      Family:
        Ref: FamilyName
  ECSService:
    Type: AWS::ECS::Service
    Properties:
      Cluster:
        Ref: ECSCluster
      DesiredCount: 1
      DeploymentController:
        Type: EXTERNAL
  BlueTaskSet:
    Type: AWS::ECS::TaskSet
    Properties:
      Cluster:
        Ref: ECSCluster
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsVpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
          - Ref: LBSecurityGroup
          Subnets:
          - Ref: Subnet1a
          - Ref: Subnet1c
      PlatformVersion: LATEST
      Scale:
        Unit: PERCENT
        Value: 1
      Service:
        Ref: ECSService
      TaskDefinition:
        Ref: BlueTaskDefinition
      LoadBalancers:
      - ContainerName:
          Ref: ContainerName
        ContainerPort: 80
        TargetGroupArn:
          Ref: lbTargetGroupBlue
  PrimaryTaskSet:
    Type: AWS::ECS::PrimaryTaskSet
    Properties:
      Cluster:
        Ref: ECSCluster
      Service:
        Ref: ECSService
      TaskSetId:
        Fn::GetAtt:
        - BlueTaskSet
        - Id

3. ECSタスク定義などのDeployスタックを作成

作成されたcf-tするにフックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成する。
このテンプレートファイルには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。さらにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPANDの指定も必要である。

dly.sh
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode - create-stack..."
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-deploy
# テンプレートの実⾏
aws cloudformation create-stack --stack-name ${CFN_STACK_NAME} --template-body file://./${CFN_TEMPLATE} ${CHANGESET_OPTION} \
    --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND

./dly.sh deploy cf-deploy-template.yml

4. CodePipelineスタックを作成

以下のYmlファイルとaws cliによるCloudFormationのCodePipelineを作成する。

hook-pipeline.yml
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with CodeDeploy Hook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxxxx
  Service:
    Type: String
    Default: xxxx
  Env:
    Type: String
    Default: dev
  App:
    Type: String
    Default: hello

  ECRName:
    Type: String
    Default: ubuntu18
  TemplateName:
    Type: String
    Default: cf-template
  ContainerName:
    Type: String
    Default: ununtu18-container1
  CodeCommitRepositoryName:
    Type: String
    Default: codecommit-ubuntu18

  ServiceName:
    Type: String
    Default: xxxx
    Description: Prefix of Service tags.

# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#

Resources:

  # CodeWatchEventを実行できるIAMRole
  CloudwatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub  xxx-cwe-${Service}-${Env}-hook-role
      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 xxx-codebuild-${Service}-${Env}-hook-role
      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: '*'

  # CloudFormationに適用するIAMRole
  CFnServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub xxx-cfn-${Service}-${Env}-hook-role
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SampleCloudFormationPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                  - codedeploy:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - sns:*
                  - cloudformation:*
                  - ecs:*
                Resource: '*'
                Effect: Allow

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub xxx-codepipeline-${Service}-${Env}-hook-role
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: xxx-CodePipelineHookPolicy
          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

              - Resource: '*'
                Effect: Allow
                Action:
                  - ecr:DescribeImages

              - 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

  # CodeDeployに適用するIAMRole
  CodeDeployRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'codedeploy.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
      RoleName: !Sub xxx-codedeploy-${Service}-${Env}-hook-role

  # 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 Updating CloudFormation Template...
              - sed -i -e "s|$REPOSITORY_URI:.*$|$REPOSITORY_URI:$IMAGE_TAG|" $TEMPLATE_NAME.yml
          artifacts:
            files: $TEMPLATE_NAME.yml
      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: TEMPLATE_NAME
            Value: !Ref TemplateName
          - Name: DOCKER_BUILDKIT
            Value: '1'
      Name: !Ref AWS::StackName

  # ------------------------------------------------------------#
  # CodePipeline 
  # ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub xxx-${ServiceName}-hook-pipeline1
      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-xxxxxxxx:xxxxxxxx:hogehoge
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: '1'
                Provider: CloudFormation
              InputArtifacts:
#                - Name: App
                - Name: BuildOutput
              Configuration:
                ActionMode: CREATE_UPDATE
                Capabilities: CAPABILITY_AUTO_EXPAND
                RoleArn: !GetAtt CFnServiceRole.Arn
#                StackName: hook-pipeline
                StackName: hook-deploy
                TemplatePath: !Sub 'BuildOutput::${TemplateName}.yml'
              RunOrder: 1
              Region: !Ref AWS::Region

# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PipelineDev:
    Description: Dev xxxx Pipeline
    Value: !Ref Pipeline
    Export:
      Name: hook-DevxxxxAppPipeline
pip.sh
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-pipeline![hook-pipeline-hello-blue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/2916287f-ae7b-b582-9626-57ff64a3264b.jpeg)

# テンプレートの実⾏
 aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION} \
    --capabilities CAPABILITY_NAMED_IAM --parameter-overrides `cat parameters.hook`

CloudFormationスタックを作成するコマンドは下記になる。
./pip.sh deploy hook-pipeline.yml

このテンプレートには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。また、テンプレートにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPANDの指定も必要です

結果確認

  1. 現用系(ブルー)アプリへのアクセス スタックが作成されたら、ECSのサービス及びタスクを調べて、作成されたコンテナまたはALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。

hook-pipeline-hello-blue.jpg

2.開発系(グリーン)アプリを作成(タスク変更)
TaskDefinition に変更が加えられたらそれがフックされ、デプロイが走るので、開発系のコンテナに切り替えます。

CodeCommitレポジトリのDockerfile中身を変更する。

RUN echo 'Hello World - BLUE!' > /var/www/html/index.htmlから
RUN echo 'Hello World - GREEN!' > /var/www/html/index.htmlへ変更。

hook-pipeline-codecommit-ubuntu18.jpg

3.CodeDeploymentの実施と開発系(グリーン)に切り替え

実行すると、CloudFormation のマクロが変更セットを作成して、CodeDeploy や グリーン系への切り替えなどを始めます。

hook-pipeline-deploy.jpg

4.開発系(グリーン)へのアクセス
開発系のコンテナに切り替え後、再び。ALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。

hook-pipeline-hello-green.jpg

5.(オプション)update-stackによる開発系(グリーン)への切り替え
“2.開発系(グリーン)アプリを作成(タスク変更)”の他、deployスタックのアップデートによる開発系アプリの作成及びグリーン系への切り替えもできる。

dly-update.sh
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode - update-stack..."
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-deploy
# テンプレートの実⾏
aws cloudformation update-stack --stack-name ${CFN_STACK_NAME} --template-body file://./${CFN_TEMPLATE} ${CHANGESET_OPTION} \
    --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND

所感

今回、CloudFormationの CodePipelineとCodeDeploy::BlueGreenフックをインテグレートにより、ECS FargateのBlue/Greenサービスデプロイを構築検証した。テスク定義の変更によるECSのBule/Green CodeDeployが動くことを確認できた。ただ、CodeDeploy::BlueGreenフックリソース定義、CloudFormationテンプレートファイルの作成及びCodePipelineの作成はちょっと手間をかかり、実際複雑な商用システムでは、CodeDeploy::BlueGreenフックとCodePipelineの連携する方法は柔軟性に十分ではないかと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0