CodeBuildのビルド内でAssumeRole(クロスアカウントアクセス)する方法とハマった話

こんにちは、佐伯です。

CodeBuildからAssumeRoleする方法はズバリこれだ!ってエントリがなかった気がするので書きました。今更感がありますがご了承ください。また、CodePipelineで少しハマったので共有目的で本エントリを書きました。

CodeBuildのビルド内からAssumeRoleする方法

ビルド内からAssumeRoleするための設定に焦点を置いています。その他もろもろの設定は省いていますのでその点ご注意ください。

AWSアカウントの定義

AWSアカウントは便宜上以下のような名称として定義しておきます。

AWSアカウント名 AWSアカウントID 備考
account-a XXXXXXXX4623 account-bからAssumeRoleされるAWSアカウント
account-b XXXXXXXX6985 account-aにAssumeRoleするAWSアカウント

account-a:IAMロールの作成

IAMロールの信頼関係にaccount-bを信頼するポリシーを設定します。設定するプリンシパルはAWSアカウントを信頼する or CodeBuildサービスロールを信頼するどちらの書き方でも動作します。

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::XXXXXXXX6985:root"
        }
    }
}
{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::XXXXXXXX6985:role/<IAMロール名>"
        }
    }
}

IAMポリシーの権限はCodeBuildのビルド内容次第ですので省略しています。

account-b:CodeBuildサービスロールへIAMポリシーを追加

ビルド内でAssumeRoleを実行するにはIAMアクション sts:AssumeRole を許可する必要があるので、以下のIAMポリシーをインラインポリシーで追加、またはユーザー管理ポリシーを作成してCodeBuildサービスロールにアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "*"
        }
    ]
}

こちらもその他のIAMポリシーの権限はCodeBuildのビルド内容次第ですので省略しています。

account-b:buildspec.ymlの作成

AWS CLIのプロファイルを使用したAssumeRole

以下のようなbuildspec.ymlを作成します。内容を補足するとpre_buildフェーズでAWS CLI設定ファイルを作成、作成した設定ファイルを環境変数AWS_CONFIG_FILEに設定しています。${ASSUME_ROLE_ARN}はビルドプロジェクトの環境変数として設定しています。

version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.7
  pre_build:
    commands:
      - echo "[profile account-b]" > .awscli-config
      - echo "role_arn = ${ASSUME_ROLE_ARN}" >> .awscli-config
      - echo "credential_source = EcsContainer" >> .awscli-config
      - export AWS_CONFIG_FILE=${CODEBUILD_SRC_DIR}/.awscli-config
  build:
    commands:
      - aws sts get-caller-identity
      - aws sts get-caller-identity --profile account-b

なお、.awscli-configは適当に決めた名前なのでなんでもいいですし、pre_buildフェーズで作成せず、ソース(GitHubなど)でファイルとして管理する形でも問題ありません。ランタイムは指定が必須なのでPython 3.7を選択してます。

以下はpre_buildフェーズ以降の実行結果抜粋です。aws sts get-caller-identityの実行結果でアカウントIDが変わっているのが確認できるかと思います。

[Container] 2019/09/18 09:45:20 Entering phase PRE_BUILD
[Container] 2019/09/18 09:45:20 Running command echo "[profile account-b]" > .awscli-config

[Container] 2019/09/18 09:45:20 Running command echo "role_arn = ${ASSUME_ROLE_ARN}" >> .awscli-config

[Container] 2019/09/18 09:45:20 Running command echo "credential_source = EcsContainer" >> .awscli-config

[Container] 2019/09/18 09:45:20 Running command export AWS_CONFIG_FILE=${CODEBUILD_SRC_DIR}/.awscli-config

[Container] 2019/09/18 09:45:20 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2019/09/18 09:45:20 Phase context status code:  Message:
[Container] 2019/09/18 09:45:20 Entering phase BUILD
[Container] 2019/09/18 09:45:20 Running command aws sts get-caller-identity
{
    "UserId": "AROA27MEXAMPLEEXAMPLE:AWSCodeBuild-66809dcf-7dc1-4c9f-baab-1d028example",
    "Account": "XXXXXXXX4623",
    "Arn": "arn:aws:sts::XXXXXXXX4623:assumed-role/codebuild-service-role/AWSCodeBuild-66809dcf-7dc1-4c9f-baab-1d028example"
}

[Container] 2019/09/18 09:45:22 Running command aws sts get-caller-identity --profile account-b
{
    "UserId": "AROAYRMEXAMPLEEXAMPLE:botocore-session-1568799923",
    "Account": "XXXXXXXX6985",
    "Arn": "arn:aws:sts::XXXXXXXX6985:assumed-role/assume-role-from-codebuild/botocore-session-1568799923"
}

[Container] 2019/09/18 09:45:24 Phase complete: BUILD State: SUCCEEDED

環境変数による設定

ツールによってはAWS CLIのプロファイル指定をサポートしていない場合もあるかもしれないので、aws sts assume-roleコマンドから一時クレデンシャルを取得して環境変数にセットする方法についても記載します。jqなどを使用して取得した一時クレデンシャルを環境変数に設定しています。

version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.7
  pre_build:
    commands:
      - aws sts get-caller-identity
  build:
    commands:
      - credentials=$(aws sts assume-role --role-arn ${ASSUME_ROLE_ARN} --role-session-name "RoleSessionFromCodeBuild" | jq .Credentials)
      - export AWS_ACCESS_KEY_ID=$(echo ${credentials} | jq -r .AccessKeyId)
      - export AWS_SECRET_ACCESS_KEY=$(echo ${credentials} | jq -r .SecretAccessKey)
      - export AWS_SESSION_TOKEN=$(echo ${credentials} | jq -r .SessionToken)
      - aws sts get-caller-identity

以下はpre_buildフェーズ以降の実行結果抜粋です。

[Container] 2019/09/18 09:40:04 Entering phase PRE_BUILD
[Container] 2019/09/18 09:40:04 Running command aws sts get-caller-identity
{
    "UserId": "AROA27MEXAMPLEEXAMPLE:AWSCodeBuild-cad340c8-820f-4de9-a5bb-f75feexample",
    "Account": "XXXXXXXX4623",
    "Arn": "arn:aws:sts::XXXXXXXX4623:assumed-role/codebuild-service-role/AWSCodeBuild-cad340c8-820f-4de9-a5bb-f75feexample"
}

[Container] 2019/09/18 09:40:07 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2019/09/18 09:40:07 Phase context status code:  Message:
[Container] 2019/09/18 09:40:07 Entering phase BUILD
[Container] 2019/09/18 09:40:07 Running command credentials=$(aws sts assume-role --role-arn ${ASSUME_ROLE_ARN} --role-session-name "RoleSessionFromCodeBuild" | jq .Credentials)

[Container] 2019/09/18 09:40:08 Running command export AWS_ACCESS_KEY_ID=$(echo ${credentials} | jq -r .AccessKeyId)

[Container] 2019/09/18 09:40:08 Running command export AWS_SECRET_ACCESS_KEY=$(echo ${credentials} | jq -r .SecretAccessKey)

[Container] 2019/09/18 09:40:08 Running command export AWS_SESSION_TOKEN=$(echo ${credentials} | jq -r .SessionToken)

[Container] 2019/09/18 09:40:08 Running command aws sts get-caller-identity
{
    "UserId": "AROAYRMEXAMPLEEXAMPLE:RoleSessionFromCodeBuild",
    "Account": "XXXXXXXX6985",
    "Arn": "arn:aws:sts::XXXXXXXX6985:assumed-role/assume-role-from-codebuild/RoleSessionFromCodeBuild"
}

[Container] 2019/09/18 09:40:09 Phase complete: BUILD State: SUCCEEDED

ハマった話

他のAWSアカウントのECRへイメージをPUSH→CodePipelineの開始を実現したくて以下の設定を行いました。

しかし、CodeBuild内でdocker pushコマンドでECRへのイメージPUSHは問題なく実行できるが、CodePipelineが開始されない事象が発生しました。

ECR PUSHトリガーでCodePipelineが開始される仕組み

CodePipelineはSourceにECRを指定し、リポジトリに指定したタグが付与されたイメージがPUSHされるとパイプラインを開始する、といった設定が可能です。この仕組みはCloudWatch Eventsで実現されています。

ECRのイベント

ECRのイベントはCloudWatch Eventsではサポートされておらず、CloudTrail 経由で配信されたイベントとなります。

なぜCloudWatch Eventsがトリガーされないのか

実際に自AWSアカウントからECRへPUSHした場合と他AWSアカウントからECRへPUSHした場合で何が変わるのかCloudTrailから確認しました。結論から言ってしまうと 2019/09/18時点では他AWSアカウントからECRへPUSHした際はCloudTrail経由でもイベントが配信されず、CloudWatch Eventsルールのトリガーとして設定できない というのが答えのようです。通りで動かないはずだ...。

自AWSアカウントからECRへPUSHした際のCloudTrailログ

detail-typeなどのフィールドが存在しており、CloudWatch Eventsのイベントフォーマットになっています。

{
    "version": "0",
    "id": "432cea0a-d3bd-6dd9-b351-07ac1example",
    "detail-type": "AWS API Call via CloudTrail",
    "source": "aws.ecr",
    "account": "XXXXXXXX4623",
    "time": "2019-09-17T13:10:10Z",
    "region": "ap-northeast-1",
    "resources": [],
    "detail": {
        "eventVersion": "1.05",
        "userIdentity": {
            "type": "AssumedRole",
            "principalId": "AROAIBLEXAMPLEEXAMPLE:1568725740989028000",
            "arn": "arn:aws:sts::XXXXXXXX4623:assumed-role/saiki.ko/1568725740989028000",
            "accountId": "XXXXXXXX4623",
            "accessKeyId": "ASIAI3EXAMPLEEXAMPLE",
            "sessionContext": {
                "sessionIssuer": {
                    "type": "Role",
                    "principalId": "AROAIBLEXAMPLEEXAMPLE",
                    "arn": "arn:aws:iam::XXXXXXXX4623:role/saiki.ko",
                    "accountId": "XXXXXXXX4623",
                    "userName": "saiki.ko"
                },
                "webIdFederationData": {},
                "attributes": {
                    "mfaAuthenticated": "false",
                    "creationDate": "2019-09-17T13:09:02Z"
                }
            },
            "invokedBy": "AWS Internal"
        },
        "eventTime": "2019-09-17T13:10:10Z",
        "eventSource": "ecr.amazonaws.com",
        "eventName": "PutImage",
        "awsRegion": "ap-northeast-1",
        "sourceIPAddress": "AWS Internal",
        "userAgent": "AWS Internal",
        "requestParameters": {
            "repositoryName": "example",
            "imageTag": "latest",
            "registryId": "XXXXXXXX4623",
            "imageManifest": "<長い一行のJSONなので省略>"
        },
        "responseElements": {
            "image": {
                "repositoryName": "example",
                "imageManifest": "<長い一行のJSONなので省略>",
                "registryId": "XXXXXXXX4623",
                "imageId": {
                    "imageDigest": "sha256:55e7a6f2bb43e38cc34285af03b4973d61f523d26cd8a57e9dexampleexample",
                    "imageTag": "latest"
                }
            }
        },
        "requestID": "313be446-508c-4c85-b4b4-8a1d3example",
        "eventID": "27556aba-1342-43bb-85bb-f17e2example",
        "resources": [
            {
                "accountId": "XXXXXXXX4623",
                "ARN": "arn:aws:ecr:ap-northeast-1:XXXXXXXX4623:repository/example"
            }
        ],
        "eventType": "AwsApiCall"
    }
}

他AWSアカウントからECRへPUSHした際のCloudTrailログ

必須となる各フィールドが存在しておらず、単なるCloudTrailログが出力されるようです。

{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "AWSAccount",
        "principalId": "AIDAI6UEXAMPLEEXAMPLE",
        "accountId": "XXXXXXXX6985"
    },
    "eventTime": "2019-09-17T14:57:56Z",
    "eventSource": "ecr.amazonaws.com",
    "eventName": "PutImage",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "AWS Internal",
    "userAgent": "AWS Internal",
    "requestParameters": {
        "repositoryName": "example",
        "imageTag": "latest",
        "registryId": "XXXXXXXX4623",
        "imageManifest": "<長い一行のJSONなので省略>"
    },
    "responseElements": {
        "image": {
            "repositoryName": "example",
            "imageManifest": "<長い一行のJSONなので省略>",
            "registryId": "XXXXXXXX4623",
            "imageId": {
                "imageDigest": "sha256:acd3ca9941a85e8ed16515bfc5328e4e2f8c128caa72959a58a127b7801ee01f",
                "imageTag": "latest"
            }
        }
    },
    "requestID": "06dbd894-f139-40ae-a6b1-72979example",
    "eventID": "9197e795-b1c5-4ca3-a793-011b1example",
    "resources": [
        {
            "accountId": "XXXXXXXX4623",
            "ARN": "arn:aws:ecr:ap-northeast-1:XXXXXXXX4623:repository/example"
        }
    ],
    "eventType": "AwsApiCall",
    "recipientAccountId": "XXXXXXXX4623",
    "sharedEventID": "6f71b837-5700-491c-b87d-64422example"
}

ワークアラウンド

他AWSアカウントから直接ECRへイメージをPUSHするのではなく、前述のCodeBuildのビルド内から対象のAWSアカウントへAssumeRoleした上でECRへイメージをPUSHすることでイベントが配信されます。これでCodePipelineが開始されます。

まとめ

AssumeRoleの方法よりはハマりポイントについて共有する目的で書きました!

なぜ動かないのか色々試した結果、素直にCloudTrailのログを見ることで原因究明の糸口になりました。今回はCloudWatch Eventsがなぜトリガーされないのかを調べましたが、デバッグ方法が限られる、且つAWSサービス同士を連携するようなサービスでハマった場合、まずはCloudTrailのログを確認してみるのがいいかもしれません。

どなたかの参考になれば幸いです。