As Terraform projects grow, structure, consistency, and safety practices become critical. These patterns are used by platform engineering teams managing hundreds of resources across multiple accounts and regions.
Directory Structure
Organise Terraform code by environment and concern — not by resource type:
infrastructure/
modules/ # reusable modules
vpc/
eks/
rds/
environments/
dev/
networking/
main.tf
variables.tf
outputs.tf
terraform.tfvars
application/
main.tf
staging/
networking/
application/
production/
networking/
application/
Each environment+concern directory has its own state file. This limits the blast radius — a mistake in production/application cannot accidentally destroy production/networking.
File Organisation Within a Module
main.tf # primary resources
variables.tf # all input variable declarations
outputs.tf # all output declarations
versions.tf # required_version and required_providers
locals.tf # local values (in larger modules)
data.tf # data sources (in larger modules)
README.md # module documentation
Naming Conventions
# Resource names: snake_case, descriptive
resource "aws_security_group" "alb_ingress" { ... } # good
resource "aws_security_group" "sg1" { ... } # bad
# Resource Name tag: consistent pattern
tags = {
Name = "${var.project}-${var.environment}-${var.component}"
# e.g.: myapp-production-web
}
# Variables and locals: snake_case
variable "instance_count" { ... } # good
variable "instanceCount" { ... } # bad
Tagging Strategy
Apply consistent tags to every resource for cost allocation, security, and operations:
locals {
required_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
Owner = var.team_email
CostCenter = var.cost_center
}
}
# Merge required tags with resource-specific tags
resource "aws_instance" "app" {
tags = merge(local.required_tags, {
Name = "${local.name_prefix}-app"
Component = "web-server"
})
}
Security Practices
# .gitignore — ALWAYS include these
*.tfstate
*.tfstate.backup
*.tfvars # may contain secrets
.terraform/
crash.log
override.tf
- Never hardcode credentials — use environment variables or IAM roles
- Mark sensitive variables and outputs as
sensitive = true - Use a secrets manager (AWS Secrets Manager, HashiCorp Vault) for values that must be retrieved at apply time
- Enable S3 bucket versioning and encryption for state files
- Restrict IAM permissions: Terraform should only have the permissions it needs
Version Pinning
terraform {
required_version = ">= 1.6.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # allow 5.x patch/minor updates
}
}
}
# Commit .terraform.lock.hcl to ensure all team members
# and CI use the exact same provider version
Testing and Linting
- terraform fmt -check: Enforce formatting in CI
- terraform validate: Syntax and type checking
- tflint: Provider-specific lint rules (catches invalid instance types, deprecated arguments)
- tfsec / Checkov / KICS: Security scanning — finds open security groups, unencrypted resources
- Terratest: Go-based testing framework for writing integration tests for Terraform modules
- terraform test: Native Terraform testing (1.6+)
State Management Safety
- Never run
terraform applyfrom a local machine against production — use CI/CD - Use
prevent_destroy = trueon databases, state buckets, and other critical resources - Use
-targetsparingly — it leaves state partially applied and can cause drift - Keep state files small — split large configurations into multiple state files
You've covered the Terraform fundamentals — IaC concepts, HCL syntax, providers, resources, state, modules, backends, and best practices. Practice by building real AWS infrastructure with Terraform: start with a VPC, add EC2 instances, then try EKS or RDS.