GCP for DevOps — Project: Terraform 3-Tier Architecture
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