Table of Contents
Why Modules Matter in Terraform
Terraform modules are a way to organize and reuse Terraform configuration. A module is simply a collection of Terraform files in a directory that you can treat as a single unit. Using modules lets you avoid repeating code, keep infrastructure definitions tidy, and share patterns across projects and teams.
In practice, you already use a module whenever you run Terraform. The root directory that contains your main.tf and related files is itself a module, called the root module. When you create additional modules and call them from the root module, you get a clear structure for complex infrastructure that would otherwise be difficult to manage.
A Terraform module is any directory with .tf files that Terraform can call using a module block. The current working directory is always the root module.
Basic Module Structure
A typical reusable Terraform module lives in its own directory. At minimum, it needs one .tf file that defines some resources. To be convenient and self-contained, most modules also define input variables, outputs, and sometimes internal submodules.
Inside a module directory you will commonly see files like main.tf, variables.tf, and outputs.tf. This is a convention, not a requirement. Terraform reads all .tf files in the directory and processes them together as one module.
You can think of the module directory as a black box. It exposes inputs and outputs, and hides the internal resource details. The root module or another module simply calls it and passes arguments, without dealing with how the internal resources are built.
Calling a Module
To use a module, the root module or a parent module declares a module block and provides a source. The source tells Terraform where to find the module, for example in a local directory, a git repository, or a registry.
In the same block, you map values to the input variables that the module expects. Terraform then instantiates the module with those values and creates all the resources defined inside it, as if they were written directly in the root module.
The key idea is that the caller does not recreate the logic. It simply plugs values into a predefined module. This keeps the calling configuration shorter and easier to read.
Module Inputs and Outputs
Inside a module, you define input variables that callers can set. Inputs give the module flexibility and allow it to be reused with different parameters. These variables are referenced inside the module to control resource arguments, counts, names, and similar settings.
Outputs work in the opposite direction. They expose selected values from inside the module, such as an IP address, an ID, or a URL. A calling module can use these outputs to connect different pieces of infrastructure.
Modules communicate only through inputs (variable blocks) and outputs (output blocks). Resource details remain internal to the module.
By keeping a clear boundary of inputs and outputs, modules stay easier to test and reuse. The calling code does not need to know about internal resource names or properties.
Local, Remote, and Registry Modules
Terraform modules can come from several places. Local modules live in directories within the same repository, often under a modules/ directory. You reference them with a local path in the source argument.
Remote modules live in version control systems such as Git or in module registries. For remote Git modules, the source contains the repository URL and an optional ref. Registry modules are published and discovered through a module registry, often organized by provider and purpose.
Registry based modules are particularly useful in teams and organizations. They allow infrastructure specialists to publish reviewed and tested modules that others can consume without copying low level details. Local modules are convenient for project specific patterns.
Reusability and Composition
Modules become powerful when you use them as building blocks. Instead of one large collection of resources in the root module, you create smaller modules that focus on a single responsibility, for example a database, a virtual network, or a compute cluster.
The root module then becomes a composition of these smaller modules. This structure makes it much easier to understand, update, and test. If you need a similar environment for another application, you can reuse the same modules and only adjust inputs.
It is common to create composition modules that themselves call other modules. For example, a web_app module might call a network module and a compute module, and then connect their outputs. The top level configuration calls only web_app, and does not need to connect networking and compute resources manually.
Versioning and Stability
When you use modules from a registry or a remote repository, you should control the version that Terraform fetches. This avoids unexpected changes when a module author publishes new versions.
Versioning modules lets teams evolve infrastructure definitions without breaking existing consumers. New features and fixes can be released in new versions, and callers can upgrade when they are ready.
Always pin module versions when using remote or registry modules to avoid unexpected changes in your infrastructure plans.
Good module versioning practices are central to safe reuse in larger organizations. They allow you to treat modules as stable dependencies instead of mutable code snippets.
Testing and Documentation of Modules
Reused modules benefit from tests and documentation. Tests for modules often involve running plans in isolated environments to verify that resources are created as expected. This helps prevent regressions when the module is changed.
Documentation, at minimum, should describe the purpose of the module, its inputs, and its outputs. Callers should not need to read the internal Terraform code to understand how to use it. Examples of usage are especially helpful to new users of a module.
A well documented and tested module behaves similarly to a small library in traditional software development. This mindset encourages careful design of the module interface and clearer separation between internal implementation and external behavior.
Module Design Guidelines
When you design modules, it is useful to keep their scope focused. A module that tries to do everything at once is harder to reuse and maintain. Instead, aim for modules that solve one problem well, and combine them as needed.
Avoid exposing internal implementation details through outputs unless callers truly need them. This keeps you free to refactor the internal resources later. Prefer simple input variables with clear names and default values when appropriate, which reduce the amount of configuration that callers must write.
Finally, be consistent in naming inputs, outputs, and resources across modules. Consistency makes modules predictable and easier to adopt, especially when many people contribute. Over time, a consistent set of modules can become a shared language for describing infrastructure across projects.