Terraform Coding Best Practices
1. General Principles
- Idempotency: Write code that produces the same result every time it is applied.
- Declarative, not imperative: Let Terraform handle the "how." Describe the desired state.
- Environment agnostic: Make code reusable across environments via variables and workspaces.
2. File and Project Structure
/terraform
/modules
/<module_name>
- main.tf
- variables.tf
- outputs.tf
/envs
/dev
/prod
- main.tf
- backend.tf
- variables.tf
- terraform.tfvars
- versions.tf
- provider.tf
- README.md
- Use logical separation (e.g., environments vs modules).
- Use backend.tf for remote state configuration.
- Use terraform.tfvars files for environment-specific values.
3. Naming Conventions
- Use snake_case for resources and variables.
- Prefix resources with their context (e.g.,
vpc_prod_main
).
- Name modules descriptively:
network
, compute
, iam
.
Example:
resource "aws_instance" "web_prod_app1" {
...
}
4. Modules
- Write small, focused modules (single purpose).
- Always use versioned modules when using external sources.
- Include:
main.tf
(resources)
variables.tf
(inputs)
outputs.tf
(exposed values)
- Do not hardcode values; always use variables.
5. Variables and Outputs
Variables
- Provide type, description, and defaults.
- Use validation blocks where applicable.
variable "region" {
type = string
description = "AWS region"
default = "us-east-1"
}
Outputs
- Only expose necessary values.
- Name outputs clearly.
output "vpc_id" {
value = aws_vpc.main.id
description = "The ID of the main VPC"
}
6. Version Pinning
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
- Pin Terraform and provider versions to avoid drift and breakage.
7. Security Best Practices
- Use remote backends with locking (e.g., S3 + DynamoDB for AWS).
- Use data sources to fetch existing values instead of hardcoding.
- Never commit secrets or state files to Git.
- Use environment variables or secret managers for sensitive data.
8. State Management
- Use remote state with proper locking and encryption.
- Use state file separation per environment.
- Use
terraform state
commands to move or inspect resources.
9. Linting, Validation, and Testing
- Use
terraform fmt
to standardize formatting.
- Use
terraform validate
before applying.
- Integrate
tflint
, checkov
, and terrascan
for static analysis.
- Use automated tests with tools like
terratest
or kitchen-terraform
.
10. CI/CD Integration
- Use pipelines to:
- Lint and validate
- Plan with
-out
option
- Enforce manual approval before
apply
- Store
terraform plan
output as artifacts.
11. Documentation
- Include README in each module with:
- Purpose
- Inputs/Outputs
- Usage examples
- Document resource changes and module versions.
12. Git Best Practices
- Use feature branches and pull requests.
- Include plan output in PR description.
- Tag releases for module versions.
- Use
.terraform.lock.hcl
to ensure deterministic builds.
13. Clean Code Tips for LLMs
- Use clear, consistent indentation and spacing.
- Comment why, not just what.
- Break large resource definitions into logical parts.
- Favor readability over cleverness.
14. Bonus: AI-Friendly Formatting
- Use consistent patterns that make code easily parseable.
- Avoid dynamic code generation unless well-justified.
- Use descriptive variable and output names.
Example: Clean, Modular Terraform Usage
module "vpc" {
source = "../modules/vpc"
name = "prod-vpc"
cidr = "10.0.0.0/16"
region = var.region
}
output "vpc_id" {
value = module.vpc.vpc_id
description = "The ID of the VPC"
}
Summary
These best practices aim to ensure:
- Readable, maintainable code
- Safe and secure deployments
- AI-generatable, modular, and robust infrastructure
Always test your code. Always document your intent.