Maintain Formatting of Embedded Terraform Provider Examples with terrafmt
When a Terraform provider repository ships Markdown documentation full of example configuration, those snippets are part of the product. If they drift out of style, or worse, stop parsing as valid HCL, users feel it immediately. terrafmt is a small tool that helps keep embedded Terraform examples formatted and syntactically correct in both local workflows and CI.
terrafmt scans files for embedded Terraform configuration, extracts the matching blocks, parses the HCL, formats it, and then either shows the diff or rewrites the file in place. It recognizes embedded configuration in a few very practical forms:
- Markdown documentation files with fenced
hcl,terraform, ortfblocks - Go source files that return raw strings
- Go source files that build examples with
fmt.Sprintf(...)
In other words, terrafmt is a bridge between "this is documentation" and "this is still code."
Note
For fmt and diff, terrafmt is formatting embedded HCL content, not simply shelling out to terraform fmt against your Markdown files.
Why It Is Useful in Provider Repositories¶
Terraform providers usually have a lot of documentation examples: resources, data sources, import examples, upgrade notes, and troubleshooting content. Those examples get copied by users straight into working configurations, so they should be treated with the same care as the Go code that implements the provider.
Adding terrafmt to a provider repository gives you a lightweight quality gate:
- malformed HCL blocks fail early
- formatting drift is caught before review
- contributors get a simple local fix path
- GitHub Actions can enforce the same rule for every pull request
This is especially useful alongside the kinds of workflows used in provider projects, such as local Terraform provider development with dev_overrides, where tight feedback loops matter.
Install terrafmt Locally¶
The simplest way to install terrafmt is with Go:
The badge above tracks the latest upstream release automatically. The pinned examples below are intentional, they show reproducible installs and should be updated when you choose to adopt a newer release.
go install github.com/katbyte/[email protected]
That puts the binary in your $GOBIN, or in $GOPATH/bin if GOBIN is not set. Make sure that directory is on your PATH.
At the time of writing, the upstream module declares go 1.23, so make sure your local Go toolchain is current enough to build it.
If you want to confirm the install:
If you are already using a repository-local tools target, you can also install it there instead of asking each contributor to do it manually.
If you prefer to pin to an exact commit instead of a release tag, Go supports that too:
What It Matches in Markdown¶
For Markdown, terrafmt looks for Terraform examples in fenced code blocks labeled as terraform, hcl, or tf.
That means this is in scope:
And an unlabeled fenced block is not the best choice if you expect terrafmt to process it.
Run It Against Markdown Docs¶
For provider documentation written in Markdown, a very practical pattern is to run terrafmt against the docs tree and limit it to *.md files.
Check Only¶
Use diff --check when you want CI or a pre-merge check to fail if a block is misformatted:
This is the mode I like for automation. If the embedded Terraform is not formatted correctly, the command exits non-zero and the workflow fails.
Rewrite In Place¶
Use fmt when you want to fix the files:
This rewrites the embedded Terraform blocks inside the matching Markdown files without touching the rest of the prose around them.
Tip
terrafmt diff is great for CI because it is non-destructive, while terrafmt fmt is great for local cleanup because it applies the formatting directly.
Run It Against Go-Embedded Examples¶
If your provider embeds Terraform examples in Go source, especially with fmt.Sprintf(...), the README's --fmtcompat option matters.
Use it when the embedded example contains Go format verbs such as %s, %d, or %[1]q:
Or to rewrite matching Go files in place:
Without --fmtcompat, terrafmt will treat those format verbs as plain text and may fail to parse examples that are otherwise valid once rendered by Go.
Add it to a Makefile¶
One of the nicest ways to operationalize terrafmt is to make it part of the repository's standard tooling flow. This example, based on the pattern used in the terraform-provider-vsphere repository, installs the tool and exposes separate lint and fix targets:
export GOPATH_BIN := $(shell go env GOPATH)/bin
export PATH := $(GOPATH_BIN):$(PATH)
tools:
go install -mod=mod github.com/katbyte/[email protected]
docs-hcl-lint: tools
@echo "==> Checking HCL formatting..."
@$(GOPATH_BIN)/terrafmt diff ./docs --check --pattern '*.md' --quiet || (echo; echo "Unexpected HCL differences. Run 'make docs-hcl-fix'."; exit 1)
docs-hcl-fix: tools
@echo "==> Applying HCL formatting..."
@$(GOPATH_BIN)/terrafmt fmt ./docs --pattern '*.md'
There are a few things I like about this pattern:
make toolsbootstraps the dependencymake docs-hcl-lintis safe for CImake docs-hcl-fixgives contributors a one-command fix- the commands are easy to remember and easy to document in a contributor guide
If you are already using a tools target for generators, linters, or documentation helpers, terrafmt fits naturally there.
Pin for Reproducible CI
The example above uses a pinned release. If your team prefers maximum immutability, use a full commit SHA instead of a tag.
Add it to GitHub Actions¶
Once the Makefile targets exist, the GitHub Actions integration becomes very small. This is the same general approach used in the terraform-provider-vsphere documentation workflow:
name: Check Documentation
on:
pull_request:
permissions:
contents: read
jobs:
docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Tools
run: make tools
- name: Check Structure
run: make docs-check
- name: Check HCL Formatting
run: make docs-hcl-lint
Why this works well.
The workflow does not need to know anything about terrafmt directly. It simply calls the repository's own make targets. That keeps the CI definition small and keeps the source of truth for tooling behavior in the repository itself.
This is one of those small improvements that pays back quickly. Reviewers do not have to point out formatting drift in docs, and contributors get immediate feedback on pull requests.
The README also documents useful exit codes for automation:
2for Terraform/HCL parsing errors in a block4for formatting differences whendiff --checkis used6when both conditions are present in the same run
What terrafmt Catches, and What It Does Not¶
terrafmt is very good at one specific job: validating and formatting embedded Terraform/HCL blocks. That makes it ideal for documentation hygiene, but it is not the same thing as a full provider test strategy.
It helps catch:
- HCL syntax errors in embedded examples
- formatting inconsistencies in fenced Terraform blocks
- documentation drift that would otherwise show up only after copy-paste by a user
It does not replace:
- acceptance tests
- import verification
- semantic checks that a resource argument actually exists in the provider
- examples that are syntactically valid but logically wrong
So yes, it is absolutely worth including in a provider repository to keep documentation examples honest. Just think of it as one layer in the documentation quality stack, not the whole stack.
A Good Default for Terraform Docs¶
If your repository contains Terraform examples inside Markdown, terrafmt is a strong default. It is easy to install, easy to wrap in make, easy to run in GitHub Actions, and directly improves the trustworthiness of your documentation.
That is a pretty good return for a very small tool.
The project README also covers blocks for extracting embedded Terraform and upgrade012 for older migration workflows, but for most provider repositories the day-to-day value is in diff and fmt.