Home› Blog› Week 4 — Shared VPC on Google Cloud: The Identity …
GCP Weekly Lab GCP Terraform

Shared VPC hub and spoke

The first week anything actually runs — one network owned centrally, one workload project borrowing a subnet, and a firewall policy that has to be attached as well as created.

Verified against current vendor documentation on 12 September 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.
GCP Platform Engineering Lab · Week 4 of 58

Lab Overview

The problem. Three weeks of hierarchy and policy, and nothing was running. A workload needs a network, and the obvious approach — a VPC per project — multiplies the number of places someone has to get firewall rules right, and makes private connectivity between projects a routing problem you have to solve yourself.

Objective. Put one network in a central host project and let a workload project attach to it, so the workload gets private addressing without owning any network configuration. Then filter it with a firewall policy applied at the folder, above the project.

Expected outcome. A virtual machine in one project drawing its IP address from a subnet in another, reachable only through Identity-Aware Proxy, with no external address at all — and a measured cost of effectively zero.

DifficultyAdvanced — five roles, two of which are not obvious, and a quota-project cascade
DeploymentTerraform via HCP Terraform remote execution, Workload Identity Federation
Reproduce itLab guide — roles, the free-tier shape, the quota cascade
Cost~$0 — and that depends on four choices, any one of which starts billing

Cloud Services and Concepts

ServicePurpose in this lab
Shared VPCLets a host project own a network that service projects attach to. The workload gets addressing without owning configuration.
Custom-mode VPCA network with no subnets until you create them, so every range is deliberate. Auto mode would create one subnet per region.
Cloud NGFW hierarchical policiesFirewall rules applied at a folder, above every project in it, so a project cannot opt out.
Identity-Aware ProxySSH to an instance with no public address, brokered by Google and governed by IAM.
Compute EngineOne e2-micro — the thing that proves the networking works.

The concept worth getting straight: host and service projects. In Shared VPC, one project owns the network and others borrow it. A virtual machine in a service project gets an address from a subnet that lives in the host project, and the service project's administrators cannot change that subnet, its routes or its firewall rules. Network configuration becomes a central concern and workload teams get addressing without it — which is the opposite trade from a VPC per project, where every team owns a firewall they probably have not thought about.

Architecture and Workflow

One network, owned centrally, borrowed by a workload host project — net-hub hub — custom-mode VPC dev-us-central1   10.20.0.0/24 prod-us-central1 10.20.1.0/24 (reserved) flow logs · Private Google Access attached service project — dev-app-01 dev-app-01 — e2-micro address 10.20.0.2 — from the HOST's subnet no external IP owns no network configuration hierarchical firewall policy — attached to the workloads FOLDER 1000 allow ingress 35.235.240.0/20 tcp:22 (Identity-Aware Proxy) 65000 deny ingress 0.0.0.0/0 all Applied above the project, so a project cannot opt out. Creating the policy and ATTACHING it are different permissions.

The flow

  1. Create a custom-mode VPC in the host project, with no subnets until each range is chosen deliberately.
  2. Create two regional subnets with flow logs and Private Google Access — one for dev, one reserved for prod.
  3. Enable the host project as a Shared VPC host and attach the workload project as a service project.
  4. Grant the service project's identities use of one subnet, not the whole network.
  5. Attach a hierarchical firewall policy to the workloads folder: allow IAP's range on port 22, deny everything else.
  6. Create one e2-micro with no external address, and confirm it draws its IP from the host project's subnet.

Two decisions worth naming

The firewall policy sits on the folder, not the project. A project-level rule can be changed by whoever administers the project. A hierarchical policy above it cannot be opted out of, which is the point of putting it there.

No external IP, deliberately. It is both a security choice and a cost one — an ephemeral public address has been billed per hour since 2020, so "no public IP" is simultaneously the hardened option and the free one.

Prerequisites

  • A host project with compute and oslogin enabled — Week 1 supplies one with auto_create_network = false.
  • A free billing slot for the spoke project.
  • Five roles, two of which are easy to miss: compute.xpnAdmin (organization-scoped, unavoidably), compute.networkAdmin, compute.instanceAdmin.v1, compute.orgSecurityPolicyAdmin to create a firewall policy and compute.orgSecurityResourceAdmin to attach it.
  • A quota project with cloudresourcemanager, serviceusage and cloudbilling enabled — needed because a folder-scoped firewall policy belongs to no project.
  • Terraform 1.9+ with hashicorp/google ~> 6.0.

Exact roles, the quota cascade and the free-tier conditions are in the lab guide.

Implementation

Fourteen resources. Commands are in the lab guide.

Repository structure: LAB.md, terraform, scripts and docs, each with a one-line responsibility.
How the week is organised. Full source on GitHub.
Google Cloud console showing the host project with no VPC networks.
The starting point, which cannot be recaptured: the host project with zero networks. Week 1 created it with auto_create_network = false, so the permissive default VPC never existed and nothing had to be deleted.

Step 1 — A custom-mode network, then subnets chosen deliberately

Why: auto mode creates a subnet in every region with ranges you did not choose. Custom mode starts empty, so each range is a decision.

The hub VPC with two regional subnets, flow logs and Private Google Access.
Validates Step 1: custom mode, two subnets, flow logs and Private Google Access on the dev range. The prod range is reserved and attached to nothing.

Step 2 — Make the host a host, and attach the spoke

Why: this one value is the whole of Shared VPC — a project is a host, and others may attach to it. Note that compute.xpnAdmin cannot be folder-scoped; host enablement is checked at the organization, so it is the widest grant the pipeline holds.

Shared VPC page showing the host project and the attached service project.
Validates Step 2: the spoke is attached. Subnet IAM then grants use of one subnet rather than the whole network.

Step 3 — Filter above the project, then run something

Why: a hierarchical policy on the folder cannot be opted out of by a project administrator. Watch for: creating the policy and attaching it are different permissions, and only the attachment filters anything.

The virtual machine showing an internal address from the host project's subnet and no external IP.
Validates the whole week: an instance in the service project holding 10.20.0.2 — an address from a subnet in a different project — with no external IP. The borrowing works.

Validation and Evidence

TestExpectedActualStatus
Infrastructure deployed14 resources14 added, 0 changed, 0 destroyedPass
Network is custom modenot autocustomPass
Host enabled and spoke attachedbothbothPass
Instance draws its address from another project's subnetan address in 10.20.0.0/2410.20.0.2Pass
Free-tier shapeall four conditionse2-micro, us-central1, pd-standard 10 GB, no external IPPass
Firewall policy attached, not merely createdassociation present on the folderattachedPass
Week 03's dry-run constraints now have something to evaluateviolations reportedpromoted to enforce on the evidencePass
SSH actually reaches the instance through IAPa session establishednot tested — the rule and the association were verified, a connection was not madeUnverified

Row four is the week. A virtual machine in one project holding an address from a subnet in another is Shared VPC working — everything else is scaffolding around that one fact.

Row eight is the gap. The IAP ingress rule exists and the policy is attached, which the console confirms; no SSH session was ever established through it. The path is configured and unexercised, and calling it "SSH works" would be a claim the evidence does not support.

Security and Cost

Security decisions

ControlDecision and trade-off
No public addressThe instance has no external IP at all, so it is not reachable from the internet by any route. Administrative access is brokered by IAP and governed by IAM.
Filtering above the projectA hierarchical policy on the folder cannot be opted out of by a project administrator — unlike a VPC firewall rule.
Default denyRule 65000 denies all ingress; rule 1000 allows only IAP's documented range on port 22.
Narrow subnet grantThe service project's identities may use one subnet, not the whole network.
Accepted over-grantcompute.xpnAdmin is organization-scoped and cannot be narrowed. It is the widest permission the pipeline holds, and it is required for host enablement.
Accepted gapNo egress control at all. Cloud NAT and egress policy arrive in Week 16.

Cost

~$0, and the shape is deliberate. Four conditions keep this inside Always Free, and changing any one of them silently starts billing:

ConditionWhy it matters
e2-microthe only Always Free machine type
us-central1one of three eligible regions
pd-standard, 10 GBbalanced or larger is billed
no external IPan ephemeral public address has been billed hourly since 2020

All four are asserted by validate.sh, because a cost property that is not checked is a cost property that drifts. Flow logs are the one to watch: they bill by volume, and they are free here only because a single idle instance generates almost no traffic.

Troubleshooting and Lessons

The firewall policy existed and filtered nothing

Problem: the policy and its rules were created successfully, and traffic was unaffected.

Root cause: roles/compute.orgSecurityPolicyAdmin carries firewallPolicies.create, update and use — but not organizations.setFirewallPolicy. The policy had never been associated with the folder.

Resolution: grant roles/compute.orgSecurityResourceAdmin on the folder, and assert the association in validation rather than the policy's existence.

Lesson: "created" and "attached" are different things, and only one of them does anything. The same create-versus-attach split appears with tags in Week 05.

`Error 404: The resource 'projects/null' was not found`

Problem: creating the folder-scoped firewall policy failed with a 404 naming a project called null.

Root cause: a folder-scoped policy is owned by no project, so the API has no project to attribute the call to.

Resolution: user_project_override = true plus billing_project on the provider — and that cascades. Every call is then attributed to the quota project, which must have cloudresourcemanager, serviceusage and cloudbilling enabled: APIs it does not itself use. The first enablement is chicken-and-egg and has to be done by hand.

A violation succeeded, then the same action was refused minutes later

Problem: after promoting Week 03's constraints, a blockProjectSshKeys violation succeeded, and an identical attempt failed a few minutes afterwards.

Root cause: enforcement lags the write — measured at 75–100 seconds in Week 03, and managed constraints are no different.

Lesson: a validator that tests a constraint immediately after an apply will report a working control as broken. Ours waits 120 seconds. And separately: enforcement is not retroactive — Google's own denial text says an instance already set to block-project-ssh-keys=false keeps access until its metadata changes.

Cleanup and Key Takeaways

Cleanup

terraform destroy removes the spoke. Keep the hub — the VPC, both subnets and the firewall policy occupy no project slot and Weeks 13–19 build on them.

Expect the first destroy to fail if constraints were promoted in the meantime. Week 03's blockProjectSshKeys was enforced after this lab deployed, and the destroy then failed because Terraform wanted to remove metadata that the now-enforced constraint refused to release. Commands are in the lab guide.

What actually happened: Week 05 destroyed this spoke to free a billing slot. The hub survived, which is why validate.sh's instance checks no longer pass while its network checks still do.

Ongoing charges: none. A custom-mode VPC with no instances costs nothing.

Key takeaways

  1. Creating a firewall policy and attaching it are different permissions. Only the attachment filters traffic, so validate the association rather than the policy.
  2. A folder-scoped resource needs a quota project, and setting one cascades into enabling APIs that resource does not use.
  3. "No public IP" is both the hardened choice and the free one — ephemeral external addresses have been billed hourly since 2020.
  4. Org policy enforcement lags, and is not retroactive. Wait before validating, and do not assume promotion fixes existing resources.
  5. `compute.xpnAdmin` cannot be narrowed. Shared VPC host enablement is checked at the organization, so this grant is as small as it can be and still the widest the pipeline holds.

References

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent