When you codify an existing AWS account — whether by hand, with an exporter, or with a scanner — you get a flat list of resources. It works, but it doesn't scale: there's no reuse, environments diverge by copy-paste, and the same VPC pattern gets rebuilt five slightly different ways. Modules fix that. The catch is doing the refactor safely, because naively moving a resource's address makes Terraform want to destroy and recreate it. This guide shows the safe path.
- What a module is
- Why modularise existing infrastructure
- Module anatomy
- The moved block: refactor without recreating
- Designing good inputs and outputs
- A staged approach
- FAQ
What a module is
A Terraform module is a reusable, parameterised group of resources, defined with input variables and outputs. Every configuration already has one — the root module. Calling other modules from it lets you package a pattern (a standard VPC, a service, a database) once and instantiate it many times with different inputs.
A module turns "copy these forty lines and tweak them" into "call this module with three inputs." That's the whole value: reuse without divergence.
Why modularise existing infrastructure
- Reuse. Define a network once; instantiate it for dev, staging, and prod.
- Consistency. Environments built from the same module can't quietly drift apart in structure.
- Readability. A root module that calls
network,data, andappmodules is far easier to reason about than 2,000 flat lines. - Smaller blast radius. Combined with separate state per component, modules make changes safer.
Module anatomy
A module is just a directory of .tf files with a conventional layout:
modules/network/
main.tf # the resources
variables.tf # inputs
outputs.tf # values exposed to callers
You call it from your root module and wire its outputs into other resources:
module "network" {
source = "./modules/network"
cidr_block = "10.0.0.0/16"
environment = "prod"
}
resource "aws_instance" "api" {
subnet_id = module.network.private_subnet_id
}
The moved block: refactor without recreating
Here's the part that makes brownfield modularisation safe. When you move a resource from the root module into a child module, its address changes — for example from aws_vpc.main to module.network.aws_vpc.main. By default Terraform reads that as "destroy the old one, create a new one," which is catastrophic for live infrastructure.
The moved block tells Terraform it's the same resource at a new address, so it updates state instead of recreating:
moved {
from = aws_vpc.main
to = module.network.aws_vpc.main
}
The golden check after any refactor:terraform planmust report no changes. If it wants to destroy or create, yourmovedblocks are incomplete — stop and fix them.
Because moved blocks live in code, the refactor is reviewable in a pull request and safe to apply incrementally. (They can be removed later, once the move is applied everywhere it's needed.)
Designing good inputs and outputs
A module is only reusable if its interface is well chosen:
- Inputs: expose what genuinely varies between uses (CIDR, environment, instance size) and give sensible defaults to the rest. Don't expose everything — each variable is a maintenance commitment.
- Outputs: publish the values other modules need (IDs, ARNs, endpoints). These are how modules compose.
- Validation: add
validationblocks to inputs to catch bad values early. - Keep modules focused. A module should do one thing well; a "module" that contains your whole account isn't a module, it's the root by another name.
A staged approach
- Land the flat baseline first. Get generated HCL into Git with a clean no-op plan before refactoring. Don't modularise and codify in the same step.
- Extract one pattern. Start with the most-repeated one — usually networking. Create the module, add
movedblocks, confirm a no-op plan, merge. - Parameterise. Once the module exists, replace hard-coded values with variables so it can serve more than one environment.
- Repeat outward. Extract the next pattern. Each step is small, reviewed, and proven by a no-op plan.
This pairs naturally with codification: first get the account into Terraform, then improve its shape. Our AWS-to-Terraform guide and ClickOps migration playbook cover getting the baseline in; state management keeps it healthy while you refactor.
InfraSync generates the flat, accurate baseline from your live AWS account — with references already resolved between resources — which is exactly the clean starting point you want before extracting modules.
Get a clean baseline worth modularising.
InfraSync scans your live AWS account and generates accurate Terraform with resolved references — the flat baseline you refactor into modules. Read-only, first PR in minutes.
Start a free scanFAQ
What is a Terraform module?
A Terraform module is a reusable, parameterised group of resources defined together with input variables and outputs. The top level of any configuration is the root module; modules you call from it let you package a pattern, such as a standard VPC or service, and reuse it with different inputs.
How do I move resources into a module without recreating them?
Use the moved block. When you relocate a resource's address into a module, a moved block tells Terraform the old address and the new one so it updates state instead of destroying and recreating the resource. After applying, terraform plan should report no changes.
Should I modularise generated Terraform right away?
Not necessarily on day one. It's often better to get flat, generated HCL into version control with a clean no-op plan first, then refactor into modules incrementally using moved blocks. Modularising too early, before the baseline is stable, adds risk without benefit.