This file defines the Terraform style conventions for this repository, based on the HashiCorp Terraform Style Guide. All Terraform code in this workspace MUST conform to these rules. When reviewing or generating Terraform code, enforce every rule below.
- Indent two spaces per nesting level.
- Align
=signs when multiple single-line arguments appear on consecutive lines at the same nesting level:ami = "abc123" instance_type = "t2.micro"
- Place all arguments at the top of a block, then nested blocks below, separated by one blank line.
- Use empty lines to separate logical groups of arguments within a block.
- Meta-arguments first: list meta-arguments (
count,for_each) at the top of a resource block, separated from other arguments by one blank line. - Meta-argument blocks last: place
lifecycle,depends_onblocks at the bottom, separated from other blocks by one blank line.resource "aws_instance" "example" { # meta-argument first count = 2 ami = "abc123" instance_type = "t2.micro" network_interface { # ... } # meta-argument block last lifecycle { create_before_destroy = true } }
- Separate top-level blocks with exactly one blank line.
- Separate nested blocks with blank lines, except when grouping related blocks of the same type.
- Run
terraform fmtbefore committing. Use-recursiveto format subdirectories.
- Run
terraform validatebefore committing to check syntax and internal consistency.
- Use
#for all comments (single-line and multi-line). Do NOT use//or/* */. - Write self-explanatory code; only add comments when necessary to clarify complexity.
-
Use a descriptive noun for every resource name.
-
Separate words with underscores (
_), not hyphens or camelCase. -
Do NOT include the resource type in the resource name (the address already contains it).
-
Wrap both resource type and name in double quotes.
Bad:
resource aws_instance webAPI-aws-instance {...}
Good:
resource "aws_instance" "web_api" {...}
- Define data sources before the resources that reference them so code "builds on itself".
- Within a resource block, order parameters as follows:
countorfor_eachmeta-argument- Resource-specific non-block parameters
- Resource-specific block parameters
lifecycleblock (if required)depends_on(if required)
-
Every variable MUST have a
typeand adescription. -
Provide a
defaultfor optional variables. -
Set
sensitive = truefor passwords, private keys, and other secrets. -
Use
validationblocks only when values have uniquely restrictive requirements. -
Order variable parameters:
typedescriptiondefault(optional)sensitive(optional)validationblocks
variable "db_disk_size" { type = number description = "Disk size for the API database" default = 100 } variable "db_password" { type = string description = "Database password" sensitive = true }
-
Every output MUST have a
description. -
Order output parameters:
descriptionvaluesensitive(optional)
output "web_public_ip" { description = "Public IP of the web instance" value = aws_instance.web.public_ip }
- Use local values sparingly; overuse makes code harder to understand.
- If referenced in multiple files, define locals in a
locals.tffile. - If specific to one file, define locals at the top of that file.
- Use descriptive nouns with underscores for local value names.
- Always include a default provider configuration (without
alias). - Define all providers in the same file.
- If multiple instances of a provider exist, define the default first.
- For non-default providers, the
aliasparameter must be the first parameter in the block.
- Use
countandfor_eachsparingly; they add complexity. - Use
countwhen resources are almost identical. - Use
for_eachwhen arguments need distinct values not derivable from an integer. - A common pattern for conditional resources:
count = var.condition ? 1 : 0. - If the effect of a meta-argument is not immediately obvious, add a comment.
main.tf— resource and data source blocks (or split by logical group as the codebase grows).variables.tf— all variable blocks, in alphabetical order.outputs.tf— all output blocks, in alphabetical order.providers.tf— allproviderblocks and configuration.terraform.tf— singleterraformblock withrequired_versionandrequired_providers.backend.tf— backend configuration.locals.tf— local values (if shared across files).override.tf— override definitions (use sparingly, comment the original resource).- When the codebase grows, split resources into logically named files (e.g.,
network.tf,storage.tf,compute.tf). It should be immediately clear where to find any resource.
- Pin provider versions in
required_providers. - Pin module versions to a specific major and minor version.
- Set a minimum
required_versionfor the Terraform binary in theterraformblock.terraform { required_providers { aws = { source = "hashicorp/aws" version = "5.34.0" } } required_version = ">= 1.7" }
- For registry modules, use the
versionparameter in themoduleblock.
- Group logically related resources into modules.
- Store local (child) modules in
./modules/<module_name>. - Follow the standard module structure.
- Name module repositories
terraform-<PROVIDER>-<NAME>if publishing to a registry.
Do NOT commit:
terraform.tfstateandterraform.tfstate.*backup files..terraform.tfstate.lock.info..terraform/directory.- Saved plan files (from
terraform plan -out). .tfvarsfiles containing sensitive information.
Always commit:
- All
.tfcode files. .terraform.lock.hcldependency lock file..gitignore.README.md.
- Never store secrets in plain-text Terraform files.
- Use provider-specific environment variables for credentials.
- Use a secrets manager (e.g., HashiCorp Vault) where possible.
- Mark sensitive variables with
sensitive = true.
- Write tests for Terraform modules.
- Run tests as pre-merge checks in pull requests or as CI/CD pipeline steps.
- Use a linter such as TFLint to enforce coding standards.
- Run
terraform fmtandterraform validatebefore every commit.