CI/CD配下のネストされたスタックを利用しているパイプラインに承認プロセスを組み込む[AWS CodePipeline+AWS CloudFormation]

はじめに

こんにちは。SCSKのふくちーぬです。

前回の記事では、CI/CD配下でネストされた AWS CloudFormation スタックの子スタックに対して、変更セットを有効にするテクニックを紹介しました。こちらの記事を読んでいない方は、まずご一読いただけますとより内容理解が進むと思います。

今回は、変更セットが妥当なものか判断するための承認フローをパイプラインに追加してみます。

構成図

前回から、パイプライン内の承認ステージの追加とメール送信用のSNSが追加されています。

CI/CDパイプラインの構成

前回作成したスタックを更新します。スタックを作成していない方は、新規にスタックを作成してください。

Cloud9及びCodeCommitのディレクトリ構成について

ディレクトリ構成は前回と同様です。”cicd.yaml”と”changeset-buildspec.yaml”のみファイルの更新があります。

changeset-buildspec.yaml

環境変数として”expoted-variables”を追加しています。これらの環境変数をエクスポートすることで、承認ステージで利用できます。

 

ここでは、スタックIDと変更セットIDを参照できるようエクスポートしています。

version: 0.2

env:
 exported-variables: #変数のエクスポート
  - SackId
  - ChangeSetId

phases:
 install:
  commands:
 build:
  commands:
   - |
    [ -d .cfn ] || mkdir .cfn   
    aws cloudformation package \
     --template-file cfn.yaml \
     --s3-bucket $S3_BUCKET \
     --output-template-file .cfn/packaged.yaml
 post_build:
  commands:
   - pwd
   - |
    #変数の設定
    stack_name=$STACK_NAME
    change_set_name="changeset"
    template_body="file://.cfn/packaged.yaml"
    parameters="file://params/param.json"
    capabilities="CAPABILITY_NAMED_IAM"
    role_arn=$CFNROLE_ARN    
    #スタックが存在するか確認する関数
    function stack_exists() {
    aws cloudformation describe-stacks --stack-name "$1" 2>&1 1>/dev/null | grep -e "ValidationError" > /dev/null 
    #|| aws cloudformation describe-stacks --stack-name "$1" | grep -e "REVIEW_IN_PROGRESS" > /dev/null
    }
    
    #スタックがレビュー中か確認する関数
    function stack_reviewin() {
    aws cloudformation describe-stacks --stack-name "$1" | grep -e "REVIEW_IN_PROGRESS"
    }    

    #変更セットが存在するか確認する関数
    function changeset_exists() {
    local stack_name="$1"
    local change_set_name="$2"
    aws cloudformation describe-change-set --stack-name "$stack_name" --change-set-name "$change_set_name" 2>&1 1>/dev/null | grep -e "ChangeSetNotFound" > /dev/null 
    }

    #変更セットを削除する関数
    function delete_changeset() {
    local stack_name="$1"
    local change_set_name="$2"
    aws cloudformation delete-change-set --stack-name "$stack_name" --change-set-name "$change_set_name"
    sleep 5  #既存の変更セットが削除されるまで待つ
    }

    #変更セットを作成する関数
    function create_changeset() {
    local stack_name="$1"
    local change_set_name="$2"
    local template_body="$3"
    local parameters="$4"
    local capabilities="$5"
    local change_set_type="$6"
    local role_arn="$7"
    aws cloudformation create-change-set \
        --stack-name "$stack_name" \
        --change-set-name "$change_set_name" \
        --template-body "$template_body" \
        --parameters "$parameters" \
        --capabilities "$capabilities" \
        --change-set-type "$change_set_type" \
        --role-arn "$role_arn" \
        --include-nested-stacks > output.json
    }
    
    #メイン
    if stack_exists "$stack_name"; then #0の場合
        echo "Stack doesn't exist. Creating new changeset."
        # if changeset_exists "$stack_name" "$change_set_name"; then
        #     echo "Changeset exists. Deleting the changeset."
        # fi
        create_changeset "$stack_name" "$change_set_name" "$template_body" "$parameters" "$capabilities" "CREATE" "$role_arn"
    else #1の場合
        echo "Stack exists."
        if changeset_exists "$stack_name" "$change_set_name"; then #0の場合
            echo "Changeset doesn't exist. Creating new changeset."  
        else #1の場合
            echo "Changeset exists. Creating new changeset."        
            delete_changeset "$stack_name" "$change_set_name"
        fi
        if stack_reviewin "$stack_name"; then #0の場合      
            echo "Stack review_in_progress." 
            create_changeset "$stack_name" "$change_set_name" "$template_body" "$parameters" "$capabilities" "CREATE" "$role_arn"
        else
            create_changeset "$stack_name" "$change_set_name" "$template_body" "$parameters" "$capabilities" "UPDATE" "$role_arn"
        fi
    fi
   - SackId=`cat output.json | jq .StackId | sed 's/"//g'`  #変数の代入
   - ChangeSetId=`cat output.json | jq .Id | sed 's/"//g'`  #変数の代入  
artifacts:
 files:
  - .cfn/*
  - params/*
 discard-paths: yes

cicd.yaml

以下3つを追加しています。

  • メール送信用のSNSトピックの追加
  • CodePipelineのIAMロールにSNSトピック発行の権限を追加
  • CodePipelineのパイプラインに承認ステージを追加

変数の受け渡し

重要な点を少し嚙み砕いて説明します。名前空間である”Namespace”を指定することで、ビルドステージにてエクスポートした変数(ここでは”SackId”・”ChangeSetId”を指す。)を後続のステージへ渡すことが可能になります。

        - Name: Build
          Actions:
            - InputArtifacts:
                - Name: SourceOutput
              Name: changeset
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              OutputArtifacts:
                - Name: BuildOutput
              Configuration:
                ProjectName: !Ref CodeBuildProjectChangeset    
              Namespace: BuildVariables 

レビュー用URLの作成

承認ステージの”ExternalEntityLink”にて、任意のレビュー用URLを指定することができます。

変更セットのURLには規則性があるので、それを変数と組み合わせることで動的にリンクを作成することができます。

 https://【リージョン】.console.aws.amazon.com/cloudformation/home?region=【リージョン】#/stacks/changesets/changes?stackId=【スタックID】&changeSetId=【変更セットID】

ここでは以下を設定することで、変更セットを確認できるURLを動的に作成します。

        - Name: Approval #承認ステージの追加
          Actions:
            - Name: approve-changeset
              ActionTypeId:
                Category: Approval
                Owner: AWS
                Version: 1
                Provider: Manual
              Configuration:
                ExternalEntityLink: !Sub https://${AWS::Region}.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/changesets/changes?stackId=#{BuildVariables.SackId}&changeSetId=#{BuildVariables.ChangeSetId}
                NotificationArn: !GetAtt SNSTopic.TopicArn 

完成したcicd.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: cfn CI/CD Pipeline

Parameters:
  ResourceName:
    Type: String
  REPOSITORYNAME:
    Type: String
    Description: aws codecommit repository name
  STACKNAME:
    Type: String
  MailAddress:
    Type: String

Resources:
  ArtifactStoreBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: !Sub s3bucket-${AWS::AccountId}-artifactbucket

  CodeBuildBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: !Sub s3bucket-${AWS::AccountId}-codebuildtbucket

# ------------------------------------------------------------#
# EventBridge Rule for Starting CodePipeline
# ------------------------------------------------------------#
  PipelineEventsRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${ResourceName}-rule-pipeline
      EventBusName: !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/default"
      EventPattern:
        source:
          - aws.codecommit
        resources:
          - !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${REPOSITORYNAME}
        detail-type:
          - "CodeCommit Repository State Change"
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceName:
            - main
      State: ENABLED
      Targets:
        - Arn:
            Fn::Join:
              - ""
              - - "arn:"
                - Ref: AWS::Partition
                - ":codepipeline:"
                - Ref: AWS::Region
                - ":"
                - Ref: AWS::AccountId
                - ":"
                - Ref: Pipeline
          Id: Target
          RoleArn: !GetAtt PipelineEventsRole.Arn
    DependsOn:
      - PipelineEventsRole
      - Pipeline    
# ------------------------------------------------------------#
#  CodePipeline Events Role  (IAM)
# ------------------------------------------------------------#
  PipelineEventsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
      Path: /
      ManagedPolicyArns:
        - !Ref PipelineEventsPolicy
      RoleName: !Sub "IRL-EVENTBRIDGE-CodePipelineAccess"  
# ------------------------------------------------------------#
#  CodePipeline Events Role Policy (IAM)
# ------------------------------------------------------------#
  PipelineEventsPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: CodePipelineAccessForEvents
      PolicyDocument:
        Statement:
          - Action: codepipeline:StartPipelineExecution
            Effect: Allow
            Resource:
              - !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}            
        Version: "2012-10-17"      
# ------------------------------------------------------------#
#  CodeBuild Role (IAM)
# ------------------------------------------------------------#
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
      Path: /
      ManagedPolicyArns:
        - !Ref CodeBuildPolicy
        - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
      RoleName: !Sub "IRL-CODEBUILD-S3CloudWatchlogsAccess"
# ------------------------------------------------------------#
#  CodeBuild Role Policy (IAM)
# ------------------------------------------------------------#
  CodeBuildPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: CodeBuildAccess
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: CloudWatchLogsAccess
            Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource:
              - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*           
          - Sid: S3Access
            Effect: Allow
            Action:
              - s3:PutObject
              - s3:GetObject
              - s3:GetObjectVersion
            Resource:
              - !Sub arn:aws:s3:::${ArtifactStoreBucket}
              - !Sub arn:aws:s3:::${ArtifactStoreBucket}/*
              - !Sub arn:aws:s3:::${CodeBuildBucket}
              - !Sub arn:aws:s3:::${CodeBuildBucket}/*
          - Sid: IAMPass
            Effect: Allow
            Action:
              - iam:PassRole
            Resource: "*"          
          - Sid: CloudFormationAccess
            Effect: Allow
            Action: cloudformation:ValidateTemplate
            Resource: "*" 
# ------------------------------------------------------------#
#  CodeBuild linter Project
# ------------------------------------------------------------#
  CodeBuildProjectLint:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub ${ResourceName}-project-lint
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
        EnvironmentVariables:
          - Name: AWS_REGION
            Value: !Ref AWS::Region
          - Name: S3_BUCKET
            Value: !Ref CodeBuildBucket     
      Source:
        Type: CODEPIPELINE       
# ------------------------------------------------------------#
#  CodeBuild changeset Project
# ------------------------------------------------------------#
  CodeBuildProjectChangeset:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub ${ResourceName}-project-changeset
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
        EnvironmentVariables:
          - Name: AWS_REGION
            Value: !Ref AWS::Region
          - Name: S3_BUCKET
            Value: !Ref CodeBuildBucket
          - Name: STACK_NAME
            Value: !Ref STACKNAME
          - Name: CFNROLE_ARN
            Value: !GetAtt  CloudformationRole.Arn
      Source:
        Type: CODEPIPELINE
        BuildSpec: changeset-buildspec.yaml   
# ------------------------------------------------------------#
#  CloudFormation Role (IAM)
# ------------------------------------------------------------#
  CloudformationRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: cloudformation.amazonaws.com
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess
      RoleName: "IRL-CLOUDFORMATION-ServiceFullAccess"
# ------------------------------------------------------------#
#  CodePipeline Role (IAM)
# ------------------------------------------------------------#
  PipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
      Path: /
      ManagedPolicyArns:
        - !Ref PipelinePolicy
      RoleName: "IRL-CODEPIPELINE-Access"
# ------------------------------------------------------------#
#  CodePipeline Role Policy (IAM)
# ------------------------------------------------------------#
  PipelinePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: CodePipelineAccess
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: S3FullAccess
            Effect: Allow
            Action: s3:*
            Resource:
              - !Sub arn:aws:s3:::${ArtifactStoreBucket}
              - !Sub arn:aws:s3:::${ArtifactStoreBucket}/*        
          - Sid: FullAccess
            Effect: Allow
            Action:
              - cloudformation:*
              - iam:PassRole
              - codecommit:GetRepository
              - codecommit:ListBranches
              - codecommit:GetUploadArchiveStatus
              - codecommit:UploadArchive
              - codecommit:CancelUploadArchive
              - codecommit:GetBranch
              - codecommit:GetCommit
            Resource: "*"
          - Sid: CodeBuildAccess
            Effect: Allow
            Action:
              - codebuild:BatchGetBuilds
              - codebuild:StartBuild
            Resource: !GetAtt CodeBuildProjectLint.Arn
          - Sid: CodeBuildChangesetAccess
            Effect: Allow
            Action:
              - codebuild:BatchGetBuilds
              - codebuild:StartBuild
            Resource: !GetAtt CodeBuildProjectChangeset.Arn      
          - Sid: SNSAccess #SNSトピック発行の権限を追加
            Effect: Allow
            Action:
              - sns:Publish
            Resource: !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${ResourceName}-topic                
# ------------------------------------------------------------#
#  CodePipeline
# ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub ${ResourceName}-pipeline
      RoleArn: !GetAtt PipelineRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactStoreBucket
      Stages:
        - Name: Source
          Actions:
            - Name: download-source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref REPOSITORYNAME
                BranchName: main
                PollForSourceChanges: false
              OutputArtifacts:
                - Name: SourceOutput
        - Name: Test
          Actions:
            - InputArtifacts:
                - Name: SourceOutput
              Name: testing
              ActionTypeId:
                Category: Test
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProjectLint
        - Name: Build
          Actions:
            - InputArtifacts:
                - Name: SourceOutput
              Name: changeset
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              OutputArtifacts:
                - Name: BuildOutput
              Configuration:
                ProjectName: !Ref CodeBuildProjectChangeset    
              Namespace: BuildVariables
        - Name: Approval #承認ステージの追加
          Actions:
            - Name: approve-changeset
              ActionTypeId:
                Category: Approval
                Owner: AWS
                Version: 1
                Provider: Manual
              Configuration:
                ExternalEntityLink: !Sub https://${AWS::Region}.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/changesets/changes?stackId=#{BuildVariables.SackId}&changeSetId=#{BuildVariables.ChangeSetId}
                NotificationArn: !GetAtt SNSTopic.TopicArn              
        - Name: Deploy
          Actions:
            - Name: execute-changeset
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: CloudFormation
              Configuration:
                StackName: !Join [ '-', [ !Ref ResourceName, 'infra-stack' ] ]
                ActionMode: CHANGE_SET_EXECUTE
                ChangeSetName: changeset
                RoleArn: !GetAtt CloudformationRole.Arn
# ------------------------------------------------------------#
#  SNS
# ------------------------------------------------------------#
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription: 
        - Endpoint: !Ref MailAddress
          Protocol: email
      TopicName: !Sub ${ResourceName}-topic                 

ポイント

  • CodeBuild内で環境変数をエクスポートして、次のステージに渡すよう設定する
  • 承認ステージのレビュー用URLにて、スタックID及び変更セットIDを使用するようURLを動的に設定する

CI/CDパイプラインの更新

更新した”cicd.yaml”ファイルを利用して、パイプラインを構成したスタックを更新してください。

サブスクリプションの確認

その後、指定したメールアドレスに届くSNSのサブスクリプションを許可してください。

サブスクリプションの確認が完了しました。

CodeCommitへのプッシュ

ここでは、更新済みの”changeset-buildspec.yaml”と”securitygroup.yaml”をCodeCommitにプッシュします。”securitygroup.yaml”では、前回同様にソースアドレスの変更等実施してください。

パイプラインが起動して、承認ステージまで進んでいます。

通知の確認

承認プロセスを挟んでいるため、指定のメールアドレスに以下のようなメッセージが届いています。

“Content to review”を押下すると、変更セットの画面に飛びます。

“Approve or reject”を押下すると、パイプラインの承認画面に飛びます。

“Approve or reject”を押下してみてください。

変更セットの確認とレビュー

承認ステージ内の”レビュー”を押下してください。以下のような画面になります。

“レビュー用URL”を押下すると、変更セットの内容を確認することができます。今回は、セキュリティグループが変更されることが明らかですね。

先ほどの画面に戻ってください。

変更セットの内容に問題ないため、コメントを記載の上”承認します”を押下して、システムをリリースします。

 

デプロイできましたね!ちょー気持ちいい

まとめ

いかがだったでしょうか。

CI/CD配下のネストされたスタックに対して、承認用のレビュー用URLを組み込んでみました。

CodePipelineに承認プロセスを取り入れることで、品質を担保したデプロイが可能になり思わぬ事故を防ぐことができます。

本記事が皆様のお役にたてば幸いです。

ではサウナラ~🔥

タイトルとURLをコピーしました