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