7 Code Quality and Continuous Integration
Infrastructure as Code benefits from decades of software engineering practice, and this chapter translates those lessons to Terraform and OpenTofu. It defines the hallmarks of high-quality code—low maintenance, easy extensibility, safety nets, simple developer workflows, and accurate documentation—then grounds them in modern collaboration: Git-based branching and pull requests, disciplined code reviews with an emphasis on security and consistency, and the principle that anything automatable should be automated. Continuous Integration is positioned as the culmination of this discipline: frequent, reliable integration into main that’s enabled by tests, linters, and policy checks built into the project itself.
The chapter builds a developer-first foundation for quality by standardizing local workflows. Teams bootstrap repositories with templates so tools and structure are consistent from day one, then use Makefiles to hide command complexity, install tooling across platforms, and provide one-call task groups. It shows how to switch seamlessly between Terraform and OpenTofu with a make variable, manage multiple engine versions per project via version files and a version manager, and enforce fast feedback with pre-commit hooks that run formatting, validation, linting, docs, and security checks before code even leaves the laptop.
With that base, it layers in concrete tools and CI enforcement. Core checks include terraform validate; testing via Terratest or the native testing framework; TFLint with provider rule sets, selective rule tuning, and inline ignores; and complementary security scanning with Checkov and Trivy, plus custom policies using OPA in TFLint or simple YAML rules in Checkov (centralized and pulled at scan time). Routine chores are automated with terraform-docs and terraform fmt, and tflint can autofix many findings. Finally, the same Make targets power CI workflows (for example with matrix builds) to validate across both OpenTofu and Terraform and multiple versions, protected by required checks and reviews, and kept current with automated dependency updates. The result is a repeatable, template-driven path to consistent, secure, and maintainable infrastructure code, with CI as the mechanism that continuously upholds those standards.
Continuous Integration Workflow
Git Branching
Pull Request
Basic CI Workflow
A Pull Request With Failing Tests
Branch Protection on Github
Dependabot Pull Request
Summary
- Infrastructure as Code is powerful because of the lessons we can learn from Software Development.
- Automated Quality Control tools such as linters and security scanners can greatly improve code quality with minimal effort.
- The use of automation to enforce policy and test code allows developers to move faster by giving them safety nets.
- Automation reduces the cognitive load of code reviews, allowing developers to focus on the proposed changes themselves.
- Automation should be easy and repeatable, which is why tools like Make and CookieCutter should be used to create standards between projects.
- Continuous Integration systems work alongside Source Control Management systems to allow teams to move quickly while ensuring high quality code.
FAQ
What does Continuous Integration (CI) really mean for Terraform/OpenTofu projects?
CI is about frequently integrating small changes into the main branch, not just “running tests before merge.” High-performing teams merge multiple changes per day, with PRs gated by automated checks (lint, validation, security scans, docs checks, tests) so reviewers can focus on security, best practices, and documentation quality instead of formatting or mechanical issues.How do Makefiles make local development easier and more consistent?
Makefiles centralize all the common commands so no one has to memorize tooling. You define targets likemake format, make test_validation, make documentation, group them under aliases like make chores, and even add install targets (e.g., make install) that detect the OS package manager. This gives every developer a consistent, one-command workflow.What’s the best way to install required tools across different operating systems?
Put installation into your Makefile and auto-detect the package manager. For example, use Homebrew on macOS and Chocolatey on Windows, with variables listing packages liketenv, tflint, terraform-docs, checkov, and trivy. For Linux, extend the Makefile for your distro’s package manager (apt/yum/apk/snap). Developers then run make install.How can we support both Terraform and OpenTofu (and multiple versions) in one repo?
Abstract the binary with a Makefile variable. SetTF_ENGINE to terraform or opentofu, derive TF_BINARY, and use it in all targets (e.g., $(TF_BINARY) fmt). Manage versions with tenv using .terraform-version and .opentofu-version files (exact versions, constraints, or keywords like latest/latest-stable). This lets you switch engines and versions per project seamlessly.Which code quality tools should we wire in, and what do they check?
- terraform validate: syntax and config sanity (afterinit -backend=false)- tflint: static analysis and best practices via core/provider/OPA plugins
- terraform fmt: formatting consistency
- terraform-docs: auto-generate and verify README docs from code
- Security scanners: Checkov and Trivy for misconfigurations across providers
Connect them to Make targets for “run” and “test” (e.g.,
make format, make test_format, make test_tflint, make test_checkov, make test_trivy, make documentation, make test_documentation).How do pre-commit hooks help, and how do we set them up?
Pre-commit hooks catch issues before the commit lands, avoiding wasted CI cycles and PR churn. Use the Pre-Commit Framework with a.pre-commit-config.yaml that either calls your Make targets or uses the pre-commit-terraform hooks. Developers run pre-commit install once; then formatting, validation, docs checks, linting, and security scans run on every commit.What’s a good way to bootstrap new modules with standard quality tooling?
Create a template. Start with a template repo or use CookieCutter to generate projects with Makefiles, pre-commit config, tflint config (provider-specific plugins), terraform-docs settings, security scanner wiring, and CI workflows. CookieCutter can prompt for provider, license, and versions, then generate the correct config files automatically.How do we handle security scanning and exceptions with Checkov and Trivy?
Run both:checkov --directory . and trivy config . are quick wins and complementary. When exceptions are truly necessary, use inline ignore comments (e.g., #checkov:skip=CKV_..., #trivy:ignore:AVD-...) with a justification, or for Trivy, .trivyignore for project-wide suppressions. For custom organization rules, load Checkov policies from a shared Git repo via a Makefile option like --external-checks-git.How do we enforce quality in CI, including validating both OpenTofu and Terraform?
In GitHub Actions: checkout code, set up tools (e.g.,setup-tflint, hashicorp/setup-terraform or opentofu/setup-opentofu), and run your Make targets. Use a matrix to vary engine (terraform/opentofu) and versions (e.g., 1.6/1.7/1.8), passing TF_ENGINE=${{ matrix.engine }} to Make. Enable branch protection to require passing checks and at least one reviewer approval before merging.How can we automate ongoing maintenance tasks like updates and formatting?
- Dependabot: configure to update GitHub Actions and Terraform modules/providers; it opens PRs that run your checks automatically. Supports private registries with secrets and self-hosted runners if needed.- Chores: bundle
terraform-docs and terraform fmt under make chores so devs refresh docs and formatting in one step.- tflint autofix: use
tflint --fix after reviewing findings; don’t auto-run it in chores to avoid unintended changes.
Terraform in Depth ebook for free