CodePipelineを使用して別アカウントにCloudFormationスタックをデプロイしてみた

2022.04.30

釣りの専門用語が分からなくて困ってます。AWS事業本部コンサルティング部の後藤です。

CodePipelineを使用して異なるアカウントにデプロイを行う場合、アカウントを跨ぐような形でCodePipelineを構築するのが一般的かと思いますが、AWSのナレッジセンターに1つのアカウント上でCodePipelineを完結させつつ、別アカウントにCloudFormationのスタックを構築する方法を見つけましたので、こちらを参考にしながらLambda作成を試してみました。

それでは、やっていきましょう!

構成図

今回構築する構成図は以下となります。

CodePipelineを構築する側をAccount(A)、CloudFormationを作成する側をAccount(B)とします。

事前準備

  • CodeBuildサービスロール作成
  • CodePipelineサービスロール作成

今回サービスロールは基本的にコンソール作成時に自動生成されるものを使用しているため、こちらは省略いたします。

Account(A) CodePipelineサービスロール修正

Account(A)のCodePipelineサービスロールから修正していきます。CodePipelineサービスロール自体はCodePipeline作成時に自動生成されたものを使用しています。

CodePipelineがAccount(B)に対してAssumeRole可能にするために以下の権限を持つIAMポリシーを作成してCodePipelineのサービスロールに付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AssumeRolePolicy",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": [
                "arn:aws:iam::[Account(B)ID]:role/*"
            ]
        }
    ]
}

Account(A) カスタマー管理のKMSキーを作成

アカウントを跨いでArtifactの受け渡しを可能とするため、Artifactの暗号化キーをAccount(A)にKMSで作成します。Pipeline自体は東京リージョンに構築するため、KMSキーも東京リージョンに作成しています。

キーのタイプは対称で作成し、キーユーザにはCodePipelineとCodeBuildのサービスロールを指定、他アカウントにAccount(B)を指定しています。キーポリシーとしては以下の通りです。

{
    "Id": "key-consolepolicy-3",
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Enable IAM User Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::Account(A):root"
            },
            "Action": "kms:*",
            "Resource": "*"
        },
        {
            "Sid": "Allow access for Key Administrators",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::Account(A):role/cm-goto.naoki"
                ]
            },
            "Action": [
                "kms:Create*",
                "kms:Describe*",
                "kms:Enable*",
                "kms:List*",
                "kms:Put*",
                "kms:Update*",
                "kms:Revoke*",
                "kms:Disable*",
                "kms:Get*",
                "kms:Delete*",
                "kms:TagResource",
                "kms:UntagResource",
                "kms:ScheduleKeyDeletion",
                "kms:CancelKeyDeletion"
            ],
            "Resource": "*"
        },
        {
            "Sid": "Allow use of the key",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::Account(A):role/service-role/AWSCodePipelineServiceRole",
                    "arn:aws:iam::Account(A):role/service-role/AWSCodeBuildServiceRole",
                    "arn:aws:iam::Account(B):root"
                ]
            },
            "Action": [
                "kms:Encrypt",
                "kms:Decrypt",
                "kms:ReEncrypt*",
                "kms:GenerateDataKey*",
                "kms:DescribeKey"
            ],
            "Resource": "*"
        },
        {
            "Sid": "Allow attachment of persistent resources",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::Account(A):role/service-role/AWSCodePipelineServiceRole",
                    "arn:aws:iam::Account(A):role/service-role/AWSCodeBuildServiceRole",
                    "arn:aws:iam::Account(B):root"
                ]
            },
            "Action": [
                "kms:CreateGrant",
                "kms:ListGrants",
                "kms:RevokeGrant"
            ],
            "Resource": "*",
            "Condition": {
                "Bool": {
                    "kms:GrantIsForAWSResource": "true"
                }
            }
        }
    ]
}

Account(A) S3 Bucket作成

Account(A)にCodePipelineのArtifactやSAMのPackage等を出力するS3 Bucketを作成します。

作成時、暗号化の設定にて上記で作成したKMSキーを設定します。

また、Bucket PolicyでAccount(B)にアクセス権を付与するよう設定します。

{
  "Id": "Policy1553183091390",
  "Version": "2012-10-17",
  "Statement": [{
      "Sid": "",
      "Action": [
        "s3:Get*",
        "s3:Put*"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::BucketName/*",
      "Principal": {
        "AWS": [
          "arn:aws:iam::Account(B)ID:root"
        ]
      }
    },
    {
      "Sid": "",
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::BucketName",
      "Principal": {
        "AWS": [
          "arn:aws:iam::Account(B)ID:root"
        ]
      }
    }
  ]
}

Account(A) CodeBuildサービスロール修正

CodeBuildで使用するサービスロールも修正していきます。CodeBuildサービスロールも自動的に作成されるものを使用するのですが、今回はS3バケットを指定するため以下権限をもったIAMポリシーを追加で付与しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::BucketName",
                "arn:aws:s3:::BucketName/*"
            ],
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation"
            ]
        }
    ]
}

Account(B) クロスアカウントIAMロール作成

Account(B)にAccount(A)のCodePipelineサービスロールからAssumeRoleするIAMロールを作成します。

こちらのIAMロールには2つのIAMポリシーを付与します。

1つ目のIAMポリシーはCloudFormationとArtifact等を格納するS3へのアクセス権限を与えます。

{
  "Version": "2012-10-17",
  "Statement": [{
      "Effect": "Allow",
      "Action": [
        "cloudformation:*",
        "iam:PassRole"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:Get*",
        "s3:Put*",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::Bucket/*"
      ]
    }
  ]
}

2つ目のIAMポリシーでは、KMSのAPIアクションを許可する権限を与えます。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "kms:DescribeKey",
      "kms:GenerateDataKey*",
      "kms:Encrypt",
      "kms:ReEncrypt*",
      "kms:Decrypt"
    ],
    "Resource": [
      "arn:aws:kms:ap-northeast-1:Account(A):key/KMS-ARN"
    ]
  }]
}

KMS-ARNは上記で作成したKMSキーのARN値に変更する必要があります。

上記2つのIAMポリシーを付与してIAMロールを作成します。IAMロールの信頼エンティティには、Account(A)を紐づけます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::Account(A):root"
            },
            "Action": "sts:AssumeRole",
            "Condition": {}
        }
    ]
}

Account(B) CloudFormationサービスロール作成

Account(B)のCloudFormation実行時に使用するサービスロールになります。

本来であれば細かく権限を設定した方が良いのですが、今回はお試し検証ということでAdministrator権限を与えています。

Account(A) CodePipeline構築

やっときましたCodePipeline構築です。今回はソースステージにCodeCommit、ビルドステージにCodeBuildを使用します。CodeBuildで実行する内容に関しては後述致します。

問題はデプロイステージになります。デプロイステージではCloudFormationを選択するのですが、実行ロールにはAccount(B)のサービスロールを指定する必要があるなど、コンソール上だけでは設定が出来ないためjsonに起こしてAWS CLIを使用して構築を行っていきます。

大枠な設定項目を理解するため、一度Account(A)のCloudFormationスタックを作成するCodePipelineを作成し、そこから以下コマンドを使用して設定をjsonに出力しました。

aws codepipeline get-pipeline --name PipelineName > codepipeline.json

設定項目を今回の仕様に変更したjsonが以下になります。

{
    "pipeline": {
        "name": "PipelineName",
        "roleArn": "arn:aws:iam::Account(A):role/service-role/AWSCodePipelineServiceRole",
        "artifactStore": {
            "type": "S3",
            "location": "BucketName",
            "encryptionKey": {
                "id": "arn:aws:kms:ap-northeast-1:Account(A):key/KMS-ID",
                "type": "KMS"
            }
        },
        "stages": [
            {
                "name": "Source",
                "actions": [
                    {
                        "name": "Source",
                        "actionTypeId": {
                            "category": "Source",
                            "owner": "AWS",
                            "provider": "CodeCommit",
                            "version": "1"
                        },
                        "runOrder": 1,
                        "configuration": {
                            "BranchName": "master",
                            "OutputArtifactFormat": "CODE_ZIP",
                            "PollForSourceChanges": "false",
                            "RepositoryName": "CodeCommitRepositoryName"
                        },
                        "outputArtifacts": [
                            {
                                "name": "SourceArtifact"
                            }
                        ],
                        "inputArtifacts": [],
                        "region": "ap-northeast-1",
                        "namespace": "SourceVariables"
                    }
                ]
            },
            {
                "name": "Build",
                "actions": [
                    {
                        "name": "Build",
                        "actionTypeId": {
                            "category": "Build",
                            "owner": "AWS",
                            "provider": "CodeBuild",
                            "version": "1"
                        },
                        "runOrder": 1,
                        "configuration": {
                            "ProjectName": "LambdaFunctionBuild"
                        },
                        "outputArtifacts": [
                            {
                                "name": "BuildArtifact"
                            }
                        ],
                        "inputArtifacts": [
                            {
                                "name": "SourceArtifact"
                            }
                        ],
                        "region": "ap-northeast-1",
                        "namespace": "BuildVariables"
                    }
                ]
            },
            {
                "name": "Deploy",
                "actions": [
                    {
                        "name": "Deploy",
                        "actionTypeId": {
                            "category": "Deploy",
                            "owner": "AWS",
                            "provider": "CloudFormation",
                            "version": "1"
                        },
                        "runOrder": 1,
                        "configuration": {
                            "ActionMode": "CREATE_UPDATE",
                            "Capabilities": "CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND",
                            "RoleArn": "arn:aws:iam::Account(B):role/CloudFormationServiceRole",
                            "StackName": "LambdaFunction",
                            "TemplatePath": "BuildArtifact::packaged-template.yml"
                        },
                        "outputArtifacts": [],
                        "inputArtifacts": [
                            {
                                "name": "BuildArtifact"
                            }
                        ],
                        "roleArn": "arn:aws:iam::Account(B):role/CrossAccountRole",
                        "region": "ap-northeast-1",
                        "namespace": "DeployVariables"
                    }
                ]
            }
        ],
        "version": 1
    }
}

変更した箇所は以下の通りです。

  • artifactStoreencryptionKeyを追加
  • Deploy Stage configurationRoleArnをAccount(B)のCloudFormationサービスロールに指定
  • Deploy Stage roleArnを追加、Account(B)のクロスアカウントIAMロールに指定

また、json出力後はmetadataが記載されているため、そちらも削除しています。

修正後、上記jsonの設定でCodePipelineを以下AWS CLIで作成します。

aws codepipeline create-pipeline --cli-input-json file://codepipeline.json

リポジトリにあげるデータ関連

リポジトリにはLambdaを構築するSAM TemplateとLambda関数、CodeBuildで使用するbuildspec.ymlを含んでいます。内容は簡単に以下の通りです。

・SAM Template

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  helloworld

  Sample SAM Template for helloworld

Globals:
  Function:
    Timeout: 5

Resources:
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      EndpointConfiguration: REGIONAL
      Name: HelloWorldApi
      StageName: prod
  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: HelloWorld
      CodeUri: function/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      Events:
        GetApi:
          Type: Api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /hello
            Method: GET

・buildspec.yml

version: 0.2
phases:
  install:
    runtime-versions:
      python: 3.9
  build:
    commands:
      - sam package --template-file template.yaml --s3-bucket BucketName --output-template-file packaged-template.yml
artifacts:
  files:
    - packaged-template.yml

CodeCommitにPushしてパイプラインを実行します。

・・・あれ?動かないぞ?ナンデー?

EventBridgeの作成

CodePipelineをAWS CLIで作成したため、本来自動生成されるEventBridgeが設定されていませんでした。そりゃ動かないですね。

EventBridgeのルールを以下のように設定します。

{
  "source": ["aws.codecommit"],
  "detail-type": ["CodeCommit Repository State Change"],
  "resources": ["arn:aws:codecommit:ap-northeast-1:Account(A):CodeCommitRepositoryName"],
  "detail": {
    "event": ["referenceCreated", "referenceUpdated"],
    "referenceType": ["branch"],
    "referenceName": ["master"]
  }
}

EventBridgeのターゲットには上記で作成したCodePipelineのARNを設定します。CodePipelineのARNはCodePipelineコンソール画面にある「設定」から確認頂けます。

パイプライン実行

改めてCodeCommitにPush!無事動く事が確認できました。そして・・・Account(B)のCloudFormationでスタックが作成され、SAMで指定したAPI Gateway、Lambdaが構築出来ていること確認できました!

まとめ

CodePipelineを使用して別アカウントにCloudFormationのスタックを作成してLambda構築を試してみました。やはりマルチアカウントとなると権限周りが煩雑になりますね。CloudFormation実行でエラーが発生したときは詳細を確認するのにアカウントBを確認しなきゃいけなかったりと運用的にも手間がかかりそうですが、パイプラインを1つのアカウントにまとめたい等の要件には合っているかもしれません。この記事が何方かのお役に立てば幸いです。