Co-authored with the amazing Daniel Lamando
Intro
Using modules to keep your Terraform code DRY is very common, but figuring out the best way to release and distribute these modules isn’t always easy. We at Forto quickly faced this struggle, as more of the organization adopted Terraform to describe their infrastructure with code. In fact, keen-eyed readers of our previous Terraform blog post might have noticed a small easter egg:
This snippet represented how we referenced Terraform modules at the time, and it exposed some significant technical debt: referencing unversioned Terraform modules by-path. This practice made every change to any module a risky endeavor. In some cases, we created extra copies of our modules to manage the risk level (leading to @v2 as seen in that screenshot).
(“Freighthub” is Forto’s previous brand name. Renaming our Github org is top on the Jira board, honestly!)
The Flow
Make a change in some module
You know, your job.
Test the new module before release
If you want to test your code locally, you can use file system path-based module source reference, like:
source = “../../../modules-repos/name_of_module”
If you want to have the unreleased change tested by the CI/CD pipeline (see previous article), you can use git-based module source reference, such as:
source = “[email protected]:freight-hub/terraform-modules-demo.git?ref=BRANCH-NAME”
Open a PR for the change
Make sure to add the text intended for the changelog in the PR description. Providing a meaningful changelog text ensures that the release history will be accurate and useful.
Apply a release increment label to the PR
Based on Semantic Versioning guidelines.
Merging is blocked until this is done.
The pipeline will post a PR comment including current & new version numbers.
On merge, we deploy wiki/changelog and push artifacts to the bucket.
Here we see the new variable in the autogenerated wiki page:
Consume the module using s3 URL
Private repos should use S3 URLs to allow Terraform to use AWS authentication and authorization, public repos can just use plain HTTP URLs
source = “s3:https://forto-terrform-modules-demo.s3.eu-west-1.amazonaws.com/terraform-aws-vpc-1.1.0.zip”
The Reasoning
Why version your modules at all?
Deterministic behavior — surprises are good for birthdays (maybe), but not infrastructure as code repos! Operators should expect to see changes in their infrastructure objects only when they change the code in their Terraform workspace, regardless of work done on the modules. When a new version for a module is desired (e.g. fixing a bug, adding functionality), an explicit change must be made to the Terraform workspace, thus ensuring ownership policies like Github CODEOWNER Required Reviews are kept.
Why Semver?
Consumers of the modules can easily tell if they need to look at the changelog before upgrading as major versions indicate large or breaking changes. Note that Patch versions don’t necessarily introduce less risk compared to minor or even major changes. Risk is managed by testing and code & plan reviews.
Some decisions we made
#1 Separate repo
The CI/CD and permission/ownership models are totally different compared to the other Terraform repos. Having modules and workspaces root code in the same repo would result in a highly complex repo configuration.
#2 Pull Request(PR) Based
We originally had a commit-based approach. Versio would check all the [conventional] commit messages in a merged PR, decide on a version bump, and release it.
While we liked the pure git approach, it resulted in some confusing user experiences. For example, commit messages from a “squash” merge might pop back into existence and fail the Versio release command. This error happens after the merge is complete which makes the damage annoying to repair.
And because GitHub does not provide centralized pre-commit hooks, we couldn’t easily enforce a policy on those commit messages, requiring users to carefully follow the repository instruction or be forced to re-write the PR’s git history.
For our from-scratch solution, we started leveraging the Github PR itself. A set of four PR labels (major, minor, patch, and no-release) replaced the complex scanning of commit messages for versioning markers such as “feat!” and “fix:”. With git history ignored, the version bumps became much more reliable and predictable.
It also allowed us to post the “planned” versioned in PR comment and disallow merges of PR with no label, ensuring attention was given to version bump size. This was implemented with our own code, in languages which we know (Typescript/YAML/Bash) and with about 95% fewer lines of code compared to Versio’s Rust codebase.
#3 Keep the version “state” in Git tags.
- This is one of Versio’s modes of operation, and we liked it! Keeping the version in an in-repo file would require an extra commit after each release.
- Connecting to an external database would add a lot more complexity!
#4 Store the module registry on Amazon S3:
- Effectively free for this amount of data.
- For private registries, AWS authentication/authorization is already handled by Terraform CLI (assuming that you are using Terraform’s S3 backend).
- We prefer strict versioning (see above), so Version constraints are not needed. If version resolving was important then we might’ve ended up on the official Terraform registry instead.
#5 Docs and Changelog in GitHub wiki:
- Free, or if you use private repos, already paid for.
- Easily accessible for developers.
- Effortlessly authenticated if needed.
#6 Use Github Actions:
- Good dynamic workflow support.
- Easy integration with Github Pull Requests
Our next steps
The big downside of strict versioning is version drift. Upgrades must be done explicitly, and if the upgrades aren’t done rigorously, previous versions of the modules will be kept in use long after new versions become available.We plan to introduce another job for the Terraform modules pipeline to automatically open PRs which upgrade all module references in the workspaces root repo to the latest version. For minor and patch version bumps, these PRs should be ready to merge as-is. Major version bumps would instead open draft PRs, as we assume the large or breaking changes will require more work before the PR is ready to merge.
That’s it
I hope you found this article relevant! Please checkout (pun very much intended) our reference repo: https://github.com/freight-hub/terraform-modules-demo.It should be possible to clone it and adjust the CI to your needs in a few hours. Finally, don’t forget to visit our careers page! We are doing all kinds of other cool stuff and most of it doesn’t end up in the blog.