Enabling APIs
Most GCP APIs are disabled by default in a new project — only a small base set is on out of the box — and Terraform cannot create a resource whose underlying API is off. That makes google_project_service one of the first resources in almost every GCP configuration — you enable compute.googleapis.com before you create an instance, run.googleapis.com before a Cloud Run service, secretmanager.googleapis.com before a secret.
It is also a footgun on three fronts. API enablement is eventually consistent, so a resource created in the same apply can race ahead of the API and fail. A misconfigured disable_on_destroy can tear down an API other configurations still depend on. And enabling APIs by hand instead of in code leaves drift the next plan tries to undo. Each of those has a clean fix, and this topic walks through them.
Why APIs Are Off by Default
A fresh GCP project starts with almost nothing enabled. This is deliberate — it keeps the attack surface and the billable surface of a new project minimal — but it means every service you intend to use must be turned on first. Compute Engine needs compute.googleapis.com, Cloud Run needs run.googleapis.com, Secret Manager needs secretmanager.googleapis.com, and so on for every API in the catalog.
For the Hatch pipeline in hatch-pipeline-dev, that is a handful of services: Compute, Cloud Run, Pub/Sub, BigQuery, Secret Manager, and Cloud Storage. Each must be enabled before the resources that depend on it exist, which is why API enablement is the bootstrap step that comes before everything else in the configuration.
The Resource
google_project_service enables one API on one project. Rather than write a separate block per API, the common pattern is a single resource with for_each over a set of service strings, which keeps the enabled-APIs list in one readable place and creates one resource instance per service.
resource "google_project_service" "apis" { for_each = toset([ "compute.googleapis.com", "run.googleapis.com", "pubsub.googleapis.com", "bigquery.googleapis.com", "secretmanager.googleapis.com", ]) project = "hatch-pipeline-dev" service = each.value disable_on_destroy = false }
Each string in the set becomes one enabled API, addressable as google_project_service.apis["run.googleapis.com"]. Adding a service to the list is a one-line diff, and the disable_on_destroy = false here is the safe default explained two sections down.
The Propagation Race
Enabling an API is not instant. The enablement propagates asynchronously across Google's backend, so a resource created in the same apply, microseconds after the service resource reports success, can still fail with API [run.googleapis.com] not enabled — the classic "API has not been used in project before or is disabled" error. The service was enabled; it just had not propagated yet.
The fix is to make the dependency explicit. A depends_on from the resource to its google_project_service forces Terraform to wait for enablement to report done before creating the resource. Where that is still not enough — the race is timing-dependent — a brief time_sleep between the service and the resource absorbs the propagation lag.
resource "time_sleep" "wait_for_apis" { depends_on = [google_project_service.apis] create_duration = "30s" } resource "google_cloud_run_v2_service" "processor" { name = "event-processor" location = "us-central1" depends_on = [time_sleep.wait_for_apis] # wait out the propagation lag # ... template ... }
The event-processor service now waits for the API set to enable and for a 30-second buffer before it is created, so the run API has time to propagate. Reach for the time_sleep only where the race actually appears — a plain depends_on handles most cases on its own.
disable_on_destroy
disable_on_destroy controls whether destroying the google_project_service resource also disables the API project-wide. Its default changed across provider majors — it was true through 6.x and is no longer true in 7.x — so do not rely on the default; set the flag explicitly. The behavior it guards against is real whenever the flag is true: in a single-stack throwaway project a destroy disabling the API is harmless, but in a shared project — where multiple Terraform configurations live in the same project — it is an outage: tear down one stack and you disable an API that the other stacks, and possibly the whole org, still need.
Set disable_on_destroy = false in any project shared across stacks. A destroy then removes the API from that configuration's state without actually turning the service off, so other configurations keep working. This single flag is the difference between a clean teardown and a cross-stack incident.
disable_dependent_services
A second flag, disable_dependent_services, controls cascade behavior. Some APIs depend on others, and disabling one can require disabling its dependents too. Left at its default the provider refuses a disable that would orphan dependent services; set the flag and it cascades the disable down the dependency tree.
Understand the flag before enabling it. Turning on disable_dependent_services so a stubborn destroy succeeds can disable more than you bargained for — every service that depended on the one you removed. Combined with disable_on_destroy = false in shared projects, you mostly avoid touching this at all.
Single-stack project — one configuration owns the project, so disable_on_destroy = true is harmless: a destroy disables APIs nothing else uses.
Shared project — multiple configurations live in one project. With disable_on_destroy = true, a destroy of one stack disables an API the others depend on, taking them down. Set disable_on_destroy = false so a teardown only drops the API from state, never from the project.
- Creating a resource before its API is enabled in the same apply and hitting "API has not been used in project before or is disabled" — order the resource after the service with
depends_on. - Assuming enablement is instant and skipping the wait when the very next resource needs the API — the propagation race fails the apply intermittently, which is maddening to debug.
- Setting
disable_on_destroy = true(or relying on the pre-7.x default) in a shared project, then destroying one stack and disabling an API the whole org still needs. - Enabling APIs by hand with
gcloud services enableinstead of in Terraform — the enablement is drift the next plan may try to "correct" by re-enabling or fighting it. - Flipping
disable_dependent_serviceson to force a stubborn destroy through, then disabling a cascade of services other workloads depended on.
- Manage API enablement in Terraform with
google_project_service, typically a single resource withfor_eachover the list of services. - Set
disable_on_destroy = falsein any project shared across stacks so a destroy never disables an API others use. - Add a
depends_onfrom each resource to its API service, and a shorttime_sleeponly where a propagation race actually appears. - Keep the enabled-APIs list close to the resources that need them so the dependency stays obvious to the next reader.
- Leave
disable_dependent_servicesat its default unless you have read exactly which services would cascade.
Knowledge Check
Why does google_project_service appear near the top of almost every GCP configuration?
- Most GCP APIs are disabled by default in a new project, and Terraform cannot create a resource whose API is off
- It configures the provider's default project and region for every resource in the file
- It is required to authenticate Terraform to GCP before any resource can be created
- It links the active billing account to the project so that any metered API usage can be charged, attaching the account before the first paid call runs
A resource fails with "API not enabled" even though the service resource succeeded in the same apply. What is happening and how do you fix it?
- Enablement propagates asynchronously; add
depends_onon the service and, where needed, a shorttime_sleep - The API needs a manual one-time approval click in the Cloud Console before Terraform can call it
- The provider version is too old to see the API; upgrade it and the enablement will take
- The resource sits in the wrong region for that service; move the whole stack to
us-central1, the one region where every API is reachable
Why set disable_on_destroy = false in a project shared across multiple Terraform stacks?
- So destroying one stack does not disable an API that the other stacks still depend on
- So the destroy completes faster by skipping the disable API call for that service
- So the API can never be turned back on again without manual Console intervention
- So Terraform stops tracking that service resource in state entirely
What is the risk of enabling APIs by hand with gcloud instead of in Terraform?
- The manual enablement is drift, and the next plan may try to reconcile or fight it
- gcloud cannot enable any APIs at all, so the command silently does nothing
- It permanently locks the API so Terraform can never import or manage it again
- It doubles the per-request cost of every future API call to that service, because manually enabled APIs are billed at a higher tier than ones turned on through Terraform
You got correct