linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/Debugging/tf-common-errors

kb/debugging ── Debugging ── beginner

Common Terraform Errors and How to Fix Them

A catalog of the Terraform errors you will hit again and again: cycle dependencies, "value depends on resource attributes that cannot be determined until apply", state lock, inconsistent plan, "for_each argument cannot include keys derived from resource attributes". For each one, the cause and the fix.

view as markdownaka: terraform-errors, terraform-troubleshooting

How to use this catalog

These are the errors that everyone who works with Terraform for more than a month runs into. They are grouped by the text of the message. If yours looks similar, read the cause first, then the fix.

If your error is not in this list, turn on tf-log-debug and look for a RESPONSE with HTTP 4xx or 5xx. The cloud usually tells you what went wrong.


Cycle: A, B, C

Error: Cycle: aws_security_group.web, aws_security_group.app

Cause

Resource A references B, and B references A. The dependency graph has a loop, so Terraform cannot tell which one to create first.

This happens most often with security groups:

hcl
resource "aws_security_group" "web" {
  ingress {
    security_groups = [aws_security_group.app.id]
  }
}
resource "aws_security_group" "app" {
  ingress {
    security_groups = [aws_security_group.web.id]
  }
}

Fix

Pull the rules out into separate aws_security_group_rule resources (or aws_vpc_security_group_ingress_rule in newer versions of the AWS provider):

hcl
resource "aws_security_group" "web" {}
resource "aws_security_group" "app" {}
resource "aws_vpc_security_group_ingress_rule" "web_from_app" {
  security_group_id            = aws_security_group.web.id
  referenced_security_group_id = aws_security_group.app.id
}
resource "aws_vpc_security_group_ingress_rule" "app_from_web" {
  security_group_id            = aws_security_group.app.id
  referenced_security_group_id = aws_security_group.web.id
}

Now the security groups are created first (with no references), then the rules between them.

Check the graph with tf-graph. You can see the cycles by eye.


Value depends on resource attributes that cannot be determined until apply

Error: Invalid for_each argument
The "for_each" value depends on resource attributes that cannot be
determined until apply, so Terraform cannot predict how many instances
will be created.

Cause

You are trying to use for_each = { ... } with a value that depends on a resource. At plan time Terraform does not know that value yet, because the resource does not exist, so it cannot tell how many instances there will be and cannot build the plan.

Example:

hcl
resource "aws_iam_user" "users" {
  for_each = toset([for r in aws_iam_role.roles : r.name])
  # ^^^^^^^^ aws_iam_role.roles does not exist yet at plan time
  name = each.key
}

Fix: three options

(a) Move the computation into terraform.tfvars. If the keys are known ahead of time, set them statically:

hcl
variable "user_names" {
  type    = set(string)
  default = ["alice", "bob"]
}
resource "aws_iam_user" "users" {
  for_each = var.user_names
  name     = each.key
}

(b) A two-phase apply. First run terraform apply -target=aws_iam_role.roles to create the roles, then a plain apply creates the users. This is a workaround. You should not rely on it in CI/CD, but sometimes it is the only way out of a bind.

(c) Split into two states. One project creates the roles and exports them through an output. A second project reads them with terraform_remote_state and creates the users.


Saved plan is stale

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.

Cause

You saved a plan with terraform plan -out=plan.bin, and then someone else (or CI) ran an apply. The state changed, so your plan is out of date.

Fix

Run the plan again:

bash
terraform plan -out=plan.bin
terraform apply plan.bin

This is a safeguard, not a bug. Terraform will not apply a stale plan, because doing so could lose data.


Error acquiring the state lock

Error: Error acquiring the state lock
Lock Info:
  ID:        abc-123-...
  Path:      s3://my-bucket/terraform.tfstate
  Operation: OperationTypeApply
  Who:       alice@laptop
  Created:   2026-05-20 14:00:00 +0000 UTC

Cause

Someone (or you, in another terminal) is already running an apply. The state is locked so that two parallel applies do not clobber each other.

Fix

  • First, wait. An apply can take a while. The message shows the ID and the time, so you can judge how long ago it started.
  • If you know the process is dead, run terraform force-unlock <ID>. This is dangerous: if the process is actually still alive, you let a second apply through and corrupt the state. Force-unlock only when you are sure.

Provider produced inconsistent final plan

Error: Provider produced inconsistent final plan
When expanding the plan for aws_s3_bucket.demo to include new values,
the provider produced a value for attribute "X" that is inconsistent
with the previous plan.

Cause

The provider contradicts itself: at plan time it returned one value for the attribute, and at apply time it returned another. This is a provider bug, not your code.

Fix

  1. Update the provider to the latest version: terraform init -upgrade.
  2. If you are already on the latest, open an issue in the provider's repo (https://github.com/hashicorp/terraform-provider-aws/issues).
  3. A temporary workaround: lifecycle.ignore_changes = [that_attribute]. This hides the problem, it does not cure it. Use it while you wait for a patch.

Reference to undeclared resource

Error: Reference to undeclared resource
A managed resource "aws_s3_bucket" "logs" has not been declared.

Cause

Your code references aws_s3_bucket.logs, but there is no such resource in the HCL. Either the name has a typo, or you forgot to write the resource itself.

Fix

Search the project:

bash
grep -r 'aws_s3_bucket' .

There should be a resource "aws_s3_bucket" "logs" {...} declaration somewhere. If it is missing, add it. If it is there, check the name, the case, and that it lives in the right module.


Inconsistent dependency lock file

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/aws: required by this
   configuration but no version is selected

Cause

The required_providers block in the HCL changed (a new provider or a new version), but .terraform.lock.hcl still holds the old versions.

Fix

bash
terraform init -upgrade

This re-reads the HCL, updates the lockfile, and downloads the versions you need. Afterward, commit the new .terraform.lock.hcl (yes, it gets committed).


Error: error configuring Terraform AWS Provider

Error: error configuring Terraform AWS Provider: no valid credential
sources for Terraform AWS Provider found.

Cause

The AWS provider did not find credentials in any of the usual places: environment variables, ~/.aws/credentials, or the instance IAM role. See aws-provider for the credentials chain.

Fix

Make sure one of these is set up:

bash
# option 1: environment variables
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=us-east-1
# option 2: shared file
aws configure   # writes ~/.aws/credentials
# check that the credentials are visible
aws sts get-caller-identity

If aws sts get-caller-identity works, Terraform will find them too.


Diagnostic pitfalls

  • The error message is on top, the cause is below. Long Terraform errors consist of a header (Error: ...) and a block with on FILE line N, in BLOCK: and a piece of code. What matters is the last line or two before the header. That is where the root cause sits.

  • "Similar" errors can be different. A "Cycle" from one pair of resources is a different cycle from another pair, and you fix each one on its own. Do not try to solve "all the Cycles at once".

  • Do not comment out HCL to get around an error. If the error is on line 50 and you comment it out, it will reappear on line 80. Cure it, do not hide it.

  • The provider logs are more informative than the Terraform message. TF_LOG_PROVIDER=DEBUG plus a grep for 'HTTP/1.1 4' often shows you the original AWS message, which is clearer.

§ команды

bash
terraform graph -draw-cycles

TF 1.4+: highlight cycles in the graph. Helps you untangle Cycle errors.

bash
terraform init -upgrade

Update the providers and the lockfile. Cures 'Inconsistent dependency lock file'.

bash
terraform force-unlock <LOCK_ID>

Release the lock by hand. ONLY when you are sure the process is dead. Otherwise you corrupt the state.

bash
aws sts get-caller-identity

Check that the AWS credentials are visible. If this command works, Terraform will find them too.

bash
TF_LOG=DEBUG terraform apply 2>&1 | grep -A 3 'HTTP/1.1 4'

Catch 4xx responses from the cloud: they usually carry a clear explanation of the problem.

§ см. также

  • tf-log-debugTF_LOG: Terraform diagnostic logs`TF_LOG` is an environment variable. It turns on Terraform's diagnostic logs: internal steps, HTTP requests to the cloud, dependency resolution. The levels are TRACE, DEBUG, INFO, WARN, ERROR. For everyday work DEBUG is enough. TF_LOG_PATH writes the output to a file.
  • tf-plan-diffHow to read a terraform plan: symbols, formatting, trapsA plan shows a diff: `+` create, `~` update in place, `-/+` replace, `-` destroy, `<=` data read. The summary line at the bottom reads `Plan: X to add, Y to change, Z to destroy`. The rule that matters: a second plan after apply should be clean ("No changes").
  • tf-depends-onResource dependencies: explicit and implicitTerraform automatically computes the creation order of resources from references in HCL (implicit dependencies). When no such reference exists but order matters, use depends_on. Use it rarely; it is usually a signal that the architecture needs reconsideration.
  • tf-count-for-eachcount and for_each: many resources from one blockcount creates N identical resources by index 0..N-1. for_each creates resources keyed from a set or map. Rule of thumb: count for identical copies, for_each when each one has its own settings. When in doubt, use for_each.
  • tf-stateState: Terraform's memory of what it createdState is the JSON file `terraform.tfstate` where Terraform records what it created in the cloud. Without it, Terraform would have no way to tell which bucket is "its own" and which belongs to something else. The file holds resource IDs, all attributes, and often secrets. It is the most sensitive part of any project.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies