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.
Jump to a section
A simple way to picture Advanced Module Practices: Versioning and Composition
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.
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.
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.
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.
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.
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.
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.
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.
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:
When the team starts a new project, they create a new root module (a new Terraform directory).
In the main.tf file, they write three module blocks, each referencing the respective module from the Terraform Registry or their private registry.
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.
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.
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.
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.
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.
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
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.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
Terraform will fetch the latest version from the source during 'terraform init'. This can introduce breaking changes unexpectedly. It is considered a bad practice.
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.
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.
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.
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.
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.
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?