はじめに
こんにちは。SCSKのふくちーぬです。
今回は、CI/CD配下でネストされた AWS CloudFormation スタックの子スタックに対して、変更状況が分かるような高度なテクニックをご紹介します。
実はCI/CD配下でネストされたスタックを採用した場合、AWS CodePipelineが子スタックの変更セットの状況まで面倒を見てくれません。つまり、親スタックの変更セットの状況しか分からないということになります。親スタックの大まかな差分しか分からないとなると、開発者はデプロイ時に自身の変更分がどこまで反映されるのか知る由もなく、少々不便ですよね。
そこで今回は、AWS CodeBuildにて変更セットを作成することでこの問題に対処しようと思います。
CodePipelineがネストされたスタックの変更セットをサポートしていない件
CodePipeineではCloudFormationのアクションを利用することで、変更セットの作成・更新・実行が可能になります。このアクションを利用して、CloudFormationテンプレートをデプロイすることができます。
しかしCodePipelineでは、ネストされたスタックに対しての記述が書かれていませんね。AWSサポートに確認したところ、CodePipelineではサポートされていないようでした。
ネストされたスタックの子スタックに対して変更セットを有効にするためには
CloudFormation APIについて
CloudFormation APIでは、サポートされています。
IncludeNestedStacks
Creates a change set for the all nested stacks specified in the template. The default behavior of this action is set to
False
. To include nested sets in a change set, specifyTrue
.
つまり、CloudFormationコンソール上からまたはAWS CLI等を通してならば子スタックの変更分も判別できるようにすることが可能です。
CodeBuildの使用
CI/CDを利用している場合は、CodeBuildも大概利用しているかと思います。今回の解決方法としては、CodeBuild内にて変更セットを作成することで対応することができます。
CodePipelineで任せている部分をCodeBuildにてカスタムで処理をするイメージとなります。
構成図
今回の構成図となります。
CodeCommit+CodeBuild+CodePipelineにてCI/CDを構成しています。
サンプルのインフラリソースも用意しています。
CI/CDパイプラインの準備
Cloud9の作成
以下に AWS Cloud9 の作成方法を記載しているので参考にしてください。
Cloud9とCodeCommitの連携
Cloud9からCodeCommitにプッシュできるように準備してください。
Cloud9及びCodeCommitのディレクトリ構成について
以下のようなディレクトリ構成としておきます。
param.json
以下の3つのCloudFormationテンプレートで使用するためのパラメータファイルです。
[ { "ParameterKey": "Environment", "ParameterValue": "dev" }, { "ParameterKey": "VPCCidrBlock", "ParameterValue": "192.168.0.0/16" } ]
-cfn.yaml
-network.yaml
-securitygroup.yaml
network.yaml
VPC・パブリックサブネット・ルートテーブル・インターネットゲートウェイを作成するCloudFormationテンプレートです。
cfn.yamlの子スタックとなります。
AWSTemplateFormatVersion: 2010-09-09 Description: Network Stack Parameters: Environment: Type: String VPCCidrBlock: Type: String Resources: # ------------------------------------------------------------# # VPC # ------------------------------------------------------------# VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VPCCidrBlock EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub ${Environment}-vpc # ------------------------------------------------------------# # InternetGateway # ------------------------------------------------------------# InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub ${Environment}-igw AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway # ------------------------------------------------------------# # RouteTable # ------------------------------------------------------------# PublicRouteTable: Type: AWS::EC2::RouteTable DependsOn: AttachGateway Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${Environment}-rtb PublicRoute: Type: AWS::EC2::Route DependsOn: AttachGateway Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway # ------------------------------------------------------------# # Subnet # ------------------------------------------------------------# PublicSubnet: Type: AWS::EC2::Subnet DependsOn: AttachGateway Properties: VpcId: !Ref VPC AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref AWS::Region CidrBlock: !Join [ "", [ !Select [ 0, !Split [ "/", !Ref VPCCidrBlock ] ], /24 ] ] Tags: - Key: Name Value: !Sub ${Environment}-subnet PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable Outputs: VpcId: Value: !Ref VPC SubnetId: Value: !Ref PublicSubnet
securitygroup.yaml
セキュリティグループを作成するCloudFormationテンプレートです。
cfn.yamlの子スタックとなります。
AWSTemplateFormatVersion: 2010-09-09 Description: Security Group Stack Parameters: Environment: Type: String VpcId: Type: AWS::EC2::VPC::Id Resources: # ------------------------------------------------------------# # SecurityGroup # ------------------------------------------------------------# SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enable ssh and web access VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 192.168.0.0/16 #192.168.0.0/24に変更 - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 192.168.0.0/16 #192.168.0.0/24に変更 Tags: - Key: Name Value: !Sub ${Environment}-sg Outputs: SGId: Value: !Ref SG
cfn.yaml
上記2ファイルを子とする親スタックとなります。
AWSTemplateFormatVersion: 2010-09-09 Description: Parent Stack Parameters: Environment: Type: String VPCCidrBlock: Type: String Resources: # ------------------------------------------------------------# # Network Stack (VPC Subnet RouteTable InternetGateway) # ------------------------------------------------------------# NW: Type: AWS::CloudFormation::Stack Properties: TemplateURL: src/network.yaml Parameters: Environment: !Ref Environment VPCCidrBlock: !Ref VPCCidrBlock # ------------------------------------------------------------# # Security Stack (SecurityGroup) # ------------------------------------------------------------# SECURITY: Type: AWS::CloudFormation::Stack Properties: TemplateURL: src/securitygroup.yaml Parameters: Environment: !Ref Environment VpcId: !GetAtt NW.Outputs.VpcId
buildspec.yaml
CodeBuildで使用するビルド仕様ファイルです。
CloudFormationテンプレートの構文チェックを実施しています。
version: 0.2 phases: pre_build: commands: - | [ -d .cfn ] || mkdir .cfn aws configure set default.region $AWS_REGION for template in src/*.yaml cfn.yaml; do echo "$template" | xargs -I% -t aws cloudformation validate-template --template-body file://% done
changeset-buildspec.yaml
CodeBuildで使用するビルド仕様ファイルです。
ネストされたスタックに対して変更セットを作成しています。
version: 0.2 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: - | #変数の設定 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 } #スタックがレビュー中か確認する関数 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 echo "Stack doesn't exist. Creating new changeset." create_changeset "$stack_name" "$change_set_name" "$template_body" "$parameters" "$capabilities" "CREATE" "$role_arn" else echo "Stack exists." if changeset_exists "$stack_name" "$change_set_name"; then echo "Changeset doesn't exist. Creating new changeset." else echo "Changeset exists. Creating new changeset." delete_changeset "$stack_name" "$change_set_name" fi if stack_reviewin "$stack_name"; then 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 artifacts: files: - .cfn/* - params/* discard-paths: yes
cicd.yaml
パイプライン構築用のCloudFormationファイルです。
AWSTemplateFormatVersion: 2010-09-09 Description: cfn CI/CD Pipeline Parameters: ResourceName: Type: String REPOSITORYNAME: Type: String Description: aws codecommit repository name STACKNAME: Type: String Resources: # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# 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 # ------------------------------------------------------------# # 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: 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
CI/CDパイプラインの作成
パイプラインを構築するための”cicd.yaml”ファイルを使用して、デプロイしておきます。
その後各種ファイルをCodeCommitにプッシュすることで、パイプラインが起動します。
パイプラインの起動・スタックの作成
以下のようにパイプラインが自動で動いて、スタックの作成まで行ってくれます。
念の為親スタックを確認しておくと、2つの子スタックが作成されたことが分かります。リンクから子スタック(NW)に飛んでみます。
VPCやサブネット等のリソースが新規作成されることが分かります。
スタックの更新
セキュリティグループのインバウンドルールのソースアドレスを変更してみます。
以下のように更新したファイルをCodeCommitにプッシュします。
AWSTemplateFormatVersion: 2010-09-09 Description: Security Group Stack Parameters: Environment: Type: String VpcId: Type: AWS::EC2::VPC::Id Resources: # ------------------------------------------------------------# # SecurityGroup # ------------------------------------------------------------# SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enable ssh and web access VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 192.168.0.0/24 - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 192.168.0.0/24 Tags: - Key: Name Value: !Sub ${Environment}-sg Outputs: SGId: Value: !Ref SG
親スタックの変更セットで子スタック(SECURITY)に変更分があることを示してくれました。セキュリティグループのみを変更したので意図した動作になりましたね。
リンクから子スタック(SECURITY)に飛んでください。
きちんとネストされたスタックの子スタックに対しても、変更セットが作成されて差分(AWS::EC2::SecurityGroup)が理解できるようになりました。
まとめ
CodePipelineでは、ネストされたスタックに対しての変更セットをサポートしていません。
CloudFormation APIではサポートしているため、CodeBuild内から呼び出すことで実現可能です。
最後に
いかがだったでしょうか。
ネストされたスタックの子スタックに対して変更セットを有効にできるようなテクニックを紹介しました。今回はCodeBuildを利用してカスタマイズすることで実現できました。
ネストされたスタックをご利用する際には、一度検討していただければと思います。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥