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 planshows 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:
- Define IAM bindings in
.tffiles using the appropriate resource type - Run
terraform planto see exactly what will change - Submit a pull request so reviewers can check the proposed access changes
- Merge and apply — Terraform reconciles GCP IAM to match your declared state
- 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.
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.
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_member | iam_binding | iam_policy | |
|---|---|---|---|
| Scope of control | One binding | All members of one role | Entire project policy |
| Additive or authoritative | Additive | Authoritative per role | Fully authoritative |
| Removes unlisted members | Never | For that role only | Yes, everything not declared |
| Safe in shared projects | Yes | Risky | No |
| Best use case | Default choice | Enforcing who holds a role when your team owns it fully | Greenfield where Terraform is sole IAM owner |
| Risk level | Low | Medium | High |
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:
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.Use
iam_bindingonly 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.Use
iam_policyonly 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.
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",
]
}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
Using
google_project_iam_policyin 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, useiam_memberinstead.Mixing
iam_bindingandiam_memberfor the same role. If you manageroles/logging.viewerwith both aniam_bindingand aniam_memberon the same project, Terraform fights itself on each plan. Pick one resource type per role per resource.Granting broad basic roles. When a module grants
roles/editorto 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.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, notgoogle_project_iam_memberwithroles/storage.objectViewer. Project-level grants apply to all resources in the project.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.
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.
| Situation | Better tool |
|---|---|
| Setting up IAM for a new project | Terraform |
| Granting access that must be reviewed before applying | Terraform |
| Keeping IAM consistent across many projects | Terraform |
| Auditing who currently has a role | gcloud |
| Emergency access fix during an incident | gcloud |
| Quick one-off grant for a developer | gcloud |
| Exploring current IAM state interactively | gcloud |
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.
Summary
- Terraform brings version control, pull request review, and plan preview to IAM changes, making over-permissive grants visible before they reach production
iam_memberis additive and safe in shared projects. It manages one binding and leaves everything else aloneiam_bindingis authoritative per role. It removes any member of that role not listed in your configiam_policyis fully authoritative. It removes everything not declared, including GCP default bindings- Use
iam_memberby default; reach foriam_bindingonly when your team fully owns a role; avoidiam_policyin shared projects - Never mix
iam_bindingandiam_memberfor the same role on the same resource - Resource-level IAM (bucket, dataset, secret) is more precise and safer than broad project-level grants
- Run
terraform planregularly to detect IAM drift, and investigate before removing access you do not recognise
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.