CodePipeline から別アカウント上の ECR を Source Action から指定したい。 これはそれなりに存在するユースケースではないでしょうか。
この実現にマジで苦しんだので、葛藤の記録を残しておきます。
概観
ポイントとなるのは以下の 2 点でした。
- CodePipeline から ECR へのアクセスはクロスアカウントアクセス。このため、CodePipeline は ECR source action 実行時に assume-role が必要
- CodePipeline で生成されるアーティファクトは S3 バケット(Artifact Store)上で暗号化される
結果として、以下の対応が必要になります。
- Account A 上の ECR のバケットポリシーで Account B からのアクセスを許可
- Account B 上の CodePipeline から、Account A で定義したクロスアカウント用 Role を assume-role するよう設定
- Account A 上でクロスアカウントアクセス用の Role を定義。以下のポリシーを付与する必要がある。
- Account A の ECR からイメージを PULL できる
- Account B 上の Artifact Store を読み書きできる
- Account B 上の CMK を利用できる
バケットポリシー
まず、ECR のバケットポリシーは以下のようなイメージになります。これにより、アカウント B からイメージの PULL を実行できるようになります。
data "aws_iam_policy_document" "cross_account_pull_policy" { statement { actions = [ "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability", "ecr:BatchGetImage" ] principals { type = "AWS" identifiers = ["arn:aws:iam::[AWSアカウントBのID]:root"] } } }
CodePipelie からクロスアカウントアクセス用の Role を引き受ける
わかりにくいのは 3. の CodePipeline からの assume-role の指定方法です。
実は CodePipeline の各 action では role_arn
が指定可能です。
ここでクロスアカウント用の Role を指定することにより、CodePipeline が一時的に別アカウントの Role を引き受けることができます。
resource "aws_codepipeline" "this" { name = var.name role_arn = aws_iam_role.codepipeline.arn (snip) stage { name = "Source" # デプロイ対象のコンテナイメージをECRから読み取る action { name = "ContainerImageDefinition" category = "Source" owner = "AWS" provider = "ECR" version = "1" output_artifacts = [local.image_artifacts_name] run_order = 2 role_arn = var.cross_account_codepipeline_access_role_arn # クロスアカウントアクセス用 Role の ARN を指定 configuration = { RepositoryName = var.repository_name ImageTag = var.image_tag } }
Account A 上でのクロスアカウントアクセス用ロールの定義
では、CodePipeline が引き受ける、Account A 上の Role はどのようなものでしょうか。 以下は当然の要件です。
- Account B を Principal に指定している
- Account A 上の ECR から Image Pull できるポリシーを付与
わかりづらいのは以下の 2 点です。
- Account B 上の Artifact Store へ読み書きできるポリシーを付与
- Account B 上の CMK の利用を可能にするポリシーを付与
Artifact Store へ読み書きするポリシーが必要なのは、CodePipeline が ECR の情報を書き込まないといけないためです。CodePipeline は各 Action の出力(Artifact)を S3 Bucket (Artifact Store)経由でやりとりします。
当然 ECR の情報も CodePipeline が Artifact Store へ書き込むことになります。 CodePipeline は Account A の Role を引き受けているため、この Role 自身が Account B の Artifact Store への書き込み権限を必要とします。
さらに話をややこしくするのは、CodePipeline は Artifact Store へ書き込むデータを暗号化することです。
CodePipeline 内のデータは、サービス所有の KMS キーを使用して保管時に暗号化されます。コードアーティファクトはカスタマー所有の S3 バケットに保存され、デフォルトの AWS マネージド SSE-KMS 暗号化キーまたはカスタマーマネージド SSE-KMS キーで暗号化されます。
多くの場合、S3 bucket の暗号化は S3 所有の KMS キーで暗号化します。しかし、Account A は Account B の KMS キーを知り得ません。 そのため、Account B 上で CMK[^1] を明示的に作成し、Artifact Store に設定し、その CMK を Account A で利用可能にする必要があります。
^1: Customer Managed Key
Artifact Store への CMK 設定は、aws_codepipeline
リソースの artifact_store
block で行えます。
resource "aws_codepipeline" "this" { name = var.name role_arn = aws_iam_role.codepipeline.arn artifact_store { type = "S3" location = aws_s3_bucket.artifact_store.bucket # CMK の明示的な設定 encryption_key { id = var.artifact_store_encryption_key_arn type = "KMS" } }
CMK を別アカウントに公開するためには、CMK のキーポリシーを明示的に設定する必要があります。 CodePipeline が当該の CMK を使えるようにすることも合わせると、以下のような設定になりました。
data "aws_caller_identity" "current" {} data "aws_iam_policy_document" "cross_account_key_policy" { statement { sid = "RootUserKeyManagement" effect = "Allow" # actions を "kms.*" にすると以下のエラーが発生する # "The new key policy will not allow you to update the key policy in the future." actions = [ "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" ] resources = ["*"] principals { type = "AWS" identifiers = [ format("arn:aws:iam::%s:root", data.aws_caller_identity.current.account_id), ] } } # see: https://aws.amazon.com/jp/premiumsupport/knowledge-center/cross-account-access-denied-error-s3/ statement { sid = "EnableUseOfArtifactStoreEncryptionKey" effect = "Allow" actions = [ "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey" ] resources = ["*"] principals { type = "AWS" identifiers = [ format("arn:aws:iam::%s:root", var.accountA_id), # Delivery環境からartifact storeへのアクセスのために必要 var.codepipeline_service_role_arn # CodePipelineからのartifact storeへのアクセスに必要 ] } } statement { sid = "EnableGrantOfArtifactStoreEncryptionKey" effect = "Allow" actions = [ "kms:CreateGrant", "kms:ListGrants", "kms:RevokeGrant" ] resources = ["*"] principals { type = "AWS" identifiers = [ format("arn:aws:iam::%s:root", var.accountA_id), var.codepipeline_service_role_arn ] } condition { test = "Bool" variable = "kms:GrantIsForAWSResource" values = [true] } } }