🏢

【Workload Identity Federation】AWS ECS から GCP Cloud Run にアクセスする設定方法

に公開

はじめに

複数クラウドを組み合わせて使う場面は多くあります。たとえば、AWS ECSからGCPのBigQueryにデータを保存したい、GitHub ActionsやCodePipelineからGCPへデプロイしたい、あるいはGCP上でAzure OpenAI APIを使いたい、といったケースです。

こうした場面では、外部クラウドからGCPに安全にアクセスするための適切な権限設定が不可欠です。この記事では、この記事では、AWS ECSからGCP Cloud Runにアクセスするシナリオを例に、GCP公式ドキュメントをベースにしたわかりやすい手順を紹介します。

GCPへのアクセス手段の比較

外部からGCPリソースにアクセスするには、主に3つの方法があります。

方法 サービスアカウント必要? 鍵(key)必要? IAM 権限の付け方 対応API 特徴
① サービスアカウントキーを渡す 必要 必要(JSON鍵) SAにIAMロール付与 全API対応 シンプルだが鍵管理が必要、漏洩リスクあり
② フェデレーションIDで直接アクセス 不要 不要 外部IDに直接IAMロール付与
principal://.../subject/...
一部APIのみ対応 鍵不要で軽量。対象API限定
③ フェデレーションID+SA権限借用 必要 不要 外部IDに roles/iam.workloadIdentityUser を付与して
SAのロールを借用
ほぼ全API対応 セキュリティ・監査性◎。実運用向け

もっとも簡単なのは①ですが、鍵管理のリスクから本番運用には不向きです。この記事では、実運用に適した③「フェデレーションID+SA権限借用」の方法に絞って解説します。

アーキテクチャ概要

AWS ECSが、自身のIAMロールが信頼できることをGCPに証明し、GCPがそれに基づいて短期トークンを発行。そのトークンを用いて、GCPサービスアカウントの権限でCloud Runへアクセスします。

オフィス入館に例えると

「Workload Identity Federation+サービスアカウントの権限借用」方式のAWS(ECSタスク)が GCP(Cloud Run)にアクセスするまでの流れを、オフィス入館のようなストーリーで解説します。

技術コンポーネント 技術的な役割 例え
AWS IAM ロール 外部ID 訪問者の会社の社員証
GCP Workload Identity Pool 外部IDを受け入れる入口 訪問先の会社の受付
GCP Workload Identity Provider 信頼するID発行元や条件を定義 受付で管理される訪問者名簿
GCP サービスアカウント(SA) GCPリソースにアクセスする正式な権限主体 社員証
roles/iam.workloadIdentityUser 外部IDにサービスアカウントの「借用許可」を与えるIAMロール 入館証貸出許可証:この訪問者に貸してOKという許可証
principalSet:// IAMバインディング 条件付きで外部IDにSA借用許可を与えるIAM構文 貸出ルール

社員証を持たない訪問者が、どうやってオフィスに入るか?

AWS IAMロールは、訪問者が持っている「社員証」に相当します。
しかし、この社員証だけではGCP(オフィスビル)に直接入ることはできません。そこで、以下のようなプロセスが必要になります。

受付訪問
まず、GCP側には「受付」があります。
これが、Workload Identity Poolにあたります。外部から来た訪問者を受け入れるかどうかを判断する窓口です。

受付では、「どの会社の人を信頼するか」「どんな肩書きの人を対象とするか」といった情報が記載された名簿(Workload Identity Provider)をもとに確認を行います。
たとえば、「AWSアカウントIDが○○で、ロールが△△の人だけ入館可」といったルールを定義できます。

入館証発行
GCP内のリソース(たとえば Cloud Run)へアクセスするには、サービスアカウントという「入館証」が必要です。

AWS IAMロールは、このサービスアカウントを一時的に「借りる」ことで、GCPのリソースにアクセスできます。
ただし、誰でも借りられるわけではなく、IAMの設定で明示的に貸出を許可(roles/iam.workloadIdentityUser)を設定しておく必要があります。

訪問者名簿
どの訪問者に貸すか?は条件付きで管理できます。
「この会社のこのロールの人だけOK」といったルールは、principalSet://... という構文で柔軟に制御できます。

これにより、たとえば「開発環境のロールはこの入館証」「本番環境のロールは別の入館証」といった切り分けも簡単に実現できます。

AWS側の設定

IAMロールの用意
ECSタスクにアタッチするIAMロールを作成します。特別な信頼ポリシーは不要です。GCP側が署名を検証します。

cred-config.json の配置
GCPが発行する cred-config.json (後述) をECSタスクに配置し、以下のように環境変数でパスを指定します。

export GOOGLE_APPLICATION_CREDENTIALS=/path/to/cred-config.json

GCP側の設定

AWS側が「訪問者の社員証」を準備したら、次はGCP側で「信頼できる訪問者」として受け入れる設定を行います。目的はAWSのロールをGCPのサービスアカウントとして一時的に動けるようにすることです。
これにより、鍵(サービスアカウントキー)を一切使わずに、安全に Cloud Run などのGCP APIを利用できます。

Workload Identity Pool 作成
まずは、AWSなどの外部IDを受け入れる受付となるプールを作成します。

gcloud iam workload-identity-pools create aws-pool \
  --location="global" \
  --display-name="aws-pool"

Provider 作成
次に、どのAWSアカウントIDとロールを受け入れるかを定義する「訪問者名簿」を作ります。

gcloud iam workload-identity-pools providers create-aws aws-provider \
  --location="global" \
  --workload-identity-pool="aws-pool" \
  --account-id="012345678900" \
  --attribute-mapping="google.subject=assertion.arn, attribute.aws_role=assertion.arn.contains('assumed-role') ? assertion.arn.extract('{account_arn}assumed-role/') + 'assumed-role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn" \
  --attribute-condition="attribute.aws_role == 'arn:aws:iam::012345678900:role/dev-ecs-task-hoge'"

サービスアカウント (SA) 作成

gcloud iam service-accounts create access-from-aws \
  --display-name="access-from-aws"

SAにIAMロールを付与
たとえば、Cloud Run にアクセスする場合は次のように roles/run.invoker を割り当てます。

gcloud projects add-iam-policy-binding your-project-id \
  --member="serviceAccount:access-from-aws@your-project.iam.gserviceaccount.com" \
  --role="roles/run.invoker"

SAを借用できるように設定
現状roles/run.invoker が付与されているSAの権限をAWS ロールにも割り振ります。SAはプリンシパルを指定することができ、SA自身に権限を付与できるだけでなく、SAの権限を指定したプリンシパルに割り振ることが可能です。
※ 権限付与するIAMは上記のProviderでマッピングしたものを指定する必要があります。

  • マッピング前: arn:aws:sts::012345678900:assumed-role/dev-ecs-task-hoge
  • マッピング後: arn:aws:iam::012345678900:role/dev-ecs-task-hoge
gcloud iam service-accounts add-iam-policy-binding access-from-aws@your-project.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/aws-pool/attribute.aws_role/arn:aws:iam::012345678900:role/dev-ecs-task-hoge"

cred-config.json 生成
AWS 環境から GCP に認証するための設定ファイル(ADC)を生成します。

gcloud iam workload-identity-pools create-cred-config \
  projects/123456789012/locations/global/workloadIdentityPools/aws-pool/providers/aws-provider \
  --service-account=access-from-aws@your-project.iam.gserviceaccount.com \
  --aws \
  --output-file=cred-config.json

このファイルを AWS の ECS に配置して、環境変数 GOOGLE_APPLICATION_CREDENTIALS で指定すればOKです。

ハマりやすいポイント

実際に構築していて、私自身が特に混乱したのはこの2点でした。

  • 属性マッピングと属性条件
  • プリンシパルの指定方法

属性マッピングと属性条件

例によって例えると、

  • 属性マッピング
    • 「この社員証のIDは、うちのルールではこう読み替えるよ」という 翻訳ルール
  • 属性条件
    • 「翻訳されたIDがこの条件に合えば入館OKだよ」という 入館フィルター

上記で指定した属性マッピングは以下の値です。

google.subject=assertion.arn

attribute.aws_role = assertion.arn.contains('assumed-role') ?
  assertion.arn.extract('{account_arn}assumed-role/') +
  'assumed-role/' +
  assertion.arn.extract('assumed-role/{role_name}/')
  : assertion.arn

AWS IAM ロールのような外部IDが発行するトークンには様々な情報が入っており、その情報をGCP内の識別子に変換する必要があります。

google.subject=assertion.arn は、GCP内で「この外部IDは誰か?」を識別する主キー (subject) に、AWSロールのARN (assertion.arn) を使うという指定です。

attribute.aws_role は、カスタム属性でCELという記法を用いて、マッチする条件を定義し、GCP内で扱いやすい形に整えた上で attribute.aws_role にセットされます。

--attribute-condition="attribute.aws_role == 'arn:aws:iam::012345678900:role/dev-ecs-task-hoge'"

上で抽出した attribute.aws_role に、特定の値(例:arn:aws:iam::012345678900:role/dev-ecs-task-hoge)が一致しているかを条件として、アクセスを制限します。複数のロールを許可したい場合は、条件式に in や startsWith() を使うことも可能です。

attribute.aws_role in [
  "arn:aws:iam::012345678900:role/dev-ecs-task-hoge",
  "arn:aws:iam::012345678900:role/dev-ecs-task-fuga
]

プリンシパルの指定方法
Workload Identity Federation を使って AWS ロールから GCP のサービスアカウントを借用するとき、
IAM バインディングで「誰に許可を与えるか」を指定する必要があります。

このときに登場するのが、プリンシパル構文です。

IDの種別 IDの形式
単一の ID principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/subject/SUBJECT_ATTRIBUTE_VALUE
グループ内のすべての ID principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/group/GROUP_ID
特定の属性値を持つすべての ID principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/attribute.ATTRIBUTE_NAME/ATTRIBUTE_VALUE

たとえばこんな指定、一見正しそうに見えて実は効かない構文です

--member="principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/aws-pool/attribute.aws_role/arn:aws:iam::012345678900:role/dev-ecs-task-hoge"

これは無効です。
なぜなら、principal:// は「完全一致する subject の値」だけを対象にする構文だからです。

以下のように、属性を使って「この条件に当てはまる外部IDは全部OK」としたい場合は、必ず principalSet:// を使います。

--member="principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/aws-pool/attribute.aws_role/arn:aws:iam::012345678900:role/dev-ecs-task-hoge"
--member=".../attribute.aws_role/arn:aws:sts::...:assumed-role/my-role"

Provider のマッピングで assumed-rolerole に変換されているので、この指定は一致しません。

--member=".../attribute.aws_role/arn:aws:iam::...:role/my-role"

バインディングが正しく適用されているかは下記のコマンドで確認できます。

gcloud iam service-accounts get-iam-policy access-from-aws@your-project.iam.gserviceaccount.com

ここで bindings: に principalSet://... が表示されていればOKです。
何も出てこず etag: だけの場合は、指定ミスでバインディングが無視されている可能性が高いです。

まとめ

この記事では、AWS ECSからGCP Cloud Runに安全かつ鍵レスでアクセスする方法として、Workload Identity Federationの設定手順をオフィス入館の例えを交えて解説しました。特に、属性マッピングやプリンシパル構文はハマりやすいため、例え話や構文例を参考にしながら設定することをおすすめします。

サービスアカウントキーは手軽に使える反面、漏洩リスクもあるため、本番環境では極力使わず、本記事のような鍵レス方式を採用しましょう。

Discussion