DEV Community

Bartłomiej Danek
Bartłomiej Danek

Posted on • Originally published at bard.sh

hcl-linter 0.0.1-alpha is out

hcl-linter 0.0.1-alpha is out

terraform fmt only touches whitespace. In a codebase with dozens of contributors and hundreds of HCL files, block order drifts, naming conventions split, and required fields go missing quietly. I wanted something that could catch and fix that automatically - so I built hcl-linter.

The first run fixed over 1000 terragrunt.hcl files.

getting started

Two commands to go from nothing to a first pass:

hcl-linter init             # scaffold .hcl-linter/ from your existing files
hcl-linter fix ./ --dry-run # preview what would change
Enter fullscreen mode Exit fullscreen mode

init walks the project, groups files by basename, and writes a .hcl-linter/default.hcl plus one extends = "default" override per unique filename. No config needed for a first formatting pass either - --format applies opinionated defaults without any config:

hcl-linter fix ./ --format --dry-run
Enter fullscreen mode Exit fullscreen mode

block order

Wrong block order is the most common drift. block_order catches and auto-fixes it:

# violation - terraform before include
terraform {
  source = "git::https://github.com/example/vpc.git"
}

include "root" {
  path = find_in_parent_folders()
}

dependency "vpc" {
  config_path = "../vpc"
}
Enter fullscreen mode Exit fullscreen mode

After hcl-linter fix:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://github.com/example/vpc.git"
}

dependency "vpc" {
  config_path = "../vpc"
}
Enter fullscreen mode Exit fullscreen mode

Config:

rules {
  block_order {
    enabled = true
    order   = ["include", "locals", "terraform", "dependency", "inputs"]
  }
}
Enter fullscreen mode Exit fullscreen mode

naming

Hyphens in block labels break dependency.my-vpc.outputs.id references in locals. name_validation flags and auto-fixes them - hyphens replaced with underscores, references in locals updated too:

# violation
dependency "my-vpc" {
  config_path = "../vpc"
}

dependency "db-primary" {
  config_path = "../database"
}
Enter fullscreen mode Exit fullscreen mode
rules {
  name_validation {
    enabled = true
    pattern = "^[a-z][a-z0-9_]*$"
    blocks  = ["dependency", "include"]
  }
}
Enter fullscreen mode Exit fullscreen mode

required fields

required_fields catches missing attributes when a block is present and auto-adds them:

# violation - expose missing on include blocks
include "root" {
  path = find_in_parent_folders()
}

include "vpc" {
  path = "../vpc"
}
Enter fullscreen mode Exit fullscreen mode

After fix:

include "root" {
  path   = find_in_parent_folders()
  expose = true
}

include "vpc" {
  path   = "../vpc"
  expose = true
}
Enter fullscreen mode Exit fullscreen mode
rules {
  required_fields {
    include {
      expose = true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

count and for_each

count_for_each warns on patterns that silently disable resources - count = 0 or for_each = {} left behind after feature flags, and count + for_each used on the same block (Terraform rejects this at plan time):

resource "aws_instance" "web" {
  count = 0         # WARNING: resource will not be created
  ami   = "ami-12345"
}

resource "aws_db_instance" "db" {
  count    = 1
  for_each = var.db_configs  # ERROR: cannot use both count and for_each
  engine   = "postgres"
}
Enter fullscreen mode Exit fullscreen mode

Not fixable - count = 0 is sometimes intentional. The rule flags it so the intent is explicit.

rules {
  count_for_each {
    enabled                = true
    warn_on_count_zero     = true
    warn_on_empty_for_each = true
    warn_on_conflict       = true
  }
}
Enter fullscreen mode Exit fullscreen mode

get_env without default

hcl_functions warns on get_env() calls with no fallback - those fail silently in CI if the variable is unset. It also validates that find_in_parent_folders() targets exist on disk:

locals {
  env  = get_env("MY_SECRET")                          # WARNING: no default
  root = find_in_parent_folders("nonexistent.hcl")     # ERROR: file not found
  port = get_env("PORT", "8080")                       # ok
}
Enter fullscreen mode Exit fullscreen mode
rules {
  hcl_functions {
    enabled                       = true
    find_in_parent_folders_exists = true
    get_env_has_default           = true
  }
}
Enter fullscreen mode Exit fullscreen mode

deprecated terraform hooks

Terragrunt renamed before_hook / after_hook to before_hooks / after_hooks a while back. terraform_block catches the old names still in use:

terraform {
  source = "git::https://github.com/example/vpc.git"

  before_hook "run_fmt" {      # WARNING: deprecated, use before_hooks
    commands = ["terragrunt"]
    execute  = ["terraform", "fmt"]
  }
}
Enter fullscreen mode Exit fullscreen mode
rules {
  terraform_block {
    enabled              = true
    no_deprecated_fields = true
  }
}
Enter fullscreen mode Exit fullscreen mode

dependency outputs

dependency_outputs statically validates that dependency.x.outputs.y references exist in the target module - no plan needed:

dependency "vpc" {
  config_path = "./mock-vpc"
}

inputs = {
  vpc_id     = dependency.vpc.outputs.vpc_id       # ok
  bad_output = dependency.vpc.outputs.nonexistent  # WARNING: not declared
}
Enter fullscreen mode Exit fullscreen mode

Walks the dependency chain, parses output blocks from .tf files. Supports .mock-outputs.json for modules where upstream state is not available locally.

rules {
  dependency_outputs {
    enabled = true
  }
}
Enter fullscreen mode Exit fullscreen mode

CI

check fails the build on any rule violation:

hcl-linter check ./infra
Enter fullscreen mode Exit fullscreen mode

fix --dry-run catches everything check does, plus byte-level drift from fixable rules - if someone committed without running fix:

hcl-linter fix ./infra --dry-run
Enter fullscreen mode Exit fullscreen mode

Both are composable: run check for semantic issues and fix --dry-run for formatting drift as separate steps.

install

go install github.com/bard-works/hcl-linter@latest
Enter fullscreen mode Exit fullscreen mode

Or grab a binary from releases:

curl -L https://github.com/bard-works/hcl-linter/releases/latest/download/hcl-linter_linux_amd64.tar.gz | tar xz
Enter fullscreen mode Exit fullscreen mode

Source and full docs: bard-works/hcl-linter


Originally published at https://bard.sh/posts/hcl-linter-release/

Top comments (0)