The Terraform Workflow
Terraform's CLI reduces all infrastructure changes to a four-command loop: init, plan, apply, destroy. Understanding what each command does — and what it does not do — lets you operate Terraform confidently and avoid the most common mistakes teams make when they skip steps.
The Core Loop
terraform init # 1. Download providers & modules
terraform plan # 2. Preview what will change
terraform apply # 3. Make it so
terraform destroy # 4. Tear it down (when done)
Between init and plan, you typically also run:
terraform fmt # Format code consistently
terraform validate # Check for syntax and type errors
This is not a one-time sequence — it is a loop. Every infrastructure change follows the same path: edit → validate → plan → apply.
terraform init
terraform init prepares a working directory for use. It must be run before any other command, and again whenever you add or change providers or module sources.
What it does:
- Downloads provider plugins specified in
required_providersinto.terraform/providers/ - Downloads module sources into
.terraform/modules/ - Initializes the configured backend (local or remote state)
- Creates or updates
.terraform.lock.hclwith provider version hashes
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.40.0...
- Installed hashicorp/aws v5.40.0 (signed by HashiCorp)
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure.
Common init flags
# Force re-download of providers (useful when lock file conflicts)
terraform init -upgrade
# Migrate state to a newly configured backend
terraform init -migrate-state
# Skip downloading modules (useful in CI when pre-downloaded)
terraform init -get=false
.terraform.lock.hcl, ignore .terraform/
The lock file records exact provider versions and checksums — commit it so the team always installs identical versions. The .terraform/ directory contains the downloaded binaries themselves — add it to .gitignore. It can be recreated by running terraform init.
terraform validate
terraform validate checks your configuration for syntax errors and type mismatches. It does not access any remote APIs — it's a pure static check that runs instantly.
$ terraform validate
Success! The configuration is valid.
# Example error output:
$ terraform validate
╷
│ Error: Missing required argument
│
│ on main.tf line 12, in resource "aws_instance" "web":
│ 12: resource "aws_instance" "web" {
│
│ The argument "ami" is required, but no definition was found.
╵
Run validate in CI before plan — it catches obvious errors without needing provider credentials.
terraform fmt
terraform fmt formats .tf files to the canonical Terraform style. It adjusts indentation, aligns = signs in blocks, and normalizes spacing. There is only one correct format — run it before committing.
# Format all .tf files in current directory (and subdirectories with -recursive)
terraform fmt
terraform fmt -recursive
# Check without modifying (exits non-zero if changes needed — useful in CI)
terraform fmt -check
terraform fmt -check -recursive
# Show a diff of what would change
terraform fmt -diff
Formatted code is easier to review in pull requests — when fmt is enforced in CI, diffs contain only meaningful changes, not whitespace noise.
terraform plan
terraform plan compares your configuration against the current state and generates an execution plan showing exactly what Terraform will create, update, or destroy. It does not make any changes.
$ terraform plan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
~ update in-place
- destroy
-/+ destroy and then create replacement
<= read (data resources)
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t3.micro"
+ id = (known after apply)
+ public_ip = (known after apply)
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Saving a plan
Save the plan to a file and apply it exactly — no human review, no prompt:
# Save plan
terraform plan -out=tfplan
# Apply the saved plan exactly
terraform apply tfplan
This is the standard CI/CD pattern: plan in one step, review the output, apply in the next step. The saved plan guarantees that what was reviewed is exactly what gets applied.
Targeting specific resources
# Only plan changes to one resource
terraform plan -target=aws_instance.web
# Multiple targets
terraform plan -target=module.vpc -target=aws_security_group.app
-target carefully
Targeting a specific resource skips dependency checks for everything else. This can leave your configuration in an inconsistent state where Terraform's understanding of dependencies no longer matches reality. Use it for emergency fixes only, and always run a full terraform plan afterwards to confirm the overall state is coherent.
Reading Plan Output
The plan output uses symbols to indicate the type of change for each resource:
| Symbol | Meaning | What happens |
|---|---|---|
+ | create | New resource will be created |
- | destroy | Existing resource will be deleted |
~ | update | Existing resource modified in-place |
-/+ | replace | Resource destroyed and recreated (watch for this!) |
<= | read | Data source will be read (no infrastructure change) |
The most important symbol to watch for is -/+ (replace). It means the resource will be deleted and recreated — which causes downtime for stateful resources like databases. Terraform shows which argument forces the replacement:
# aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
~ id = "i-0abc123" -> (known after apply) # forces replacement
+ ami = "ami-0newami789" # forces replacement
~ instance_type = "t3.micro" -> "t3.small"
}
In this example, changing ami forces replacement (EC2 instances cannot change their AMI in-place). Changing instance_type does not — that is an in-place update via a stop/start cycle.
terraform apply
terraform apply executes the changes described in a plan. Without a saved plan file, it generates a fresh plan and prompts for confirmation before proceeding.
$ terraform apply
# ... plan output ...
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.web: Creating...
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Creation complete after 22s [id=i-0abc123def456]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "54.123.45.67"
Auto-approve (for CI/CD)
# Skip the confirmation prompt — only use in automation
terraform apply -auto-approve
# Apply a pre-approved saved plan (preferred for CI/CD)
terraform apply tfplan
In CI/CD, the recommended pattern is: terraform plan -out=tfplan in one job (post the plan output for human review), then terraform apply tfplan in a separate job after approval. This guarantees the applied changes match exactly what was reviewed — no surprises from infrastructure that changed between plan and apply.
terraform destroy
terraform destroy removes all resources managed by the current configuration. It is equivalent to terraform apply -destroy and follows the same plan → confirm → execute flow.
$ terraform destroy
# ... plan output showing all resources to be destroyed ...
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
aws_instance.web: Destroying... [id=i-0abc123]
aws_instance.web: Destruction complete after 30s
Destroy complete! Resources: 1 destroyed.
Terraform respects dependency order — it destroys resources in the reverse order they were created, so dependent resources are removed before the things they depend on.
# Preview what would be destroyed without actually doing it
terraform plan -destroy
# Destroy with auto-approve (dangerous — use with extreme care)
terraform destroy -auto-approve
Day-to-Day Workflow
Here is the complete workflow for a typical infrastructure change:
# 1. Edit your .tf files
vim main.tf
# 2. Format and validate
terraform fmt
terraform validate
# 3. Preview the change
terraform plan
# 4. Review the plan carefully — especially -/+ replacements
# 5. Apply when satisfied
terraform apply
# 6. Verify outputs
terraform output
For a team using CI/CD:
# Developer pushes a branch — CI runs:
terraform fmt -check -recursive # Fails if not formatted
terraform validate # Fails on syntax errors
terraform plan -out=tfplan # Posts plan to PR for review
# After PR approval and merge to main — CI runs:
terraform apply tfplan # Applies the exact reviewed plan
Other useful commands
# Show current state (what Terraform knows about)
terraform show
# List resources in state
terraform state list
# Show outputs from the last apply
terraform output
terraform output -json
# Refresh state to match real infrastructure (read-only — no changes)
terraform apply -refresh-only
Key Takeaways
terraform init— downloads providers and modules. Run after anysourceorrequired_providerschange. Commit.terraform.lock.hcl; gitignore.terraform/.terraform validate— static syntax check, no API calls. Use in CI to fail fast before planning.terraform fmt— enforces canonical formatting. Use-checkin CI.terraform plan— previews changes without applying them. Read the output carefully, especially-/+replacements. Save with-out=tfplanfor CI.terraform apply— executes the plan. Always review the plan first. Use saved plans in CI/CD to guarantee what was reviewed is what gets applied.terraform destroy— tears down all managed resources in dependency order. Irreversible. Preview withterraform plan -destroyfirst.