Below is a production-style Terraform baseline for GCP Hub-Spoke + Private GKE. It uses current Terraform/GCP patterns: private GKE, Workload Identity, Cloud NAT, Shared VPC-ready layout, and secure node pool defaults. Google’s docs confirm Workload Identity Federation is enabled with PROJECT_ID.svc.id.goog, Cloud NAT is managed NAT for private outbound access, and Terraform is officially supported for GKE provisioning. (Google Cloud Documentation)
Repo layout
gcp-gke-hub-spoke/├── providers.tf├── variables.tf├── main.tf├── outputs.tf├── terraform.tfvars└── modules/ ├── network/ │ └── main.tf ├── nat/ │ └── main.tf └── gke-private/ └── main.tf
providers.tf
terraform { required_version = ">= 1.6.0" required_providers { google = { source = "hashicorp/google" version = "~> 6.0" } }}provider "google" { project = var.hub_project_id region = var.region}
variables.tf
variable "hub_project_id" {}variable "spoke_project_id" {}variable "region" { default = "northamerica-northeast1"}variable "hub_vpc_name" { default = "hub-vpc"}variable "spoke_vpc_name" { default = "spoke-prod-vpc"}variable "gke_cluster_name" { default = "prod-private-gke"}variable "gke_subnet_cidr" { default = "10.10.0.0/20"}variable "pod_cidr" { default = "10.20.0.0/16"}variable "service_cidr" { default = "10.30.0.0/20"}variable "master_ipv4_cidr_block" { default = "172.16.0.0/28"}
main.tf
module "hub_network" { source = "./modules/network" project_id = var.hub_project_id name = var.hub_vpc_name region = var.region subnets = { hub-transit = "10.0.0.0/24" hub-security = "10.0.1.0/24" }}module "spoke_network" { source = "./modules/network" project_id = var.spoke_project_id name = var.spoke_vpc_name region = var.region subnets = { gke-nodes = var.gke_subnet_cidr } secondary_ranges = { gke-nodes = { pods = var.pod_cidr services = var.service_cidr } }}module "spoke_nat" { source = "./modules/nat" project_id = var.spoke_project_id region = var.region network = module.spoke_network.network_self_link router_name = "spoke-prod-router" nat_name = "spoke-prod-cloud-nat"}module "private_gke" { source = "./modules/gke-private" project_id = var.spoke_project_id region = var.region cluster_name = var.gke_cluster_name network = module.spoke_network.network_self_link subnetwork = module.spoke_network.subnet_self_links["gke-nodes"] pod_range_name = "pods" service_range_name = "services" master_ipv4_cidr_block = var.master_ipv4_cidr_block}
Module: modules/network/main.tf
variable "project_id" {}variable "name" {}variable "region" {}variable "subnets" { type = map(string)}variable "secondary_ranges" { type = map(any) default = {}}resource "google_compute_network" "vpc" { project = var.project_id name = var.name auto_create_subnetworks = false routing_mode = "GLOBAL"}resource "google_compute_subnetwork" "subnet" { for_each = var.subnets project = var.project_id name = each.key region = var.region network = google_compute_network.vpc.id ip_cidr_range = each.value private_ip_google_access = true dynamic "secondary_ip_range" { for_each = lookup(var.secondary_ranges, each.key, {}) content { range_name = secondary_ip_range.key ip_cidr_range = secondary_ip_range.value } } log_config { aggregation_interval = "INTERVAL_5_SEC" flow_sampling = 0.5 metadata = "INCLUDE_ALL_METADATA" }}resource "google_compute_firewall" "deny_all_ingress" { project = var.project_id name = "${var.name}-deny-all-ingress" network = google_compute_network.vpc.name direction = "INGRESS" priority = 65534 deny { protocol = "all" } source_ranges = ["0.0.0.0/0"]}resource "google_compute_firewall" "allow_internal" { project = var.project_id name = "${var.name}-allow-internal" network = google_compute_network.vpc.name direction = "INGRESS" priority = 1000 allow { protocol = "tcp" } allow { protocol = "udp" } allow { protocol = "icmp" } source_ranges = values(var.subnets)}output "network_self_link" { value = google_compute_network.vpc.self_link}output "subnet_self_links" { value = { for k, v in google_compute_subnetwork.subnet : k => v.self_link }}
Module: modules/nat/main.tf
variable "project_id" {}variable "region" {}variable "network" {}variable "router_name" {}variable "nat_name" {}resource "google_compute_router" "router" { project = var.project_id name = var.router_name region = var.region network = var.network}resource "google_compute_router_nat" "nat" { project = var.project_id name = var.nat_name router = google_compute_router.router.name region = var.region nat_ip_allocate_option = "AUTO_ONLY" source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" log_config { enable = true filter = "ERRORS_ONLY" }}
Module: modules/gke-private/main.tf
variable "project_id" {}variable "region" {}variable "cluster_name" {}variable "network" {}variable "subnetwork" {}variable "pod_range_name" {}variable "service_range_name" {}variable "master_ipv4_cidr_block" {}resource "google_service_account" "gke_nodes" { project = var.project_id account_id = "${var.cluster_name}-nodes" display_name = "GKE node service account"}resource "google_project_iam_member" "gke_node_logging" { project = var.project_id role = "roles/logging.logWriter" member = "serviceAccount:${google_service_account.gke_nodes.email}"}resource "google_project_iam_member" "gke_node_monitoring" { project = var.project_id role = "roles/monitoring.metricWriter" member = "serviceAccount:${google_service_account.gke_nodes.email}"}resource "google_container_cluster" "cluster" { project = var.project_id name = var.cluster_name location = var.region network = var.network subnetwork = var.subnetwork remove_default_node_pool = true initial_node_count = 1 release_channel { channel = "REGULAR" } workload_identity_config { workload_pool = "${var.project_id}.svc.id.goog" } private_cluster_config { enable_private_nodes = true enable_private_endpoint = true master_ipv4_cidr_block = var.master_ipv4_cidr_block } ip_allocation_policy { cluster_secondary_range_name = var.pod_range_name services_secondary_range_name = var.service_range_name } master_authorized_networks_config {} network_policy { enabled = true provider = "CALICO" } addons_config { network_policy_config { disabled = false } http_load_balancing { disabled = false } } logging_config { enable_components = [ "SYSTEM_COMPONENTS", "WORKLOADS", "APISERVER", "CONTROLLER_MANAGER", "SCHEDULER" ] } monitoring_config { enable_components = [ "SYSTEM_COMPONENTS", "APISERVER", "CONTROLLER_MANAGER", "SCHEDULER" ] } enable_shielded_nodes = true binary_authorization { evaluation_mode = "PROJECT_SINGLETON_POLICY_ENFORCE" }}resource "google_container_node_pool" "secure_pool" { project = var.project_id name = "secure-pool" location = var.region cluster = google_container_cluster.cluster.name node_count = 2 node_config { machine_type = "e2-standard-4" service_account = google_service_account.gke_nodes.email oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] shielded_instance_config { enable_secure_boot = true enable_integrity_monitoring = true } workload_metadata_config { mode = "GKE_METADATA" } labels = { environment = "prod" security = "restricted" } metadata = { disable-legacy-endpoints = "true" } } management { auto_repair = true auto_upgrade = true } autoscaling { min_node_count = 2 max_node_count = 6 }}output "cluster_name" { value = google_container_cluster.cluster.name}output "endpoint" { value = google_container_cluster.cluster.endpoint sensitive = true}
terraform.tfvars
hub_project_id = "my-hub-project"spoke_project_id = "my-spoke-prod-project"region = "northamerica-northeast1"hub_vpc_name = "hub-vpc"spoke_vpc_name = "spoke-prod-vpc"gke_cluster_name = "prod-private-gke"
Deploy
terraform initterraform fmt -recursiveterraform validateterraform planterraform apply
Important production notes
This baseline creates separate hub and spoke VPCs, but true centralized egress through the hub needs one of these designs:
Option 1: Shared VPCHub/Host Project owns the VPCSpoke/Service Projects consume subnetsOption 2: Network Connectivity CenterHub connects spokes with routing controlOption 3: VPC PeeringSimple hub-spoke, but no transitive routing
For real enterprise GKE, I would use:
Shared VPC + Private GKE + Cloud NAT + Cloud Armor + Artifact Registry+ Binary Authorization + Secret Manager + Workload Identity+ NetworkPolicy + centralized logging
Use this as the secure base, then add Cloud Armor ingress, Private Service Connect, and Secret Manager CSI driver next.