Home / Networking

Shared VPC on GCP with Terraform — Host Project, Service Projects & IAM Done Right | 2026

Shared VPC on GCP with Terraform — Host Project, Service Projects & IAM Done Right | 2026

Shared VPC allows multiple service projects to share a single Virtual Private Cloud network hosted in a dedicated host project. Using Terraform, you can automate host project activation, service project attachment, subnet delegation, and least-privilege IAM so teams get centralized network governance without losing project autonomy.

By Mateusz Chmielewski · Jul 27, 2026 · 16 min read

What Is Shared VPC?

Shared VPC is a Google Cloud networking feature that lets an organization connect resources from multiple projects to a common Virtual Private Cloud (VPC) network. The VPC lives in a host project; other projects, called service projects, attach to it and use delegated subnets.

Think of the host project as a building owner who maintains the electrical and plumbing backbone. Service projects are tenants who lease specific floors (subnets). The owner controls the building infrastructure, while tenants control their own furniture and equipment (compute resources).

ConceptExplanationWhen to use
Host ProjectThe project that owns the Shared VPC network and subnets.Create one per environment (dev, staging, prod) to isolate network governance and billing.
Service ProjectA project that attaches to the host project's VPC and consumes delegated subnets.Use for application teams, workloads, or business units that need network connectivity without managing the VPC.
Subnet DelegationThe process of granting a service project permission to use specific subnets inside the host VPC.When you want a service project to create VMs, GKE clusters, or load balancers in a defined IP range.
Host Project AdminA role that can manage the Shared VPC host project and attach service projects.Assign to the platform or network team that owns the central VPC.
Network UserA role that lets a service project use a delegated subnet or Shared VPC network.Grant to application project service accounts or groups at the subnet level for least privilege.
Service Project AdminA role that lets administrators manage service project attachments.Assign to application team leads who need to attach their project but not manage the host VPC.

Why Use Shared VPC?

In multi-project GCP organizations, every team creates its own VPC. This leads to overlapping RFC 1918 ranges, complex peering meshes, duplicated NAT/firewall costs, and inconsistent security policies.

Shared VPC centralizes network management in a host project while letting service projects own their compute resources and billing. Teams share a single, non-overlapping IP plan, centralized firewall rules, and Cloud NAT. Combine with [VPC Service Controls](/tutorial/terraform-vpc-service-controls-custom-module-guide), [Static Egress Gateways](/tutorial/cloud-run-gke-static-egress-ip-terraform-serverless-vpc-access-nat), and [GKE Spot VMs](/tutorial/spot-vms-preemptible-gke-cost-optimization).

FeaturethisServicealtAaltB
IP Address ManagementCentralized in host projectPer-project VPCsVPC Peering
Firewall & RoutingCentralized and consistentFragmented across projectsRequires peering route exchange
Billing GranularityService projects keep their own billingProject-level billingHost project pays for egress
Operational OverheadLow (single network to manage)High (many networks)Medium (peering relationships)

Prerequisites

  • GCP Organization with an established resource hierarchy (organization or folder)
  • A billing account attached to all projects
  • Terraform CLI v1.5.0+ and a configured remote backend (GCS recommended)
  • IAM permission to enable Shared VPC: `roles/compute.xpnAdmin` on the organization or folder
  • Owner or Editor role on the host project and service projects, or equivalent custom roles
  • A dedicated GCP project to act as the Shared VPC host project

Step-by-Step Guide

Step 1: Bootstrap the Terraform Backend and Provider

Configuring remote state storage, state locking, and a constrained provider so the Shared VPC foundation is safe to operate in a team. Local state files cause conflicts and risk leaking sensitive values. A GCS backend with versioning and locking is the baseline for production Terraform.

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }

  backend "gcs" {
    bucket = "my-org-tfstate"
    prefix = "shared-vpc/prod"
  }
}

provider "google" {
  project = var.host_project_id
  region  = var.region
}

provider "google" {
  alias   = "service"
  project = var.service_project_id
  region  = var.region
}

Step 2: Define Variables and Validation

Declaring typed variables with validation rules for project IDs, regions, and CIDR ranges. Validation catches misconfiguration at plan time, before GCP API calls fail or create invalid resources.

variable "host_project_id" {
  description = "Project ID that will own the Shared VPC"
  type        = string
}

variable "service_project_ids" {
  description = "List of service project IDs to attach to the Shared VPC"
  type        = list(string)
}

variable "region" {
  description = "Primary region for subnets"
  type        = string
  default     = "europe-west1"
}

variable "subnets" {
  description = "Map of subnet configurations keyed by name"
  type = map(object({
    region        = string
    ip_cidr_range = string
    service_projects = list(string)
  }))
}

variable "enable_flow_logs" {
  description = "Enable VPC flow logs on all subnets"
  type        = bool
  default     = true
}

variable "environment" {
  description = "Environment label, e.g. dev, staging, prod"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

Step 3: Create the Host VPC and Subnets

Building the central VPC, subnets, and a default set of firewall rules in the host project. The host project is the single source of truth for the network topology. Keeping subnets here prevents overlapping ranges and inconsistent routing.

locals {
  common_labels = {
    managed_by = "terraform"
    env        = var.environment
  }
}

resource "google_compute_network" "shared_vpc" {
  name                    = "${var.environment}-shared-vpc"
  auto_create_subnetworks = false
  routing_mode            = "GLOBAL"
  project                 = var.host_project_id
  delete_default_routes_on_create = false
}

resource "google_compute_subnetwork" "delegated" {
  for_each = var.subnets

  name          = each.key
  region        = each.value.region
  network       = google_compute_network.shared_vpc.id
  ip_cidr_range = each.value.ip_cidr_range
  project       = var.host_project_id

  private_ip_google_access = true

  log_config {
    aggregation_interval = "INTERVAL_5_SEC"
    flow_sampling        = 0.5
    metadata             = "INCLUDE_ALL_METADATA"
  }

  labels = local.common_labels
}

resource "google_compute_firewall" "allow_internal" {
  name        = "${var.environment}-allow-internal"
  network     = google_compute_network.shared_vpc.id
  project     = var.host_project_id
  description = "Allow all RFC1918 internal traffic across Shared VPC"

  allow {
    protocol = "tcp"
    ports    = ["0-65535"]
  }

  allow {
    protocol = "udp"
    ports    = ["0-65535"]
  }

  allow {
    protocol = "icmp"
  }

  source_ranges = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
}

Step 4: Enable the Host Project and Attach Service Projects

Activating Shared VPC on the host project and attaching each service project to it. Without explicit attachment, service projects cannot consume host subnets. This step links the host network to application projects.

resource "google_compute_shared_vpc_host_project" "host" {
  project = var.host_project_id
}

resource "google_compute_shared_vpc_service_project" "service" {
  for_each = toset(var.service_project_ids)

  host_project    = google_compute_shared_vpc_host_project.host.project
  service_project = each.value
}

# Wait for host/attachment propagation before delegating subnets
resource "time_sleep" "wait_for_xpn" {
  depends_on      = [google_compute_shared_vpc_service_project.service]
  create_duration = "30s"
}

Step 5: Delegate Subnets to Service Projects

Granting each service project permission to use the specific subnets it needs, rather than the entire network. Subnet-level delegation is the foundation of least privilege in Shared VPC. It prevents a service project from consuming IP space assigned to another team.

locals {
  subnet_delegations = merge([
    for subnet_name, subnet in var.subnets : {
      for project_id in subnet.service_projects : "${subnet_name}-${project_id}" => {
        subnet_name = subnet_name
        project_id  = project_id
      }
    }
  ]...)
}

resource "google_compute_subnetwork_iam_member" "network_user" {
  for_each = local.subnet_delegations

  project    = var.host_project_id
  region     = var.subnets[each.value.subnet_name].region
  subnetwork = google_compute_subnetwork.delegated[each.value.subnet_name].name
  role       = "roles/compute.networkUser"
  member     = "serviceAccount:${each.value.project_id}@cloudservices.gserviceaccount.com"
}

# Also grant the default Compute Engine service account for legacy workflows
resource "google_compute_subnetwork_iam_member" "network_user_legacy" {
  for_each = local.subnet_delegations

  project    = var.host_project_id
  region     = var.subnets[each.value.subnet_name].region
  subnetwork = google_compute_subnetwork.delegated[each.value.subnet_name].name
  role       = "roles/compute.networkUser"
  member     = "serviceAccount:${each.value.project_id}@compute-system.iam.gserviceaccount.com"
}

Step 6: Assign Host Project and Organization IAM

Granting the minimum set of roles needed to manage the host project and enable Shared VPC attachments. Overly broad IAM at the organization level violates least privilege. Separate network administration from service project administration.

locals {
  host_project_admins = [
    "group:[email protected]",
    "serviceAccount:[email protected]",
  ]

  service_project_admins = [
    "group:[email protected]",
  ]
}

# Organization or folder level: required to enable/disable Shared VPC
resource "google_organization_iam_member" "xpn_admin" {
  for_each = toset(local.host_project_admins)

  org_id = var.organization_id
  role   = "roles/compute.xpnAdmin"
  member = each.value
}

# Host project level: manage VPC, subnets, and firewall rules
resource "google_project_iam_member" "host_network_admin" {
  for_each = toset(local.host_project_admins)

  project = var.host_project_id
  role    = "roles/compute.networkAdmin"
  member  = each.value
}

# Host project level: ability to attach service projects
resource "google_project_iam_member" "host_service_agent" {
  for_each = toset(local.host_project_admins)

  project = var.host_project_id
  role    = "roles/compute.securityAdmin"
  member  = each.value
}

# Service project level: administrators can manage attachments but not the host network
resource "google_project_iam_member" "service_project_admin" {
  for_each = {
    for pair in setproduct(local.service_project_admins, var.service_project_ids) : "${pair[0]}-${pair[1]}" => {
      member  = pair[0]
      project = pair[1]
    }
  }

  project = each.value.project
  role    = "roles/compute.networkUser"
  member  = each.value.member
}

Step 7: Consume the Shared VPC from a Service Project

Creating a Compute Engine VM in a service project that uses a delegated subnet from the host VPC. This proves the entire chain works: host project enabled, service project attached, subnet delegated, and IAM bound.

resource "google_compute_instance" "vm" {
  provider     = google.service
  name         = "shared-vpc-test-vm"
  machine_type = "e2-medium"
  zone         = "${var.region}-b"
  project      = var.service_project_ids[0]

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-12"
    }
  }

  network_interface {
    subnetwork = "projects/${var.host_project_id}/regions/${var.region}/subnets/app-subnet"
  }

  metadata = {
    enable-oslogin = "TRUE"
  }

  labels = {
    managed_by = "terraform"
    env        = var.environment
  }
}

Verification & Connectivity Tests

Best Practices

  • One Host Project Per Environment
  • Delegate Subnets, Not Networks
  • Use Google Groups for Human Access
  • Tag Resources Consistently
  • Enable VPC Flow Logs
  • Store State Remotely with Locking
  • Validate Variables at Plan Time

Common Mistakes

  • {"errorCode":"PERMISSION_DENIED","symptoms":"Terraform apply fails when enabling the host project or attaching service projects.","rootCause":"The runner lacks `roles/compute.xpnAdmin` at the organization or folder level.","fixCommand":"gcloud organizations add-iam-policy-binding ORG_ID --member=serviceAccount:TERRAFORM_SA --role=roles/compute.xpnAdmin","code":"resource \"google_organization_iam_member\" \"xpn_admin\" {\n org_id = var.organization_id\n role = \"roles/compute.xpnAdmin\"\n member = \"serviceAccount:[email protected]\"\n}\n","language":"hcl","filename":"iam.tf","prevention":"Grant `roles/compute.xpnAdmin` to the Terraform service account at the folder or organization level, not just on the host project."}
  • {"errorCode":"HOST_PROJECT_NOT_FOUND","symptoms":"Service project attachment fails with the host project ID not recognized.","rootCause":"The host project is not yet enabled as a Shared VPC host, or the service project is in a different organization.","fixCommand":"gcloud compute shared-vpc enable-host-project HOST_PROJECT_ID","code":"resource \"google_compute_shared_vpc_host_project\" \"host\" {\n project = var.host_project_id\n}\n\nresource \"google_compute_shared_vpc_service_project\" \"service\" {\n for_each = toset(var.service_project_ids)\n\n host_project = google_compute_shared_vpc_host_project.host.project\n service_project = each.value\n}\n","language":"hcl","filename":"shared-vpc.tf","prevention":"Let Terraform manage `google_compute_shared_vpc_host_project` and reference it from `google_compute_shared_vpc_service_project` to enforce ordering."}
  • {"errorCode":"SUBNET_NOT_DELEGATED","symptoms":"VM creation in a service project fails because the subnet cannot be used.","rootCause":"The service project does not have `roles/compute.networkUser` on the specific subnet.","fixCommand":"gcloud compute networks subnets get-iam-policy SUBNET --region=REGION --project=HOST_PROJECT_ID","code":"resource \"google_compute_subnetwork_iam_member\" \"network_user\" {\n for_each = local.subnet_delegations\n\n project = var.host_project_id\n region = var.subnets[each.value.subnet_name].region\n subnetwork = google_compute_subnetwork.delegated[each.value.subnet_name].name\n role = \"roles/compute.networkUser\"\n member = \"serviceAccount:${each.value.project_id}@cloudservices.gserviceaccount.com\"\n}\n","language":"hcl","filename":"iam.tf","prevention":"Bind `roles/compute.networkUser` at the subnet level for both the cloudservices and compute-system service accounts of each service project."}
  • {"errorCode":"OVERLAPPING_SUBNET_RANGES","symptoms":"Terraform apply fails with a CIDR overlap error or on-premises VPN routes conflict.","rootCause":"Subnet CIDRs were chosen without an organization-wide IP address management plan.","fixCommand":"Audit all subnet ranges with `gcloud compute networks subnets list --project=HOST_PROJECT_ID`.","code":"variable \"subnets\" {\n type = map(object({\n region = string\n ip_cidr_range = string\n service_projects = list(string)\n }))\n\n validation {\n condition = length(distinct(values(var.subnets)[*].ip_cidr_range)) == length(values(var.subnets))\n error_message = \"All subnet CIDR ranges must be unique.\"\n }\n}\n","language":"hcl","filename":"variables.tf","prevention":"Maintain a central IPAM spreadsheet or tool and reserve non-overlapping RFC1918 space per region and environment before creating subnets."}
  • {"errorCode":"IAM_PROPAGATION_DELAY","symptoms":"Subnet IAM binding succeeds, but a VM still cannot be created immediately.","rootCause":"IAM changes can take 30-60 seconds to propagate across projects.","fixCommand":"Wait briefly and retry `terraform apply`.","code":"resource \"time_sleep\" \"wait_for_xpn\" {\n depends_on = [google_compute_subnetwork_iam_member.network_user]\n create_duration = \"60s\"\n}\n\nresource \"google_compute_instance\" \"vm\" {\n depends_on = [time_sleep.wait_for_xpn]\n # ...\n}\n","language":"hcl","filename":"compute.tf","prevention":"Add a `time_sleep` resource that depends on IAM bindings before creating dependent resources, or rerun the apply."}

Cost Analysis

Featuremetriccost1kcost10kcost100kcost1m
Shared VPC Feature$0.00$0.00$0.00$0.00
VPC Flow Logs~$3.00~$25.00~$220.00~$1,900.00
GCS Backend Storage~$0.10~$0.10~$0.50~$2.00

References

Browse all tutorials