CloudFormation 一撃で EC2 の Blue/Green Deployment の CodePipeline を構築する

2021.04.29

はじめに

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

皆さん、Blue/Green (以下B/G)デプロイメントしてますか?

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

今さらなのですが EC2 の B/G デプロイを実行する CodePipeline を構築する機会があったのでまとめておきます。

CloudFormation の CodeDeploy デプロイメントグループは現在(2021/4/29)、EC2 AutoScaling の対応をしていないため、カスタムリソースで構築します。

AWS::CodeDeploy::DeploymentGroup - AWS CloudFormation

ブルー/グリーンデプロイの場合、AWSCloudFormationはLambdaコンピューティングプラットフォームでのデプロイのみをサポートします。AWS::CodeDeploy::BlueGreen フックを使用して、ECSのBlue/Greenの展開を実行できます。

カスタムリソースとは

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

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

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

EC2 の Blue Green デプロイメントについて

EC2のB/Gデプロイでは、CodeDeployを利用して、自動切り替えと手動による切り替えと、AutoScalingGroup(以下 ASG)自動コピーによる切り替えとASG、タグベースによるEC2、オンプレの任意組み合わせによる切り替えの2つがあります。

手動切り替えの場合、CodeDeploy のデプロイメントグループにてリプレース対象の指定、デプロイ設定でデプロイ先のリソースを指定します。

まとめると以下表のようになります。

実行方法 パターン1 パターン2 パターン3
自動 ASG自動コピー なし なし
手動 ASG切り替え 非ASG (EC2 with Tag) オンプレミス

手動実行のフローはCodeDeploy で非 AutoScaling のインスタンスへの Blue/Green デプロイを試してみた | DevelopersIO の図を拝借すると以下のように実行されます。

事前に置き換え後のリソースを作成しておく必要があります。

なお、CodePipelineのように自動でデプロイを実行させる場合は ASG のコピーが必須です。

下図のようにデプロイ先のリソースが存在しないというエラーが発生します。

deploy_failed

自動実行のフローはCodeDeploy のデプロイ方式に Blue/Green Deployment が追加されました | DevelopersIO の図を拝借すると以下のように実行されます。

自動、手動のどちらの場合でも、仮にロールバックした後は新規作成したリソースを手動で削除する必要があります。

結果のイメージ

元の状態

blue_instance

リプレースされた状態

green_instance

構成図

今回構成する図は以下です。

本来は ALB が配置されるパブリックサブネットとインスタンスが配置されるプライベートサブネットとで分離するのが自然ですが、本稿ではインスタンスもALBと同じパブリックサブネットに配置することを想定しています。

ec2_asg_bg_deploy2

前提条件

  • VPC, サブネット, セキュリティグループが作成済み, インターネット通信可能であること
  • AutoScalingで利用するためのAMIが作成済み、CodeDeploy エージェントのインストール がされていること

  • CodeCommitが作成済み

スタック作成時に以下パラメーターの入力が必須です。

(その他デフォルト指定しているパラメータもあります)

パラメーター名 説明
VpcId 構築に使用するVPC ID
AlbSecurityGroupId ALBで使用するセキュリティグループID
AlbSubnetId1 ALBで使用するサブネットID
AlbSubnetId2 ALBで使用するサブネットID
LaunchConfigImageId Launch Configに指定するAMI ID
Ec2KeyPair EC2で使用するキーペアを指定
AppSecurityGroupId EC2で使用するセキュリティグループID
AppSubnetId1 EC2が配置されるサブネットID(ALBと同じサブネットを使用します)
AppSubnetId2 EC2が配置されるサブネットID(ALBと同じサブネットを使用します)
CodeCommitRepositoryName 使用するCodeCommitリポジトリの名前

準備

CodeCommitに以下をプッシュします。

なお、CodePipelineによる自動デプロイではファイル上書きデプロイを設定できないので、必要に応じて appspec.ymlで元のファイルを削除するように対応します。

  • ソースコード(index.html, hello.conf)
  • appspec.yml
  • (本稿では beforeInstall.sh を利用)

ちなみに、index.html や hello.conf の素材は こちら を使っています。

参考

## appspec.yml

version: 0.0
os: linux
files:
  - source: ./hello.conf
    destination: /etc/nginx/conf.d/
  - source: ./index.html
    destination: /usr/share/nginx/html/
    hooks:
    BeforeInstall:
    - location: ./beforeInstall.sh
## beforeInstall.sh
#!/bin/sh
if [ -e /usr/share/nginx/html/index.html ]; then
    rm /usr/share/nginx/html/index.html
fi
if [ -e /etc/nginx/conf.d/hello.conf ]; then
    rm /etc/nginx/conf.d/hello.conf
fi

Cloudformationテンプレート

注記

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

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

  • トラフィックを再ルーティングさせるのは5分後です。
  • デプロイ設定は CodeDeployDefault.AllAtOnce です。
  • 元インスタンスは15分後に削除します。
  • デプロイが失敗したらロールバックします。

Working with deployment configurations in CodeDeploy - AWS CodeDeploy

このパイプライン上の Build ステージでは 何もしていません。ご利用する環境や言語に応じて、適切なビルドやテストを導入するようにして下さい。

CodeBuild のビルド仕様に関するリファレンス - AWS CodeBuild

承認ステージはコメントアウトしています。必要に応じて SNS Topic Arn をパラメータに設定して利用して下さい。

AWSTemplateFormatVersion: '2010-09-09'
Description: AutoScaling Group and CodeDeploy for Blue/Green Pipeline Create.
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  ProjectName:
    Default: 'demo'
    Type: String

  VpcId:
    Description: 'VPC ID'
    Type: AWS::EC2::VPC::Id

  AlbSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id

  AlbSubnetId1:
    Description: 'Alb Subnet 1st'
    Type: AWS::EC2::Subnet::Id

  AlbSubnetId2:
    Description: 'Alb Subnet 2st'
    Type: AWS::EC2::Subnet::Id

  LaunchConfigImageId:
    Description: 'Enter ami-id'
    Type: AWS::EC2::Image::Id

  Ec2KeyPair:
    Description: 'Key Pair for EC2'
    Type: AWS::EC2::KeyPair::KeyName

  InstanceType:
    Description: Enter InstanceType
    Type: String
    Default: 't2.micro'

  AppSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id

  #PublicSubnet1
  AppSubnetId1:
    Description: 'App Subnet 1st'
    Type: AWS::EC2::Subnet::Id

  #PublicSubnet2
  AppSubnetId2:
    Description: 'App Subnet 2st'
    Type: AWS::EC2::Subnet::Id

  InternetAlbName:
    Type: String
    Default: 'Alb'

  TargetGroupName:
    Type: String
    Default: 'tg'

  CodeDeployAppName:
    Type: String
    Default: 'app'

  CodeDeployDeploymentGroupName:
    Type: String
    Default: 'dg'

  DeploymentConfigName:
    Type: String
    Default: 'CodeDeployDefault.AllAtOnce'

  CodeCommitRepositoryName:
    Type: String

  BranchName:
    Type: String
    Default: 'master'

  # SNS Topic Arn
  # NotificationArn:
  # Type: String

Resources:
  # ------------------------------------------------------------#
  #  Target Group
  # ------------------------------------------------------------#
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VpcId
      Name: !Sub ${ProjectName}-${TargetGroupName}
      Protocol: HTTP
      HealthCheckPath: '/'
      Port: 80
      TargetType: instance
  # ------------------------------------------------------------#
  #  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
  AlbListener:
    Type: 'AWS::ElasticLoadBalancingV2::Listener'
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref InternetAlb
      Port: 80
      Protocol: HTTP
  # ------------------------------------------------------------#
  # Iam Roles
  # ------------------------------------------------------------#
  # EC2に適用するIAMRole
  Ec2IamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'ec2.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Policies:
        - PolicyName: !Sub ${ProjectName}-CodeDeployForEC2Policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: 'Allow'
                Action:
                  - 's3:Get*'
                  - 's3:List*'
      RoleName: !Sub ${ProjectName}-ec2-role
  IamInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: '/'
      Roles:
        - !Ref Ec2IamRole

  # 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/service-role/AWSCodeDeployRole
      RoleName: !Sub ${ProjectName}-codedeploy-role

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ProjectName}-${CodeDeployAppName}-CodePipelineServiceRole
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub ${ProjectName}-${CodeDeployAppName}-CodePipelineServicePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - ec2.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:
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - sns:*
                  - cloudformation:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuildBatches
                  - codebuild:StartBuildBatch
                Resource: '*'
                Effect: Allow

  # CodeWatchEventを実行するIAMRole
  CloudwatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ProjectName}-${CodeDeployAppName}-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 ${ProjectName}-${CodeDeployAppName}-CodeBuildServiceRole
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub ${ProjectName}-${CodeDeployAppName}-CodeBuildServicePolicy
          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
                Resource: '*'
  # ------------------------------------------------------------#
  #  Auto Scaling Service
  # ------------------------------------------------------------#
  LaunchConfiguration:
    Type: AWS::AutoScaling::LaunchConfiguration
    Properties:
      AssociatePublicIpAddress: true
      IamInstanceProfile: !Ref IamInstanceProfile
      ImageId: !Ref LaunchConfigImageId
      InstanceMonitoring: false
      InstanceType: !Ref InstanceType
      KeyName: !Ref Ec2KeyPair
      LaunchConfigurationName: !Sub ${ProjectName}-lcg
      SecurityGroups:
        - !Ref AppSecurityGroupId
  AutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName: !Sub ${ProjectName}-asg
      LaunchConfigurationName: !Ref LaunchConfiguration
      DesiredCapacity: '1'
      MaxSize: '1'
      MinSize: '1'
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-asg-ec2
          PropagateAtLaunch: true
      TargetGroupARNs:
        - !Ref TargetGroup
      VPCZoneIdentifier:
        - !Ref AppSubnetId1
        - !Ref AppSubnetId2
  # ------------------------------------------------------------#
  # 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']
              autoScalingGroups = event['ResourceProperties']['autoScalingGroups']
              serviceRoleArn = event['ResourceProperties']['serviceRoleArn']
              deploymentConfigName = event['ResourceProperties']['deploymentConfigName']

              print('REQUEST RECEIVED:\n' + json.dumps(event))
              responseData = {}
              try:
                res = client.get_application(applicationName=appName)
                if res['ResponseMetadata']['HTTPStatusCode'] == 200:
                    res = client.delete_application(
                        applicationName=appName
                    )
                    logger.info(res)
                    logger.info("SUCCESS: Existed CodeDeploy Application deleted.")
              except:
                pass
              try:
                res = client.create_application(
                    applicationName=appName,
                    computePlatform='Server'
                )
                logger.info(res)
                logger.info("SUCCESS: CodeDeploy Application created.")
                res = client.create_deployment_group(
                    applicationName=appName,
                    deploymentGroupName=deploymentGroup,
                    autoScalingGroups=[
                        autoScalingGroups,
                    ],
                    deploymentConfigName=deploymentConfigName,
                    serviceRoleArn=serviceRoleArn,
                    autoRollbackConfiguration={
                        'enabled': True,
                        'events': [
                            'DEPLOYMENT_FAILURE',
                        ]
                    },
                    deploymentStyle={
                        'deploymentType': 'BLUE_GREEN',
                        'deploymentOption': 'WITH_TRAFFIC_CONTROL'
                    },
                    blueGreenDeploymentConfiguration={
                        'terminateBlueInstancesOnDeploymentSuccess': {
                            'action': 'TERMINATE',
                            'terminationWaitTimeInMinutes': 15
                        },
                        'deploymentReadyOption': {
                            'actionOnTimeout': 'STOP_DEPLOYMENT',
                            'waitTimeInMinutes': 5
                        },
                        'greenFleetProvisioningOption': {
                            'action': 'COPY_AUTO_SCALING_GROUP'
                        }
                    },
                    loadBalancerInfo={
                        'targetGroupInfoList': [
                                    {
                                        'name': event['ResourceProperties']['TargetGroup']
                                    },
                          ]
                    },
                )
              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.")
                  responseData = {'success': str(res)}
                  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:
      - AutoScalingGroup
    Properties:
      ServiceToken: !GetAtt LambdaFunction.Arn
      Region: !Ref AWS::Region
      autoScalingGroups: !Sub '${ProjectName}-asg'
      deploymentConfigName: !Ref DeploymentConfigName
      serviceRoleArn: !GetAtt CodeDeployRole.Arn
      TargetGroup: !Sub '${ProjectName}-${TargetGroupName}'
      appName: !Sub '${ProjectName}-${CodeDeployAppName}'
      deploymentGroup: !Sub '${ProjectName}-${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
  # ------------------------------------------------------------#
  # Alb 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 S3 Artifact Bucket
  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:
            - !Ref BranchName
      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:
            install:
              commands:
                - echo Entered the install phase...
                - echo Please Something to do in the install phase...
            pre_build:
              commands:
                - echo Entered the pre_build phase...
                - echo Please Something to do in the pre_build phase...
            build:
              commands:
                - echo Entered the build phase...
                - echo Build started on `date`
                - echo Please Something to do in the build phase...
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Please Something to do in the post_build phase...
          artifacts:
            files:
              - '**/*'
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: DOCKER_BUILDKIT
            Value: '1'
      Name: !Ref AWS::StackName
  # ------------------------------------------------------------#
  # CodePipeline
  # ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub ${ProjectName}-${CodeDeployAppName}-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: !Ref BranchName
              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 '${CodeDeployAppName} will be updated. Do you want to deploy it?'
        #         NotificationArn: !Ref NotificationArn
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: '1'
                Provider: CodeDeploy
              Configuration:
                ApplicationName: !Sub '${ProjectName}-${CodeDeployAppName}'
                DeploymentGroupName: !Sub '${ProjectName}-${CodeDeployDeploymentGroupName}'
              RunOrder: 1
              InputArtifacts:
                - Name: App
              Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  InternetAlbUrl:
    Description: URL of the alb
    Value: !Join ['', ['http://', !GetAtt InternetAlb.DNSName]]
  PipelinelogicalID:
    Description: logical ID.
    Value: !Ref Pipeline

さいごに

なかなかEC2のB/Gデプロイをする機会もなくなってきているかと思いますが、利用する方がいましたら参考になれば幸いです。

参考