David Guerrero

engineer extraordinaire

An opinionated Terraform style guide

Infrastructure is getting more complex, the Terraform/OpenTofu adoption is growing, and somehow there’s fairly little guidance on managing these codebases at scale.

As a heavy user of Terragrunt, I rely a lot on small, focused Terraform modules that can be combined into larger infrastructure pieces externally (called units in the Terragrunt world). If you use pure Terraform/OpenTofu, you might also compose smaller modules into larger ones to achieve a similar effect.

Enforcing shared code style guidelines across all modules seems pretty obvious, especially with rising LLM-based contributions. Still, the official Terraform Style Guide is itself pretty light, and the OpenTofu Style Conventions even lighter. Both rely mostly on running terraform fmt and calling it a day.

Most of the actual style rules these days come from TFLint, the most prominent Terraform rule set being tflint-ruleset-terraform.

Even then, I believe we can go further and introduce more style rules. Terraform modules all follow the same basic structure: variables, resources, outputs. The blocks themselves also contain the same attributes: for_each, count, depends_on

Let’s make sure we can always find the same information in the same place, in every module.

The rules

File naming

This is the most controversial and impactful one at the same time:

module/
  00-variables.tf
  01-providers.tf
  02-locals.tf
  03-data.tf
  ...
  10-ec2.tf
  20-alb.tf
  30-hetzner.tf
  ...
  99-outputs.tf

It might seem weird at first, but I find it hard to switch back after trying this structure. Everything is always in the same place and in the same order. File browsers display the correct order of files, and each file has a specific purpose:

The only drawback is that people not familiar enough with Terraform could wrongly assume that the file naming has an impact on resource creation order.

Order of arguments

To make blocks uniform and easier to read:

Trailing commas

my_list = [
  1,
  2,
]

my_dict = {
  a = 1
  b = 2
}

Map assignments

The map assignments must always be done with the equal sign (=). The colon (:) is not accepted, even though it is valid HCL.

foo = { a = 1 } # Perfect!
bar = { b : 1 } # Invalid, breaks some syntax highlighters

Comments

It’s something the official Terraform Style Guide mentions but is never enforced anywhere: only use #, for all comments.

Additionally, there should be a single space between the # and the comment content.

The spacing between the code and the comments is already handled by terraform fmt.

Multi-line lists

Surprisingly, terraform fmt accepts a lot of weird formatting for lists, these must not be used:

# Invalid
subnets = ["subnet-1", "subnet-2", ]

# Invalid
availability_zones = [
  "us-east-1a",
  "us-east-1b", ]

# Invalid
public_ips = ["1.1.1.1",
  "2.2.2.2",
]

# Very invalid
dns_servers = ["8.8.8.8"
, "8.8.4.4"]

Empty lines

Composability

If you want to keep your modules lean and composable, you likely want to enforce these:

Enforcing the rules

All rules are packaged into a TFLint plugin called tflint-ruleset-terraform-style (GitHub link), or simply terraform_style. You can add it to your .tflint.hcl this way:

plugin "terraform_style" {
  enabled = true
  version = "0.2.1"
  source  = "github.com/Heldroe/tflint-ruleset-terraform-style"
}

This should be used in conjunction with other tools like terraform fmt, terraform validate, and TFLint rule sets like tflint-ruleset-terraform.

Customization

Most rules are customizable: you can change the expected file names, starting index for resource files, order of arguments, allowed blocks in each file… For example:

rule "terraform_style_terraform_file" {
  enabled        = true
  filename       = "07-setup"
  allowed_blocks = ["terraform", "locals"]
}

rule "terraform_style_output_arguments" {
  enabled = true
  order   = ["value", "description"]
}

And of course, each rule can be enabled or disabled individually.

#Engineering #Terraform