Upgrades & Troubleshooting
Upgrade Terraform and providers without breakage, and decode the errors you'll actually hit - cycles, inconsistent plans, stale locks, unknown for_each keys, and lock-file mismatches
Upgrades & Troubleshooting
Two things you can't avoid in long-lived Terraform: upgrading the versions under you, and debugging the plan that does something you didn't expect. This page is the field guide for both.
Upgrading Terraform (the CLI)
Terraform follows semantic versioning loosely — minor releases are backward-compatible, but the state format can be upgraded one way. Once a newer CLI writes your state, older CLIs can't read it. So the whole team and CI must move together.
-
Pin the version with
required_versionso a stray newer CLI can't silently bump your state:terraform { required_version = "~> 1.7" } -
Manage multiple versions with a version manager —
tfenv(Terraform) ortenv(Terraform + OpenTofu) — so each repo gets the version it pins. -
Read the upgrade guide for the target minor version before bumping; run
terraform planand expect no changes from the upgrade alone.
Upgrading Providers
Provider major versions do introduce breaking changes. The AWS provider 4→5 jump, for example, reworked how S3 bucket sub-resources are declared.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # widen deliberately, one major at a time
}
}
}terraform init -upgrade # pull the newest version allowed by constraintsA safe upgrade loop:
- Bump one provider's constraint at a time.
- Read that provider's upgrade guide for the major version.
terraform init -upgrade, then refresh the lock file for all platforms (see below).terraform plan— investigate every diff. A clean upgrade is often not zero-change; the provider may have renamed attributes.- Apply in staging first.
Keep the Lock File Honest
After any upgrade, the .terraform.lock.hcl changes (see Refactoring & State Evolution). Re-record hashes for every platform your team and CI run on, or CI will reject the lock:
terraform providers lock \
-platform=linux_amd64 \
-platform=darwin_arm64Common Errors and How to Fix Them
Error: Cycle
Error: Cycle: aws_security_group.a, aws_security_group.bTwo resources depend on each other. Common with security groups that reference each other's IDs. Break it by using a separate aws_security_group_rule resource instead of inline rules, or removing an unnecessary depends_on. Visualize it:
terraform graph | dot -Tsvg > graph.svgProvider produced inconsistent final plan / result
Error: Provider produced inconsistent final planThe provider promised one value at plan time and returned another at apply. Usually a provider bug or an attribute the cloud computes server-side. Fixes, in order: upgrade the provider, add the volatile attribute to lifecycle { ignore_changes = [...] }, or report it upstream.
Error acquiring the state lock
Error: Error acquiring the state lock
Lock Info: ID: 1a2b3c... , Who: ci@runnerSomeone (or a killed CI job) holds the lock. If you're certain no apply is running, release it — never with -lock=false:
terraform force-unlock 1a2b3c...Saved plan is stale
Error: Saved plan is staleThe config or state changed between plan -out=tfplan and apply tfplan. Re-run the plan. In CI, keep plan and apply close together, or apply directly from a fresh plan.
Resource already exists
Error: resource already exists / EntityAlreadyExistsThe object was created outside Terraform (or a previous apply created it but state was lost). Bring it under management with an import block instead of recreating:
import {
to = aws_iam_role.ci
id = "ci-deploy-role"
}Invalid for_each / count argument
Error: Invalid for_each argument
The "for_each" value depends on resource attributes that cannot be
determined until apply.for_each and count keys must be known at plan time. You fed them values that only exist after another resource is created. Fixes: key the map on static/input values rather than computed attributes; or bootstrap the dependency in a separate apply (a rare, legitimate use of -target).
Inconsistent dependency lock file
Error: Inconsistent dependency lock file
the cached package for ... does not match any checksum in the lock fileThe lock file lacks a hash for the current platform (classic Mac-laptop-vs-Linux-CI mismatch). Add the missing platform hashes with terraform providers lock -platform=..., or run terraform init -upgrade if a version genuinely changed.
Provider configuration not present
Error: Provider configuration not presentYou removed a provider block (or its module) while resources it manages still live in state. Either keep the provider configured until those resources are gone, or remove the resources first (removed block / destroy).
Two Debugging Tools Worth Knowing
terraform console
An interactive REPL for evaluating expressions against your state and variables — the fastest way to debug a gnarly for expression or check what a function returns:
$ terraform console
> cidrsubnet("10.0.0.0/16", 8, 2)
"10.0.2.0/24"
> [for s in aws_subnet.public : s.id]
> var.environment == "production"TF_LOG
When the error message isn't enough, turn on logging to see every provider API call (covered in Expert Patterns):
TF_LOG=DEBUG TF_LOG_PATH=tf.log terraform planWhat's Next
You can now keep Terraform running through upgrades and incidents. One more thing worth knowing: the open-source fork many teams now run instead → OpenTofu.