The Terraform Workflow
The idea in one sentence
Section titled “The idea in one sentence”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.
init, plan, apply
Section titled “init, plan, apply”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.
terraform initterraform plan is a dry run. It reads real Azure state, compares it against your configuration, and prints the diff — resources to create, update in place, or destroy — without changing anything.
terraform plan# example outputTerraform will perform the following actions:
# azurerm_resource_group.main will be created + resource "azurerm_resource_group" "main" { + id = (known after apply) + location = "eastus" + name = "example-resources" }
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 Azure API to create, update, or destroy resources.
terraform applyterraform destroy is the inverse of apply: it computes a plan to remove every resource this configuration manages, and — after confirmation — deletes them.
terraform destroyflowchart 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"] Saving a plan before applying it
Section titled “Saving a plan before applying it”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 Azure 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.
terraform plan -out=tfplanterraform apply tfplanThis 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.
The provider dependency lock file
Section titled “The provider dependency lock file”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/azurerm" { version = "4.5.0" constraints = "~> 4.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 ~> 4.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.