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:
variable "project_id" { type = string}
variable "name_suffix" { type = string}
variable "location" { type = string}resource "google_storage_bucket" "this" { name = "${var.project_id}-${var.name_suffix}" location = var.location
uniform_bucket_level_access = true}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:
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