DEV Community

Cover image for Terraform Modules for Reusable GCP Infrastructure (With a Real VPC Module Example)
Kamal Rhrabla
Kamal Rhrabla

Posted on

Terraform Modules for Reusable GCP Infrastructure (With a Real VPC Module Example)

Terraform modules are the best way to stop copy/pasting infrastructure code across projects and environments. Instead of rewriting the same VPC, subnets, firewall rules, and settings over and over, you wrap that logic in a module and reuse it with clean inputs/outputs.

This post shows:

  • What a module is (in practice)
  • How to structure modules in a repo
  • How to write a reusable GCP VPC module
  • How to call it for dev and prod
  • Common module mistakes (and how to avoid them)

What is a Terraform module?

A Terraform module is just a folder containing Terraform files (.tf) that expose:

  • inputs (variables.tf)
  • outputs (outputs.tf)
  • resources (main.tf, etc.)

You can call a module multiple times with different parameters, like:

module "network_dev" {
  source     = "../modules/vpc"
  project_id = "my-dev-project"
  vpc_name   = "dev-vpc"
}
Enter fullscreen mode Exit fullscreen mode

Recommended repository structure

Here’s a clean layout that works well for real teams:

terraform/
  modules/
    vpc/
      main.tf
      variables.tf
      outputs.tf
      versions.tf
      README.md
  envs/
    dev/
      main.tf
      providers.tf
      backend.tf
      terraform.tfvars
    prod/
      main.tf
      providers.tf
      backend.tf
      terraform.tfvars
Enter fullscreen mode Exit fullscreen mode

Idea: keep modules generic and environments thin.


Module example: a reusable GCP VPC module

This VPC module will create:

  • 1 VPC network
  • N subnets (from a list)
  • Optional firewall rules (basic example)

modules/vpc/versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 5.0.0"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

modules/vpc/variables.tf

variable "project_id" {
  description = "GCP project ID where resources will be created."
  type        = string
}

variable "vpc_name" {
  description = "Name of the VPC."
  type        = string
}

variable "routing_mode" {
  description = "VPC routing mode: REGIONAL or GLOBAL."
  type        = string
  default     = "REGIONAL"

  validation {
    condition     = contains(["REGIONAL", "GLOBAL"], var.routing_mode)
    error_message = "routing_mode must be REGIONAL or GLOBAL."
  }
}

variable "subnets" {
  description = <<EOT
List of subnets to create.

Example:
[
  { name="public-us-central1", region="us-central1", cidr="10.10.0.0/24", private_google_access=true },
  { name="private-us-central1", region="us-central1", cidr="10.10.1.0/24", private_google_access=true }
]
EOT

  type = list(object({
    name                  = string
    region                = string
    cidr                  = string
    private_google_access = bool
  }))
}

variable "firewall_rules" {
  description = <<EOT
Optional firewall rules.
Keep it simple: allow protocol/ports from source ranges to target tags.
EOT

  type = list(object({
    name          = string
    direction     = optional(string, "INGRESS")
    priority      = optional(number, 1000)
    source_ranges = list(string)
    target_tags   = list(string)
    protocol      = string
    ports         = list(string)
  }))

  default = []
}
Enter fullscreen mode Exit fullscreen mode

modules/vpc/main.tf

resource "google_compute_network" "this" {
  project                 = var.project_id
  name                    = var.vpc_name
  auto_create_subnetworks = false
  routing_mode            = var.routing_mode
}

resource "google_compute_subnetwork" "this" {
  for_each = { for s in var.subnets : s.name => s }

  project                  = var.project_id
  name                     = each.value.name
  ip_cidr_range            = each.value.cidr
  region                   = each.value.region
  network                  = google_compute_network.this.id
  private_ip_google_access = each.value.private_google_access
}

resource "google_compute_firewall" "this" {
  for_each = { for r in var.firewall_rules : r.name => r }

  project     = var.project_id
  name        = each.value.name
  network     = google_compute_network.this.name
  direction   = each.value.direction
  priority    = each.value.priority
  target_tags = each.value.target_tags

  source_ranges = each.value.source_ranges

  allow {
    protocol = each.value.protocol
    ports    = each.value.ports
  }
}
Enter fullscreen mode Exit fullscreen mode

modules/vpc/outputs.tf

output "network_id" {
  description = "The ID of the VPC network."
  value       = google_compute_network.this.id
}

output "network_name" {
  description = "The name of the VPC network."
  value       = google_compute_network.this.name
}

output "subnet_ids" {
  description = "Subnet IDs keyed by subnet name."
  value       = { for k, s in google_compute_subnetwork.this : k => s.id }
}
Enter fullscreen mode Exit fullscreen mode

Using the module: dev environment example

envs/dev/main.tf

module "vpc" {
  source     = "../../modules/vpc"
  project_id = var.project_id
  vpc_name   = "dev-vpc"

  subnets = [
    {
      name                  = "dev-us-central1-public"
      region                = "us-central1"
      cidr                  = "10.10.0.0/24"
      private_google_access = true
    },
    {
      name                  = "dev-us-central1-private"
      region                = "us-central1"
      cidr                  = "10.10.1.0/24"
      private_google_access = true
    }
  ]

  firewall_rules = [
    {
      name          = "dev-allow-ssh-from-office"
      source_ranges = ["203.0.113.10/32"]
      target_tags   = ["ssh"]
      protocol      = "tcp"
      ports         = ["22"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

envs/dev/variables.tf

variable "project_id" {
  type        = string
  description = "Dev project ID"
}
Enter fullscreen mode Exit fullscreen mode

Module tips (this is what saves you later)

1) Keep modules “dumb” and input-driven

Avoid hardcoding:

  • regions
  • CIDRs
  • environment names
  • project IDs

2) Prefer composable small modules

Instead of a huge “platform module”, consider:

  • modules/vpc
  • modules/cloud_run
  • modules/cloud_sql
  • modules/iam

Then compose them per environment.

3) Be careful with IAM resources

Some IAM resources are authoritative and can remove access unexpectedly.
When in doubt, start with *_iam_member resources and grow from there.

4) Version your modules

If you publish modules (Git tags), you can pin versions:

module "vpc" {
  source = "git::https://github.com/YOUR_ORG/YOUR_REPO.git//modules/vpc?ref=v1.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Wrap-up

Terraform modules turn your GCP infrastructure into reusable building blocks:

  • less duplication
  • safer changes
  • consistent environments

Top comments (0)