Photo by PJH on Unsplash

On this page

This chapter follows Formatting and validating Terraform files. Modules are how teams share patterns—a standard VPC, a Cloud Storage bucket with logging, or an IAM layout—without copy-pasting hundreds of lines.

Root module versus child modules

Every Terraform working directory is a module. The configuration you run terraform apply in is the root module. A child module is a module called from another module via a module block.

Child modules declare input variables (their public API) and outputs (values the parent can use). Resources inside a module are namespaced: module.network.vpc_id, not a bare resource name at the root.

Keep root modules thin: environment-specific tfvars, backend, and calls into shared child modules.

Writing a child module

A minimal GCS module might look like this:

modules/log-bucket/variables.tf
variable "project_id" {
type = string
}
variable "name_suffix" {
type = string
}
variable "location" {
type = string
}
modules/log-bucket/main.tf
resource "google_storage_bucket" "this" {
name = "${var.project_id}-${var.name_suffix}"
location = var.location
uniform_bucket_level_access = true
}
modules/log-bucket/outputs.tf
output "bucket_name" {
value = google_storage_bucket.this.name
}

The module directory contains its own .tf files; it does not need a separate terraform block unless you pin providers inside the module (often providers are configured only in the root).

Calling a module

From the root module:

main.tf
module "logs" {
source = "./modules/log-bucket"
project_id = var.project_id
name_suffix = "app-logs"
location = var.region
}
output "log_bucket" {
value = module.logs.bucket_name
}

Run terraform init after adding or changing source so Terraform installs the module.

Module sources

Source form Example Typical use
Local path source = "./modules/network" Monorepo modules under development
Git source = "git::https://example.com/terraform/network.git?ref=v1.2.0" Shared modules with tag pins
Registry source = "terraform-google-modules/cloud-storage/google" Curated, versioned public modules

Registry modules require a version constraint:

module "cloud_storage" {
source = "terraform-google-modules/cloud-storage/google"
version = "~> 6.0"
names = ["foo"]
project_id = var.project_id
location = var.region
}

Pin versions explicitly. Floating to latest Registry releases breaks CI when upstream introduces breaking changes.

Versioning and upgrades

Tag semantic versions on internal git modules (v1.0.0) and reference them with ?ref=v1.0.0. After upgrading a module version, run terraform init -upgrade to refresh the module cache, then plan and review diffs carefully—module upgrades can replace resources if argument names changed.

Document breaking changes in module README files and changelog entries so root modules can bump version constraints deliberately.

Refactoring with moved blocks

When you rename a resource or lift configuration into a module, Terraform otherwise plans destroy/create. moved blocks (Terraform 1.1+) retarget state without recreating cloud objects:

moved {
from = google_storage_bucket.logs
to = module.logs.google_storage_bucket.this
}

Apply once with the moved block present, then remove the block after state reflects the new address. Combine with State data in Terraform for import and removed when retiring resources.

Next: State data in Terraform explains backends, locking, drift, and workspaces.

Check your understanding

Question 1 of 6

0 of 6 checked

  1. 1. Which module do you run terraform apply in?

Search articles

Type a word to search published articles.

Open the search page