Steven's Knowledge

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_version so a stray newer CLI can't silently bump your state:

    terraform {
      required_version = "~> 1.7"
    }
  • Manage multiple versions with a version manager — tfenv (Terraform) or tenv (Terraform + OpenTofu) — so each repo gets the version it pins.

  • Read the upgrade guide for the target minor version before bumping; run terraform plan and 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 constraints

A safe upgrade loop:

  1. Bump one provider's constraint at a time.
  2. Read that provider's upgrade guide for the major version.
  3. terraform init -upgrade, then refresh the lock file for all platforms (see below).
  4. terraform plan — investigate every diff. A clean upgrade is often not zero-change; the provider may have renamed attributes.
  5. 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_arm64

Common Errors and How to Fix Them

Error: Cycle

Error: Cycle: aws_security_group.a, aws_security_group.b

Two 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.svg

Provider produced inconsistent final plan / result

Error: Provider produced inconsistent final plan

The 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@runner

Someone (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 stale

The 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 / EntityAlreadyExists

The 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 file

The 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 present

You 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 plan

What'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.

On this page