|
| 1 | +# Terraform Style Guide |
| 2 | + |
| 3 | +This file defines the Terraform style conventions for this repository, based on the [HashiCorp Terraform Style Guide](https://developer.hashicorp.com/terraform/language/style). All Terraform code in this workspace MUST conform to these rules. When reviewing or generating Terraform code, enforce every rule below. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Code Formatting |
| 8 | + |
| 9 | +- Indent **two spaces** per nesting level. |
| 10 | +- Align `=` signs when multiple single-line arguments appear on consecutive lines at the same nesting level: |
| 11 | + ```hcl |
| 12 | + ami = "abc123" |
| 13 | + instance_type = "t2.micro" |
| 14 | + ``` |
| 15 | +- Place all arguments at the **top** of a block, then nested blocks **below**, separated by one blank line. |
| 16 | +- Use **empty lines** to separate logical groups of arguments within a block. |
| 17 | +- **Meta-arguments first**: list meta-arguments (`count`, `for_each`) at the top of a resource block, separated from other arguments by one blank line. |
| 18 | +- **Meta-argument blocks last**: place `lifecycle`, `depends_on` blocks at the bottom, separated from other blocks by one blank line. |
| 19 | + ```hcl |
| 20 | + resource "aws_instance" "example" { |
| 21 | + # meta-argument first |
| 22 | + count = 2 |
| 23 | +
|
| 24 | + ami = "abc123" |
| 25 | + instance_type = "t2.micro" |
| 26 | +
|
| 27 | + network_interface { |
| 28 | + # ... |
| 29 | + } |
| 30 | +
|
| 31 | + # meta-argument block last |
| 32 | + lifecycle { |
| 33 | + create_before_destroy = true |
| 34 | + } |
| 35 | + } |
| 36 | + ``` |
| 37 | +- Separate **top-level blocks** with exactly one blank line. |
| 38 | +- Separate **nested blocks** with blank lines, except when grouping related blocks of the same type. |
| 39 | +- Run `terraform fmt` before committing. Use `-recursive` to format subdirectories. |
| 40 | + |
| 41 | +## Code Validation |
| 42 | + |
| 43 | +- Run `terraform validate` before committing to check syntax and internal consistency. |
| 44 | + |
| 45 | +## Comments |
| 46 | + |
| 47 | +- Use `#` for **all** comments (single-line and multi-line). Do NOT use `//` or `/* */`. |
| 48 | +- Write self-explanatory code; only add comments when necessary to clarify complexity. |
| 49 | + |
| 50 | +## Resource Naming |
| 51 | + |
| 52 | +- Use a **descriptive noun** for every resource name. |
| 53 | +- Separate words with **underscores** (`_`), not hyphens or camelCase. |
| 54 | +- Do NOT include the resource type in the resource name (the address already contains it). |
| 55 | +- Wrap both resource type and name in **double quotes**. |
| 56 | + |
| 57 | + **Bad:** |
| 58 | + ```hcl |
| 59 | + resource aws_instance webAPI-aws-instance {...} |
| 60 | + ``` |
| 61 | + **Good:** |
| 62 | + ```hcl |
| 63 | + resource "aws_instance" "web_api" {...} |
| 64 | + ``` |
| 65 | + |
| 66 | +## Resource Order |
| 67 | + |
| 68 | +- Define **data sources before** the resources that reference them so code "builds on itself". |
| 69 | +- Within a resource block, order parameters as follows: |
| 70 | + 1. `count` or `for_each` meta-argument |
| 71 | + 2. Resource-specific non-block parameters |
| 72 | + 3. Resource-specific block parameters |
| 73 | + 4. `lifecycle` block (if required) |
| 74 | + 5. `depends_on` (if required) |
| 75 | + |
| 76 | +## Variables |
| 77 | + |
| 78 | +- Every variable MUST have a `type` and a `description`. |
| 79 | +- Provide a `default` for optional variables. |
| 80 | +- Set `sensitive = true` for passwords, private keys, and other secrets. |
| 81 | +- Use `validation` blocks only when values have uniquely restrictive requirements. |
| 82 | +- Order variable parameters: |
| 83 | + 1. `type` |
| 84 | + 2. `description` |
| 85 | + 3. `default` (optional) |
| 86 | + 4. `sensitive` (optional) |
| 87 | + 5. `validation` blocks |
| 88 | + |
| 89 | + ```hcl |
| 90 | + variable "db_disk_size" { |
| 91 | + type = number |
| 92 | + description = "Disk size for the API database" |
| 93 | + default = 100 |
| 94 | + } |
| 95 | +
|
| 96 | + variable "db_password" { |
| 97 | + type = string |
| 98 | + description = "Database password" |
| 99 | + sensitive = true |
| 100 | + } |
| 101 | + ``` |
| 102 | + |
| 103 | +## Outputs |
| 104 | + |
| 105 | +- Every output MUST have a `description`. |
| 106 | +- Order output parameters: |
| 107 | + 1. `description` |
| 108 | + 2. `value` |
| 109 | + 3. `sensitive` (optional) |
| 110 | + |
| 111 | + ```hcl |
| 112 | + output "web_public_ip" { |
| 113 | + description = "Public IP of the web instance" |
| 114 | + value = aws_instance.web.public_ip |
| 115 | + } |
| 116 | + ``` |
| 117 | + |
| 118 | +## Local Values |
| 119 | + |
| 120 | +- Use local values **sparingly**; overuse makes code harder to understand. |
| 121 | +- If referenced in multiple files, define locals in a `locals.tf` file. |
| 122 | +- If specific to one file, define locals at the **top** of that file. |
| 123 | +- Use descriptive nouns with underscores for local value names. |
| 124 | + |
| 125 | +## Provider Configuration |
| 126 | + |
| 127 | +- Always include a **default provider configuration** (without `alias`). |
| 128 | +- Define **all providers** in the same file. |
| 129 | +- If multiple instances of a provider exist, define the **default first**. |
| 130 | +- For non-default providers, the `alias` parameter must be the **first** parameter in the block. |
| 131 | + |
| 132 | +## Dynamic Resource Count (`count` / `for_each`) |
| 133 | + |
| 134 | +- Use `count` and `for_each` **sparingly**; they add complexity. |
| 135 | +- Use `count` when resources are almost identical. |
| 136 | +- Use `for_each` when arguments need distinct values not derivable from an integer. |
| 137 | +- A common pattern for conditional resources: `count = var.condition ? 1 : 0`. |
| 138 | +- If the effect of a meta-argument is not immediately obvious, add a comment. |
| 139 | + |
| 140 | +## File Naming Conventions |
| 141 | + |
| 142 | +- `main.tf` — resource and data source blocks (or split by logical group as the codebase grows). |
| 143 | +- `variables.tf` — all variable blocks, in **alphabetical order**. |
| 144 | +- `outputs.tf` — all output blocks, in **alphabetical order**. |
| 145 | +- `providers.tf` — all `provider` blocks and configuration. |
| 146 | +- `terraform.tf` — single `terraform` block with `required_version` and `required_providers`. |
| 147 | +- `backend.tf` — backend configuration. |
| 148 | +- `locals.tf` — local values (if shared across files). |
| 149 | +- `override.tf` — override definitions (use sparingly, comment the original resource). |
| 150 | +- 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. |
| 151 | + |
| 152 | +## Version Pinning |
| 153 | + |
| 154 | +- Pin **provider versions** in `required_providers`. |
| 155 | +- Pin **module versions** to a specific major and minor version. |
| 156 | +- Set a minimum `required_version` for the Terraform binary in the `terraform` block. |
| 157 | + ```hcl |
| 158 | + terraform { |
| 159 | + required_providers { |
| 160 | + aws = { |
| 161 | + source = "hashicorp/aws" |
| 162 | + version = "5.34.0" |
| 163 | + } |
| 164 | + } |
| 165 | + required_version = ">= 1.7" |
| 166 | + } |
| 167 | + ``` |
| 168 | +- For registry modules, use the `version` parameter in the `module` block. |
| 169 | + |
| 170 | +## Module Structure |
| 171 | + |
| 172 | +- Group logically related resources into modules. |
| 173 | +- Store local (child) modules in `./modules/<module_name>`. |
| 174 | +- Follow the [standard module structure](https://developer.hashicorp.com/terraform/language/modules/develop/structure). |
| 175 | +- Name module repositories `terraform-<PROVIDER>-<NAME>` if publishing to a registry. |
| 176 | + |
| 177 | +## .gitignore |
| 178 | + |
| 179 | +Do NOT commit: |
| 180 | +- `terraform.tfstate` and `terraform.tfstate.*` backup files. |
| 181 | +- `.terraform.tfstate.lock.info`. |
| 182 | +- `.terraform/` directory. |
| 183 | +- Saved plan files (from `terraform plan -out`). |
| 184 | +- `.tfvars` files containing sensitive information. |
| 185 | + |
| 186 | +Always commit: |
| 187 | +- All `.tf` code files. |
| 188 | +- `.terraform.lock.hcl` dependency lock file. |
| 189 | +- `.gitignore`. |
| 190 | +- `README.md`. |
| 191 | + |
| 192 | +## Secrets Management |
| 193 | + |
| 194 | +- Never store secrets in plain-text Terraform files. |
| 195 | +- Use provider-specific environment variables for credentials. |
| 196 | +- Use a secrets manager (e.g., HashiCorp Vault) where possible. |
| 197 | +- Mark sensitive variables with `sensitive = true`. |
| 198 | + |
| 199 | +## Testing |
| 200 | + |
| 201 | +- Write tests for Terraform modules. |
| 202 | +- Run tests as pre-merge checks in pull requests or as CI/CD pipeline steps. |
| 203 | + |
| 204 | +## Linting |
| 205 | + |
| 206 | +- Use a linter such as [TFLint](https://github.com/terraform-linters/tflint) to enforce coding standards. |
| 207 | +- Run `terraform fmt` and `terraform validate` before every commit. |
0 commit comments