CodeDeployでAutoScalingグループにデプロイする設定をCloudFormationで作成してみた

2023.01.24

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

前回のブログで単体で起動しているEC2にデプロイするCI/CD環境をCloudFormationで作成しました。

今回はAutoScalingでスケールアウト、スケールインする環境にデプロイする設定をCloudFormationで作成してみます。

作成するもの

殆ど前回と同じものを作成します。
追加されるのはALB、AutoScalingグループ、ALBでバランシングするサブネット
設定が変わるのがCodeDeploy、CodePipelineになります
前回はCodePipelineの一連の流れが知りたかったのでCodeBuildを作成しましたが、今回はCodeDeployがメインになるのでビルドフェーズは作成しません。

  • アーティファクト用S3バケット
  • CodeCommit
  • CodeDeploy
  • CodePipeline
  • CodeDeploy用サービスロール
  • CodePipeline用サービスロール
  • CodePipelineを動かすためのEventBridgeルール
  • EC2(AutoScalingグループ管理)用IAMロール
  • デプロイ先のAutoScalingグループ、ゴールデンAMI作成用EC2、VPC、サブネット、セキュリティグループ、NATゲートウェイなど

構成図にすると以下のようになります。

今回はEC2をプライベートサブネットに配置するのでパブリックサブネットに1つNATゲートウェイを配置しています。

今回のブログではCodeCommitにgit pushできるIAMユーザの作成は記載していません。
以下の公式ドキュメントをご確認いただき作成してください。
CodeCommit での IAM の使用: Git 認証情報、SSH キー、および AWS アクセスキー

作成してみた

IAMロール + アーティファクト用S3 + ネットワーク周り + ゴールデンAMI作成用EC2

作成したCloudFormationテンプレートは以下になります。

CloudFormationテンプレート (ここをクリックしてください)
AWSTemplateFormatVersion: "2010-09-09"

Description: CI/CD test Stack

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for VPC
        Parameters:
          - VPCCIDR
      - Label: 
          default: Parameters for Subnet
        Parameters:
          - PublicSubnet01CIDR
          - PublicSubnet02CIDR
          - PrivateSubnet01CIDR
          - PrivateSubnet02CIDR
      - Label: 
          default: Parameters for ec2
        Parameters:
          - EC2VolumeSize
          - EC2VolumeIOPS
          - EC2AMI
          - EC2InstanceType

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  VPCCIDR:
    Default: 172.30.0.0/16
    Type: String

  PublicSubnet01CIDR:
    Default: 172.30.1.0/24
    Type: String

  PublicSubnet02CIDR:
    Default: 172.30.2.0/24
    Type: String

  PrivateSubnet01CIDR:
    Default: 172.30.3.0/24
    Type: String

  PrivateSubnet02CIDR:
    Default: 172.30.4.0/24
    Type: String

  EC2VolumeSize:
    Default: 32
    Type: Number

  EC2VolumeIOPS:
    Default: 3000
    Type: Number

  EC2AMI:
    Default: ami-0bba69335379e17f8
    Type: AWS::EC2::Image::Id

  EC2InstanceType:
    Default: t3.micro
    Type: String

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------# 
  S3:
    Type: AWS::S3::Bucket
    Properties: 
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Sub ${AWS::StackName}-${AWS::AccountId}-artifact
      OwnershipControls:
        Rules: 
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${AWS::AccountId}-artifact

# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  EC2IAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - "s3:GetObject"
              - "s3:ListBucket"
            Resource: 
              - !Join 
                - ''
                - - !GetAtt S3.Arn
                  - '/*'
              - !GetAtt S3.Arn
      ManagedPolicyName: iam-policy-deploy-ec2

  EC2IAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - ec2.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Ref EC2IAMPolicy
      RoleName: iam-role-ec2
      Tags:
        - Key: Name
          Value: iam-role-ec2

  EC2IAMInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: iam-instanceprofile-ec2
      Roles: 
        - !Ref EC2IAMRole

  CodeDeployIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - codedeploy.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
      RoleName: iam-role-codedeploy
      Tags:
        - Key: Name
          Value: iam-role-codedeploy

  CodePipelineIAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            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"
            Resource: 
              - "*"
          - Effect: Allow
            Action:
              - "s3:GetObject"
              - "s3:PutObject"
              - "s3:ListBucket"
            Resource: 
              - !Join 
                - ''
                - - !GetAtt S3.Arn
                  - '/*'
              - !GetAtt S3.Arn
          - Effect: Allow
            Action:
              - "sns:Publish"
            Resource: 
              - "*"
      ManagedPolicyName: iam-policy-codepipeline

  CodePipelineIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - codepipeline.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - !Ref CodePipelineIAMPolicy
      RoleName: iam-role-codepipeline
      Tags:
        - Key: Name
          Value: iam-role-codepipeline

# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------# 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: test-vpc

# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------# 
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
        - Key: Name
          Value: !Sub test-igw

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------# 
  PublicSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PublicSubnet01CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub test-public-subnet-01
      VpcId: !Ref VPC

  PublicSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref PublicSubnet02CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub test-public-subnet-02
      VpcId: !Ref VPC

  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnet01CIDR
      MapPublicIpOnLaunch: false
      Tags: 
        - Key: Name
          Value: !Sub test-private-subnet-01
      VpcId: !Ref VPC

  PrivateSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref PrivateSubnet02CIDR
      MapPublicIpOnLaunch: false
      Tags: 
        - Key: Name
          Value: !Sub test-private-subnet-02
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# NatGateWay
# ------------------------------------------------------------# 
  NatGateWayEIP:
    Type: AWS::EC2::EIP
    Properties: 
      Domain: vpc
      Tags:
        - Key: Name
          Value: eip-natgw

  NatGateWay:
    Type: AWS::EC2::NatGateway
    Properties: 
      AllocationId: !GetAtt NatGateWayEIP.AllocationId
      ConnectivityType: public
      SubnetId: !Ref PublicSubnet01
      Tags:
        - Key: Name
          Value: test-natgw-1a

# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------# 
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: test-public-rtb

  PublicRouteTableRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicRouteTable

  PublicRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet01

  PublicRtAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet02

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: test-private-rtb

  PrivateRouteTableRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateWay
      RouteTableId: !Ref PrivateRouteTable

  PrivateRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet01

  PrivateRtAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet02

# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------# 
  ALBSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for alb
      GroupName: test-sg-alb
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - CidrIp: 0.0.0.0/0
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
      Tags: 
        - Key: Name
          Value: test-sg-alb
      VpcId: !Ref VPC

  EC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for ec2
      GroupName: test-sg-ec2-web
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress:
        - FromPort: 80
          IpProtocol: tcp
          SourceSecurityGroupId: !Ref ALBSG
          ToPort: 80
      Tags: 
        - Key: Name
          Value: test-sg-ec2-web
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------# 
  EC2:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: true
            Iops: !Ref EC2VolumeIOPS
            VolumeSize: !Ref EC2VolumeSize
            VolumeType: gp3
      DisableApiTermination: false
      IamInstanceProfile: !Ref EC2IAMInstanceProfile
      ImageId: !Ref EC2AMI
      InstanceType: !Ref EC2InstanceType
      NetworkInterfaces: 
        - DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet: 
            - !Ref EC2SG
          SubnetId: !Ref PrivateSubnet01
      Tags:
        - Key: Name
          Value: test-ec2
      UserData: !Base64 |
        #!/bin/bash
        yum update -y
        yum install ruby -y
        wget https://aws-codedeploy-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/install
        chmod +x ./install
        ./install auto
        service codedeploy-agent start
        yum install httpd -y
        echo "CodeDeploy Test" > /var/www/html/index.html
        systemctl start httpd
        systemctl enable httpd

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  CodeDeployIAMRoleARN:
    Value: !GetAtt CodeDeployIAMRole.Arn
    Export: 
      Name: iam-role-codedeploy-arn

  CodePipelineIAMRoleARN:
    Value: !GetAtt CodePipelineIAMRole.Arn
    Export: 
      Name: iam-role-codepipeline-arn

  S3Name:
    Value: !Ref S3
    Export: 
      Name: S3Name

  ALBSGName:
    Value: !Ref ALBSG
    Export: 
      Name: ALBSGName

  EC2SGName:
    Value: !Ref EC2SG
    Export: 
      Name: EC2SGName

  PublicSubnet01ID:
    Value: !Ref PublicSubnet01
    Export: 
      Name: PublicSubnet01ID

  PublicSubnet02ID:
    Value: !Ref PublicSubnet02
    Export: 
      Name: PublicSubnet02ID

  PrivateSubnet01ID:
    Value: !Ref PrivateSubnet01
    Export: 
      Name: PrivateSubnet01ID

  PrivateSubnet02ID:
    Value: !Ref PrivateSubnet02
    Export: 
      Name: PrivateSubnet02ID

こちらのCloudFormationテンプレートをデプロイするとVPCなどのネットワーク周り、CI/CDで使用するIAMロール、アーティファクト用S3、プライベートサブネットに1台EC2が作成されます。
EC2はCodeDeployエージェントをUserDataでインストールするようにしています。
デプロイは以下のコマンドを実行します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --capabilities CAPABILITY_NAMED_IAM

デプロイが完了したら以下のコマンドを実行してEC2のAMIを作成します。
実行するとAMI IDが出力されます。
こちらはAutoScalingで使用する起動テンプレートを作成する際に使用します。

aws ec2 create-image --instance-id インスタンスID --name AMIに付ける名前

ALB + AutoScalingグループ

ここではALBとAutoScalingグループを作成します。
作成したCloudFormationテンプレートは以下になります。

AWSTemplateFormatVersion: "2010-09-09"

Description: AutoScaling Stack

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  EC2VolumeSize:
    Default: 32
    Type: Number

  EC2VolumeIOPS:
    Default: 3000
    Type: Number

  EC2AMI:
    Type: AWS::EC2::Image::Id

  EC2InstanceType:
    Default: t3.micro
    Type: String

Resources:
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------# 
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: false
      Name: test-alb
      Scheme: internet-facing
      SecurityGroups:
        - !ImportValue ALBSGName
      Subnets: 
        - !ImportValue PublicSubnet01ID
        - !ImportValue PublicSubnet02ID
      Tags: 
        - Key: Name
          Value: test-alb
      Type: application

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Matcher:
        HttpCode: 200
      Name: test-alb-tg
      Port: 80
      Protocol: HTTP
      ProtocolVersion: HTTP1
      Tags: 
        - Key: Name
          Value: test-alb-tg
      TargetType: instance
      UnhealthyThresholdCount: 2
      VpcId: !ImportValue VPC

  ALBHTTPListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP

# ------------------------------------------------------------#
# AutoScaling
# ------------------------------------------------------------# 
  LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties: 
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              DeleteOnTermination: true
              Encrypted: true
              Iops: !Ref EC2VolumeIOPS
              Throughput: 125
              VolumeSize: !Ref EC2VolumeSize
              VolumeType: gp3
        IamInstanceProfile: 
          Name: !ImportValue EC2IAMInstanceProfile
        ImageId: !Ref EC2AMI
        InstanceType: !Ref EC2InstanceType
        NetworkInterfaces: 
          - AssociatePublicIpAddress: false
            DeleteOnTermination: true
            DeviceIndex: 0
            Groups: 
              - !ImportValue EC2SGName
            SubnetId: !ImportValue PrivateSubnet01ID
        TagSpecifications: 
          - ResourceType: instance
            Tags: 
              - Key: Name
                Value: test-ec2
      LaunchTemplateName: test-ec2-lt

  AutoScalingGloup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties: 
      AutoScalingGroupName: test-ec2-asg
      AvailabilityZones:
        - ap-northeast-1a
        - ap-northeast-1c
      DesiredCapacity: 2
      HealthCheckGracePeriod: 300
      HealthCheckType: ELB
      LaunchTemplate: 
        LaunchTemplateId: !Ref LaunchTemplate
        Version: 1
      MaxSize: 4
      MetricsCollection:
        - Granularity: 1Minute
      MinSize: 2
      TargetGroupARNs: 
        - !Ref TargetGroup
      VPCZoneIdentifier: 
        - !ImportValue PrivateSubnet01ID
        - !ImportValue PrivateSubnet02ID

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  AutoScalingGloupName:
    Value: !Ref AutoScalingGloup
    Export: 
      Name: AutoScalingGloupName

28行目から79行目でALBとターゲットグループを作成しています。
AutoScalingでEC2を起動するのでターゲットグループにEC2と紐づける記載はありません。
84行目から136行目で起動テンプレートとAutoScalingグループを作成しています。
AutoScalingグループ作成時は最小2台の状態でEC2が起動するようになっています。
デプロイは以下のコマンドを実行します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=EC2AMI,ParameterValue=AMI ID

CodeCommit

CodeCommitにつきましては前回と同じCloudFormationテンプレートと手順になりますので以下のブログをご確認ください。

CodeDeploy

CodeDeployはAutoScalingグループにデプロイできるように設定していきます。
作成したCloudFormationテンプレートは以下になります。

AWSTemplateFormatVersion: "2010-09-09"

Description: CodeDeploy Stack

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for CodeDeploy
        Parameters:
          - ApplicationName
          - DeploymentGroupName

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  ApplicationName:
    MaxLength: 100
    Type: String

  DeploymentGroupName:
    MaxLength: 100
    Type: String

Resources:
# ------------------------------------------------------------#
# CodeDeploy
# ------------------------------------------------------------# 
  CodeDeployApplication:
    Type: AWS::CodeDeploy::Application
    Properties:
      ApplicationName: !Ref ApplicationName
      ComputePlatform: Server
      Tags: 
        - Key: Name
          Value: !Ref ApplicationName

  CodeDeployGroup:
    Type: AWS::CodeDeploy::DeploymentGroup
    Properties:
      ApplicationName: !Ref CodeDeployApplication
      AutoRollbackConfiguration: 
        Enabled: true
        Events:
          - DEPLOYMENT_FAILURE
      AutoScalingGroups:
        - !ImportValue AutoScalingGloupName
      DeploymentConfigName: CodeDeployDefault.HalfAtATime
      DeploymentGroupName: !Ref DeploymentGroupName
      DeploymentStyle:
        DeploymentOption: WITH_TRAFFIC_CONTROL
        DeploymentType: IN_PLACE
      LoadBalancerInfo: 
        TargetGroupInfoList:
          - Name: !ImportValue TargetGroupName
      OutdatedInstancesStrategy: UPDATE
      ServiceRoleArn: !ImportValue iam-role-codedeploy-arn
      Tags:
        - Key: Name
          Value: !Ref DeploymentGroupName

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  CodeDeployApplicationName:
    Value: !Ref CodeDeployApplication
    Export: 
      Name: codedeploy-app-name

  CodeDeployGrouplicationName:
    Value: !Ref CodeDeployGroup
    Export: 
      Name: codedeploy-group-name

50行目のAutoScalingGroupsでデプロイするAutoScalingグループを指定しています。
デプロイ設定はCodeDeployDefault.HalfAtATimeを指定しています。
一度のデプロイで半分のインスタンスにデプロイする設定です。
例えば4台EC2があれば2台ずつ行う動きになります。
デプロイは以下のコマンドを実行します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=ApplicationName,ParameterValue=アプリケーション名 ParameterKey=DeploymentGroupName,ParameterValue=デプロイグループ名

CodePipeline

前回はビルドフェーズがありましたが今回は削除しています。
今回はデプロイの前に手動承認フェーズを追加してみました。
作成したCloudFormationテンプレートは以下になります。

AWSTemplateFormatVersion: "2010-09-09"

Description: CodePipeline Stack

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for SNS
        Parameters:
          - MailAddress
      - Label: 
          default: Parameters for CodePipeline
        Parameters:
          - CodePipelineName

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  MailAddress:
    Type: String

  CodePipelineName:
    MaxLength: 100
    Type: String

Resources:
# ------------------------------------------------------------#
# SNS
# ------------------------------------------------------------# 
  SnsTopic:
    Type: AWS::SNS::Topic
    Properties: 
      Subscription:
        - Endpoint: !Ref MailAddress
          Protocol: email
      TopicName: sns-codepipeline-approval

  SnsTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties: 
      PolicyDocument:
        Version: '2012-10-17'
        Id: approval
        Statement:
          - Sid: approval
            Effect: Allow
            Principal: 
              AWS: '*'
            Action: 
              - 'SNS:GetTopicAttributes'
              - 'SNS:SetTopicAttributes'
              - 'SNS:AddPermission'
              - 'SNS:RemovePermission'
              - 'SNS:DeleteTopic'
              - 'SNS:Subscribe'
              - 'SNS:ListSubscriptionsByTopic'
              - 'SNS:Publish'
            Resource: !Ref SnsTopic
            Condition: 
              StringEquals: 
                'AWS:SourceOwner': !Sub ${AWS::AccountId}
      Topics:
        - !Ref SnsTopic

# ------------------------------------------------------------#
# CodePipeline
# ------------------------------------------------------------# 
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !ImportValue S3Name
        Type: S3
      Name: !Ref CodePipelineName
      RoleArn: !ImportValue iam-role-codepipeline-arn
      Stages: 
        - Actions:
          - ActionTypeId: 
              Category: Source
              Owner: AWS
              Provider: CodeCommit
              Version: 1
            Configuration:
              RepositoryName: !ImportValue codecommit-repository-name
              BranchName: main
              PollForSourceChanges: false
              OutputArtifactFormat: CODE_ZIP
            Name: Source
            Namespace: SourceVariables
            OutputArtifacts:
              - Name: SourceArtifact
            Region: ap-northeast-1
            RunOrder: 1
          Name: Source
        - Actions:
          - ActionTypeId: 
              Category: Approval
              Owner: AWS
              Provider: Manual
              Version: 1
            Configuration:
              NotificationArn: !Ref SnsTopic
            Name: Approval
            Namespace: ApprovalVariables
            Region: ap-northeast-1
            RunOrder: 1
          Name: Approval
        - Actions:
          - ActionTypeId: 
              Category: Deploy
              Owner: AWS
              Provider: CodeDeploy
              Version: 1
            Configuration:
              ApplicationName: !ImportValue codedeploy-app-name
              DeploymentGroupName: !ImportValue codedeploy-group-name
            Name: Deploy
            Namespace: DeployVariables
            InputArtifacts:
              - Name: SourceArtifact
            Region: ap-northeast-1
            RunOrder: 1
          Name: Deploy
      Tags:
        - Key: Name
          Value: !Ref CodePipelineName

# ------------------------------------------------------------#
# EventBridge
# ------------------------------------------------------------# 
  EventBridgeIAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - "codepipeline:StartPipelineExecution"
            Resource: 
              - !Join 
                - ''
                - - 'arn:aws:codepipeline:ap-northeast-1:'
                  - !Sub '${AWS::AccountId}:'
                  - !Ref CodePipeline
      ManagedPolicyName: iam-policy-eventbridge

  EventBridgeIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - events.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - !Ref EventBridgeIAMPolicy
      RoleName: iam-role-eventbridge
      Tags:
        - Key: Name
          Value: iam-role-eventbridge

  EventBridge:
    Type: AWS::Events::Rule
    Properties: 
      Description: for codepipeline
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - 'CodeCommit Repository State Change'
        resources:
          - !ImportValue codecommit-repository-arn
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - main
      Name: eventbridge-codepipeline
      State: ENABLED
      Targets: 
        - Arn: !Join 
            - ''
            - - 'arn:aws:codepipeline:ap-northeast-1:'
              - !Sub '${AWS::AccountId}:'
              - !Ref CodePipeline
          Id: CodePipeline
          RoleArn: !GetAtt EventBridgeIAMRole.Arn

35行目から68行目のSNSトピックは手動承認フェーズで承認者にメールを送信できるよう作成します。
100行目から112行目で手動承認フェーズを記載しています。
デプロイは以下のコマンドを実行します。

aws cloudformation create-stack --stack-name スタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=MailAddress,ParameterValue=SNSで通知するメールアドレス ParameterKey=CodePipelineName,ParameterValue=CodePipeline名 --capabilities CAPABILITY_NAMED_IAM

動作確認

CodeCommitリポジトリにtest.htmlという名前のHTMLファイルをgit pushしてみます。
ディレクトリの中身は以下のようになっています。

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2023/01/24     14:37            327 appspec.yml
-a----        2022/09/29      1:16            158 test.html

以下のコマンドを実行してCodeCommitリポジトリにpushします。

git add .
git commit -m "add test.html"
git push origin main

デプロイが開始され手動承認フェーズまで来ると以下の件名でメールが届きます。

APPROVAL NEEDED: AWS CodePipeline CodePipeline名 for action Approval

メールが届いたら本文の「Approve or reject:」の横にあるURLをクリックすればCodePipelineの画面へ飛びます。
CodePipelineの画面に移動したら手動承認フェーズの「レビュー」をクリックします。

クリックすると承認するか却下するのかを選択できるので「承認します」をクリックします。
承認するとデプロイが開始されます。

デプロイが現在どのような状態なのか確認するにはデプロイフェーズの「詳細」をクリックすると確認ができます。


しばらく待つとデプロイが完了します。
EC2にアクセスしてドキュメントルートを確認するとデプロイされたことが確認できます。

drwxr-xr-x 2 root   root    60 Jan 24 08:37 .
drwxr-xr-x 4 root   root    33 Jan 23 13:46 ..
-rwxr-xr-x 1 apache apache 327 Dec 31  1979 appspec.yml
-rw-r--r-- 1 root   root    16 Jan 23 13:46 index.html
-rwxr-xr-x 1 apache apache 158 Dec 31  1979 test.html

スケールアウトしてEC2が起動したときの動作

EC2がスケールアウトした際もCodeDeployが動くのか確認します。
以下のコマンドを実行してEC2の台数を増やします。

aws autoscaling set-desired-capacity --auto-scaling-group-name AutoScalingグループ名 --desired-capacity 3

コマンドを実行後CodeDeployの「デプロイメント」から一番新しいデプロイIDのステータスが進行中になっていることが確認できます。

さいごに

今回はAutoScalingグループにデプロイするCodeDeploy設定をCloudFormationで作成してみました。
複雑な設定になってしまうのかと勝手に思っていたのですが、単一のEC2からAutoScalingグループがデプロイ先になったとしても設定が複雑化することはありませんでした。