Terraform fmt, validate & destroy — The Cleanup Crew

terraform fmt, validate & destroy — The Cleanup Crew
terraform fmt — Why Formatting Matters More Than You Think

HCL has an official canonical format. terraform fmt enforces it automatically — consistent indentation, aligned = signs, normalized whitespace.

terraform fmt               # Format all .tf files in current directory
terraform fmt -check        # Check only (exit code 1 if unformatted — useful in CI)
terraform fmt -diff         # Show what would change without writing
terraform fmt -recursive    # Format recursively including subdirectories

Before:

resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type    =    "t2.micro"
  tags = {Name="web"}
}

After terraform fmt:

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags          = { Name = "web" }
}

Why does formatting matter? Because inconsistent formatting creates noisy diffs. If one person uses 2-space indentation and another uses 4-space, every file touchpoint generates formatting changes that obscure the actual infrastructure changes in code review. Enforcing terraform fmt means diffs only contain meaningful changes.

Run terraform fmt -recursive before every commit. Better yet, add it as a pre-commit hook or enforce it in CI with -check. If a PR contains unformatted HCL, that’s a signal the developer’s workflow has a gap.

An important distinction: terraform fmt only reformats whitespace and alignment. It does NOT validate whether your resource arguments are correct, whether your providers are configured, or whether your config is syntactically valid. It’s a cosmetic tool, not a correctness tool.

terraform validate — Catching Errors Before They Hit the Cloud

After terraform init, you can validate your configuration without making any API calls:

terraform validate

What validate checks — and what it doesn’t — is a distinction worth understanding:

What it catches (configuration-level errors):
– Syntax errors — malformed HCL, unclosed braces, invalid characters
– Type mismatches — passing a string where a number is expected
– Invalid attribute names — using an argument that doesn’t exist for a resource type
– Missing required arguments — leaving out a field the provider requires
– Reference errors — referencing a resource or variable that doesn’t exist

What it does NOT catch (runtime errors):
– Whether your provider credentials are valid
– Whether an AMI ID actually exists in AWS
– Whether your IAM role has sufficient permissions
– Whether you’ve exceeded your account’s resource quotas
– Whether an IP range is available or a name is already taken

$ terraform validate
Success! The configuration is valid.

Or:

$ terraform validate
│ Error: Missing required argument
│   on main.tf line 3, in resource "aws_instance" "web":
│ The argument "ami" is required, but no definition was found.

The practical implication: a passing terraform validate doesn’t guarantee terraform apply will succeed. Validation catches config-level errors — the easy stuff. Runtime errors like “permission denied,” “quota exceeded,” or “resource not found” only appear during plan or apply when Terraform actually talks to the cloud API.

Still, validation is essential in CI/CD because it’s fast (runs in milliseconds, no API calls), catches common mistakes early, and provides clear error messages with line numbers. Every pipeline should run terraform validate before terraform plan.

One prerequisite: terraform validate requires terraform init to have been run first. It needs the provider schemas installed to know what valid arguments look like. Without init, you’ll get a different error about missing providers.

Validation vs. Runtime Errors — Know the Difference

This distinction matters because it determines when you’ll catch problems:

Error TypeCaught ByExampleWhen
Syntax errorfmt, validateMissing closing braceImmediately
Invalid argumentvalidateintance_type (typo)After init
Wrong typevalidateString where number expectedAfter init
Wrong valuePlan/ApplyAMI ID doesn’t exist in regionAt plan time
Permission deniedApplyIAM role lacks EC2 permissionsAt apply time
Quota exceededApplyVPC limit reached in accountAt apply time
Name conflictApplyS3 bucket name already takenAt apply time

The earlier you catch errors, the less damage they cause. Validation catches categories 1-3 in seconds. Planning catches category 4 in seconds to minutes. Apply failures (categories 5-7) are the most expensive because they may have already partially modified your infrastructure.

terraform destroy — Tearing It All Down
terraform destroy              # Shows destroy plan, asks for confirmation
terraform destroy -auto-approve  # No confirmation prompt

terraform destroy is syntactic sugar for:

terraform apply -destroy

They do exactly the same thing.

What it destroys: Everything tracked in the current state file. All resources Terraform manages in this workspace. Nothing more — resources created manually or by other tools are untouched.

Destroy order: The reverse of the dependency graph. If the subnet depends on the VPC, destroy order is: subnet first, then VPC. Terraform automatically determines this from its dependency tracking.

Destroy order (reverse of creation):
1. aws_instance.web        (created last → destroyed first)
2. aws_subnet.public
3. aws_internet_gateway.main
4. aws_vpc.main             (created first → destroyed last)

Why reverse order? Because you can’t delete a VPC while it still contains subnets, and you can’t delete a subnet while instances are still running in it. The dependency graph ensures resources are removed in an order that respects these constraints.

Targeted Destroy
terraform destroy -target=aws_instance.web

Same caveats as -target for apply — it can leave dependent resources in an inconsistent state. If you destroy only the EC2 instance but not the security group that was created specifically for it, you’re left with an orphaned security group that Terraform still tracks but that serves no purpose.

Targeted destroy is a troubleshooting tool, not a workflow.

Protecting Resources from Destroy

Some resources you never want destroyed accidentally — production databases, state backends, DNS zones. Use lifecycle:

resource "aws_rds_instance" "prod_db" {
  ...
  lifecycle {
    prevent_destroy = true
  }
}

Now any operation that would destroy this resource — terraform destroy, terraform apply after removing it from config, or a -/+ replacement — will error out:

Error: Instance cannot be destroyed
│ Resource aws_rds_instance.prod_db has lifecycle.prevent_destroy
│ set, but the plan calls for this resource to be destroyed.

This is a safety net, not a lock. To actually destroy the resource, you remove prevent_destroy = true from the config, apply, and then run destroy. The two-step process is intentional — it forces you to explicitly acknowledge what you’re about to do.

The Pre-Commit Routine

Every time before committing Terraform changes — whether locally or in CI:

# 1. Format — consistent style
terraform fmt -recursive

# 2. Validate — catch syntax and type errors
terraform validate

# 3. Plan — see what will actually change
terraform plan

This catches the vast majority of problems before they reach a pull request. Format catches style issues. Validate catches config errors. Plan catches logical errors and unintended changes.

Key Takeaways

fmt and validate are different tools solving different problems. fmt reformats whitespace and alignment — it’s cosmetic. validate checks for logical and syntactic errors — it’s correctness. Neither one guarantees a successful apply; they’re complementary layers in a defense-in-depth approach.

terraform destroy only touches resources in the current workspace’s state file. It won’t destroy resources in other workspaces, other Terraform projects, or resources created outside of Terraform. The scope is always limited to what Terraform currently manages.

prevent_destroy is one of the most important lifecycle arguments for production workloads. Apply it to anything whose accidental deletion would cause data loss: databases, S3 buckets containing important data, DNS zones, state backend resources. The small inconvenience of a two-step destroy is worth the protection against catastrophic accidents.