# Terraform CLI Cheat Sheet — Core Workflow

> **Tool:** Terraform
> **Category:** Infrastructure as Code
> **Verified against:** Terraform v1.9.8, flags verified via `terraform <cmd> -help` run locally, 2026-08-21
> **Official docs:** https://developer.hashicorp.com/terraform/cli

The everyday loop: initialize, validate, plan, apply, destroy — plus formatting and variable input.

## Initializing a working directory

```bash
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

```bash
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 too
```

`validate` 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

```bash
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

```bash
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

```bash
terraform destroy
terraform destroy -target=aws_instance.web   # destroy a single resource
terraform destroy -auto-approve
```

`destroy` 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>`)

```bash
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 prompt
```

This 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

```bash
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

```bash
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 locally
```

Module `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

```hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"        # any 5.x, but not 6.0
    }
  }
}
```

```bash
terraform providers                       # print the resolved provider requirements tree for this config
terraform providers lock                  # write/update .terraform.lock.hcl for the constrained providers
```

The `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

```bash
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](https://graphviz.org/doc/info/lang.html) — 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

```bash
terraform import aws_instance.web i-0123456789abcdef0
```

```hcl
import {
  to = aws_instance.web
  id = "i-0123456789abcdef0"
}
```

```bash
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.
