背景

Google CloudでVPC Service Controls(VPC-SC)を有効化した際、思わぬ落とし穴にはまりました。
具体的には「VPC-SC有効プロジェクトのインフラ作業後、別プロジェクトのTerraformが突然動かなくなる」という事象です。

VPC-SCとは

VPC-SCはGoogle Cloudのセキュリティ機能で、Cloud StorageやFirestoreなどのAPIを「ペリメーター(境界)」で囲い、境界外からのアクセスをブロックします。
アクセス制御はホワイトリスト型で、許可するものを明示的に列挙し、それ以外はすべてデフォルトで拒否されます。

代表的な保護対象サービスの例:

  • storage.googleapis.com(Cloud Storage)
  • firestore.googleapis.com
  • aiplatform.googleapis.com(Vertex AI)
  • run.googleapis.com(Cloud Run)
  • secretmanager.googleapis.com

事象:別プロジェクトのterraform applyがブロックされる

前提となる構成

プロジェクト VPC-SC 役割
プロジェクトA 有効 VPC-SCを設定している側
プロジェクトB 無効 別の案件・用途のプロジェクト

TerraformのstateファイルはそれぞれのプロジェクトのCloud Storageバケットで管理しています。

  • プロジェクトBのconfig.tf
terraform {
backend "gcs" {
bucket = "project-b-tfstate"
prefix = "terraform/state"
}
}

このGCSバケットへのアクセスがVPC-SCによってブロックされます。

操作の流れ

  1. プロジェクトAのVPC-SCを含んだTerraformを実行するため、ADCのquota_projectを設定
  2. プロジェクトAへの terraform apply → 成功
  3. 同じプリンシパルを使うため、再ログインせずにプロジェクトBのTerraformを実行
  4. terraform apply403エラーでブロック

プロジェクトBはVPC-SCが有効ではない。それでもブロックされる。

発生するエラー

terraform init または terraform apply の冒頭でstateバケットへのアクセスが失敗し、以下のようなエラーが出力されます。

│ Error: error loading state: Failed to open state file at
│ gs://project-b-tfstate/terraform/state/default.tfstate:
│ googleapi: got HTTP response code 403 with body:
│ <!--?xml version='1.0' encoding='UTF-8'?-->
│
│ <code>SecurityPolicyViolated</code>
│ Request violates VPC Service Controls.
│
|
| <details>Request is prohibited by organization's policy.
│ vpcServiceControlsUniqueIdentifier: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</details>

IAMの権限は問題ないのに403が返る点、SecurityPolicyViolated というエラーコードと Request violates VPC Service Controls. というメッセージが特徴です。
vpcServiceControlsUniqueIdentifier はVPC-SC違反ログをGoogle Cloudコンソールで検索する際に使用できる識別子です。

対処方法

先に対処方法から紹介します。

プロジェクトAの作業前

gcloud auth application-default set-quota-project <VPC-SC有効プロジェクトID>

作業終了後(他プロジェクトに戻る前に必ず実施)

rm ~/.config/gcloud/application_default_credentials.json
gcloud auth application-default login --disable-quota-project # quota_project未設定の状態で再取得

現在の設定確認

cat ~/.config/gcloud/application_default_credentials.json

注意点

gcloud config set billing/quota_project はgcloud CLI向けの設定で、Terraformには効きません。
Terraform(ADC)には gcloud auth application-default set-quota-project が必要です。

詳しい解説

結論から言うとブロックされているのはアクセス先(プロジェクトB)では無く、アクセス元(プロジェクトA)側でした。
仕組みを理解するには VPC-SCのデプロイで必要になるquota_project設定がVPC-SCの評価でどう扱われるかを知る必要があります。

quota_projectが持つ二つの役割

gcloud auth application-default set-quota-project を実行すると、~/.config/gcloud/application_default_credentials.json に以下のように記録されます。

{
"type": "authorized_user",
"quota_project_id": "project-a"
}

Terraformはこのファイルを読んでGoogle Cloud APIを呼び出すため、このフィールドが残っている限り、どのディレクトリで apply してもquota_project=project-aとしてリクエストが飛び続けます。

quota_project には二つの役割があります:

役割 説明
① 課金・クォータの帰属先 そのAPIコールの費用・クォータをどのプロジェクトに計上するか
② VPC-SCにおけるリクエストの発信元 どのペリメーターから発信されたリクエストかの判定に使われる

VPC-SC有効化前は①しか意味を持ちませんでしたが、VPC-SC有効化後は②の役割も発動します

VPC-SCの評価(Egressポリシー)

quota_project=project-a を設定すると、ローカルマシンからのリクエストであっても「プロジェクトAのペリメーター内部から発信されたリクエスト」として扱われます。

terraform apply(プロジェクトB、quota_project=project-a)
└─ storage.googleapis.com へのAPIコール
├─ IAMチェック: OK(アカウントに権限あり)
└─ VPC-SCチェック
├─ リクエストの発信元: プロジェクトAのペリメーター内部(quota_projectによる帰属)
├─ アクセス先: プロジェクトBのbucket(ペリメーター外部)
└─ Egressポリシー: ペリメーター内→外への通信はデフォルトでブロック
└─ 明示的なEgress許可なし → BLOCKED (403)

「ペリメーター内部から外部へのデータ持ち出し(Egress)を防ぐ」というVPC-SCの基本原則が発動しているため、アクセス先がVPC-SC未設定のプロジェクトであっても同様にブロックされます。
プロジェクトAへのデプロイ時は同プロジェクト内のため問題なくapply出来たと言う訳です。

VPC-SC有効化前はなぜ問題なかったか

VPC-SC有効化前は、アクセス制御はIAMのみで行われていました。

(VPC-SCなし)
terraform apply(quota_project=project-a)
└─ GCS APIコール
└─ IAMチェック: アカウントに権限があるか? → あればOK

quota_project はこの場合課金先・クォータ消費先を指定するだけのフィールドで、アクセス可否には影響しません。

VPC-SCを有効にすることで quota_project が「リクエストの発信元ペリメーター」を決定するコンテキストとしても扱われるようになり、同じ操作でブロックされるようになりました。これはVPC-SCが意図通りに機能している結果です。

まとめ

項目 VPC-SCなし VPC-SCあり
アクセス制御の主体 IAMのみ IAM + VPC-SC
(独立した追加レイヤー)
quota_project の役割 課金・クォータ先の指定のみ ・課金/クォータ先
・リクエストの発信元ペリメーター判定にも使われる
IAM通過後のブロック なし あり
(VPC-SC Egressポリシーで拒否)
デフォルト動作 許可(IAMで制御) 拒否
(明示的に許可されたもののみ通す)
アクセス先にVPC-SCがなくてもブロックされるか なし あり
(発信元ペリメーターのEgressポリシーで評価されるため)

VPC-SCは強力なセキュリティ機能ですが、ADCのquota_projectという普段意識しない設定がapplyに必要になり、かつ「リクエストの発信元ペリメーター」として扱われブロックが発生する点は見落としがちです。
作業後のADCリセットを手順に組み込んでおくことをおすすめします。