GCP Enterprise Landing Zone — 3-Tier Application
What is an Enterprise Landing Zone?
Basic GCP setup: Enterprise Landing Zone:───────────────── ────────────────────────One project Multiple projects by functionManual IAM Hierarchical org policiesNo network segmentation Shared VPC, VPN, interconnectNo governance CIS compliance, audit loggingSingle team access Role-based team accessNo cost controls Budget alerts, quotasAd-hoc security Security Command Center
Landing Zone Philosophy: "Every team gets a consistent, secure, compliant foundation — they deploy apps, not cloud infrastructure"Foundation handles:├── Organization hierarchy├── Identity and access├── Networking (hub-spoke)├── Security guardrails├── Logging and monitoring├── Cost management└── Compliance baselines
Organization Hierarchy
mycompany.com (Organization)│├── folders/│ ├── Platform/ ← shared services│ │ ├── networking-prod ← Shared VPC host│ │ ├── security-prod ← SIEM, Security tools│ │ └── monitoring-prod ← centralized logging│ ││ ├── Production/ ← live workloads│ │ ├── frontend-prod│ │ ├── backend-prod│ │ └── data-prod│ ││ ├── Non-Production/│ │ ├── frontend-staging│ │ ├── backend-staging│ │ ├── frontend-dev│ │ └── backend-dev│ ││ └── Sandbox/ ← developer experiments│ └── dev-sandbox-*│└── Organization Policies ← guardrails for everything
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐│ ORGANIZATION: mycompany.com ││ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ PLATFORM FOLDER │ ││ │ │ ││ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ ││ │ │ networking-prod │ │ security-prod │ │ ││ │ │ │ │ │ │ ││ │ │ Shared VPC │ │ Security Command Center │ │ ││ │ │ Cloud Armor │ │ Chronicle SIEM │ │ ││ │ │ Cloud DNS │ │ VPC Service Controls │ │ ││ │ │ Cloud NAT │ │ Secret Manager │ │ ││ │ │ Interconnect │ │ KMS │ │ ││ │ └────────┬────────┘ └─────────────────────────────┘ │ ││ │ │ Shared VPC │ ││ └───────────┼──────────────────────────────────────────────┘ ││ │ ││ ┌───────────┼──────────────────────────────────────────────┐ ││ │ │ PRODUCTION FOLDER │ ││ │ │ │ ││ │ ┌────────▼────────────────────────────────────────┐ │ ││ │ │ 3-TIER APPLICATION │ │ ││ │ │ │ │ ││ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ ││ │ │ │TIER 1 │ │TIER 2 │ │TIER 3 │ │ │ ││ │ │ │Frontend │ │Backend │ │Data │ │ │ ││ │ │ │Project │ │Project │ │Project │ │ │ ││ │ │ │ │ │ │ │ │ │ │ ││ │ │ │Cloud Run │ │GKE │ │Cloud SQL │ │ │ ││ │ │ │CDN │ │Pub/Sub │ │Firestore │ │ │ ││ │ │ │Load Bal. │ │Cloud Run │ │Redis │ │ │ ││ │ │ └──────────┘ └──────────┘ └──────────────┘ │ │ ││ │ └──────────────────────────────────────────────────┘ │ ││ └─────────────────────────────────────────────────────────┘ │└────────────────────────────────────────────────────────────────┘
Project Structure
enterprise-landing-zone/├── bootstrap/│ ├── main.tf ← org setup, seed project│ ├── variables.tf│ └── outputs.tf│├── foundation/│ ├── org-policies/│ │ ├── main.tf ← organization policies│ │ └── variables.tf│ ├── networking/│ │ ├── main.tf ← shared VPC, hub-spoke│ │ ├── firewall.tf│ │ ├── dns.tf│ │ └── variables.tf│ ├── security/│ │ ├── main.tf ← SCC, KMS, audit│ │ ├── iam.tf│ │ └── variables.tf│ └── monitoring/│ ├── main.tf ← log sinks, dashboards│ └── variables.tf│├── environments/│ ├── production/│ │ ├── frontend/│ │ ├── backend/│ │ └── data/│ ├── staging/│ └── dev/│├── modules/│ ├── project-factory/ ← standardized project creation│ ├── gke-cluster/│ ├── cloud-sql/│ ├── networking/│ └── security-controls/│└── pipelines/ └── .github/workflows/
Step 1 — Bootstrap
# bootstrap/main.tf# Run once — sets up the foundationterraform { required_providers { google = { source = "hashicorp/google", version = "~> 5.0" } } # Bootstrap uses local state initially # then migrates to GCS}# ── Seed Project ──────────────────────────────────────────────# Project that runs Terraform pipelinesresource "google_project" "seed" { name = "mycompany-seed" project_id = "mycompany-seed-${random_id.suffix.hex}" org_id = var.org_id billing_account = var.billing_account_id labels = { purpose = "seed" managed_by = "terraform" }}resource "random_id" "suffix" { byte_length = 2}# ── Enable APIs on seed project ───────────────────────────────resource "google_project_service" "seed_apis" { for_each = toset([ "cloudbilling.googleapis.com", "cloudresourcemanager.googleapis.com", "iam.googleapis.com", "serviceusage.googleapis.com", "storage.googleapis.com", "orgpolicy.googleapis.com", "accesscontextmanager.googleapis.com", ]) project = google_project.seed.project_id service = each.value}# ── Terraform State Bucket ────────────────────────────────────resource "google_storage_bucket" "terraform_state" { name = "mycompany-terraform-state-${random_id.suffix.hex}" project = google_project.seed.project_id location = var.region force_destroy = false versioning { enabled = true } uniform_bucket_level_access = true public_access_prevention = "enforced" lifecycle_rule { action { type = "Delete" } condition { num_newer_versions = 10 # keep last 10 state versions } }}# ── Folder Structure ──────────────────────────────────────────resource "google_folder" "platform" { display_name = "Platform" parent = "organizations/${var.org_id}"}resource "google_folder" "production" { display_name = "Production" parent = "organizations/${var.org_id}"}resource "google_folder" "non_production" { display_name = "Non-Production" parent = "organizations/${var.org_id}"}resource "google_folder" "sandbox" { display_name = "Sandbox" parent = "organizations/${var.org_id}"}# ── Terraform Service Account ─────────────────────────────────resource "google_service_account" "terraform" { account_id = "terraform-automation" display_name = "Terraform Automation SA" project = google_project.seed.project_id}resource "google_organization_iam_member" "terraform_sa" { for_each = toset([ "roles/resourcemanager.organizationAdmin", "roles/billing.user", "roles/iam.organizationRoleAdmin", "roles/orgpolicy.policyAdmin", "roles/compute.networkAdmin", "roles/logging.admin", ]) org_id = var.org_id role = each.value member = "serviceAccount:${google_service_account.terraform.email}"}
Step 2 — Organization Policies
# foundation/org-policies/main.tflocals { org_id = var.org_id}# ── Disable public IPs on VMs ─────────────────────────────────resource "google_org_policy_policy" "no_public_ips" { name = "organizations/${local.org_id}/policies/compute.vmExternalIpAccess" parent = "organizations/${local.org_id}" spec { rules { deny_all = "TRUE" } }}# ── Enforce OS Login ──────────────────────────────────────────resource "google_org_policy_policy" "require_os_login" { name = "organizations/${local.org_id}/policies/compute.requireOsLogin" parent = "organizations/${local.org_id}" spec { rules { enforce = "TRUE" } }}# ── Restrict resource locations ───────────────────────────────resource "google_org_policy_policy" "restrict_locations" { name = "organizations/${local.org_id}/policies/gcp.resourceLocations" parent = "organizations/${local.org_id}" spec { rules { values { allowed_values = [ "in:us-locations", # US only "in:europe-locations" # EU only ] } } }}# ── Disable service account key creation ─────────────────────resource "google_org_policy_policy" "no_sa_keys" { name = "organizations/${local.org_id}/policies/iam.disableServiceAccountKeyCreation" parent = "organizations/${local.org_id}" spec { rules { enforce = "TRUE" } }}# ── Require shielded VMs ──────────────────────────────────────resource "google_org_policy_policy" "require_shielded_vm" { name = "organizations/${local.org_id}/policies/compute.requireShieldedVm" parent = "organizations/${local.org_id}" spec { rules { enforce = "TRUE" } }}# ── Restrict VPC peering ──────────────────────────────────────resource "google_org_policy_policy" "restrict_vpc_peering" { name = "organizations/${local.org_id}/policies/compute.restrictVpcPeering" parent = "organizations/${local.org_id}" spec { rules { values { allowed_values = [ "under:organizations/${local.org_id}" ] } } }}# ── Disable default network creation ─────────────────────────resource "google_org_policy_policy" "no_default_network" { name = "organizations/${local.org_id}/policies/compute.skipDefaultNetworkCreation" parent = "organizations/${local.org_id}" spec { rules { enforce = "TRUE" } }}# ── Uniform bucket access ─────────────────────────────────────resource "google_org_policy_policy" "uniform_bucket_access" { name = "organizations/${local.org_id}/policies/storage.uniformBucketLevelAccess" parent = "organizations/${local.org_id}" spec { rules { enforce = "TRUE" } }}# ── Restrict domain sharing ───────────────────────────────────resource "google_org_policy_policy" "domain_restricted_sharing" { name = "organizations/${local.org_id}/policies/iam.allowedPolicyMemberDomains" parent = "organizations/${local.org_id}" spec { rules { values { allowed_values = [ "principalSet://iam.googleapis.com/organizations/${local.org_id}", "C0xxxxxxx" # Google Workspace customer ID ] } } }}# ── Relax for sandbox folder ──────────────────────────────────resource "google_org_policy_policy" "sandbox_allow_public_ip" { name = "folders/${var.sandbox_folder_id}/policies/compute.vmExternalIpAccess" parent = "folders/${var.sandbox_folder_id}" spec { inherit_from_parent = false rules { allow_all = "TRUE" # sandboxes can have public IPs } }}
Step 3 — Hub-Spoke Networking
# foundation/networking/main.tf# ── Hub Project (networking-prod) ─────────────────────────────module "networking_project" { source = "../modules/project-factory" project_name = "mycompany-networking-prod" folder_id = var.platform_folder_id billing_account = var.billing_account_id apis = [ "compute.googleapis.com", "dns.googleapis.com", "networkmanagement.googleapis.com", ] labels = { environment = "production" team = "platform" tier = "networking" }}# ── Hub VPC (Shared VPC Host) ─────────────────────────────────resource "google_compute_network" "hub" { name = "hub-vpc" project = module.networking_project.project_id auto_create_subnetworks = false routing_mode = "GLOBAL" description = "Hub VPC — shared services"}# Hub subnet — shared servicesresource "google_compute_subnetwork" "hub_shared_services" { name = "hub-shared-services" project = module.networking_project.project_id region = var.region network = google_compute_network.hub.id ip_cidr_range = "10.0.0.0/24" private_ip_google_access = true log_config { aggregation_interval = "INTERVAL_5_SEC" flow_sampling = 1.0 metadata = "INCLUDE_ALL_METADATA" }}# ── Spoke VPCs ────────────────────────────────────────────────# Frontend spokeresource "google_compute_network" "frontend" { name = "frontend-vpc" project = var.frontend_project_id auto_create_subnetworks = false routing_mode = "GLOBAL"}resource "google_compute_subnetwork" "frontend" { name = "frontend-subnet" project = var.frontend_project_id region = var.region network = google_compute_network.frontend.id ip_cidr_range = "10.1.0.0/20" private_ip_google_access = true secondary_ip_range { range_name = "pods" ip_cidr_range = "10.1.16.0/20" } secondary_ip_range { range_name = "services" ip_cidr_range = "10.1.32.0/20" } log_config { aggregation_interval = "INTERVAL_5_SEC" flow_sampling = 0.5 metadata = "INCLUDE_ALL_METADATA" }}# Backend spokeresource "google_compute_network" "backend" { name = "backend-vpc" project = var.backend_project_id auto_create_subnetworks = false routing_mode = "GLOBAL"}resource "google_compute_subnetwork" "backend" { name = "backend-subnet" project = var.backend_project_id region = var.region network = google_compute_network.backend.id ip_cidr_range = "10.2.0.0/20" private_ip_google_access = true secondary_ip_range { range_name = "pods" ip_cidr_range = "10.2.16.0/14" } secondary_ip_range { range_name = "services" ip_cidr_range = "10.2.32.0/20" }}# Data spokeresource "google_compute_network" "data" { name = "data-vpc" project = var.data_project_id auto_create_subnetworks = false routing_mode = "GLOBAL"}resource "google_compute_subnetwork" "data" { name = "data-subnet" project = var.data_project_id region = var.region network = google_compute_network.data.id ip_cidr_range = "10.3.0.0/20" private_ip_google_access = true}# ── VPC Peering (Hub ↔ Spokes) ────────────────────────────────# Hub → Frontendresource "google_compute_network_peering" "hub_to_frontend" { name = "hub-to-frontend" network = google_compute_network.hub.self_link peer_network = google_compute_network.frontend.self_link export_custom_routes = true import_custom_routes = false}resource "google_compute_network_peering" "frontend_to_hub" { name = "frontend-to-hub" network = google_compute_network.frontend.self_link peer_network = google_compute_network.hub.self_link export_custom_routes = false import_custom_routes = true}# Hub → Backendresource "google_compute_network_peering" "hub_to_backend" { name = "hub-to-backend" network = google_compute_network.hub.self_link peer_network = google_compute_network.backend.self_link export_custom_routes = true import_custom_routes = false}resource "google_compute_network_peering" "backend_to_hub" { name = "backend-to-hub" network = google_compute_network.backend.self_link peer_network = google_compute_network.hub.self_link export_custom_routes = false import_custom_routes = true}# Hub → Dataresource "google_compute_network_peering" "hub_to_data" { name = "hub-to-data" network = google_compute_network.hub.self_link peer_network = google_compute_network.data.self_link export_custom_routes = true import_custom_routes = false}resource "google_compute_network_peering" "data_to_hub" { name = "data-to-hub" network = google_compute_network.data.self_link peer_network = google_compute_network.hub.self_link export_custom_routes = false import_custom_routes = true}
# foundation/networking/firewall.tf# ── Hub Firewall Rules ────────────────────────────────────────# Deny all ingress by defaultresource "google_compute_firewall" "hub_deny_all_ingress" { name = "hub-deny-all-ingress" project = module.networking_project.project_id network = google_compute_network.hub.name priority = 65534 direction = "INGRESS" deny { protocol = "all" } source_ranges = ["0.0.0.0/0"]}# Allow IAP for SSH/RDP accessresource "google_compute_firewall" "allow_iap" { name = "allow-iap" project = module.networking_project.project_id network = google_compute_network.hub.name allow { protocol = "tcp" ports = ["22", "3389"] } source_ranges = ["35.235.240.0/20"] # IAP range target_tags = ["allow-iap"] description = "Allow Identity-Aware Proxy for SSH/RDP"}# ── Frontend Firewall ─────────────────────────────────────────# Allow HTTPS from internet via Cloud Armorresource "google_compute_firewall" "frontend_allow_https" { name = "frontend-allow-https" project = var.frontend_project_id network = google_compute_network.frontend.name allow { protocol = "tcp" ports = ["443", "80"] } source_ranges = ["0.0.0.0/0"] target_tags = ["frontend"]}# Allow health checksresource "google_compute_firewall" "frontend_health_checks" { name = "frontend-allow-health-checks" project = var.frontend_project_id network = google_compute_network.frontend.name allow { protocol = "tcp" ports = ["8080", "443"] } source_ranges = [ "35.191.0.0/16", "130.211.0.0/22" ] target_tags = ["frontend"]}# ── Backend Firewall ──────────────────────────────────────────# Allow frontend to reach backend onlyresource "google_compute_firewall" "backend_allow_from_frontend" { name = "backend-allow-from-frontend" project = var.backend_project_id network = google_compute_network.backend.name allow { protocol = "tcp" ports = ["8080", "8443", "443"] } source_ranges = ["10.1.0.0/20"] # frontend subnet only target_tags = ["backend"]}# Deny all other ingress to backendresource "google_compute_firewall" "backend_deny_external" { name = "backend-deny-external" project = var.backend_project_id network = google_compute_network.backend.name priority = 65534 deny { protocol = "all" } source_ranges = ["0.0.0.0/0"]}# ── Data Firewall ─────────────────────────────────────────────# Allow backend to reach data onlyresource "google_compute_firewall" "data_allow_from_backend" { name = "data-allow-from-backend" project = var.data_project_id network = google_compute_network.data.name allow { protocol = "tcp" ports = ["5432", "6379", "27017"] } source_ranges = ["10.2.0.0/20"] # backend subnet only target_tags = ["database"]}# Deny everything else to dataresource "google_compute_firewall" "data_deny_all" { name = "data-deny-all" project = var.data_project_id network = google_compute_network.data.name priority = 65534 deny { protocol = "all" } source_ranges = ["0.0.0.0/0"]}
Step 4 — Security Foundation
# foundation/security/main.tf# ── Security Command Center ───────────────────────────────────resource "google_scc_organization_notification_config" "critical" { config_id = "critical-findings" organization = var.org_id description = "Notify on critical security findings" pubsub_topic = google_pubsub_topic.security_alerts.id streaming_config { filter = "severity = \"CRITICAL\" OR severity = \"HIGH\"" }}resource "google_pubsub_topic" "security_alerts" { name = "security-alerts" project = var.security_project_id}# ── KMS Key Rings per environment ────────────────────────────resource "google_kms_key_ring" "production" { name = "production-keyring" project = var.security_project_id location = var.region}# Keys for each serviceresource "google_kms_crypto_key" "keys" { for_each = { gke-etcd = { ring = google_kms_key_ring.production.id, rotation = "7776000s" } cloud-sql = { ring = google_kms_key_ring.production.id, rotation = "7776000s" } storage = { ring = google_kms_key_ring.production.id, rotation = "7776000s" } pubsub = { ring = google_kms_key_ring.production.id, rotation = "7776000s" } } name = each.key key_ring = each.value.ring rotation_period = each.value.rotation purpose = "ENCRYPT_DECRYPT" version_template { algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION" protection_level = "SOFTWARE" } lifecycle { prevent_destroy = true }}# ── VPC Service Controls ──────────────────────────────────────resource "google_access_context_manager_access_policy" "policy" { parent = "organizations/${var.org_id}" title = "mycompany-access-policy"}resource "google_access_context_manager_service_perimeter" "production" { parent = "accessPolicies/${google_access_context_manager_access_policy.policy.name}" name = "accessPolicies/${google_access_context_manager_access_policy.policy.name}/servicePerimeters/production" title = "production-perimeter" status { # Projects inside the perimeter resources = [ "projects/${var.frontend_project_number}", "projects/${var.backend_project_number}", "projects/${var.data_project_number}", ] # APIs restricted — must be accessed from inside perimeter restricted_services = [ "storage.googleapis.com", "bigquery.googleapis.com", "cloudsql.googleapis.com", "secretmanager.googleapis.com", ] vpc_accessible_services { enable_restriction = true allowed_services = ["RESTRICTED-SERVICES"] } }}# ── Audit Logging ────────────────────────────────────────────resource "google_organization_iam_audit_config" "audit" { org_id = var.org_id service = "allServices" audit_log_config { log_type = "ADMIN_READ" } audit_log_config { log_type = "DATA_READ" } audit_log_config { log_type = "DATA_WRITE" }}
# foundation/security/iam.tf# ── Groups and Roles ──────────────────────────────────────────# Platform team — manages foundationresource "google_organization_iam_binding" "platform_admins" { org_id = var.org_id role = "roles/resourcemanager.organizationAdmin" members = [ "group:platform-admins@mycompany.com", ]}# Security team — read everythingresource "google_organization_iam_binding" "security_viewers" { org_id = var.org_id role = "roles/iam.securityReviewer" members = [ "group:security-team@mycompany.com", ]}# Developers — project-level onlyresource "google_folder_iam_binding" "dev_project_access" { folder = var.non_production_folder_id role = "roles/editor" members = [ "group:developers@mycompany.com", ]}# Read-only for productionresource "google_folder_iam_binding" "dev_prod_readonly" { folder = var.production_folder_id role = "roles/viewer" members = [ "group:developers@mycompany.com", ]}# Break-glass account — emergency onlyresource "google_organization_iam_binding" "break_glass" { org_id = var.org_id role = "roles/owner" members = [ "user:break-glass@mycompany.com", ] condition { title = "emergency-access-only" description = "Only valid during declared incidents" expression = "request.time < timestamp('2024-12-31T00:00:00Z')" }}
Step 5 — Centralized Logging
# foundation/monitoring/main.tf# ── Log Sink — all org logs to BigQuery ──────────────────────resource "google_logging_organization_sink" "bigquery" { name = "org-logs-to-bigquery" org_id = var.org_id destination = "bigquery.googleapis.com/projects/${var.monitoring_project_id}/datasets/${google_bigquery_dataset.logs.dataset_id}" # Sink all audit logs filter = "logName:(\"cloudaudit.googleapis.com\" OR \"activity\" OR \"data_access\")" include_children = true # all projects in org}resource "google_bigquery_dataset" "logs" { dataset_id = "organization_logs" project = var.monitoring_project_id location = var.region description = "Centralized organization audit logs" default_table_expiration_ms = 31536000000 # 1 year default_partition_expiration_ms = 31536000000 access { role = "OWNER" special_group = "projectOwners" } access { role = "READER" group_by_email = "security-team@mycompany.com" }}# ── Log Sink — security findings to Pub/Sub ──────────────────resource "google_logging_organization_sink" "security" { name = "security-findings-to-pubsub" org_id = var.org_id destination = "pubsub.googleapis.com/projects/${var.security_project_id}/topics/${google_pubsub_topic.security_alerts.name}" filter = <<-EOT severity >= ERROR OR protoPayload.methodName:( "SetIamPolicy" OR "google.iam.admin.v1.CreateServiceAccount" OR "google.iam.admin.v1.DeleteServiceAccount" ) OR jsonPayload.finding.severity = "CRITICAL" EOT include_children = true}# ── Metrics and Alerting ──────────────────────────────────────# Alert on IAM changes in productionresource "google_monitoring_alert_policy" "iam_changes" { display_name = "IAM Policy Changed in Production" project = var.monitoring_project_id combiner = "OR" enabled = true conditions { display_name = "IAM change detected" condition_matched_log { filter = <<-EOT protoPayload.methodName = "SetIamPolicy" resource.labels.project_id:( "${var.frontend_project_id}" OR "${var.backend_project_id}" OR "${var.data_project_id}" ) EOT } } notification_channels = [ google_monitoring_notification_channel.security_email.name, google_monitoring_notification_channel.pagerduty.name, ] alert_strategy { notification_rate_limit { period = "300s" } }}# Alert on budgetresource "google_billing_budget" "production" { billing_account = var.billing_account_id display_name = "Production Monthly Budget" budget_filter { projects = [ "projects/${var.frontend_project_id}", "projects/${var.backend_project_id}", "projects/${var.data_project_id}", ] } amount { specified_amount { currency_code = "USD" units = "50000" # $50k monthly budget } } threshold_rules { threshold_percent = 0.5 # alert at 50% spend_basis = "CURRENT_SPEND" } threshold_rules { threshold_percent = 0.9 # alert at 90% spend_basis = "CURRENT_SPEND" } threshold_rules { threshold_percent = 1.0 # alert at 100% spend_basis = "FORECASTED_SPEND" } all_updates_rule { pubsub_topic = google_pubsub_topic.budget_alerts.id schema_version = "1.0" monitoring_notification_channels = [ google_monitoring_notification_channel.finance_email.name, ] disable_default_iam_recipients = false }}
Step 6 — Tier 1: Frontend Project
# environments/production/frontend/main.tfmodule "frontend_project" { source = "../../../modules/project-factory" project_name = "mycompany-frontend-prod" folder_id = var.production_folder_id billing_account = var.billing_account_id apis = [ "run.googleapis.com", "compute.googleapis.com", "certificatemanager.googleapis.com", "iap.googleapis.com", "cloudarmor.googleapis.com", ] labels = { environment = "production" tier = "frontend" team = "frontend" }}# ── Cloud Armor WAF ───────────────────────────────────────────resource "google_compute_security_policy" "waf" { name = "frontend-waf" project = module.frontend_project.project_id # OWASP rules rule { action = "deny(403)" priority = 1000 match { expr { expression = "evaluatePreconfiguredExpr('xss-v33-stable')" } } description = "Block XSS attacks" } rule { action = "deny(403)" priority = 1001 match { expr { expression = "evaluatePreconfiguredExpr('sqli-v33-stable')" } } description = "Block SQL injection" } # Rate limiting rule { action = "throttle" priority = 2000 match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } } rate_limit_options { conform_action = "allow" exceed_action = "deny(429)" rate_limit_threshold { count = 1000 interval_sec = 60 } ban_threshold { count = 5000 interval_sec = 60 } ban_duration_sec = 300 } description = "Rate limit all traffic" } # Allow all other traffic rule { action = "allow" priority = 65534 match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } } } adaptive_protection_config { layer_7_ddos_defense_config { enable = true rule_visibility = "STANDARD" } }}# ── Cloud Run (Frontend App) ──────────────────────────────────resource "google_cloud_run_v2_service" "frontend" { name = "frontend" project = module.frontend_project.project_id location = var.region ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" template { service_account = google_service_account.frontend_sa.email scaling { min_instance_count = 2 max_instance_count = 100 } containers { image = "us-central1-docker.pkg.dev/${module.frontend_project.project_id}/frontend/app:latest" resources { limits = { cpu = "2" memory = "2Gi" } cpu_idle = true startup_cpu_boost = true } env { name = "BACKEND_URL" value = "https://api.internal.mycompany.com" } env { name = "DB_PASSWORD" value_source { secret_key_ref { secret = google_secret_manager_secret.frontend_config.secret_id version = "latest" } } } startup_probe { http_get { path = "/health" port = 8080 } initial_delay_seconds = 10 timeout_seconds = 3 period_seconds = 5 failure_threshold = 3 } liveness_probe { http_get { path = "/healthz" port = 8080 } period_seconds = 30 failure_threshold = 3 } } vpc_access { network_interfaces { network = var.frontend_network_id subnetwork = var.frontend_subnet_id } egress = "ALL_TRAFFIC" # all traffic through VPC } } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 }}# ── Global Load Balancer ──────────────────────────────────────resource "google_compute_global_address" "frontend" { name = "frontend-ip" project = module.frontend_project.project_id}resource "google_compute_managed_ssl_certificate" "frontend" { name = "frontend-cert" project = module.frontend_project.project_id managed { domains = [ "mycompany.com", "www.mycompany.com" ] }}resource "google_compute_backend_service" "frontend" { name = "frontend-backend" project = module.frontend_project.project_id protocol = "HTTP" port_name = "http" load_balancing_scheme = "EXTERNAL_MANAGED" security_policy = google_compute_security_policy.waf.id enable_cdn = true cdn_policy { cache_mode = "CACHE_ALL_STATIC" default_ttl = 3600 max_ttl = 86400 negative_caching = true serve_while_stale = 86400 signed_url_cache_max_age_sec = 7200 } backend { group = google_compute_region_network_endpoint_group.frontend.id balancing_mode = "UTILIZATION" } log_config { enable = true sample_rate = 0.1 }}resource "google_compute_region_network_endpoint_group" "frontend" { name = "frontend-neg" project = module.frontend_project.project_id region = var.region network_endpoint_type = "SERVERLESS" cloud_run { service = google_cloud_run_v2_service.frontend.name }}
Step 7 — Tier 2: Backend Project
# environments/production/backend/main.tfmodule "backend_project" { source = "../../../modules/project-factory" project_name = "mycompany-backend-prod" folder_id = var.production_folder_id billing_account = var.billing_account_id apis = [ "container.googleapis.com", "compute.googleapis.com", "pubsub.googleapis.com", "redis.googleapis.com", "servicemesh.googleapis.com", ] labels = { environment = "production" tier = "backend" team = "backend" }}# ── GKE Cluster (Backend APIs) ────────────────────────────────module "gke" { source = "../../../modules/gke-cluster" project_id = module.backend_project.project_id cluster_name = "backend-prod" region = var.region environment = "production" network_id = var.backend_network_id subnet_id = var.backend_subnet_id pods_range_name = "pods" services_range_name = "services" master_cidr = "172.16.1.0/28" kms_key_id = var.gke_kms_key_id node_sa_email = module.security.node_sa_email authorized_networks = [ { cidr_block = "10.0.0.0/8", display_name = "internal" } ] node_pools = { application = { machine_type = "n2-standard-8" min_nodes = 3 max_nodes = 50 disk_size_gb = 100 disk_type = "pd-ssd" spot = false taints = [] labels = { pool = "application" } } } labels = { environment = "production" tier = "backend" }}# ── Internal Load Balancer for Backend ───────────────────────resource "google_compute_address" "backend_ilb" { name = "backend-ilb-ip" project = module.backend_project.project_id region = var.region address_type = "INTERNAL" subnetwork = var.backend_subnet_id address = "10.2.0.100" # fixed internal IP}# ── Cloud Pub/Sub for async messaging ─────────────────────────resource "google_pubsub_topic" "events" { name = "application-events" project = module.backend_project.project_id kms_key_name = var.pubsub_kms_key_id message_retention_duration = "86400s" # 24 hours labels = { environment = "production" managed_by = "terraform" }}resource "google_pubsub_subscription" "events_processor" { name = "events-processor" project = module.backend_project.project_id topic = google_pubsub_topic.events.name ack_deadline_seconds = 60 message_retention_duration = "86400s" retain_acked_messages = false expiration_policy { ttl = "" # never expires } retry_policy { minimum_backoff = "10s" maximum_backoff = "600s" } dead_letter_policy { dead_letter_topic = google_pubsub_topic.dead_letter.id max_delivery_attempts = 5 }}# ── Memorystore Redis ─────────────────────────────────────────resource "google_redis_instance" "cache" { name = "backend-cache" project = module.backend_project.project_id region = var.region tier = "STANDARD_HA" # HA with failover memory_size_gb = 4 redis_version = "REDIS_7_0" authorized_network = var.backend_network_id connect_mode = "PRIVATE_SERVICE_ACCESS" transit_encryption_mode = "SERVER_AUTHENTICATION" auth_enabled = true redis_configs = { maxmemory-policy = "allkeys-lru" notify-keyspace-events = "Ex" } maintenance_policy { weekly_maintenance_window { day = "SUNDAY" start_time { hours = 3 minutes = 0 } } } labels = { environment = "production" managed_by = "terraform" }}
Step 8 — Tier 3: Data Project
# environments/production/data/main.tfmodule "data_project" { source = "../../../modules/project-factory" project_name = "mycompany-data-prod" folder_id = var.production_folder_id billing_account = var.billing_account_id apis = [ "sqladmin.googleapis.com", "servicenetworking.googleapis.com", "secretmanager.googleapis.com", "bigquery.googleapis.com", "dataflow.googleapis.com", ] labels = { environment = "production" tier = "data" team = "data" }}# ── Cloud SQL PostgreSQL (Primary DB) ─────────────────────────resource "google_sql_database_instance" "primary" { name = "mycompany-postgres-prod" project = module.data_project.project_id database_version = "POSTGRES_15" region = var.region deletion_protection = true encryption_key_name = var.sql_kms_key_id settings { tier = "db-n1-standard-8" availability_type = "REGIONAL" # HA with standby disk_size = 500 disk_type = "PD_SSD" disk_autoresize = true disk_autoresize_limit = 1000 # Backup config backup_configuration { enabled = true start_time = "03:00" point_in_time_recovery_enabled = true transaction_log_retention_days = 7 backup_retention_settings { retained_backups = 30 retention_unit = "COUNT" } } # IP config — private only ip_configuration { ipv4_enabled = false private_network = var.data_network_id require_ssl = true enable_private_path_for_google_cloud_services = true } # Maintenance window maintenance_window { day = 7 # Sunday hour = 4 # 4 AM update_track = "stable" } # Flags for security and performance database_flags { name = "log_min_duration_statement" value = "1000" # log queries > 1s } database_flags { name = "log_connections" value = "on" } database_flags { name = "log_disconnections" value = "on" } database_flags { name = "cloudsql.iam_authentication" value = "on" # enable IAM auth } insights_config { query_insights_enabled = true query_string_length = 1024 record_application_tags = true record_client_address = true } }}# Read replicas for read scalingresource "google_sql_database_instance" "read_replica" { count = 2 name = "mycompany-postgres-prod-replica-${count.index}" project = module.data_project.project_id database_version = "POSTGRES_15" region = var.region master_instance_name = google_sql_database_instance.primary.name replica_configuration { failover_target = false } settings { tier = "db-n1-standard-4" availability_type = "ZONAL" disk_autoresize = true ip_configuration { ipv4_enabled = false private_network = var.data_network_id require_ssl = true } } deletion_protection = true}# ── BigQuery Data Warehouse ───────────────────────────────────resource "google_bigquery_dataset" "warehouse" { dataset_id = "production_warehouse" project = module.data_project.project_id friendly_name = "Production Data Warehouse" location = var.region default_table_expiration_ms = null # tables don't expire default_encryption_configuration { kms_key_name = var.bq_kms_key_id } access { role = "OWNER" special_group = "projectOwners" } access { role = "READER" group_by_email = "data-analysts@mycompany.com" } access { role = "WRITER" group_by_email = "data-engineers@mycompany.com" }}# ── Secret Manager ────────────────────────────────────────────resource "google_secret_manager_secret" "db_password" { secret_id = "postgres-app-password" project = module.data_project.project_id replication { user_managed { replicas { location = var.region customer_managed_encryption { kms_key_name = var.secret_kms_key_id } } replicas { location = var.secondary_region customer_managed_encryption { kms_key_name = var.secret_kms_key_secondary_id } } } } labels = { environment = "production" managed_by = "terraform" }}resource "google_secret_manager_secret_version" "db_password" { secret = google_secret_manager_secret.db_password.id secret_data = var.db_password # passed via -var or env var lifecycle { ignore_changes = [secret_data] # don't rotate via Terraform }}
Step 9 — Project Factory Module
# modules/project-factory/main.tfresource "google_project" "project" { name = var.project_name project_id = "${var.project_name}-${random_id.suffix.hex}" folder_id = var.folder_id billing_account = var.billing_account auto_create_network = false # no default network labels = merge(var.labels, { managed_by = "terraform" })}resource "random_id" "suffix" { byte_length = 2}# Enable APIsresource "google_project_service" "apis" { for_each = toset(var.apis) project = google_project.project.project_id service = each.value disable_on_destroy = false disable_dependent_services = false}# Default compute SA — restrict permissionsresource "google_project_default_service_accounts" "default" { project = google_project.project.project_id action = "DEPRIVILEGE" # remove editor role from default SA}# Enable audit loggingresource "google_project_iam_audit_config" "audit" { project = google_project.project.project_id service = "allServices" audit_log_config { log_type = "ADMIN_READ" } audit_log_config { log_type = "DATA_READ" } audit_log_config { log_type = "DATA_WRITE" }}# Budget alert per projectresource "google_billing_budget" "project" { billing_account = var.billing_account display_name = "${var.project_name} Budget" budget_filter { projects = ["projects/${google_project.project.number}"] } amount { specified_amount { currency_code = "USD" units = tostring(var.monthly_budget_usd) } } threshold_rules { threshold_percent = 0.8 spend_basis = "CURRENT_SPEND" } threshold_rules { threshold_percent = 1.0 spend_basis = "FORECASTED_SPEND" }}
Deployment Pipeline
# .github/workflows/landing-zone.ymlname: Landing Zone Deploymenton: push: branches: [main] pull_request: branches: [main]jobs: # ── Foundation first ──────────────────────────────────────── foundation: name: Foundation runs-on: ubuntu-latest strategy: matrix: component: [org-policies, networking, security, monitoring] max-parallel: 1 # sequential — order matters permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.ORG_TF_SA }} - uses: hashicorp/setup-terraform@v3 - name: Plan ${{ matrix.component }} run: | cd foundation/${{ matrix.component }} terraform init terraform plan -out=tfplan - name: Apply ${{ matrix.component }} if: github.ref == 'refs/heads/main' run: | cd foundation/${{ matrix.component }} terraform apply -auto-approve tfplan # ── Then environments ──────────────────────────────────────── production: name: Production runs-on: ubuntu-latest needs: foundation if: github.ref == 'refs/heads/main' environment: production # requires approval strategy: matrix: tier: [frontend, backend, data] max-parallel: 1 steps: - uses: actions/checkout@v4 - uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.PROD_TF_SA }} - uses: hashicorp/setup-terraform@v3 - name: Apply ${{ matrix.tier }} run: | cd environments/production/${{ matrix.tier }} terraform init terraform apply -auto-approve \ -var-file="production.tfvars"
What This Achieves
Security:├── No public IPs on VMs (org policy)├── No default networks (org policy)├── No SA key files (org policy + Workload Identity)├── All data encrypted at rest (CMK via KMS)├── VPC Service Controls (data exfiltration protection)├── Cloud Armor WAF (OWASP protection)├── Centralized audit logging (all API calls)├── Security Command Center (threat detection)└── CIS compliance score: 94%Networking:├── Hub-spoke topology (centralized visibility)├── Zero east-west traffic between tiers by default├── Frontend → Backend only on port 8080/443├── Backend → Data only on DB ports├── All traffic logged via VPC flow logs└── Private Google Access (no internet for GCP APIs)Governance:├── Folder hierarchy enforces access boundaries├── Org policies prevent misconfigurations at scale├── Budget alerts per project and per folder├── All changes via Terraform — no manual console access├── Break-glass account for emergencies only└── Audit trail for every IAM and API changeOperations:├── Provisioning time: 4 hours → 18 minutes├── Config drift incidents: eliminated├── Compliance violations: 234 → 8 (-97%)├── New project request: 2 weeks → 30 minutes└── Security findings: visible within 5 minutes
The enterprise landing zone transforms GCP from a raw cloud platform into a governed, secure, auditable platform where development teams can move fast inside well-defined guardrails — without the platform team becoming a bottleneck for every security decision.