What validate does
terraform validate answers one question: would your HCL compile if
Terraform tried to execute it? It runs without contacting AWS, without
reading state, and without downloading providers (provided they are already
downloaded).
Specifically, validate catches:
- Broken HCL syntax: an unclosed brace, a missing quote.
- Unknown arguments:
bucket_nameinstead ofbucketonaws_s3_bucket. - Wrong types:
count = "three"(a number is required). - References to resources, variables, or locals that do not exist.
- Dependency cycles (cycle detected).
- Wrong function signatures:
format()called with no arguments.
What validate does not catch:
- A bucket with that name is already taken (that is plan/apply territory).
- You lack permissions in AWS (plan/apply again).
- Drift in state (that is refresh/plan).
- A
datasource that returns an empty result.
Basic usage
cd ~/myproject
terraform init -backend=false # run once to download the provider
terraform validate
On success:
Success! The configuration is valid.
On error:
Error: Unsupported argument
on main.tf line 5, in resource "aws_s3_bucket" "demo":
5: bucket_name = "my-bucket"
An argument named "bucket_name" is not expected here.
Did you mean "bucket"?
Note that Terraform offers a suggestion via "Did you mean". This works
for similar names: bucket_name becomes bucket, tag becomes tags.
Why you need init -backend=false
To validate, Terraform needs the provider binaries; without them it does not
know the resource schemas. But terraform init without flags tries to
configure the backend. If the backend is remote, it contacts S3 and asks for
credentials.
You do not need that on every PR. The standard pattern is:
terraform init -backend=false -input=false
terraform validate
-backend=falseskips backend setup and does not connect to remote state.-input=falsedisables interactive prompts (CI has nothing to type).
This is the standard PR-review pattern: check that HCL is valid without touching prod state.
Machine-readable output
For IDE integrations and automation, use -json:
terraform validate -json
Output:
{"format_version": "1.0",
"valid": false,
"error_count": 1,
"warning_count": 0,
"diagnostics": [
{"severity": "error",
"summary": "Unsupported argument",
"detail": "An argument named \"bucket_name\" is not expected here.",
"range": {"filename": "main.tf",
"start": {"line": 5, "column": 3}, "end": {"line": 5, "column": 14}}
}
]
}
Any IDE can parse this and highlight errors directly in the editor. All Terraform plugins (VS Code, JetBrains) run this same JSON output under the hood.
How validate differs from plan
| validate | plan | |
|---|---|---|
| Contacts the cloud | no | yes (refresh) |
| Reads state | no | yes |
| Downloads provider | yes (via init) | yes |
| Speed | one second | seconds to minutes |
| Catches typos and wrong names | yes | yes |
| Catches missing cloud resources | no | yes |
| Works in PR without AWS credentials | yes | no |
Rule of thumb: run validate on every save in the IDE. Run plan on every PR (with credentials).
Pitfalls
-
Validate requires at least one
initrun. Without.terraform/providers/in the directory, validate does not know the resource schemas and fails with "provider not configured". In a fresh CI build, runinit -backend=falsebefore each validate. -
Validate checks one root module. If your repository has
environments/prod/andenvironments/staging/, you need to validate each directory separately. A single validate from the repository root does not cover them all. -
Validate does not substitute variable values. If HCL references
var.fooand the variable is not declared, that is an error. If the variable is declared but has nodefault, validate passes because the value may arrive at runtime. -
Validate does not catch logic errors.
count = var.enabled ? 1 : 0passes even ifvar.enabledis always false: the expression is valid, just useless. -
Strictness changes between TF versions. In TF 1.5+ validate became stricter about provider-schema incompatibilities. After upgrading TF, run validate against your existing projects.