Skip to content

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-unlock does NOT roll back partial applies. If the run died mid-apply, some resources may exist in the cloud but not in state. Always run tofu plan after 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.hcl and terraform.tfstate files 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 -json outputs ALL sensitive values in plaintext. Anyone with read access to CI logs that run this command can see every secret. Pipe through jq 'del(.db_password, .api_key)' or avoid -json in 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 .