CLI Essentials

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.

The terraform binary is the one tool every Terraform workflow goes through: a developer at a laptop, a pipeline runner and a platform such as HCP Terraform all end up running the same handful of commands. This page is the reference for them. It follows the grouping used by the official command reference: the five main commands (init, validate, plan, apply, destroy), then all other commands (alphabetical in the help output, grouped by purpose here), then the global options, the CLI configuration file and the environment variables. It closes with the deprecated commands and flags and what replaces each of them.

It is written against Terraform 1.17 and OpenTofu 1.13, the baseline of this section. Flag lists were checked against the 1.17 command reference; where OpenTofu behaves differently, the difference is called out next to the command and collected in Terraform and OpenTofu differences. The page sits early in the reading path: it assumes the first run described in Getting Started with Terraform and is the companion of the dependency graph and the plan (what plan and apply do internally), State (what the state commands manipulate) and Backends and Remote State (what init -migrate-state moves).

The shell examples use the Bookshelf scenario (catalog-api, reviews-api, web, PostgreSQL, Redis, a covers bucket). Output blocks are abbreviated and marked as illustrative: the exact text of Terraform output changes between minor versions, which is why automation should use the -json and -detailed-exitcode interfaces described below and never parse human-readable output.

The shape of every command

Every invocation has the same structure:

terraform [global options] <subcommand> [options] [arguments]

Global options (-chdir, -help, -version) go before the subcommand; options of the subcommand go after it. Run terraform with no arguments to list the commands your installed version supports, and terraform <command> -help (or terraform -help <command>) for the short inline help of one of them. The inline help describes the most important characteristics of each command; the website reference is more detailed.

The commands fall into four families, which is a more useful mental model than the alphabetical help output:

Family Commands

The core loop

init, validate, plan, apply, destroy. These are the "main commands" in the help output.

Inspect and explore

show, output, console, graph, providers, modules, version, metadata.

Change what Terraform believes

state (and its subcommands), import, workspace, force-unlock, and the deprecated taint, untaint and refresh.

Housekeeping and integration

fmt, get, login, logout, test, query, stacks.

Two properties are worth knowing before the details. First, most commands that touch state take a state lock for the duration of the operation and accept -lock=false and -lock-timeout=DURATION; the lock is covered in State. Second, every command that evaluates the configuration accepts -var and -var-file, because evaluating the configuration needs the root module’s input variables; the formats are described under Input variables on the command line.

Global options

Option Meaning

-chdir=DIR

Switch to a different working directory before running the subcommand. Every file Terraform would normally read or write in the current directory (the configuration, .terraform/, terraform.tfstate, the lock file) is read or written in DIR instead. Two things deliberately do not follow it: the CLI configuration file is processed before -chdir is applied, and a reference to path.cwd in the configuration yields the original directory (use path.root for the root module directory).

-help

Print the help output, or the help of the subcommand that follows.

-version

An alias for the version subcommand.

-chdir replaces an old pattern: Terraform 0.13 and earlier accepted a directory as a positional argument of plan, apply and some other commands. That was deprecated in 0.14 and removed in 0.15, and -chdir is what to use in wrapper scripts and pipelines. If you need .terraform/ written somewhere other than the target directory (a read-only checkout, for instance), combine it with TF_DATA_DIR.

terraform -chdir=environments/production init
terraform -chdir=environments/production plan -out=tfplan
terraform -chdir=environments/production apply tfplan

Shell completion

For bash and zsh, terraform -install-autocomplete adds the completion hook to your shell profile and terraform -uninstall-autocomplete removes it (restart the shell after installing). It is covered where it belongs in the installation walkthrough of Getting Started with Terraform.

Version and security checks

CLI commands talk to HashiCorp’s Checkpoint service to learn whether a newer version exists and whether there are security bulletins for the running one; terraform version shows the result. Only an anonymous identifier is sent. Set the environment variable CHECKPOINT_DISABLE to any non-empty value to disable it for all HashiCorp products, or use the CLI configuration settings disable_checkpoint and disable_checkpoint_signature (see The CLI configuration file). Air-gapped CI runners should disable it to avoid timeouts on blocked networks.

Main commands

init

terraform init prepares a working directory. It is the first command to run after writing a configuration or cloning one, and it is always safe to run again: it never deletes your configuration or state, although later runs can report errors when settings changed. It performs four jobs in this order:

  1. Optionally copies a source module into an empty directory (-from-module=MODULE-SOURCE).

  2. Initializes the backend named in the terraform block.

  3. Installs child modules referenced by module blocks.

  4. Installs provider plugins and writes the dependency lock file .terraform.lock.hcl.

Option Effect

-input=true

Ask for input if necessary; when false, error instead of prompting.

-lock=false, -lock-timeout=DURATION

Disable state locking during state-related steps, or wait for a lock. The default timeout is 0s, which fails immediately if another process holds the lock.

-no-color, -json

Disable colour codes; enable the machine-readable JSON UI output.

-upgrade

Upgrade modules and providers. For providers it ignores the selections recorded in the lock file and installs the newest version that satisfies the constraints; for modules it re-fetches already-installed modules.

-backend=false

Skip backend configuration. Some later steps need an initialized backend, so use it only on a directory that was initialized before. The common legitimate use is terraform init -backend=false before validate, so validation runs without any credentials.

-backend-config=...

Supply partial backend configuration (a key=value pair or a file) for settings that are dynamic or sensitive and so cannot be written in the backend block.

-reconfigure

Disregard any existing backend configuration and initialize against the new one without migrating state.

-migrate-state

Copy existing state to the newly configured backend; may prompt to confirm the migration of workspace states.

-force-copy

Suppress the migration prompts and answer "yes"; it also enables -migrate-state automatically.

-get=false

Skip child module installation.

-plugin-dir=PATH

Read provider plugins only from this directory, as if it were a filesystem_mirror in the CLI configuration. Meant as a one-time override, for instance to test a local build of a provider.

-lockfile=MODE

Set the dependency lock file mode. The only documented value is readonly, which suppresses lock file changes but still verifies checksums against what is recorded. It conflicts with -upgrade.

-from-module=MODULE-SOURCE

Copy a module (a version control source or an example) into an empty directory before initializing it.

Changing the backend configuration requires either -reconfigure or -migrate-state; plain init refuses and tells you so. The difference is the one that matters in practice: -migrate-state carries your state to the new location, -reconfigure abandons the link to the old one and starts the new backend as it finds it. Use -backend-config in pipelines to inject the bucket, prefix or key per environment without editing files:

# Local, first use: install providers and modules, create .terraform.lock.hcl
terraform init

# CI: no prompts, backend settings injected per environment, lock file must not change
terraform init -input=false -backend-config=env/staging.s3.tfbackend -lockfile=readonly

# Move state from the local backend to a newly added remote backend block
terraform init -migrate-state

# Re-point to a different backend without moving state (the old state stays where it was)
terraform init -reconfigure -backend-config="bucket=bookshelf-tfstate-eu-west-1"

# Pick up newer provider and module versions within the constraints, then review the lock file diff
terraform init -upgrade

Two more points from the 1.17 reference. Input variables referenced in a module block’s source or version arguments are not available during init, so such a variable must declare const = true (the variable block gained the const argument for exactly this purpose); see Modules and Variables, Outputs and Locals. And the option -get-plugins=false no longer exists (removed in 0.15), having been superseded by the provider_installation and plugin_cache_dir settings described below. The lock file and provider selection are the subject of Providers; commit .terraform.lock.hcl, never .terraform/.

validate

terraform validate checks whether the configuration is syntactically valid and internally consistent: attribute names, value types, references. It ignores variable values and existing state, does not contact remote services (remote state, provider APIs) and is therefore safe as an editor post-save hook or a CI step. It needs an initialized directory so that providers and modules are available.

Option Meaning

-json

Machine-readable result, always without colour. Top-level keys are format_version, valid, error_count, warning_count and diagnostics; each diagnostic has severity, summary, detail and optionally range and snippet. Consumers must tolerate non-JSON text on stdout when Terraform fails before validation starts.

-no-color

Plain output.

-var, -var-file

Set root module variables (optional; validation does not depend on their values).

terraform init -backend=false
terraform validate
terraform validate -json | jq '.valid, .error_count'

To validate in the context of a particular workspace and set of variable values use plan, which includes an implied validation. Custom conditions (precondition, postcondition, variable validation, check) are evaluated by plan and apply, not by validate; see Lifecycle, Validation and Checks.

plan

terraform plan creates an execution plan: it refreshes the state from the remote objects (by default), compares the configuration with the prior state and proposes changes, without carrying any of them out. Its options are grouped by the reference into planning modes, planning options and other options. The conceptual side (how the graph is walked, what a plan contains, -replace versus -target, drift) is in Dependency Graph and Plan; this section lists what each flag does.

Planning modes are mutually exclusive:

Mode Meaning

normal (default)

Make the remote objects match the configuration.

-destroy

Plan to destroy every remote object managed by the configuration. Same as terraform destroy.

-refresh-only

Plan only to update state and root module outputs to match changes made outside Terraform (for instance during an incident). It is the replacement for the deprecated terraform refresh.

Planning options (shared with apply when it plans):

Option Meaning

-refresh=false

Skip synchronising state with the remote objects. Faster, but external changes are ignored and the plan can be wrong. Not allowed in refresh-only mode.

-minimal-refresh

New in 1.17. Refresh only the resource instances that the plan intends to change: Terraform first compares the configuration with the prior state and skips the refresh of instances with no planned change. Each instance decides independently. It has the same blind spot as -refresh=false (external changes to unchanged resources are not seen), cannot be combined with -refresh=false and is not allowed in refresh-only mode. Terraform still refreshes a resource whose schema version changed, so avoid both options when upgrading providers.

-replace=ADDRESS

Plan to replace the instance with this address even if nothing requires it (a degraded object, immutable infrastructure). Repeat to replace several. Cannot be used with -destroy.

-target=ADDRESS

Focus the plan on the matching instances and everything they depend on. For exceptional situations only (recovering from mistakes, working around limitations): routine use leads to undetected drift. A resource address selects one instance (aws_instance.example[0]), a whole resource (all its instances) or a whole module instance.

-var 'NAME=VALUE', -var-file=FILENAME

Set root module variables; repeatable.

-invoke=action.<TYPE>.<LABEL>

Plan only the invocation of one provider action and exclude all other configuration. See Provisioners, Actions and Post-apply Operations.

Other options:

Option Meaning

-out=FILENAME

Save the plan to an opaque file that apply (and show) can consume. The file contains the full configuration, all values and the input variables; sensitive values are stored in cleartext, so treat it as a secret. Do not give it a .tf suffix, or later commands try to parse it as configuration. The convention is tfplan.

-detailed-exitcode

Exit status 0 for success with an empty diff, 1 for an error, 2 for success with a non-empty diff. The basis of drift detection in pipelines.

-generate-config-out=PATH

Experimental. For import blocks without a matching resource block, write generated HCL to a new file (the path must not exist). See Import and Query.

-compact-warnings

Show only warning summaries, unless an error accompanies them.

-input=false

Do not prompt for unassigned root variables; fail instead.

-json

Machine-readable JSON UI output; implies -input=false.

-lock=false, -lock-timeout=DURATION

State locking controls. Disabling the lock is dangerous if others can run commands against the same workspace.

-no-color

Plain output.

-parallelism=n

Limit concurrent graph operations; the default is 10.

For configurations that use the local backend only, plan also accepts the legacy -state option (see Deprecated commands and flags).

# Interactive
terraform plan

# Pipeline: saved plan, no prompts, machine-readable exit status
terraform plan -input=false -out=tfplan -detailed-exitcode
echo $?   # 0: no changes, 1: error, 2: changes present

# Drift check only: what changed outside Terraform?
terraform plan -refresh-only

# Replace a degraded container instead of using the deprecated taint command
terraform plan -replace='docker_container.catalog_api'

Input variables on the command line

-var 'NAME=VALUE' is parsed twice, by your shell and then by Terraform. Primitive types (string, number, bool) take the bare value; every other type, including list, map, set and any, takes a Terraform expression. Terraform errors if there is a space around the equals sign. Prefer -var-file for anything but trivial values, because it avoids quoting problems entirely. The official recommendation for Windows is the Command Prompt rather than PowerShell, which cannot pass literal quotes to external programs correctly.

# Unix-style shell: single quotes keep the value literal
terraform plan -var 'region=eu-west-1'
terraform plan -var 'covers_bucket_tags=["web","catalog"]'

# Windows Command Prompt: double quotes, with escaped inner quotes
terraform plan -var "covers_bucket_tags=[\"web\",\"catalog\"]"

# Preferred for anything non-trivial
terraform plan -var-file=staging.tfvars

The other ways of assigning variables (terraform.tfvars, *.auto.tfvars, TF_VAR_ environment variables) and their precedence are covered in Variables, Outputs and Locals.

apply

terraform apply executes the operations of a plan. It has two modes:

  • Automatic plan mode (no plan file): Terraform creates a new plan as if you had run plan, shows it and asks for confirmation. All planning modes and planning options are accepted. -auto-approve skips the prompt.

  • Saved plan mode (a plan file argument): Terraform performs exactly the saved operations and does not prompt, because passing the file is the approval. No planning modes or options may be added, since the file already holds the result of those decisions, and -auto-approve is ignored.

Option Meaning

-auto-approve

Skip the interactive approval. Applies to destructive changes too, so in automation produce and review a plan first.

-compact-warnings, -no-color, -lock=false, -lock-timeout=DURATION

Same as for plan.

-input=false

Disable all prompts. Because that includes the approval prompt, an apply without -auto-approve or a saved plan then fails rather than proceeding.

-json

JSON UI output. Implies -input=false, and requires either -auto-approve or a saved plan.

-parallelism=n

Concurrent operations while walking the graph; the default is 10.

-replace=ADDRESS

Replace the instance with this address (automatic plan mode).

-invoke=action.<TYPE>.<LABEL>

Invoke a single provider action; instances of a for_each or count action are addressed with an index, for example action.aws_lambda_invoke.test["first"].

all planning modes and options

Only without a saved plan.

If an error occurs during an apply, Terraform logs it, saves the changes made so far to state, releases the lock and exits. There is no automatic rollback: fix the cause and apply again, and the configuration converges from wherever the run stopped. For the local backend only, apply still accepts the legacy -state, -state-out and -backup options.

# Interactive: plan, review, confirm
terraform apply

# Two-step, the form to use in automation
terraform plan -input=false -out=tfplan
terraform show tfplan                      # review the saved plan
terraform apply -input=false tfplan        # no prompt: the file is the approval

# One-shot refresh-only reconciliation with a prompt
terraform apply -refresh-only

# Replace one object
terraform apply -replace='docker_container.reviews_api'

destroy

terraform destroy deprovisions every object managed by the configuration. It is documented as a convenience alias for terraform apply -destroy, so it accepts most apply options but no plan file and forces destroy mode. To preview without acting, run terraform plan -destroy. -target can limit the destruction to one resource and its dependents, with the same caveats as for plan.

terraform plan -destroy -out=destroy.tfplan
terraform apply destroy.tfplan

# Tear down the local Bookshelf stack when finished
terraform destroy

# Exceptional: one resource and what depends on it
terraform destroy -target='docker_container.web'

Resources protected with prevent_destroy make the plan fail; see Lifecycle, Validation and Checks.

Inspecting configuration, state and plans

show

terraform show [options] [file] renders a state file or a saved plan file in human-readable form; with no file it shows the latest state snapshot. Use it to inspect a plan before applying it. -json produces the machine-readable representation (the format is documented in the JSON output format reference and consumed by policy tools and scanners; see Machine-readable Output and Integrations). -no-color is the only other option. Be careful: show -json prints sensitive values in plain text.

terraform show                       # current state, human-readable
terraform show tfplan                # a saved plan
terraform show -json tfplan | jq '.resource_changes[] | {address, actions: .change.actions}'

When a provider’s schema version changed since the state was written, upgrade the state first so that show -json can render it; a plan must have been created without -refresh=false to be shown as JSON.

output

terraform output [options] [NAME] prints the root module’s outputs from state: all of them, or the one named. Only root module outputs are available; to see a child module’s output, re-export it from the root.

Option Meaning

-json

Stable JSON, one key per output (or only the named one); pipe to jq.

-raw

Print a single string, number or boolean value with no quoting or newline, ideal for shell scripts. It fails for complex values; use -json for those.

-no-color

Plain output.

-state=path

Legacy option for the local backend only.

By name or with -json/-raw, sensitive outputs are shown in plain text; the unqualified listing shows <sensitive>. Ephemeral outputs are never stored in state, so output cannot print them. Use -json or -raw in automation, because the default human-readable layout may change.

terraform output
terraform output -raw covers_bucket_name
DB_HOST=$(terraform output -raw postgres_endpoint)
terraform output -json service_urls | jq -r '.catalog_api'

console

terraform console opens an interactive prompt to evaluate expressions against the configuration and the current state of the selected workspace. It is the quickest way to test a for expression, a cidrsubnet call or a regular expression. It holds the state lock while open and reads remote state first. Leave it with exit, Control-C or Control-D.

Option Meaning

-scope=ADDRESS

Evaluate in the scope of a child module instead of the root, for example -scope='module.child', -scope='module.child[0]' or -scope='module.child["key"]'.

-plan

Generate a plan first and evaluate against the planned state, so values of not-yet-created resources are more complete. Slower, and planning can run side effects in unusual configurations (an external data source runs its command).

-state=path

Legacy option for the local backend.

Non-interactive use works by piping newline-separated expressions in; only the final result is printed.

echo 'cidrsubnet("10.0.0.0/16", 8, 3)' | terraform console
terraform console -scope='module.network'

graph

terraform graph emits the dependency graph in the DOT language of Graphviz. The default output is a simplified graph of resources and data sources showing only dependency ordering; -type selects a more detailed graph of the internal operation, at the cost of exposing implementation details.

Option Meaning

-type=TYPE

One of plan, plan-refresh-only, plan-destroy or apply.

-plan=tfplan

Graph for applying the given saved plan; implies -type=apply.

-draw-cycles

Highlight cycles with coloured edges; only with a real graph operation type. Useful for "Cycle" errors.

-var, -var-file

Root module variables.

terraform graph | dot -Tsvg > graph.svg
terraform graph -type=plan -draw-cycles | dot -Tpng > plan-graph.png

Graphviz’s dot renders the result. The meaning of the graph is explained in Dependency Graph and Plan.

providers, modules, version and metadata

terraform providers shows the provider requirements of the configuration in the current directory and where each requirement came from (which module declared it), as a tree. It accepts -var/-var-file and has three subcommands:

Subcommand Purpose

providers lock [options] [providers...]

Add provider selections and checksums to the lock file without installing providers. Options: -platform=OS_ARCH (repeatable; pre-populate checksums for every platform the team uses, for example linux_amd64, darwin_arm64, windows_amd64), -fs-mirror=PATH, -net-mirror=URL and -enable-plugin-cache (off by default because the cache is not an authoritative source). Review the signer information it prints before committing the lock file.

providers mirror [options] <target-dir>

Download the required providers into a directory with the layout of a filesystem mirror (and index files usable as a network mirror). Options: -platform=OS_ARCH (repeatable) and -lock-file=false to ignore the lock file.

providers schema -json

Print the full schemas of the providers in use, as JSON. -json is required.

terraform modules (Terraform 1.10 and later) lists every module declared in the configuration with its key, source and version, as a tree or, with -json, as a machine-readable list (format_version is 1.0); it is aimed at auditing and policies on module consumption. terraform get [-update] downloads or updates the modules declared in the root module into .terraform/ (init does this too; get is the module-only variant).

terraform version [-json] prints the Terraform version, platform, selected providers and, unless Checkpoint is disabled, upgrade and security information; with -json it omits upgrade information. terraform metadata functions -json (Terraform 1.4 and later) prints the signatures of all functions available in the running version, which editor tooling uses; -json is required.

terraform providers
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64
terraform providers mirror -platform=linux_amd64 ./provider-mirror
terraform modules -json | jq '.modules[].source'
terraform version -json | jq -r '.terraform_version'

The registry, tiers and lock file are explained in Providers; the use of mirrors in air-gapped environments is shown under Provider installation.

fmt

terraform fmt [options] [target...] rewrites configuration files to the canonical style (a subset of the language style conventions plus minor readability adjustments). With no target it scans the current directory; the target can be a directory, a file or - for standard input. It is intentionally opinionated and has no style options. The canonical format can change slightly between versions, so run fmt after upgrading Terraform.

Flag Meaning

-list=false

Do not list files whose formatting differs.

-diff

Show the diffs of the changes.

-write=false

Do not overwrite files (implied by -check and by reading standard input).

-check

Exit with status 0 only if the input is already formatted; otherwise exit non-zero and list the offending files. The CI gate.

-recursive

Process subdirectories too; by default only the given directory.

-no-color

Plain output.

terraform fmt -recursive                 # fix in place
terraform fmt -check -recursive -diff    # CI: fail if anything would change, show what

Changing state and workspaces

The state family operates on the state of the selected workspace in the configured backend. State surgery is a last resort; for renames and restructuring, moved, removed and import blocks leave a reviewable trail in the configuration and are preferred (see Refactoring: moved, removed and import). The state file, locking and the dangers of push are in State.

state

Command Purpose

state list [options] [address...]

List resource instances in state, optionally filtered by address; -id=ID filters by the id attribute.

state show [options] ADDRESS

Show the attributes of one instance, human-readable or with -json.

state identities -json [address...]

Print the resource identities recorded in state, for resource types whose provider supports identity; -json is required and -id=ID filters by id. Identities feed identity-based import.

state mv [options] SOURCE DESTINATION

Move an instance or module to another address, within a state or (with -state-out) to another state file. -dry-run reports what would move; -lock, -lock-timeout, -var and -var-file apply.

state rm [options] ADDRESS...

Stop managing instances without destroying the remote objects. -dry-run, -lock, -lock-timeout, -var, -var-file.

state replace-provider [options] FROM_FQN TO_FQN

Rewrite the provider source address recorded in state, for example after a provider moved namespaces. -auto-approve skips confirmation.

state pull

Write the current state to standard output, whatever the backend. Redirect it to a file to take a backup.

state push [options] PATH

Overwrite the remote state with a local file. Safety checks guard against differing lineage and a higher remote serial; -force disables them. Use with extreme care.

terraform state list
terraform state show 'docker_container.catalog_api'
terraform state pull > backup-$(date +%Y%m%d).tfstate     # always before surgery
terraform state mv 'docker_container.api' 'docker_container.catalog_api'
terraform state rm 'docker_container.legacy_worker'

Quote addresses that contain brackets or double quotes so the shell does not interpret them. Resource address syntax is described in the state documentation.

import

terraform import [options] ADDRESS ID associates an existing remote object with a resource address in state. The ID format is provider-specific and documented per resource. It does not write configuration: you must have a matching resource block first. Import each remote object into exactly one address. Options include -config=path (the directory holding the provider configuration, default the working directory), -input, -lock, -lock-timeout, -parallelism, -var, -var-file, and the deprecated -provider. For local backends the legacy -state, -state-out and -backup options apply.

The reference itself recommends the declarative alternative: import blocks, optionally with plan -generate-config-out, run during an ordinary apply and reviewable in a pull request. Discovery of existing resources through .tfquery.hcl files is the query command below. Both are covered in Import and Query.

terraform import 'aws_s3_bucket.covers' bookshelf-covers-example

query

terraform query runs the list blocks of the .tfquery.hcl files in the current directory against the remote infrastructure and prints the discovered resources, so that they can be imported.

Option Meaning

-generate-config-out=PATH

Generate import and resource blocks for the results into a new file; with -json the configuration is part of the JSON output instead.

-json

Machine-readable output.

-var, -var-file

Variables defined in the .tfquery.hcl configuration.

-policies=PATH

Documented as requiring Terraform 1.17.0 or later, in HCP Terraform: select a policy-set directory in the uploaded configuration to evaluate against the discovered resources; repeatable. Applicable workspace policy sets are evaluated even without the flag, and query-time evaluation does not evaluate Sentinel or OPA policies. Resource identities alone are not enough policy input, so include_resource = true on the list block (or -generate-config-out) is needed.

-no-color

Plain output.

terraform query
terraform query -generate-config-out=to-import/generated.tf

workspace

terraform workspace manages the named states of one configuration (CLI workspaces). They are not the same thing as HCP Terraform workspaces; the limits of CLI workspaces and when to prefer directories are discussed in Workspaces, Environments and Project Structure.

Command Purpose

workspace list [-json]

List workspaces; the current one is marked with an asterisk. -json gives machine-readable output.

workspace show

Print the current workspace name (the default is default).

workspace new [options] NAME

Create and select a workspace. -state=path initializes it from an existing state file; -lock and -lock-timeout apply.

workspace select [-or-create] NAME

Select a workspace; -or-create creates it if it does not exist.

workspace delete [options] NAME

Delete a workspace. Refuses if it still tracks resources unless -force; after deletion Terraform can no longer manage that infrastructure. -lock and -lock-timeout apply.

The selected workspace can be overridden with TF_WORKSPACE, which is recommended only for non-interactive use because it is easy to forget it is set.

terraform workspace new staging
terraform workspace select -or-create production
terraform workspace list

force-unlock

terraform force-unlock [options] LOCK_ID removes a lock on the state of the current configuration. It does not modify infrastructure. The lock ID is printed in the "Error acquiring the state lock" message. The only option is -force, which skips the confirmation. A local state file cannot be unlocked from another process, and how the lock is held depends on the backend. Use it only after confirming that no run is still active: unlocking under a running apply invites state corruption.

terraform force-unlock 6a2c0f12-0000-0000-0000-000000000000   # placeholder lock ID

Authentication, testing and Stacks

login and logout

terraform login [hostname] obtains an API token for HCP Terraform, Terraform Enterprise or another host implementing the login protocol, by opening a web browser on the same machine; it is interactive only. Without a hostname it uses app.terraform.io, and organisations in HCP Europe use the hostname app.terraform.io/eu. By default the token is saved in plain text in credentials.tfrc.json in the Terraform data directory of the user (the command tells you the exact location before saving and lets you cancel); a credentials helper can store it elsewhere. terraform logout [hostname] removes the stored credentials locally (or calls the helper’s forget) but does not revoke the token on the server. For unattended runs, do not use login: configure credentials blocks, or better the TF_TOKEN_ environment variables described in Credentials.

test

terraform test runs the .tftest.hcl files of a module: it searches the current directory and the tests directory, runs the run blocks as plan or apply operations against in-memory state (separate from the real state) and then destroys what it created. Because it creates real infrastructure unless mocks or command = plan are used, run it in dedicated accounts and watch the cleanup output.

Option Meaning

-filter=testfile

Limit the run to specific test files; repeatable.

-test-directory=DIR

Directory to search for test files instead of tests. Test files in the main directory are always loaded. The reference advises keeping the default.

-verbose

Print the plan or state of every run block.

-json

Machine-readable results.

-junit-xml=PATH

Write a JUnit XML report; incompatible with -cloud-run.

-parallelism=n

Concurrent plan/apply operations within one run; default 10.

-run-parallelism=n

Concurrent run blocks within one test file; default 10.

-cloud-run=MODULE_SOURCE

Run the tests remotely in HCP Terraform against a module from the private registry; run terraform login first.

terraform test
terraform test -filter=tests/validations.tftest.hcl -verbose
terraform test -junit-xml=test-report.xml

Writing tests, mocks and the CI integration are in Testing.

stacks

terraform stacks manages Terraform Stacks (the multi-deployment orchestration layer of HCP Terraform). It is a separate plugin-backed command with its own global flags and is described conceptually in Terraform Stacks. The reference lists these commands and subcommand groups:

Command Purpose

init [-upgrade] [-json]

Download remote modules, sub-Stacks and providers and prepare the component configuration (creates .terraform/).

validate [-json]

Validate the component and deployment configuration locally; run init and providers-lock first.

fmt [target]

Rewrite .tfstack.hcl, .tfcomponent.hcl and .tfdeploy.hcl files to canonical format; accepts -check, -diff, -list, -write and -recursive.

providers-lock [-platform=os_arch]

Create or update .terraform.lock.hcl for the Stack; by default includes checksums for linux_amd64 (HCP Terraform) and the current machine.

create

Create a Stack in HCP Terraform: -organization-name, -project-name, -stack-name (each overridable by the environment variables TF_STACKS_ORGANIZATION_NAME, TF_STACKS_PROJECT_NAME and TF_STACKS_STACK_NAME), -with-template, -json.

list

List Stacks in an organization, optionally filtered by -project-name.

diagnostics -id ID

Retrieve warnings and errors for a configuration (ID prefix stc-) or a deployment step (prefix sds-).

version

Print the Stacks plugin version.

configuration, deployment-run, deployment-group, migrate

Subcommand groups to manage configuration versions, deployment runs and groups, and to migrate traditional workflows to Stacks.

Global flags for all stacks commands are -chdir, -plugin-cache-dir, -no-color, -usage and -stacks-version. On Terraform Enterprise, set TF_STACKS_HOSTNAME to the hostname of the instance.

The CLI configuration file

The CLI configuration file holds per-user settings that apply to every working directory, as opposed to the infrastructure configuration. It uses HCL syntax with its own attributes and blocks. Its location depends on the operating system:

  • on Windows, terraform.rc in the user’s %APPDATA% directory;

  • everywhere else, .terraformrc directly in the home directory;

  • anywhere, a file named by the TF_CLI_CONFIG_FILE environment variable, which should follow the naming pattern *.tfrc.

Windows Explorer can hide extensions: terraform.rc.txt is not recognised even if it is displayed as terraform.rc. Because the file is processed before -chdir, it is not affected by that option.

# ~/.terraformrc (illustrative; no real tokens)
plugin_cache_dir   = "$HOME/.terraform.d/plugin-cache"
disable_checkpoint = true

credentials "app.terraform.io" {
  # Prefer the TF_TOKEN_app_terraform_io environment variable in CI instead of writing a token here.
  token = "REPLACE_WITH_USER_OR_TEAM_TOKEN"
}

The available top-level settings are:

Setting Meaning

credentials block (one per host)

A block with a token argument for an HCP Terraform, Terraform Enterprise or other Terraform-services host.

credentials_helper block

At most one block selecting an external helper program (optional args) that stores and retrieves credentials.

disable_checkpoint, disable_checkpoint_signature

Disable the version and bulletin checks, or only the anonymous identifier sent with them.

plugin_cache_dir

Enable the shared provider plugin cache and name its directory.

plugin_cache_may_break_dependency_lock_file

Transitional setting (also available as an environment variable) that lets the cache satisfy a provider even if the lock file lacks an entry.

provider_installation

Customise where init gets providers from.

Credentials

Terraform expects API tokens for services such as the private registry and remote operations in credentials blocks, which terraform login writes for you. The token must be a user token or a team token; organisation tokens cannot be used for CLI actions. The credentials hostname must match the hostname used in module sources and backend configuration.

Since Terraform 1.2 a token can instead be supplied with an environment variable named TF_TOKEN_ plus the hostname with periods replaced by underscores, for example TF_TOKEN_app_terraform_io for app.terraform.io. Hostnames with non-ASCII characters must be converted to punycode, and hyphens (usually invalid in variable names) may be written as double underscores. The order of preference is the environment variable first, then the credentials block (including what terraform login stored), then a configured credentials helper. This is the right pattern for pipelines: inject the secret from the CI secret store and nothing is written to disk.

# CI job: token comes from the secret store, never committed (placeholder shown)
export TF_TOKEN_app_terraform_io="${HCP_TERRAFORM_TOKEN}"
terraform init -input=false

Provider installation

By default init downloads providers from their origin registries. The provider_installation block overrides that with a list of installation methods, each optionally restricted by include and exclude patterns (exclusions win; the registry.terraform.io/ prefix may be omitted, and */* means registry.terraform.io/*/*):

Method Meaning

direct

Ask the origin registry and download from the location it names. No arguments.

filesystem_mirror

Look in a local directory (required path) laid out either packed (HOSTNAME/NAMESPACE/TYPE/terraform-provider-TYPE_VERSION_TARGET.zip) or unpacked (HOSTNAME/NAMESPACE/TYPE/VERSION/TARGET). The unpacked layout allows symbolic links instead of copies.

network_mirror

Ask an HTTPS server implementing the provider network mirror protocol (required url, ending in a slash). Only configure mirrors you trust: a mirror with a valid certificate can still serve modified packages.

dev_overrides

A block mapping provider addresses to local directories, bypassing version and checksum checks, for provider development. It must come first, and direct {} should follow it or no other provider is available. Intended for temporary use only.

# ~/.terraformrc: serve the internal registry from a local mirror, everything else directly
provider_installation {
  filesystem_mirror {
    path    = "/usr/share/terraform/providers"
    include = ["registry.example.internal/*/*"]
  }
  direct {
    exclude = ["registry.example.internal/*/*"]
  }
}

If the file has no provider_installation block, Terraform builds an implied configuration: filesystem mirrors for the existing per-OS plugin directories (for example $HOME/.terraform.d/plugins on Linux and macOS, terraform.d/plugins in the working directory if present) plus an implied direct method that excludes the providers found in those mirrors. To use a mirror exclusively, remove direct or exclude the providers from it.

The plugin cache avoids downloading the same provider for each working directory: set plugin_cache_dir (the directory must already exist; on Windows use forward slashes) or the TF_PLUGIN_CACHE_DIR environment variable. The cache directory must not also be a filesystem mirror, Terraform never deletes entries from it, and it is not guaranteed to be safe for concurrent init runs. By default a cached package is used only if it matches a checksum in the lock file.

Environment variables

Terraform reads a number of environment variables. Most are for CI and debugging; none is required for normal use. The table lists the ones documented in the 1.17 environment variable reference.

Variable Effect

TF_LOG

Enable detailed logs on stderr. Levels, in decreasing verbosity: TRACE, DEBUG, INFO, WARN, ERROR. Setting it to JSON logs at TRACE level in a JSON encoding, which is not a stable interface. Unset it (or off) to disable.

TF_LOG_CORE, TF_LOG_PROVIDER

Enable logging separately for Terraform core and for provider plugins, with the same levels.

TF_LOG_PATH

Append the log to this file. Logging still requires TF_LOG (or the variants above) to be set.

TF_INPUT

When false or 0, behave as if -input=false was passed to every command that supports it.

TF_VAR_name

Set the root module input variable name. Lists and maps use Terraform syntax. This source has the lowest precedence among variable sources.

TF_CLI_ARGS

Extra arguments inserted after the subcommand and before arguments typed on the command line (so the command line wins). Parsed like shell input, so quotes group arguments.

TF_CLI_ARGS_name

Same, for the subcommand name only, for example TF_CLI_ARGS_plan set to -refresh=false.

TF_DATA_DIR

Where per-working-directory data such as the backend configuration is stored; the default is .terraform in the current directory. Use the same value for every command, starting with init. Handy when the directory is read-only.

TF_WORKSPACE

Select a workspace without workspace select, overriding the existing selection. Recommended for non-interactive use only.

TF_IN_AUTOMATION

Any non-empty value stops Terraform suggesting follow-up commands. Cosmetic; do not parse output.

TF_REGISTRY_DISCOVERY_RETRY

Maximum retries of the registry client for connection errors and retry-safe 5xx responses.

TF_REGISTRY_CLIENT_TIMEOUT

Timeout in seconds for registry requests; the default is 10.

TF_STATE_PERSIST_INTERVAL

How often (seconds) state is persisted to a remote backend during apply; the minimum is 20 for remote backends and a backend may set a larger minimum.

TF_CLI_CONFIG_FILE

Path to the CLI configuration file. TERRAFORM_CONFIG is a deprecated alias.

TF_PLUGIN_CACHE_DIR

Alternative to the plugin_cache_dir setting.

TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE

Any value other than empty or 0 enables the transitional setting of the same name.

TF_TOKEN_hostname

Bearer token for the Terraform-services host hostname with periods as underscores (1.2 and later).

CHECKPOINT_DISABLE

Any non-empty value disables the version and bulletin checks.

TF_STACKS_HOSTNAME, TF_STACKS_ORGANIZATION_NAME, TF_STACKS_PROJECT_NAME, TF_STACKS_STACK_NAME

Defaults for the terraform stacks commands (Terraform Enterprise hostname and the target organization, project and Stack).

TF_APPEND_USER_AGENT

Documented on the S3 backend page: text appended to the User-Agent header of the AWS requests made by that backend.

For the TF_LOG family, pipelines usually combine TF_LOG=DEBUG with TF_LOG_PATH and upload the file as a build artifact only on failure; the log can contain sensitive data, so treat it accordingly. Provider authentication variables (AWS_*, ARM_*, GOOGLE_*) belong to the providers and are covered with each cloud.

# A typical pipeline preamble
export TF_IN_AUTOMATION=true
export TF_INPUT=0
export TF_CLI_ARGS_plan="-lock-timeout=60s"
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"
export TF_VAR_db_password="${DB_PASSWORD}"   # injected from the secret store, never committed
terraform init
terraform plan -out=tfplan

Deprecated commands and flags

Deprecated Use instead

terraform taint ADDRESS

terraform apply -replace=ADDRESS (or plan -replace). The replacement shows the effect in the plan, while taint changes state immediately and lets another user plan against the tainted object first. The command accepts -allow-missing, -lock and -lock-timeout. The reference marks taint as deprecated.

terraform untaint ADDRESS

Still the way to clear a taint that Terraform set itself after a failed multi-step create; if you later find the object degraded after all, replace it with apply -replace. Options: -allow-missing, -lock, -lock-timeout, -no-color.

terraform refresh

terraform apply -refresh-only (or plan -refresh-only). refresh is an alias for apply -refresh-only -auto-approve: with misconfigured credentials Terraform could conclude that every object was deleted and drop them from state without asking.

import -provider

Marked deprecated in the import reference; by default Terraform uses the provider configured for the target resource, which is almost always right.

-state, -state-out, -backup

Legacy options accepted only by the local backend, preserved for backward compatibility and no longer recommended. Configure a remote backend (or the path argument of the local backend) so that everyone uses the same state location. They override the workspace-based state selection.

a directory argument to plan, apply, init

-chdir (and TF_DATA_DIR). Removed in 0.15.

init -get-plugins=false

provider_installation and plugin_cache_dir. Removed in 0.15.

TERRAFORM_CONFIG

TF_CLI_CONFIG_FILE.

terraform env

terraform workspace. Absent from the Terraform 1.17 command list; OpenTofu documents it as deprecated.

What changed recently

  • 1.17 (as documented in the 1.17 reference): plan and apply gained -minimal-refresh; terraform query gained -policies for query-time policy evaluation in HCP Terraform; the 1.17 reference documents that input variables used in module source and version must declare const = true.

  • 1.14 and later: the terraform query command and list blocks in .tfquery.hcl files (the release that introduced them is documented in Import and Query).

  • 1.10: the terraform modules command.

  • 1.4: terraform metadata functions -json.

  • 1.2: TF_TOKEN_ environment variable credentials.

  • 0.15 and earlier: -chdir replaced directory arguments, and -refresh-only and apply -destroy made refresh and the separate destroy path unnecessary.

The section What Changed and Migration places these in the wider release history.

Terraform and OpenTofu differences

The tofu binary has the same command structure, and the core loop, state, workspace, import, console, graph, fmt, validate, show, output, providers and force-unlock behave the same. Differences found in the OpenTofu command reference (which tracks the development branch, so confirm any flag against the documentation for your exact version):

  • the binary and the environment prefix: tofu, with the same TF_ variables (TF_LOG, TF_VAR_, TF_CLI_ARGS, TF_DATA_DIR, TF_WORKSPACE and so on) and additionally TF_ENCRYPTION for state and plan encryption, TF_IGNORE and TF_PROVIDER_DOWNLOAD_RETRY;

  • the CLI configuration file keeps its names (.terraformrc, terraform.rc) and TF_CLI_CONFIG_FILE;

  • tofu reads configuration from both .tf and .tofu files (see OpenTofu);

  • additional plan and apply options: -exclude, -exclude-file, -target-file, -concise, -json-into, -consolidate-warnings, -consolidate-errors, -show-sensitive, -deprecation and -lint (and -json-into on init);

  • no terraform query, terraform modules or terraform stacks equivalents are listed in the OpenTofu command list, and -policies, HCP Terraform integration and terraform login flows are HashiCorp specific;

  • tofu env still exists as a deprecated form of tofu workspace;

  • tofu metadata functions exists, documented on its own page.

See OpenTofu for the wider comparison, including state encryption and early evaluation.

Where the books are outdated

Older material (including the books this section draws on for concepts) shows habits that the current reference discourages:

  • terraform taint to force a replacement: use -replace so the plan shows it.

  • terraform refresh to sync state: use -refresh-only, which prompts you to review the detected changes.

  • -state, -state-out and -backup flags for juggling state files, or state pull/push to migrate to a remote backend: change the backend block and run terraform init -migrate-state.

  • passing a directory to terraform plan or apply: use -chdir.

  • parsing the human-readable output of output, plan or show: use -json, -raw and -detailed-exitcode.

  • the term "environment" for terraform env: use terraform workspace.

Pitfalls

  • Running apply -auto-approve without a reviewed, saved plan: the plan applied is the one computed at that moment, which may differ from the one a reviewer saw.

  • Forgetting that saved plan files contain sensitive values in cleartext.

  • Using -target as a routine tool, which leaves drift undetected.

  • Combining -var with unquoted shell special characters; prefer -var-file.

  • Leaving TF_WORKSPACE exported in a shell and applying to the wrong environment.

  • Running init -migrate-state without a backup of the existing state (take one with terraform state pull).

  • Committing .terraform/, plan files or credentials.tfrc.json; commit .terraform.lock.hcl.

  • Setting TF_LOG=TRACE in CI and publishing the log: it can contain secrets.