OpenTofu Footguns¶
1. Running tofu apply with Local State in a Team¶
You start using OpenTofu with default local state (terraform.tfstate). A colleague applies from their machine, producing a different local state. Both files diverge silently, and the next apply tries to create already-existing resources — or worse, recreates them.
Fix: Configure remote state (S3+DynamoDB, GCS, or OpenTofu Cloud) from day one. Add terraform.tfstate* to .gitignore immediately. Never commit state files.
2. tofu destroy on the Wrong Workspace¶
You meant to destroy the dev environment but forgot to check which workspace was active. tofu destroy -auto-approve wipes production before you notice. OpenTofu does not prompt you which workspace you're on.
Fix: Always run tofu workspace show before any destructive operation. Add workspace_name to your prompt or alias tofu to print the workspace first. Require explicit -target or workspace selection in automation.
One-liner: Add this to your shell profile to always show the active workspace:
alias tofu='echo "Workspace: $(tofu workspace show 2>/dev/null)" && command tofu'. Or addTF_WORKSPACEto your PS1 prompt so it is always visible.
3. Forgetting -target Leaves the State Inconsistent¶
You use -target=module.vpc to apply a partial change quickly. Tofu only updates that resource's state. Now your state and config are mismatched — later full applies may recreate resources that already exist, or try to modify dependencies that were created out-of-order.
Fix: Use -target only as a temporary escape hatch, never as normal workflow. Always follow up with a full tofu plan to verify the state is consistent. Document the partial apply in commit messages.
4. Provider Version Left Unpinned Breaks Production¶
Your required_providers block uses version = ">= 3.0". A new provider major version is released with breaking changes. The next tofu init on a fresh CI runner downloads the new version and the apply fails — or succeeds with unexpected behavior.
Fix: Pin to a minor version constraint: version = "~> 5.20". Commit .terraform.lock.hcl to version control so all environments use identical provider versions. Upgrade providers deliberately using tofu init -upgrade after reviewing changelogs.
5. Importing a Resource Without Updating Config First¶
You run tofu import aws_instance.web i-0abc123 to bring an existing instance under management. The import succeeds, but your config doesn't match the actual resource attributes. The next tofu plan shows massive diffs — it wants to recreate or significantly modify the just-imported resource.
Fix: Always write (or generate) the matching config before importing. Run tofu plan immediately after import and resolve all diffs before applying. Use tofu show after import to see all the resource attributes you need to represent in HCL.
6. Storing Secrets in State (and Shipping State to S3 Unencrypted)¶
You have a resource that outputs a database password. OpenTofu stores it in the state file in plaintext. Your S3 backend bucket has no server-side encryption. Anyone with S3 read access sees all secrets.
Fix: Enable S3 bucket encryption (server_side_encryption_configuration). Use OpenTofu's native state encryption feature (not available in Terraform). Mark sensitive outputs with sensitive = true. Use a secrets manager (Vault, AWS Secrets Manager) instead of output values for credentials.
Gotcha: OpenTofu's state encryption (a key differentiator from Terraform) encrypts the entire state file at rest. But
sensitive = trueonly hides values in CLI output — the value is still plaintext in unencrypted state. You need both:sensitive = truefor CLI output AND state encryption for the file itself.
7. Using count Instead of for_each for Resource Collections¶
You create multiple subnets using count = length(var.subnet_cidrs). Later you remove an element from the middle of the list. Tofu renumbers all subsequent resources (index shift), forcing unnecessary recreation of subnets that didn't actually change.
Fix: Use for_each with a map or set instead of count for any resource collection where items might be added or removed. for_each uses stable keys rather than list indices, so removing one item doesn't affect others.
Under the hood: With
count, resources are addressed asaws_subnet.main[0],aws_subnet.main[1]. Remove item 0 and everything shifts: old[1]becomes new[0], triggering destroy+recreate. Withfor_each, resources are addressed by key:aws_subnet.main["us-east-1a"]. Removing one key has zero effect on others.
8. Running tofu init Without Reading the Lock File Into CI¶
Your local .terraform.lock.hcl pins specific provider versions and hashes. In CI you run tofu init but don't commit the lock file. CI downloads whatever version satisfies constraints — possibly different from local. Behavior differs between environments.
Fix: Commit .terraform.lock.hcl to version control. In CI, run tofu init without -upgrade so it uses the locked versions. To update providers, run tofu init -upgrade locally, review the diff, and commit the updated lock file.
9. Mixing OpenTofu and Terraform State on the Same Backend Path¶
A team switches some machines to tofu while others still use terraform. Both tools write to the same S3 key. State file format is compatible, but behavioral differences (encryption features, registry references) cause unexpected plan outputs or provider download failures.
Fix: Complete the migration atomically — update all CI pipelines and developer machines together. Use a wrapper script that enforces the correct binary. Pin the OpenTofu version in a .opentofu-version file (similar to .terraform-version for tfenv).