Terraform · lesson 2 of 17
Beginner
7- 1 Terraform Fundamentals (IaC)
- 2 Terraform Workflow (init → validate → plan → apply)
- 3 Terraform State (tfstate, remote state, locking)
- 4 Terraform Variables and Outputs
- 5 Providers and Resources (the building blocks of Terraform)
- 6 Terraform Locals: Computed Values Without Repetition
- 7 Terraform Modules (reuse, composition, and clean structure)
Intermediate
7- 8 Terraform count and for_each (repeat resources safely)
- 9 The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes
- 10 Data Sources and Dependencies (read existing infrastructure)
- 11 sensitive = true: What It Hides, and What It Doesn't
- 12 Remote State Safety (locking, drift, and recovery)
- 13 Workspaces and Environments (dev/stage/prod patterns)
- 14 Terraform CI/CD with GitHub Actions (plan/apply)
Advanced
3Also part of Terraform
4Terraform Workflow (init → validate → plan → apply)
What terraform init, fmt, validate, plan -out and apply each guarantee, with real output, including the errors each step catches and a stale saved plan.
By SkillByExample · Updated
Every Terraform change goes through the same commands in the same order. Each one checks
something the next one depends on. The examples use Terraform 1.16.5 and hashicorp/random
3.9.1, which runs entirely on your machine, and this main.tf:
terraform {
required_version = ">= 1.6.0"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
resource "random_pet" "name" {
prefix = "web"
}
resource "random_integer" "port" {
min = 8000
max = 8999
}
output "endpoint" {
value = "${random_pet.name.id}:${random_integer.port.result}"
}
init: nothing works without it
Here’s what happens if you plan in a fresh directory before running init, and then what
init itself prints.
$ terraform plan -no-color
Error: Inconsistent dependency lock file
The following dependency selections recorded in the lock file are
inconsistent with the current configuration:
- provider registry.terraform.io/hashicorp/random: required by this configuration but no version is selected
To make the initial dependency selections that will initialize the dependency
lock file, run:
terraform init
$ terraform init -no-color
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/random versions matching "~> 3.9"...
- Installing hashicorp/random v3.9.1...
- Installed hashicorp/random v3.9.1 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
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. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
The configuration only names the provider and a version constraint (~> 3.9 means 3.9 or any
later 3.x). init resolves that to a real release, 3.9.1, and checks its signature. It saves
the binary in .terraform/ (don’t commit this) and records the choice in
.terraform.lock.hcl (commit this). Run init again whenever you add a provider, a module or
a backend.
fmt and validate: cheap checks before anything runs
Introduce two mistakes: write min=8000 with no spaces, and misspell random_pet.name as
random_pet.nmae in the output.
$ terraform fmt
main.tf
$ terraform validate -no-color
Error: Reference to undeclared resource
on main.tf line 22, in output "endpoint":
22: value = "${random_pet.nmae.id}:${random_integer.port.result}"
A managed resource "random_pet" "nmae" has not been declared in the root
module.
fmt fixed the spacing without saying how. It only prints the names of the files it rewrote.
validate caught the typo using nothing but the .tf files. It doesn’t read state or call any
API, so it finishes in a fraction of a second (about 0.1s here) and needs no credentials. That’s why CI runs it first. Fix
the typo and it reports Success! The configuration is valid.
plan -out, then apply the saved plan
$ terraform plan -out=tfplan -no-color
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# random_integer.port will be created
+ resource "random_integer" "port" {
+ id = (known after apply)
+ max = 8999
+ min = 8000
+ result = (known after apply)
}
# random_pet.name will be created
+ resource "random_pet" "name" {
+ id = (known after apply)
+ length = 2
+ prefix = "web"
+ separator = "-"
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ endpoint = (known after apply)
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"
$ terraform apply -no-color tfplan
random_pet.name: Creating...
random_integer.port: Creating...
random_pet.name: Creation complete after 0s [id=web-upright-longhorn]
random_integer.port: Creation complete after 0s [id=8612]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
endpoint = "web-upright-longhorn:8612"
apply tfplan didn’t print the plan again or ask for confirmation. Saving the plan was the
review step, so applying the file just carries it out. The two resources don’t reference each
other, so they were created in parallel, and the order of the Creating... lines can differ
between runs.
A saved plan goes stale
Apply the same tfplan file a second time:
$ terraform apply -no-color tfplan
Error: Saved plan is stale
The given plan file can no longer be applied because the state was changed by
another operation after the plan was created.
A saved plan records the state it was computed against. Here the first apply changed the state,
so the plan no longer describes a valid transition and Terraform refuses it. The same thing
happens when a teammate applies something between your plan and your apply. You can’t
accidentally apply a reviewed plan on top of changes nobody reviewed.
Practice
1. Delete the max = 8999 line and run terraform validate. What does the error name?
Solution
$ terraform validate -no-color
Error: Missing required argument
on main.tf line 16, in resource "random_integer" "port":
16: resource "random_integer" "port" {
The argument "max" is required, but no definition was found.
validate knows each resource’s schema from the provider binary init downloaded, so a missing
required argument is caught without any plan.
2. With the resources applied, change max to 9999, save a plan, and inspect it with
terraform show tfplan instead of applying it.
Solution
$ terraform show -no-color tfplan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# random_integer.port must be replaced
-/+ resource "random_integer" "port" {
~ id = "8612" -> (known after apply)
~ max = 8999 -> 9999 # forces replacement
~ result = 8612 -> (known after apply)
# (1 unchanged attribute hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.
Changes to Outputs:
~ endpoint = "web-upright-longhorn:8612" -> (known after apply)
A plan file is binary. terraform show turns it back into the same text plan printed, so a
reviewer can read it later, for example after CI saved it as a build artifact.
3. Put the original max back. Then destroy everything through a saved plan instead of
terraform destroy.
Solution
$ terraform plan -destroy -out=destroy.tfplan -no-color | tail -4
Saved the plan to: destroy.tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "destroy.tfplan"
$ terraform apply -no-color destroy.tfplan
random_integer.port: Destroying... [id=8612]
random_pet.name: Destroying... [id=web-upright-longhorn]
random_integer.port: Destruction complete after 0s
random_pet.name: Destruction complete after 0s
Apply complete! Resources: 0 added, 0 changed, 2 destroyed.
terraform destroy is shorthand for plan -destroy followed by apply. Splitting it gives a
destroy the same review step as any other change.
Next: Terraform state, the file that made “Saved plan is stale” possible, and the commands for inspecting it.