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:

ConceptPulumiOpenTofu
State ContainerStack (org/project/stack)Workspace + State File
Preview Changespreview commandplan command
Deployup or updateapply
Auto-approve Flag--yes--auto-approve
Stack Deletiondelete or rmNo equivalent (just delete state)
InitializationCreates stack metadataInitializes 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 .terraform directory
  • 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 .terraform directory 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 up which 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 apply to 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 --destroy flag)

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 plan and 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-approve is 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, apply shows you a plan and waits for confirmation. This is your safety net.
  • No automatic initialization: Unlike Pulumi's up, you must run init before first apply
  • 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 apply to continue or use refresh to 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 plan or apply to 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:

  1. If changes match your manifest → You're good, carry on
  2. If unexpected changes → Investigate who/what made them
  3. If you want to revert manual changes → Run apply to 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-approve is 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:

  1. Permanent deletion: Most cloud providers permanently delete resources. Some have soft-delete/trash, but don't count on it.
  2. Data loss: Databases, storage buckets, and other stateful resources will lose their data unless you have backups.
  3. Dependency risk: If other resources depend on what you're destroying, they may break.
  4. No undo: Once you confirm, there's no rollback. The resources are gone.
  5. 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:

  1. Someone made manual changes outside OpenTofu
  2. Provider API defaults changed
  3. Computed values changed upstream
  4. 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:

PulumiOpenTofu
Stack = org/project/stackState file + (optional) workspace
Stack stored in Pulumi backendState stored in configured backend
pulumi stack commandstofu state commands
Stack FQDN in manifest labelsState location in module backend config


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

Manifest Structure Guide

Manifest Structure Guide Your complete guide to writing and understanding Project Planton manifests. What is a Manifest? A manifest is a YAML file that describes a piece of infrastructure you want to deploy. Think of it as a recipe card: it lists what you want to create (a database, a Kubernetes cluster, a storage bucket) and how you want it configured (size, region, settings). Simple example: This manifest says: "Create a Cloudflare R2 bucket named my-app-assets in the Western North America...
Read next article