Terraform Basics #3: Clean Code and Standards — Writing Scalable Terraform Like a Pro
“A Practical Guide to Writing Maintainable Terraform That Teams Can Rely On”
In the last post, we talked about what makes Terraform code OK, Good, and Best. Today, we’re going beyond tutorials — into the mindset and techniques of writing clean, scalable, and professional Terraform code.
This post isn't about using modules — it's about how to write Terraform that your future self and teammates will thank you for. From naming conventions to structure, from dynamic blocks to pre-commit hooks — this is the coding discipline that separates demo code from production-grade.
Why Coding Standards Matter in Terraform
Terraform is code — and like any codebase, it can become messy, repetitive, or hard to debug. Clean Terraform code is:
Easier to collaborate on
Easier to debug and extend
Safer to deploy repeatedly
There’s more than one way to do things in Terraform — so we’ll compare basic vs dynamic patterns to help you pick the right approach for your use case.
File and Project Structure
Basic (OK):
Everything in a single
main.tf
file
Clean (Better):
project/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
Best:
project/
├── modules/
│ └── compute/
├── environments/
│ └── dev/
├── scripts/ (optional)
├── README.md
Organize by component (module) and by environment. Use root-level files for orchestration.
Variable Declaration
Basic:
variable "vm_name" {}
Professional:
variable "vm_name" {
description = "Name of the virtual machine"
type = string
validation {
condition = length(var.vm_name) > 2
error_message = "VM name must be at least 3 characters."
}
}
Use description
, type
, and validation
to document and enforce expected input.
Resource Declaration
Terraform resources define the actual infrastructure components you want to create. A clean declaration helps ensure reusability and clarity — especially when your infrastructure grows beyond a handful of resources.
Basic (Static):
resource "google_compute_instance" "demo" {
name = "vm1"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
network = "default"
access_config {}
}
}
This works for a single instance but isn't scalable. Everything is hardcoded, making changes or expansion repetitive and error-prone.
Dynamic (Clean + Reusable):
resource "google_compute_instance" "vm" {
for_each = var.instances
name = each.value.name
zone = each.value.zone
machine_type = each.value.machine_type
boot_disk {
initialize_params {
image = each.value.image
}
}
network_interface {
network = each.value.network
access_config {}
}
}
With this approach, you're looping over a map of VM definitions — allowing flexible, repeatable VM creation.
variable "instances" {
description = "Map of VM configurations"
type = map(object({
name = string
zone = string
machine_type = string
image = string
network = string
}))
}
You can define your terraform.tfvars
like this:
instances = {
web1 = {
name = "web-1"
zone = "us-central1-a"
machine_type = "e2-medium"
image = "debian-cloud/debian-11"
network = "default"
},
web2 = {
name = "web-2"
zone = "us-central1-b"
machine_type = "e2-standard-2"
image = "debian-cloud/debian-11"
network = "default"
}
}
This allows for scalable deployments with minimal code duplication. It’s easier to maintain, extend, and version control.
Tip: Dynamic blocks make your code future-proof. Start simple, but keep scalability in mind.
Dynamic (Clean + Reusable):
resource "google_compute_instance" "vm" {
for_each = var.instances
name = each.value.name
zone = each.value.zone
...
}
variable "instances" {
type = map(object({
name = string
zone = string
}))
}
Use for_each
with map(object)
for scalable patterns.
Outputs and Documentation
OK: No outputs or no descriptions
Best:
output "vm_ip" {
description = "The public IP address of the VM"
value = google_compute_instance.vm.network_interface[0].access_config[0].nat_ip
}
Use terraform-docs
to auto-generate markdown docs for modules.
Code Quality and Tooling
Run these regularly:
terraform fmt
— enforces consistent styleterraform validate
— catches syntax and logic issuestflint
— linter for common Terraform anti-patternscheckov
— security scanning (optional)
Automate using pre-commit hooks:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.77.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
Naming Conventions
Use
kebab-case
for filenames:main.tf
,backend.tf
Use
snake_case
for variable names:project_id
,region
Use consistent prefixes for resources:
vm_web
,bucket_logs
Avoid magic names like demo
or test-resource
— name things to be self-explanatory.
Comparison: Basic vs Clean
Clean Code Checklist
Here's a summary of the most important Terraform clean code practices to guide your everyday work:
Areas for Further Improvement
Even great Terraform code can be made stronger with a few more enhancements:
Before/After snapshots: Show side-by-side comparisons of poorly written vs refactored code for better understanding
Local-first mindset: All tools like
fmt
,validate
,tflint
, andcheckov
can be run locally — no CI/CD needed. This makes adoption easier for individuals and small teams.Installation links: Include links or one-liners for installing tools like
tflint
andcheckov
to reduce friction for beginnersOptional visuals: A simple diagram of clean Terraform structure or lifecycle can be helpful for visual learners
What's Next
You’ve learned how to structure and write Terraform like a professional — but clean code is only one piece of the puzzle. Next, we’ll shift our focus to module reuse, which is essential for scaling infrastructure work across teams and environments.
Here’s what we’ll explore:
How to use modules stored locally in the same project directory
How to consume modules from Git repositories like GitHub
How to pull and leverage public modules from the Terraform Registry
We’ll compare these options based on:
Ease of use and setup
Versioning and maintainability
Portability and collaboration
This will give you the confidence to build smarter, reusable Terraform patterns across your stack.
Summary
Clean Terraform code isn't about being fancy — it's about being consistent, predictable, and scalable.
In this post, you learned:
The difference between basic and professional infrastructure code
How to structure files, use validation, and document outputs
How to declare resources dynamically and consistently
How to improve code quality with linting, formatting, and hooks
By adopting these habits early, you’ll write infrastructure code that’s easier to share, extend, and maintain.
Stay tuned for the next post — we’ll cover how to reuse infrastructure more effectively by leveraging modules from local folders, Git repositories, and the Terraform Registry.