LayerX エンジニアブログ

LayerX の エンジニアブログです。

AWS マルチアカウント環境からの Google Cloud フェデレーション設計 — AI時代に合わせた社内認証基盤づくり

はじめに

LayerX Fintech 事業部から、三井物産デジタル・アセットマネジメント(以下、MDM) に出向している piroshi です。

AI 活用や業務自動化が当たり前になってきた今、データや処理はプラットフォームをまたいで動くことが増えています。特に「システム基盤はAWSで動かしつつ、社内業務は Google Workspace 前提」といった構成では、AWS 上のワークロードから Google Cloud(以下、GCP) や Google Workspace の API へ安全にアクセスしたい場面が自然に出てきます。

MDM でもそのニーズが顕在化しました。具体的には、共有ドライブの構成情報を定期的に取得して監査(権限・設定の棚卸し)を自動化したい、という要件があります。加えて、別のチームでは Google Drive 上のリソースにアクセスする業務ツール開発も並行して進んでいました。ここでチームごとに認証方式が増えていくと、長期鍵の配布・権限の肥大化・監査のしづらさといった問題が起きやすくなります。そこで、個別最適に寄せるのではなく、安全で監査可能な共通のアクセス方式を用意することを目標に設計を検討しました。

サービスアカウントキーを発行して AWS 側に置けば動きますが、長期鍵の配布・ローテーション・棚卸しは運用負荷が高く、漏えい時の影響も大きくなります。今回は Workload Identity Federation(WIF) を使い、静的キーを持たずに短命トークンでアクセスする構成を設計しました。

WIF を使った実装の情報は多く見つかりましたが、AWS がマルチアカウント構成の場合にどう設計すべきかについて触れているものがなかなか見つからず、さらに、GCP のベストプラクティスに沿って「WIF を専用プロジェクトに集約する」構成を採ると、具体的にどう設計・運用が変わるのかイメージが湧きにくかったです。

この記事では、「AWS Organizations を使ったマルチアカウント環境」で、「GCP 側へ出ていく認証経路(入口)を運用・統制の都合で Security アカウントに集約」しつつ、「ワークロード単位で識別して最小権限を保つ」ためにどう設計したかをまとめます。検証時にハマったポイントとあわせて共有できればと思います。

なお、記事では AWS→GCP へのフェデレーションに焦点を当てています。Google Workspace API を操作する場合の Domain-Wide Delegation(DWD)設定については、ここでは扱いません。

前提となる制約(再掲)

  • AWS Organizations を使ったマルチアカウント戦略を採用している
  • Google 側へ出ていく認証経路(入口)を、運用・統制の都合で Security アカウントに集約したい
  • そのうえで、「ワークロード単位で識別して最小権限を付与」を維持する

結論: 「入口」と「権限」を分けた 2 段階のアクセス制御

採用した構成は以下です。まず認証フローの概要を示します。

flowchart LR
    subgraph AWS_Org[AWS Organizations]
        subgraph Workloads[Workload Accounts]
            WL_A[Workload Account A]
            WL_B[Workload Account B]
        end
        Security[Security Account]
    end

    subgraph GCP_Org[GCP Organization]
        subgraph WIF_Proj[WIF 専用プロジェクト]
            WIF[WIF Pool/Provider]
        end
        subgraph Service_Projs[サービスプロジェクト群]
            SA_A[SA / Resources A]
            SA_B[SA / Resources B]
        end
    end

    WL_A -->|1. Cross-Account AssumeRole| Security
    WL_B -->|1. Cross-Account AssumeRole| Security
    Security -->|2. Token Exchange| WIF
    WIF -->|3. Impersonate| SA_A
    WIF -->|3. Impersonate| SA_B

Service Account(SA)の権限で GCP リソースにアクセスします。Google Workspace API を使う場合は、SA に Domain-Wide Delegation(DWD)を付与してユーザーとして操作する構成になります。

監査ログの取得ポイント

この構成では、認証フローの各ステップでログが取得できます。

ステップ ログ 確認できること
AWS: AssumeRole CloudTrail どのロールがどのロールを AssumeRole したか
GCP: Token exchange Cloud Audit Logs(WIF プロジェクト) attribute_condition の成否、どのフェデレーション ID がトークン交換したか
GCP: SA impersonation Cloud Audit Logs(SA のプロジェクト) どのフェデレーション ID がどの SA を impersonate したか
Workspace API OAuth ログ、各サービス監査ログ(Drive 等) Google Workspace へのアクセス状況

WIF 専用プロジェクトと SA のプロジェクトが分かれていても、それぞれのプロジェクトで Cloud Audit Logs が記録されるため、追跡が可能です。

設計思想: 2段階のアクセス制御

レイヤー 制御内容 役割
第1段階: Provider attribute_condition で「受け入れる AWS 側の IAM ロール」を絞る 想定外のアカウント/ロールを入口で落とす
第2段階: Service Account principalSet による IAM Binding で「どのロールがどの SA を impersonate できるか」を制御 ワークロード単位の権限分離(最小権限)

principalSet とは: WIF のフェデレーション ID(federated identity)を指定するための識別子です。属性に基づいて「どのフェデレーション ID に権限を付与するか」を定義します。

形式: principalSet://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{POOL_ID}/attribute.{ATTRIBUTE_NAME}/{ATTRIBUTE_VALUE}

例: principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/aws-workload-id/attribute.aws_role/arn:aws:sts::111111111111:assumed-role/gcp-wif-workload-a-role

この例では、aws_role 属性が arn:aws:sts::111111111111:assumed-role/gcp-wif-workload-a-role(= AWS の該当 IAM Role の ARN)に一致するフェデレーション ID に対して権限を付与します。

Pool に到達できても、SA への IAM Binding がなければフェデレーション ID は impersonate できません。

認証フローの概要

  1. Workload Account の実行ロール(IAM Role)から、Security Account 上の GCP Federation 用ロール(IAM Role)にクロスアカウント AssumeRole
  2. そのロールの AWS 認証情報で、WIF が必要とする署名付き GetCallerIdentity 相当の情報を作り、Google STS へトークン交換
  3. Provider の attribute_condition が true の場合のみ、トークン交換が成立
  4. SA に対する roles/iam.workloadIdentityUser の IAM Binding(principalSet でフェデレーション ID を指定)により、対象 SA を impersonate
  5. SA の権限で GCP リソース (必要なら DWD で Workspace API) にアクセス

補足: WIF が識別する「AWS 側の ID」とは

WIF が AWS 側の ID を識別する際に使うのは、STS の GetCallerIdentity が返す ARN です。assumed-role の場合、arn:aws:sts::{ACCOUNT_ID}:assumed-role/{ROLE_NAME}/{SESSION_NAME} の形式になります。

セッション名は実行ごとに変わるため、WIF の Pool 側の設定(attribute_mapping) でセッション名を除去した ARN(arn:aws:sts::{ACCOUNT_ID}:assumed-role/{ROLE_NAME})を属性として使用します。

ARN のうち、ユーザー側でコントロールできるのは IAM Role の名前です。そのため、命名規則を統一することで attribute_conditionprincipalSet での制御がしやすくなります。

設計で検討した 5 つのポイント

1. GCP プロジェクト構成: 既存のプロジェクトを利用 vs 専用プロジェクトを新設

選択肢 評価
既存プロジェクトに WIF リソースを追加 ❌ 却下
WIF 専用プロジェクトを新規作成 ✅ 採用
  • GCP のベストプラクティスで「専用のプロジェクトを使用して Workload Identity プールとプロバイダを管理する」ことが推奨されている
  • セキュリティ境界の明確化(他リソースと分離)
  • WIF の管理権限(誰が Pool/Provider を触れるか)を分離・集中管理できる
  • 一貫した attribute_mapping や attribute_condition を適用できる
  • 外部からのフェデレーション経路を一箇所に集約することで、監視・監査すべきポイントが明確になり、セキュリティ上の攻撃対象領域を限定できる

WIF を専用プロジェクトに集約し、実際のアクセス先となる SA が別のプロジェクトにあってもアクセスできるのかが懸念でしたが、SA に対して roles/iam.workloadIdentityUser を付与する IAM Binding でクロスプロジェクトの impersonation が可能でした。

common-wif のような命名で、一元管理用の専用プロジェクトを用意しました。

2. AWS IAM Role 構成: 単一共有 vs ワークロード別分離

この設計では、AWS 側に 2 種類の IAM Role が登場します。

ロールの種類 配置先 役割
実行ロール Workload Account Lambda や ECS タスクなど、ワークロードが実行時に使用するロール。ワークロードごとに異なる。
GCP Federation 用ロール Security Account GCP への認証(WIF)に使用するロール。実行ロールから AssumeRole される。

ここで検討するのは、Security Account の GCP Federation 用ロールを、全ワークロードで共有するか、ワークロードごとに分けるかです。

選択肢 評価
全ワークロードで共有する単一の GCP Federation 用ロール ❌ 却下
ワークロードごとに個別の GCP Federation 用ロールを作成 ✅ 採用

前提として、各ワークロードが必要とする権限(AWS 側のアクセス先リソースや、GCP 側で取得したい SA の権限)が異なる場合、実行ロールをワークロードごとに分けるのが自然です。

しかし、入口を Security Account に集約する設計では、GCP から見えるのは Security Account 側の「GCP Federation 用ロール」です(厳密には、識別できるのは STS ARN で、そのなかに IAM Role Name が含まれます)。GCP 側で利用できる属性は GetCallerIdentity 由来の情報(account/arn/userid など)で、実質「最後に AssumeRole した AWS 側のロール」を元に制御することになります。

よって、ワークロード単位の分離を GCP 側で実現するには、GCP Federation 用ロールもワークロードごとに分ける必要があります

AWS(Security Account): GCP Federation 用ロール(例: workload-a)

# Security Account 側(ID: 111111111111)
# - SA へのアクセスを許可するワークロードの実行ロールからの AssumeRole を許可
# - AWS リソースへのアクセス権限は不要なため、信頼ポリシーのみ定義(IAM ポリシーのアタッチなし)
resource "aws_iam_role" "gcp_wif_workload_a" {
  name               = "gcp-wif-workload-a-role"
  assume_role_policy = data.aws_iam_policy_document.trust_from_workload_a_exec.json
}

data "aws_iam_policy_document" "trust_from_workload_a_exec" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = [
        "arn:aws:iam::222222222222:role/workload-a-exec-role"
      ]
    }
  }
}

AWS(Workload Account A): 実行ロールに AssumeRole 権限を付与

# Workload Account A 側 (ID: 222222222222)
# Security Account の GCP Federation 用ロールへの AssumeRole を許可
data "aws_iam_policy_document" "allow_assume_gcp_wif" {
  statement {
    effect    = "Allow"
    actions   = ["sts:AssumeRole"]
    resources = ["arn:aws:iam::111111111111:role/gcp-wif-workload-a-role"]
  }
}

resource "aws_iam_role_policy" "workload_a_exec_assume_gcp_wif" {
  name   = "assume-gcp-wif"
  role   = aws_iam_role.workload_a_exec.name
  policy = data.aws_iam_policy_document.allow_assume_gcp_wif.json
}

3. attribute_condition: 完全一致 vs パターンマッチ

まず、attribute_conditionattribute_mapping の役割を整理します。

attribute_conditionWIF Provider に設定する CEL(Common Expression Language)式で、「この Pool にアクセスできるフェデレーション ID」をフィルタリングします。条件に合致しないフェデレーション ID はトークン交換を拒否されます。

条件式の中では attribute_mapping で定義した属性を参照できます。attribute_mapping は外部 IdP(AWS)から受け取る情報(assertion)を GCP 側の属性にマッピングする設定で、Provider に定義します(公式ドキュメントの例を参考にしています)。

例えば、AWS から受け取る assertion.arn が以下の値だった場合:

arn:aws:sts::111111111111:assumed-role/gcp-wif-workload-a-role/session123

attribute_mapping でセッション名(session123)を除去し、attribute.aws_role には以下が格納されます:

arn:aws:sts::111111111111:assumed-role/gcp-wif-workload-a-role

これにより、実行ごとに変わるセッション名に依存せず、IAM Role 単位での制御が可能になります。

では、attribute_condition でどのように AWS 側のロールをフィルタするかを検討します。

選択肢 評価
Role ARN を完全一致で列挙 `== "{workload-a の ARN}" | == "{workload-b の ARN}"` | ❌ 却下
prefix でパターンマッチ startsWith("...assumed-role/gcp-wif-") ✅ 採用

完全一致で列挙する方法では、ワークロード追加のたびに Provider の attribute_condition を更新する必要があります。gcp-wif- という prefix でのパターンマッチなら、命名規則に従う限り attribute_condition の変更は不要です。

採用した attribute_condition の例:

attribute.aws_role.startsWith('arn:aws:sts::111111111111:assumed-role/gcp-wif-')

4. principalSet: Pool 全体許可 vs 個別 Role 指定

principalSetService Account に対して roles/iam.workloadIdentityUser を付与する IAM Binding の member として指定する、フェデレーション ID の識別子に該当します。「どのフェデレーション ID がこの SA を impersonate(権限借用)できるか」を制御します。

指定方法には粒度の違いがあり、Pool 全体を許可するか、特定の属性値(assumed-role ARN)で絞り込むかを選べます。

選択肢 評価
principalSet://.../{pool}/*(Pool 全体許可) ❌ 却下
principalSet://.../attribute.aws_role/{assumed-role ARN}(個別指定) ✅ 採用

Pool 全体許可では、Workload A 用の GCP Federation ロールが Workload B の SA を impersonate できてしまい、ワークロード間の分離が壊れます。個別指定なら「この AWS ロールはこの SA だけ」という 1:1 の対応を強制できます。

GCP 側 Terraform(Pool / Provider / IAM Binding)

Workload Identity Pool / Provider

# WIF 専用プロジェクト
resource "google_iam_workload_identity_pool" "aws_pool" {
  project                   = "my-wif-project"
  workload_identity_pool_id = "aws-workload-id"
  display_name              = "AWS Workload Identity Pool"
  description               = "Federation from AWS Security Account"
}

resource "google_iam_workload_identity_pool_provider" "aws_provider" {
  project                            = "my-wif-project"
  workload_identity_pool_id          = google_iam_workload_identity_pool.aws_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "security-account"
  display_name                       = "AWS Security Account Provider"

  aws {
    account_id = "111111111111"  # Security Account ID
  }

  attribute_mapping = {
    "google.subject" = "assertion.arn"
    # セッション名を除去して assumed-role 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"
  }

  # 命名規則に合致する assumed-role のみ受け入れ
  attribute_condition = "attribute.aws_role.startsWith('arn:aws:sts::111111111111:assumed-role/gcp-wif-')"
}

Service Account と IAM Binding(workload-a 例)

# サービスプロジェクト側で作る想定
resource "google_service_account" "workload_a" {
  project      = "my-service-project"
  account_id   = "workload-a"
  display_name = "Service Account for workload-a"
}

# 重要: principalSet は PROJECT_NUMBER を使う(PROJECT_ID ではない)
resource "google_service_account_iam_binding" "workload_a_wif_user" {
  service_account_id = google_service_account.workload_a.name
  role               = "roles/iam.workloadIdentityUser"

  members = [
    "principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/aws-workload-id/attribute.aws_role/arn:aws:sts::111111111111:assumed-role/gcp-wif-workload-a-role"
  ]
}

運用への考慮

新ワークロード追加時の担当タスク

  • 管理者:

    • AWS(Security): GCP Federation 用 IAM Role を追加
    • GCP: ワークロード専用 SA を作成し、必要最小限の権限を付与
    • GCP: SA に roles/iam.workloadIdentityUser の IAM Binding(principalSet で Federation 用ロールを指定)
  • 開発者:

    • AWS(Workload): 実行ロールに GCP Federation 用ロールへの AssumeRole 権限を付与

管理者側の GCP 作業(SA 作成 + IAM Binding)は以下のような Terraform で管理できます:

# ワークロード専用 SA
resource "google_service_account" "workload_a" {
  project      = "my-wif-project"
  account_id   = "workload-a"
  display_name = "Service Account for workload-a"
}

# IAM Binding: GCP Federation 用ロール → SA
# aws_iam_role.gcp_wif_workload_a は Security Account 側で定義した GCP Federation 用ロール
resource "google_service_account_iam_binding" "workload_a_wif_user" {
  service_account_id = google_service_account.workload_a.name
  role               = "roles/iam.workloadIdentityUser"

  members = [
    # IAM Role ARN (arn:aws:iam::...) を STS assumed-role ARN 形式に変換
    "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.aws_pool.name}/attribute.aws_role/arn:aws:sts::${data.aws_caller_identity.security.account_id}:assumed-role/${aws_iam_role.gcp_wif_workload_a.name}"
  ]
}

命名規則に従う限り、Provider の attribute_condition は基本的に触りません。

補足: 管理権限もセットで設計する

WIF の設計とあわせて、以下のようなガードレールも検討しておくと安心です。

ハマりポイント・学び

この設計を検証する中で苦労した点を紹介します。

1) コンポーネントが多く、トラブルシューティングが難しい

WIF は AWS と GCP を跨ぎ、IAM Role, STS, Provider, Pool, SA など関連するコンポーネントが多いです。問題が発生した場合は、推測ではなくログをもとに原因を切り分けていくのが確実です。

コンポーネント ログ 見えるもの
AWS: AssumeRole CloudTrail どのロールがどのロールを AssumeRole したか
GCP: Token exchange Cloud Audit Logs(iam.googleapis.com) attribute_condition の成否など
GCP: SA impersonation Cloud Audit Logs(iam.googleapis.com) どのフェデレーション ID がどの SA を impersonate したか
Workspace API 管理コンソール監査ログ DWD など Workspace 側の監査

2) AssumeRole 時の環境変数の扱いに注意(Lambda など)

例えば Lambda で Google Cloud クライアントライブラリ(Python の google-auth、Node.js の google-auth-library など)を使用する場合、ライブラリは AWS 認証情報を環境変数(AWS_ACCESS_KEY_ID など)から取得します。

Lambda では起動時に実行ロールの認証情報が環境変数にセットされています。AssumeRole 後の認証情報で環境変数を上書きすると、以降の AWS SDK 呼び出しも AssumeRole 後の認証情報で動作してしまいます。つまり、元の実行ロールに付与していた権限が使えなくなります。

今回は try-finally で環境変数を書き戻すことで対処しました。(これがベストかは悩ましいところです。)

const prev = { accessKeyId: process.env.AWS_ACCESS_KEY_ID, /* ... */ };
try {
  process.env.AWS_ACCESS_KEY_ID = assumedCreds.AccessKeyId;
  // ... WIF 処理(GCP クライアントライブラリ経由)
} finally {
  process.env.AWS_ACCESS_KEY_ID = prev.accessKeyId; // 必ず元に戻す
}

まとめ

  • 入口(Security Account)と権限(GCP 側の SA)を分け、WIF を専用プロジェクトに集約すると、マルチアカウントでも運用しやすい
  • Provider は「受け入れ条件」、IAM Binding は「実際の権限」と役割分担すると設計が整理できる
  • attribute_mappingprincipalSet でワークロードを識別し、最小権限の原則を守れる
  • ログ(CloudTrail / Cloud Audit Logs / Workspace 監査ログ)を前提に、切り分け可能な形にしておく

参考資料

おわりに

今回は、マルチアカウントな AWS 環境から GCP/Google Workspace に安全にアクセスするために、Workload Identity Federation をどう設計したかをまとめました。運用はこれからですが、実際に回し始めると設計時点では見えなかった運用上の課題やメリット、勘所も出てくるはずです。このあたりは、運用で得た知見として別途まとめて記事にしたいと思います。

MDM のコーポレートシステム部では、こうした認証・認可や監査性を仕組みで支えつつ、社内の開発や業務が安心して速く回る土台を作っています。
このあたりの領域に興味がある方、ぜひ一緒にやりましょう!!

open.talentio.com

open.talentio.com