IaC GCP

Terraform Module: GCP Project Service — Enable APIs Predictably, Without Tearing Them Down on Destroy

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:

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

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_on matters. 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 modules depends_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 configlive/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 configlive/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


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.

TerraformGCPProject ServiceModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading