Skip to content
Infrastructure as Code iac testing 4 min read

tflint

terraform validate confirms that a configuration is syntactically correct and internally consistent, but it has no knowledge of the cloud you are deploying to. It will happily accept an EC2 instance type that does not exist, a deprecated argument that the provider still parses, or an interpolation-only expression that should be simplified. TFLint is the linter that fills that gap: a fast, offline-by-default static analyzer that catches plausible-but-wrong code before you ever run plan. It works with both Terraform and OpenTofu.

What tflint catches that validate misses

validate reasons only about the structure of your HCL. TFLint reasons about meaning. Its core ruleset flags unused declarations, deprecated index syntax, missing variable types, and naming-convention violations. More importantly, its provider rulesets understand the actual AWS, Azure, and Google APIs, so they can reject values that are syntactically valid but operationally impossible.

Consider this configuration, which terraform validate passes without complaint:

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.mikro"
}

The instance type is a typo (t2.mikro instead of t2.micro). validate cannot know that — but the AWS ruleset ships an enumeration of every real instance type:

tflint

Output:

1 issue(s) found:

Error: "t2.mikro" is an invalid value as instance_type (aws_instance_invalid_type)

  on main.tf line 3:
   3:   instance_type = "t2.mikro"

Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.39.0/docs/rules/aws_instance_invalid_type.md

That mistake would otherwise survive until a failed apply round-trip to the AWS API.

Configuration with .tflint.hcl

TFLint is driven by a .tflint.hcl file at the root of your repository. The plugin block enables a provider ruleset, and rule blocks toggle or tune individual checks. The core ruleset is built in; provider rulesets are downloaded by tflint --init.

config {
  call_module_type = "all"
  force            = false
}

plugin "aws" {
  enabled = true
  version = "0.39.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

rule "terraform_naming_convention" {
  enabled = true
  format  = "snake_case"
}

rule "terraform_unused_declarations" {
  enabled = true
}

Pinning the plugin version is essential — an unpinned ruleset means CI can start failing on a release you never reviewed. After editing the config, install the plugins:

tflint --init

Output:

Installing "aws" plugin...
Installed "aws" (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.39.0)

--init downloads plugin binaries over the network and verifies them against a checksum. The lint pass itself is fully offline — it never calls the cloud API — so it is safe to run without credentials in any CI job.

Deep checking

By default the AWS ruleset only inspects values it can resolve statically. Enabling deep checking lets it call read-only AWS APIs to validate references against your real account — for example, confirming that an AMI ID or IAM instance profile actually exists. This requires credentials and is slower, so reserve it for a dedicated CI stage rather than the fast inner loop.

tflint --enable-rule=aws_instance_invalid_ami

Useful flags

FlagPurpose
--initDownload and install configured plugins
--recursiveLint the current directory and every nested module
--chdir=<dir>Run as if started in <dir>
--format=compactSingle-line output (also json, sarif, checkstyle)
--minimum-failure-severity=errorExit non-zero only on errors, not warnings
--fixAuto-fix issues for rules that support it
--only=<rule>Run a single named rule

Scanning a whole monorepo of modules in one pass is the common case:

tflint --recursive --format=compact

Output:

modules/network/main.tf:12:3: Warning - terraform "required_version" attribute is required (terraform_required_version)
modules/web/main.tf:3:3: Error - "t2.mikro" is an invalid value as instance_type (aws_instance_invalid_type)

CI integration

TFLint is a single static binary, which makes it trivial to drop into any pipeline. In GitHub Actions, the official setup action installs the binary and caches the plugins between runs. The sarif output format uploads findings directly into the Security tab as annotations on the diff.

name: tflint
on: [pull_request]

jobs:
  tflint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: v0.53.0
      - run: tflint --init
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - run: tflint --recursive --format=compact

Because linting needs no cloud credentials, it sits early in the quality ladder — right after terraform validate and before security scanning. A non-zero exit code fails the job, so a bad instance type or deprecated argument blocks the merge instead of the deploy.

Best Practices

  • Pin both the TFLint version and every plugin version so CI results are reproducible across machines.
  • Always enable the relevant provider ruleset (aws, azurerm, google) — the core rules alone miss most real bugs.
  • Commit .tflint.hcl to the repo so every developer and CI runner lints with identical rules.
  • Run tflint --recursive to cover nested modules, not just the root configuration.
  • Reserve deep checking for a credentialed CI stage; keep the default offline lint in pre-commit for speed.
  • Emit --format=sarif in CI to surface findings as inline pull-request annotations.
  • Treat warnings as actionable: tune them off explicitly in config rather than ignoring them silently.
Last updated June 14, 2026
Was this helpful?