OpenTofu - Street-Level Ops¶
Quick Diagnosis Commands¶
# Check version and confirm you're on OpenTofu, not Terraform
tofu version
# Initialize working directory (downloads providers + modules)
tofu init
# See what would change without applying
tofu plan
# Apply with auto-approval (use carefully in prod)
tofu apply -auto-approve
# Destroy all resources in state
tofu destroy
# List all resources in state
tofu state list
# Show details of a specific resource
tofu state show aws_instance.web
# Show full state as JSON
tofu show -json | jq .
# Validate config syntax and internal consistency
tofu validate
# Format all .tf files
tofu fmt -recursive
# Refresh state against real infrastructure
tofu apply -refresh-only
# Detect drift without touching anything
tofu plan -refresh-only -detailed-exitcode
echo $? # 0=no drift, 2=drift detected
# Workspace management
tofu workspace list
tofu workspace new staging
tofu workspace select production
tofu workspace show
# State manipulation
tofu state mv aws_instance.old aws_instance.new
tofu state rm aws_instance.decommissioned
tofu state pull > backup.tfstate
tofu state push backup.tfstate
# Import existing resource into state
tofu import aws_instance.web i-0123456789abcdef0
# Targeted operations (use sparingly)
tofu plan -target=module.vpc
tofu apply -target=aws_security_group.web
# Test modules (OpenTofu-native, not in Terraform)
tofu test
# Provider and module debugging
TF_LOG=DEBUG tofu plan 2>&1 | head -100
TF_LOG_PATH=/tmp/tofu.log tofu apply
# Show provider requirements
tofu providers
# Lock provider versions
tofu providers lock -platform=linux_amd64 -platform=darwin_arm64
# Upgrade providers within constraints
tofu init -upgrade
Common Scenarios¶
Scenario 1: State lock stuck after failed apply¶
Gotcha:
force-unlockdoes NOT roll back partial applies. If the run died mid-apply, some resources may exist in the cloud but not in state. Always runtofu planafter unlocking to detect orphaned resources.
A Terraform/OpenTofu run died mid-apply and left a state lock. New runs fail with "Error acquiring the state lock."
# Get the lock ID from the error message, then force-unlock
tofu force-unlock <LOCK_ID>
# If using S3 backend with DynamoDB, check the lock table
aws dynamodb scan --table-name terraform-locks \
--filter-expression "attribute_exists(LockID)" \
--query "Items[*].{LockID:LockID.S,Info:Info.S}"
# Delete a stuck DynamoDB lock manually (last resort)
aws dynamodb delete-item \
--table-name terraform-locks \
--key '{"LockID": {"S": "mybucket/path/to/terraform.tfstate"}}'
# Verify state is consistent before proceeding
tofu plan
Fix: Always use remote state with locking (S3+DynamoDB, GCS, Terraform Cloud). Never share local state files.
Scenario 2: Resource drift — real infra diverged from state¶
Someone changed a security group manually. Plan shows unexpected diffs.
# See what drifted
tofu plan -refresh-only
# If drift is intentional (manual fix you want to keep), update state to match reality
tofu apply -refresh-only
# If you want to restore IaC-defined state, just apply normally
tofu apply
# For a single resource, check state vs reality
tofu state show aws_security_group.web
aws ec2 describe-security-groups --group-ids sg-xxxxx
# Reconcile a resource that was re-created outside Tofu
tofu state rm aws_instance.web
tofu import aws_instance.web i-0newinstanceid
Scenario 3: Migrating from Terraform to OpenTofu¶
Remember: OpenTofu reads existing
.terraform.lock.hclandterraform.tfstatefiles directly. The state format is compatible. The main risk is providers that have moved to the BSL license -- check that all your providers are available in the OpenTofu registry before switching.
Existing Terraform codebase, need to switch to OpenTofu.
# 1. Install tofu (https://opentofu.org/docs/intro/install/)
brew install opentofu # or use tfenv equivalent
# 2. Run tofu init in existing Terraform directory
# OpenTofu reads existing .terraform.lock.hcl and terraform.tfstate
cd /path/to/terraform/project
tofu init
# 3. Verify plan matches expected (no surprise changes)
tofu plan
# 4. Re-lock providers for OpenTofu registry
tofu providers lock
# 5. Update CI to call tofu instead of terraform
# In GitHub Actions:
# - uses: opentofu/setup-opentofu@v1
# with:
# tofu_version: 1.7.0
# 6. Check for Terraform-only registry references
grep -r "registry.terraform.io" .
# OpenTofu uses registry.opentofu.org for OpenTofu-native providers
Scenario 4: Module testing with tofu test¶
OpenTofu has native test framework. Write tests in .tftest.hcl files.
# tests/vpc.tftest.hcl
run "creates_vpc" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR must be 10.0.0.0/16"
}
}
run "apply_and_verify" {
command = apply
assert {
condition = aws_vpc.main.enable_dns_hostnames == true
error_message = "DNS hostnames must be enabled"
}
}
# Run all tests
tofu test
# Run specific test file
tofu test -filter=tests/vpc.tftest.hcl
# Run with verbose output
tofu test -verbose
Key Patterns¶
Remote State Configuration (S3 Backend)¶
terraform {
backend "s3" {
bucket = "my-tofu-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# Partial backend config (pass secrets at init time, not in code)
tofu init \
-backend-config="access_key=..." \
-backend-config="secret_key=..."
Provider Authentication Patterns¶
# AWS — use environment variables, never hardcode
export AWS_PROFILE=myprofile
export AWS_REGION=us-east-1
# OR
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
# GCP — application default credentials
gcloud auth application-default login
# Check which credentials Tofu will use
tofu plan 2>&1 | grep -i "credential\|auth\|provider"
State Workspace Pattern for Environments¶
# Create per-environment workspaces
tofu workspace new dev
tofu workspace new staging
tofu workspace new prod
# Reference workspace in config
# In .tf files: terraform.workspace == "prod"
resource "aws_instance" "web" {
instance_type = terraform.workspace == "prod" ? "m5.large" : "t3.micro"
}
# Apply to specific environment
tofu workspace select prod && tofu plan
Provider Version Pinning¶
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
OpenTofu State Encryption (OpenTofu-only feature)¶
# Encrypt state at rest — not available in Terraform
terraform {
encryption {
key_provider "pbkdf2" "my_key" {
passphrase = var.state_passphrase
}
method "aes_gcm" "my_method" {
keys = key_provider.pbkdf2.my_key
}
state {
method = method.aes_gcm.my_method
}
}
}
Handling Sensitive Outputs¶
Default trap:
tofu output -jsonoutputs ALL sensitive values in plaintext. Anyone with read access to CI logs that run this command can see every secret. Pipe throughjq 'del(.db_password, .api_key)'or avoid-jsonin CI entirely.
# Sensitive values are redacted in output
tofu output db_password # shows (sensitive value)
# Get the actual value
tofu output -raw db_password
# Export all outputs as JSON (sensitive values included)
tofu output -json | jq .