Stage 4 · Provision
Terraform Fundamentals
Module Design
Composition, versioning, registry, and testing modules — building reusable infrastructure components.
What Are Modules?
A module is a container for one or more Terraform resources that are used together. Every Terraform configuration is a module — the root module. You can create child modules to encapsulate common patterns, reduce duplication, and enforce standards across teams.
Modules are the primary mechanism for reuse in Terraform. Instead of copying and pasting resource blocks, you create a module once and call it from multiple configurations. Changes to the module propagate to every caller during the next apply.
Module Structure
A module is a directory containing .tf files. The directory name becomes the module name. The standard convention is to include variables.tf, outputs.tf, and main.tf at minimum.
modules/
vpc/
variables.tf # Input variables
main.tf # Resource definitions
outputs.tf # Output values
versions.tf # Provider and version constraints
README.md # DocumentationThis structure keeps modules self-contained and discoverable. The variables.tf file defines what the caller must provide. The outputs.tf file defines what the module exposes back to the caller.
variable "cidr_block" {
description = "CIDR block for the VPC"
type = string
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "enable_nat_gateway" {
description = "Whether to create a NAT gateway"
type = bool
default = true
}Variables define the module's interface. The validation block rejects invalid inputs before Terraform makes any API calls. The default value makes enable_nat_gateway optional.
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "IDs of private subnets"
value = aws_subnet.private[*].id
}Outputs expose values from the module to the caller. They can be referenced in other modules, passed to other resources, or displayed after apply.
Calling Modules
You call a module using a module block in your root configuration. The source argument tells Terraform where to find the module — a local path, a Git URL, the Terraform Registry, or an HTTP URL.
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.0.0.0/16"
enable_nat_gateway = true
}
# Reference outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.private_subnet_ids[0]
}The module block creates an instance of the module. All outputs from the module are accessible via module.vpc.output_name. The module's resources are namespaced — they become module.vpc.aws_vpc.main in the state file.
Module Registry
The Terraform Registry hosts community and official modules. Private registries (Terraform Cloud, GitLab, GitHub with Terraform) serve modules for internal teams. Registry modules are versioned and discoverable.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
}The source format is <NAMESPACE>/<MODULE>/<PROVIDER>. Version constraints follow the same syntax as provider versions. Always pin to a specific version or use ~> for patch updates.
Module Versioning
Modules should follow semantic versioning. The version constraint determines which releases are acceptable. The ~> operator is the most common constraint — it allows patch and minor updates but blocks major versions.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0" # Allows 5.x.x, blocks 6.0.0
}
module "eks" {
source = "./modules/eks"
version = "1.2.3" # Exact version — no updates
}The ~> operator on a two-part constraint (5.0) allows any 5.x.y release. On a three-part constraint (1.2.3), it allows 1.2.x but blocks 1.3.0. This gives you safe auto-updates within the range you define.
The .terraform.lock.hcl file locks provider and module versions. Commit it to version control to ensure everyone uses the same versions. Run terraform init -upgrade to update the lock file.
Module Testing
Modules need tests to verify correctness. Terratest is the standard Go-based testing framework. It provisions real infrastructure, validates it, then tears it down.
Terratest provisions real infrastructure. Always run tests in a dedicated test account and ensure defer terraform.Destroy is present. A missing destroy call leaves infrastructure running and billing.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.