Concept
MediumTerraform 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 1What 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 type | Runs during | Evaluates |
|---|---|---|
validation block | plan | Variable values only |
precondition | plan and apply | Any expression before resource creation |
postcondition | apply | self (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.hcl6. 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 planTF_LOG levels:
| Level | Shows |
|---|---|
ERROR | Fatal errors only |
WARN | Warnings and errors |
INFO | High-level operations (default for most troubleshooting) |
DEBUG | Detailed operations including HTTP requests |
TRACE | Extremely 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
| Tool | What It Checks | Install |
|---|---|---|
| tflint | Provider-specific rules, deprecated resources, naming conventions | brew install tflint |
| checkov | Security and compliance policies (CIS, PCI, HIPAA) | pip install checkov |
| tfsec | Security misconfigurations (now merged into trivy) | brew install tfsec |
| trivy | Unified security scanner including IaC | brew install trivy |
| infracost | Cost estimation from Terraform plans | brew 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"
15fi10. Quick Reference
| Tool / Feature | Purpose |
|---|---|
terraform validate | Syntax and type checking; no API calls |
terraform fmt | Auto-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 test | Run .tftest.hcl unit and integration tests (v1.6+) |
TF_LOG=DEBUG | Enable verbose logging for troubleshooting |
TF_LOG_PATH | Write logs to a file |
terraform console | Interactive expression and function evaluator |
tflint | Provider-specific linting rules |
checkov / trivy | Security policy scanning |
-detailed-exitcode | Distinguish no-change vs change-needed in CI |