Skip to main content
RunBook Academy

← All labs in Terraform

Lab · intermediate · ~25 min

Lab: Modules — Build, Call, Refactor

C · Simulation

Objectives

  • Create a module with a useful interface
  • Call the module from a root configuration
  • Refactor the module call to use module variables
  • Verify the refactor does not destroy the real-world resource

Prerequisites

Objective

By the end of this lab, you will have:

  • Created a module with a clear interface.
  • Called the module from a root configuration.
  • Refactored the module call to use module inputs.
  • Verified the refactor does not destroy the real-world resource.

Requirements

  • A Linux or macOS workstation with shell access.
  • The Terraform CLI 1.9.x or later installed.

Scenario

You have a configuration that creates a file. The configuration has grown. The next refactor is to extract the file into a module so the same pattern can be reused.

Tasks

Task 1: Create the working directory

mkdir -p ~/rb-modules-lab/root
mkdir -p ~/rb-modules-lab/modules/greeting
cd ~/rb-modules-lab

Task 2: Create the module

Create modules/greeting/variables.tf:

variable "filename" {
  type        = string
  description = "The path to the greeting file."
}

variable "content" {
  type        = string
  description = "The content of the greeting file."
  default     = "Hello from Terraform!\n"
}

Create modules/greeting/main.tf:

terraform {
  required_version = ">= 1.9.0"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

resource "local_file" "greeting" {
  filename = var.filename
  content  = var.content
}

output "filename" {
  value       = local_file.greeting.filename
  description = "The path to the greeting file."
}

output "size" {
  value       = local_file.greeting.content_length
  description = "The size of the greeting file in bytes."
}

Create modules/greeting/outputs.tf:

output "filename" {
  value       = local_file.greeting.filename
  description = "The path to the greeting file."
}

output "size" {
  value       = local_file.greeting.content_length
  description = "The size of the greeting file in bytes."
}

Task 3: Create the root configuration

Create root/main.tf:

terraform {
  required_version = ">= 1.9.0"
}

module "greeting" {
  source = "../modules/greeting"

  filename = "${path.module}/greeting.txt"
  content  = "Hello from the modules lab!\n"
}

Create root/outputs.tf:

output "greeting_file" {
  value = module.greeting.filename
}

output "greeting_size" {
  value = module.greeting.size
}

Task 4: Initialise and apply

cd ~/rb-modules-lab/root
terraform init
terraform apply

Verify:

cat ~/rb-modules-lab/root/greeting.txt

The file is created.

Task 5: Inspect the state

terraform state list

Expected:

module.greeting.local_file.greeting

The state has the module resource.

Task 6: Refactor the root to use a variable

Edit root/main.tf:

terraform {
  required_version = ">= 1.9.0"
}

variable "greeting_content" {
  type    = string
  default = "Hello from the modules lab!\n"
}

module "greeting" {
  source = "../modules/greeting"

  filename = "${path.module}/greeting.txt"
  content  = var.greeting_content
}

Run the plan:

terraform plan

The plan may or may not propose changes. If the content is the same, the plan is empty. If the content is different, the plan proposes to update the file.

Task 7: Move the module to a deeper path

Suppose the module is renamed from greeting to hello. This is the production test of module refactoring.

Edit root/main.tf:

module "hello" {
  source = "../modules/greeting"

  filename = "${path.module}/greeting.txt"
  content  = var.greeting_content
}

Add a moved block:

moved {
  from = module.greeting
  to   = module.hello
}

module "hello" {
  source = "../modules/greeting"

  filename = "${path.module}/greeting.txt"
  content  = var.greeting_content
}

Run the plan:

terraform plan

The plan is empty (or close to empty). The state has been moved from module.greeting.local_file.greeting to module.hello.local_file.greeting.

Task 8: Verify the state

terraform state list

The state has the new module path.

Task 9: Verify the file is unchanged

cat ~/rb-modules-lab/root/greeting.txt

The file content is unchanged.

Task 10: Remove the moved block

The moved block is one-shot. After the refactor, it can be removed.

Validation

The lab is successful if:

  • The module was created with a clear interface.
  • The root configuration called the module.
  • The refactor renamed the module without destroying the real-world resource.
  • The plan is empty after the refactor.

Expected Outcome

At the end of the lab:

+---------------------------------+
| ~/rb-modules-lab/                  |
|   modules/                        |
|   └── greeting/                   |
|       ├── main.tf                  |
|       ├── outputs.tf               |
|       └── variables.tf             |
|   root/                           |
|       ├── greeting.txt             |
|       └── main.tf                  |
+---------------------------------+

The greeting.txt file is created. The state has the new module path. The plan is empty.

Cleanup

cd ~/rb-modules-lab/root
terraform destroy
rm -rf .terraform .terraform.lock.hcl terraform.tfstate*

The module and root configurations are the artefacts worth keeping.

What You Learned

You learned the module pattern:

  1. The module has a clear interface. Inputs are variables; outputs are the results.
  2. The root configuration calls the module. The modules interface is the contract.
  3. The moved block preserves the state. Renaming a module does not destroy the real-world resources.
  4. The plan is empty after the refactor. The refactor is invisible to the apply.

Deliverables

  • · A module with a clear interface
  • · A root configuration that calls the module
  • · A refactored configuration that uses module inputs
  • · An empty plan after the refactor

Verification status

Last reviewed
2026-08-12
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.