Core Workflow
.mdVerified against Terraform v1.9.8, flags verified via `terraform <cmd> -help` run locally, 2026-08-21 · official docs
The everyday loop: initialize, validate, plan, apply, destroy — plus formatting and variable input.
Initializing a working directory#
terraform init
terraform init -upgrade # also upgrade provider/module versions to the latest allowed
terraform init -backend=false # skip remote backend setup (e.g. for a quick local validate)init is always safe to re-run — it never deletes configuration or state. Run it any time you add a new provider, module, or change the backend block.
Validating configuration#
terraform validate
terraform validate -json # machine-readable output, useful in CI
terraform fmt # rewrite files to canonical formatting
terraform fmt -check # exit non-zero if formatting would change (CI check, no rewrite)
terraform fmt -recursive # format all subdirectories toovalidate only checks syntax and internal consistency — it does not contact providers or check whether your credentials/values would actually work against real infrastructure. That's what plan is for.
Planning changes#
terraform plan
terraform plan -out=tfplan # save the plan to a file for a later, exact apply
terraform plan -var="instance_count=3"
terraform plan -var-file="prod.tfvars"
terraform plan -target=aws_instance.web # limit planning to one resource/module (use sparingly)
terraform plan -destroy # preview what a destroy would do, without doing it-target is a scalpel for a specific fix or debugging session, not a routine workflow — repeatedly targeting individual resources instead of planning the whole configuration can let real drift between your state and the full config go unnoticed.
Applying changes#
terraform apply
terraform apply tfplan # apply an exact, previously-saved plan — no new plan, no prompt
terraform apply -auto-approve # skip the interactive yes/no prompt (CI pipelines)
terraform apply -var="instance_count=3"Applying a saved plan file (terraform apply tfplan) is the safer pattern for CI/CD — it guarantees the infrastructure change applied is exactly what was reviewed in the plan step, with no window for the underlying config or state to drift between plan and apply.
Destroying infrastructure#
terraform destroy
terraform destroy -target=aws_instance.web # destroy a single resource
terraform destroy -auto-approvedestroy is a convenience alias for apply -destroy — same safety considerations apply: no undo, and CI usage should require explicit human approval unless the environment is genuinely disposable (e.g. ephemeral PR preview environments).
The plan-file workflow (plan -out / apply <plan-file>)#
terraform plan -out=tfplan # write the plan to a binary file instead of just printing it
terraform show tfplan # re-read a saved plan file in human-readable form
terraform show -json tfplan # machine-readable plan, for CI gating/policy checks
terraform apply tfplan # apply exactly what's in the saved plan — no re-plan, no promptThis is the two-step pattern CI/CD pipelines should use instead of a bare terraform apply: a "plan" job produces and uploads tfplan as a build artifact, a human or a policy gate reviews it (terraform show tfplan renders it back to readable form), and a separate "apply" job downloads that exact artifact and runs terraform apply tfplan. Because the applied plan is a file, not a re-evaluation of the current config, there's no window for drift between what was reviewed and what gets applied.
Forcing resource replacement#
terraform plan -replace=aws_instance.web # preview replacing one resource instance, without applying
terraform apply -replace=aws_instance.web # replace it — destroy + recreate in the same apply-replace is a plan-customization flag (also accepted by apply directly, per terraform plan -help) — the modern equivalent of the older terraform taint/terraform untaint commands. Prefer -replace in new scripts: it's explicit about which apply/plan the replacement lands in, whereas taint mutates state ahead of time and is easy to forget you left set.
Modules#
terraform init # also downloads any modules referenced by `source = "..."`
terraform init -upgrade # re-resolve modules (and providers) to the latest allowed versions
terraform get # download/update modules only, without a full init
terraform get -update # re-download modules even if already present locallyModule source addresses come in a few common forms: a local relative path (./modules/vpc), a Terraform Registry reference (terraform-aws-modules/vpc/aws), or a Git URL (git::https://example.com/vpc.git?ref=v1.2.0). Pin registry and Git module sources to an explicit version/tag in production configs — an unpinned Git ref (or none at all, which defaults to the default branch) means the module's code can change out from under you on the next init -upgrade.
Provider version constraints#
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # any 5.x, but not 6.0
}
}
}terraform providers # print the resolved provider requirements tree for this config
terraform providers lock # write/update .terraform.lock.hcl for the constrained providersThe version constraint in required_providers only bounds which versions Terraform is allowed to select — the actual version in use for a given init is recorded in .terraform.lock.hcl, which should be committed to version control so every teammate and CI run resolves the identical provider version until someone deliberately runs init -upgrade.
Visualizing the dependency graph#
terraform graph # DOT-format dependency graph of the current config
terraform graph -type=plan # graph the more detailed plan-time evaluation, not just the config summary
terraform graph | dot -Tsvg > graph.svg # render to an image (requires Graphviz's `dot` installed separately)graph outputs raw DOT — Terraform does not render an image itself. Useful for untangling "why does changing this one variable seem to trigger changes across half my resources" in a large configuration.
Importing existing infrastructure#
terraform import aws_instance.web i-0123456789abcdef0import {
to = aws_instance.web
id = "i-0123456789abcdef0"
}terraform plan -generate-config-out=generated.tf # (experimental in v1.9.8) write a starting .tf config for the import
terraform apply # actually perform the import (plus any other planned changes)Two different import mechanisms exist in this version: the older terraform import CLI command (imports into state only — you still hand-write the matching resource block, or plan will show a large diff), and the newer import {} configuration block combined with -generate-config-out, which can generate a starting .tf file for you from the real object's attributes. The block-based approach is the current recommended pattern for anything beyond a one-off import, since it's declarative, reviewable in a plan, and repeatable — but always review the generated config carefully before committing it, it's a starting point, not guaranteed-correct code.