DevOps Nimbus · Terraform

Terraform errors and fixes

75 errors and surprises, each with the exact message, what it means, each likely cause with a check that proves it and its fix, and how to prevent it. Then learn it for good: break and fix each one yourself in the browser playground. The interactive version also searches pasted error text and opens each fix in a practice scenario.

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

What it means

Before any plan, Terraform compares the providers your code requires with the exact versions recorded in .terraform.lock.hcl (the lock file). They disagree. The indented line names the provider and says which way: "no version is selected" means the lock has no entry for it; "doesn't match the updated version constraints" means the locked version is no longer allowed.

Causes, most likely first

  1. terraform init was never run in this checkout, or a new provider or module was added. Only init writes lock entries, so the new requirement has none.

    Check

    grep 'provider "' .terraform.lock.hcl   # the provider named in the error is missing from this list
    terraform providers   # shows which module requires it

    Fix

    terraform init, then commit the changed .terraform.lock.hcl.

  2. A version constraint was raised above the locked version. init keeps the locked version on purpose, so the lock and the new constraint conflict.

    Check

    grep -A1 'hashicorp/aws' .terraform.lock.hcl   # version = "5.31.0" while required_providers asks for ~> 5.80

    Fix

    terraform init -upgrade, review the lock file diff, and commit it in the same pull request as the constraint change.

  3. You switched between Terraform and OpenTofu. Terraform locks registry.terraform.io/hashicorp/aws and OpenTofu locks registry.opentofu.org/hashicorp/aws, so the other tool finds no entry for its own address.

    Check

    grep 'provider "' .terraform.lock.hcl   # registry host differs from the one in the error
    terraform version; tofu version 2>/dev/null   # which tool you ran

    Fix

    Run init with the tool the repository uses, and use that one tool everywhere, CI included.

Prevent it

Now learn it: break and fix it yourself in Forgot terraform init, Lock file pins an old provider, then take Level 00 · init, plan, apply. Open interactive

Required plugins are not installed

Error: Required plugins are not installed

The installed provider plugins are not consistent with the packages
selected in the dependency lock file

What it means

The lock file selects provider versions, but the provider programs (plugins) are not in this directory's .terraform folder. Terraform never downloads them during plan or apply; only init does.

Causes, most likely first

  1. A fresh clone, a new CI runner, or a deleted .terraform folder. The lock file is committed, the downloaded plugins are not.

    Check

    ls .terraform/providers/*/hashicorp/aws/ 2>/dev/null   # no output: nothing is installed

    Fix

    terraform init. In CI, make it the first step of every job.

  2. The lock file changed (a pull, or a teammate's upgrade) and init has not run since, so the installed version is the old one.

    Check

    grep -A1 'hashicorp/aws' .terraform.lock.hcl   # the version the lock selects
    ls .terraform/providers/*/hashicorp/aws/   # a different version is installed

    Fix

    terraform init (no -upgrade needed: it installs what the lock file says).

  3. Terraform looks in another data directory. TF_DATA_DIR, or a CI cache restored to a different path, points away from the .terraform that init filled.

    Check

    echo "$TF_DATA_DIR"   # set: Terraform uses that directory, not .terraform

    Fix

    Unset TF_DATA_DIR, or run init with the same value the later steps use.

Prevent it

Now learn it: break and fix it yourself in Forgot terraform init, then take Level 00 · init, plan, apply. Open interactive

Failed to query available provider packages

Error: Failed to query available provider packages

Could not retrieve the list of available versions for provider
hashicorp/aws: no available releases match the given constraints ~> 5.0, >= 6.0

What it means

terraform init asked the registry (or your mirror) for the versions of a provider and could not pick one. The text after "for provider ...:" names the reason: no release satisfies every constraint, the lock file is in the way, the provider does not exist, or the registry could not be reached.

Causes, most likely first

  1. The root module and a child module require incompatible versions, for example the root says ~> 5.0 and a module says >= 6.0. All constraints must hold at once, so no release fits.

    Check

    terraform providers   # each module's constraint, side by side

    Fix

    Pin the module to a release that supports your provider major, or move the root constraint in a reviewed change.

  2. The lock file pins a version the constraints no longer allow. The message then says "must use terraform init -upgrade".

    Check

    grep -A1 'hashicorp/aws' .terraform.lock.hcl   # the locked version

    Fix

    terraform init -upgrade, then commit the lock file.

  3. A module uses a third-party provider without declaring it in its own required_providers, or the source has a typo. Terraform then looks for hashicorp/<name>, which does not exist ("does not have a provider named").

    Check

    terraform providers   # a hashicorp/<name> line you did not expect
    grep -rn -A4 required_providers --include='*.tf' .   # a module without the provider's source

    Fix

    Declare the real source (for example datadog/datadog) in every module that uses it.

  4. No route to the registry: a proxy, an offline runner, or a provider_installation block that sends Terraform to a mirror without this provider.

    Check

    cat "$HOME/.terraformrc" 2>/dev/null   # provider_installation or network_mirror (or the file in TF_CLI_CONFIG_FILE)
    curl -sI https://registry.terraform.io/.well-known/terraform.json | head -1   # no HTTP 200: no route

    Fix

    Fix the proxy settings, or add the provider to the mirror.

Prevent it

Now learn it: break and fix it yourself in Provider version conflict, Lock file pins an old provider, then take Level 10 · Providers and versions. Open interactive

Provider package doesn't match the lock file checksums

Error: Failed to install provider

Error while installing hashicorp/aws v5.80.0: the current package for
registry.terraform.io/hashicorp/aws 5.80.0 doesn't match any of the
checksums previously recorded in the dependency lock file

What it means

Every provider package is checked against the hashes in .terraform.lock.hcl. This package matches none of them. "h1:" hashes cover one platform each; "zh:" hashes come from the registry for every platform. A lock file with only an h1: line was written from a mirror or cache on one machine, and other platforms fail this check.

Causes, most likely first

  1. The lock file was written on another platform (a Mac) through a mirror or plugin cache, so it only has that platform's h1: hash, and Linux CI fails.

    Check

    grep -A12 'hashicorp/aws' .terraform.lock.hcl   # one or two h1: lines and no zh: lines
    terraform version   # this machine's platform, such as linux_amd64

    Fix

    terraform providers lock -platform=darwin_arm64 -platform=linux_amd64 -platform=windows_amd64 (it asks the origin registry by default), review the diff, commit.

  2. The mirror serves a different build than the registry, rebuilt or tampered with. The hashes are doing their job.

    Check

    git log -p -- .terraform.lock.hcl | grep -m5 h1:   # hashes for a platform that used to work have changed

    Fix

    Stop and check the mirror's copy against the registry before trusting it. Do not just regenerate the lock file.

  3. The lock file was edited by hand or merged badly, so the hash list is wrong.

    Check

    git diff origin/main -- .terraform.lock.hcl   # hashes removed or mixed between versions

    Fix

    Take the lock file from main and run terraform providers lock again for every platform.

Prevent it

Now learn it: break and fix it yourself in Lock file checksums for one platform, then take Level 10 · Providers and versions. Open interactive

Unsupported Terraform Core version

Error: Unsupported Terraform Core version

This configuration does not support Terraform version 1.10.5.

What it means

A terraform block has required_version, and the CLI you are running is outside it. If the message starts with "Module ... (from ...)", the constraint is in a downloaded module, not your code.

Causes, most likely first

  1. Your CLI is older or newer than the pin, often a narrow pin such as ~> 1.5.0 that allows 1.5.x only.

    Check

    terraform version   # the CLI version
    grep -rn required_version --include='*.tf' .   # the constraint, including modules in .terraform/modules

    Fix

    Install a matching CLI (tfenv install and tfenv use), or widen the constraint in a reviewed change and upgrade CI with it.

  2. A module requires a newer CLI than you have. Its constraint applies too.

    Check

    grep -rn required_version .terraform/modules   # the module's constraint

    Fix

    Upgrade the CLI, or pin the module to a release that supports your version.

  3. You ran OpenTofu against a constraint written for Terraform. tofu checks required_version against its own version number, which is not Terraform's.

    Check

    tofu version 2>/dev/null; command -v terraform   # which binary actually ran

    Fix

    Use the tool the repository is written for, or keep tofu-specific constraints in .tofu files (OpenTofu 1.8+).

Prevent it

Now learn it: break and fix it yourself in Unsupported Terraform Core version, then take Level 10 · Providers and versions. Open interactive

State was written by a newer provider version

Error: Resource instance managed by newer provider version

The current state of aws_s3_bucket.site was created by a newer provider version than is
currently selected. Upgrade the aws provider to work with this state.

What it means

Terraform asked the provider to read a resource from state, and the provider your lock file selects says the record was written by a newer version of itself, in a schema (storage layout) it does not know. The Terraform CLI version is rarely the cause in 1.x: every 1.x release writes state format 4 and reads the others' state. The message "state snapshot was created by Terraform vX, which is newer than current" comes from old 0.x releases.

Causes, most likely first

  1. CI or a teammate upgraded the provider and applied, and your checkout still has the older lock file, or you pulled the new one and did not run init.

    Check

    terraform version   # the provider versions this checkout uses
    git diff origin/main -- .terraform.lock.hcl   # main selects a newer aws provider

    Fix

    Pull the newer lock file and run terraform init, or terraform init -upgrade within your constraints.

  2. The provider was pinned back after a bad release, but the state was already written by the newer one. Providers cannot read schemas from their future.

    Check

    aws s3api list-object-versions --bucket <bucket> --prefix <key> --max-items 5   # when the state changed, to match with the upgrade

    Fix

    Stay on the newer provider, or restore the state version written before the upgrade (S3 versioning). Never edit schema_version by hand.

  3. Terraform 0.x only: a newer CLI wrote the state, and old CLIs refused it.

    Check

    terraform state pull | jq -r .terraform_version   # the CLI that last wrote the state

    Fix

    Upgrade the CLI to that version or later.

Prevent it

Now learn it: break and fix it yourself in State written by a newer provider, then take Level 10 · Providers and versions. Open interactive

Module not installed

Error: Module not installed

This module is not yet installed. Run "terraform init" to install all
modules required by this configuration.

What it means

A module block points at code that terraform init has not downloaded into .terraform/modules. Plan never downloads modules itself.

Causes, most likely first

  1. A new module call was added, or the source address of one changed, since the last init.

    Check

    cat .terraform/modules/modules.json   # the module key from the error is missing, or has an old source

    Fix

    terraform init.

  2. A fresh clone, a new CI runner, or a deleted .terraform folder. Modules are downloaded per directory and not committed.

    Check

    ls .terraform/modules 2>/dev/null   # nothing there

    Fix

    terraform init as the first step of every job.

Prevent it

Now learn it: break and fix it yourself in Module output that does not exist, then take Level 09 · Modules. Open interactive

Module version requirements have changed

Error: Module version requirements have changed

The version requirements have changed since this module was installed and
the installed version is no longer acceptable. Run "terraform init" to install all modules
required by this configuration.

What it means

The version argument of a module block no longer allows the version that init downloaded earlier, so Terraform stops instead of using the wrong code.

Causes, most likely first

  1. Someone changed or added the module's version constraint, and this directory still has the older download.

    Check

    cat .terraform/modules/modules.json   # the installed version
    grep -n -A2 'source *=' main.tf   # the version the code asks for

    Fix

    terraform init, which downloads a version that matches.

  2. The module has no version pin, and a teammate's init -upgrade moved it, so code and download drift apart between machines.

    Check

    grep -n -A2 'source *=' main.tf   # no version line under a registry source

    Fix

    Add an explicit version to the module block, then terraform init.

Prevent it

Now learn it: break and fix it yourself in Unpinned module upgraded itself, then take Level 09 · Modules. Open interactive

No valid credential sources found

Error: No valid credential sources found

Please see https://registry.terraform.io/providers/hashicorp/aws
for more information about providing credentials.

What it means

The AWS provider looked through its credential chain (environment variables, the shared config profile, SSO, a container or instance role) and found nothing to sign requests with. "no EC2 IMDS role found" means the last step, the instance metadata service, gave nothing either.

Causes, most likely first

  1. Nothing is set in this shell or job: AWS_PROFILE is unset and there is no default profile or key.

    Check

    aws configure list   # profile <not set> and no access_key: the chain is empty (keys are masked)
    env | grep -o '^AWS_[A-Z_]*'   # variable names only, never values

    Fix

    export AWS_PROFILE=<profile>; for SSO profiles run aws sso login --profile <profile> first.

  2. In GitHub Actions, there is no aws-actions/configure-aws-credentials step, or the job lacks permissions: id-token: write, so no role is assumed.

    Check

    grep -rn -B2 -A6 'configure-aws-credentials' .github/workflows   # missing step, or no id-token: write

    Fix

    Add the step with role-to-assume and grant permissions: id-token: write to the job.

  3. On EC2, ECS or in a container there is no role, or IMDSv2's hop limit of 1 stops a container from reaching the metadata service.

    Check

    aws sts get-caller-identity   # fails inside the container, works on the host: hop limit

    Fix

    Give the task or pod its own role (ECS task role, EKS Pod Identity or IRSA). Raising the hop limit to 2 also works, but hands the node's role to every container.

  4. The CLI finds credentials, but the provider does not, because the provider block sets profile, shared_credentials_files or assume_role differently from your shell.

    Check

    aws sts get-caller-identity   # works in this shell
    grep -n -A6 'provider "aws"' *.tf   # a profile or file that does not exist here

    Fix

    Make the provider block match the environment, or drop the hard-coded profile and set AWS_PROFILE per environment.

Prevent it

Now learn it: break and fix it yourself in No valid credential sources found, then take Level 15 · Secure by default. Open interactive

Unsupported argument

Error: Unsupported argument

An argument named "instance_typ" is not expected here. Did you mean
"instance_type"?

What it means

Terraform checked the block against the schema (the list of arguments a resource, data source or module accepts) and found a name it does not define. The error points at the file and line, and "Did you mean" suggests the closest real name.

Causes, most likely first

  1. A typo in the argument name.

    Check

    terraform validate   # the same error with a "Did you mean" suggestion

    Fix

    Use the suggested name.

  2. The argument exists, but only in a newer provider than the one locked: the docs you read are for the latest version.

    Check

    terraform version   # the provider version in use; open the docs for that version

    Fix

    Upgrade the provider (raise the constraint, terraform init -upgrade) in a reviewed change.

  3. The argument was removed or renamed in the provider version you upgraded to.

    Check

    git diff origin/main -- .terraform.lock.hcl   # a provider major version changed

    Fix

    Follow the provider's upgrade guide for the new name.

  4. For a module block: the installed module version does not declare that input variable.

    Check

    grep -n 'variable "' .terraform/modules/<name>/variables.tf   # the input is not there

    Fix

    Pin the module version the code was written for, or use its new input name.

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, Unpinned module upgraded itself, AWS provider major upgrade, then take Level 01 · Speaking HCL. Open interactive

Unsupported block type

Error: Unsupported block type

Blocks of type "ingres" are not expected here. Did you mean "ingress"?

What it means

A nested or top-level block uses a name the schema does not define. Arguments are written name = value; blocks are written name { }, and the two are not interchangeable.

Causes, most likely first

  1. A typo in the block name.

    Check

    terraform validate   # "Did you mean" names the real block

    Fix

    Use the suggested name.

  2. An argument written as a block, such as tags { } instead of tags = { }.

    Check

    grep -n 'tags {' *.tf   # a map argument written without =

    Fix

    Write it as an argument: tags = { Name = "web" }.

  3. A block newer than your Terraform or provider: import blocks need Terraform 1.5, removed blocks 1.7.

    Check

    terraform version   # older than the version that added the block

    Fix

    Upgrade the CLI or provider, or use the command-line equivalent (terraform import).

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, then take Level 01 · Speaking HCL. Open interactive

Missing required argument

Error: Missing required argument

The argument "ami" is required, but no definition was found.

What it means

A required argument, or a module input variable without a default, has no value in this block.

Causes, most likely first

  1. The argument was forgotten or deleted.

    Check

    terraform validate   # names the argument and the block

    Fix

    Set the argument.

  2. A module upgrade added a new required input.

    Check

    git diff origin/main -- '*.tf' | grep version   # the module version changed
    grep -n -A3 'variable "' .terraform/modules/<name>/variables.tf   # the new input has no default

    Fix

    Pass the new input, after reading the module's changelog.

Prevent it

Now learn it: break and fix it yourself in Unpinned module upgraded itself, then take Level 01 · Speaking HCL. Open interactive

Invalid reference

Error: Invalid reference

A reference to a resource type must be followed by at least one attribute
access, specifying the resource name.

What it means

Terraform read a bare word as the start of a resource reference (type.name.attribute) and found only the first part. A value needs its namespace: var., local., module., data., or a resource type and name.

Causes, most likely first

  1. A variable or local written without its prefix, such as env instead of var.env.

    Check

    terraform validate   # the line and the bare word

    Fix

    Use var.<name>, local.<name>, module.<name>.<output>, or <type>.<name>.<attr>.

  2. Interpolation inside a string without the prefix, such as "${name}-web".

    Check

    grep -n '${[a-z_]*}' *.tf   # ${name} without var.

    Fix

    Write "${var.name}-web".

Prevent it

Now learn it: break and fix it yourself in Invalid reference, then take Level 01 · Speaking HCL. Open interactive

Reference to undeclared resource

Error: Reference to undeclared resource

A managed resource "aws_security_group" "web_sg" has not been declared in
the root module.

What it means

The reference names a resource type and name that this module does not declare. Each module only sees its own resources.

Causes, most likely first

  1. The resource was renamed and some references were not updated.

    Check

    grep -rn 'resource "aws_security_group"' .   # the real name

    Fix

    Fix the reference. If the rename is new, add a moved block so state follows.

  2. The resource lives in another module (a child or the parent), which a module cannot reach into.

    Check

    grep -rn 'resource "aws_security_group" "web_sg"' .   # found under modules/, not here

    Fix

    Expose it as an output of that module and use module.<name>.<output>.

Prevent it

Now learn it: break and fix it yourself in Invalid reference, Module output that does not exist, then take Level 01 · Speaking HCL. Open interactive

Reference to undeclared input variable

Error: Reference to undeclared input variable

An input variable with the name "region" has not been declared.

What it means

var.<name> is used, but no variable block in this module declares it. Variables belong to one module; the root's variables are not visible inside child modules.

Causes, most likely first

  1. The variable block is missing or misspelled.

    Check

    grep -rn 'variable "' *.tf   # no block with that name here

    Fix

    Add variable "<name>" {} with a type and description.

  2. The variable is declared in the root, but used inside a child module.

    Check

    pwd; grep -rn 'variable "region"' ..   # declared one level up

    Fix

    Declare it in the child module too and pass it in the module block.

Prevent it

Now learn it: break and fix it yourself in Invalid reference, then take Level 02 · Inputs and outputs. Open interactive

Unsupported attribute

Error: Unsupported attribute

This object does not have an attribute named "subnet_id".

What it means

The code reads an attribute that the object does not have: a module output that was never declared, a typo, or a list or map used as if it were one object.

Causes, most likely first

  1. The module does not declare that output.

    Check

    grep -n 'output "' modules/<name>/outputs.tf   # no output with that name

    Fix

    Add the output to the module, or use one that exists.

  2. A typo in a resource's exported attribute.

    Check

    terraform console   # then: keys(aws_instance.web) lists real attributes

    Fix

    Use the attribute name the console shows.

  3. The value is a list (count) or map (for_each), and lists have no named attributes.

    Check

    terraform console   # type(module.vpc.subnets) shows list or map

    Fix

    Index it ([0], ["key"]) or use a splat ([*].subnet_id).

Prevent it

Now learn it: break and fix it yourself in Module output that does not exist, then take Level 09 · Modules. Open interactive

Missing resource instance key

Error: Missing resource instance key

Because aws_instance.bastion has "count" set, its attributes must be
accessed on specific instances.

What it means

With count or for_each, a resource is a collection of instances, so its attributes need an index ([0], ["key"]) or a splat ([*]).

Causes, most likely first

  1. count was added to make a resource optional, and old references still treat it as one object.

    Check

    grep -n 'count' main.tf   # count on the resource named in the error

    Fix

    Use one(aws_instance.bastion[*].public_ip), which gives null when count is 0.

  2. The resource was converted to for_each, so instances are keyed by name.

    Check

    terraform state list | grep bastion   # instances with ["key"]

    Fix

    Use aws_instance.bastion["name"].attr, or a for expression over all of them.

Prevent it

Now learn it: break and fix it yourself in Missing resource instance key, then take Level 07 · Many of a kind. Open interactive

Cycle

Error: Cycle: aws_security_group.db, aws_security_group.app

What it means

Terraform builds a graph of what depends on what. The names in the error reference each other in a loop, so there is no order in which to create them.

Causes, most likely first

  1. Two security groups with inline rules pointing at each other: each needs the other's ID first.

    Check

    grep -n -A6 'ingress {' main.tf   # each group's rule names the other

    Fix

    Create both groups without inline rules, then link them with aws_vpc_security_group_ingress_rule and egress_rule resources.

  2. A depends_on closes a loop with an ordinary reference.

    Check

    grep -rn depends_on *.tf   # a depends_on between the named objects
    terraform graph | grep -E 'aws_security_group\.(db|app)'   # the edges

    Fix

    Remove the depends_on; references already order things.

Prevent it

Now learn it: break and fix it yourself in Dependency cycle, then take Level 03 · The dependency graph. Open interactive

Invalid for_each argument

Error: Invalid for_each argument

The "for_each" set includes values derived from resource attributes that
cannot be determined until apply

What it means

for_each keys decide resource addresses, so they must be known at plan time. This value depends on something created in the same apply, or is a list rather than a map or set of strings.

Causes, most likely first

  1. The keys come from resource attributes created in this apply, such as IDs: for_each = toset(aws_subnet.x[*].id).

    Check

    terraform plan   # the error points at the for_each line

    Fix

    Key by names written in code (a map of name to config) and keep IDs in the values.

  2. A list was passed, such as for_each = var.names. Lists are rejected; the message then says it must be a map or set of strings.

    Check

    terraform console   # type(var.names) shows list or tuple

    Fix

    Wrap it in toset().

Prevent it

Now learn it: break and fix it yourself in for_each over values known after apply, count deletes the wrong runner, then take Level 07 · Many of a kind. Open interactive

Invalid count argument

Error: Invalid count argument

The "count" value depends on resource attributes that cannot be determined
until apply

What it means

count must be known at plan time, because it decides how many instances exist. This one depends on a value only known after apply.

Causes, most likely first

  1. count = length(<something created in this apply>), such as a list of IDs from another resource.

    Check

    terraform plan   # the error points at the count line

    Fix

    Derive count from variables or locals written in code.

Prevent it

Now learn it: break and fix it yourself in for_each over values known after apply, then take Level 07 · Many of a kind. Open interactive

Provider configuration not present

Error: Provider configuration not present

To work with aws_acm_certificate.shop its original provider configuration at
provider["registry.terraform.io/hashicorp/aws"].us_east_1 is required

What it means

An object needs a provider configuration that no provider block declares. "its original provider configuration" means the object is in state and was made by that configuration, often an alias such as aws.us_east_1.

Causes, most likely first

  1. The provider block or alias was removed while objects it created are still in state. Terraform needs it to destroy or refresh them.

    Check

    terraform providers   # the alias appears under the state's requirements only
    terraform state list | grep acm   # objects still in state that use it

    Fix

    Put the provider block back until those objects are destroyed or moved, then remove it.

  2. The alias is misspelled.

    Check

    grep -rn alias *.tf   # us-east-1 vs us_east_1

    Fix

    Declare provider "aws" { alias = "us_east_1" region = "us-east-1" } with the exact name.

  3. A child module expects the alias passed in, and the module block does not pass it.

    Check

    grep -n -A5 'module "' main.tf   # no providers = { ... } line

    Fix

    Add providers = { aws.us_east_1 = aws.us_east_1 } to the module block.

Prevent it

Now learn it: break and fix it yourself in Certificate in the wrong region, then take Level 10 · Providers and versions. Open interactive

No value for required variable

Error: No value for required variable

The root module input variable "environment" is not set, and has no default
value.

What it means

A root variable has no default and got no value from any source: tfvars files, TF_VAR_ environment variables or -var flags. With -input=false (as in CI) Terraform cannot ask, so it stops.

Causes, most likely first

  1. terraform.tfvars is gitignored, so it exists on your laptop but never reaches CI.

    Check

    git check-ignore -v terraform.tfvars   # prints the ignore rule: CI never has this file

    Fix

    Set TF_VAR_<name> in the workflow (from a secret store for secrets), or commit a non-secret env.tfvars and pass -var-file.

  2. The pipeline does not set TF_VAR_<name>, or sets it with different case: TF_VAR_Environment does not set var.environment.

    Check

    env | grep -o '^TF_VAR_[A-Za-z0-9_]*'   # names only; values may be secrets

    Fix

    Export TF_VAR_<name> with the exact variable name.

  3. The command ran in a different folder than the tfvars file, so the automatic files were not loaded.

    Check

    ls *.tfvars   # no terraform.tfvars or *.auto.tfvars here

    Fix

    Run in the right folder, or pass -var-file with its path.

Prevent it

Now learn it: break and fix it yourself in Variable not set in CI, then take Level 02 · Inputs and outputs. Open interactive

Invalid value for variable

Error: Invalid value for variable

environment must be dev, staging or prod.

What it means

A validation block on the variable rejected the value. The text shown is that block's error_message, and the line after it names the rule.

Causes, most likely first

  1. A typo or an alias, such as production for prod.

    Check

    grep -n environment *.tfvars   # the value being passed
    grep -n -A6 'variable "environment"' variables.tf   # the rule

    Fix

    Pass a valid value.

  2. The value comes from a source you did not expect. *.auto.tfvars and -var beat terraform.tfvars, and TF_VAR_ loses to every file.

    Check

    ls *.auto.tfvars   # a file overriding terraform.tfvars
    env | grep -o '^TF_VAR_[A-Za-z0-9_]*'   # names only

    Fix

    Remove the stray source, so one place sets each value.

Prevent it

Now learn it: break and fix it yourself in Invalid value for variable, then take Level 02 · Inputs and outputs. Open interactive

Output refers to sensitive values

Error: Output refers to sensitive values

To reduce the risk of accidentally exporting sensitive data that was
intended to be only internal, Terraform requires that any root module
output containing sensitive data be explicitly marked as sensitive

What it means

A root output contains a value marked sensitive (a sensitive variable, or a provider attribute such as a password), and the output itself is not marked sensitive. Terraform refuses to print it by accident.

Causes, most likely first

  1. A string or URL built from a sensitive value, which keeps the mark.

    Check

    terraform plan   # names the output and its file and line
    grep -rn 'sensitive *= *true' *.tf   # the variables or outputs its value is built from

    Fix

    Add sensitive = true to the output, and read it with terraform output -raw <name> where you need it. Do not remove sensitive from the variable to silence the error.

Prevent it

Now learn it: break and fix it yourself in Output refers to sensitive values, then take Level 15 · Secure by default. Open interactive

No outputs found

Warning: No outputs found

The state file either has no outputs defined, or all the defined outputs
are empty.

What it means

terraform output reads outputs from state, not from code. This state has none saved yet.

Causes, most likely first

  1. The output was added after the last apply.

    Check

    terraform plan   # "Changes to Outputs" lists it

    Fix

    terraform apply (or apply -refresh-only, which saves outputs without changing resources).

  2. You are reading another workspace or backend than the one that was applied.

    Check

    terraform workspace show   # not the workspace you expect
    terraform state list | head   # empty: wrong state

    Fix

    terraform workspace select <name>, or init with the right backend.

Prevent it

Now learn it: break and fix it yourself in No outputs found, then take Level 02 · Inputs and outputs. Open interactive

terraform fmt -check exits with code 3

terraform fmt -check -recursive
main.tf
Error: Process completed with exit code 3.

What it means

terraform fmt -check lists files that are not in canonical style and exits with code 3, which fails the CI step. It changes nothing.

Causes, most likely first

  1. Hand-indented code or misaligned = signs.

    Check

    terraform fmt -check -diff -recursive   # the exact changes it wants

    Fix

    terraform fmt -recursive and commit.

  2. CI runs a different Terraform version, whose formatting rules can differ slightly, so files formatted locally still fail.

    Check

    terraform version   # compare with terraform_version in the workflow

    Fix

    Pin the same version in .terraform-version and setup-terraform.

Prevent it

Now learn it: break and fix it yourself in CI fails on terraform fmt, then take Level 14 · Terraform in CI/CD. Open interactive

Error acquiring the state lock

Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        b7d1e9a2-5c3f-4e8b-a1d6-0f2c9e4b7a35
  Operation: OperationTypeApply
  Who:       runner@fv-az1279-614

What it means

Before changing state, Terraform takes a lock so two runs cannot write at once. It could not. If the message prints Lock Info, another run holds the lock, and Who, Operation and Created say which. If there is no Lock Info, the lock call itself failed. With DynamoDB locking a held lock shows ConditionalCheckFailedException; with S3 native locking (use_lockfile) it shows PreconditionFailed (412).

Causes, most likely first

  1. A colleague or pipeline is applying right now and holds the lock legitimately.

    Check

    # Read Who and Created in Lock Info, then look for that job or person
    gh run list --status in_progress   # a running workflow matches Who

    Fix

    Wait for it to finish. Use -lock-timeout=5m in CI so jobs wait instead of failing.

  2. A CI job was killed mid-apply (runner recycled, timeout, Ctrl+C) and never released its lock.

    Check

    aws dynamodb get-item --table-name tf-locks --key '{"LockID":{"S":"<bucket>/<key>"}}'   # DynamoDB: the item exists, Info matches Lock Info
    aws s3api head-object --bucket <bucket> --key <key>.tflock   # use_lockfile (Terraform 1.10+, OpenTofu 1.10+): the lock object exists

    Fix

    Once you are sure the owner is gone: terraform force-unlock <LOCK_ID> (works for both lock types). Never use -lock=false to get past it.

  3. Two configurations or workspaces share one bucket and key, so they share a lock (and a state).

    Check

    grep -rn -A4 'backend "s3"' --include='*.tf' .   # the same key in two places

    Fix

    Give each configuration its own key, and move state with init -migrate-state.

  4. No Lock Info: the role cannot write the lock (dynamodb:PutItem on the table, or s3:PutObject on <key>.tflock), or dynamodb_table names a table that does not exist.

    Check

    # The Error message line shows AccessDenied, AccessDeniedException or ResourceNotFoundException
    aws dynamodb describe-table --table-name tf-locks --query Table.TableStatus   # ResourceNotFoundException: no table

    Fix

    Grant the lock actions on that table or key to the role Terraform runs as, or fix dynamodb_table. On Terraform 1.11+ or OpenTofu 1.10+ prefer use_lockfile; Terraform deprecates dynamodb_table from 1.11.

Prevent it

Now learn it: break and fix it yourself in State lock stuck after a crashed CI job, then take Level 13 · Shared state. Open interactive

Backend initialization required

Error: Backend initialization required, please run "terraform init"

Reason: Initial configuration of the requested backend "s3"

What it means

The backend block (where state lives) differs from what this directory was initialised with, recorded in .terraform/terraform.tfstate. The Reason line says how: initial configuration, changed, or unset.

Causes, most likely first

  1. A fresh checkout, or the backend block was added: this directory was never initialised with it.

    Check

    ls .terraform/terraform.tfstate 2>/dev/null   # missing: never initialised

    Fix

    terraform init.

  2. The backend block or -backend-config values changed since the last init, so Terraform must decide whether to move state.

    Check

    git diff -- '*.tf'   # the backend block changed
    jq '.backend.type, (.backend.config | {bucket, key, region})' .terraform/terraform.tfstate   # what was used last; print only these fields, the file can hold credentials

    Fix

    terraform init -migrate-state to copy existing state to the new place; -reconfigure only when no state should move.

  3. TF_DATA_DIR, or a CI cache restored to another path, points at a .terraform from another configuration.

    Check

    echo "$TF_DATA_DIR"   # set to a shared or cached path

    Fix

    Use one data directory per configuration, or run init in each job.

Prevent it

Now learn it: break and fix it yourself in Move state to an S3 backend, Backend without locking, then take Level 13 · Shared state. Open interactive

Backend configuration changed

Error: Backend configuration changed

A change in the backend configuration has been detected, which may require
migrating existing state.

What it means

terraform init noticed the backend settings differ from the last init and needs you to say whether state should move.

Causes, most likely first

  1. The bucket or key changed: state must be copied to the new place.

    Check

    git diff -- versions.tf   # bucket or key differs
    jq '.backend.config | {bucket, key, region}' .terraform/terraform.tfstate   # the old values

    Fix

    terraform init -migrate-state, then check terraform state list shows everything.

  2. Only locking or other settings changed (dynamodb_table, use_lockfile, encrypt): the state stays where it is.

    Check

    git diff -- versions.tf   # bucket and key unchanged

    Fix

    terraform init -reconfigure.

  3. A different -backend-config file than last time (another environment).

    Check

    history | grep backend-config | tail -3   # the file used last

    Fix

    Use a separate checkout or data directory per environment, and pass the right file.

Prevent it

Now learn it: break and fix it yourself in Backend without locking, then take Level 13 · Shared state. Open interactive

S3 bucket does not exist (backend)

Error: Failed to get existing workspaces: S3 bucket "nimbus-tf-state" does not exist.

What it means

S3 says no bucket by that name exists, in any account. A bucket that exists in another account answers 403 AccessDenied instead, so this is the name, not your permissions.

Causes, most likely first

  1. A typo in the bucket name.

    Check

    aws s3api head-bucket --bucket nimbus-tf-state   # 404: no such bucket anywhere; 403: it exists and the problem is elsewhere

    Fix

    Fix the name, then terraform init.

  2. The -backend-config file or partial configuration for another environment or account was used.

    Check

    grep -n -A8 'backend "s3"' *.tf   # bucket, region, profile, assume_role
    aws sts get-caller-identity   # the account you meant?

    Fix

    Pass the right -backend-config file, then terraform init -reconfigure.

  3. The bucket was never created in this account (the bootstrap step was skipped), or was deleted.

    Check

    aws s3api list-buckets --query "Buckets[?contains(Name,'tf-state')].Name"   # nothing similar

    Fix

    Create it from its bootstrap configuration, with versioning on and public access blocked. Never create an empty bucket just to make the error go away if state should exist: you would start from empty state.

Prevent it

Now learn it: break and fix it yourself in Backend bucket does not exist, then take Level 13 · Shared state. Open interactive

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.

What it means

A saved plan file records the state it was made from. The state has changed since (its serial moved on), so applying the old plan could undo someone else's work.

Causes, most likely first

  1. Another apply ran between your plan and your apply.

    Check

    terraform show tfplan | head   # the plan you are trying to apply
    terraform state pull | jq .serial   # higher than when you planned

    Fix

    Plan again with -out, review it, and apply that file.

  2. In CI, plan and apply run in different jobs and something applied in between.

    Check

    # the workflow's history shows another apply between the two jobs

    Fix

    Serialise applies per state (a concurrency group) and plan close to apply.

Prevent it

Now learn it: break and fix it yourself in Saved plan is stale, then take Level 14 · Terraform in CI/CD. Open interactive

Cannot import non-existent remote object

Error: Cannot import non-existent remote object

While attempting to import an existing object to "aws_s3_bucket.assets",
the provider detected that no object exists with the given id.

What it means

Terraform asked the provider to read the object with the ID you gave, and the provider found nothing with that ID in the account and region it uses.

Causes, most likely first

  1. The ID is in the wrong format for this resource type. Each type documents its own import ID.

    Check

    # Read the Import section of the resource's docs: a bucket name, an ARN, or an id like sg-0a1b2c3d

    Fix

    Use the documented ID format.

  2. The object is in another region or account than the provider uses.

    Check

    aws sts get-caller-identity   # the account
    grep -n region *.tf   # the provider's region, which may differ from your CLI's

    Fix

    Import with the provider alias or profile for the right account and region.

Prevent it

Now learn it: break and fix it yourself in Importing an existing bucket, then take Level 08 · Reading what exists. Open interactive

Resource already managed by Terraform

Error: Resource already managed by Terraform

Terraform is already managing a remote object for aws_s3_bucket.assets.

What it means

The address you are importing into already has an object in state, so Terraform will not attach a second one.

Causes, most likely first

  1. The import ran twice, or the object was already imported by an import block.

    Check

    terraform state show aws_s3_bucket.assets   # it is there, with the same ID

    Fix

    Nothing to do; remove the duplicate import.

  2. The address is wrong: you meant another resource name, and this one holds a different object.

    Check

    terraform state show aws_s3_bucket.assets | grep -m1 id   # a different ID

    Fix

    Import into the right address. Use terraform state rm only if state is truly wrong.

Prevent it

Now learn it: break and fix it yourself in Importing an existing bucket, then take Level 08 · Reading what exists. Open interactive

Workspace doesn't exist

Workspace "staging" doesn't exist.

You can create this workspace with the "new" subcommand
or include the "-or-create" flag with the "select" subcommand.

What it means

No state exists under that workspace name in this backend, so Terraform will not switch to it.

Causes, most likely first

  1. A typo, or the workspace was never created.

    Check

    terraform workspace list   # the names that exist

    Fix

    Select an existing one, or create it on purpose with terraform workspace new.

  2. TF_WORKSPACE is set (often in CI) to a name that has no state yet.

    Check

    echo "${TF_WORKSPACE:-not set}"   # a name missing from terraform workspace list

    Fix

    Fix or unset TF_WORKSPACE, or create the workspace in a deliberate step.

  3. A different backend, or workspace_key_prefix changed, so existing workspaces are looked for elsewhere.

    Check

    grep -n workspace_key_prefix *.tf   # changed in git diff?

    Fix

    Restore the old prefix, or migrate state to the new one.

Prevent it

Now learn it: break and fix it yourself in Wrong workspace selected, then take Level 13 · Shared state. Open interactive

Plan shows -/+ … # forces replacement

-/+ resource "aws_db_instance" "orders" {
      ~ identifier = "orders-db" -> "orders-db-prod" # forces replacement

What it means

-/+ means Terraform will destroy the object and create a new one. "# forces replacement" marks the argument that cannot change in place; for a database or a disk, that means its data goes with it.

Causes, most likely first

  1. An argument the API cannot change in place was edited: an identifier, name, availability zone, AMI or user_data.

    Check

    terraform plan | grep -B3 'forces replacement'   # the argument and its old and new value

    Fix

    Revert the argument. If the change is needed, plan a migration (snapshot, then create_before_destroy where the name allows it).

  2. The value became unknown at plan time, often because depends_on on a module defers a data source, so Terraform must assume it changed.

    Check

    terraform plan | grep 'known after apply' | grep 'forces replacement'   # the replaced argument is unknown, not changed
    terraform plan | grep 'read during apply'   # a deferred data source

    Fix

    Remove the depends_on and pass real references instead.

Prevent it

Now learn it: break and fix it yourself in A rename that replaces the database, DependencyViolation on replace, depends_on replaces the app server, then take Level 05 · Reading a plan. Open interactive

Instance cannot be destroyed

Error: Instance cannot be destroyed

Resource aws_db_instance.orders has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed.

What it means

Your guard worked: the plan would destroy a resource that has lifecycle { prevent_destroy = true }, so Terraform refuses the whole plan. The guard lives in the resource block, so deleting the block removes it too, and Terraform then destroys the object without this error.

Causes, most likely first

  1. A change forces replacement, which includes a destroy.

    Check

    terraform plan | grep -B3 'forces replacement'   # the argument that triggered it

    Fix

    Undo that change.

  2. terraform destroy, or plan -destroy, includes the protected resource.

    Check

    # the command you ran was destroy or plan -destroy

    Fix

    For a deliberate, reviewed teardown only: remove prevent_destroy, apply, then destroy.

Prevent it

Now learn it: break and fix it yourself in A rename that replaces the database, then take Level 06 · Lifecycle rules. Open interactive

Objects have changed outside of Terraform

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply"

What it means

Before planning, Terraform refreshed state from the real objects and found differences. These are listed first; the plan below will undo them unless your code matches the change.

Causes, most likely first

  1. Someone edited the object in the console or CLI.

    Check

    terraform plan -refresh-only   # exactly what changed outside Terraform

    Fix

    Unwanted change: apply to put it back. Wanted change: update the code to match.

  2. Another system owns that attribute, such as an autoscaler setting desired_capacity.

    Check

    terraform plan -refresh-only   # the same attribute changes every time

    Fix

    lifecycle { ignore_changes = [desired_capacity] }, so only one owner manages it.

Prevent it

Now learn it: break and fix it yourself in Drift from a console change, Every apply resets the autoscaler, then take Level 12 · Drift. Open interactive

Removing one list item changes or destroys others

  # aws_instance.runner[0] will be updated in-place
  # aws_instance.runner[2] will be destroyed
  # (because index [2] is out of range for count)

What it means

count addresses instances by position ([0], [1], [2]). Removing an item from the middle of the list shifts every index after it, so Terraform updates the later instances and destroys the last one.

Causes, most likely first

  1. count = length(list) is used for named things, and an item was removed or reordered.

    Check

    terraform state list | grep runner   # instances by number
    terraform plan   # in-place updates plus a destroy at the end

    Fix

    Switch to for_each keyed by name, and move each instance with moved blocks (from = aws_instance.runner[0], to = aws_instance.runner["a"]).

Prevent it

Now learn it: break and fix it yourself in count deletes the wrong runner, then take Level 07 · Many of a kind. Open interactive

Renamed resource plans destroy and create

  # aws_instance.web will be destroyed
  # (because aws_instance.web is not in configuration)
  # aws_instance.app will be created

What it means

Terraform tracks objects by address (type.name). The old address is gone from the code, so its object is planned for destruction, and the new address is planned as a new object.

Causes, most likely first

  1. The resource was renamed.

    Check

    terraform plan   # one destroy and one create of the same type

    Fix

    Add moved { from = aws_instance.web to = aws_instance.app } and plan again: it should show no changes.

  2. The resource was moved into (or out of) a module, which changes its address.

    Check

    terraform state list | grep web   # the old address, without module.

    Fix

    moved { from = aws_instance.web to = module.app.aws_instance.web }.

Prevent it

Now learn it: break and fix it yourself in Moved block after a rename, then take Level 11 · Refactoring safely. Open interactive

Data source read during apply causes replacement

  # module.app.data.aws_ami.base will be read during apply
  # (depends on a resource or a module with changes pending)

What it means

A data source will be read during apply instead of plan, so its results are unknown now. Anything built from them shows as known after apply, and can force replacements.

Causes, most likely first

  1. depends_on on a module or data source makes Terraform wait for every pending change in it before reading.

    Check

    grep -rn depends_on *.tf   # depends_on = [module.x]

    Fix

    Remove depends_on and pass real values by reference. Keep depends_on for hidden dependencies only.

  2. The data source's arguments reference a resource that changes in this plan.

    Check

    terraform plan | grep -A3 'read during apply'   # the argument comes from a changing resource

    Fix

    Reference a stable value instead, such as a name written in code.

Prevent it

Now learn it: break and fix it yourself in depends_on replaces the app server, then take Level 09 · Modules. Open interactive

Resource targeting is in effect

Warning: Resource targeting is in effect

You are creating a plan with the -target option, which means that the
result of this plan may not represent all of the changes requested

What it means

-target planned only the named resources and their dependencies. Everything else in the configuration waits silently, so state and code drift apart.

Causes, most likely first

  1. -target was used to ship one change quickly.

    Check

    terraform plan   # without -target, to see what else is pending

    Fix

    Plan and apply without -target. Keep -target for recovery from a broken apply.

Prevent it

Now learn it: break and fix it yourself in -target left changes behind, then take Level 14 · Terraform in CI/CD. Open interactive

DependencyViolation when deleting a security group

Error: deleting Security Group (sg-0a1b2c3d4e5f60718): DependencyViolation: resource sg-0a1b2c3d4e5f60718 has a dependent object

What it means

AWS will not delete a security group while something still uses it: an instance, a network interface (ENI) or another group's rule. Terraform destroys before it creates by default, so replacing a group in use hits this.

Causes, most likely first

  1. Terraform is replacing a group that instances still use.

    Check

    terraform plan | grep -B2 'aws_security_group'   # -/+ on the group

    Fix

    lifecycle { create_before_destroy = true } with name_prefix, so the new group exists before the old one goes.

  2. Leftover network interfaces, often from Lambda or load balancers, still use the group.

    Check

    aws ec2 describe-network-interfaces --filters Name=group-id,Values=<sg> --query 'NetworkInterfaces[].[NetworkInterfaceId,Description]'   # what still uses it

    Fix

    Remove those users first. ENIs that Lambda created are released by AWS some time after the function stops using them.

  3. Something outside this configuration uses it: another stack's instance, or another group's rule.

    Check

    aws ec2 describe-security-groups --filters Name=ip-permission.group-id,Values=<sg> --query 'SecurityGroups[].GroupId'   # groups whose rules point at it

    Fix

    Change or remove those references, in their own configuration.

Prevent it

Now learn it: break and fix it yourself in DependencyViolation on replace, then take Level 06 · Lifecycle rules. Open interactive

InvalidGroup.Duplicate

Error: creating Security Group (web): InvalidGroup.Duplicate: The security group 'web' already exists for VPC 'vpc-0a1b2c3d4e5f60718'

What it means

Security group names must be unique within a VPC, and a group with this name already exists there.

Causes, most likely first

  1. create_before_destroy with a fixed name: the new group is created while the old one with the same name still exists.

    Check

    grep -n -B2 -A4 'create_before_destroy' *.tf   # on a group with name =, not name_prefix

    Fix

    Use name_prefix instead of name.

  2. A hand-made group, or one from another configuration, has the same name.

    Check

    aws ec2 describe-security-groups --filters Name=group-name,Values=web Name=vpc-id,Values=<vpc> --query 'SecurityGroups[].[GroupId,Tags]'   # who owns it

    Fix

    Import it if this configuration should own it, or choose another name.

Prevent it

Now learn it: break and fix it yourself in DependencyViolation on replace, then take Level 06 · Lifecycle rules. Open interactive

BucketAlreadyOwnedByYou / BucketAlreadyExists

Error: creating S3 Bucket (nimbus-assets-2026): BucketAlreadyOwnedByYou: Your previous request to create the named bucket succeeded and you already own it.

What it means

S3 bucket names are global. BucketAlreadyOwnedByYou means your account already has it; BucketAlreadyExists means another account does.

Causes, most likely first

  1. BucketAlreadyOwnedByYou: the bucket was made by hand or by another configuration, or state was lost or not migrated.

    Check

    terraform state list | grep aws_s3_bucket   # not in this state
    aws s3api get-bucket-tagging --bucket nimbus-assets-2026   # tags may say which stack made it

    Fix

    Import it with an import block, or fix the state (restore or migrate) if it should already be there.

  2. BucketAlreadyExists: someone else owns the name.

    Check

    aws s3api head-bucket --bucket nimbus-assets-2026   # 403: exists, not yours

    Fix

    Choose another name, for example with the account ID or a random suffix.

Prevent it

Now learn it: break and fix it yourself in Importing an existing bucket, Move state to an S3 backend, then take Level 08 · Reading what exists. Open interactive

BucketNotEmpty

Error: deleting S3 Bucket (nimbus-archive-2019): BucketNotEmpty: The bucket you tried to delete is not empty

What it means

S3 refuses to delete a bucket that still holds objects, object versions or delete markers, unless force_destroy is set.

Causes, most likely first

  1. The bucket holds data, and the plan removes or destroys it.

    Check

    aws s3 ls s3://<bucket> --recursive --summarize | tail -2   # Total Objects above 0

    Fix

    If the data must stay: a removed block with lifecycle { destroy = false }, or terraform state rm. Only empty it if you mean to delete the data.

  2. Versioning: old versions and delete markers remain after the current objects are gone, so aws s3 ls shows nothing.

    Check

    aws s3api list-object-versions --bucket <bucket> --max-items 5   # Versions or DeleteMarkers listed

    Fix

    Decide as above; emptying a versioned bucket means deleting every version.

Prevent it

Now learn it: break and fix it yourself in Stop managing without destroying, then take Level 11 · Refactoring safely. Open interactive

Cannot delete protected DB Instance

Error: deleting RDS DB Instance (loadtest-db): InvalidParameterCombination: Cannot delete protected DB Instance, please disable deletion protection and try again.

What it means

deletion_protection is on for this database, so AWS refuses the delete. Terraform's state still has the database.

Causes, most likely first

  1. terraform destroy, or a change that replaces the database, ran against a protected instance.

    Check

    aws rds describe-db-instances --db-instance-identifier loadtest-db --query 'DBInstances[0].DeletionProtection'   # true

    Fix

    For a deliberate teardown: set deletion_protection = false, apply, then destroy. Never terraform state rm it to get around the protection.

Prevent it

Now learn it: break and fix it yourself in Destroy stops at the database, then take Level 06 · Lifecycle rules. Open interactive

AccessControlListNotSupported

Error: creating S3 Bucket ACL (nimbus-partner-reports,public-read): AccessControlListNotSupported: The bucket does not allow ACLs

What it means

New S3 buckets have Object Ownership set to bucket owner enforced, which turns ACLs off. Any ACL other than private is rejected.

Causes, most likely first

  1. Code from an older tutorial sets acl = "public-read" or an aws_s3_bucket_acl resource.

    Check

    aws s3api get-bucket-ownership-controls --bucket <bucket>   # BucketOwnerEnforced

    Fix

    Remove the ACL. Grant access with a bucket policy, CloudFront, or presigned URLs, and keep a public access block.

Prevent it

Now learn it: break and fix it yourself in Public bucket ACL rejected, then take Level 15 · Secure by default. Open interactive

InvalidViewerCertificate (CloudFront)

Error: creating CloudFront Distribution: InvalidViewerCertificate: The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain.

What it means

CloudFront only accepts ACM certificates from us-east-1, and only once they are issued.

Causes, most likely first

  1. The certificate was created with the default regional provider, in another region.

    Check

    terraform state show aws_acm_certificate.<name> | grep arn   # arn:aws:acm:eu-west-1:...

    Fix

    Create it with an aliased us-east-1 provider (provider = aws.us_east_1).

  2. The certificate is still pending DNS validation.

    Check

    aws acm describe-certificate --region us-east-1 --certificate-arn <arn> --query Certificate.Status   # PENDING_VALIDATION, not ISSUED

    Fix

    Reference aws_acm_certificate_validation.<name>.certificate_arn, so the distribution waits for validation.

Prevent it

Now learn it: break and fix it yourself in Certificate in the wrong region, then take Level 10 · Providers and versions. Open interactive

no matching EC2 VPC found

Error: no matching EC2 VPC found

What it means

A data source's filters matched nothing in the account and region the provider uses. (A different message means more than one matched.)

Causes, most likely first

  1. The tag values differ in this environment.

    Check

    aws ec2 describe-vpcs --region <provider region> --filters Name=tag:Name,Values=<name> --query 'Vpcs[].VpcId'   # empty: the filter matches nothing

    Fix

    Match the real tags, and narrow filters until exactly one object matches.

  2. The provider uses another region or account than you expect.

    Check

    aws sts get-caller-identity   # the account
    grep -n region *.tf   # the provider's region

    Fix

    Run with the right profile, or use the provider alias for that region.

Prevent it

Now learn it: break and fix it yourself in Data source not found, then take Level 08 · Reading what exists. Open interactive

RequestLimitExceeded or Rate exceeded during apply

Error: creating EC2 Instance: operation error EC2: RunInstances, exceeded maximum number of attempts, 1, https response error StatusCode: 503, RequestID: ..., api error RequestLimitExceeded: Request limit exceeded.

What it means

AWS throttled the API calls (RequestLimitExceeded, Throttling) and the provider gave up retrying. Part of the apply succeeded and is in state; the rest is still to do.

Causes, most likely first

  1. max_retries is set very low in the provider block, so the provider gives up at once ("exceeded maximum number of attempts, 1").

    Check

    grep -n max_retries *.tf   # max_retries = 1

    Fix

    Remove max_retries (the default retries with backoff) or raise it.

  2. A very large apply at the default -parallelism=10 calls the same API too fast.

    Check

    terraform state list | wc -l   # thousands of resources in one state

    Fix

    terraform apply -parallelism=3 for very large changes, and split very large states.

  3. Other tools are using the same account's API quota at the same time.

    Check

    # CloudTrail or the CI history shows other jobs running then

    Fix

    Spread scheduled pipelines over time, then run apply again: completed resources are already in state.

Prevent it

Now learn it: break and fix it yourself in API rate limit during apply, then take Level 10 · Providers and versions. Open interactive

timeout while waiting for state to become 'available'

Error: waiting for RDS DB Instance (warehouse-db) create: timeout while waiting for state to become 'available' (last state: 'creating', timeout: 40m0s)

What it means

The API accepted the create, but the object was not ready within the provider's wait time (40m by default for an RDS instance). Terraform marks it tainted in state, so the next apply would replace it.

Causes, most likely first

  1. The create is slow by nature: a restore from a large snapshot, a large Multi-AZ instance, encryption of large volumes.

    Check

    aws rds describe-db-instances --db-instance-identifier warehouse-db --query 'DBInstances[0].DBInstanceStatus'   # available now
    terraform state show aws_db_instance.warehouse | head -3   # tainted

    Fix

    If it is healthy now: terraform untaint aws_db_instance.warehouse, and add timeouts { create = "120m" } for next time.

  2. The create really failed or is stuck (a bad parameter group, a service incident).

    Check

    aws rds describe-events --source-type db-instance --source-identifier warehouse-db   # the failure reason

    Fix

    Let the next apply replace it after fixing the cause, reading that plan first.

Prevent it

Now learn it: break and fix it yourself in Timeout leaves a tainted database, then take Level 05 · Reading a plan. Open interactive

Plan says the resource is tainted, so must be replaced

  # aws_db_instance.warehouse is tainted, so must be replaced
-/+ resource "aws_db_instance" "warehouse" {

What it means

State marks the object as not fully created (a failed or timed-out create, a failed provisioner, or terraform taint), so the next apply destroys it and creates a new one.

Causes, most likely first

  1. The create timed out, but the object came up fine later.

    Check

    terraform state show aws_db_instance.warehouse | head -3   # tainted
    aws rds describe-db-instances --db-instance-identifier warehouse-db --query 'DBInstances[0].DBInstanceStatus'   # available

    Fix

    terraform untaint aws_db_instance.warehouse, then plan: it should not replace it.

  2. A provisioner failed, or the object really is broken.

    Check

    # the earlier apply's output shows the provisioner or create error

    Fix

    Let the replacement happen, ideally with terraform apply -replace=<addr> so it is explicit.

  3. Someone ran terraform taint.

    Check

    # the CI or shell history shows terraform taint

    Fix

    If it was a mistake: terraform untaint <addr>.

Prevent it

Now learn it: break and fix it yourself in Timeout leaves a tainted database, Replace a broken instance, then take Level 05 · Reading a plan. Open interactive

Plan wants to create everything that already exists

Plan: 2 to add, 0 to change, 0 to destroy.
(every resource shows "will be created", but they all exist)

What it means

Terraform plans to create objects that already exist, which means it is reading an empty or different state: the wrong backend, bucket, key or workspace, or the state was overwritten.

Causes, most likely first

  1. The wrong workspace or backend key.

    Check

    terraform workspace show   # not the one you expect
    jq '.backend.config | {bucket, key}' .terraform/terraform.tfstate   # not the key you expect

    Fix

    Select the right workspace or init with the right backend. Do not apply.

  2. init -reconfigure was used instead of -migrate-state, so the new backend started empty.

    Check

    terraform state list   # empty
    aws s3 ls s3://<old-bucket>/<key>   # the old state is still there

    Fix

    terraform init -migrate-state from the old backend.

  3. The state was overwritten, for example by state push -force of an empty file.

    Check

    aws s3api list-object-versions --bucket <bucket> --prefix <key>   # an older, larger version

    Fix

    Download the last good version (aws s3api get-object --bucket <bucket> --key <key> --version-id <id> recovered.tfstate), check it, then terraform state push -force recovered.tfstate.

Prevent it

Now learn it: break and fix it yourself in Restore an overwritten state, Move state to an S3 backend, Backend bucket does not exist, then take Level 13 · Shared state. Open interactive

cannot import state with serial … over newer state

Failed to write state: cannot import state with serial 2 over newer state with serial 3

What it means

terraform state push refuses to replace state with a lower serial (an older write) or a different lineage (another state's history), so you cannot overwrite newer work by accident.

Causes, most likely first

  1. You are restoring an older version on purpose.

    Check

    terraform state pull | jq '{serial, lineage}'   # the current state
    jq '{serial, lineage}' <file>   # same lineage, lower serial

    Fix

    After checking the file is the one you want, and keeping a backup of the current state: terraform state push -force <file>.

  2. The file comes from another configuration or workspace.

    Check

    jq '{serial, lineage}' <file>   # a different lineage

    Fix

    Find the right file. Do not force it.

Prevent it

Now learn it: break and fix it yourself in Restore an overwritten state, then take Level 13 · Shared state. Open interactive

Warning: Argument is deprecated

Warning: Argument is deprecated

  with aws_s3_bucket.reports,
  on main.tf line 3, in resource "aws_s3_bucket" "reports":
   3:   acl    = "private"

Use the aws_s3_bucket_acl resource instead

What it means

This is a warning, not an error: the argument still works with your provider version, but will be removed in a later major version. The line after "Use" names the replacement.

Causes, most likely first

  1. The code was written for an older provider, for example inline acl, versioning or lifecycle_rule on aws_s3_bucket, which moved to separate resources in AWS provider 4.

    Check

    terraform validate   # lists every deprecated argument and where it is
    terraform version   # the provider version, to read the right upgrade guide

    Fix

    Move each one to the replacement resource or argument, and check the plan shows no replacement of the bucket.

Prevent it

Now learn it: break and fix it yourself in AWS provider major upgrade, then take Level 10 · Providers and versions. Open interactive

Test assertion failed (terraform test)

Error: Test assertion failed

  on tests/naming.tftest.hcl line 10, in run "bucket_name_has_environment":
  10:     condition     = aws_s3_bucket.logs.bucket == "nimbus-logs-dev"

The logs bucket name must end with the environment.

What it means

terraform test ran a run block and one of its assert conditions was false for the planned (or applied) values. The text at the end is the assertion's error_message.

Causes, most likely first

  1. A code change broke a convention the test encodes.

    Check

    terraform test -verbose   # the planned values the assertion saw
    terraform console   # evaluate the condition

    Fix

    Fix the code.

  2. The test's variables or expectations no longer match the module.

    Check

    git log -p -- tests/   # the test, not the code, changed

    Fix

    Change the test only when the rule itself changed, in a reviewed change.

Prevent it

Now learn it: break and fix it yourself in terraform test fails, then take Level 14 · Terraform in CI/CD. Open interactive

The plan always shows the same change

  # aws_security_group.db will be updated in-place
  ~ resource "aws_security_group" "db" {
      - ingress {
          - from_port   = 9187 -> null

What it means

The same change appears on every plan, even after apply. Either two owners manage the same thing, or a value never matches what the provider reads back.

Causes, most likely first

  1. Inline security group rules are mixed with separate aws_vpc_security_group_ingress_rule resources on the same group: each apply removes the other's rules.

    Check

    grep -rn 'security_group_id' *.tf   # separate rules on a group that also has inline ingress

    Fix

    Use one style per group: all inline, or all separate rule resources.

  2. Another system owns the attribute, such as an autoscaler.

    Check

    terraform plan -refresh-only   # the attribute changes between runs

    Fix

    ignore_changes for that attribute.

  3. depends_on defers a data source, so its value is unknown on every plan.

    Check

    terraform plan | grep 'read during apply'   # a data source deferred on every plan

    Fix

    Remove the depends_on and pass real references.

Prevent it

Now learn it: break and fix it yourself in A plan that never settles, Every apply resets the autoscaler, depends_on replaces the app server, then take Level 12 · Drift. Open interactive

InvalidPermission.Duplicate

Error: creating VPC Security Group Ingress Rule: InvalidPermission.Duplicate: the specified rule "peer: 10.0.1.0/24, TCP, from port: 5432, to port: 5432, ALLOW" already exists

What it means

The security group already has exactly this rule, so AWS refuses to add it again.

Causes, most likely first

  1. Inline rules were moved to separate rule resources without importing the existing rules.

    Check

    aws ec2 describe-security-group-rules --filters Name=group-id,Values=<sg> --query 'SecurityGroupRules[].[SecurityGroupRuleId,FromPort,CidrIpv4]'   # the rule and its sgr- ID

    Fix

    Import the existing rule by its sgr- ID.

  2. The rule was added by hand in the console.

    Check

    terraform state list | grep ingress_rule   # not in state

    Fix

    Import it, or delete the hand-made rule and let Terraform create it.

Prevent it

Now learn it: break and fix it yourself in A plan that never settles, then take Level 12 · Drift. Open interactive

terraform: command not found

$ terraform init
bash: terraform: command not found

What it means

The shell looked in every directory on PATH for a program called terraform and found none. Nothing of Terraform ran.

Causes, most likely first

  1. Terraform was never installed on this machine or CI runner.

    Check

    command -v terraform   # no output: not on PATH
    ls /usr/local/bin/terraform ~/.tfenv/bin 2>/dev/null   # nothing in the usual places either

    Fix

    Install it from the HashiCorp package repository or the release zip into a PATH directory such as /usr/local/bin. In GitHub Actions add hashicorp/setup-terraform before any terraform command.

  2. It is installed, but in a folder that is not on PATH, or the terminal or CI step did not load the profile that sets PATH.

    Check

    echo "$PATH"   # the folder with the binary is not listed

    Fix

    Add that folder to PATH in your shell profile, open a new terminal, then run terraform version.

  3. tfenv is installed but no version is selected, so its shim has nothing to run.

    Check

    tfenv list   # no version, or none marked with *

    Fix

    tfenv install 1.9.8 && tfenv use 1.9.8 (or the version in .terraform-version).

  4. The team uses OpenTofu, whose binary is tofu.

    Check

    command -v tofu   # found: run tofu instead

    Fix

    Use tofu, or install the tool the README names.

Prevent it

Now learn it: break and fix it yourself in Forgot terraform init, then take Level 00 · init, plan, apply. Open interactive

No configuration files

Error: No configuration files

Plan requires configuration to be present. Planning without a configuration
would mark everything for destruction, which is normally not what is desired.

What it means

Terraform found no .tf files in the directory it ran in. With existing state, an empty configuration would plan to destroy everything, so it stops.

Causes, most likely first

  1. The command ran in the repository root instead of the module folder.

    Check

    pwd; ls *.tf   # no .tf files here
    git ls-files '*.tf' | head   # where they really are

    Fix

    cd into the folder with the .tf files, or run terraform -chdir=envs/prod plan.

  2. A CI step is missing working-directory or -chdir, so it runs at the checkout root.

    Check

    grep -n -B2 -A4 'terraform plan' .github/workflows/*.yml   # no working-directory on the step

    Fix

    Set working-directory on the step (or defaults.run.working-directory), or use -chdir.

  3. The files have another extension (.tf.txt, .hcl), so Terraform ignores them.

    Check

    ls   # main.tf.txt or main.hcl instead of main.tf

    Fix

    Rename them to .tf.

Prevent it

Now learn it: break and fix it yourself in Forgot terraform init, then take Level 00 · init, plan, apply. Open interactive

Failed to download module

Error: Failed to download module

Could not download module "network" (main.tf:1) source code from
"git::https://github.com/example/terraform-aws-network.git?ref=v9.9.9": error downloading ...

What it means

terraform init tried to fetch a module from the address in its source argument and failed. The text after the colon is the underlying git, HTTP or registry error, and it says which part was wrong.

Causes, most likely first

  1. The ref in ?ref= (a tag or branch) does not exist in that repository.

    Check

    git ls-remote --tags https://github.com/example/terraform-aws-network.git | grep v9.9.9   # no output: no such tag

    Fix

    Point ?ref= at a tag that exists, then terraform init.

  2. The repository is private and this machine or runner has no SSH key or token for it. Git reports this as not found or authentication failed.

    Check

    git ls-remote https://github.com/example/terraform-aws-network.git >/dev/null && echo reachable   # fails here, works on your laptop: credentials

    Fix

    Give the runner read access with a deploy key, or a token through a git credential helper. Never put a token in the source URL.

  3. A typo in the source address, or no network route to the Git host or registry from CI.

    Check

    grep -rn 'source *=' *.tf   # compare with the repository's real URL
    terraform init   # read the full git error under the message

    Fix

    Fix the address, or the runner's network access, then terraform init again.

Prevent it

Now learn it: break and fix it yourself in Unpinned module upgraded itself, then take Level 09 · Modules. Open interactive

Security token expired or invalid

Error: validating provider credentials: retrieving caller identity from STS: operation error STS:
GetCallerIdentity, https response error StatusCode: 403, api error ExpiredToken: The security token
included in the request is expired

What it means

The provider found credentials, but AWS STS (the service that checks who you are) rejected them. ExpiredToken means temporary credentials ran out; InvalidClientTokenId means the key is unknown to this account or region.

Causes, most likely first

  1. The SSO session or assumed-role credentials expired, often during a long apply.

    Check

    aws sts get-caller-identity   # fails with the same ExpiredToken

    Fix

    aws sso login --profile <profile>, then run again. In CI set role-duration-seconds longer than the apply takes (up to the role's maximum session duration; a chained role is capped at 1 hour).

  2. Old AWS_ACCESS_KEY_ID and AWS_SESSION_TOKEN are exported in the shell. Environment keys come before AWS_PROFILE in the chain, so the stale ones win.

    Check

    env | grep -o '^AWS_[A-Z_]*'   # names only: AWS_SESSION_TOKEN present though you use a profile
    aws configure list   # access_key type env, not shared-credentials-file

    Fix

    unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN, then run again.

  3. The access key was deactivated or deleted (InvalidClientTokenId), or the call went to an opt-in region the account has not enabled, which answers the same.

    Check

    aws configure list   # the last four characters of the key in use
    aws account list-regions --region-opt-status-contains DISABLED --query 'Regions[].RegionName'   # the provider's region is listed

    Fix

    Use a valid key or, better, short-lived role credentials; or enable the region, or use another one.

Prevent it

Now learn it: break and fix it yourself in No valid credential sources found, then take Level 15 · Secure by default. Open interactive

AccessDenied / UnauthorizedOperation (403)

Error: creating EC2 Instance: operation error EC2: RunInstances, https response error StatusCode: 403,
api error UnauthorizedOperation: You are not authorized to perform this operation. User:
arn:aws:iam::111122223333:user/ci is not authorized to perform: ec2:RunInstances

What it means

AWS knows who you are but no policy allows this action on this resource. The message names the identity (User: arn:...) and the action that was refused.

Causes, most likely first

  1. Terraform runs as a different identity than you expect: another profile, or the CI role.

    Check

    aws sts get-caller-identity   # compare with the ARN in the error

    Fix

    Run with the intended profile or role.

  2. The role lacks the action (ec2:RunInstances, s3:CreateBucket, iam:PassRole ...).

    Check

    aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names ec2:RunInstances --query 'EvaluationResults[].EvalDecision'   # implicitDeny

    Fix

    Grant the missing action to that role, scoped to the resources it manages. Not a wildcard, not admin.

  3. An explicit Deny: a service control policy, a permissions boundary, a resource policy, or a condition such as required tags or region.

    Check

    aws sts decode-authorization-message --encoded-message <message> --query DecodedMessage --output text   # when the error includes an encoded message: shows the Deny and its source

    Fix

    Meet the condition (add the tags, use the allowed region), or change the SCP or boundary through its owners.

  4. A KMS key policy does not let the role use the key.

    Check

    # the error mentions kms:Decrypt, kms:GenerateDataKey or the key ARN

    Fix

    Add the role to the key policy for the actions it needs.

Prevent it

Now learn it: break and fix it yourself in No valid credential sources found, then take Level 15 · Secure by default. Open interactive

Duplicate resource configuration

Error: Duplicate resource "aws_s3_bucket" configuration

A aws_s3_bucket resource named "logs" was already declared at main.tf:30,1-34. Resource names
must be unique per type in each module.

What it means

Two resource blocks in one module have the same type and name. Terraform reads every .tf file in a folder as one configuration, so they clash even in different files.

Causes, most likely first

  1. A block was copied into a new file without renaming it, or a merge kept both versions.

    Check

    grep -rn 'resource "aws_s3_bucket" "logs"' *.tf   # two hits

    Fix

    Rename one block, and add a moved block if the renamed one is already in state.

  2. A backup copy such as main_old.tf sits in the folder and is read too.

    Check

    ls *.tf   # main_old.tf or main.tf.bak.tf

    Fix

    Delete the stray file.

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, then take Level 01 · Speaking HCL. Open interactive

Call to unknown function

Error: Call to unknown function

There is no function named "lenght".

What it means

The name is not a Terraform built-in function. Terraform has no user-defined functions, so a typo or a name from another language fails here.

Causes, most likely first

  1. A typo (lenght, tostr, lookUp) or a name from another language (len, str).

    Check

    terraform console   # try the expression with the right name

    Fix

    Use the real name: length(), tostring(), lookup().

  2. A function newer than this Terraform, or a provider-defined function (provider::aws::arn_parse) on Terraform older than 1.8 or without that provider in required_providers.

    Check

    terraform version   # older than the docs you read

    Fix

    Upgrade Terraform (and declare the provider), or use an older equivalent.

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, then take Level 01 · Speaking HCL. Open interactive

Argument or block definition required

Error: Argument or block definition required

An argument or block definition is required here.

What it means

Each line in a block body must start an argument (name = value) or a nested block. The line shown starts with something else.

Causes, most likely first

  1. A quoted key written straight into a resource, such as "Name" = "web", instead of inside a map such as tags = { }.

    Check

    terraform validate   # the line starts with a quote

    Fix

    Move it into the map: tags = { "Name" = "web" }.

  2. A merge conflict marker (<<<<<<<, =======) was left in the file.

    Check

    grep -n '^<<<<<<<\|^=======\|^>>>>>>>' *.tf   # any hit is a leftover conflict marker

    Fix

    Resolve the conflict and remove the markers.

  3. A stray character at the start of a line, often from text pasted from JSON or YAML.

    Check

    terraform fmt -check -diff   # the odd line stands out

    Fix

    Remove the stray characters.

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, then take Level 01 · Speaking HCL. Open interactive

Unclosed configuration block

Error: Unclosed configuration block

There is no closing brace for this block before the end of the file. This may be caused by
incorrect brace nesting elsewhere in this file.

What it means

A { has no matching }. The line in the error is where the open block starts; the missing brace can be anywhere after it.

Causes, most likely first

  1. A closing brace was deleted while editing, or a nested block (ingress, tags, dynamic) was closed one level too early or too late.

    Check

    terraform fmt -check -diff   # indentation shows where nesting broke

    Fix

    Add the missing } at the end of the block named in the message, then terraform fmt.

  2. An unterminated string or heredoc swallowed the closing brace.

    Check

    grep -n '<<' main.tf   # a heredoc whose end marker is missing or indented wrongly

    Fix

    Close the string or heredoc.

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, then take Level 01 · Speaking HCL. Open interactive

Invalid expression

Error: Invalid expression

Expected the start of an expression, but found an invalid expression token.

What it means

Terraform expected a value after = (or inside brackets) and found nothing it can read.

Causes, most likely first

  1. An argument with nothing after =, or an unfinished edit on the line shown.

    Check

    terraform validate   # the file and line

    Fix

    Give the argument a value, or remove the line.

  2. A trailing operator, or a stray comma or bracket inside a list or map.

    Check

    terraform fmt -check -diff   # fmt also fails at the same spot

    Fix

    Finish or remove the half-written expression.

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, then take Level 01 · Speaking HCL. Open interactive

Incorrect attribute value type

Error: Incorrect attribute value type

Inappropriate value for attribute "vpc_security_group_ids": set of string required.

What it means

The value has the wrong type for the argument: a string where a list or set is needed, a list where a map is needed, and so on. The message ends with the type the argument requires.

Causes, most likely first

  1. A single value passed to an argument that takes a list or set: vpc_security_group_ids = aws_security_group.app.id.

    Check

    terraform console   # type(aws_security_group.app.id) is string

    Fix

    Wrap it in brackets: [aws_security_group.app.id].

  2. A variable declared with the wrong type, or a module output that is a list used where one value is expected.

    Check

    grep -n -A3 'variable "' variables.tf   # its type
    terraform providers schema -json | jq '.provider_schemas[].resource_schemas.aws_instance.block.attributes.vpc_security_group_ids.type'   # the type required

    Fix

    Fix the declared type, or convert with one(), [0], tolist(), toset() or tomap().

Prevent it

Now learn it: break and fix it yourself in Unsupported argument, then take Level 01 · Speaking HCL. Open interactive

Invalid index

Error: Invalid index

The given key does not identify an element in this collection value: the given index is greater
than or equal to the length of the collection.

What it means

The code asks for an element that is not there: an index past the end of a list, or a map key that does not exist.

Causes, most likely first

  1. A fixed index such as [2] on a list that has fewer elements in this environment.

    Check

    terraform console   # length(aws_subnet.private) is 2

    Fix

    Loop over the collection (for_each or a for expression) instead of indexing a fixed position. Avoid element(): it wraps around and silently reuses another element.

  2. A map key that differs in case or spelling.

    Check

    terraform console   # keys(var.amis)

    Fix

    Use the real key; where it is truly optional, lookup(map, key, default) or try(...).

  3. count or for_each produced fewer instances than the code assumes.

    Check

    terraform state list | grep <name>   # fewer instances than the index

    Fix

    Key resources with for_each and refer to them by key.

Prevent it

Now learn it: break and fix it yourself in Missing resource instance key, then take Level 07 · Many of a kind. Open interactive

Value for undeclared variable

Warning: Value for undeclared variable

The root module does not declare a variable named "regoin" but a value was found in file
"terraform.tfvars".

What it means

A value was given for a variable that no variable block declares. From a .tfvars file it is a warning and the value is ignored; from -var on the command line it is an error.

Causes, most likely first

  1. A typo in the name in terraform.tfvars or -var (regoin for region).

    Check

    grep -rn 'variable "' *.tf   # the declared names
    grep -n regoin *.tfvars   # the misspelled one

    Fix

    Correct the name.

  2. The variable was renamed or removed, and the tfvars were not updated.

    Check

    git log -S 'variable "regoin"' --oneline   # when it changed

    Fix

    Update the tfvars, or add the variable back if it is still needed.

  3. Values meant for a child module were given to the root module.

    Check

    grep -rn 'variable "<name>"' modules/   # declared in a module, not the root

    Fix

    Declare it in the root and pass it in the module block.

Prevent it

Now learn it: break and fix it yourself in tfvars precedence, then take Level 02 · Inputs and outputs. Open interactive

State data in S3 does not have the expected content

Error: Error refreshing state: state data in S3 does not have the expected content.

This may be caused by unusually long delays in S3 processing a previous state update.

What it means

With dynamodb_table set, the S3 backend keeps an MD5 digest of the state in the DynamoDB table (item <bucket>/<key>-md5). The state object in S3 no longer matches it. With use_lockfile alone there is no digest and no such check.

Causes, most likely first

  1. The state was edited, uploaded or restored by hand in S3, so the digest was not updated.

    Check

    aws s3api list-object-versions --bucket <bucket> --prefix <key> --max-items 5   # who changed it, and when
    aws dynamodb get-item --table-name <table> --key '{"LockID":{"S":"<bucket>/<key>-md5"}}'   # the stored digest

    Fix

    If the state in S3 is the right one, set the Digest item to the value the error prints (or delete the -md5 item). If not, restore the right object version first.

  2. Some runs write the state without dynamodb_table (use_lockfile only, or an older checkout), so the digest goes stale.

    Check

    grep -rn -A6 'backend "s3"' --include='*.tf' .   # dynamodb_table in some configurations, not others

    Fix

    Give every checkout and pipeline the same backend settings, then fix the digest once.

  3. A crashed write, or rarely a short delay after a write.

    Check

    # the error goes away on a retry a minute later

    Fix

    Wait a minute and retry once.

Prevent it

Now learn it: break and fix it yourself in Restore an overwritten state, State lock stuck after a crashed CI job, then take Level 13 · Shared state. Open interactive

Provider produced inconsistent result

Error: Provider produced inconsistent final plan

When expanding the plan for aws_lb_listener.https to include new values learned so far during apply,
provider "registry.terraform.io/hashicorp/aws" produced an invalid new value for .certificate_arn

What it means

Terraform checks that a provider does what it said it would. During apply, the provider returned a value that differs from its own plan, which Terraform reports as a provider bug. The attribute at the end of the message (.certificate_arn here) is where it went wrong.

Causes, most likely first

  1. A known provider bug for this resource, often fixed in a later release.

    Check

    terraform version   # the provider version
    # search the provider's changelog and issues for the resource and attribute named in the error

    Fix

    Upgrade the provider within your constraint (terraform init -upgrade) and plan again. If it repeats, report it with a debug log (TF_LOG=DEBUG); the log can hold secrets, so redact it first.

  2. Something else changed the attribute while the apply ran, or a value known only after apply fed an argument the provider normalises (case, ordering).

    Check

    terraform plan   # often clean on the second run; read it before applying

    Fix

    Run plan and apply again, reading that plan first. Make the input stable (normalise case, sort lists) if it keeps happening.

Prevent it

Now learn it: break and fix it yourself in AWS provider major upgrade, then take Level 10 · Providers and versions. Open interactive

InvalidAMIID.NotFound

Error: creating EC2 Instance: operation error EC2: RunInstances, api error InvalidAMIID.NotFound:
The image id '[ami-0123456789abcdef0]' does not exist

What it means

AMI IDs are per region. The ID does not exist in the region the provider uses, was deregistered, or is not shared with this account.

Causes, most likely first

  1. A hard-coded AMI ID copied from another region.

    Check

    aws ec2 describe-images --image-ids ami-0123456789abcdef0 --region eu-west-1   # NotFound here, found in another region

    Fix

    Look the image up with a data "aws_ami" block (owners and filters) or an SSM parameter.

  2. The image was deregistered, or it is private and not shared with this account.

    Check

    aws ec2 describe-images --owners <owner-account> --filters Name=name,Values=<name>* --query 'Images[].ImageId'   # a newer ID, or nothing you can see

    Fix

    Use the current image ID, or ask the owner to share it with this account.

Prevent it

Now learn it: break and fix it yourself in Data source not found, then take Level 08 · Reading what exists. Open interactive

VcpuLimitExceeded

Error: creating EC2 Instance: operation error EC2: RunInstances, api error VcpuLimitExceeded: You have
requested more vCPU capacity than your current vCPU limit of 32 allows

What it means

The account's EC2 vCPU quota for this group of instance families in this region is used up. Nothing is wrong with the configuration. L-1216C47A covers the standard On-Demand families (A, C, D, H, I, M, R, T, Z); G, P, X and others have their own quotas.

Causes, most likely first

  1. A new account starts with low default quotas.

    Check

    aws service-quotas get-service-quota --service-code ec2 --quota-code L-1216C47A --query Quota.Value   # a small number

    Fix

    Request a quota increase in Service Quotas.

  2. Too many instances already run in the region, or this change asks for a larger type or count than before.

    Check

    aws ec2 describe-instances --filters Name=instance-state-name,Values=running --query 'length(Reservations[].Instances[])'   # how many run now

    Fix

    Stop unused instances, or use a smaller type or count.

Prevent it

Now learn it: break and fix it yourself in API rate limit during apply, then take Level 05 · Reading a plan. Open interactive