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]
}
}
}

