| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/local provider 2.9.0 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user |
| Scope | terraform validate — scope, successful validation, syntax and schema errors, invalid references, type mismatches, -json output, init -backend=false workflow, comparisons with plan and fmt, and a brief CI pattern. Does not cover custom validation blocks, Checkov or TFLint, cloud authentication, state locking, or full pipeline design. |
| Related guides | terraform init Terraform HCL syntax Terraform providers Terraform lab environment on Ubuntu Terraform Associate certification course |
After you edit .tf files, the fastest sanity check is:
terraform validateterraform validate tells you whether Terraform can parse your configuration and whether arguments, types, and references line up with installed provider schemas. It runs locally and finishes in seconds, which makes it ideal before you commit or open a pull request.
Every command in this walkthrough runs in one working directory, ~/terraform-labs/terraform-validate/. You initialize the lab once, then overwrite main.tf with heredocs for each example so you can copy the article step by step.
What terraform validate checks
terraform validate is a static check against your configuration and the schemas Terraform loaded from installed providers and modules.
It covers:
- HCL syntax — braces, quotes, and block structure must parse
- Internal consistency — resource types, argument names, and references must make sense in the module where they appear
- Schema rules from installed providers — required arguments, supported attributes, and expression types the provider schema exposes
It does not prove that your infrastructure will work in the real world. Validation cannot confirm:
- Cloud or API credentials are valid
- Remote resources you reference actually exist
- A provider can complete create, update, or destroy at runtime
- Remote service settings outside Terraform’s schema are correct
Treat Success! The configuration is valid. as “the configuration is well formed,” not “production is safe to apply.”
validate → configuration + schemas on disk
plan → configuration + state + provider refresh
apply → real API / runtime changesValidate a Terraform configuration
The lab uses hashicorp/local so you can validate real configuration without cloud credentials. Create the directory once and stay in it for the rest of the article.
Prepare and initialize the lab
Create the working directory:
mkdir -p ~/terraform-labs/terraform-validate && cd ~/terraform-labs/terraform-validateWrite a minimal root module that manages one local file:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = "validate ok"
}
EOFterraform validate expects provider plugins in .terraform/. Initialize the directory:
terraform init -input=falseInitializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)
Terraform has been successfully initialized!The error examples reuse one working directory and normally reuse the installed provider. Where the article deliberately removes .terraform/, initialization is repeated to demonstrate fresh-checkout and CI behavior.
Run terraform validate
With providers on disk, check the configuration:
terraform validateSuccess! The configuration is valid.That single line is what you want before sharing a module or merging a pull request: Terraform parsed the files and found no schema or reference errors.
Why terraform init is required
Validate needs provider schemas from installed plugins. Remove the local install metadata to see what happens on a fresh checkout before init:
rm -rf .terraform .terraform.lock.hclRun validate without reinitializing:
terraform validate╷
│ Error: Missing required provider
│
│ This configuration requires provider registry.terraform.io/hashicorp/local,
│ but that provider isn't available. You may be able to install it
│ automatically by running:
│ terraform init
╵Restore the provider install before the error demos:
terraform init -input=falseErrors terraform validate can detect
The subsections below overwrite main.tf in the same directory. Because the provider is already installed, each example needs only cat and terraform validate.
Syntax errors
A missing closing brace is one of the most common mistakes:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = "broken"
# missing closing brace
EOFRun validate against the broken file:
terraform validate╷
│ Error: Unclosed configuration block
│
│ on main.tf line 12, in resource "local_file" "example":
│ 12: resource "local_file" "example" {
│
│ There is no closing brace for this block before the end of the file. This
│ may be caused by incorrect brace nesting elsewhere in this file.
╵Add the closing } for the resource block, or restore the good configuration from the lab setup section, before you continue.
Unsupported arguments
Provider schemas define which arguments each resource accepts. Add a fake attribute:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = "test"
not_a_real_argument = "oops"
}
EOFThe extra attribute is not in the local_file schema:
terraform validate╷
│ Error: Unsupported argument
│
│ on main.tf line 15, in resource "local_file" "example":
│ 15: not_a_real_argument = "oops"
│
│ An argument named "not_a_real_argument" is not expected here.
╵Terraform matched your configuration against the local_file schema from hashicorp/local and rejected the unknown argument.
Invalid references
References must point at resources, data sources, or symbols Terraform can see in the same module:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = terraform_data.missing.output
}
EOFterraform_data.missing was never declared in this module:
terraform validate╷
│ Error: Reference to undeclared resource
│
│ on main.tf line 14, in resource "local_file" "example":
│ 14: content = terraform_data.missing.output
│
│ A managed resource "terraform_data" "missing" has not been declared in the
│ root module.
╵Point content at a real value before you expect validate to pass.
Type mismatches
Schemas also constrain expression types. count must be a number, not a string:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
count = "two"
filename = "${path.module}/example.txt"
content = "test"
}
EOFcount must be a number, not a string:
terraform validate╷
│ Error: Incorrect value type
│
│ on main.tf line 13, in resource "local_file" "example":
│ 13: count = "two"
│
│ Invalid expression value: a number is required.
╵Not every type surprise fails at validate time. For example, content = 12345 on local_file passed validate in this lab because Terraform accepts that value for the attribute schema. When you need a hard failure before apply, use explicit type constraints or validation rules in a dedicated lesson on custom conditions.
The table below summarizes the error patterns you just reproduced, plus a few related messages you may see in other modules:
| Symptom | Likely cause | Fix |
|---|---|---|
Unclosed configuration block |
Missing } or broken nesting |
Close every block; run terraform fmt to expose structure |
Unsupported argument |
Typo or argument not in provider schema | Remove or rename the argument; check provider docs |
Reference to undeclared resource |
Wrong resource name or missing declaration | Declare the target or fix the reference path |
Incorrect value type |
String where a number is required (or similar) | Adjust the expression type to match the schema |
Missing required provider |
terraform init not run in this directory |
Run terraform init (or init -backend=false in CI) |
Missing required argument |
Required schema attribute omitted | Add the argument the provider schema marks required |
Success! but apply fails |
Valid HCL; runtime, permissions, or API limits | Debug with terraform plan and provider error output |
Use terraform validate in automation
CI jobs and scripts use the same validate command with flags that skip backend setup or emit JSON.
JSON output
Restore a valid configuration before you inspect machine-readable output:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = "validate ok"
}
EOFThe -json flag prints one JSON document on stdout:
terraform validate -json{
"format_version": "1.0",
"valid": true,
"error_count": 0,
"warning_count": 0,
"diagnostics": []
}Pipe through jq on Ubuntu when you want a quick field check:
terraform validate -json | jq .When validation fails, the same structure appears with valid set to false and populated diagnostics. Each entry includes severity, summary, detail, and source location fields (range.filename, range.start.line) suitable for CI parsers. Warnings may appear in diagnostics while valid remains true — only errors make the configuration invalid.
Validate without configuring a backend
Pull-request checks often run before credentials for a remote state bucket exist. A root module may declare a remote backend even though CI only needs to validate syntax:
cat > main.tf <<'EOF'
terraform {
backend "s3" {
bucket = "example-tfstate-bucket"
key = "terraform-validate/lab/terraform.tfstate"
region = "us-east-1"
}
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = "ok"
}
EOFA normal terraform init would configure the S3 backend and require access to that bucket. For validation-only jobs, clear prior init metadata and skip backend initialization:
rm -rf .terraform .terraform.lock.hclInstall providers without configuring the S3 backend:
terraform init -backend=false -input=falseInitializing provider plugins...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)
Terraform has been successfully initialized!For this validation-only workflow, -backend=false skips backend initialization while still installing the provider needed by terraform validate. Validation still works because it only needs schemas on disk:
terraform validateSuccess! The configuration is valid.This pattern checks module syntax early in CI. It does not replace a full terraform plan against real state before production apply.
CI validation workflow
On a fresh checkout, run formatting, initialization, and validation in that order. Each step depends on the working directory state the previous step left:
terraform fmt -check -recursivefmt -check exits non-zero when files would change. Next install providers without touching the configured backend:
terraform init -backend=false -input=falseFinally validate the configuration:
terraform validateterraform validate exits unsuccessfully when validation errors make the configuration invalid; warnings may be reported without making validation fail. Fail the job on a non-zero exit from any of the three commands. Wiring these into GitHub Actions, GitLab CI, or Jenkins is out of scope here.
terraform validate vs fmt vs plan
The three commands sit at different layers of the workflow:
| Command | Primary job | Needs init | Reads state | Contacts providers for real infrastructure |
|---|---|---|---|---|
terraform fmt |
Canonical HCL formatting | No | No | No |
terraform validate |
Configuration consistency | Usually yes | No | No |
terraform plan |
Proposed changes vs state | Yes | Yes | Yes (refresh) |
[terraform fmt](/terraform-fmt/) and terraform validate complement each other: fmt fixes spacing and layout; validate catches semantic mistakes fmt will never see.
You normally do not need to run terraform validate immediately before every terraform plan, because plan already performs an implied validation check. Use validate as a fast standalone editor or CI check; use plan when you need to validate in the context of a particular run, workspace, or input-variable values.
Why validation can pass while apply fails
Validation does not execute provider operations. Overwrite main.tf with a path the OS refuses to write:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "/proc/self/mem"
content = "cannot write here"
}
EOFConfirm validate is still happy:
terraform validateSuccess! The configuration is valid.terraform plan may still succeed because it only builds a change set. Apply is where the provider hits the filesystem:
terraform apply -auto-approve -no-colorlocal_file.example: Creating...
╷
│ Error: Create local file error
│
│ with local_file.example,
│ on main.tf line 10, in resource "local_file" "example":
│
│ An unexpected error occurred while writing the file
│
│ Original Error: write /proc/self/mem: input/output error
╵The configuration was syntactically valid; the runtime environment was not. That gap is why teams review plan output even after validate passes.
References
- terraform validate command — HashiCorp Terraform CLI documentation
- Validate the Terraform configuration — HashiCorp tutorial
- terraform plan command — implied validation during plan
- JSON output format — Terraform machine-readable output structure
Summary
terraform validate is the quick gate between editing .tf files and running plan or apply.
On an initialized working directory it confirms that:
- HCL parses
- References resolve
- Arguments match installed provider schemas
That is exactly what you want in local development and CI before anyone merges configuration changes.
The single lab under ~/terraform-labs/terraform-validate/ walked through successive main.tf overwrites for syntax, schema, reference, and type failures, deliberate re-init after removing .terraform/, JSON diagnostics, init -backend=false with a declared S3 backend, and a complete CI command sequence.
The /proc/self/mem example is the caveat to remember: validate proves consistency, not that a provider can complete an operation in your environment.
Pair validate with terraform fmt for formatting. You do not need validate immediately before every plan because plan includes an implied validation check — run plan when you need proposed changes against state.
Custom validation blocks, preconditions, and third-party policy tools are covered in Terraform validation checks for constraints beyond core validate.
When plan output looks right, continue with terraform apply to execute the approved change set.

