GCP for DevOps — Project: Production VPC with Terraform
Project 1: Production VPC with Terraform
Build a production-ready VPC network using Terraform, complete with multiple subnets, firewall rules, Cloud NAT, and Cloud Router. This project demonstrates Infrastructure as Code best practices with GCP networking.
Architecture
Production VPC Architecture:
┌─────────────────────────────────────────────────────────┐
│ VPC: production-vpc │
│ │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ Public Subnet │ │ Private Subnet │ │
│ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │
│ │ us-central1 │ │ us-central1 │ │
│ │ │ │ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ Bastion │ │ │ │ App VMs │ │ │
│ │ │ Host │ │ │ │ (no ext │ │ │
│ │ └──────────┘ │ │ │ IP) │ │ │
│ └────────────────────┘ │ └──────────┘ │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ Cloud NAT │ │
│ │ (via Cloud Router) │ │
│ └─────────┬──────────┘ │
│ │ │
│ Internet │
└─────────────────────────────────────────────────────────┘
Step 1: Project Structure
gcp-production-vpc/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
├── backend.tf
└── modules/
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf
Step 2: Configure Remote State 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 = "vpc/production"
}
}
provider "google" {
project = var.project_id
region = var.region
}
Create the state bucket first:
$ gcloud storage buckets create gs://my-project-terraform-state \
--location=us-central1 \
--uniform-bucket-level-access
Creating gs://my-project-terraform-state/...
$ gcloud storage buckets update gs://my-project-terraform-state \
--versioning
Updating gs://my-project-terraform-state/...
Step 3: VPC Module
# modules/vpc/variables.tf
variable "project_id" {
description = "GCP project ID"
type = string
}
variable "region" {
description = "GCP region"
type = string
}
variable "vpc_name" {
description = "Name of the VPC"
type = string
}
variable "public_subnet_cidr" {
description = "CIDR for the public subnet"
type = string
}
variable "private_subnet_cidr" {
description = "CIDR for the private subnet"
type = string
}
# modules/vpc/main.tf
resource "google_compute_network" "vpc" {
name = var.vpc_name
auto_create_subnetworks = false
project = var.project_id
}
resource "google_compute_subnetwork" "public" {
name = "${var.vpc_name}-public"
ip_cidr_range = var.public_subnet_cidr
region = var.region
network = google_compute_network.vpc.id
log_config {
aggregation_interval = "INTERVAL_5_SEC"
flow_sampling = 0.5
metadata = "INCLUDE_ALL_METADATA"
}
}
resource "google_compute_subnetwork" "private" {
name = "${var.vpc_name}-private"
ip_cidr_range = var.private_subnet_cidr
region = var.region
network = google_compute_network.vpc.id
private_ip_google_access = true
log_config {
aggregation_interval = "INTERVAL_5_SEC"
flow_sampling = 0.5
metadata = "INCLUDE_ALL_METADATA"
}
}
# Cloud Router for NAT
resource "google_compute_router" "router" {
name = "${var.vpc_name}-router"
region = var.region
network = google_compute_network.vpc.id
}
# Cloud NAT for private subnet outbound access
resource "google_compute_router_nat" "nat" {
name = "${var.vpc_name}-nat"
router = google_compute_router.router.name
region = var.region
nat_ip_allocate_option = "AUTO_ONLY"
source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"
subnetwork {
name = google_compute_subnetwork.private.id
source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
}
log_config {
enable = true
filter = "ERRORS_ONLY"
}
}
# Firewall: Allow SSH to bastion only
resource "google_compute_firewall" "allow_ssh_bastion" {
name = "${var.vpc_name}-allow-ssh-bastion"
network = google_compute_network.vpc.id
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["bastion"]
}
# Firewall: Allow internal communication
resource "google_compute_firewall" "allow_internal" {
name = "${var.vpc_name}-allow-internal"
network = google_compute_network.vpc.id
allow {
protocol = "tcp"
ports = ["0-65535"]
}
allow {
protocol = "udp"
ports = ["0-65535"]
}
allow {
protocol = "icmp"
}
source_ranges = [var.public_subnet_cidr, var.private_subnet_cidr]
}
# Firewall: Deny all ingress (low priority)
resource "google_compute_firewall" "deny_all_ingress" {
name = "${var.vpc_name}-deny-all"
network = google_compute_network.vpc.id
priority = 65534
deny {
protocol = "all"
}
source_ranges = ["0.0.0.0/0"]
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = google_compute_network.vpc.id
}
output "vpc_name" {
value = google_compute_network.vpc.name
}
output "public_subnet_id" {
value = google_compute_subnetwork.public.id
}
output "private_subnet_id" {
value = google_compute_subnetwork.private.id
}
output "router_name" {
value = google_compute_router.router.name
}
Step 4: Root Module
# variables.tf
variable "project_id" {
description = "GCP project ID"
type = string
}
variable "region" {
description = "GCP region"
type = string
default = "us-central1"
}
# main.tf
module "vpc" {
source = "./modules/vpc"
project_id = var.project_id
region = var.region
vpc_name = "production-vpc"
public_subnet_cidr = "10.0.1.0/24"
private_subnet_cidr = "10.0.2.0/24"
}
# Bastion host in public subnet
resource "google_compute_instance" "bastion" {
name = "bastion-host"
machine_type = "e2-micro"
zone = "${var.region}-a"
tags = ["bastion"]
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
subnetwork = module.vpc.public_subnet_id
access_config {} # Gives external IP
}
labels = {
env = "prod"
role = "bastion"
}
}
# outputs.tf
output "vpc_name" {
value = module.vpc.vpc_name
}
output "bastion_external_ip" {
value = google_compute_instance.bastion.network_interface[0].access_config[0].nat_ip
}
Step 5: Deploy
# Initialize Terraform
$ terraform init
Initializing modules...
Initializing the backend...
Terraform has been successfully initialized!
# Plan
$ terraform plan -var="project_id=my-project"
Plan: 8 to add, 0 to change, 0 to destroy.
# Apply
$ terraform apply -var="project_id=my-project" -auto-approve
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
bastion_external_ip = "35.192.0.100"
vpc_name = "production-vpc"
# Verify
$ gcloud compute networks subnets list --network=production-vpc
NAME REGION RANGE
production-vpc-public us-central1 10.0.1.0/24
production-vpc-private us-central1 10.0.2.0/24
Step 6: Verify and Clean Up
# Test SSH to bastion
$ gcloud compute ssh bastion-host --zone=us-central1-a
# Destroy all resources
$ terraform destroy -var="project_id=my-project" -auto-approve
Destroy complete! Resources: 8 destroyed.
Key Learnings
- Terraform modules for reusable VPC configurations
- GCS backend for remote state with versioning
- Cloud NAT for private subnet outbound access
- Layered firewall rules with priority-based deny-all
- Flow logs for network monitoring
Next → GKE Autopilot CI/CD Pipeline