Skip to content

The Terraform Workflow

Four commands cover almost everything you do day to day: terraform init sets up the working directory, terraform plan shows you a dry-run diff, terraform apply executes it, and terraform destroy tears down what the configuration manages.

terraform init is the command you run first, in any directory, and after adding a new provider or module. It reads the required_providers block, downloads the matching provider plugins, and initializes the backend that stores state.

Terminal window
terraform init

terraform plan is a dry run. It reads real AWS state, compares it against your configuration, and prints the diff — resources to create, update in place, or destroy — without changing anything.

Terminal window
terraform plan
# example output
Terraform will perform the following actions:
# aws_s3_bucket.reports will be created
+ resource "aws_s3_bucket" "reports" {
+ bucket = "acme-monthly-reports"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.

terraform apply executes a plan: it recomputes the diff (unless you hand it a saved plan file, covered below), shows it to you, asks for confirmation, and then actually calls the AWS API to create, update, or destroy resources.

Terminal window
terraform apply

terraform destroy is the inverse of apply: it computes a plan to remove every resource this configuration manages, and — after confirmation — deletes them.

Terminal window
terraform destroy
flowchart LR
  init["terraform init"] --> plan["terraform plan"]
  plan --> review{"Review the diff"}
  review --> apply["terraform apply"]
  apply --> real["Real infrastructure updated"]
  init -.-> destroy["terraform destroy"]
  destroy -.-> gone["Managed resources removed"]
The core Terraform workflow, with destroy as a separate path

Plain terraform apply recomputes its own plan right before running it. Between the moment you reviewed a plan output and the moment someone runs apply, the real world can change — someone else applies a conflicting change, an AWS quota shifts, anything. The plan apply recomputes at that point might not be the one you reviewed.

Saving the plan to a file closes that gap: apply is then told to execute exactly that file, with no recomputation.

Terminal window
terraform plan -out=tfplan
terraform apply tfplan

This pattern matters most in CI/CD: a pipeline can run plan -out=tfplan, post the diff for human review, and only run apply tfplan after approval — guaranteeing the applied changes are precisely the ones a human signed off on.

When terraform init resolves your required_providers version constraints, it writes the exact versions and cryptographic checksums it selected into .terraform.lock.hcl, in the same directory as your configuration.

# .terraform.lock.hcl (excerpt — Terraform manages this file's contents)
provider "registry.terraform.io/hashicorp/aws" {
version = "5.55.0"
constraints = "~> 5.0"
hashes = [
"h1:examplehashvaluegeneratedbyterraforminit=",
]
}

This file should be committed to version control. It is the provider-level analogue of a package-lock.json or Gemfile.lock: without it, two different runs of terraform init — your laptop today, a teammate’s laptop tomorrow, a CI runner next week — could each resolve ~> 5.0 to a different patch release, and a subtle behavior difference in the provider could produce a different plan on someone else’s machine for no visible reason. With the lock file committed, every init installs the identical versions until someone deliberately runs terraform init -upgrade to move the constraints forward.

What does terraform plan actually do
Why would you save a plan to a file with plan -out=tfplan before running apply
What is .terraform.lock.hcl for, and should it be committed to git
What does terraform destroy do