Quick take — A reusable hashicorp/google ~> 5.0 module for google_project_service: for_each over a set of API names with disable_on_destroy=false as the safe default, dependency-ordered enablement, and optional google_project_service_identity service agents. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.
Quickstart (copy-paste)
Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):
provider "google" {
project = "my-project"
region = "us-central1"
}
module "project_services" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-project-service?ref=v1.0.0"
project_id = "..." # Project whose APIs you are enabling.
services = [ # Fully-qualified API names to enable.
"compute.googleapis.com",
"iam.googleapis.com",
"...",
]
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
What this module is
Before any GCP resource can be created in a project, the API that backs it must be enabled. You cannot create a google_compute_instance until compute.googleapis.com is on; you cannot create a Cloud SQL instance until sqladmin.googleapis.com is on. The google_project_service resource manages exactly one such API enablement, and a real project needs a dozen or more. The naive approach — a long list of near-identical resource blocks — is verbose and easy to get subtly wrong, and two of its arguments are genuine footguns:
disable_on_destroydefaults totrue. That means aterraform destroy(or removing a service from the list) will disable the API on the project. Disabling a live API is far more dangerous than it sounds: it can break resources you did not intend to touch — including resources managed by other Terraform stacks, other teams, or click-ops — that depend on that API. In a long-lived project where infrastructure churns but the project itself persists, disabling APIs on destroy is almost never what you want.disable_dependent_servicescontrols blast radius. If you do disable an API, GCP will refuse if other enabled services depend on it — unlessdisable_dependent_services = true, which cascades and disables them too. Left at its default offalse, you at least get an error instead of a silent cascade, but combined withdisable_on_destroy = trueit is a recipe for surprises.
This module drives google_project_service with for_each over a set(string) of API names, so enabling a new API is a one-line addition to a list rather than a new resource block. Critically, it makes disable_on_destroy = false the default — the safe posture for shared, long-lived projects — while still letting you opt into teardown behaviour for ephemeral sandboxes. It also exposes optional google_project_service_identity resources to provision the Google-managed service agents some APIs need before you can grant them IAM roles (for example, the Cloud KMS, Pub/Sub, or Artifact Registry service agents used in cross-service flows).
Centralizing API enablement also fixes a sequencing problem: resources must not be created before the APIs they need exist. By making downstream stacks depends_on this module’s output, you get a clean, deterministic ordering — APIs first, then everything that uses them.
When to use it
- You are bootstrapping a project (landing zone, app project, data project) and need a consistent, reviewed list of APIs enabled before any workload resources are created.
- You run many projects and want a single paved-road module so every project enables the same baseline APIs the same way, with the same safe
disable_on_destroy = falseposture. - You need service agents (Google-managed service accounts for KMS, Pub/Sub, Cloud Build, Artifact Registry, etc.) created and their emails exported so you can grant them IAM roles in cross-service flows.
- You want to guarantee ordering — APIs enabled before the resources that depend on them — by having workload stacks
depends_onthis module rather than racing API enablement against resource creation.
Use this before and alongside your resource modules; it deliberately owns API enablement and (optionally) service-agent identities, not the workload resources themselves. For sweeping org-wide API governance across hundreds of projects, layer this under a project-factory pattern rather than calling it once per project by hand.
Module structure
terraform-module-gcp-project-service/
├── versions.tf
├── main.tf
├── variables.tf
└── outputs.tf
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
# google_project_service_identity is a beta resource and must be created
# with the google-beta provider. It is only used when create_service_identities
# is non-empty; pass `providers = { google-beta = google-beta }` from the root.
google-beta = {
source = "hashicorp/google-beta"
version = "~> 5.0"
}
}
}
main.tf
resource "google_project_service" "this" {
for_each = var.services
project = var.project_id
service = each.value
# Safe default for long-lived projects: do NOT disable the API when this
# resource is destroyed or removed from the set. Disabling a live API can
# break resources owned by other stacks/teams that depend on it.
disable_on_destroy = var.disable_on_destroy
# Only relevant when disable_on_destroy = true. When false, GCP errors if
# other enabled services depend on this one (instead of cascading a disable).
disable_dependent_services = var.disable_dependent_services
}
# Optional: generate Google-managed service agents (service identities) for the
# APIs that expose one. The resulting `email`/`member` are used to grant the
# agent IAM roles in cross-service flows (e.g. KMS CryptoKey access to Pub/Sub).
resource "google_project_service_identity" "this" {
provider = google-beta
for_each = var.create_service_identities
project = var.project_id
service = each.value
# Ensure the API itself is enabled before requesting its service identity.
depends_on = [google_project_service.this]
}
variables.tf
variable "project_id" {
type = string
description = "ID of the project whose APIs are being enabled."
validation {
condition = can(regex("^[a-z][a-z0-9-]{4,28}[a-z0-9]$", var.project_id))
error_message = "project_id must be a valid GCP project ID (6-30 chars, lowercase letters/digits/hyphens)."
}
}
variable "services" {
type = set(string)
description = "Set of fully-qualified API service names to enable (e.g. \"compute.googleapis.com\")."
validation {
condition = alltrue([
for s in var.services : can(regex("^[a-z0-9.-]+\\.googleapis\\.com$", s))
])
error_message = "Each service must be a fully-qualified API name ending in .googleapis.com (e.g. compute.googleapis.com)."
}
}
variable "disable_on_destroy" {
type = bool
description = "Whether to disable each API when the resource is destroyed. Keep false for shared/long-lived projects."
default = false
}
variable "disable_dependent_services" {
type = bool
description = "When disabling an API, also disable services that depend on it. Only relevant if disable_on_destroy is true."
default = false
}
variable "create_service_identities" {
type = set(string)
description = "Set of API service names for which to generate a Google-managed service agent (service identity). Uses the google-beta provider."
default = []
validation {
condition = alltrue([
for s in var.create_service_identities : can(regex("^[a-z0-9.-]+\\.googleapis\\.com$", s))
])
error_message = "Each service identity entry must be a fully-qualified API name ending in .googleapis.com."
}
}
outputs.tf
output "enabled_services" {
description = "Sorted list of API service names enabled by this module."
value = sort([for s in google_project_service.this : s.service])
}
output "service_ids" {
description = "Map of service name => resource ID (format \"<project>/<service>\")."
value = { for k, s in google_project_service.this : k => s.id }
}
output "service_identity_emails" {
description = "Map of service name => generated service-agent email, for the APIs in create_service_identities."
value = { for k, si in google_project_service_identity.this : k => si.email }
}
output "service_identity_members" {
description = "Map of service name => IAM member string (\"serviceAccount:<email>\") for granting roles to the agent."
value = { for k, si in google_project_service_identity.this : k => si.member }
}
How to use it
module "project_services" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-project-service?ref=v1.0.0"
# The beta provider is required for the optional service identities.
providers = {
google = google
google-beta = google-beta
}
project_id = "kv-prod-data-7b21"
services = [
"compute.googleapis.com",
"iam.googleapis.com",
"cloudkms.googleapis.com",
"pubsub.googleapis.com",
"sqladmin.googleapis.com",
"artifactregistry.googleapis.com",
"logging.googleapis.com",
"monitoring.googleapis.com",
]
# Keep APIs enabled even if this stack is destroyed (shared, long-lived project).
disable_on_destroy = false
# Provision the Pub/Sub and KMS service agents so we can grant them roles.
create_service_identities = [
"pubsub.googleapis.com",
"cloudkms.googleapis.com",
]
}
# Downstream: every workload resource depends on the APIs being enabled first.
resource "google_compute_network" "app" {
project = "kv-prod-data-7b21"
name = "prod-data-vpc"
auto_create_subnetworks = false
depends_on = [module.project_services]
}
# Grant the Pub/Sub service agent permission to use a KMS key for CMEK topics.
resource "google_kms_crypto_key_iam_member" "pubsub_cmek" {
crypto_key_id = google_kms_crypto_key.topics.id
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
member = module.project_services.service_identity_members["pubsub.googleapis.com"]
}
Why
depends_onmatters. Terraform parallelizes aggressively; without an explicit dependency it may try to create a resource before its API is enabled, producing a confusing “API not enabled” error on the first apply (and success on a retry). Making workload modulesdepends_on = [module.project_services]removes that race entirely.
With Terragrunt
Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.
1. Root config — live/terragrunt.hcl (inherited by every module):
remote_state {
backend = "gcs"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
# ...gcs state bucket/container + key per path...
}
}
2. Module config — live/prod/project_services/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-project-service?ref=v1.0.0"
}
inputs = {
project_id = "..."
services = ["...", "..."]
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/project_services && terragrunt apply # this module
terragrunt run-all apply # every module under live/prod
Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. With run-all, declare a dependency on this module so APIs are enabled before workload stacks apply. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
project_id |
string |
— | Yes | ID of the project whose APIs are being enabled. |
services |
set(string) |
— | Yes | Fully-qualified API names to enable (e.g. compute.googleapis.com). |
disable_on_destroy |
bool |
false |
No | Disable each API on destroy; keep false for shared/long-lived projects. |
disable_dependent_services |
bool |
false |
No | Cascade-disable dependent services (only relevant when disable_on_destroy is true). |
create_service_identities |
set(string) |
[] |
No | APIs for which to generate a service agent (uses the google-beta provider). |
Outputs
| Name | Description |
|---|---|
enabled_services |
Sorted list of API service names enabled by this module. |
service_ids |
Map of service name → resource ID (<project>/<service>). |
service_identity_emails |
Map of service name → generated service-agent email. |
service_identity_members |
Map of service name → IAM member string for granting roles. |
Enterprise scenario
A data platform team manages roughly forty long-lived GCP projects — one per data domain — across dev, staging, and prod. They publish this module at v1.0.0 and call it once per project with a curated baseline of APIs (compute, iam, cloudkms, pubsub, bigquery, artifactregistry, logging, monitoring). Because the module defaults disable_on_destroy = false, tearing down or refactoring a workload stack never disables an API out from under another team that shares the project — a failure mode they hit hard once before standardizing. For CMEK-encrypted Pub/Sub and BigQuery, they list those APIs in create_service_identities, and the module’s service_identity_members output feeds straight into KMS IAM bindings so the Google-managed agents can use the encryption keys. Every workload module in the org carries depends_on = [module.project_services], so a fresh project bootstraps deterministically: APIs first, service agents next, then workloads — no “API not enabled” flakes on the first apply.
Best practices
- Keep
disable_on_destroy = falsefor shared, long-lived projects. Disabling an API on destroy can break resources owned by other stacks, teams, or click-ops in the same project. Only set ittruefor genuinely ephemeral sandbox projects you intend to tear down whole. - Leave
disable_dependent_services = falseunless you fully understand the cascade. It only matters when you are disabling APIs, and a cascade can silently disable a chain of services. Prefer an explicit error you can react to over a silent teardown. - Order enablement before consumption. Make workload resources/modules
depends_onthis module (or declare a Terragruntdependency) so Terraform never races resource creation against API enablement — the classic “API not enabled, then succeeds on retry” flake. - Use a curated baseline set, and add APIs as one-line list edits. Driving
for_eachover aset(string)makes enabling a new API a reviewed single-line diff. Keep the baseline list in one place so every project enables the same APIs the same way. - Generate service identities only where you need them. Provision a service agent via
create_service_identitiesfor the cross-service flows that require granting the Google-managed agent IAM roles (KMS, Pub/Sub, Cloud Build, Artifact Registry). Remember this is a beta resource using the google-beta provider, and once created it cannot be updated or destroyed. - Pin the module and the provider versions. Use
?ref=<tag>so the set of enabled APIs for a project never changes from an unreviewed module bump, and keepgoogle/google-betapinned to~> 5.0so beta-resource behaviour stays stable.
Part of the KloudVin Terraform module library. Run this first in every project, then wire the VPC Network, GKE, and Cloud SQL modules with depends_on = [module.project_services] so their APIs are guaranteed to be enabled.