はじめに
こんにちは。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に承認プロセスを取り入れることで、品質を担保したデプロイが可能になり思わぬ事故を防ぐことができます。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥