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
-
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 itFix
terraform init, then commit the changed .terraform.lock.hcl.
-
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.80Fix
terraform init -upgrade, review the lock file diff, and commit it in the same pull request as the constraint change.
-
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 ranFix
Run init with the tool the repository uses, and use that one tool everywhere, CI included.
Prevent it
- Commit .terraform.lock.hcl
- Change constraints and run init -upgrade in the same pull request
- Use one tool, terraform or tofu, per repository and in CI
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
-
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 installedFix
terraform init. In CI, make it the first step of every job.
-
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 installedFix
terraform init (no -upgrade needed: it installs what the lock file says).
-
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 .terraformFix
Unset TF_DATA_DIR, or run init with the same value the later steps use.
Prevent it
- Run terraform init as the first step of every CI job
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
-
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 sideFix
Pin the module to a release that supports your provider major, or move the root constraint in a reviewed change.
-
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 versionFix
terraform init -upgrade, then commit the lock file.
-
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 sourceFix
Declare the real source (for example datadog/datadog) in every module that uses it.
-
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 routeFix
Fix the proxy settings, or add the provider to the mirror.
Prevent it
- Pin module versions
- Upgrade provider majors deliberately, one pull request each
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
-
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_amd64Fix
terraform providers lock -platform=darwin_arm64 -platform=linux_amd64 -platform=windows_amd64 (it asks the origin registry by default), review the diff, commit.
-
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 changedFix
Stop and check the mirror's copy against the registry before trusting it. Do not just regenerate the lock file.
-
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 versionsFix
Take the lock file from main and run terraform providers lock again for every platform.
Prevent it
- Run providers lock for every platform whenever a provider version changes
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
-
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/modulesFix
Install a matching CLI (tfenv install and tfenv use), or widen the constraint in a reviewed change and upgrade CI with it.
-
A module requires a newer CLI than you have. Its constraint applies too.
Check
grep -rn required_version .terraform/modules # the module's constraintFix
Upgrade the CLI, or pin the module to a release that supports your version.
-
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 ranFix
Use the tool the repository is written for, or keep tofu-specific constraints in .tofu files (OpenTofu 1.8+).
Prevent it
- Pin to a tested range (~> 1.10) and keep CI and laptops on the same version (.terraform-version)
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
-
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 providerFix
Pull the newer lock file and run terraform init, or terraform init -upgrade within your constraints.
-
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 upgradeFix
Stay on the newer provider, or restore the state version written before the upgrade (S3 versioning). Never edit schema_version by hand.
-
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 stateFix
Upgrade the CLI to that version or later.
Prevent it
- Commit .terraform.lock.hcl and upgrade providers in one reviewed pull request that CI applies
- Pin the CLI too (.terraform-version, terraform_version in setup-terraform) so people and CI match
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
-
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 sourceFix
terraform init.
-
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 thereFix
terraform init as the first step of every job.
Prevent it
- Run init after pulling changes that touch module blocks
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
-
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 forFix
terraform init, which downloads a version that matches.
-
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 sourceFix
Add an explicit version to the module block, then terraform init.
Prevent it
- Always set version on registry modules
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
-
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 valuesFix
export AWS_PROFILE=<profile>; for SSO profiles run aws sso login --profile <profile> first.
-
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: writeFix
Add the step with role-to-assume and grant permissions: id-token: write to the job.
-
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 limitFix
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.
-
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 hereFix
Make the provider block match the environment, or drop the hard-coded profile and set AWS_PROFILE per environment.
Prevent it
- Never put access_key or secret_key in provider blocks
- Start each CI job with aws sts get-caller-identity so a missing role fails first and clearly
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
-
A typo in the argument name.
Check
terraform validate # the same error with a "Did you mean" suggestionFix
Use the suggested name.
-
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 versionFix
Upgrade the provider (raise the constraint, terraform init -upgrade) in a reviewed change.
-
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 changedFix
Follow the provider's upgrade guide for the new name.
-
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 thereFix
Pin the module version the code was written for, or use its new input name.
Prevent it
- terraform validate in CI and in your editor
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
-
A typo in the block name.
Check
terraform validate # "Did you mean" names the real blockFix
Use the suggested name.
-
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" }.
-
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 blockFix
Upgrade the CLI or provider, or use the command-line equivalent (terraform import).
Prevent it
- terraform fmt and validate before pushing
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
-
The argument was forgotten or deleted.
Check
terraform validate # names the argument and the blockFix
Set the argument.
-
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 defaultFix
Pass the new input, after reading the module's changelog.
Prevent it
- Read module changelogs before upgrading
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
-
A variable or local written without its prefix, such as env instead of var.env.
Check
terraform validate # the line and the bare wordFix
Use var.<name>, local.<name>, module.<name>.<output>, or <type>.<name>.<attr>.
-
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
- Editor support for HCL highlights undeclared names
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
-
The resource was renamed and some references were not updated.
Check
grep -rn 'resource "aws_security_group"' . # the real nameFix
Fix the reference. If the rename is new, add a moved block so state follows.
-
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 hereFix
Expose it as an output of that module and use module.<name>.<output>.
Prevent it
- Rename with moved blocks and update references in the same change
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
-
The variable block is missing or misspelled.
Check
grep -rn 'variable "' *.tf # no block with that name hereFix
Add variable "<name>" {} with a type and description.
-
The variable is declared in the root, but used inside a child module.
Check
pwd; grep -rn 'variable "region"' .. # declared one level upFix
Declare it in the child module too and pass it in the module block.
Prevent it
- Keep variables.tf next to the code that uses them
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
-
The module does not declare that output.
Check
grep -n 'output "' modules/<name>/outputs.tf # no output with that nameFix
Add the output to the module, or use one that exists.
-
A typo in a resource's exported attribute.
Check
terraform console # then: keys(aws_instance.web) lists real attributesFix
Use the attribute name the console shows.
-
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 mapFix
Index it ([0], ["key"]) or use a splat ([*].subnet_id).
Prevent it
- Document module outputs
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
-
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 errorFix
Use one(aws_instance.bastion[*].public_ip), which gives null when count is 0.
-
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
- Use one() for optional single resources
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
-
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 otherFix
Create both groups without inline rules, then link them with aws_vpc_security_group_ingress_rule and egress_rule resources.
-
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 edgesFix
Remove the depends_on; references already order things.
Prevent it
- Prefer standalone rule resources to inline blocks
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
-
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 lineFix
Key by names written in code (a map of name to config) and keep IDs in the values.
-
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 tupleFix
Wrap it in toset().
Prevent it
- Plan from an empty workspace in CI to catch it early
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
-
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 lineFix
Derive count from variables or locals written in code.
Prevent it
- Keep counts static
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
-
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 itFix
Put the provider block back until those objects are destroyed or moved, then remove it.
-
The alias is misspelled.
Check
grep -rn alias *.tf # us-east-1 vs us_east_1Fix
Declare provider "aws" { alias = "us_east_1" region = "us-east-1" } with the exact name.
-
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 = { ... } lineFix
Add providers = { aws.us_east_1 = aws.us_east_1 } to the module block.
Prevent it
- Remove aliases only after the resources using them are gone
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
-
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 fileFix
Set TF_VAR_<name> in the workflow (from a secret store for secrets), or commit a non-secret env.tfvars and pass -var-file.
-
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 secretsFix
Export TF_VAR_<name> with the exact variable name.
-
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 hereFix
Run in the right folder, or pass -var-file with its path.
Prevent it
- Run CI exactly as people do: same files, -input=false
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
-
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 ruleFix
Pass a valid value.
-
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 onlyFix
Remove the stray source, so one place sets each value.
Prevent it
- Keep validation rules; they catch exactly this
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
-
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 fromFix
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
- Do not remove sensitive from variables to silence 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
-
The output was added after the last apply.
Check
terraform plan # "Changes to Outputs" lists itFix
terraform apply (or apply -refresh-only, which saves outputs without changing resources).
-
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 stateFix
terraform workspace select <name>, or init with the right backend.
Prevent it
- Apply after adding outputs
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
-
Hand-indented code or misaligned = signs.
Check
terraform fmt -check -diff -recursive # the exact changes it wantsFix
terraform fmt -recursive and commit.
-
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 workflowFix
Pin the same version in .terraform-version and setup-terraform.
Prevent it
- Format on save, or a pre-commit hook
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
-
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 WhoFix
Wait for it to finish. Use -lock-timeout=5m in CI so jobs wait instead of failing.
-
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 existsFix
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.
-
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 placesFix
Give each configuration its own key, and move state with init -migrate-state.
-
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 tableFix
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
- Let CI jobs finish or time out cleanly, and run one apply per state at a time (a CI concurrency group)
- Use -lock-timeout=5m in CI rather than -lock=false
- Prefer use_lockfile on Terraform 1.11+ or OpenTofu 1.10+; Terraform deprecates dynamodb_table from 1.11
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
-
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 initialisedFix
terraform init.
-
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 credentialsFix
terraform init -migrate-state to copy existing state to the new place; -reconfigure only when no state should move.
-
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 pathFix
Use one data directory per configuration, or run init in each job.
Prevent it
- Change backends in a dedicated change with a migration plan
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
-
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 valuesFix
terraform init -migrate-state, then check terraform state list shows everything.
-
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 unchangedFix
terraform init -reconfigure.
-
A different -backend-config file than last time (another environment).
Check
history | grep backend-config | tail -3 # the file used lastFix
Use a separate checkout or data directory per environment, and pass the right file.
Prevent it
- Review backend diffs carefully
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
-
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 elsewhereFix
Fix the name, then terraform init.
-
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.
-
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 similarFix
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
- Never create a new bucket to make the error go away: you would start from empty state
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
-
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 plannedFix
Plan again with -out, review it, and apply that file.
-
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 jobsFix
Serialise applies per state (a concurrency group) and plan close to apply.
Prevent it
- Plan and apply in one pipeline run
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
-
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-0a1b2c3dFix
Use the documented ID format.
-
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'sFix
Import with the provider alias or profile for the right account and region.
Prevent it
- Prefer import blocks, reviewed in a plan
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
-
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 IDFix
Nothing to do; remove the duplicate import.
-
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 IDFix
Import into the right address. Use terraform state rm only if state is truly wrong.
Prevent it
- Check terraform state list before importing
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
-
A typo, or the workspace was never created.
Check
terraform workspace list # the names that existFix
Select an existing one, or create it on purpose with terraform workspace new.
-
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 listFix
Fix or unset TF_WORKSPACE, or create the workspace in a deliberate step.
-
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
- Show the workspace in your prompt
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
-
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 valueFix
Revert the argument. If the change is needed, plan a migration (snapshot, then create_before_destroy where the name allows it).
-
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 sourceFix
Remove the depends_on and pass real references instead.
Prevent it
- lifecycle { prevent_destroy = true } and deletion_protection on stateful resources
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
-
A change forces replacement, which includes a destroy.
Check
terraform plan | grep -B3 'forces replacement' # the argument that triggered itFix
Undo that change.
-
terraform destroy, or plan -destroy, includes the protected resource.
Check
# the command you ran was destroy or plan -destroyFix
For a deliberate, reviewed teardown only: remove prevent_destroy, apply, then destroy.
Prevent it
- Keep prevent_destroy on data stores, with deletion_protection on the AWS side, which still holds when the block is deleted
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
-
Someone edited the object in the console or CLI.
Check
terraform plan -refresh-only # exactly what changed outside TerraformFix
Unwanted change: apply to put it back. Wanted change: update the code to match.
-
Another system owns that attribute, such as an autoscaler setting desired_capacity.
Check
terraform plan -refresh-only # the same attribute changes every timeFix
lifecycle { ignore_changes = [desired_capacity] }, so only one owner manages it.
Prevent it
- Read-only console access; one owner per attribute
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
-
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 endFix
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
- for_each for anything with an identity
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
-
The resource was renamed.
Check
terraform plan # one destroy and one create of the same typeFix
Add moved { from = aws_instance.web to = aws_instance.app } and plan again: it should show no changes.
-
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
- Add moved blocks in the same pull request as the rename
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
-
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.
-
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 resourceFix
Reference a stable value instead, such as a name written in code.
Prevent it
- Use depends_on only for hidden dependencies
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
-
-target was used to ship one change quickly.
Check
terraform plan # without -target, to see what else is pendingFix
Plan and apply without -target. Keep -target for recovery from a broken apply.
Prevent it
- Reserve -target for recovery
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
-
Terraform is replacing a group that instances still use.
Check
terraform plan | grep -B2 'aws_security_group' # -/+ on the groupFix
lifecycle { create_before_destroy = true } with name_prefix, so the new group exists before the old one goes.
-
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 itFix
Remove those users first. ENIs that Lambda created are released by AWS some time after the function stops using them.
-
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 itFix
Change or remove those references, in their own configuration.
Prevent it
- Use name_prefix and create_before_destroy for security groups
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
-
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_prefixFix
Use name_prefix instead of name.
-
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 itFix
Import it if this configuration should own it, or choose another name.
Prevent it
- name_prefix for replaceable resources
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
-
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 itFix
Import it with an import block, or fix the state (restore or migrate) if it should already be there.
-
BucketAlreadyExists: someone else owns the name.
Check
aws s3api head-bucket --bucket nimbus-assets-2026 # 403: exists, not yoursFix
Choose another name, for example with the account ID or a random suffix.
Prevent it
- Import blocks for existing objects; keep state safe
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
-
The bucket holds data, and the plan removes or destroys it.
Check
aws s3 ls s3://<bucket> --recursive --summarize | tail -2 # Total Objects above 0Fix
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.
-
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 listedFix
Decide as above; emptying a versioned bucket means deleting every version.
Prevent it
- Avoid force_destroy on buckets that hold data
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
-
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' # trueFix
For a deliberate teardown: set deletion_protection = false, apply, then destroy. Never terraform state rm it to get around the protection.
Prevent it
- Keep it on in production; never state rm to get around 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
-
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> # BucketOwnerEnforcedFix
Remove the ACL. Grant access with a bucket policy, CloudFront, or presigned URLs, and keep a public access block.
Prevent it
- Public access block on every bucket
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
-
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).
-
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 ISSUEDFix
Reference aws_acm_certificate_validation.<name>.certificate_arn, so the distribution waits for validation.
Prevent it
- One aliased provider per extra region, passed explicitly
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
-
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 nothingFix
Match the real tags, and narrow filters until exactly one object matches.
-
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 regionFix
Run with the right profile, or use the provider alias for that region.
Prevent it
- Tag consistently from code
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
-
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 = 1Fix
Remove max_retries (the default retries with backoff) or raise it.
-
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 stateFix
terraform apply -parallelism=3 for very large changes, and split very large states.
-
Other tools are using the same account's API quota at the same time.
Check
# CloudTrail or the CI history shows other jobs running thenFix
Spread scheduled pipelines over time, then run apply again: completed resources are already in state.
Prevent it
- Keep provider retries at their default
- Split very large states
- Spread scheduled pipelines over time
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
-
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 # taintedFix
If it is healthy now: terraform untaint aws_db_instance.warehouse, and add timeouts { create = "120m" } for next time.
-
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 reasonFix
Let the next apply replace it after fixing the cause, reading that plan first.
Prevent it
- Set timeouts on slow resources
- Read plans after a failed apply before applying again
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
-
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' # availableFix
terraform untaint aws_db_instance.warehouse, then plan: it should not replace it.
-
A provisioner failed, or the object really is broken.
Check
# the earlier apply's output shows the provisioner or create errorFix
Let the replacement happen, ideally with terraform apply -replace=<addr> so it is explicit.
-
Someone ran terraform taint.
Check
# the CI or shell history shows terraform taintFix
If it was a mistake: terraform untaint <addr>.
Prevent it
- Timeouts that fit the resource
- Avoid provisioners
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
-
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 expectFix
Select the right workspace or init with the right backend. Do not apply.
-
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 thereFix
terraform init -migrate-state from the old backend.
-
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 versionFix
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
- Versioning on the state bucket
- Never apply a plan that creates things you know exist
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
-
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 serialFix
After checking the file is the one you want, and keeping a backup of the current state: terraform state push -force <file>.
-
The file comes from another configuration or workspace.
Check
jq '{serial, lineage}' <file> # a different lineageFix
Find the right file. Do not force it.
Prevent it
- Use state push only for recovery, with a backup of the current state
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
-
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 guideFix
Move each one to the replacement resource or argument, and check the plan shows no replacement of the bucket.
Prevent it
- Fix deprecations before each provider major upgrade
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
-
A code change broke a convention the test encodes.
Check
terraform test -verbose # the planned values the assertion saw terraform console # evaluate the conditionFix
Fix the code.
-
The test's variables or expectations no longer match the module.
Check
git log -p -- tests/ # the test, not the code, changedFix
Change the test only when the rule itself changed, in a reviewed change.
Prevent it
- Run terraform test in CI on every pull request
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
-
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 ingressFix
Use one style per group: all inline, or all separate rule resources.
-
Another system owns the attribute, such as an autoscaler.
Check
terraform plan -refresh-only # the attribute changes between runsFix
ignore_changes for that attribute.
-
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 planFix
Remove the depends_on and pass real references.
Prevent it
- Agree on one style per resource type across teams
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
-
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- IDFix
Import the existing rule by its sgr- ID.
-
The rule was added by hand in the console.
Check
terraform state list | grep ingress_rule # not in stateFix
Import it, or delete the hand-made rule and let Terraform create it.
Prevent it
- Do not mix inline and standalone rules
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
-
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 eitherFix
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.
-
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 listedFix
Add that folder to PATH in your shell profile, open a new terminal, then run terraform version.
-
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).
-
The team uses OpenTofu, whose binary is tofu.
Check
command -v tofu # found: run tofu insteadFix
Use tofu, or install the tool the README names.
Prevent it
- Pin the version in .terraform-version and install it the same way on laptops and in CI
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
-
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 areFix
cd into the folder with the .tf files, or run terraform -chdir=envs/prod plan.
-
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 stepFix
Set working-directory on the step (or defaults.run.working-directory), or use -chdir.
-
The files have another extension (.tf.txt, .hcl), so Terraform ignores them.
Check
ls # main.tf.txt or main.hcl instead of main.tfFix
Rename them to .tf.
Prevent it
- Use -chdir in scripts and CI so the folder is explicit
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
-
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 tagFix
Point ?ref= at a tag that exists, then terraform init.
-
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: credentialsFix
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.
-
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 messageFix
Fix the address, or the runner's network access, then terraform init again.
Prevent it
- Pin modules to release tags and check them with git ls-remote in review
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
-
The SSO session or assumed-role credentials expired, often during a long apply.
Check
aws sts get-caller-identity # fails with the same ExpiredTokenFix
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).
-
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-fileFix
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN, then run again.
-
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 listedFix
Use a valid key or, better, short-lived role credentials; or enable the region, or use another one.
Prevent it
- Use short-lived role credentials, not long-lived keys
- Keep one way of passing credentials per environment
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
-
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 errorFix
Run with the intended profile or role.
-
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' # implicitDenyFix
Grant the missing action to that role, scoped to the resources it manages. Not a wildcard, not admin.
-
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 sourceFix
Meet the condition (add the tags, use the allowed region), or change the SCP or boundary through its owners.
-
A KMS key policy does not let the role use the key.
Check
# the error mentions kms:Decrypt, kms:GenerateDataKey or the key ARNFix
Add the role to the key policy for the actions it needs.
Prevent it
- Give the CI role a reviewed policy in code, and test new resource types in a sandbox account first
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
-
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 hitsFix
Rename one block, and add a moved block if the renamed one is already in state.
-
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.tfFix
Delete the stray file.
Prevent it
- Keep only live .tf files in a module folder
- Run terraform validate in CI
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
-
A typo (lenght, tostr, lookUp) or a name from another language (len, str).
Check
terraform console # try the expression with the right nameFix
Use the real name: length(), tostring(), lookup().
-
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 readFix
Upgrade Terraform (and declare the provider), or use an older equivalent.
Prevent it
- Run terraform validate before committing
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
-
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 quoteFix
Move it into the map: tags = { "Name" = "web" }.
-
A merge conflict marker (<<<<<<<, =======) was left in the file.
Check
grep -n '^<<<<<<<\|^=======\|^>>>>>>>' *.tf # any hit is a leftover conflict markerFix
Resolve the conflict and remove the markers.
-
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 outFix
Remove the stray characters.
Prevent it
- Run terraform fmt on save and terraform validate in CI
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
-
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 brokeFix
Add the missing } at the end of the block named in the message, then terraform fmt.
-
An unterminated string or heredoc swallowed the closing brace.
Check
grep -n '<<' main.tf # a heredoc whose end marker is missing or indented wronglyFix
Close the string or heredoc.
Prevent it
- Format on save so broken nesting shows up at once
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
-
An argument with nothing after =, or an unfinished edit on the line shown.
Check
terraform validate # the file and lineFix
Give the argument a value, or remove the line.
-
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 spotFix
Finish or remove the half-written expression.
Prevent it
- Run terraform validate before committing
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
-
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 stringFix
Wrap it in brackets: [aws_security_group.app.id].
-
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 requiredFix
Fix the declared type, or convert with one(), [0], tolist(), toset() or tomap().
Prevent it
- Declare a type on every variable and output you share between modules
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
-
A fixed index such as [2] on a list that has fewer elements in this environment.
Check
terraform console # length(aws_subnet.private) is 2Fix
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.
-
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(...).
-
count or for_each produced fewer instances than the code assumes.
Check
terraform state list | grep <name> # fewer instances than the indexFix
Key resources with for_each and refer to them by key.
Prevent it
- Do not hard-code positions; key resources with for_each and refer to them by key
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
-
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 oneFix
Correct the name.
-
The variable was renamed or removed, and the tfvars were not updated.
Check
git log -S 'variable "regoin"' --oneline # when it changedFix
Update the tfvars, or add the variable back if it is still needed.
-
Values meant for a child module were given to the root module.
Check
grep -rn 'variable "<name>"' modules/ # declared in a module, not the rootFix
Declare it in the root and pass it in the module block.
Prevent it
- Treat this warning as an error in review: the value you set is being ignored
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
-
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 digestFix
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.
-
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 othersFix
Give every checkout and pipeline the same backend settings, then fix the digest once.
-
A crashed write, or rarely a short delay after a write.
Check
# the error goes away on a retry a minute laterFix
Wait a minute and retry once.
Prevent it
- Change state only with terraform state commands
- Keep S3 versioning on for the state bucket
- During a move to use_lockfile, give every checkout and pipeline the same backend settings
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
-
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 errorFix
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.
-
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 applyingFix
Run plan and apply again, reading that plan first. Make the input stable (normalise case, sort lists) if it keeps happening.
Prevent it
- Keep providers reasonably current and pinned through the lock file
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
-
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 regionFix
Look the image up with a data "aws_ami" block (owners and filters) or an SSM parameter.
-
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 seeFix
Use the current image ID, or ask the owner to share it with this account.
Prevent it
- Never hard-code AMI IDs; resolve them per region
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
-
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 numberFix
Request a quota increase in Service Quotas.
-
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 nowFix
Stop unused instances, or use a smaller type or count.
Prevent it
- Check quotas before scaling, and alarm on quota usage
Now learn it: break and fix it yourself in API rate limit during apply, then take Level 05 · Reading a plan. Open interactive