Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

GitHub Actions から GCP に繋ぐとき実際に出る3つのエラー

公開 読了時間 約6分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

Workload Identity 連携の impersonation 失敗は、GitHub Actions 側の id-token 権限・プール側の attribute_condition・サービスアカウント側の workloadIdentityUser バインディングという3層のどこかが欠けているために起きるが、エラー文言はどの層かを教えてくれない。

結論

Workload Identity 連携の失敗は、設定が3層に分かれていることが原因です。エラー文言は「どの層か」を教えてくれません。

  • GitHub Actions 側 … job に id-token: write が付いているか
  • プール側attribute_condition が実行元のリポジトリを許可しているか
  • サービスアカウント側roles/iam.workloadIdentityUser のバインディングが残っているか

3層のどれが欠けても、返ってくるのは似た文言です。切り分けの順番を先に決めておかないと、権限まわりを当てずっぽうに触ることになります。以下は実際に出た3つのエラーを、踏む順に並べたものです。

症状

1. Unable to acquire impersonation credentials

最初に当たるのはこれです。認証ステップそのものが通りません。原因の候補は3つあり、上の層ほど安く確認できます。

  • job に permissionsid-token: write が無い。 GitHub 側で完結するので最初に見ます
  • 別リポジトリから動かしている。 プールの attribute_conditionassertion.repository で実行元を限定しているため、フォークや別リポジトリのワークフローは弾かれます
  • サービスアカウントへの roles/iam.workloadIdentityUser バインディングが drift で消えた。 terraform apply で復元します

3つ目が厄介です。コードは正しいのに動かない状態で、しかも直前の変更とは無関係に起きます。誰かが手で IAM を触ると、Terraform の状態とクラウド側の実体がずれ、この形で表に出ます。

2. Permission 'iam.serviceAccounts.getAccessToken' denied

こちらは impersonation の入口までは通っていて、宛先のサービスアカウントに拒否されています。 権限の付け忘れに見えますが、実際に多いのは取り違えです。

  • サービスアカウントの email を間違えている。 plan 用と deploy 用を分けている構成では、権限の範囲も別々です。plan 用の資格情報で deploy 先を叩けば当然拒否されます
  • 環境名が不一致。 Terraform 側の env_name と、GitHub Actions が参照している Environment がずれていると、意図していない側のサービスアカウントに向かいます

エラーは「権限がない」としか言いません。「向き先が違う」とは言ってくれないので、まず email が意図した1つになっているかを確認します。

3. Provider / pool が見つからない

そもそもプールやプロバイダが存在していないケースです。確認します。

gcloud iam workload-identity-pools providers describe <provider-id> \
  --location=global \
  --workload-identity-pool=<pool-id> \
  --project=<project-id>

存在しなければ terraform apply が未実施です。該当環境のディレクトリで apply します。

原因

3つとも根は同じで、信頼の連鎖が3箇所に分かれて保存されていることです。

GitHub は job ごとに OIDC トークンを発行します。そのトークンをプールが受け取り、attribute_condition で「どのリポジトリからなら受け入れるか」を判定します。通過したものが principal となり、サービスアカウント側の IAM ポリシーに載っていれば impersonation が成立します。

この3つは別々の場所で管理されています。 ワークフローの YAML、Terraform のプール定義、サービスアカウントの IAM ポリシー。どこか1つが欠けても連鎖は切れますが、切れた場所は認証エラーとしてしか観測できません。

直す

実体側がどうなっているかは、サービスアカウントの IAM ポリシーを直接読むのが最も速いです。

gcloud iam service-accounts get-iam-policy \
  <sa-name>@<project-id>.iam.gserviceaccount.com \
  --project=<project-id>

members に、この形の principalSet が入っているのが期待される状態です。

principalSet://iam.googleapis.com/projects/<num>/locations/global/workloadIdentityPools/<pool-id>/attribute.repository/<owner>/<repo>

ここに実行元のリポジトリが入っていなければ、ワークフローをどう直しても通りません。 逆に入っていれば、3層のうち下の2つは生きているので、GitHub 側の permissions を見に行きます。

疑わしいときは即時に遮断できる

JSON キーを置かない構成なので、rotate する対象がそもそもありません。 代わりに、リポジトリからの impersonation を直接止められます。

gcloud iam service-accounts remove-iam-policy-binding \
  <sa-name>@<project-id>.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member='principalSet://iam.googleapis.com/projects/<num>/locations/global/workloadIdentityPools/<pool-id>/attribute.repository/<owner>/<repo>'

バインディングを外した時点で、そのリポジトリからの認証は通らなくなります。キーの失効を待つ必要はありません。

ただし、この操作のあとは必ず Terraform 側を更新して PR を出し直します。 gcloud で直接 IAM を触った状態を放置すると構成が drift し、次に誰かが apply したときに外したはずのバインディングが復活します。応急処置とコードの更新は対にしておきます。

そして、その drift が忘れた頃に出てくる姿が、この記事の1つ目のエラーです。緊急遮断を放置した結果と、原因不明の認証失敗は、同じものの表と裏です。

再発しにくい形

3層に分かれていること自体は変えられません。変えられるのは、確認の順番を固定しておくことです。

安い順に、GitHub 側の permissions → サービスアカウントの get-iam-policy → プールとプロバイダの存在確認。この順で見れば、1つ目は YAML を開くだけ、2つ目はコマンド1本で、3層のどこが切れているかまで絞れます。

エラー文言から原因へ辿れない類の話は他にもあります。terraform の enabled = false では環境を撤去できない では、エラーが原因のモジュールとは別の場所に出ます。

よくある質問

Q1Unable to acquire impersonation credentials はどこを見ればいいですか?

3箇所あります。ワークフローの job に permissions の id-token: write が付いているか、プールの attribute_condition が実行元リポジトリを許可しているか、サービスアカウント側に roles/iam.workloadIdentityUser のバインディングが残っているかです。上から順に確認すると、GitHub 側で完結する1つ目が最も安く切り分けられます。

Q2getAccessToken denied は権限の付け忘れですか?

付け忘れよりも、宛先の取り違えのほうが多いです。plan 用と deploy 用でサービスアカウントを分けている構成では、ワークフローが参照している環境と Terraform 側の環境名が食い違うだけで、権限を持たない側のサービスアカウントに向かって同じエラーが出ます。まず email が意図した1つになっているかを見ます。

Q3Workload Identity 連携にキーの rotate は要りますか?

要りません。JSON キーを置かない構成なので、rotate する対象そのものがありません。代わりに必要なのは、疑わしいときにリポジトリからの impersonation を止める操作です。サービスアカウントの IAM ポリシーから該当の principalSet を外せば即時に遮断できます。

Q4緊急で遮断したあとは何をすればいいですか?

Terraform 側を同じ内容に更新して PR を出し直します。遮断は gcloud で直接 IAM ポリシーを触る操作なので、そのままにすると構成が drift します。次に誰かが apply したときに、外したはずのバインディングが復活します。応急処置とコードの更新は必ず対にします。