Skip to main content
RunBook Academy

← All labs in Terraform

Lab · advanced · ~30 min

Lab: Modules and Refactoring with moved Blocks

C · Simulation

Objectives

  • Create a module with inputs and outputs
  • Call the module from a root configuration
  • Refactor a resource inside the module (rename, move, restructure)
  • Use moved blocks to preserve state without recreating resources
  • Verify the refactor with terraform plan (no changes)

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 a resource inside the module (rename or move).
  • Used a moved block to preserve state.
  • Verified the refactor with a plan that is empty.

The moved block is the under-used feature that allows refactoring without recreating resources. The lab demonstrates the pattern.

Architecture

A module with one resource, refactored across two scenarios:

+--------------------------------+
| Root module (root/)            |
|   ↓                             |
| module "greeting"               |
|   ↓                             |
| modules/greeting/main.tf        |
|   random_pet.name (initial)     |
|   local_file.greeting           |
|   ↓                             |
| ~/rb-modules-lab/                |
|   ├── root/                     |
|   │   └── main.tf               |
|   └── modules/                  |
|       └── greeting/             |
|           ├── main.tf           |
|           └── variables.tf      |
+--------------------------------+

The lab refactors the module by renaming the resource and using a moved block to preserve state.

Requirements

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

Scenario

You maintain a Terraform configuration with a module that creates a greeting file. You want to refactor the modules internal naming to make the module more reusable. The refactor must not destroy the existing file.

Tasks

Task 1: Create the working directory

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

Task 2: Write the modules variables

Create modules/greeting/variables.tf:

variable "name_length" {
  type        = number
  description = "The number of words in the random name."
  default     = 1
}

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

variable "content_template" {
  type        = string
  description = "The content template, with ${name} replaced by the random name."
  default     = "Hello, ${name}!\n"
}

Task 3: Write the modules main configuration

Create modules/greeting/main.tf:

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

resource "random_pet" "name" {
  length = var.name_length
}

resource "local_file" "greeting" {
  filename = var.filename
  content  = replace(var.content_template, "${name}", random_pet.name.id)
}

output "name" {
  value       = random_pet.name.id
  description = "The generated random name."
}

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

The module:

  • Takes a filename and a content template.
  • Generates a random name.
  • Creates the file.

The modules outputs are the random name and the filename.

Task 4: Write the root configuration

Create root/main.tf:

terraform {
  required_version = ">= 1.9.0"
}

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

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

The root module calls the module with a relative path.

Task 5: Initialise and apply

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

Verify the file:

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

The file was created. The state has the modules resources.

Inspect the state:

terraform state list

Expected:

module.greeting.random_pet.name
module.greeting.local_file.greeting

The state has the modules resources, addressed by the module path.

terraform state show module.greeting.local_file.greeting

The state shows the attributes.

Task 6: Verify the empty plan

terraform plan

Expected:

No changes. Your infrastructure matches the configuration.

The plan is empty.

Task 7: Scenario A — Rename the resource without a moved block

Edit modules/greeting/main.tf to rename the resource:

# Rename random_pet.name to random_pet.random_name
resource "random_pet" "random_name" {
  length = var.name_length
}

# Update the reference
resource "local_file" "greeting" {
  filename = var.filename
  content  = replace(var.content_template, "${name}", random_pet.random_name.id)
}

Run the plan from the root:

cd ~/rb-modules-lab/root
terraform plan

Question A.1: What does the plan show?

Answer

The plan shows a destroy + create:

# module.greeting.random_pet.name will be destroyed
- resource "random_pet" "name" {
  ...
}

# module.greeting.random_pet.random_name will be created
+ resource "random_pet" "random_name" {
  ...
}

Plan: 1 to add, 0 to change, 1 to destroy.

The renamed resource is treated as a new resource. The old resource is destroyed. The new resource is created.

The destruction is a “soft” destroy for random_pet (the resource is just a name in state), but the same pattern for a real-world resource would destroy and recreate the resource.

This is the wrong outcome. The refactor should not destroy the resource.

Task 8: Add a moved block to preserve the resource

Revert the rename (so the resource is back to name):

# Back to the original
resource "random_pet" "name" {
  length = var.name_length
}

resource "local_file" "greeting" {
  filename = var.filename
  content  = replace(var.content_template, "${name}", random_pet.name.id)
}

Now apply the rename with a moved block:

# modules/greeting/main.tf
moved {
  from = random_pet.name
  to   = random_pet.random_name
}

resource "random_pet" "random_name" {
  length = var.name_length
}

Run the plan:

cd ~/rb-modules-lab/root
terraform plan

Question A.2: What does the plan show?

Answer

The plan is empty:

No changes. Your infrastructure matches the configuration.

The moved block tells Terraform that the resource at the from address has been moved to the to address. Terraform updates the state to reflect the new address. The state is preserved.

Verify the state:

terraform state list

Expected:

module.greeting.random_pet.random_name
module.greeting.local_file.greeting

The state has the new address.

terraform state show module.greeting.random_pet.random_name

The state shows the attributes. The attributes are preserved from the original resource.

Task 9: Scenario B — Move a resource into a submodule

The module currently has the random_pet and local_file resources. The next refactor moves the local_file into a sub-module.

Create modules/greeting/file/main.tf:

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

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

variable "content" {
  type        = string
  description = "The content of the file."
}

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

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

Edit modules/greeting/main.tf to delegate to the submodule:

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

moved {
  from = random_pet.name
  to   = random_pet.random_name
}

resource "random_pet" "random_name" {
  length = var.name_length
}

module "file" {
  source = "./file"

  filename = var.filename
  content  = replace(var.content_template, "${name}", random_pet.random_name.id)
}

output "name" {
  value       = random_pet.random_name.id
  description = "The generated random name."
}

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

Run the plan:

cd ~/rb-modules-lab/root
terraform plan

Question B.1: What does the plan show?

Answer

The plan shows the resource moved into the submodule:

# module.greeting.local_file.greeting has moved to module.greeting.module.file.local_file.file

The plan is otherwise empty. The state is updated to reflect the new address.

Verify the state:

terraform state list

Expected:

module.greeting.module.file.local_file.file
module.greeting.random_pet.random_name

The state has the new addresses.

Task 10: Verify file integrity

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

The file content is unchanged. The refactor preserved the real-world resource.

Validation

The lab is successful if:

  • The module was created with a clear interface.
  • The root module called the module.
  • The rename was done without destroying the resource.
  • The state was preserved through the refactor.
  • The plan was empty after each refactor.

Expected Outcome

At the end of the lab:

+---------------------------------+
| ~/rb-modules-lab/                |
|   .terraform/                    |
|   .terraform.lock.hcl            |
|   root/                          |
|   │   greeting.txt               |
|   │   main.tf                    |
|   └── modules/                   |
|       └── greeting/              |
|           ├── main.tf            |
|           ├── variables.tf       |
|           └── file/              |
|               └── main.tf        |
+---------------------------------+

The greeting.txt file was created by the module. The state has the modules resources, addressed by the module path.

Cleanup

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

What You Learned

You learned the refactoring workflow:

  1. Always use a moved block when renaming a resource or moving it within a module hierarchy.
  2. A refactor without a moved block destroys the resource. The plan will show the destroy + create.
  3. A moved block tells Terraform that the resource at the from address has been moved to the to address. The state is updated; the plan is empty.
  4. Verify the plan is empty after every refactor. A non-empty plan is evidence of a problem.
  5. A wrong moved block can corrupt state. Verify the from and to addresses carefully.

Deliverables

  • · A module with a clear interface
  • · A root configuration that calls the module
  • · A refactored module that uses moved blocks
  • · A 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.