Courseiva
TF-003Chapter 11 of 15Objective 5.2

Advanced Module Practices: Versioning and Composition

Module versioning and composition are the practices that allow you to reliably reuse and combine Terraform configuration building-blocks (called modules) across different projects. This matters for your TF-003 exam because the most efficient way to manage infrastructure at scale is to avoid reinventing the wheel, and the exam heavily tests your knowledge of how to select, use, and share these pre-built components safely.

12 min read
Intermediate
Updated Jul 23, 2026
Editorial oversight: Johnson Ajibi· Security Systems Architect · MSc IT Security · IEEE Senior Member

A simple way to picture Advanced Module Practices: Versioning and Composition

The Cookbook Recipe Version and Meal Planning Analogy

Your kitchen, a Sunday afternoon. You are baking a complex three-layer cake for a friend's birthday. You find the recipe online, but it's from a blog that has been updated many times. The first time you baked this cake, you used the '1.0' version of the recipe, which called for buttermilk. You saved that version because it was perfect.

Now, the blog has a '2.0' version. It swaps buttermilk for yoghurt to make the cake lighter. You don't want a lighter cake; you want the dense, rich version you made before. So, instead of clicking the latest post, you specifically look for the archived '1.0' recipe page. You are performing version pinning.

Meanwhile, your friend is also making a side dish, a basil cream. You don't need to invent that recipe yourself. You pull it from a different, well-known cooking website. That basil cream recipe is its own independent component. You combine it with your cake recipe to create a full meal. This is module composition. You didn't write the basil cream instruction from scratch; you reused it. You are also sharing your complete meal plan, including the specific versions of both recipes used, so your friend can replicate the exact taste. If they just grab the latest 'basil cream' recipe without checking the version, their cream might be too runny, and the whole meal is ruined. Versioning and composition ensure your entire meal—your infrastructure—is predictable and reproducible every single time.

How It Actually Works

In Terraform, a module is a container for multiple resources that are used together. Think of it as a pre-packaged set of instructions to create a specific piece of infrastructure, like a virtual network or a database server. Just like software code, these modules change over time—they get bug fixes, new features, or breaking changes.

Versioning is the system of assigning unique identifiers (like '1.0.0' or '2.1.3') to each iteration of a module. This is called Semantic Versioning (SemVer). The format is MAJOR.MINOR.PATCH. A MAJOR version change (1.0.0 to 2.0.0) means the module has changes that will break your existing configuration if you update without adjusting your code. A MINOR version change (1.0.0 to 1.1.0) adds new features but should not break existing functionality. A PATCH version change (1.0.0 to 1.0.1) is for backwards-compatible bug fixes.

When you use a module in your Terraform code, you specify a source (where to find the module, e.g., the Terraform Registry, GitHub, or a local path) and a version constraint. A version constraint is a rule you write in your code to tell Terraform which versions of the module you are willing to accept. Common constraints include:

'>= 1.0.0' (any version at or above 1.0.0)

'~> 1.0.0' (only PATCH-level updates within the 1.0.x series, e.g., 1.0.1 is allowed, but 1.1.0 is not)

'= 1.0.0' (exact version lock, you only want that specific version)

If you do not specify a version constraint, Terraform will fetch the latest version available from the source, which can lead to unexpected changes in your infrastructure when the module author publishes a new version. This is called 'version drift' and is a common cause of production incidents.

Composition is the practice of building larger, more complex infrastructure by combining multiple smaller, focused modules. Instead of writing one gigantic module that provisions a web server, a database, and a load balancer all tangled together, you create separate modules for each component: a 'web-server' module, a 'database' module, and a 'load-balancer' module. You then create a 'root module' (your main Terraform configuration) that calls these three modules, passing in the correct configuration values (called input variables) for each. This is the composition pattern.

The Terraform Registry is a public, centralised repository (like an app store for modules) where module authors publish their work. Modules on the Registry have enforced versioning and documentation. When a module is published to the Registry, the Registry enforces Semantic Versioning rules. You can browse the Registry to find a module that already solves your problem, rather than writing everything from scratch. This is the sharing aspect of the objective.

Best practices for using modules include:

Always specify a version constraint for any module you source from a remote location like the Registry or a Git repository.

Prefer using the '= MAJOR.MINOR.PATCH' constraint in production to lock to an exact version, and only upgrade after testing in a development environment.

Keep modules small and focused on a single responsibility (one module for a virtual machine, another for the network security group).

Use the Terraform Registry as your primary source for well-maintained, community-vetted modules.

Design your own modules to accept input variables for customisation and to output useful information (like the IP address of the created server) for other parts of your composition to consume.

Shows how a root module composes three separate modules, with outputs from one module feeding into inputs of another.

Walk-Through

1

Define the module source

In your root module's main.tf, you write a 'module' block and set the 'source' argument to the location of the module, e.g., 'terraform-aws-modules/vpc/aws' for a Registry module, or 'git::https://github.com/example/vpc.git' for a Git repository.

2

Add a version constraint

Inside the same 'module' block, add a 'version' argument with a constraint string like '"= 3.0.0"' to lock to a specific version. This prevents Terraform from updating the module without your explicit change to the constraint.

3

Configure input variables

Look up the module's documentation to see what input variables it accepts (e.g., 'cidr_block', 'region'). Then, in your root module, assign values to those variables inside the 'module' block, often using references to other resources or variables you've defined.

4

Use outputs for composition

After defining the module, you can reference its output values elsewhere in your root module. For example, if the VPC module outputs 'vpc_id', you can use 'module.vpc.vpc_id' as an input to another module, like a security group module.

5

Run terraform init

Execute 'terraform init' to download the specified version of the module to your local .terraform directory. Terraform will verify the version constraint against what is available in the source. If a matching version is found, it caches it. If not, it gives an error.

6

Test and promote version changes

When you want to upgrade a module, you change the version constraint in your development environment, run 'terraform init', and run 'terraform plan' to see the changes. Once validated, you commit the new version constraint to your production configuration, ensuring all team members use the same version.

What This Looks Like on the Job

An IT professional at a mid-sized e-commerce company manages the cloud infrastructure using Terraform. The company has a standard deployment that includes a web server, a database, and a content delivery network (CDN).

Instead of writing the entire configuration for every new product launch, the team has created three modules:

The 'web-server' module (version 2.1.0) - provisions an Amazon EC2 instance with the company's custom application image.

The 'database' module (version 3.0.0) - creates an RDS MySQL database instance with specified storage and backup settings.

The 'cdn' module (version 1.5.2) - sets up a CloudFront distribution in front of the web server.

Step-by-step, what they do:

1.

When the team starts a new project, they create a new root module (a new Terraform directory).

2.

In the main.tf file, they write three module blocks, each referencing the respective module from the Terraform Registry or their private registry.

3.

Crucially, they pin the versions in the root module: source = "terraform-aws-modules/vpc/aws" and version = "= 3.0.0". This prevents Terraform from accidentally downloading version 4.0.0 which might have breaking changes.

4.

They pass input variables (like subnet IDs, database password from a secrets manager, and the application version) from the root module into each child module.

5.

They also use output values from one module as inputs to another. For example, the output of the web-server module (its security group ID) is passed into the load-balancer module to allow traffic.

When the database team publishes an update to the database module (version 3.0.1, a bug fix), the infrastructure team does not immediately see that change in their production deployment because they have explicitly pinned to version = 3.0.0 in their root module. They first create a test environment, update the version constraint to '~> 3.0.0' to pull in the patch, run terraform plan to verify nothing breaks, and only then commit the new constraint to production.

This practice prevents the 'works on my machine' problem across teams. The website team can update the web-server module independently of the database team, and the root module composition glues everything together. If a junior engineer accidentally types 'version = ">= 1.0.0"', a senior engineer would catch this in code review because it violates the team's policy of using exact version pins for production modules.

How TF-003 Actually Tests This

The TF-003 exam tests specific, concrete knowledge about module versioning and composition. It does not ask vague 'best practice' questions. It tests mechanisms.

Key areas you must know:

The syntax for specifying a module source and version. You will be given a module block and asked to select the correct version constraint syntax. The trap is that version constraints use a specific operator ('=', '~>', '>=', '<=', '!=') and the exam will test whether you know the difference between '~> 1.0' and '>= 1.0, < 2.0'.

The format of version constraints in the 'required_version' setting for Terraform itself versus the 'version' argument within a module block. These are two different configuration points.

The behaviour of the '~>' operator. The exam loves this. You must know that '~> 1.0.0' allows only patch updates up to 1.0.x, while '~> 1.0' allows minor updates up to 1.x.

The concept of a 'module composition' and how outputs from one module feed into inputs of another. The exam will present a diagram or a config with two module blocks and ask if the configuration is valid (e.g., can module A's output be used as module B's input?). The correct answer is yes, by using the module A output reference.

The Terraform Registry's role. Know that the Registry enforces Semantic Versioning, provides documentation, and is where public modules are shared.

Common traps the exam sets:

Asking which version constraint allows the most flexibility vs. the most strict. You must know '= 1.0.0' is strictest and '>= 1.0.0' is most flexible.

Offering a version constraint with a missing '=' sign. For example, 'version = "1.0.0"' without an operator is equivalent to '= 1.0.0' (exact match). The exam may present 'version = 1.0.0' (without quotes) as a distracter.

Testing whether you know that you can define modules locally (using a relative path) without a version constraint. Version constraints are only required for remote sources.

Definitions to memorise exactly:

Module composition: the practice of building infrastructure by combining multiple modules.

Version pinning: specifying an exact version to prevent unwanted updates.

Version constraint: a string used to restrict which module versions are acceptable.

Key Takeaways

Always specify a version constraint when using a module from a remote source to prevent unexpected changes during 'terraform init'.

Semantic versioning (MAJOR.MINOR.PATCH) is the standard versioning scheme for modules in the Terraform Registry.

The '~>' operator (pessimistic constraint) allows only the rightmost part of the version to increment, e.g., '~> 1.0' allows 1.x, but '~> 1.0.0' allows only 1.0.x.

Module composition involves creating small, single-purpose modules and combining them in a root module to build complex infrastructure.

Outputs from one module can be referenced as inputs to another module, enabling data flow between components.

The Terraform Registry enforces semantic versioning and provides a public platform for sharing and discovering modules.

Version pinning (using an exact version like '= 1.0.0') is the safest practice for production environments.

You can compose modules from different sources (Registry, GitHub, local paths) in the same root module.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

Version Pinning

Specifies an exact version like '= 2.0.0'

Changes only when you manually edit the constraint

Safer for production as it prevents unexpected upgrades

Version Range

Specifies a range like '>= 2.0.0, < 3.0.0'

Automatically accepts patch releases within the range

Risk of unexpected changes if a new patch introduces a bug

Module (Registry)

Sourced from a remote repository like Terraform Registry

Requires a version constraint for reproducibility

Supports semantic versioning and automatic documentation

Module (Local Path)

Sourced from a relative or absolute file path on your machine

Does not require a version constraint (path is the version)

No built-in version checking; changes are immediate

Module Composition

Multiple small, focused modules combined

Each module can be versioned independently

Easier to test and swap component versions

Monolithic Module

Single large module that does everything

One version for the entire system

Difficult to upgrade just one part without affecting others

Terraform Registry

Requires version constraints to work properly

Registry enforces semantic versioning conventions

More discoverable through search and documentation

GitHub as Module Source

Can use Git branches and tags as versions

No automatic enforcement of versioning conventions

Requires manual documentation and discovery

Watch Out for These

Mistake

If I don't specify a version, Terraform will use the version I already have cached locally.

Correct

If you do not specify a version, Terraform will fetch the latest version from the source every time you run 'terraform init', which can cause infrastructure changes without your consent.

Beginners assume that Terraform is 'lazy' and will reuse what is in the local cache. In reality, without a version constraint, Terraform treats the source as a mutable tag that it checks for updates.

Mistake

Semantic versioning in Terraform modules works the same as in libraries like npm or RubyGems.

Correct

While it follows the same MAJOR.MINOR.PATCH pattern, Terraform's module system is less strict about enforcing SemVer promises. A module author could publish a 'minor' release that actually breaks something, and Terraform will not detect it.

People assume a universal standard. But Terraform Registry does not automatically validate that a minor release is backwards-compatible with your provider configurations. The author can make mistakes.

Mistake

Composition means I should put all my module calls into one big file.

Correct

Composition means logically separating modules into focused components and then calling them from a root module. You can split your root module across multiple files (main.tf, outputs.tf, variables.tf) for organisation, but the 'composition' is the design pattern, not the file structure.

Beginners confuse the concept of 'composition' with the physical organisation of Terraform files. They think putting everything in one file is 'composing' them, but composition is about the architectural relationship of independent modules.

Mistake

You cannot use modules from the Terraform Registry without configuring a remote backend.

Correct

Modules from the Registry can be used with any backend (including local). The Registry is just a source of module code, not a state store.

The exams conflate 'Terraform Registry' with 'remote state' in their study materials, leading beginners to think they are coupled. They are completely independent concepts.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

What happens if I don't specify a version in a module block?

Terraform will fetch the latest version from the source during 'terraform init'. This can introduce breaking changes unexpectedly. It is considered a bad practice.

Can I use a module from the Terraform Registry without creating an account?

Yes, the Terraform Registry is public and does not require an account to use modules. You only need an account if you want to publish your own modules.

How do I update a module to a new version?

Change the version constraint in your 'module' block to match the new version (e.g., from '"= 1.0.0"' to '"= 1.1.0"'), then run 'terraform init -upgrade' to force Terraform to download the new version.

Can I reference the output of one module in another module in the same configuration?

Yes. Use the syntax 'module.module_name.output_name' to pass the output value to another module's input variable or to a resource attribute.

What is the difference between a module and a resource in Terraform?

A resource is a single infrastructure object (like an EC2 instance). A module is a container that groups multiple resources together and can be reused across configurations. You call a module, but you declare a resource.

Does the Terraform Registry support private modules?

Yes, the Terraform Registry (both public and private versions) supports private modules that require authentication to access. The private registry is often used within organisations.

Terms Worth Knowing

Keep going

You've finished Advanced Module Practices: Versioning and Composition. Continue through the TF-003 study guide to build a complete picture of the exam.

Done with this chapter?