Workload Identity FederationでGitHub ActionsからGoogle Cloudへ認証する

GitHub Actions からトークンを介して Google Cloud へキーなしで認証する仕組みを表すイラスト。「Workload Identity Federation」「GitHub Actions to Google Cloud」の文字入り 認証・IAM

はじめに

Workload Identity Federation を使うと、GitHub Actions からサービスアカウントのキーを使わずに Google Cloud へ認証できます。GitHub が発行する短時間だけ有効なトークンを、Google Cloud のトークンに交換する仕組みです。

本記事では、GitHub Actions から Terraform の plan と apply を Google Cloud に対して実行している構成を紹介します。あわせて、運用しながら踏んだ落とし穴もまとめました。

Workforce Identity Federation は、人が外部の ID で Google Cloud にサインインする仕組みです。違いはWorkforce Identity Federationとは?Workloadとの違いで解説しています。Azure 側の同じ構成はGitHub ActionsからAzureへOIDCで認証する手順を参照してください。

本記事のまとめ

  • Provider には必ず attribute_condition を書き、認証できるリポジトリを絞る。
  • サービスアカウントを借りられる相手も、リポジトリの属性で指定する。
  • ワークフローは id-token: write と google-github-actions/auth で足りる。
  • roles/editor を付けても足りない権限があり、足すたびに最初の付与は人が行う。
GitHub Actions の plan ジョブのログ。Authenticate to Google Cloud のステップが成功し、workload_identity_provider と service_account は *** と表示されている
plan ジョブの認証ステップ。Provider のリソース名とサービスアカウントは GitHub Secrets 経由で渡しているため *** になる。認証情報ファイルのパスに含まれるリポジトリ名は <REPOSITORY> に伏せた

GitHub Actions から Google Cloud へ認証する構成

Google Cloud 側で作るものは、Workload Identity Pool、Provider、サービスアカウントの 3 つです。Provider が「どの GitHub のトークンを受け入れるか」を決め、サービスアカウントのロールが「何をしてよいか」を決めます。用語と全体像はGoogle Cloud の Workload Identity Federation のドキュメントにまとまっています。

作るもの役割
Workload Identity Pool外部の ID を受け入れる入れ物。プロジェクトに作る
ProviderGitHub を信頼し、トークンのどの項目を使うかを決める
サービスアカウントTerraform が実際に使う ID。ロールはここに付ける
ワークフローGitHub のトークンを要求し、サービスアカウントを借りる

最初だけは人が作るものもあります。私はプロジェクトの作成、state を置く Cloud Storage のバケット、CI 用サービスアカウントとその最初のロールを gcloud で用意しました。Pool と Provider、サービスアカウントとの紐付けは、プロジェクトごとのディレクトリで Terraform に書いています。

Google Cloud コンソールの Workload Identity 連携の一覧。GitHub Actions Pool(ID: github-actions-pool)の下に OIDC の GitHub Actions Provider(ID: github-actions-provider)があり、どちらもステータスが有効
Terraform で作った Pool と Provider が一覧に並んだ状態。表示されている ID は本文のコードで指定した github-actions-pool と github-actions-provider で、伏せた値は無い

Workload Identity Federation の Pool と Provider を作る

Provider では、GitHub のトークンの発行元(issuer)と、トークンのどの項目を使うかを決めます。いちばん大事なのは attribute_condition で、自分のリポジトリ以外のトークンを入口で弾きます。

resource "google_iam_workload_identity_pool" "github_pool" {
  project                   = var.project_id
  workload_identity_pool_id = "github-actions-pool"
  display_name              = "GitHub Actions Pool"
}

resource "google_iam_workload_identity_pool_provider" "github_provider" {
  project                            = var.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.github_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-actions-provider"

  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.repository" = "assertion.repository"
  }

  # 自分のリポジトリからのトークンだけを受け入れる
  attribute_condition = "attribute.repository == '${local.github_repo}'"

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

GitHub のトークンは、GitHub を使う誰のリポジトリからでも発行されます。条件を書かないと、他人のリポジトリのトークンも Pool に入れてしまいます。google-github-actions/auth の README でも、Pool への入口を絞る条件を必ず付けるよう求めています。

同じ README によると、Pool、Provider、IAM の設定が反映されるまでには最大 5 分ほどかかります。作った直後に認証が失敗しても、少し待ってから試し直してください。

github-actions-provider の編集画面。属性のマッピングは google.subject に assertion.sub、attribute.repository に assertion.repository が設定され、属性条件の条件 CEL は attribute.repository == でリポジトリを 1 つに限定している。リポジトリ名は黒塗りのラベルに置き換えてある
本文の HCL がコンソールに反映された状態。attribute_mapping の 2 行と attribute_condition がそのまま表示されている。リポジトリ名は <REPOSITORY-OWNER/NAME> に伏せた

サービスアカウントを借りられるリポジトリを絞る

次に、GitHub Actions がサービスアカウントを借りられるようにします。サービスアカウントに roles/iam.workloadIdentityUser を付け、相手はリポジトリの属性で指定します。

resource "google_service_account_iam_member" "github_actions_wif_binding" {
  service_account_id = google_service_account.github_actions.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_pool.name}/attribute.repository/${local.github_repo}"
}

私の構成では、1 つの Pool から 2 つのサービスアカウントを借りられるようにしています。プロジェクトの中を操作するものと、組織全体の設定を操作するものです。どちらも同じリポジトリだけに許可しています。

なお、README では、サービスアカウントを介さずに Pool の ID へ直接ロールを付ける方式が推奨されています。私の構成はサービスアカウントを介す方式です。これから作るなら、直接付与する方式も比べてみてください。

CI 用サービスアカウントのアクセス権を持つプリンシパルの一覧。principalSet でリポジトリを 1 つ指定したプリンシパルに、Workload Identity ユーザーとサービス アカウント トークン作成者のロールが付いている。プロジェクト番号とリポジトリ名は黒塗りのラベルに置き換えてある
サービスアカウントを借りられる相手を principalSet で 1 つのリポジトリに絞った状態。プロジェクト番号は <PROJECT-NUM>、リポジトリ名は <REPOSITORY-OWNER/NAME> に伏せ、サービスアカウントの一意 ID が出ていたパンくずは切り落とした

ワークフローで google-github-actions/auth を使う

ワークフロー側は、id-token: write の権限を付けて google-github-actions/auth を呼ぶだけです。渡す値は Provider のリソース名とサービスアカウントのメールアドレスの 2 つで、秘密の値は無いので、キーを管理する必要がありません。

permissions:
  contents: read
  id-token: write        # GitHub にトークンを要求するために必須
  pull-requests: write

steps:
  - uses: actions/checkout@v4

  - name: Authenticate to Google Cloud
    uses: google-github-actions/auth@v2
    with:
      workload_identity_provider: ${{ secrets.wif_provider }}
      service_account: ${{ secrets.service_account }}

このあとの Terraform は、何も設定しなくてもこの認証情報を使います。私の構成では、plan は PR を作ると自動で走り、apply は PR に terraform apply 環境名 とコメントして GitHub Actions を起動しています。

このワークフローはジョブに environment: を書いていますが、認証には影響しません。条件をリポジトリの属性で書いているためです。Azure は subject の完全一致で照合するので、同じ書き方をすると認証が通らなくなります。

GitHub Actions が PR に投稿した plan の結果コメント。Plan: 1 to add, 0 to change, 0 to destroy と表示され、apply と plan を起動するコメント文が添えられている。プロジェクト ID が出る 3 箇所は黒塗りのラベルに置き換えてある
plan が成功して PR にコメントが投稿された状態。コメントで apply を起動する文面もここに出る。プロジェクト ID は <PROJECT-ID> に伏せた

運用しながら踏んだ落とし穴

認証そのものは一度作ると安定して動きました。つまずいたのは、認証の後にある権限と、ワークフローの起動の仕方でした。

roles/editor でも足りない権限がある

CI 用サービスアカウントには roles/editor を付けていますが、足りない場面が何度もありました。たとえば、IAM のバインディングを Terraform で管理するには、IAM ポリシーを変更する権限(roles/resourcemanager.projectIamAdmin)が別に要ります。

Workforce Identity Federation で使う OAuth client を Terraform で作ったときは、apply が 403 で失敗しました。iam.oauthClients.* の権限は、roles/owner と roles/editor のどちらにも含まれていません。roles/iam.oauthClientAdmin を明示して付けるしかありませんでした。

サービスアカウントが自分にロールを足す権限を持っていないときは、最初の付与を人が gcloud で行い、あとから import ブロックで Terraform に取り込んでいます。

組織全体の操作は別のサービスアカウントにする

プロジェクト用のサービスアカウントには、組織レベルの IAM を管理する権限がありません。組織の設定を扱うディレクトリには専用のサービスアカウントを用意し、同じ Pool から借りられるようにしました。

コメントで起動する apply は main の定義で動く

PR へのコメントで起動するワークフローは、PR のブランチではなく main にあるワークフローの定義で動きます。ワークフローに新しい Secret の受け渡しを足した PR で、apply をコメントから起動しました。結果は Apply complete! Resources: 0 added, 0 changed, 0 destroyed. で、成功したのに何も作られていません。

main の定義にはまだ受け渡しが無く、値が空のまま渡ったためです。ワークフローの変更は、main にマージしてからでないとコメント起動には反映されません。

一方、PR をマージすると、その PR へのコメントで apply する起点が失われます。マージ済みの PR へコメントしても、マージ前の内容が適用されるためです。必要な apply は、マージする前にすべて終わらせています。

まとめ

GitHub Actions から Google Cloud へ、サービスアカウントのキーを使わずに Terraform を実行できています。GitHub Secrets に置いているのは、Provider のリソース名とサービスアカウントのメールアドレスだけです。

  • Provider には attribute_condition を必ず書き、サービスアカウントを借りられる相手もリポジトリの属性で絞る。
  • ワークフローは id-token: write と google-github-actions/auth で認証する。
  • roles/editor で足りない権限は明示して付け、コメント起動の apply は main の定義で動くことを前提に進める。

ここに書いた反映までの待ち時間、ロールに含まれる権限、公式が勧める付与の方式、コンソールの画面構成は、どれも変わります。設定する前に、Google Cloud と google-github-actions/auth の公式ドキュメントで最新を確かめてください。