LoginSignup
0
0

【AWS CDK (Python)】 クロスアカウントでのデプロイ方法(パイプライン版)

Posted at

はじめに

本記事は、私自身の備忘録を兼ねてAWS CDKをこれから始める方の一助になればと思い、AWS CDKの使い方等をまとめたものです。
前回、AWS CDKで別アカウントにリソースをデプロイしましたが、今回は、その応用でパイプラインを使って別アカウントにリソースをデプロイしたいと思います。
前回の記事は、こちら(【AWS CDK (Python)】 クロスアカウントでのデプロイ方法)を参照してください。
なお、本記事は私自身の経験を基に記載していますが、間違いがあったらすみません。

パイプラインで別アカウントにAWS CDKでリソースをデプロイ

基本的には前回(【AWS CDK (Python)】 クロスアカウントでのデプロイ方法)、Cloud9でやってたことをCodeBuildでやるだけです。
ただパイプラインでやるとちょっと勝手が違うので、少し手を加えてます。
構成としては以下のようになります。
image.png

パイプラインは2つ作ります。本題の別アカウントにリソースをデプロイするパイプラインは、図の下半分部分になります。CodeCommitにpushされたCDKのソースコードを元にCodeBuildでcdk deployを実行し、別アカウントにリソースをデプロイします。
で、上半分のパイプラインは何かというと、下のCodeBuildで使うコンテナイメージを作成しています。もちろんCodeBuildに標準で用意されているイメージでも問題ないのですが、そうすると毎回cdk deployの度にCDKやPythonをインストールする必要があり、CodeBuildの実行時間も長くなってしまいます。ですので、今回は、カスタムのコンテナイメージを作成した上で、そのイメージにCDKやPythonなどの必要なパッケージをあらかじめインストールするようにして、cdk deployの際はそのカスタムイメージを利用することにします。
なお、コンテナイメージの作成部分については、パイプラインである必要はない(手動でコンテナイメージ作ってECRに格納しても全然OK)と思いますので、そこはお好みで。

環境

本記事は以下の環境を使用して記載しています。

  • AWS Cloud9
  • AWS CDK:2.142.1
  • Python: 3.12.3
  • Node.js: 20.13.1

パイプラインを使用してクロスアカウントでのCDKデプロイ

以下の流れでパイプラインを使用してクロスアカウントでのCDKデプロイ確認をしていきます。
また、今回は2つのパイプラインをCDKで作成します。

準備1. (Account A & B)Cloud9の作成
準備2. (Account A & B)AWS CDKのインストール・初期化
準備3. (Account A)cdk bootstrapの実行
1. (Account A)コンテナイメージを作成するパイプラインの作成
2. (Account A)コンテナイメージの作成
3. (Account B)cdk bootstrapの実行
4. (Account A)別アカウントへデプロイするパイプラインの作成
5. (Account A)別アカウントへデプロイ


準備1. (Account A & B)Cloud9の作成

まず、作業場所になるCloud9を作成します。
Cloud9の作成は、以下の記事を参考にしてください。

準備2. (Account A & B)AWS CDKのインストール・初期化

次に、作成したCloud9にAWS CDKをインストールし初期化します。
AWS CDKのインストール・初期化は、以下の記事を参考にしてください。

準備3. (Account A)cdk bootstrapの実行

次に、cdk bootstrapを実行し、cdkの実行に必要なリソースを整備します。(Account B分は、後で実施するため、ここではAccount Aだけします。)
cdk bootstrapは、以下の記事を参考にしてください。

1. (Account A)コンテナイメージを作成するパイプラインの作成

では、冒頭で述べたようにまず、CodeBuildで使用するコンテナイメージを作成するパイプラインを作成します。
CDKのソースコードは以下のようになります。

CDKコード全体
app.py
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_app_stack import CdkAppStack

app = cdk.App()

CdkAppStack = CdkAppStack(app, "CdkBuildImgStack",
    )

app.synth()
cdk_app/cdk_app_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    aws_iam as iam,
    aws_codebuild as codebuild,
    aws_codepipeline as codepipeline,
    aws_events as events,
    aws_codecommit as codecommit,
    aws_ecr as ecr,
    aws_logs as logs
)
from constructs import Construct
import os

# S3バケットを作成する共通関数
def CreateBucket(scope, bucket_id):

    cfn_bucket = s3.CfnBucket(
        scope,
        bucket_id,
        versioning_configuration = s3.CfnBucket.VersioningConfigurationProperty(
            status = "Enabled"
        ),
        bucket_name = "cdk-test-{0}-c3hw2bm4".format(bucket_id)
    )
    return cfn_bucket

# CodeBuild用のIAMロールを作成
def CreateBuildIAMRole(scope, artifact_s3bucket):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "codebuild.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Resource": "*",
                "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                ]
            },
            {
                "Effect": "Allow",
                "Resource": [
                    "arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
                    "arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
                ],
                "Action": [
                    "s3:PutObject",
                    "s3:GetObject",
                    "s3:GetObjectVersion",
                    "s3:GetBucketAcl",
                    "s3:GetBucketLocation"
                ]
            }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "codebuild-build-custom-img-role",
            assume_role_policy_document=assume_role_policy_document,
            managed_policy_arns=["arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess"],
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="codebuild-build-custom-img-policy"
                )
            ],
            role_name="codebuild-build-custom-img-role"
        )
    
    return cfn_role

# CodePipeline用のIAMロールを作成
def CreatePipelineIAMRole(scope, artifact_s3bucket):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "codepipeline.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "codecommit:CancelUploadArchive",
                    "codecommit:GetBranch",
                    "codecommit:GetCommit",
                    "codecommit:GetUploadArchiveStatus",
                    "codecommit:UploadArchive"
                ],
                "Resource": "*"
            },
            {
                "Action": [
                    "codebuild:BatchGetBuilds",
                    "codebuild:StartBuild",
                    "codebuild:BatchGetBuildBatches",
                    "codebuild:StartBuildBatch"
                ],
                "Resource": "*",
                "Effect": "Allow"
            },
            {
                "Action": [
                    "s3:Get*",
                    "s3:Put*",
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
                    "arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
                ],
                "Effect": "Allow"
            },
            {
                "Action": [
                    "cloudwatch:*"
                ],
                "Resource": "*",
                "Effect": "Allow"
            }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "codepipeline-build-custom-img-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="codepipeline-build-custom-img-policy"
                )
            ],
            role_name="codepipeline-build-custom-img-role"
        )
    
    return cfn_role

# EvantBridge用のIAMロールを作成
def CreateExecPipelineIAMRole(scope, pipeline_arn):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "events.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                    "Effect": "Allow",
                    "Action": "codepipeline:StartPipelineExecution",
                    "Resource": pipeline_arn
                }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "exec-build-custom-img-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="exec-build-custom-img-policy"
                )
            ],
            role_name="exec-build-custom-img-role"
        )
    
    return cfn_role

# CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
def CodecommitEvents(scope, rep_name, pipeline_arn, exec_pipeline_role):

    event_pattern = {
        "source": ["aws.codecommit"],
        "detail-type": ["CodeCommit Repository State Change"],
        "resources": [
            "arn:aws:codecommit:{0}:{1}:{2}".format(
                "ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], rep_name)
        ],
        "detail": {
            "event": ["referenceCreated", "referenceUpdated"],
            "referenceType": ["branch"],
            "referenceName": ["main"]
        }
    }

    cfn_rule = events.CfnRule(
        scope,
        "change-custom-image-repo-events",
        event_pattern=event_pattern,
        name="change-custom-image-repo-events",
        state="ENABLED",
        targets=[
            events.CfnRule.TargetProperty(
                arn=pipeline_arn,
                id="change-custom-image-repo-events",
                role_arn=exec_pipeline_role.attr_arn,
            )
        ]
    )

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # アーティファクトバケットを作成
        artifact_s3bucket = CreateBucket(self, "artifact-bucket-build-img")

        # CodeBuild用のIAMロールの作成
        build_role = CreateBuildIAMRole(self, artifact_s3bucket)
        # CodePipeline用のIAMロールの作成
        pipeline_role = CreatePipelineIAMRole(self, artifact_s3bucket)

        # CodeCommitリポジトリの作成
        cfn_repository = codecommit.CfnRepository(
            self,
            "custom-build-img-code-repo",
            repository_name="custom-build-img-code-repo",
            repository_description="For custom images in CodeBuild"
        )

        # ECRリポジトリの作成
        repository_policy_text = {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "Allow codebuild",
              "Effect": "Allow",
              "Principal": {
                "Service": "codebuild.amazonaws.com"
              },
              "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:BatchGetImage",
                "ecr:GetDownloadUrlForLayer"
              ]
            }
          ]
        }

        cfn_container_repository = ecr.CfnRepository(
            self,
            "custom-build-img-repo",
            empty_on_delete=False,
            repository_name="custom-build-img-repo",
            repository_policy_text=repository_policy_text
        )


        # Buildプロジェクト用ロググループの作成
        cfn_log_group = logs.CfnLogGroup(
            self,
            "build-custom-img-project-loggroup",
            log_group_name="/aws/codebuild/build-custom-img-project",
            retention_in_days=14
        )

        # Buildプロジェクトの作成
        cfn_project = codebuild.CfnProject(
            self, 
            "build-custom-img-project",
            artifacts=codebuild.CfnProject.ArtifactsProperty(
                type="CODEPIPELINE",
            ),
            environment=codebuild.CfnProject.EnvironmentProperty(
                compute_type="BUILD_GENERAL1_SMALL",
                image="aws/codebuild/amazonlinux2-x86_64-standard:5.0",
                type="LINUX_CONTAINER",
                privileged_mode=True,
            ),
            service_role=build_role.attr_arn,
            source=codebuild.CfnProject.SourceProperty(
                type="CODEPIPELINE",
                build_spec="buildspec.yaml"
            ),
            logs_config=codebuild.CfnProject.LogsConfigProperty(
                cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
                    status="ENABLED",
                    group_name=cfn_log_group.log_group_name,
                ),
            ),
            name="build-custom-img-project",
        )

        # CodePipelineの作成
        source_actions = []
        # Sourceアクション(CodeCommit(Lambdaコード、buildspec))の作成
        source_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Source",
                    owner="AWS",
                    provider="CodeCommit",
                    version="1"
                ),
                name="Source_codecommit",
                configuration={
                    "RepositoryName": cfn_repository.attr_name,
                    "BranchName": "main",
                    "PollForSourceChanges": False
                },
                namespace="SourceVariables_codecommit",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="SourceArtifact_codecommit"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Buildアクションの作成
        build_actions = []
        build_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Build",
                    owner="AWS",
                    provider="CodeBuild",
                    version="1"
                ),
                name="Build",
                configuration={
                    "ProjectName": cfn_project.name,
                    "PrimarySource": "SourceArtifact_codecommit"
                },
                input_artifacts=[
                    codepipeline.CfnPipeline.InputArtifactProperty(
                        name="SourceArtifact_codecommit"
                    )
                ],
                namespace="BuildVariables",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="BuildArtifact"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Pipelineの作成
        cfn_pipeline = codepipeline.CfnPipeline(
            self,
            "pipeline-build-custom-img",
            role_arn=pipeline_role.attr_arn,
            stages=[
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=source_actions,
                    name="Source",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=build_actions,
                    name="Build",
                )
            ],
            artifact_store=codepipeline.CfnPipeline.ArtifactStoreProperty(
                location=artifact_s3bucket.ref,
                type="S3",
            ),
            name="pipeline-build-custom-img",
            pipeline_type="V2",
            restart_execution_on_update=False
        )

        pipeline_arn = "arn:aws:codepipeline:{0}:{1}:{2}".format(
                "ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], cfn_pipeline.name)

        # EventBridge用のIAMロールの作成
        exec_pipeline_role = CreateExecPipelineIAMRole(self, pipeline_arn)
        # CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
        CodecommitEvents(self, cfn_repository.attr_name, pipeline_arn, exec_pipeline_role)

この部分の詳細な説明は割愛します。(特段、特別なことはしてませんので。)
コードを作ったら、cdk deployしてパイプラインを作成します。
作成したら、こういったパイプラインができると思います。
※まだCodeCommitに何もファイルがないので、実行は失敗します。
image.png

2. (Account A)コンテナイメージの作成

パイプラインを作成したらCodeCommitにファイルをpushします。
必要なファイルは以下のとおりです。

  • buildspec.yaml
    CodeBuildでコンテナイメージを作成するためのbuildspecファイルです。コンテナイメージを作成して、ECRにpushしています。
version: 0.2

phases:
  install:
    commands:
      - ls -lR
  build:
    commands:
      - AWS_ACCOUNT_ID=$(echo ${CODEBUILD_BUILD_ARN} | cut -f 5 -d :)
      - AWS_REGION=$(echo ${CODEBUILD_BUILD_ARN} | cut -f 4 -d :)
      - docker build -t custom-build-img-repo .
  post_build:
    commands:
      - aws --version
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
      - docker tag custom-build-img-repo:latest $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/custom-build-img-repo:latest
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/custom-build-img-repo:latest
  • Dockerfile
    コンテナイメージを作成するためのDockerfileです。CodeBuildの標準イメージにPythonとAWS CDKをインストールしています。ベースのイメージやPython、AWS CDKのバージョンなどは良しなに。
FROM public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:5.0

RUN pyenv install 3.12.3
RUN pyenv global 3.12.3
RUN python --version
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install -r requirements.txt
RUN npm install -g aws-cdk@2.142.1
  • requirements.txt
    Dockerfile内で使うrequirements.txtです。
aws-cdk-lib==2.142.1
constructs>=10.0.0,<11.0.0

この3ファイルをpushしたらパイプラインが実行されるので、後は待ってればコンテナイメージができます。
image.png

ECRにコンテナイメージが出来ていることも確認できます。
image.png

3. (Account B)cdk bootstrapの実行

次に、デプロイされる側のアカウント(Account B)でcdk bootstrapを実行しクロスアカウントでのデプロイに必要なRoleなどを作成します。
cdk bootstrapは、以下の記事を参考にしてください。

4. (Account A)別アカウントへデプロイするパイプラインの作成

では、本題の別アカウントへデプロイするパイプラインをCDKで作成します。
作成するパイプラインは以下のような流れを想定しています。
パイプラインはデプロイする環境や目的によって変わると思うので、アレンジしてください。
image.png

CDKのソースコードは以下のようになります。

CDKコード全体
app.py
#!/usr/bin/env python3
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_app_stack import CdkAppStack

app = cdk.App()

CdkAppStack = CdkAppStack(app, "CdkPipelineStack",
    )

app.synth()
cdk_app/cdk_app_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    aws_iam as iam,
    aws_codebuild as codebuild,
    aws_codepipeline as codepipeline,
    aws_events as events,
    aws_codecommit as codecommit,
    aws_logs as logs
)
from constructs import Construct
import os

# S3バケットを作成する共通関数
def CreateBucket(scope, bucket_id):

    cfn_bucket = s3.CfnBucket(
        scope,
        bucket_id,
        versioning_configuration = s3.CfnBucket.VersioningConfigurationProperty(
            status = "Enabled"
        ),
        bucket_name = "cdk-test-{0}-c3hw2bm4".format(bucket_id)
    )
    return cfn_bucket

# CodeBuild用のIAMロールを作成
def CreateBuildIAMRole(scope, artifact_s3bucket):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "codebuild.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Resource": "*",
                "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                ]
            },
            {
                "Effect": "Allow",
                "Resource": [
                    "arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
                    "arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
                ],
                "Action": [
                    "s3:PutObject",
                    "s3:GetObject",
                    "s3:GetObjectVersion",
                    "s3:GetBucketAcl",
                    "s3:GetBucketLocation"
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "sts:AssumeRole"
                ],
                "Resource": [
                    "arn:aws:iam::<Account B>:role/cdk-hnb659fds-*-<Account B>-*"
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "ecr:GetAuthorizationToken"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "ecr:BatchCheckLayerAvailability",
                    "ecr:GetDownloadUrlForLayer",
                    "ecr:BatchGetImage"
                ],
                "Resource": "*"
            }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "codebuild-exec-cdk-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="codebuild-exec-cdk-policy"
                )
            ],
            role_name="codebuild-exec-cdk-role"
        )
    
    return cfn_role

# CodePipeline用のIAMロールを作成
def CreatePipelineIAMRole(scope, artifact_s3bucket):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "codepipeline.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "codecommit:CancelUploadArchive",
                    "codecommit:GetBranch",
                    "codecommit:GetCommit",
                    "codecommit:GetUploadArchiveStatus",
                    "codecommit:UploadArchive"
                ],
                "Resource": "*"
            },
            {
                "Action": [
                    "codebuild:BatchGetBuilds",
                    "codebuild:StartBuild",
                    "codebuild:BatchGetBuildBatches",
                    "codebuild:StartBuildBatch"
                ],
                "Resource": "*",
                "Effect": "Allow"
            },
            {
                "Action": [
                    "s3:Get*",
                    "s3:Put*",
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
                    "arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
                ],
                "Effect": "Allow"
            },
            {
                "Action": [
                    "cloudwatch:*"
                ],
                "Resource": "*",
                "Effect": "Allow"
            }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "codepipeline-exec-cdk-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="codepipeline-exec-cdk-policy"
                )
            ],
            role_name="codepipeline-exec-cdk-role"
        )
    
    return cfn_role

# EvantBridge用のIAMロールを作成
def CreateExecPipelineIAMRole(scope, pipeline_arn):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "events.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                    "Effect": "Allow",
                    "Action": "codepipeline:StartPipelineExecution",
                    "Resource": pipeline_arn
                }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "exec-cdk-pipeline-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="exec-cdk-pipeline-policy"
                )
            ],
            role_name="exec-cdk-pipeline-role"
        )
    
    return cfn_role

# CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
def CodecommitEvents(scope, rep_name, pipeline_arn, exec_pipeline_role):

    event_pattern = {
        "source": ["aws.codecommit"],
        "detail-type": ["CodeCommit Repository State Change"],
        "resources": [
            "arn:aws:codecommit:{0}:{1}:{2}".format(
                "ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], rep_name)
        ],
        "detail": {
            "event": ["referenceCreated", "referenceUpdated"],
            "referenceType": ["branch"],
            "referenceName": ["main"]
        }
    }

    cfn_rule = events.CfnRule(
        scope,
        "change-cdk-repo-events",
        event_pattern=event_pattern,
        name="change-cdk-repo-events",
        state="ENABLED",
        targets=[
            events.CfnRule.TargetProperty(
                arn=pipeline_arn,
                id="change-cdk-repo-events",
                role_arn=exec_pipeline_role.attr_arn,
            )
        ]
    )

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)


        # アーティファクトバケットを作成
        artifact_s3bucket = CreateBucket(self, "artifact-bucket-cdk-pipeline")

        # CodeBuild用のIAMロールの作成
        build_role = CreateBuildIAMRole(self, artifact_s3bucket)
        # CodePipeline用のIAMロールの作成
        pipeline_role = CreatePipelineIAMRole(self, artifact_s3bucket)

        # CodeCommitリポジトリの作成
        cfn_repository = codecommit.CfnRepository(
            self,
            "cdk-repo",
            repository_name="cdk-repo",
            repository_description="For cdk"
        )

        # Pre_Deploy Buildプロジェクト用ロググループの作成
        cfn_changeset_log_group = logs.CfnLogGroup(
            self,
            "exec-cdk-changeset-project-loggroup",
            log_group_name="/aws/codebuild/exec-cdk-changeset-project",
            retention_in_days=14
        )

        # Pre_Deploy Buildプロジェクトの作成
        cfn_changeset_project = codebuild.CfnProject(
            self, 
            "exec-cdk-changeset-project",
            artifacts=codebuild.CfnProject.ArtifactsProperty(
                type="CODEPIPELINE",
            ),
            environment=codebuild.CfnProject.EnvironmentProperty(
                compute_type="BUILD_GENERAL1_SMALL",
                image="{0}.dkr.ecr.{1}.amazonaws.com/{2}:latest".format(
                    os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1", "custom-build-img-repo"),
                type="LINUX_CONTAINER",
                privileged_mode=False,
            ),
            service_role=build_role.attr_arn,
            source=codebuild.CfnProject.SourceProperty(
                type="CODEPIPELINE",
                build_spec="buildspec_changeset.yaml"
            ),
            logs_config=codebuild.CfnProject.LogsConfigProperty(
                cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
                    status="ENABLED",
                    group_name=cfn_changeset_log_group.log_group_name,
                ),
            ),
            name="exec-cdk-changeset-project",
        )

        # Deploy Buildプロジェクト用ロググループの作成
        cfn_deploy_log_group = logs.CfnLogGroup(
            self,
            "exec-cdk-deploy-project-loggroup",
            log_group_name="/aws/codebuild/exec-cdk-deploy-project",
            retention_in_days=14
        )

        # Deploy Buildプロジェクトの作成
        cfn_deploy_project = codebuild.CfnProject(
            self, 
            "exec-cdk-deploy-project",
            artifacts=codebuild.CfnProject.ArtifactsProperty(
                type="CODEPIPELINE",
            ),
            environment=codebuild.CfnProject.EnvironmentProperty(
                compute_type="BUILD_GENERAL1_SMALL",
                image="{0}.dkr.ecr.{1}.amazonaws.com/{2}:latest".format(
                    os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1", "custom-build-img-repo"),
                type="LINUX_CONTAINER",
                privileged_mode=False,
            ),
            service_role=build_role.attr_arn,
            source=codebuild.CfnProject.SourceProperty(
                type="CODEPIPELINE",
                build_spec="buildspec_deploy.yaml"
            ),
            logs_config=codebuild.CfnProject.LogsConfigProperty(
                cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
                    status="ENABLED",
                    group_name=cfn_deploy_log_group.log_group_name,
                ),
            ),
            name="exec-cdk-deploy-project",
        )

        # Destroy Buildプロジェクト用ロググループの作成
        cfn_destroy_log_group = logs.CfnLogGroup(
            self,
            "exec-cdk-destroy-project-loggroup",
            log_group_name="/aws/codebuild/exec-cdk-destroy-project",
            retention_in_days=14
        )

        # Destroy Buildプロジェクトの作成
        cfn_destroy_project = codebuild.CfnProject(
            self, 
            "exec-cdk-destroy-project",
            artifacts=codebuild.CfnProject.ArtifactsProperty(
                type="CODEPIPELINE",
            ),
            environment=codebuild.CfnProject.EnvironmentProperty(
                compute_type="BUILD_GENERAL1_SMALL",
                image="{0}.dkr.ecr.{1}.amazonaws.com/{2}:latest".format(
                    os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1", "custom-build-img-repo"),
                type="LINUX_CONTAINER",
                privileged_mode=False,
            ),
            service_role=build_role.attr_arn,
            source=codebuild.CfnProject.SourceProperty(
                type="CODEPIPELINE",
                build_spec="buildspec_destroy.yaml"
            ),
            logs_config=codebuild.CfnProject.LogsConfigProperty(
                cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
                    status="ENABLED",
                    group_name=cfn_destroy_log_group.log_group_name,
                ),
            ),
            name="exec-cdk-destroy-project",
        )

        # CodePipelineの作成
        # Sourceアクション(CodeCommit)の作成
        source_actions = []
        source_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Source",
                    owner="AWS",
                    provider="CodeCommit",
                    version="1"
                ),
                name="Source_codecommit",
                configuration={
                    "RepositoryName": cfn_repository.attr_name,
                    "BranchName": "main",
                    "PollForSourceChanges": False
                },
                namespace="SourceVariables_codecommit",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="SourceArtifact_codecommit"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Pre_Deploy Buildアクションの作成
        pre_deploy_build_actions = []
        pre_deploy_build_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Build",
                    owner="AWS",
                    provider="CodeBuild",
                    version="1"
                ),
                name="Pre_Deploy",
                configuration={
                    "ProjectName": cfn_changeset_project.name,
                    "PrimarySource": "SourceArtifact_codecommit"
                },
                input_artifacts=[
                    codepipeline.CfnPipeline.InputArtifactProperty(
                        name="SourceArtifact_codecommit"
                    )
                ],
                namespace="PreDeployVariables",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="PreDeployArtifact"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Deploy Buildアクションの作成
        deploy_build_actions = []
        deploy_build_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Build",
                    owner="AWS",
                    provider="CodeBuild",
                    version="1"
                ),
                name="Deploy",
                configuration={
                    "ProjectName": cfn_deploy_project.name,
                    "PrimarySource": "SourceArtifact_codecommit"
                },
                input_artifacts=[
                    codepipeline.CfnPipeline.InputArtifactProperty(
                        name="SourceArtifact_codecommit"
                    )
                ],
                namespace="DeployVariables",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="DeployArtifact"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Destroy Buildアクションの作成
        destroy_build_actions = []
        destroy_build_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Build",
                    owner="AWS",
                    provider="CodeBuild",
                    version="1"
                ),
                name="Destroy",
                configuration={
                    "ProjectName": cfn_destroy_project.name,
                    "PrimarySource": "SourceArtifact_codecommit"
                },
                input_artifacts=[
                    codepipeline.CfnPipeline.InputArtifactProperty(
                        name="SourceArtifact_codecommit"
                    )
                ],
                namespace="DestroyVariables",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="DestroyArtifact"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Approval(deploy)アクションの作成
        deploy_approval_actions = []
        deploy_approval_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Approval",
                    owner="AWS",
                    provider="Manual",
                    version="1"
                ),
                name="ManualApproval",
                configuration={
                    "CustomData": "After approval, deploy the resource."
                },
                run_order=1,
                timeout_in_minutes=1440
            )
        )

        # Approval(destroy)アクションの作成
        destroy_approval_actions = []
        destroy_approval_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Approval",
                    owner="AWS",
                    provider="Manual",
                    version="1"
                ),
                name="ManualApproval",
                configuration={
                    "CustomData": "After approval, delete the resource."
                },
                run_order=1,
                timeout_in_minutes=43200
            )
        )

        # Pipelineの作成
        cfn_pipeline = codepipeline.CfnPipeline(
            self,
            "pipeline-exec-cdk",
            role_arn=pipeline_role.attr_arn,
            stages=[
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=source_actions,
                    name="Source",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=pre_deploy_build_actions,
                    name="Pre_Deploy",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=deploy_approval_actions,
                    name="Pre_Deploy_ManualApproval",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=deploy_build_actions,
                    name="Deploy",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=destroy_approval_actions,
                    name="Pre_Destroy_ManualApproval",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=destroy_build_actions,
                    name="Destroy",
                )
            ],
            artifact_store=codepipeline.CfnPipeline.ArtifactStoreProperty(
                location=artifact_s3bucket.ref,
                type="S3",
            ),
            name="pipeline-exec-cdk",
            pipeline_type="V2",
            restart_execution_on_update=False
        )

        pipeline_arn = "arn:aws:codepipeline:{0}:{1}:{2}".format(
                "ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], cfn_pipeline.name)

        # EventBridge用のIAMロールの作成
        exec_pipeline_role = CreateExecPipelineIAMRole(self, pipeline_arn)
        # CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
        CodecommitEvents(self, cfn_repository.attr_name, pipeline_arn, exec_pipeline_role)

では、処理毎に順に説明します。

S3バケットの作成

パイプラインのアーティファクト用バケットを作成します。

cdk_app/cdk_app_stack.py
(中略)

# S3バケットを作成する共通関数
def CreateBucket(scope, bucket_id):

    cfn_bucket = s3.CfnBucket(
        scope,
        bucket_id,
        versioning_configuration = s3.CfnBucket.VersioningConfigurationProperty(
            status = "Enabled"
        ),
        bucket_name = "cdk-test-{0}-c3hw2bm4".format(bucket_id)
    )
    return cfn_bucket

(中略)

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # アーティファクトバケットを作成
        artifact_s3bucket = CreateBucket(self, "artifact-bucket-cdk-pipeline")

(中略)

IAMロールの作成

CodePipelineとCodeBuildの実行に必要なIAMロールを作成します。
今回、CodeBuildでAWS CDKを実行し、別アカウントにリソースをデプロイするので、CodeBuildで使用するIAMロールには、AssumeRoleの許可を付けて、Account BのロールにAssumeRoleできるようにします。
あとは、ECRのカスタムイメージを使うので、必要な権限を付けておきましょう。

cdk_app/cdk_app_stack.py
(中略)

# CodeBuild用のIAMロールを作成
def CreateBuildIAMRole(scope, artifact_s3bucket):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "codebuild.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Resource": "*",
                "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                ]
            },
            {
                "Effect": "Allow",
                "Resource": [
                    "arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
                    "arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
                ],
                "Action": [
                    "s3:PutObject",
                    "s3:GetObject",
                    "s3:GetObjectVersion",
                    "s3:GetBucketAcl",
                    "s3:GetBucketLocation"
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "sts:AssumeRole"
                ],
                "Resource": [
                    "arn:aws:iam::<Account B>:role/cdk-hnb659fds-*-<Account B>-*"
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "ecr:GetAuthorizationToken"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "ecr:BatchCheckLayerAvailability",
                    "ecr:GetDownloadUrlForLayer",
                    "ecr:BatchGetImage"
                ],
                "Resource": "*"
            }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "codebuild-exec-cdk-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="codebuild-exec-cdk-policy"
                )
            ],
            role_name="codebuild-exec-cdk-role"
        )
    
    return cfn_role

# CodePipeline用のIAMロールを作成
def CreatePipelineIAMRole(scope, artifact_s3bucket):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "codepipeline.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "codecommit:CancelUploadArchive",
                    "codecommit:GetBranch",
                    "codecommit:GetCommit",
                    "codecommit:GetUploadArchiveStatus",
                    "codecommit:UploadArchive"
                ],
                "Resource": "*"
            },
            {
                "Action": [
                    "codebuild:BatchGetBuilds",
                    "codebuild:StartBuild",
                    "codebuild:BatchGetBuildBatches",
                    "codebuild:StartBuildBatch"
                ],
                "Resource": "*",
                "Effect": "Allow"
            },
            {
                "Action": [
                    "s3:Get*",
                    "s3:Put*",
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
                    "arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
                ],
                "Effect": "Allow"
            },
            {
                "Action": [
                    "cloudwatch:*"
                ],
                "Resource": "*",
                "Effect": "Allow"
            }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "codepipeline-exec-cdk-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="codepipeline-exec-cdk-policy"
                )
            ],
            role_name="codepipeline-exec-cdk-role"
        )
    
    return cfn_role

(中略)

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

(中略)

        # CodeBuild用のIAMロールの作成
        build_role = CreateBuildIAMRole(self, artifact_s3bucket)
        # CodePipeline用のIAMロールの作成
        pipeline_role = CreatePipelineIAMRole(self, artifact_s3bucket)

(中略)

CodeCommit、Sourceアクションの作成

パイプラインの入力になるCodeCommitとSourceアクションを作成します。

cdk_app/cdk_app_stack.py
(中略)

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

(中略)

        # CodeCommitリポジトリの作成
        cfn_repository = codecommit.CfnRepository(
            self,
            "cdk-repo",
            repository_name="cdk-repo",
            repository_description="For cdk"
        )

(中略)

        # CodePipelineの作成
        # Sourceアクション(CodeCommit)の作成
        source_actions = []
        source_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Source",
                    owner="AWS",
                    provider="CodeCommit",
                    version="1"
                ),
                name="Source_codecommit",
                configuration={
                    "RepositoryName": cfn_repository.attr_name,
                    "BranchName": "main",
                    "PollForSourceChanges": False
                },
                namespace="SourceVariables_codecommit",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="SourceArtifact_codecommit"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

(中略)

CodeBuildプロジェクト、Buildアクションの作成

Pre Deploy(変更セットの作成)、Deploy、DestroyそれぞれのCodeBuildプロジェクト、Buildアクションとログ出力用のロググループを作成します。
それぞれのCodeBuildプロジェクトでは、事前に作成したカスタムコンテナイメージを使用して起動するように設定しています。

cdk_app/cdk_app_stack.py
(中略)

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

(中略)

        # Pre_Deploy Buildプロジェクト用ロググループの作成
        cfn_changeset_log_group = logs.CfnLogGroup(
            self,
            "exec-cdk-changeset-project-loggroup",
            log_group_name="/aws/codebuild/exec-cdk-changeset-project",
            retention_in_days=14
        )

        # Pre_Deploy Buildプロジェクトの作成
        cfn_changeset_project = codebuild.CfnProject(
            self, 
            "exec-cdk-changeset-project",
            artifacts=codebuild.CfnProject.ArtifactsProperty(
                type="CODEPIPELINE",
            ),
            environment=codebuild.CfnProject.EnvironmentProperty(
                compute_type="BUILD_GENERAL1_SMALL",
                image="{0}.dkr.ecr.{1}.amazonaws.com/{2}:latest".format(
                    os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1", "custom-build-img-repo"),
                type="LINUX_CONTAINER",
                privileged_mode=False,
            ),
            service_role=build_role.attr_arn,
            source=codebuild.CfnProject.SourceProperty(
                type="CODEPIPELINE",
                build_spec="buildspec_changeset.yaml"
            ),
            logs_config=codebuild.CfnProject.LogsConfigProperty(
                cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
                    status="ENABLED",
                    group_name=cfn_changeset_log_group.log_group_name,
                ),
            ),
            name="exec-cdk-changeset-project",
        )

        # Deploy Buildプロジェクト用ロググループの作成
        cfn_deploy_log_group = logs.CfnLogGroup(
            self,
            "exec-cdk-deploy-project-loggroup",
            log_group_name="/aws/codebuild/exec-cdk-deploy-project",
            retention_in_days=14
        )

        # Deploy Buildプロジェクトの作成
        cfn_deploy_project = codebuild.CfnProject(
            self, 
            "exec-cdk-deploy-project",
            artifacts=codebuild.CfnProject.ArtifactsProperty(
                type="CODEPIPELINE",
            ),
            environment=codebuild.CfnProject.EnvironmentProperty(
                compute_type="BUILD_GENERAL1_SMALL",
                image="{0}.dkr.ecr.{1}.amazonaws.com/{2}:latest".format(
                    os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1", "custom-build-img-repo"),
                type="LINUX_CONTAINER",
                privileged_mode=False,
            ),
            service_role=build_role.attr_arn,
            source=codebuild.CfnProject.SourceProperty(
                type="CODEPIPELINE",
                build_spec="buildspec_deploy.yaml"
            ),
            logs_config=codebuild.CfnProject.LogsConfigProperty(
                cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
                    status="ENABLED",
                    group_name=cfn_deploy_log_group.log_group_name,
                ),
            ),
            name="exec-cdk-deploy-project",
        )

        # Destroy Buildプロジェクト用ロググループの作成
        cfn_destroy_log_group = logs.CfnLogGroup(
            self,
            "exec-cdk-destroy-project-loggroup",
            log_group_name="/aws/codebuild/exec-cdk-destroy-project",
            retention_in_days=14
        )

        # Destroy Buildプロジェクトの作成
        cfn_destroy_project = codebuild.CfnProject(
            self, 
            "exec-cdk-destroy-project",
            artifacts=codebuild.CfnProject.ArtifactsProperty(
                type="CODEPIPELINE",
            ),
            environment=codebuild.CfnProject.EnvironmentProperty(
                compute_type="BUILD_GENERAL1_SMALL",
                image="{0}.dkr.ecr.{1}.amazonaws.com/{2}:latest".format(
                    os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1", "custom-build-img-repo"),
                type="LINUX_CONTAINER",
                privileged_mode=False,
            ),
            service_role=build_role.attr_arn,
            source=codebuild.CfnProject.SourceProperty(
                type="CODEPIPELINE",
                build_spec="buildspec_destroy.yaml"
            ),
            logs_config=codebuild.CfnProject.LogsConfigProperty(
                cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
                    status="ENABLED",
                    group_name=cfn_destroy_log_group.log_group_name,
                ),
            ),
            name="exec-cdk-destroy-project",
        )

(中略)

        # Pre_Deploy Buildアクションの作成
        pre_deploy_build_actions = []
        pre_deploy_build_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Build",
                    owner="AWS",
                    provider="CodeBuild",
                    version="1"
                ),
                name="Pre_Deploy",
                configuration={
                    "ProjectName": cfn_changeset_project.name,
                    "PrimarySource": "SourceArtifact_codecommit"
                },
                input_artifacts=[
                    codepipeline.CfnPipeline.InputArtifactProperty(
                        name="SourceArtifact_codecommit"
                    )
                ],
                namespace="PreDeployVariables",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="PreDeployArtifact"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Deploy Buildアクションの作成
        deploy_build_actions = []
        deploy_build_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Build",
                    owner="AWS",
                    provider="CodeBuild",
                    version="1"
                ),
                name="Deploy",
                configuration={
                    "ProjectName": cfn_deploy_project.name,
                    "PrimarySource": "SourceArtifact_codecommit"
                },
                input_artifacts=[
                    codepipeline.CfnPipeline.InputArtifactProperty(
                        name="SourceArtifact_codecommit"
                    )
                ],
                namespace="DeployVariables",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="DeployArtifact"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

        # Destroy Buildアクションの作成
        destroy_build_actions = []
        destroy_build_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Build",
                    owner="AWS",
                    provider="CodeBuild",
                    version="1"
                ),
                name="Destroy",
                configuration={
                    "ProjectName": cfn_destroy_project.name,
                    "PrimarySource": "SourceArtifact_codecommit"
                },
                input_artifacts=[
                    codepipeline.CfnPipeline.InputArtifactProperty(
                        name="SourceArtifact_codecommit"
                    )
                ],
                namespace="DestroyVariables",
                output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
                    name="DestroyArtifact"
                )],
                region="ap-northeast-1",
                run_order=1
            )
        )

(中略)

Manual Approvalアクションの作成

DeployとDestroyの前に置く手動承認アクションを2つ作成します。それぞれ、目的に合わせてタイムアウト値(分)を設定してください。

cdk_app/cdk_app_stack.py
(中略)

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

(中略)

        # Approval(deploy)アクションの作成
        deploy_approval_actions = []
        deploy_approval_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Approval",
                    owner="AWS",
                    provider="Manual",
                    version="1"
                ),
                name="ManualApproval",
                configuration={
                    "CustomData": "After approval, deploy the resource."
                },
                run_order=1,
                timeout_in_minutes=1440
            )
        )

        # Approval(destroy)アクションの作成
        destroy_approval_actions = []
        destroy_approval_actions.append(
            codepipeline.CfnPipeline.ActionDeclarationProperty(
                action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
                    category="Approval",
                    owner="AWS",
                    provider="Manual",
                    version="1"
                ),
                name="ManualApproval",
                configuration={
                    "CustomData": "After approval, delete the resource."
                },
                run_order=1,
                timeout_in_minutes=43200
            )
        )

(中略)

パイプラインの作成

これまでに作成した各アクションを順番に設定してパイプラインを作成します。

cdk_app/cdk_app_stack.py
(中略)

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

(中略)

        # Pipelineの作成
        cfn_pipeline = codepipeline.CfnPipeline(
            self,
            "pipeline-exec-cdk",
            role_arn=pipeline_role.attr_arn,
            stages=[
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=source_actions,
                    name="Source",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=pre_deploy_build_actions,
                    name="Pre_Deploy",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=deploy_approval_actions,
                    name="Pre_Deploy_ManualApproval",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=deploy_build_actions,
                    name="Deploy",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=destroy_approval_actions,
                    name="Pre_Destroy_ManualApproval",
                ),
                codepipeline.CfnPipeline.StageDeclarationProperty(
                    actions=destroy_build_actions,
                    name="Destroy",
                )
            ],
            artifact_store=codepipeline.CfnPipeline.ArtifactStoreProperty(
                location=artifact_s3bucket.ref,
                type="S3",
            ),
            name="pipeline-exec-cdk",
            pipeline_type="V2",
            restart_execution_on_update=False
        )

(中略)

パイプラインの自動起動設定

最後にCodeCommitにpushされた時にパイプラインが起動するようにEventBridgeルールとIAMロールを作成します。

cdk_app/cdk_app_stack.py
(中略)

# EvantBridge用のIAMロールを作成
def CreateExecPipelineIAMRole(scope, pipeline_arn):

    assume_role_policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "events.amazonaws.com"
                }
            }
        ]
    }

    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                    "Effect": "Allow",
                    "Action": "codepipeline:StartPipelineExecution",
                    "Resource": pipeline_arn
                }
        ]
    }

    cfn_role = iam.CfnRole(
            scope,
            "exec-cdk-pipeline-role",
            assume_role_policy_document=assume_role_policy_document,
            policies=[
                iam.CfnRole.PolicyProperty(
                    policy_document=policy_document,
                    policy_name="exec-cdk-pipeline-policy"
                )
            ],
            role_name="exec-cdk-pipeline-role"
        )
    
    return cfn_role

# CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
def CodecommitEvents(scope, rep_name, pipeline_arn, exec_pipeline_role):

    event_pattern = {
        "source": ["aws.codecommit"],
        "detail-type": ["CodeCommit Repository State Change"],
        "resources": [
            "arn:aws:codecommit:{0}:{1}:{2}".format(
                "ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], rep_name)
        ],
        "detail": {
            "event": ["referenceCreated", "referenceUpdated"],
            "referenceType": ["branch"],
            "referenceName": ["main"]
        }
    }

    cfn_rule = events.CfnRule(
        scope,
        "change-cdk-repo-events",
        event_pattern=event_pattern,
        name="change-cdk-repo-events",
        state="ENABLED",
        targets=[
            events.CfnRule.TargetProperty(
                arn=pipeline_arn,
                id="change-cdk-repo-events",
                role_arn=exec_pipeline_role.attr_arn,
            )
        ]
    )

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

(中略)

        pipeline_arn = "arn:aws:codepipeline:{0}:{1}:{2}".format(
                "ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], cfn_pipeline.name)

        # EventBridge用のIAMロールの作成
        exec_pipeline_role = CreateExecPipelineIAMRole(self, pipeline_arn)
        # CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
        CodecommitEvents(self, cfn_repository.attr_name, pipeline_arn, exec_pipeline_role)

コードを作ったら、cdk deployしてパイプラインを作成します。
※まだCodeCommitに何もファイルがないので、実行は失敗します。
image.png

5. (Account A)別アカウントへデプロイ

パイプラインの作成ができたので、デプロイに必要なファイルをCodeCommitにpushします。
必要なファイルは以下のとおりです。

  • cdk-app/app.py
  • cdk-app/cdk_app/cdk_app_stack.py
    Account BにデプロイするCDKのソースコードです。S3バケットを作るだけの単純な内容です。
    Account Bにデプロイするので、CdkAppStackのenvパラメータにAccount BのアカウントIDを渡しています。
cdk-app/app.py
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_app_stack import CdkAppStack

app = cdk.App()
CdkAppStack(
        app,
        "CdkAppStack",
        env={'account': '<Account B>', 'region': 'ap-northeast-1'}
    )

app.synth()
cdk-app/cdk_app/cdk_app_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    RemovalPolicy
)
from constructs import Construct

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-2024mmdd",
            removal_policy=RemovalPolicy.DESTROY
        )
  • buildspec_changeset.yaml
  • buildspec_deploy.yaml
  • buildspec_destroy.yaml
    それぞれのCodeBuildプロジェクトで使用するbuildspecファイルです。中身はほぼ同じでcdkのコマンド部が違ってるだけです。
buildspec_changeset.yaml
version: 0.2
phases:
  install:
    commands:
      - python --version
      - cdk --version
      - mkdir work
      - cd work/
      - mkdir cdk-app
      - cd cdk-app/
      - cdk init app --language python
      - ls -lR ./
      - cp ../../cdk-app/app.py .
      - cp ../../cdk-app/cdk_app/* cdk_app/.
  build:
    commands:
      - echo Create Changeset...
      - cdk deploy --require-approval never --no-execute --change-set-name pre-deploy
buildspec_deploy.yaml
version: 0.2
phases:
  install:
    commands:
      - python --version
      - cdk --version
      - mkdir work
      - cd work/
      - mkdir cdk-app
      - cd cdk-app/
      - cdk init app --language python
      - ls -lR ./
      - cp ../../cdk-app/app.py .
      - cp ../../cdk-app/cdk_app/* cdk_app/.
  build:
    commands:
      - echo Deploy...
      - cdk deploy --require-approval never
buildspec_destroy.yaml
version: 0.2
phases:
  install:
    commands:
      - python --version
      - cdk --version
      - mkdir work
      - cd work/
      - mkdir cdk-app
      - cd cdk-app/
      - cdk init app --language python
      - ls -lR ./
      - cp ../../cdk-app/app.py .
      - cp ../../cdk-app/cdk_app/* cdk_app/.
  build:
    commands:
      - echo Destroy...
      - cdk destroy -f

ファイルをpushするとパイプラインが実行されます。
image.png

Pre_Deployステージの実行後、Pre_Deploy_ManualApprovalステージで止まると思うので、Account BのCloudFormationのコンソールで変更セットを確認してみます。

image.png

変更セットが出来ています。
パイプラインに戻って、パイプラインを続行しましょう。Pre_Deploy_ManualApprovalステージを承認してパイプラインを進めます。
パイプラインが進むと、Deployステージが実行され、Pre_Destroy_ManualApprovalステージで一時停止します。
image.png

Account BにS3バケットが出来ているはずなので、確認してみます。
image.png

きちんと出来てました!
パイプラインに戻って、Pre_Destroy_ManualApprovalステージを承認してパイプラインを進めます。
パイプラインが進むと、Destroyステージが実行されます。
image.png

Account BのS3バケットを確認すると先ほど作成したバケットが削除されていることが確認できます。
image.png

これで、パイプラインによる別アカウントへのデプロイが確認できました!

まとめ

パイプラインによるクロスアカウントでのCDKデプロイを確認しました。前回、Cloud9でやったことの応用ですが、パイプラインでインフラのデプロイをしたい場合は参考にしてみてください。
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0