OpenTofu Commands Reference
Your complete guide to managing infrastructure with project-planton tofu commands.
Overview
Think of OpenTofu as your infrastructure's blueprint compiler. Just as a compiler takes source code and produces an executable, OpenTofu takes your manifest and produces a plan for infrastructure changes. The project-planton CLI wraps OpenTofu operations with manifest-driven workflows, giving you the same consistent experience as Pulumi but using the battle-tested Terraform/OpenTofu engine.
The Infrastructure Lifecycle
┌──────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐ ┌─────────┐
│ init │ -> │ plan │ -> │ apply │ -> │ refresh │ -> │ destroy │
└──────────┘ └─────────┘ └────────┘ └─────────┘ └─────────┘
│ │ │ │ │
Initialize Preview Deploy Sync State Teardown
Backend Changes Resources with Cloud Resources
Key Concepts
Manifest: A YAML file describing your infrastructure resource (e.g., r2-bucket.yaml, eks-cluster.yaml). Think of it as a recipe.
State File: Where OpenTofu stores information about managed resources. Unlike Pulumi's "stack" concept, OpenTofu uses state files stored in backends (S3, GCS, local, etc.).
Module Directory: Where the Terraform/OpenTofu IaC code lives. Usually auto-detected from your manifest's resource kind, but can be overridden with --module-dir.
Backend: Where OpenTofu stores state files. Configure this in your module's backend.tf file (supports S3, GCS, Azure Blob, local file system, and more).
Plan File: An optional output from the plan command that can be applied exactly as planned (not commonly used with project-planton).
OpenTofu vs Pulumi: Key Differences
If you're familiar with the Pulumi commands, here's how OpenTofu differs:
| Concept | Pulumi | OpenTofu |
|---|---|---|
| State Container | Stack (org/project/stack) | Workspace + State File |
| Preview Changes | preview command | plan command |
| Deploy | up or update | apply |
| Auto-approve Flag | --yes | --auto-approve |
| Stack Deletion | delete or rm | No equivalent (just delete state) |
| Initialization | Creates stack metadata | Initializes backend connection |
Commands
init - Initialize Backend
What it does: Initializes the OpenTofu backend, downloads required provider plugins, and prepares the module directory for operations. Think of this as "setting up your workspace."
When to use:
- First time working with a module
- After cleaning
.terraformdirectory - After changing backend configuration
- When provider requirements change
Behavior:
- Downloads and installs provider plugins based on module requirements
- Initializes the backend (S3, GCS, local, etc.) for state storage
- Creates
.terraformdirectory with cached plugins and configuration - Does NOT create or modify any cloud resources
- Idempotent - safe to run multiple times
Usage:
project-planton tofu init --manifest <manifest-file> [flags]
Examples:
# Initialize for a basic deployment
project-planton tofu init \
--manifest ops/cloud-resources/prod/r2-bucket.yaml
# Initialize using kustomize overlay
project-planton tofu init \
--kustomize-dir backend/services/api \
--overlay prod
# Initialize with explicit module directory (for development/testing)
project-planton tofu init \
--manifest ops/resources/vpc.yaml \
--module-dir ~/projects/custom-modules/aws-vpc
What you'll see:
● Loading manifest...
✔ Manifest loaded
● Validating manifest...
✔ Manifest validated
● Preparing OpenTofu execution...
✔ Execution prepared
🤝 Handing off to OpenTofu...
Output below is from OpenTofu
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v5.30.0...
- Installed hashicorp/aws v5.30.0
OpenTofu has been successfully initialized!
You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure.
Important Notes:
- Run this first: Unlike
pulumi upwhich auto-creates stacks, OpenTofu requires explicit initialization - Cached plugins: Downloaded plugins are cached in
.terraform/- delete this to force re-download - Backend setup: The first init for a new state file creates it in your backend
- Module updates: Run init again after updating module dependencies
plan - Preview Infrastructure Changes
What it does: Creates an execution plan showing what OpenTofu will do when you apply your manifest. This is your "diff" before committing changes—you see exactly what will be created, modified, or destroyed.
When to use:
- Before running
applyto understand what will change - To validate your manifest produces the expected infrastructure changes
- During code review to demonstrate infrastructure changes
- When debugging unexpected behavior
- To create a "destroy plan" (using
--destroyflag)
Behavior:
- Compares your manifest against the current infrastructure state
- Shows a detailed execution plan with additions, modifications, and deletions
- Does NOT modify any cloud resources
- Does NOT modify state file
- Optionally saves the plan to a file (not commonly done with project-planton)
Usage:
project-planton tofu plan --manifest <manifest-file> [flags]
Examples:
# Plan changes for a Kubernetes deployment
project-planton tofu plan \
--manifest services/api/deployment.yaml
# Plan with field overrides (useful for testing different configurations)
project-planton tofu plan \
--manifest services/api/deployment.yaml \
--set spec.replicas=5 \
--set spec.container.image.tag=v2.0.0
# Plan using kustomize (common for multi-environment setups)
project-planton tofu plan \
--kustomize-dir backend/services/api \
--overlay staging
# Create a destroy plan (preview what destroy will do)
project-planton tofu plan \
--manifest ops/resources/test-cluster.yaml \
--destroy
Reading the output:
● Loading manifest...
✔ Manifest loaded
● Validating manifest...
✔ Manifest validated
● Preparing OpenTofu execution...
✔ Execution prepared
🤝 Handing off to OpenTofu...
Output below is from OpenTofu
OpenTofu will perform the following actions:
# cloudflare_r2_bucket.bucket will be created
+ resource "cloudflare_r2_bucket" "bucket" {
+ account_id = "abc123"
+ id = (known after apply)
+ location = "WNAM"
+ name = "my-bucket"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Legend:
+= Resource will be created~= Resource will be modified (shows specific attribute changes)-= Resource will be deleted-/+= Resource will be replaced (destroy then create, often due to immutable property changes)(known after apply)= Value will be computed during apply
Plan Modes:
# Standard plan (create/update resources)
project-planton tofu plan --manifest resource.yaml
# Destroy plan (preview destruction)
project-planton tofu plan --manifest resource.yaml --destroy
apply - Deploy Infrastructure
What it does: Applies your manifest to create, update, or configure cloud resources. This is the "execute" command—it actually makes the infrastructure changes.
When to use:
- After reviewing changes with
planand confirming they look correct - Initial deployment of new infrastructure
- Updating existing infrastructure with new configurations
- Applying configuration changes (scaling, updates, feature flags)
Behavior:
- Shows a plan of changes (unless you use
--auto-approve) - Waits for your confirmation before proceeding (unless
--auto-approveis provided) - Creates/updates/deletes cloud resources to match your manifest
- Updates state file to reflect the new infrastructure state
- Attempts to roll back on failures (where provider supports it)
Usage:
project-planton tofu apply --manifest <manifest-file> [flags]
Examples:
# Interactive deployment (will show plan and ask for confirmation)
project-planton tofu apply \
--manifest ops/resources/database.yaml
# Non-interactive deployment (CI/CD pipelines)
project-planton tofu apply \
--manifest ops/resources/database.yaml \
--auto-approve
# Deploy with field overrides
project-planton tofu apply \
--manifest ops/resources/cache.yaml \
--set spec.instanceSize=large \
--set spec.replicas=3
# Deploy using kustomize overlay
project-planton tofu apply \
--kustomize-dir backend/services/worker \
--overlay prod \
--auto-approve
What you'll see:
● Loading manifest...
✔ Manifest loaded
● Validating manifest...
✔ Manifest validated
● Preparing OpenTofu execution...
✔ Execution prepared
🤝 Handing off to OpenTofu...
Output below is from OpenTofu
OpenTofu will perform the following actions:
# gcp_sql_database_instance.main_db will be created
+ resource "gcp_sql_database_instance" "main_db" {
+ database_version = "POSTGRES_14"
+ name = "main-db"
+ project = "my-project"
+ region = "us-central1"
+ tier = "db-n1-standard-1"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
OpenTofu will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
gcp_sql_database_instance.main_db: Creating...
gcp_sql_database_instance.main_db: Still creating... [10s elapsed]
gcp_sql_database_instance.main_db: Still creating... [20s elapsed]
gcp_sql_database_instance.main_db: Creation complete after 2m15s [id=main-db]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Important Notes:
- Plan-then-apply workflow: By default,
applyshows you a plan and waits for confirmation. This is your safety net. - No automatic initialization: Unlike Pulumi's
up, you must runinitbefore firstapply - State locking: OpenTofu automatically locks state during apply to prevent concurrent modifications (if backend supports it)
- Failed applies: If apply fails midway, state will reflect the partial changes. You can re-run
applyto continue or userefreshto sync state - Idempotent: Running apply with no changes is safe—OpenTofu detects no changes needed
refresh - Sync State with Reality
What it does: Updates OpenTofu's state file to match the actual resources in your cloud provider without modifying any resources. Think of this as "git fetch" for infrastructure—it synchronizes your state with reality.
When to use:
- After manual changes made outside OpenTofu (e.g., via cloud console, CLI, or other tools)
- Before running
planorapplyto ensure state accuracy - After failed deployments to resynchronize state
- When troubleshooting drift between desired and actual state
- After importing existing resources into OpenTofu management
Behavior:
- Queries your cloud provider for the current state of managed resources
- Updates state file to reflect actual resource properties
- Does NOT modify any cloud resources
- Does NOT change your manifest file
- Shows what changed in the state (if anything)
- Detects resources that were deleted outside OpenTofu
Usage:
project-planton tofu refresh --manifest <manifest-file> [flags]
Examples:
# Refresh to sync state after manual changes
project-planton tofu refresh \
--manifest ops/resources/s3-bucket.yaml
# Refresh before important operations
project-planton tofu refresh \
--manifest ops/resources/production-db.yaml && \
project-planton tofu apply \
--manifest ops/resources/production-db.yaml
What you'll see:
● Loading manifest...
✔ Manifest loaded
● Validating manifest...
✔ Manifest validated
● Preparing OpenTofu execution...
✔ Execution prepared
🤝 Handing off to OpenTofu...
Output below is from OpenTofu
aws_s3_bucket.assets: Refreshing state... [id=assets-prod-xyz123]
Note: Objects have changed outside of OpenTofu
OpenTofu detected the following changes made outside of OpenTofu since the
last "tofu apply":
# aws_s3_bucket.assets has changed
~ resource "aws_s3_bucket" "assets" {
id = "assets-prod-xyz123"
~ tags = {
+ "Environment" = "production"
}
# (10 unchanged attributes hidden)
}
This is a data-only operation. No changes will be made to your
infrastructure.
Understanding Drift:
Drift occurs when someone (or something) modifies infrastructure outside of OpenTofu:
Before refresh:
State File: bucket versioning = false
Actual Cloud:
AWS Console: someone enabled versioning = true
After refresh:
State File: bucket versioning = true (synced!)
Next steps after refresh:
- If changes match your manifest → You're good, carry on
- If unexpected changes → Investigate who/what made them
- If you want to revert manual changes → Run
applyto restore manifest configuration
Important Note: Starting with Terraform 0.15+ and OpenTofu, refresh is automatically run as part of plan and apply. Explicit refresh is mainly useful for viewing drift without planning changes.
destroy - Teardown Infrastructure
What it does: Destroys all cloud resources managed by your manifest. This is the "rm -rf" of infrastructure—use with extreme caution.
When to use:
- Tearing down temporary environments (dev, testing, ephemeral previews)
- Decommissioning infrastructure that's no longer needed
- Cleaning up after testing or experimentation
- Cost optimization (shutting down unused resources)
- Before major refactoring (destroy old, deploy new)
Behavior:
- Shows a destroy plan (resources to be deleted)
- Waits for explicit confirmation (unless
--auto-approveis provided) - Deletes resources in reverse dependency order (children before parents)
- Updates state file to reflect deletion
- Cannot be undone once confirmed
Usage:
project-planton tofu destroy --manifest <manifest-file> [flags]
Examples:
# Interactive destroy (will ask for confirmation)
project-planton tofu destroy \
--manifest ops/resources/dev-cluster.yaml
# Non-interactive destroy (automation/CI)
project-planton tofu destroy \
--manifest ops/resources/test-environment.yaml \
--auto-approve
# Destroy temporary environment
project-planton tofu destroy \
--kustomize-dir backend/services/api \
--overlay pr-123 \
--auto-approve
What you'll see:
● Loading manifest...
✔ Manifest loaded
● Validating manifest...
✔ Manifest validated
● Preparing OpenTofu execution...
✔ Execution prepared
🤝 Handing off to OpenTofu...
Output below is from OpenTofu
OpenTofu will perform the following actions:
# gcp_container_cluster.test_cluster will be destroyed
- resource "gcp_container_cluster" "test_cluster" {
- name = "test-cluster" -> null
- location = "us-central1" -> null
- ...
}
# gcp_container_node_pool.default_pool will be destroyed
- resource "gcp_container_node_pool" "default_pool" {
- name = "default-pool" -> null
- ...
}
Plan: 0 to add, 0 to change, 2 to destroy.
Do you really want to destroy all resources?
OpenTofu will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
gcp_container_node_pool.default_pool: Destroying... [id=default-pool]
gcp_container_node_pool.default_pool: Still destroying... [10s elapsed]
gcp_container_node_pool.default_pool: Destruction complete after 45s
gcp_container_cluster.test_cluster: Destroying... [id=test-cluster]
gcp_container_cluster.test_cluster: Still destroying... [10s elapsed]
gcp_container_cluster.test_cluster: Destruction complete after 2m30s
Destroy complete! Resources: 2 destroyed.
⚠️ Safety Warnings:
- Permanent deletion: Most cloud providers permanently delete resources. Some have soft-delete/trash, but don't count on it.
- Data loss: Databases, storage buckets, and other stateful resources will lose their data unless you have backups.
- Dependency risk: If other resources depend on what you're destroying, they may break.
- No undo: Once you confirm, there's no rollback. The resources are gone.
- State remains: Unlike cloud resources, the state file remains. To clean it up, manually delete the state file from your backend.
Best Practices:
# ✅ Good: Review before destroying
project-planton tofu plan --manifest prod.yaml --destroy # Preview destruction
project-planton tofu destroy --manifest prod.yaml # Interactive confirmation
# ⚠️ Risky: Blind destruction
project-planton tofu destroy --manifest prod.yaml --auto-approve
# ✅ Good: Backup data first
aws s3 sync s3://my-bucket ./backup-$(date +%Y%m%d)/
project-planton tofu destroy --manifest s3-bucket.yaml
# ✅ Good: Verify manifest before destroying
cat prod.yaml # Make absolutely sure this is the right file
project-planton tofu destroy --manifest prod.yaml
Common Flags
All commands support these flags. They're like the universal remote for infrastructure management.
Manifest Input
--manifest <file>: Path to your resource manifest YAML file.
project-planton tofu apply --manifest ops/resources/my-resource.yaml
--kustomize-dir <dir> + --overlay <name>: Use kustomize for environment-specific configurations.
# Loads kustomize base + overlays/prod
project-planton tofu apply \
--kustomize-dir backend/services/api \
--overlay prod
Priority: --manifest > --kustomize-dir + --overlay
Execution Control
--module-dir <path>: Override the OpenTofu module directory (defaults to current directory).
# Use local development module instead of released version
project-planton tofu apply \
--manifest my-resource.yaml \
--module-dir ~/projects/custom-modules/my-module
--auto-approve: Auto-approve without confirmation prompts (for CI/CD). Available for apply and destroy commands.
project-planton tofu apply --manifest resource.yaml --auto-approve
--destroy: Create a destruction plan (only for plan command).
# Preview what destroy will do
project-planton tofu plan --manifest resource.yaml --destroy
--set <key>=<value>: Override manifest values at runtime (repeatable flag).
project-planton tofu apply \
--manifest deployment.yaml \
--set spec.replicas=10 \
--set spec.container.image.tag=v2.1.0 \
--set metadata.env=staging
Credential Injection
These flags inject provider credentials (alternative to environment variables):
--aws-credential <file>: Path to AWS credential YAML--azure-credential <file>: Path to Azure credential YAML--gcp-credential <file>: Path to GCP credential YAML--kubernetes-cluster <file>: Path to Kubernetes cluster credential YAML--confluent-credential <file>: Path to Confluent Cloud credential YAML--docker-credential <file>: Path to Docker registry credential YAML--mongodb-atlas-credential <file>: Path to MongoDB Atlas credential YAML--snowflake-credential <file>: Path to Snowflake credential YAML
Example:
project-planton tofu apply \
--manifest ops/aws-resources/vpc.yaml \
--aws-credential ~/.config/planton/credentials/aws-prod.yaml
Common Workflows
First-Time Deployment
# 1. Initialize the backend and providers
project-planton tofu init --manifest my-resource.yaml
# 2. Preview what will be created
project-planton tofu plan --manifest my-resource.yaml
# 3. Deploy the infrastructure
project-planton tofu apply --manifest my-resource.yaml
Updating Existing Infrastructure
# 1. Edit your manifest
vim ops/resources/my-app.yaml
# 2. Preview the changes
project-planton tofu plan --manifest ops/resources/my-app.yaml
# 3. Apply if changes look good
project-planton tofu apply --manifest ops/resources/my-app.yaml
Testing Configuration Changes
# Preview with overrides (no changes to manifest file)
project-planton tofu plan \
--manifest api-deployment.yaml \
--set spec.replicas=20 \
--set spec.resources.limits.cpu=4000m
# If it looks good, apply with same overrides
project-planton tofu apply \
--manifest api-deployment.yaml \
--set spec.replicas=20 \
--set spec.resources.limits.cpu=4000m
# Later, commit the changes to manifest
vim api-deployment.yaml # Make changes permanent
Emergency Rollback
# Scenario: v2.0.0 deployment has issues, need to roll back to v1.9.5
# Option 1: Override the current manifest
project-planton tofu apply \
--manifest deployment.yaml \
--set spec.container.image.tag=v1.9.5
# Option 2: Revert manifest to previous version
git checkout HEAD~1 deployment.yaml
project-planton tofu apply --manifest deployment.yaml
# Option 3: Use a previous Git revision
git show HEAD~5:deployment.yaml > /tmp/previous-deployment.yaml
project-planton tofu apply --manifest /tmp/previous-deployment.yaml
Syncing After Manual Changes
# Someone made changes via AWS console, need to sync state
# 1. Refresh to see what changed
project-planton tofu refresh --manifest s3-bucket.yaml
# 2. Review the diff
project-planton tofu plan --manifest s3-bucket.yaml
# 3. Decide:
# - Changes match manifest? → Do nothing, state is synced
# - Changes don't match? → Update manifest or revert via `apply`
# 4. If reverting manual changes:
project-planton tofu apply --manifest s3-bucket.yaml # Restores manifest config
Multi-Environment Deployment
# Using kustomize overlays for different environments
# Deploy to dev
project-planton tofu apply \
--kustomize-dir services/api \
--overlay dev
# Preview staging changes
project-planton tofu plan \
--kustomize-dir services/api \
--overlay staging
# Deploy to production (with extra caution)
project-planton tofu plan \
--kustomize-dir services/api \
--overlay prod
# Review carefully...
project-planton tofu apply \
--kustomize-dir services/api \
--overlay prod
Local Module Development
# Testing changes to OpenTofu module code without publishing
cd ~/projects/project-planton/apis/.../.../iac/tofu
# Initialize with local module
project-planton tofu init \
--manifest ~/manifests/test-resource.yaml \
--module-dir .
# Preview with local module
project-planton tofu plan \
--manifest ~/manifests/test-resource.yaml \
--module-dir .
# Iterate: edit module code, run plan again
vim main.tf
project-planton tofu plan \
--manifest ~/manifests/test-resource.yaml \
--module-dir .
# Deploy with local module
project-planton tofu apply \
--manifest ~/manifests/test-resource.yaml \
--module-dir .
CI/CD Pipeline
#!/bin/bash
# deploy.sh - Automated deployment script
set -e # Exit on error
MANIFEST="ops/resources/app-${ENV}.yaml"
echo "🔍 Planning changes..."
project-planton tofu plan --manifest "$MANIFEST"
echo "🚀 Deploying infrastructure..."
project-planton tofu apply --manifest "$MANIFEST" --auto-approve
echo "✅ Deployment complete"
GitHub Actions Example:
name: Deploy Infrastructure
on:
push:
branches: [main]
paths: ['ops/resources/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup OpenTofu
uses: opentofu/setup-opentofu@v1
with:
tofu_version: 1.6.0
- name: Deploy Resources
run: |
project-planton tofu init \
--manifest ops/resources/prod-infra.yaml
project-planton tofu apply \
--manifest ops/resources/prod-infra.yaml \
--auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Troubleshooting
Error: "Backend initialization required"
Symptom: Commands fail saying backend needs initialization.
Cause: Haven't run init yet or .terraform directory was deleted.
Solution:
# Run init to set up backend and providers
project-planton tofu init --manifest my-resource.yaml
# Then try your command again
project-planton tofu plan --manifest my-resource.yaml
Error: "state lock"
Symptom: Command fails saying state is locked by another process.
Cause: A previous operation crashed or is still running, leaving the state locked.
Solution:
# Option 1: Wait for the other operation to complete
# Option 2: Force unlock (use with caution - only if you're certain no operation is running)
cd <module-directory>
tofu force-unlock <lock-id>
# Then retry your operation
project-planton tofu apply --manifest my-resource.yaml
Provider Authentication Failures
Symptom: "failed to configure provider" or authentication errors.
Causes: Missing or invalid cloud provider credentials.
Solutions:
For AWS:
# Check credentials
aws sts get-caller-identity
# Or provide credential file
project-planton tofu apply \
--manifest resource.yaml \
--aws-credential ~/.aws/credentials-prod.yaml
For GCP:
# Check credentials
gcloud auth list
gcloud config get-value project
# Or set environment variable
export GOOGLE_APPLICATION_CREDENTIALS=~/gcp-key.json
project-planton tofu apply --manifest resource.yaml
For Cloudflare:
# Set API token
export CLOUDFLARE_API_TOKEN="your-token-here"
project-planton tofu apply --manifest resource.yaml
Plan Shows Unexpected Changes
Symptom: plan shows modifications you didn't make.
Causes:
- Someone made manual changes outside OpenTofu
- Provider API defaults changed
- Computed values changed upstream
- State file is out of sync
Solution:
# First, sync state with reality
project-planton tofu refresh --manifest resource.yaml
# Then plan again
project-planton tofu plan --manifest resource.yaml
# If changes persist, check for:
# - Manual modifications in cloud console
# - Provider version changes
# - Upstream resource changes
Resource Already Exists
Symptom: "resource already exists" error during apply.
Cause: Resources exist in cloud but not in state file (created outside OpenTofu or state lost).
Solution:
# Option 1: Import existing resources (advanced)
cd <module-directory>
tofu import <resource-type>.<resource-name> <cloud-resource-id>
# Option 2: Manually delete cloud resources
# (Use cloud provider console/CLI to delete conflicting resources)
# Option 3: Use different resource names in manifest
vim my-resource.yaml # Change metadata.name or resource IDs
Module Not Found
Symptom: "module not found" or "no configuration files" error.
Cause: OpenTofu can't find the module directory or it contains no .tf files.
Solution:
# Check module directory exists
ls -la <module-directory>
# Verify it contains .tf files
ls <module-directory>/*.tf
# If using custom module, ensure --module-dir points to correct location
project-planton tofu init \
--manifest resource.yaml \
--module-dir /correct/path/to/module
Best Practices
1. Always Plan Before Applying
# ✅ Good: Review changes first
project-planton tofu plan --manifest resource.yaml
# Read output, verify changes look correct
project-planton tofu apply --manifest resource.yaml
# ⚠️ Risky: Blind deployment
project-planton tofu apply --manifest resource.yaml --auto-approve
Why: Plan is your safety net. It catches mistakes before they become expensive incidents.
2. Use Version Control for Manifests
# ✅ Good: Track changes in Git
git add ops/resources/my-resource.yaml
git commit -m "feat: increase database instance size"
git push
# Deploy via CI/CD or manually
# ❌ Bad: Direct edits without version control
vim /tmp/my-resource.yaml
project-planton tofu apply --manifest /tmp/my-resource.yaml
Why: Version control gives you change history, rollback capability, and code review.
3. Initialize Before Each Session
# ✅ Good: Run init when starting work
project-planton tofu init --manifest resource.yaml
project-planton tofu plan --manifest resource.yaml
# ⚠️ Risky: Assuming init was already run
project-planton tofu plan --manifest resource.yaml # May fail
Why: Init is fast and idempotent. Running it ensures providers and backend are ready.
4. Use Descriptive Resource Names
# ✅ Good: Clear, hierarchical naming
metadata:
name: prod-api-database
# ❌ Bad: Generic, unclear names
metadata:
name: db1
Why: Good names make it obvious what infrastructure the manifest manages.
5. Test Changes in Lower Environments First
# ✅ Good: Progressive deployment
project-planton tofu apply --kustomize-dir services/api --overlay dev
# Test in dev...
project-planton tofu apply --kustomize-dir services/api --overlay staging
# Test in staging...
project-planton tofu apply --kustomize-dir services/api --overlay prod
# ❌ Bad: YOLO to production
project-planton tofu apply --kustomize-dir services/api --overlay prod --auto-approve
Why: Lower environments catch issues before they impact production.
6. Use --set for Temporary Overrides Only
# ✅ Good: Quick testing
project-planton tofu plan \
--manifest deployment.yaml \
--set spec.replicas=1 # Test with minimal resources
# ❌ Bad: Permanent changes via flag
# (6 months later: "Why is prod running 1 replica?!")
project-planton tofu apply \
--manifest deployment.yaml \
--set spec.replicas=1 \
--auto-approve
Why: Flags don't persist. Commit important changes to your manifest.
7. Document Provider Credentials
# ✅ Good: Document in README
# ops/README.md
# Deploy with:
# export CLOUDFLARE_API_TOKEN=$(pass cloudflare/api-token)
# project-planton tofu apply --manifest r2-bucket.yaml
# ⚠️ Bad: Tribal knowledge
# (New team member: "How do I deploy this?")
Why: Documentation prevents "works on my machine" situations.
8. Clean Up State Files After Destroy
# After destroying resources, consider cleaning up state file
project-planton tofu destroy --manifest temp-resource.yaml --auto-approve
# Optionally, remove state file from backend
# (This step depends on your backend - S3, GCS, etc.)
aws s3 rm s3://my-terraform-state/path/to/state/terraform.tfstate
Why: Prevents confusion from orphaned state files.
Tips & Tricks
Viewing State
# View current state (requires being in module directory)
cd <module-directory>
tofu state list
tofu state show <resource-address>
Targeting Specific Resources
# Apply changes to only specific resources (requires being in module directory)
cd <module-directory>
tofu apply -target=aws_s3_bucket.my_bucket
Viewing Outputs
# View outputs from last apply (requires being in module directory)
cd <module-directory>
tofu output
tofu output -json # Get outputs as JSON
Debugging
# Enable verbose logging
export TF_LOG=DEBUG
project-planton tofu plan --manifest resource.yaml
# Or trace level for maximum verbosity
export TF_LOG=TRACE
project-planton tofu plan --manifest resource.yaml
Format Validation
# Validate Terraform syntax (requires being in module directory)
cd <module-directory>
tofu validate
# Format Terraform files
tofu fmt
State Management
Understanding State
OpenTofu tracks your infrastructure in a state file (terraform.tfstate). This file maps your manifest to real-world resources.
State storage options:
- Local: State file on disk (default, not recommended for teams)
- S3: State in AWS S3 bucket (common for AWS deployments)
- GCS: State in Google Cloud Storage (common for GCP deployments)
- Azure Blob: State in Azure Storage (common for Azure deployments)
- Pulumi Cloud, Terraform Cloud, etc.: Managed state backends
Why state matters:
- Tracks which cloud resources belong to which manifest
- Stores resource IDs and properties
- Enables drift detection (comparing state to reality)
- Supports locking to prevent concurrent modifications
State file location is configured in your module's backend.tf (or similar configuration).
State vs Stack
If you're coming from Pulumi:
| Pulumi | OpenTofu |
|---|---|
| Stack = org/project/stack | State file + (optional) workspace |
| Stack stored in Pulumi backend | State stored in configured backend |
pulumi stack commands | tofu state commands |
| Stack FQDN in manifest labels | State location in module backend config |
Related Documentation
- OpenTofu Documentation - Official OpenTofu documentation
- Terraform Compatibility - OpenTofu's compatibility with Terraform
- Manifest Structure Guide - Understanding Project Planton manifests
- Credentials Guide - Setting up cloud provider credentials
- CLI Reference - Complete CLI command reference
Getting Help
Found a bug? Open an issue
Need support? Check existing issues or discussions
Contributing? Pull requests welcome!
Remember: Infrastructure as code is code. Apply the same discipline you'd apply to application code—version control, testing, code review, and automation. Your infrastructure deserves it. 🚀
Next article