Monorepo vs Multi-repo
A Terraform Monorepo (or Terralith), keeps all of your Terraform modules in a single Git repository, often in a modules
directory. While simple at first, this pattern makes it difficult for other teams to reuse your modules and complicates versioning. It’s generally considered an anti-pattern unless the modules are highly specific to a single application.
Instead, you should almost always use the multi-repo pattern. Each module lives in its own dedicated Git repository. This approach makes your module a standalone component that is easy to share, maintain, and version. Consumers of your module can then reference a specific version using a Git tag.
|
|
Writing
A well-written module is like a function, it accepts inputs, performs an action and returns outputs.
- Variables as an API
- Treat your
variables
as the public API of your module. Every variable should have a clear description, type, and a default value if possible. This make it easier to use and understand.
- Treat your
- Focused outputs
- Use
outputs
to expose important resource attributes that consumers of the module might need.
- Use
- Keep it simple
- A good module does one thing well. For example, create one module for networking and another for a database. This makes your modules more flexible.
Example project structure:
|
|
Documentation
Good documentation is important for making your modules usable. Manually writing it tho, is boring as heck. Instead, you should automatically generate it from your code using a tool like terraform-docs.
It scans your .tf
files and updates your README.md
file with required versions, descriptions, types, and default values for all of your variables and outputs.
To use it, run the following command in your module’s root directory:
|
|
You should ideally run this in a pre-commit hook or a CI/CD pipeline to ensure your documentation is always up-to-date with your code.
Versioning
Versioning allows users to consume your module without worrying about unexpected breaking changes. The standard practice is to use Semantic Versioning (SemVer), which follows a MAJOR.MINOR.PATCH
format.
- MAJOR (
1.0.0
>2.0.0
): For incompatible or breaking changes. - MINOR (
1.0.0
>1.1.0
): For adding new features in a backward-compatible way. - PATCH (
1.0.0
>1.0.1
): For backward-compatible bug fixes.
To release a new version in Git, you simply create and push a new Git tag.
|
|
This process can also be automated with a tool like release-drafter. This can be easily implemented in a GitHub Actions pipeline. It builds a release draft with a changelog based on labels in pull requests. You configure everything in a .github/release-drafter.yml
file.
Using a Repository Template
To save you the trouble of creating a module template yourself, I’ve created my own template you’re free to use - vetlekise/terraform-module-template
This template includes pre-commit hooks for linting, compliance scanning, and vulnerability scanning. This catches most issues before they’re even pushed.
It also includes a CI/CD pipeline using GitHub Actions for linting, documentation, and releases. This catches stuff that gets past the pre-commit hooks. It also automatically builds your documentation, and automatically creates a release draft with a changelog based on labels in pull requests.
Conclusion
By following these practices, you can create Terraform modules that are robust, reusable, and easy to maintain. Here’s a short summary of the most important points:
- Use multi-repo project structure if you want the module to be easily maintainable and accessible to others.
- Look at variables as input parameters for the user.
- Version your modules using Semantic Versioning (X.Y.Z)
- Automate your documentation and versioning