Building a 3-Tier Application in GCP

GCP Enterprise Landing Zone — 3-Tier Application

What is an Enterprise Landing Zone?

Basic GCP setup: Enterprise Landing Zone:
───────────────── ────────────────────────
One project Multiple projects by function
Manual IAM Hierarchical org policies
No network segmentation Shared VPC, VPN, interconnect
No governance CIS compliance, audit logging
Single team access Role-based team access
No cost controls Budget alerts, quotas
Ad-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 foundation
terraform {
required_providers {
google = { source = "hashicorp/google", version = "~> 5.0" }
}
# Bootstrap uses local state initially
# then migrates to GCS
}
# ── Seed Project ──────────────────────────────────────────────
# Project that runs Terraform pipelines
resource "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.tf
locals {
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 services
resource "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 spoke
resource "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 spoke
resource "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 spoke
resource "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 → Frontend
resource "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 → Backend
resource "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 → Data
resource "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 default
resource "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 access
resource "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 Armor
resource "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 checks
resource "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 only
resource "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 backend
resource "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 only
resource "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 data
resource "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 service
resource "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 foundation
resource "google_organization_iam_binding" "platform_admins" {
org_id = var.org_id
role = "roles/resourcemanager.organizationAdmin"
members = [
"group:platform-admins@mycompany.com",
]
}
# Security team — read everything
resource "google_organization_iam_binding" "security_viewers" {
org_id = var.org_id
role = "roles/iam.securityReviewer"
members = [
"group:security-team@mycompany.com",
]
}
# Developers — project-level only
resource "google_folder_iam_binding" "dev_project_access" {
folder = var.non_production_folder_id
role = "roles/editor"
members = [
"group:developers@mycompany.com",
]
}
# Read-only for production
resource "google_folder_iam_binding" "dev_prod_readonly" {
folder = var.production_folder_id
role = "roles/viewer"
members = [
"group:developers@mycompany.com",
]
}
# Break-glass account — emergency only
resource "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 production
resource "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 budget
resource "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.tf
module "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.tf
module "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.tf
module "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 scaling
resource "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.tf
resource "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 APIs
resource "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 permissions
resource "google_project_default_service_accounts" "default" {
project = google_project.project.project_id
action = "DEPRIVILEGE" # remove editor role from default SA
}
# Enable audit logging
resource "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 project
resource "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.yml
name: Landing Zone Deployment
on:
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 change
Operations:
├── 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.

Leave a Reply