Managing IAM with Terraform in GCP: Member vs Binding vs Policy

Terraform lets you manage GCP IAM as code, so every access change goes through version control, a pull request, and a terraform plan before it reaches production. But choosing the wrong Terraform IAM resource type can silently delete bindings your colleagues added. This page explains how to get IAM-as-code right.

Simple explanation

GCP IAM controls who can do what on which resource. You can manage IAM through the Cloud Console or with gcloud commands. Terraform lets you define those same permissions in code, check them into Git, and apply them in a controlled, reviewable way.

The catch is that Terraform has three different IAM resource types, and they behave very differently:

  • iam_member: adds one binding. Leaves everything else alone. Safest default.
  • iam_binding: manages all members of a single role. Removes unlisted members.
  • iam_policy: manages the entire project IAM policy. Removes everything not declared.

Most problems with Terraform IAM come from using iam_binding or iam_policy in shared projects where other teams or tools also manage access. Understanding which resource type to reach for is the most important decision on this page.

Why manage IAM with Terraform?

In a team environment, IAM permissions change constantly. Without version control, it is almost impossible to answer: why does this service account have roles/editor? Who approved that binding? Is it still needed?

Managing GCP IAM with Terraform gives you:

  • Version control: every IAM change is a Git commit with an author and timestamp
  • Pull request review: over-permissive grants get caught before they reach production
  • Plan preview: terraform plan shows exactly which bindings will be added or removed before you apply
  • Repeatability: bootstrap a new project with the same IAM baseline every time
  • Drift detection: run plans regularly to spot access changes made outside Terraform

This workflow pairs naturally with the principle of least privilege. It is much easier to enforce minimal permissions when every grant is a reviewed line of code rather than a point-and-click action in the Console.

How it works

The workflow for managing IAM with Terraform follows the same pattern as any other infrastructure-as-code change:

  1. Define IAM bindings in .tf files using the appropriate resource type
  2. Run terraform plan to see exactly what will change
  3. Submit a pull request so reviewers can check the proposed access changes
  4. Merge and apply — Terraform reconciles GCP IAM to match your declared state
  5. Run plans periodically between applies to detect drift from out-of-band changes

Terraform stores the last known state of your IAM in a state file. When it runs a plan, it compares that state to live GCP configuration and shows the diff. If someone added a binding through the Console since your last apply, it appears as an unexpected addition.

The three Terraform IAM resource types

Google’s Terraform provider has three resource types for managing IAM policies at the project level. Each accepts a role name and behaves differently from the others.

Analogy: the whiteboard

Think of your project’s IAM policy as a whiteboard with a section for each role. iam_member adds a sticky note under one role without touching any other notes. iam_binding erases the entire section for one role and rewrites it from your list. iam_policy erases the entire whiteboard and starts fresh. Miss someone, and they lose access immediately.

google_project_iam_member — additive, safest default

Manages one principal-to-role binding. Applying it adds that binding. Removing it from config removes that one binding. Everything else in the project’s IAM policy is untouched. This is the right choice for the vast majority of situations.

Risk level: low. Safe to use in shared projects alongside bindings from other teams, other modules, and manually added access.

# Adds one binding — leaves all other bindings alone
resource "google_project_iam_member" "platform_logging_viewer" {
  project = "my-app-prod"
  role    = "roles/logging.viewer"
  member  = "group:platform-engineers@example.com"
}

google_project_iam_binding — authoritative per role

Manages the complete member list for one specific role. If a principal has that role in GCP but is not listed in your Terraform config, Terraform removes them on the next apply.

Risk level: medium. Safe only when your team fully owns that role assignment and no other team or process ever grants it. In shared projects, this is a common cause of accidental access removal.

# WARNING: Authoritative for roles/logging.viewer on this project.
# Any principal NOT listed here will be REMOVED by Terraform on next apply.
resource "google_project_iam_binding" "logging_viewers" {
  project = "my-app-prod"
  role    = "roles/logging.viewer"

  members = [
    "group:platform-engineers@example.com",
    "group:security-auditors@example.com",
  ]
}

google_project_iam_policy — fully authoritative, highest risk

Manages the entire IAM policy for the project. Replaces the full policy with whatever is in your Terraform config. Any binding that is not declared gets removed on apply, including bindings from other teams and GCP’s own default service account bindings.

Risk level: high. Only appropriate when Terraform is the sole source of truth for the entire project IAM policy.

Use with extreme caution

In a shared project, a single terraform apply with google_project_iam_policy can silently remove every binding your team, other teams, and GCP itself added. This includes default bindings for Compute Engine and Cloud Build service accounts. Most teams should never reach for this resource type.

Comparison table

iam_memberiam_bindingiam_policy
Scope of controlOne bindingAll members of one roleEntire project policy
Additive or authoritativeAdditiveAuthoritative per roleFully authoritative
Removes unlisted membersNeverFor that role onlyYes, everything not declared
Safe in shared projectsYesRiskyNo
Best use caseDefault choiceEnforcing who holds a role when your team owns it fullyGreenfield where Terraform is sole IAM owner
Risk levelLowMediumHigh
Other scopes

All three resource types are available at other scopes too: google_folder_iam_member, google_organization_iam_member, and for individual resources like google_storage_bucket_iam_member, google_bigquery_dataset_iam_member, and google_secret_manager_secret_iam_member. Resource-level bindings are often the safer and more precise option. Grant access to a specific bucket rather than the whole project wherever possible.

Which one should you use?

Follow this decision order:

  1. Default to iam_member. It is the only option that composes safely with bindings managed by other teams, other Terraform modules, or access granted in the Console. Use it unless you have a specific reason not to.

  2. Use iam_binding only when your team fully owns a role. This means no other team or tool ever grants this role on this project, and you want Terraform to enforce that. In practice, this is rare in multi-team environments.

  3. Use iam_policy only in tightly controlled projects where Terraform is the single source of truth for all IAM. No Console access, no other tooling, no other teams. Even then, carefully account for GCP-managed default bindings or they will be removed on the next apply.

When in doubt

If you are unsure, choose iam_member. The risk of accidentally deleting access with iam_binding or iam_policy in a shared project is real and hard to reverse quickly.

When to use Terraform for IAM

Terraform IAM management is particularly valuable in these scenarios:

  • Shared platform teams: when a platform team provisions access for multiple application teams, Terraform ensures every grant is reviewed and auditable.

  • Project bootstrap: setting up a consistent IAM baseline across many projects. Define it once, apply it everywhere via a reusable module structure.

  • Tightly controlled environments: production projects where every access change should go through a PR and approval process.

  • Resource-level permissions: granting a service account access to a specific Cloud Storage bucket or Secret Manager secret rather than broad project-level access. See Cloud Storage IAM vs ACLs for how this works with buckets specifically.

  • Temporary access with conditions: granting time-limited access using IAM Conditions that automatically expire, defined and tracked in code.

  • Service account impersonation: managing who can impersonate a service account through Terraform-managed bindings rather than ad hoc Console grants.

Practical examples

Safe iam_member — the standard pattern

# Additive: adds this one binding, leaves all other project IAM alone
resource "google_project_iam_member" "platform_logging_viewer" {
  project = "my-app-prod"
  role    = "roles/logging.viewer"
  member  = "group:platform-engineers@example.com"
}

Temporary access with an expiry condition

Use IAM Conditions to grant time-limited access that is tracked in code and expires automatically.

resource "google_project_iam_member" "contractor_temp_access" {
  project = "my-app-prod"
  role    = "roles/logging.viewer"
  member  = "user:contractor@external.com"

  condition {
    title       = "expires-q3-2026"
    description = "Temporary access for infrastructure audit"
    expression  = "request.time < timestamp(\"2026-09-30T23:59:59Z\")"
  }
}

Resource-level IAM — safer than project-level grants

Instead of granting a service account broad project access, grant it access only to the specific resources it needs. This is a core least privilege technique.

# Grant write access to a specific bucket only — not the whole project
resource "google_storage_bucket_iam_member" "etl_writer" {
  bucket = google_storage_bucket.data_lake.name
  role   = "roles/storage.objectCreator"
  member = "serviceAccount:${google_service_account.etl_job.email}"
}

# Grant read access to a specific BigQuery dataset only
resource "google_bigquery_dataset_iam_member" "analysts_reader" {
  dataset_id = google_bigquery_dataset.analytics.dataset_id
  project    = "my-app-prod"
  role       = "roles/bigquery.dataViewer"
  member     = "group:data-analysts@example.com"
}

# Grant access to a specific secret only
resource "google_secret_manager_secret_iam_member" "app_secret_access" {
  secret_id = google_secret_manager_secret.db_password.secret_id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.app.email}"
}

iam_binding — with a clear warning

# WARNING: Authoritative for roles/logging.viewer on this project.
# Terraform will REMOVE any principal with this role who is not listed here.
# Only use this if your team is the sole owner of this role assignment.
resource "google_project_iam_binding" "logging_viewers" {
  project = "my-app-prod"
  role    = "roles/logging.viewer"

  members = [
    "group:platform-engineers@example.com",
    "group:security-auditors@example.com",
  ]
}
CI policy check

Add a CI step that fails the pipeline if any module introduces roles/editor, roles/owner, or roles/viewer bindings. See basic vs predefined roles for why these broad roles are problematic in production Terraform.

Common mistakes

  1. Using google_project_iam_policy in a shared project. This removes every binding not declared in your config, including those from other teams and GCP’s own default service account bindings. In shared environments, use iam_member instead.

  2. Mixing iam_binding and iam_member for the same role. If you manage roles/logging.viewer with both an iam_binding and an iam_member on the same project, Terraform fights itself on each plan. Pick one resource type per role per resource.

  3. Granting broad basic roles. When a module grants roles/editor to simplify setup, every team using that module inherits the over-grant. Enforce least privilege in the code, not in a comment asking reviewers to watch out.

  4. Granting project-level access when resource-level is possible. A service account that only needs to read one Cloud Storage bucket should get google_storage_bucket_iam_member, not google_project_iam_member with roles/storage.objectViewer. Project-level grants apply to all resources in the project.

  5. Ignoring out-of-band IAM changes. Emergency Console changes or access granted by a third-party tool show as drift in your next plan. If you never run plans between applies, drift accumulates silently. Treat drift reports as action items to investigate, not noise to dismiss.

Managing IAM drift safely

IAM drift happens when someone changes IAM outside of Terraform, through the Console, gcloud, or another automation tool. The binding exists in GCP but not in your Terraform state, or vice versa.

Run terraform plan regularly, not just before applies, to detect drift early. The plan output shows bindings that exist in GCP but not in your config, and the changes your next apply would make.

Investigate before removing

When you find drift, do not immediately remove it. An unexpected binding might be a security incident, an emergency fix during an outage, or a legitimate change not yet added to the codebase. Removing it before understanding why it is there can break running services.

Emergency IAM changes happen. When they do, the gcloud CLI is the fastest tool for reading and modifying bindings directly. After the emergency resolves, reconcile that change back into Terraform code so the grant is tracked, reviewed, and not silently removed on the next apply.

Terraform IAM vs gcloud

Both tools manage the same underlying GCP IAM API. The choice is about workflow, not capability.

SituationBetter tool
Setting up IAM for a new projectTerraform
Granting access that must be reviewed before applyingTerraform
Keeping IAM consistent across many projectsTerraform
Auditing who currently has a rolegcloud
Emergency access fix during an incidentgcloud
Quick one-off grant for a developergcloud
Exploring current IAM state interactivelygcloud

In most team environments, Terraform owns the long-term IAM state and gcloud is used for reading, debugging, and emergency changes that are later reconciled into code. The two tools complement each other. See managing IAM with gcloud for the CLI side of this workflow.

Frequently asked questions

What is the difference between google_project_iam_member and google_project_iam_binding?

iam_member is additive. It manages one principal-to-role binding without affecting anything else in the project’s IAM policy. iam_binding is authoritative per role. It manages the complete list of members for a specific role and removes any member not listed in your config on the next apply. Use iam_member by default, especially in shared projects.

Does Terraform overwrite GCP IAM?

It depends on the resource type. iam_member only touches the bindings it declares. iam_binding overwrites the member list for a given role. iam_policy overwrites the entire project IAM policy. Using the wrong type in a shared project is one of the most common causes of accidental access removal.

Should I use google_project_iam_policy in production?

Only if Terraform is the sole source of truth for your entire project IAM policy and no other team, tool, or person ever adds bindings outside of it. In practice, this is rare. Most production projects have shared access and should use iam_member instead.

Can I manage bucket IAM and project IAM in the same Terraform repo?

Yes. You can freely mix project-level resources like google_project_iam_member with resource-level resources like google_storage_bucket_iam_member in the same repo. They operate at different scopes and do not conflict. See Cloud Storage IAM vs ACLs for how bucket-level IAM works in detail.

How do I stop Terraform from deleting manually added IAM bindings?

Use google_project_iam_member instead of iam_binding or iam_policy. Because iam_member is additive, it only manages the bindings explicitly declared in your config and leaves all other bindings untouched.

Frequently asked questions

What is the difference between google_project_iam_member and google_project_iam_binding?

iam_member is additive — it manages one principal-to-role binding without touching anything else. iam_binding is authoritative per role — it manages all members of a specific role, and removes any member not listed in your config on the next apply. Use iam_member by default in shared projects to avoid unintended removals.

Does Terraform overwrite GCP IAM?

It depends on which resource type you use. google_project_iam_member is additive and only manages the bindings it declares. google_project_iam_binding overwrites the full member list for a given role. google_project_iam_policy overwrites the entire project IAM policy, removing anything not declared in Terraform. Choosing the wrong type is one of the most common causes of accidental access removal.

Should I use google_project_iam_policy in production?

Only if Terraform is the sole source of truth for your entire project IAM policy and no other team or tool ever adds bindings outside of it. In shared projects or multi-team environments, iam_policy is dangerous because it removes any binding it was not told about — including GCP default service account bindings.

Can I manage bucket IAM and project IAM in the same Terraform repo?

Yes. You can freely mix project-level resources like google_project_iam_member with resource-level resources like google_storage_bucket_iam_member in the same repo. These operate at different scopes and do not conflict with each other. Resource-level bindings are often the safer and more precise option.

How do I stop Terraform from deleting manually added IAM bindings?

Use google_project_iam_member instead of google_project_iam_binding or google_project_iam_policy. iam_member is additive and only manages the bindings explicitly declared in your config. The other two resource types are authoritative and will remove bindings they were not told to keep.

Last verified: 22 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.