/Terraform Testing, Validation & Debugging
Concept
Medium

Terraform Testing, Validation & Debugging

5 min read

1. Layers of Terraform Validation

Rendering diagram…

2. terraform validate

Checks configuration for syntax errors and internal consistency without contacting any provider API:

bash
1terraform validate
2# Success! The configuration is valid.
3
4# In CI — non-zero exit code on failure
5terraform validate && echo "OK" || exit 1

What it checks:

  • HCL syntax is valid
  • Required arguments are present
  • Argument types are correct
  • References to undefined variables or resources
  • Module input types match

What it does NOT check:

  • Whether resources actually exist (no API calls)
  • Whether your credentials work
  • Logical correctness of values

3. Input Variable Validation Blocks

Enforce valid inputs at plan time with custom error messages:

hcl
1variable "environment" {
2  type        = string
3  description = "Deployment environment"
4
5  validation {
6    condition     = contains(["dev", "staging", "prod"], var.environment)
7    error_message = "environment must be one of: dev, staging, prod."
8  }
9}
10
11variable "instance_count" {
12  type    = number
13  default = 1
14
15  validation {
16    condition     = var.instance_count >= 1 && var.instance_count <= 20
17    error_message = "instance_count must be between 1 and 20."
18  }
19}

4. Preconditions & Postconditions (Terraform 1.2+)

Assert conditions on resources and data sources during planning and applying:

hcl
1resource "aws_instance" "web" {
2  ami           = data.aws_ami.ubuntu.id
3  instance_type = var.instance_type
4
5  lifecycle {
6    precondition {
7      condition     = data.aws_ami.ubuntu.architecture == "x86_64"
8      error_message = "The selected AMI must be x86_64 architecture."
9    }
10
11    postcondition {
12      condition     = self.public_ip != ""
13      error_message = "Instance must have a public IP; check subnet settings."
14    }
15  }
16}
17
18data "aws_s3_bucket" "state" {
19  bucket = var.state_bucket
20
21  lifecycle {
22    postcondition {
23      condition     = self.versioning[0].enabled
24      error_message = "State bucket must have versioning enabled."
25    }
26  }
27}
Check typeRuns duringEvaluates
validation blockplanVariable values only
preconditionplan and applyAny expression before resource creation
postconditionapplyself (the created resource) after creation

5. Terraform Test Framework (Terraform 1.6+)

Write .tftest.hcl files for automated infrastructure testing:

hcl
1# tests/s3_bucket.tftest.hcl
2run "creates_bucket_with_versioning" {
3  command = plan   # or apply
4
5  variables {
6    bucket_name = "test-bucket-${run.id}"
7  }
8
9  assert {
10    condition     = aws_s3_bucket.this.bucket == "test-bucket-${run.id}"
11    error_message = "Bucket name does not match variable."
12  }
13
14  assert {
15    condition     = aws_s3_bucket_versioning.this.versioning_configuration[0].status == "Enabled"
16    error_message = "Versioning must be enabled."
17  }
18}
bash
1terraform test              # run all .tftest.hcl files
2terraform test -filter=tests/s3_bucket.tftest.hcl

6. Debugging with TF_LOG

Control log verbosity using the TF_LOG environment variable:

bash
1# Log levels (increasing verbosity): ERROR, WARN, INFO, DEBUG, TRACE
2export TF_LOG=DEBUG
3terraform apply
4
5# Log only provider plugin traffic
6export TF_LOG_PROVIDER=TRACE
7
8# Save logs to a file instead of stderr
9export TF_LOG_PATH=./terraform-debug.log
10export TF_LOG=DEBUG
11terraform plan

TF_LOG levels:

LevelShows
ERRORFatal errors only
WARNWarnings and errors
INFOHigh-level operations (default for most troubleshooting)
DEBUGDetailed operations including HTTP requests
TRACEExtremely verbose; includes full API request/response bodies

7. terraform console — Interactive Debugging

Test expressions and functions without running a full plan:

bash
1terraform console
2
3# Inside the console:
4> var.environment
5"prod"
6> local.common_tags
7{ "Environment" = "prod", "Project" = "web-app" }
8> length(var.availability_zones)
93
10> upper("hello")
11"HELLO"
12> cidrsubnet("10.0.0.0/16", 8, 0)
13"10.0.0.0/24"
14> jsondecode("{"key": "value"}")
15{ "key" = "value" }

8. External Linting Tools

ToolWhat It ChecksInstall
tflintProvider-specific rules, deprecated resources, naming conventionsbrew install tflint
checkovSecurity and compliance policies (CIS, PCI, HIPAA)pip install checkov
tfsecSecurity misconfigurations (now merged into trivy)brew install tfsec
trivyUnified security scanner including IaCbrew install trivy
infracostCost estimation from Terraform plansbrew install infracost
bash
1# tflint usage
2tflint --init
3tflint --recursive
4
5# checkov usage
6checkov -d .
7checkov -f main.tf
8
9# trivy iac scan
10trivy config .

9. plan Exit Codes for CI/CD

bash
1# -detailed-exitcode: differentiate "no changes" from "changes needed"
2terraform plan -detailed-exitcode
3# Exit 0 = succeeded, no changes
4# Exit 1 = error
5# Exit 2 = succeeded, changes present
6
7# Use in CI scripts:
8terraform plan -detailed-exitcode -out=plan.tfplan
9EXIT_CODE=$?
10if [ $EXIT_CODE -eq 1 ]; then
11  echo "Plan failed"
12  exit 1
13elif [ $EXIT_CODE -eq 2 ]; then
14  echo "Changes detected — awaiting approval"
15fi

10. Quick Reference

Tool / FeaturePurpose
terraform validateSyntax and type checking; no API calls
terraform fmtAuto-format HCL to canonical style
variable validation {}Enforce valid inputs with custom error messages
precondition {}Assert conditions before resource creation
postcondition {}Assert conditions on the created resource
terraform testRun .tftest.hcl unit and integration tests (v1.6+)
TF_LOG=DEBUGEnable verbose logging for troubleshooting
TF_LOG_PATHWrite logs to a file
terraform consoleInteractive expression and function evaluator
tflintProvider-specific linting rules
checkov / trivySecurity policy scanning
-detailed-exitcodeDistinguish no-change vs change-needed in CI