HCL Syntax and Configuration Files
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Every Terraform configuration is a set of text files written in the Terraform language, whose native syntax is HCL (the HashiCorp Configuration Language). Before looking at resources, variables or modules, it pays to know exactly what those files may contain, which files Terraform reads and in which order, and which conventions every team ends up adopting. This page is the reference for all three. It describes the language as documented for Terraform 1.17 and OpenTofu 1.13, and it is the second step of the reading path after Getting Started: the following pages build on it, starting with Providers, Resources and Data Sources and Variables, Outputs and Locals. For the motivation behind infrastructure as code see Introduction and Infrastructure as Code.
The shape of a configuration
The language has only two syntactic constructs, arguments and blocks. Everything else — expressions, functions, types — appears on the right-hand side of an argument and is covered in Types and Expressions and Functions.
| Construct | Meaning |
|---|---|
Argument |
Assigns a value to a name inside a block: |
Block |
A container for other content. A block has a type, zero or more labels and a body enclosed in braces. The body holds arguments and further (nested) blocks. |
Top-level block |
A block that may appear directly in a file rather than inside another block. Terraform recognizes a limited set of top-level block types (see Top-level block types). |
Nested block |
A block inside the body of another block, such as |
# A top-level block: type "resource", two labels, a body.
resource "docker_container" "catalog_api" {
name = "catalog-api" # argument
image = docker_image.catalog_api.image_id # argument with a reference expression
# A nested block with no labels.
ports {
internal = 8080
external = 18080
}
# Another nested block, defined by Terraform itself.
lifecycle {
create_before_destroy = true
}
}
Blocks and labels
The number and meaning of labels depends on the block type. A resource block takes two labels, the provider-defined
resource type (docker_container) and a local name (catalog_api) that is unique within the module for that type.
A variable block takes one label, the variable name. A terraform or locals block takes none. Each combination of
block type and labels identifies one object, so declaring the same resource type and name twice is an error.
Labels are written as quoted strings, and the style guide recommends wrapping both the type and the name in double
quotes.
Because the labels together form an address, Terraform and its tools talk about objects by address:
docker_container.catalog_api, var.environment, module.network, data.docker_network.default. The address of an
object is the contract between files, which is why splitting blocks across files never changes behavior.
Arguments and attributes
A single-line argument is name = expression, and each argument sits on its own line. Terraform does not use commas
between arguments. Separating arguments with a newline is mandatory inside blocks; the single-line block form
(variable "x" { type = string }) accepts exactly one argument. Which arguments are required, optional or computed
is defined by the schema of the block, which for resource and data blocks comes from the provider
(Providers).
Identifiers
Identifiers name arguments, block types and Terraform constructs such as resources, input variables, locals and modules. Valid characters are letters, digits, underscores and hyphens, and the first character must not be a digit. Terraform follows the Unicode identifier rules (UAX #31), extended to also accept the ASCII hyphen, so non-ASCII letters are legal. In practice teams use lowercase words separated by underscores, because hyphens force bracket notation in some contexts and underscores are what the style guide recommends.
Comments
| Syntax | Behavior |
|---|---|
|
Starts a single-line comment that ends at the end of the line. This is the default and recommended style. |
|
Also a single-line comment. Formatting tools such as |
|
Delimits a comment that can span several lines. It cannot be nested. |
# Single-line comment (the style the official guide recommends).
// Also valid, but rewritten to # by some formatters.
/*
Multi-line comment. The style guide prefers repeated # lines
and treats this form as supported only for backward compatibility.
*/
variable "environment" {
type = string # trailing comments are fine
description = "Deployment environment of the Bookshelf stack."
}
Encoding and line endings
Configuration files must be encoded as UTF-8. Non-ASCII characters are allowed in identifiers, comments and string values, but the language delimiters themselves are ASCII. Both LF and CRLF line endings are accepted; LF is the idiom and formatting tools may convert CRLF to LF, which matters for diffs on mixed Windows and Unix teams.
The JSON syntax
Why a language of its own rather than plain JSON or YAML? The HCL project draws the line between data serialization formats and configuration languages: JSON and YAML serialize data, while HCL sits between a data format and a general-purpose programming language. It stays easy for people to read and write, adds the expressions and blocks that declarative configuration needs, and, because the application declares which arguments and blocks it expects, it can report clearer errors. For interoperability HCL also defines a JSON form of the same model, which is what Terraform’s JSON syntax is.
Source: HCL — README, section Why?
Every construct of the native syntax also has a JSON representation, for configurations that are generated by
programs rather than written by people. Files in this syntax end in .tf.json (and, for OpenTofu only, .tofu.json).
Terraform loads them together with the .tf files of the same directory, and the two syntaxes can be mixed freely in
one module. The JSON form is not a data format for configuration values — for that use .tfvars.json, covered below — but a different spelling of the same language. The native syntax stays the right choice for anything a human edits;
JSON is for generators, for example the output of cdktf synth in the days before CDK for Terraform was deprecated and
archived (2025-12-10), or a small script that emits a block per entry of an inventory.
The mapping rules are short.
-
The root of the file is a JSON object whose property names are the top-level block types:
resource,variable,output,locals,module,provider,terraformand so on. -
Each label of a block becomes one more level of nested object. The innermost object is the block body.
-
Property names inside a body are argument names.
-
Booleans, numbers and
nullare literals. Objects and arrays map recursively. -
Strings are parsed as templates, so
"${var.environment}"is an interpolation. A string consisting of exactly one interpolation sequence returns the referenced value itself, not its string form. -
A nested block without labels is a plain object. When there are several blocks of the same type, or when order matters, use an array of objects. A single object keyed by unique labels is a shorthand that does not preserve order.
-
A property named
//inside a block body (or at the root) is a comment and is ignored. It is not ignored inside an object that is evaluated as an expression, where it would be an ordinary key.
# main.tf -- native syntax
variable "environment" {
type = string
description = "Deployment environment."
default = "dev"
}
resource "docker_image" "redis" {
name = "redis:7"
}
output "redis_image" {
value = docker_image.redis.name
}
{
"//": "main.tf.json -- the same configuration in JSON syntax",
"variable": {
"environment": {
"type": "string",
"description": "Deployment environment.",
"default": "dev"
}
},
"resource": {
"docker_image": {
"redis": {
"name": "redis:7"
}
}
},
"output": {
"redis_image": {
"value": "${docker_image.redis.name}"
}
}
}
A few block types have special rules in JSON. In resource and data blocks provider is a plain string such as
aws.alias, depends_on is an array of reference strings, and in lifecycle the ignore_changes value is either
the string all or an array of attribute names. In variable, the type is a string such as list(string) and the
default is a literal value that is not templated. In module, source and version are literal strings and
providers maps child provider addresses to parent addresses as strings. Several configurations of one provider are an
array under the provider name. In the terraform block all values are literal, and backend uses a compact form with
one key naming the backend type.
{
"provider": {
"docker": [
{ "host": "unix:///var/run/docker.sock" },
{ "alias": "remote", "host": "ssh://deploy@build.example.com:22" }
]
},
"terraform": {
"required_version": ">= 1.9.0",
"backend": { "local": { "path": "terraform.tfstate" } }
}
}
The same idea applies to other file types: variable definition files have a JSON form (.tfvars.json), and test files
have one (.tftest.json). JSON is also how programs read Terraform’s output: the machine-readable UI, JSON plans and
JSON state are described in the integrations part of this section and are a separate topic from the configuration
syntax described here.
File types and how they are loaded
A directory of Terraform files is a module. The directory where you run Terraform is the root module, and calls to other directories or registry sources are child modules (see Modules). Terraform evaluates all configuration files of a module as one single document: splitting blocks across files has no effect on behavior, and there is no per-file load order for ordinary configuration files. Only the files directly in the directory belong to the module; sub-directories are separate modules and are not loaded unless called.
| File | Purpose | Loaded when |
|---|---|---|
|
Configuration in the native syntax: resources, variables, outputs, providers and the rest. |
Always, with all other |
|
The same configuration in the JSON syntax. |
Always, together with the |
|
Values for the root module’s input variables. |
Automatically, if present in the working directory. |
|
More variable values, one file per concern. |
Automatically, in lexical order. |
any other |
Variable values for a specific environment, such as |
Only with |
|
Surgical modifications of other blocks without editing them. |
After all normal files, in lexical order. |
|
Provider versions and checksums selected by |
Read by |
|
Test files for |
Only by |
|
Query files with |
Only by |
|
Stacks: component and deployment configuration. |
Only by HCP Terraform Stacks (a Stack is not a normal module). |
|
OpenTofu-specific variants of |
Only by OpenTofu, which prefers them over a same-named |
Variable definition files
.tfvars files are written in HCL syntax but contain nothing except assignments of values to variable names. The
JSON form, .tfvars.json, is a single JSON object whose root keys are variable names. Terraform loads a definition
file automatically when its name is terraform.tfvars, terraform.tfvars.json, or ends in .auto.tfvars or
.auto.tfvars.json. All other files are passed explicitly with -var-file.
# terraform.tfvars -- loaded automatically
environment = "dev"
catalog_api = {
image = "bookshelf/catalog-api:1.4.0"
replicas = 2
}
# prod.tfvars -- loaded only with: terraform apply -var-file=prod.tfvars
environment = "prod"
catalog_api = {
image = "bookshelf/catalog-api:1.4.0"
replicas = 6
}
Source: Terraform Docs — Input Variables.
When several sources assign the same variable, the later in the following list wins; the first entry has the highest precedence.
| Priority | Source |
|---|---|
Highest |
|
↓ |
|
↓ |
|
↓ |
|
↓ |
Environment variables with the |
Lowest |
The |
The precedence rule is the reason secrets belong in TF_VAR_ environment variables or a secret store and not in a
committed .tfvars file; see Variables, Outputs and Locals
for validation, sensitivity and ephemeral variables.
Override files
An override file is a .tf or .tf.json file named exactly override.tf or override.tf.json, or ending in
_override.tf or _override.tf.json. Terraform skips override files while loading the normal configuration and then
processes them in lexicographical order by file name; within a file, blocks are processed by position, and later
overrides win. Overrides exist for the rare case where the original files cannot or should not be edited — a
generated file, or a local developer tweak that is not committed — and the style guide asks teams to use override.tf
sparingly and to document its use in the original definitions.
An override block matches an original block with the same header (block type plus all labels). The merge rules are:
-
An argument in the override replaces the argument with the same name in the original.
-
Nested blocks in the override replace all nested blocks of the same type in the original. Their contents are not merged, and block types the override does not mention stay unchanged.
-
The merged result must still be valid for that block type.
-
In
resourceanddatablocks,lifecycleis merged argument by argument, so an override that only setscreate_before_destroykeeps the originalignore_changes. If the override hasprovisionerblocks, the original’s provisioners are ignored, and aconnectionblock replaces the original’s completely. -
depends_onis not allowed in override blocks of any type and is an error. -
In
variableblocks, changingtypeconverts the originaldefaultto the new type (an error if conversion fails), and a newdefaultmust be compatible with the type. -
In
localsblocks, overrides apply per named value, whicheverlocalsblock defined it. -
In the
terraformblock, settings are merged individually.required_providersmerges element by element, while a newrequired_versionor per-provider constraint replaces the original’s constraint entirely. Acloudorbackendblock in an override takes precedence over the original’s backend configuration, in either direction.
# main.tf
resource "docker_container" "web" {
name = "web"
image = docker_image.web.image_id
lifecycle {
ignore_changes = [env]
create_before_destroy = false
}
}
# web_override.tf -- local tweak, not committed
resource "docker_container" "web" {
image = "bookshelf/web:debug"
lifecycle {
create_before_destroy = true # ignore_changes from main.tf is kept
}
}
Source: Terraform Docs — Override Files.
The dependency lock file
.terraform.lock.hcl records which provider versions terraform init selected and their checksums (h1: and zh:
hashes), so later runs make the same choices. It uses HCL syntax but is not a configuration file; it tracks providers
only, not modules, and lives in the root working directory next to the .terraform cache directory. The file is
generated, so you never write it by hand, but you do commit it so that dependency changes go through code review.
terraform init -upgrade ignores the existing selections and picks the newest versions matching the constraints, and
terraform providers lock -platform=... pre-records checksums for several operating systems and architectures, which
matters when a team mixes macOS laptops and Linux CI runners. The full treatment is in
Providers.
Test files
Test files use the .tftest.hcl or .tftest.json extension, which is how Terraform discovers them. They are read
only by terraform test, and their root-level blocks are test (optional, at most one, for settings such as
parallel), run (one or more; each executes apply, the default, or plan, and may contain variables,
module, providers, assert and expect_failures), variables (optional, at most one) and provider (zero or
more). The test framework needs Terraform 1.6 or later, and provider mocking arrived in 1.7. Test files do not
participate in the module’s configuration and cannot be referenced from it; see
Testing.
# tests/storage.tftest.hcl
variables {
environment = "test"
}
run "covers_bucket_name_has_environment" {
command = plan
assert {
condition = output.covers_bucket_name == "bookshelf-covers-test"
error_message = "The covers bucket name must include the environment."
}
}
Source: Terraform Docs — Tests.
Query files
.tfquery.hcl files are inputs to the terraform query command: they hold parameters that search for resources in
existing infrastructure so that you can import them into state and manage them as code. A query file may contain
list, provider, variable and locals blocks, and Terraform loads every query file in the root configuration
directory. The list block takes a resource type and a label, the required provider meta-argument, optional
count or for_each (mutually exclusive), include_resource (return full objects instead of only identities),
limit (default 100) and a provider-specific config block. List resources and terraform query arrived in
Terraform 1.14; the workflow is explained in Import and Query.
# discover.tfquery.hcl
list "docker_container" "bookshelf" {
provider = docker
include_resource = true
limit = 50
}
Stacks files
Terraform Stacks (HCP Terraform) split infrastructure definition from deployment into two file types, both placed at the root of the Stack’s repository:
-
.tfcomponent.hclfiles define the Stack’s components and their input values and replace the traditional root module. Their blocks arecomponent,required_providers,provider,removed,variable,outputandlocals, and a Stack needs at least onecomponent. -
.tfdeploy.hclfiles define how many times the Stack is deployed, with which inputs, and how the deployment runs are orchestrated. Their blocks aredeployment,deployment_group,deployment_auto_approve,identity_token,store,publish_output,upstream_inputandlocals, and a Stack needs at least onedeployment.
The suffix determines the file type and the file name before it is free, so a Stack is usually split into files such as
components.tfcomponent.hcl, providers.tfcomponent.hcl and deployments.tfdeploy.hcl. HCP Terraform processes every
matching file in the root directory. Stacks are not ordinary modules and the CLI’s init, plan and apply do not
read these files; they are the subject of Terraform Stacks. A third
suffix, .tfstacks.hcl, is not described on the official Stack file reference, so this page does not rely on it.
OpenTofu and .tofu files
OpenTofu stores code in plain text files with the .tf or .tofu extension, and its JSON variant uses .tf.json or
.tofu.json. The practical consequence is a precedence rule: when foo.tf and foo.tofu exist in the same directory,
OpenTofu loads only the .tofu file and ignores the .tf file, while Terraform does the opposite — it knows nothing
about .tofu and reads only the .tf file. A module author can therefore ship foo.tf for Terraform users and
foo.tofu for OpenTofu users with OpenTofu-only features (state encryption, early variable evaluation, provider
for_each and so on), without the two tools tripping over each other. More on the fork and the other differences is in
OpenTofu.
$ ls
main.tf main.tofu variables.tf
$ tofu plan # reads main.tofu and variables.tf; main.tf is ignored
$ terraform plan # reads main.tf and variables.tf; main.tofu is ignored
Source: OpenTofu Docs — Files.
The loading pipeline
The figure shows which files contribute to a run. Normal configuration files are merged first, override files are applied on top, variable values are collected from the sources in the precedence order above, and the lock file constrains which provider builds are installed. Test, query and Stacks files are not part of this pipeline at all: each is read by its own command.
The terraform block
The terraform block configures Terraform itself: which CLI version may run the code, which providers it needs, where
state is stored, whether the workspace lives in HCP Terraform, and which experimental language features are enabled.
terraform {
required_version = ">= 1.9.0, < 2.0.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.6"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
backend "local" {
path = "terraform.tfstate"
}
}
Two constraints apply to every setting in this block. Only constant values are allowed: arguments cannot refer to named objects such as resources or input variables, and built-in functions cannot be used. That restriction exists because Terraform needs these settings before it has evaluated anything. (OpenTofu relaxes this for some settings through early variable evaluation, a difference covered in OpenTofu.)
| Setting | Meaning |
|---|---|
|
A version constraint string for the Terraform CLI. Terraform exits with an error if its own version does not satisfy it; constraints from child modules also apply. It does not govern provider versions. |
|
A map from each provider’s local name to an object with |
|
Where state is stored and how operations run; the default is |
|
Connects the configuration to HCP Terraform or Terraform Enterprise. Only one |
|
A list of experimental language feature names to opt into. Experiments can change or be removed without notice, emit a warning on every plan or apply, and are not recommended for production. |
|
Metadata fields that a provider expects from the module using it. Modules populate them independently of provider configuration; very few providers use them. |
The cloud block
Inside cloud, organization and workspaces are required for HCP Terraform. The workspaces block selects either a
single workspace with name or all workspaces carrying a set of tags (a list or map), and the two are mutually
exclusive; project selects the HCP Terraform project. hostname defaults to app.terraform.io and is how a
Terraform Enterprise instance is addressed. The token argument exists but the documentation advises against writing
credentials in configuration: use terraform login or the CLI configuration file instead. Key-only tags need
Terraform Enterprise v202411-1 or later, or Terraform 1.10 or later.
Environment variables can supply the same values: TF_CLOUD_ORGANIZATION, TF_CLOUD_HOSTNAME, TF_CLOUD_PROJECT
and TF_WORKSPACE. They apply only when the matching attribute is absent from the configuration (the configuration
wins if both are set), and to configure cloud entirely through environment variables an empty cloud block must
still be present. TF_WORKSPACE cannot create a workspace; it must already exist, and when tags is used it must be
one of the tagged workspaces.
terraform {
required_version = ">= 1.9.0"
cloud {
organization = "example-bookshelf"
workspaces {
tags = ["bookshelf", "app"]
}
}
}
Source: Terraform Docs — terraform block reference. See HCP Terraform and Enterprise for workspaces, runs and projects.
Experiments and OpenTofu differences
The experiments setting is intended for features still being designed in Terraform’s own release cycle. Because of the
warning on every run, it is something to enable on a branch for evaluation, never in the baseline of a module that
others consume. OpenTofu keeps the terraform block and adds an encryption block inside it for state and plan
encryption; see OpenTofu.
Top-level block types
Terraform recognizes a limited set of block types at the top level of a file. Each has a dedicated page in this section; the table lists them with a pointer to where they are covered. Block types that exist in the language but are only valid in one special file type are listed separately.
| Block type | Purpose | Covered in |
|---|---|---|
|
Settings for Terraform itself: version constraint, providers, backend, |
This page |
|
Configures a provider; |
|
|
Declares an infrastructure object that Terraform manages. |
|
|
Reads information about existing objects. |
|
|
An ephemeral resource that exists only while one operation runs and is never stored in state or plan (Terraform 1.10). |
|
|
An input variable of the module. |
|
|
A value exposed by the module. |
|
|
Named expressions reused inside the module. |
|
|
Calls another module. |
|
|
Records that an object changed address, so state is renamed instead of destroyed and recreated. |
|
|
Removes an object from state, with |
|
|
Brings an existing object under management; takes |
|
|
Assertions evaluated at the end of plan or apply that produce warnings rather than errors, optionally with a scoped
|
|
|
Declares a provider-defined action, invoked from the CLI or from a resource’s |
Source: Terraform Docs — action block reference, Terraform Docs — check block reference, Terraform Docs — import block reference, Terraform Docs — removed block reference, Terraform Docs — moved block reference and Terraform Docs — ephemeral block reference.
Some block types are valid only in a specific kind of file:
| File | Blocks |
|---|---|
|
|
|
|
|
|
|
|
The language also has nested blocks defined by Terraform itself and valid only inside a parent: lifecycle,
provisioner, connection, precondition and postcondition, validation (inside variable), required_providers
and backend (inside terraform), and dynamic for generating nested blocks. They are introduced where their parent
is, in Meta-Arguments and
Lifecycle, Validation and Checks.
Placing the same block type in another file is harmless in the module sense, but a block that is valid only in one file
type is rejected elsewhere — a run block in main.tf, or a list block in a normal .tf file, is an error, because
list is only allowed in .tfquery.hcl files.
A complete small module
The following Bookshelf fragment uses the conventions described in the style guide below. It runs locally with the Docker provider and needs no cloud credentials.
# terraform.tf
terraform {
required_version = ">= 1.9.0"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.6"
}
}
}
# variables.tf
variable "catalog_api_image" {
type = string
description = "Container image of the catalog API."
default = "bookshelf/catalog-api:1.4.0"
}
variable "environment" {
type = string
description = "Deployment environment."
default = "dev"
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "The environment must be dev, test or prod."
}
}
# main.tf
resource "docker_network" "bookshelf" {
name = "bookshelf-${var.environment}"
}
resource "docker_image" "catalog_api" {
name = var.catalog_api_image
}
resource "docker_container" "catalog_api" {
name = "catalog-api-${var.environment}"
image = docker_image.catalog_api.image_id
networks_advanced {
name = docker_network.bookshelf.name
}
ports {
internal = 8080
external = 18080
}
lifecycle {
create_before_destroy = true
}
}
# outputs.tf
output "catalog_api_container_name" {
description = "Name of the catalog API container."
value = docker_container.catalog_api.name
}
Source: Terraform Docs — Style Guide.
The style guide
HashiCorp publishes a style guide for the Terraform language. Parts of it are enforced by the formatter and the rest are conventions to adopt, ideally with a linter such as TFLint (see Code Quality and Security Scanning).
Formatting
-
Indent two spaces per nesting level.
-
Align the equals signs of consecutive single-line arguments.
-
Inside a block, put arguments first, then a blank line, then nested blocks.
-
Put meta-arguments (
count,for_each,providerand so on) first, separated from the rest by a blank line, and meta-argument blocks such aslifecyclelast. -
Separate top-level blocks with one blank line. Nested blocks get blank lines too, except when grouping related blocks of the same type, such as several
provisionerblocks. -
Use
#for all comments, including multi-line ones, and avoid//and/* */.
resource "docker_container" "reviews_api" {
count = var.environment == "prod" ? 2 : 1
name = "reviews-api-${count.index}"
image = docker_image.reviews_api.image_id
ports {
internal = 8080
}
lifecycle {
create_before_destroy = true
}
}
Source: Terraform Docs — Style Guide.
Naming and ordering
-
Name objects with descriptive nouns separated by underscores, and do not repeat the resource type in the name (
docker_container.catalog_api, notdocker_container.catalog_api_container). -
Define data sources and dependencies before the resources that reference them. Terraform builds its graph from references, so ordering is for readers only.
-
The suggested order of arguments inside a resource is:
countorfor_each; other arguments; nested blocks;lifecycle;depends_on. -
Give every variable a
typeand adescriptionand every output adescription. Order variable parameters as type, description, default, sensitive, thenvalidation; order outputs as description, value, sensitive. -
Use variables, locals,
countandfor_eachsparingly, and add a comment when a meta-argument’s effect is not obvious. Usecountfor nearly identical instances (including a ternary for conditional creation, such asvar.enabled ? 1 : 0) andfor_eachwhen instances need distinct values. -
Always include a default (alias-free)
providerconfiguration, define all providers together in one file with the default first, and putaliasfirst in aliased providers.
File layout
| File | Content |
|---|---|
|
A single |
|
Backend configuration. |
|
All provider blocks, the default first. |
|
Resources and data sources. |
|
|
|
|
|
Local values used across files (file-specific locals go at the top of that file). |
|
Overrides, used sparingly and documented in the original definitions. |
As a module grows, split main.tf by concern — network.tf, storage.tf, compute.tf — so that a maintainer can
find any resource immediately.
Version control and pinning
Do not commit state files and their backups, the state lock info file, the .terraform directory, saved plan files or
.tfvars files containing sensitive data. Do commit all code, .terraform.lock.hcl, a .gitignore and a README.md
describing inputs and outputs. Pin provider versions in required_providers, set required_version to a minimum
Terraform version, and pin registry modules with the version argument to a specific major and minor version
(Terraform ignores version for local modules). Registry module repositories are named
terraform-<PROVIDER>-<NAME>.
$ cat .gitignore
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
prod.secret.tfvars
Source: Terraform Docs — Style Guide.
terraform fmt
terraform fmt rewrites configuration files to the canonical format. It applies a subset of the style conventions
plus minor readability adjustments, has no customization options ("intentionally opinionated"), and may change its
rules slightly between Terraform versions, which is why CI should pin the Terraform version. It minimizes changes to
configurations that already follow the documented style, and files generated by Terraform already conform.
| Flag | Behavior |
|---|---|
|
Stops printing the names of files with formatting inconsistencies. |
|
Does not overwrite files. Implied by |
|
Shows the formatting changes as diffs. |
|
Checks whether the input is formatted. Exits |
|
Also processes sub-directories. By default only the given or current directory is processed. |
|
Removes color from the output. |
The target is a directory (the current one by default), a single file, or - to read from standard input and write the
formatted result to standard output.
$ terraform fmt # rewrite files in the current directory
main.tf
$ terraform fmt -recursive # include sub-directories such as modules/
modules/network/main.tf
$ terraform fmt -diff -write=false # preview the changes only
$ terraform fmt -check -recursive # CI gate: non-zero exit if anything is unformatted
$ terraform fmt -list=false # rewrite quietly
$ cat main.tf | terraform fmt - # format standard input
Source: Terraform Docs — terraform fmt.
terraform fmt only normalizes whitespace and layout; it does not check semantics. The companion command,
terraform validate, checks that the configuration is syntactically valid and internally consistent without accessing
remote services, and it is run after init. A common pre-commit and pipeline sequence is terraform fmt -check -recursive, then terraform init -backend=false, then terraform validate; see
CLI Essentials. OpenTofu has the identical tofu fmt and tofu validate.
What changed recently
-
Terraform 1.6 introduced the
.tftest.hcltesting framework and 1.7 added mocking. -
Terraform 1.10 introduced ephemeral resources (the
ephemeralblock) and write-only arguments. -
Terraform 1.14 introduced list resources,
.tfquery.hclfiles withterraform query, and theactionblock. -
CDK for Terraform, the best-known producer of
.tf.json, was deprecated and archived on 2025-12-10; the migration path iscdktf synth --hcl, which turns the generated JSON into native HCL. -
Older tutorials show
required_providersentries withoutsourceaddresses (the pre-0.13 style) and//or slash-star comments; they still parse, but current guidance is an explicitsourceand#comments. -
The
action,listandephemeralblocks and the Stacks files postdate books written for Terraform 1.9 and OpenTofu 1.8, so those books do not list them; the official block reference is the authority for the list above.