GCP for DevOps — Project: Terraform 3-Tier Architecture

4 minute read

Project 4: Terraform 3-Tier Architecture

Build a production 3-tier web application architecture entirely with Terraform: an HTTP(S) Load Balancer fronting a Managed Instance Group of web servers, connected to a Cloud SQL PostgreSQL database through a private network.

Architecture

3-Tier Architecture:
┌─────────────────────────────────────────────────────────┐
│                       Internet                           │
│                          │                               │
│                  ┌───────▼────────┐                      │
│                  │  HTTP(S) Load  │  Tier 1: Web         │
│                  │  Balancer      │                      │
│                  └───────┬────────┘                      │
│                          │                               │
│         ┌────────────────┼────────────────┐              │
│         ▼                ▼                ▼              │
│    ┌─────────┐     ┌─────────┐     ┌─────────┐         │
│    │  VM 1   │     │  VM 2   │     │  VM 3   │ Tier 2  │
│    │  (MIG)  │     │  (MIG)  │     │  (MIG)  │ App     │
│    └────┬────┘     └────┬────┘     └────┬────┘         │
│         │               │               │               │
│         └───────────────┼───────────────┘               │
│                         │ Private IP                     │
│                  ┌──────▼───────┐                        │
│                  │  Cloud SQL   │  Tier 3: Database      │
│                  │  PostgreSQL  │                        │
│                  │  (Private)   │                        │
│                  └──────────────┘                        │
└─────────────────────────────────────────────────────────┘

Project Structure

terraform-3tier/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
├── backend.tf
├── network.tf
├── compute.tf
├── database.tf
├── loadbalancer.tf
└── startup.sh

Step 1: Provider and Backend

# backend.tf
terraform {
  required_version = ">= 1.5"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
  backend "gcs" {
    bucket = "my-project-terraform-state"
    prefix = "3tier/production"
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}
# variables.tf
variable "project_id" {
  type = string
}

variable "region" {
  type    = string
  default = "us-central1"
}

variable "zone" {
  type    = string
  default = "us-central1-a"
}

variable "db_password" {
  type      = string
  sensitive = true
}

Step 2: Network Layer

# network.tf
resource "google_compute_network" "main" {
  name                    = "three-tier-vpc"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "web" {
  name          = "web-subnet"
  ip_cidr_range = "10.0.1.0/24"
  region        = var.region
  network       = google_compute_network.main.id
}

resource "google_compute_subnetwork" "db" {
  name          = "db-subnet"
  ip_cidr_range = "10.0.2.0/24"
  region        = var.region
  network       = google_compute_network.main.id
  private_ip_google_access = true
}

# Private services access for Cloud SQL
resource "google_compute_global_address" "private_ip" {
  name          = "sql-private-ip"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = google_compute_network.main.id
}

resource "google_service_networking_connection" "private_vpc" {
  network                 = google_compute_network.main.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_ip.name]
}

# Firewall rules
resource "google_compute_firewall" "allow_http" {
  name    = "allow-http"
  network = google_compute_network.main.id

  allow {
    protocol = "tcp"
    ports    = ["80", "443"]
  }
  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["web"]
}

resource "google_compute_firewall" "allow_health_check" {
  name    = "allow-health-check"
  network = google_compute_network.main.id

  allow {
    protocol = "tcp"
    ports    = ["80"]
  }
  # Google health check IP ranges
  source_ranges = ["130.211.0.0/22", "35.191.0.0/16"]
  target_tags   = ["web"]
}

resource "google_compute_firewall" "allow_internal" {
  name    = "allow-internal"
  network = google_compute_network.main.id

  allow {
    protocol = "tcp"
    ports    = ["0-65535"]
  }
  source_ranges = ["10.0.0.0/16"]
}

Step 3: Compute Layer (MIG)

# compute.tf

# Instance template
resource "google_compute_instance_template" "web" {
  name_prefix  = "web-template-"
  machine_type = "e2-small"
  tags         = ["web"]

  disk {
    source_image = "debian-cloud/debian-12"
    auto_delete  = true
    boot         = true
    disk_size_gb = 20
  }

  network_interface {
    subnetwork = google_compute_subnetwork.web.id
    access_config {} # External IP for demo
  }

  metadata_startup_script = templatefile("startup.sh", {
    db_host = google_sql_database_instance.main.private_ip_address
    db_name = "appdb"
    db_user = "appuser"
    db_pass = var.db_password
  })

  labels = {
    env  = "prod"
    tier = "web"
  }

  lifecycle {
    create_before_destroy = true
  }
}

# Health check
resource "google_compute_health_check" "web" {
  name               = "web-health-check"
  check_interval_sec = 10
  timeout_sec        = 5

  http_health_check {
    port         = 80
    request_path = "/health"
  }
}

# Managed Instance Group
resource "google_compute_region_instance_group_manager" "web" {
  name               = "web-mig"
  region             = var.region
  base_instance_name = "web"

  version {
    instance_template = google_compute_instance_template.web.id
  }

  target_size = 3

  named_port {
    name = "http"
    port = 80
  }

  auto_healing_policies {
    health_check      = google_compute_health_check.web.id
    initial_delay_sec = 120
  }
}

# Autoscaler
resource "google_compute_region_autoscaler" "web" {
  name   = "web-autoscaler"
  region = var.region
  target = google_compute_region_instance_group_manager.web.id

  autoscaling_policy {
    min_replicas    = 2
    max_replicas    = 10
    cooldown_period = 60

    cpu_utilization {
      target = 0.7
    }
  }
}

Step 4: Database Layer

# database.tf
resource "google_sql_database_instance" "main" {
  name             = "app-db-instance"
  database_version = "POSTGRES_15"
  region           = var.region

  depends_on = [google_service_networking_connection.private_vpc]

  settings {
    tier              = "db-custom-2-4096"
    availability_type = "REGIONAL"
    disk_size         = 20
    disk_type         = "PD_SSD"

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.main.id
    }

    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
      start_time                     = "02:00"
    }

    maintenance_window {
      day          = 7  # Sunday
      hour         = 3  # 3 AM
      update_track = "stable"
    }

    database_flags {
      name  = "log_checkpoints"
      value = "on"
    }
  }

  deletion_protection = false
}

resource "google_sql_database" "app" {
  name     = "appdb"
  instance = google_sql_database_instance.main.name
}

resource "google_sql_user" "app" {
  name     = "appuser"
  instance = google_sql_database_instance.main.name
  password = var.db_password
}

Step 5: Load Balancer

# loadbalancer.tf

# Backend service
resource "google_compute_backend_service" "web" {
  name                  = "web-backend"
  protocol              = "HTTP"
  port_name             = "http"
  timeout_sec           = 30
  health_checks         = [google_compute_health_check.web.id]
  load_balancing_scheme = "EXTERNAL"

  backend {
    group           = google_compute_region_instance_group_manager.web.instance_group
    balancing_mode  = "UTILIZATION"
    max_utilization = 0.8
  }
}

# URL map
resource "google_compute_url_map" "web" {
  name            = "web-url-map"
  default_service = google_compute_backend_service.web.id
}

# HTTP proxy
resource "google_compute_target_http_proxy" "web" {
  name    = "web-http-proxy"
  url_map = google_compute_url_map.web.id
}

# Global forwarding rule
resource "google_compute_global_forwarding_rule" "web" {
  name       = "web-forwarding-rule"
  target     = google_compute_target_http_proxy.web.id
  port_range = "80"
}

Step 6: Outputs

# outputs.tf
output "load_balancer_ip" {
  value = google_compute_global_forwarding_rule.web.ip_address
}

output "db_private_ip" {
  value = google_sql_database_instance.main.private_ip_address
}

output "mig_instance_group" {
  value = google_compute_region_instance_group_manager.web.instance_group
}

Step 7: Deploy and Verify

$ terraform init
Terraform has been successfully initialized!

$ terraform plan -var="project_id=my-project" -var="db_password=SecureP@ss123"
Plan: 18 to add, 0 to change, 0 to destroy.

$ terraform apply -var="project_id=my-project" -var="db_password=SecureP@ss123" -auto-approve
Apply complete! Resources: 18 added.

Outputs:
  load_balancer_ip  = "34.120.0.50"
  db_private_ip     = "10.0.2.5"

# Test (wait a few minutes for health checks)
$ curl http://34.120.0.50/health
{"status": "healthy"}

Clean Up

$ terraform destroy -var="project_id=my-project" -var="db_password=SecureP@ss123" -auto-approve
Destroy complete! Resources: 18 destroyed.

Key Learnings

  • Terraform for complete 3-tier infrastructure
  • Private networking with Cloud SQL via VPC peering
  • HTTP(S) Load Balancer with backend service and health checks
  • MIG with autoscaling and auto-healing
  • Separation of network, compute, and database layers

Next → Monitoring & Alerting System

Updated: