Terraform
Terraform Standards
1. Module Structure
Every Terraform module follows a consistent file structure. This makes modules predictable — anyone on the team knows exactly where to look for any given concern.
module/ ├── main.tf # Resources ├── variables.tf # Input variables with descriptions ├── outputs.tf # Output values ├── versions.tf # Provider and Terraform version constraints └── README.md # Usage, inputs, outputs
Do
One resource type per file for complex modules. Split iam.tf, networking.tf, compute.tf when main.tf exceeds ~150 lines.
Don't
Put multiple unrelated resource types in main.tf. When main.tf exceeds 300 lines, it is a signal that the module has too many responsibilities.
2. Remote State
Always use S3 backend with DynamoDB locking. Local state fails the moment two engineers or two pipeline runs execute simultaneously. State files are encrypted at rest with SSE-S3.
terraform {
backend "s3" {
bucket = "your-org-terraform-state"
key = "project/env/component.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
} Do
Use separate state files per environment and component. dev/vpc.tfstate, dev/eks.tfstate, prod/vpc.tfstate
Don't
Use local state in CI/CD. The state file disappears with
the runner. Don't commit .tfstate files to version control.
Warning
Never commit .tfstate files. They contain plaintext secrets, resource IDs and configuration
that should never be in version control. Add *.tfstate and *.tfstate.backup to .gitignore.
3. Variable Naming
Use snake_case throughout. Prefix variables with their resource type for clarity.
Every variable must have a description.
variable "rds_instance_class" {
description = "RDS instance type for the primary database"
type = string
default = "db.t3.micro"
}
variable "eks_node_count" {
description = "Desired number of EKS worker nodes"
type = number
default = 2
}
variable "eks_min_node_count" {
description = "Minimum number of EKS worker nodes for autoscaling"
type = number
default = 1
} ✅ Correct prefixes
rds_— database resourceseks_— cluster resourcesvpc_— networking resourcesiam_— access and roles
❌ Avoid
db_class— ambiguous resource type-
nodeCount— camelCase breaks convention -
x,val— no descriptive name - Variables without
description
4. OIDC Authentication
Never use static AWS credentials in CI/CD pipelines. Always configure GitHub Actions OIDC provider. This gives pipelines 15-minute ephemeral tokens scoped to minimum required permissions.
# In your GitHub Actions workflow
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-role
aws-region: eu-west-1
# Never use this:
# aws-access-key-id: NEVER_USE_STATIC_KEYS
# aws-secret-access-key: NEVER_USE_STATIC_KEYS
The Terraform OIDC provider and the deploy role require
separate IAM roles with separate permission sets. The
Terraform role needs terraform:* on state resources. The deploy role needs only what the running
service requires.
5. Environment Separation
Separate state files per environment using variable files. Use Terraform workspaces only for truly identical infrastructure — different environments typically have different configurations and should use separate state files.
environments/
├── dev/
│ ├── terraform.tfvars # dev-specific values
│ └── backend.tf # dev state configuration
└── prod/
├── terraform.tfvars # prod-specific values
└── backend.tf # prod state configuration
# Apply
terraform init -backend-config=environments/dev/backend.tf
terraform apply -var-file=environments/dev/terraform.tfvars Do
Use separate state files and variable files per environment. The differences between environments should be explicit, auditable and version-controlled.
Warning
Terraform workspaces share the same backend configuration. If dev and prod differ in more than variable values (topology, enabled modules, resource sizes), use separate state files, not workspaces.