Skip to content

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, or tf blocks
  • 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:

Latest release

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:

terrafmt help

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:

go install github.com/katbyte/terrafmt@499f71ed889552f0e30aec6f9094264b73f43757 # v0.5.6

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:

```terraform
resource "example_thing" "demo" {
  name = "demo"
}
```

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:

terrafmt diff ./docs --check --pattern '*.md' --quiet

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:

terrafmt fmt ./docs --pattern '*.md'

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:

terrafmt diff ./internal --pattern '*.go' --fmtcompat

Or to rewrite matching Go files in place:

terrafmt fmt ./internal --pattern '*.go' --fmtcompat

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 tools bootstraps the dependency
  • make docs-hcl-lint is safe for CI
  • make docs-hcl-fix gives 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:

  • 2 for Terraform/HCL parsing errors in a block
  • 4 for formatting differences when diff --check is used
  • 6 when 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.