はじめに
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 から Google Cloud へ認証する構成
Google Cloud 側で作るものは、Workload Identity Pool、Provider、サービスアカウントの 3 つです。Provider が「どの GitHub のトークンを受け入れるか」を決め、サービスアカウントのロールが「何をしてよいか」を決めます。用語と全体像はGoogle Cloud の Workload Identity Federation のドキュメントにまとまっています。
| 作るもの | 役割 |
| Workload Identity Pool | 外部の ID を受け入れる入れ物。プロジェクトに作る |
| Provider | GitHub を信頼し、トークンのどの項目を使うかを決める |
| サービスアカウント | Terraform が実際に使う ID。ロールはここに付ける |
| ワークフロー | GitHub のトークンを要求し、サービスアカウントを借りる |
最初だけは人が作るものもあります。私はプロジェクトの作成、state を置く Cloud Storage のバケット、CI 用サービスアカウントとその最初のロールを gcloud で用意しました。Pool と Provider、サービスアカウントとの紐付けは、プロジェクトごとのディレクトリで Terraform に書いています。

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 がサービスアカウントを借りられるようにします。サービスアカウントに 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 へ直接ロールを付ける方式が推奨されています。私の構成はサービスアカウントを介す方式です。これから作るなら、直接付与する方式も比べてみてください。

ワークフローで 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 の完全一致で照合するので、同じ書き方をすると認証が通らなくなります。

運用しながら踏んだ落とし穴
認証そのものは一度作ると安定して動きました。つまずいたのは、認証の後にある権限と、ワークフローの起動の仕方でした。
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 の公式ドキュメントで最新を確かめてください。
