5 releases (3 stable)
Uses new Rust 2024
| 1.1.1 | Jun 19, 2026 |
|---|---|
| 1.0.0 | Jun 6, 2026 |
| 0.1.1 | Jun 1, 2026 |
| 0.1.0 | Jun 1, 2026 |
#1647 in Command line utilities
35KB
607 lines
tfpin
A small, fast linter that keeps your Terraform repository on the versions and conventions you've
agreed on. Point it at a .tfpin.toml config and it checks every .tf file for:
- the expected Terraform version (
terraform.required_version); - pinned provider versions (
required_providers); - pinned module versions (
module { source, version }); - an S3 backend convention — fixed attributes plus a state
keythat mirrors the file's path; - leftover refactoring blocks (
moved,import,removed) that should be deleted after use.
Every rule lives in the config file, so the same binary works for any organisation. tfpin exits
0 when everything is fine and 1 when any violation is found, which makes it a natural CI gate.
Install
Download a binary from the releases page, or build from source:
cargo install --path .
# or
cargo build --release # -> target/release/tfpin
Usage
tfpin [PATHS]...
PATHS— directories or.tffiles to scan. Defaults to the config file's directory. When given, they narrow (never widen) the set selected by yourinclude/excludeglobs — handy for checking only the files changed in a PR.-c, --config <FILE>— path to the config. By default tfpin discovers.tfpin.tomlby walking up from the current directory.-v, --verbose— print each file as it is checked.--ignore-forbidden-blocks— skip theforbidden_blockscheck for this run (handy while a refactor is mid-flight).
tfpin # check the whole repo
tfpin platform/stack/prod # check one subtree
tfpin -c ci/.tfpin.toml -v # explicit config, verbose
tfpin --ignore-forbidden-blocks # skip the forbidden_blocks check
Exit codes: 0 clean · 1 violations found · 2 could not run (missing config, bad pattern, …).
Configuration
Create a .tfpin.toml at the root of your repository. Every section is optional — a check only
runs when its section is present, and only enforces what you specify. See
.tfpin.example.toml for a fully-commented template.
terraform_version = "~> 1.15.5"
forbidden_blocks = ["moved", "import", "removed"]
[providers]
aws = "~> 6.24"
kubernetes = "~> 2.37, >= 2.37.1" # compound constraints compared verbatim
[modules]
"terraform-aws-modules/vpc/aws" = "~> 6.2"
[backend.s3]
allowed_account_ids = ["111111111111"]
bucket = "acme-terraform-state"
region = "us-west-1"
use_lockfile = true
key_template = "acme/infra/{dir}"
Notes:
[backend.s3]is free-form. Any attribute you list is compared verbatim against thebackend "s3"block (string, bool, number or array). Attributes on the block that you don't list are ignored, so extras likeencrypt,kms_key_idorprofileare fine.key_templateis the one reserved key.{dir}is replaced by the.tffile's directory, relative to the config file, joined with/. Forplatform/foo/main.tfwithkey_template = "acme/infra/{dir}", the expectedkeyisacme/infra/platform/foo.- Skipping is intentional. A provider/module without a
version, or one not listed in the config, is left alone. The contract is "if it's pinned, it must be pinned correctly." **/.terraform/**is always excluded.
GitHub Action
Run tfpin in CI with the bundled composite action, which downloads the matching prebuilt binary:
name: terraform-lint
on: [pull_request]
jobs:
tfpin:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: lra/tfpin@v1
# with:
# version: v0.1.0 # or "latest" (default)
# config: ci/.tfpin.toml # optional
# args: "platform" # extra args / paths
# ignore-forbidden-blocks: true # skip the forbidden_blocks check
# working-directory: .
The job fails when tfpin finds any violation.
Development
cargo test # unit + integration tests (fixtures under tests/fixtures/)
cargo clippy --all-targets -- -D warnings
cargo fmt --all
Source layout: src/config.rs (the .tfpin.toml model), src/hcl_nav.rs (helpers over the
hcl parse tree), src/checks/ (one module per rule), and
src/main.rs (CLI, file discovery, output).
License
Dependencies
~8–12MB
~229K SLoC