OpenTofu Coder
⚠️ SIMPLICITY FIRST - Default to Flat Files
ALWAYS start with the simplest approach. Only add complexity when explicitly requested.
Simple (DEFAULT) vs Overengineered
Aspect ✅ Simple (Default) ❌ Overengineered
Structure Flat .tf files in one directory Nested modules/ + environments/ directories
Modules None or only remote registry modules Custom local modules for simple resources
Environments Workspaces OR single tfvars Duplicate directory per environment
Variables Inline defaults, minimal tfvars Complex variable hierarchies
File count 3-5 .tf files total 15+ files across nested directories
When to Use Simple Approach (90% of cases)
-
Managing 1-5 resources of each type
-
Single provider, single region
-
Small team or solo developer
-
Standard infrastructure patterns
When Complexity is Justified (10% of cases)
-
Enterprise multi-region, multi-account
-
Reusable modules shared across teams
-
Complex dependency chains
-
User explicitly requests modular structure
Rule: If you can define everything in 5 flat .tf files, DO IT.
Simple Project Structure (DEFAULT)
infra/ ├── main.tf # All resources ├── variables.tf # Input variables ├── outputs.tf # Outputs ├── versions.tf # Provider versions └── terraform.tfvars # Variable values (gitignored)
Overview
OpenTofu is a community-driven, open-source fork of Terraform under MPL-2.0 license, maintained by the Linux Foundation. It uses HashiCorp Configuration Language (HCL) for declarative infrastructure management across cloud providers.
Core Philosophy
Prioritize:
-
Declarative over imperative: Describe desired state, not steps
-
Idempotency: Apply safely multiple times with same result
-
Modularity: Compose infrastructure from reusable modules
-
State as truth: State file is the source of truth for managed resources
-
Immutable infrastructure: Replace resources rather than mutate in place
HCL Syntax Essentials
Resource Blocks
resource "aws_instance" "web" { ami = var.ami_id instance_type = var.instance_type
tags = { Name = "${var.project}-web" Environment = var.environment } }
Data Sources
data "aws_ami" "ubuntu" { most_recent = true
filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] }
owners = ["099720109477"] # Canonical }
Variables
variable "environment" { description = "Deployment environment (dev, staging, prod)" type = string default = "dev"
validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be dev, staging, or prod." } }
variable "instance_types" { description = "Map of environment to instance type" type = map(string) default = { dev = "t3.micro" staging = "t3.small" prod = "t3.medium" } }
Outputs
output "instance_ip" { description = "Public IP of the web instance" value = aws_instance.web.public_ip sensitive = false }
output "database_password" { description = "Generated database password" value = random_password.db.result sensitive = true }
Locals
locals { common_tags = { Project = var.project Environment = var.environment ManagedBy = "OpenTofu" }
name_prefix = "${var.project}-${var.environment}" }
Meta-Arguments
count - Create Multiple Instances
resource "aws_instance" "server" { count = var.server_count
ami = var.ami_id instance_type = var.instance_type
tags = { Name = "${local.name_prefix}-server-${count.index}" } }
for_each - Create from Map/Set
resource "aws_iam_user" "users" { for_each = toset(var.user_names)
name = each.value path = "/users/" }
resource "aws_security_group_rule" "ingress" { for_each = var.ingress_rules
type = "ingress" from_port = each.value.port to_port = each.value.port protocol = each.value.protocol cidr_blocks = each.value.cidr_blocks security_group_id = aws_security_group.main.id }
depends_on - Explicit Dependencies
resource "aws_instance" "app" { ami = var.ami_id instance_type = var.instance_type
depends_on = [ aws_db_instance.database, aws_elasticache_cluster.cache ] }
lifecycle - Control Resource Behavior
resource "aws_instance" "critical" { ami = var.ami_id instance_type = var.instance_type
lifecycle { prevent_destroy = true create_before_destroy = true ignore_changes = [ tags["LastUpdated"], user_data ] } }
Replace when AMI changes
resource "aws_instance" "immutable" { ami = var.ami_id instance_type = var.instance_type
lifecycle { replace_triggered_by = [ null_resource.ami_trigger ] } }
Module Design
Module Structure
modules/ └── vpc/ ├── main.tf # Primary resources ├── variables.tf # Input variables ├── outputs.tf # Output values ├── versions.tf # Required providers └── README.md # Documentation
Calling Modules
module "vpc" { source = "./modules/vpc"
cidr_block = "10.0.0.0/16" environment = var.environment
azs = ["us-east-1a", "us-east-1b"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"] }
Remote module with version
module "eks" { source = "terraform-aws-modules/eks/aws" version = "~> 20.0"
cluster_name = local.cluster_name cluster_version = "1.29"
vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnet_ids }
Module Best Practices
-
Expose minimal, clear interface of variables
-
Use sensible defaults where possible
-
Document all variables and outputs
-
Avoid over-generic "god" modules
-
Prefer composition over configuration flags
-
Version pin remote modules
State Management
Remote Backend (S3)
terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/network/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "terraform-locks" } }
OpenTofu State Encryption (Unique Feature)
terraform { encryption { key_provider "pbkdf2" "main" { passphrase = var.state_encryption_passphrase }
method "aes_gcm" "encrypt" {
keys = key_provider.pbkdf2.main
}
state {
method = method.aes_gcm.encrypt
enforced = true
}
plan {
method = method.aes_gcm.encrypt
enforced = true
}
} }
State Commands
List resources in state
tofu state list
Show specific resource
tofu state show aws_instance.web
Move resource (refactoring)
tofu state mv aws_instance.old aws_instance.new
Remove from state (without destroying)
tofu state rm aws_instance.imported
Import existing resource
tofu import aws_instance.web i-1234567890abcdef0
See Provider Configuration for AWS provider setup, authentication methods, and multi-provider patterns.
See Environment Strategies for workspaces and directory-based environment management.
CLI Workflow
Initialize working directory
tofu init
Validate configuration
tofu validate
Format code
tofu fmt -recursive
Preview changes
tofu plan -out=plan.tfplan
Apply changes
tofu apply plan.tfplan
Destroy infrastructure
tofu destroy
Show current state
tofu show
Refresh state from actual infrastructure
tofu refresh
Best Practices Checklist
When writing OpenTofu/Terraform code:
-
Use remote backend with locking for team use
-
Enable state encryption (OpenTofu feature)
-
Never commit .tfstate or .tfvars with secrets to VCS
-
Pin provider and module versions
-
Use tofu plan before every apply
-
Use lifecycle.prevent_destroy for critical resources
-
Document all variables and outputs
-
Use locals for computed values and tags
-
Prefer for_each over count for named resources
-
Use validation blocks for variable constraints
-
Store secrets in secret managers, not in code
Common Patterns
Conditional Resources
resource "aws_eip" "static" { count = var.create_elastic_ip ? 1 : 0
instance = aws_instance.web.id }
Dynamic Blocks
resource "aws_security_group" "main" { name = "${local.name_prefix}-sg"
dynamic "ingress" { for_each = var.ingress_rules content { from_port = ingress.value.port to_port = ingress.value.port protocol = ingress.value.protocol cidr_blocks = ingress.value.cidr_blocks } } }
References
For detailed patterns and examples:
-
references/hcl-patterns.md - Advanced HCL patterns
-
references/project-scaffolding.md - Directory structure, .gitignore, next_steps output, security-first variables
-
references/post-provisioning.md - bin/setup-server scripts for post-infra, pre-deployment setup
-
references/state-management.md - State operations and encryption
-
references/provider-examples.md - Multi-cloud provider configs
-
references/makefile-automation.md - Makefile workflows for plan/apply/destroy