Debugging and GCP Propagation Gotchas
Most Terraform-on-GCP failures are not bugs in your HCL — they are timing. GCP grants IAM, enables APIs, and creates resources asynchronously, so a dependent resource in the very same apply hits a control plane that has not caught up yet and fails with a permission or "not enabled" error that vanishes when you re-run. The configuration is correct; the clock is the problem.
TF_LOG shows you the real API request and response behind Terraform's tidy error message, and time_sleep, depends_on, and provider-level retry are the fixes for the propagation class of failure that defines debugging on GCP. Learn to recognize the signature — fails on a fresh apply, passes on the re-run — and most of these stop being mysterious.
TF_LOG and TF_LOG_PATH
Setting TF_LOG=DEBUG (or TRACE for more) exposes every HTTP request and response between the provider and the Google APIs, and TF_LOG_PATH writes it to a file so you can read it without it scrolling past. This is how you turn "Error 403" into the exact API, method, and message that actually failed — the difference between guessing at a cause and reading it.
# Exposes every request/response between the provider and the Google APIs export TF_LOG=DEBUG export TF_LOG_PATH=./tf-debug.log terraform apply
Reading a Google API Error
The provider surfaces the raw API error, and learning to read it is faster than guessing. The HTTP code names the class — 403 permission, 404 not-found, 409 conflict — and the body carries the reason and often a precise detail like SERVICE_DISABLED or userProjectMissing. The message usually names the missing grant or the service that was not enabled; once you read it as the API's own words rather than Terraform's, the fix is obvious.
The GCP Propagation Class of Failure
An IAM binding granted in one resource is not instantly visible to a resource that depends on it. An API enabled by google_project_service is not instantly usable by the next resource. So an apply that creates the grant and the dependent in one run fails intermittently — it passes on a re-run because by then the grant has propagated, and fails on a fresh apply because it had not. That pattern, "fails fresh, passes on re-run," is the signature of asynchronous propagation, not a logic error in your HCL.
The Fixes
There are three fixes, and you reach for the lightest that makes the apply deterministic. depends_on orders the dependent strictly after the grant or enablement, so Terraform at least waits for that API call to return. time_sleep, from the hashicorp/time provider, inserts a deliberate wait for the propagation that the API return does not guarantee. And provider-level retry or google_project_service timeouts absorb the transient error. Use depends_on for ordering and add time_sleep only when ordering is correct but the grant still has not propagated.
resource "time_sleep" "wait_for_iam" { depends_on = [google_project_iam_member.processor] create_duration = "30s" # cover propagation, not the API call } resource "google_cloud_run_v2_service" "api" { depends_on = [time_sleep.wait_for_iam] # ... }
Crash Logs and When It Is Actually a Bug
A true provider crash writes a crash.log with a Go stack trace. That, not a 403, is what you attach to a provider issue — a 403 is the API telling you something is missing, while a crash is the provider itself falling over. Distinguishing a propagation flake (the re-run passes) from a real crash (the re-run still crashes) tells you whether to add a time_sleep or file a bug, and saves everyone the noise of "Terraform is broken" reports that were really IAM timing.
depends_on — orders one resource strictly after another and waits only for the upstream API call to return. Use it for ordering: make the dependent run after the grant or the API enablement, never before.
time_sleep — inserts a fixed wall-clock delay to cover propagation that completes after the API returns. Add it only when ordering is already correct but the grant or API enablement still has not propagated by the time the dependent runs.
- Granting an IAM role and creating the resource that needs it in the same apply with no ordering, then seeing it fail on a fresh apply and pass on re-run — the classic propagation race read as a flaky tool instead of async IAM.
- Creating a resource immediately after
google_project_serviceenables its API and hittingSERVICE_DISABLED, because enablement had not propagated when the next resource fired. - Sprinkling long
time_sleepblocks everywhere to "fix flakiness" instead of first adding the correctdepends_on, which masks ordering bugs and slows every apply. - Debugging blind on a 403 without
TF_LOG, guessing at the cause when the DEBUG log names the exact API and the missing permission. - Filing a "Terraform is broken" issue for what is really propagation timing, with no
crash.logand no DEBUG output, when adepends_onortime_sleepwas the fix.
- Set
TF_LOG=DEBUGwithTF_LOG_PATHthe moment an error is non-obvious, and read the raw Google API request and response before changing any HCL. - Order IAM grants and API enablement before their dependents with
depends_on, and reservetime_sleepfor the residual propagation gap ordering cannot close. - Treat "fails on fresh apply, passes on re-run" as the diagnostic signature of GCP eventual consistency, not a flaky tool.
- Keep
time_sleepdurations as short as testing allows, since every second is paid on every apply. - Attach
crash.logplus DEBUG output to any genuine provider bug report, and confirm a re-run still crashes before filing.
time_sleep resource
gcloud --log-http · Cloud Audit Logs raw API debugging and grant confirmation
Knowledge Check
What do TF_LOG and TF_LOG_PATH expose?
- Every HTTP request and response between the provider and the Google APIs, written to a file, turning "Error 403" into the exact API and message
- A formatted summary of which resources will be added, changed, or destroyed, with the per-attribute diff and the resource-count totals the plan output already prints to the console at the end of a run
- The full contents of the remote state file dumped out in plaintext to the path
- A complete inventory of every provider plugin version installed locally on the machine
When is time_sleep the right fix rather than depends_on?
- When ordering is already correct but the grant or API enablement still has not propagated by the time the dependent runs
- Whenever any apply fails for any reason, dropped in as a blanket generic retry mechanism that pauses and re-attempts the failed resource a fixed number of times until it eventually goes through
- When you simply need to order one resource strictly after another in the graph
- Only when the provider has actually crashed and written a Go stack trace
Why does granting an IAM role and creating the resource that needs it in the same apply fail intermittently?
- The grant is asynchronous and has not propagated when the dependent fires, so a fresh apply fails and a re-run passes
- Terraform shuffles the resources into a fresh random order on every single run, so whether the grant happens to land before the dependent resource is pure luck from one apply to the next
- IAM grants can only ever be applied from a separate, dedicated root module
- The provider rate-limits IAM calls and silently drops roughly every other one
How do you tell a propagation flake from a real provider crash?
- A propagation flake passes on re-run; a real crash writes a
crash.logstack trace and still crashes on re-run - A flake always surfaces as a 500 error while a crash always surfaces as a 403
- There is no reliable way to distinguish the two from the local output without first filing a provider bug report and waiting for a maintainer to confirm which category your failure falls into
- A crash only ever happens on the very first apply and never recurs afterward
You got correct