★★ 中級

06. IAM

GCP IAM は「member(誰が)role(何ができる)resource スコープで付与」というシンプルなモデル。ただし Terraform の 3 種類のリソース(policy / binding / member)の使い分けを誤ると大事故になります。

3 種類のリソース

GCP IAM の Terraform リソースは 同じ目的に 3 種類 あります。違いを理解せず使うと 全 IAM 削除事故 が起きる。

リソース動作安全度
google_*_iam_member1 ロール × 1 member を 追加。他に影響なし★★★ 推奨
google_*_iam_binding1 ロール の member 一覧を完全に置き換え。他で同じロールに追加した member は消える★★ 注意
google_*_iam_policyそのリソースの 全 IAM ポリシーを置き換え。Terraform 外で付けたロールも全消滅原則使わない
policy は事故の元 google_project_iam_policy を 1 行書いただけで、コンソール/組織で付けてあった全 IAM が消滅します。本番では絶対に使わないこと。

member の書式

user:alice@example.com                                              # 人間
serviceAccount:my-sa@myapp-prd.iam.gserviceaccount.com              # SA
group:dev-team@example.com                                          # Workspace グループ
domain:example.com                                                  # ドメイン全員
allUsers                                                            # 全インターネット(公開)
allAuthenticatedUsers                                               # Google アカウントログイン済み全員
principal://iam.googleapis.com/projects/.../locations/.../subject/  # Workload Identity

スコープ

権限を付ける場所は階層に応じて選びます:

スコープTerraform リソース
Organizationgoogle_organization_iam_member
Foldergoogle_folder_iam_member
Projectgoogle_project_iam_member
Bucketgoogle_storage_bucket_iam_member
SA 自身google_service_account_iam_member
Secret Manager Secretgoogle_secret_manager_secret_iam_member

主要なロール

# プロジェクト全体に Editor(広い権限。本番ではなるべく避ける)
resource "google_project_iam_member" "alice_editor" {
  project = "myapp-prd"
  role    = "roles/editor"
  member  = "user:alice@example.com"
}

# Cloud Storage Object 読み取りだけ
resource "google_storage_bucket_iam_member" "vm_read" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.vm.email}"
}

# Cloud Run の Invoker(呼び出しのみ)
resource "google_cloud_run_v2_service_iam_member" "public" {
  location = google_cloud_run_v2_service.api.location
  name     = google_cloud_run_v2_service.api.name
  role     = "roles/run.invoker"
  member   = "allUsers"   # 全公開(注意)
}

よく使う基本ロール(広い、本番非推奨)

ロール権限
roles/ownerすべて + IAM 付与
roles/editorリソース変更全般
roles/viewer閲覧のみ

本番では サービス固有ロールroles/storage.objectViewer 等)を組み合わせて使うのが鉄則。

Service Account

resource "google_service_account" "app" {
  account_id   = "myapp-api"
  display_name = "myapp API runtime SA"
  description  = "Used by Cloud Run service myapp-api"
}

# プロジェクトレベルで権限付与
resource "google_project_iam_member" "app_storage" {
  project = "myapp-prd"
  role    = "roles/storage.objectAdmin"
  member  = "serviceAccount:${google_service_account.app.email}"
}

resource "google_project_iam_member" "app_secret" {
  project = "myapp-prd"
  role    = "roles/secretmanager.secretAccessor"
  member  = "serviceAccount:${google_service_account.app.email}"
}

# SA を「なりすませる」権限を CI に付与(Impersonation)
resource "google_service_account_iam_member" "ci_impersonate_app" {
  service_account_id = google_service_account.app.name
  role               = "roles/iam.serviceAccountTokenCreator"
  member             = "user:ci@example.com"
}

カスタムロール

resource "google_project_iam_custom_role" "myapp_deployer" {
  role_id     = "myappDeployer"
  title       = "myapp Deployer"
  description = "Deploy myapp resources only"
  project     = "myapp-prd"

  permissions = [
    "run.services.create",
    "run.services.update",
    "run.services.get",
    "artifactregistry.repositories.uploadArtifacts",
    "storage.objects.create",
    "storage.objects.get",
  ]
}

resource "google_project_iam_member" "ci_deployer" {
  project = "myapp-prd"
  role    = google_project_iam_custom_role.myapp_deployer.id
  member  = "serviceAccount:ci-terraform@myapp-prd.iam.gserviceaccount.com"
}

条件付き IAM

「特定の時間帯のみ」「特定のリソースのみ」のように条件を CEL(Common Expression Language)で記述できる。

resource "google_storage_bucket_iam_member" "limited" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  member = "user:alice@example.com"

  condition {
    title       = "expires_end_of_2026"
    description = "2026 年末まで有効"
    expression  = "request.time < timestamp('2027-01-01T00:00:00Z')"
  }
}