opentofu-coder
This skill guides writing Infrastructure as Code using OpenTofu (open-source Terraform fork). Use when creating .tf files, managing cloud infrastructure, configuring providers, or designing reusable modules.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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
Provider Configuration
AWS Provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = local.common_tags
}
}
# Multiple provider configurations
provider "aws" {
alias = "us_west"
region = "us-west-2"
}
resource "aws_instance" "west" {
provider = aws.us_west
# ...
}
Provider Authentication
# Environment variables (preferred)
# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
# AWS_PROFILE for named profiles
# Named profile from ~/.aws/credentials (recommended)
provider "aws" {
region = var.aws_region
profile = "suppli" # Uses [suppli] section from ~/.aws/credentials
}
# Or explicit (NOT recommended for secrets)
provider "aws" {
region = var.aws_region
access_key = var.aws_access_key # Use env vars instead
secret_key = var.aws_secret_key
}
# Assume role
provider "aws" {
region = var.aws_region
assume_role {
role_arn = "arn:aws:iam::123456789012:role/DeployRole"
session_name = "TofuDeployment"
}
}
Environment Strategies
Workspaces
# Create and switch workspaces
tofu workspace new dev
tofu workspace new staging
tofu workspace new prod
# Switch workspace
tofu workspace select prod
# List workspaces
tofu workspace list
# Use workspace in configuration
locals {
environment = terraform.workspace
instance_type = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.medium"
}[terraform.workspace]
}
Directory-Based Environments (Alternative)
infrastructure/
├── modules/ # Shared modules
│ ├── vpc/
│ └── eks/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ └── terraform.tfvars
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
.tfstateor.tfvarswith secrets to VCS - Pin provider and module versions
- Use
tofu planbefore everyapply - Use
lifecycle.prevent_destroyfor critical resources - Document all variables and outputs
- Use
localsfor computed values and tags - Prefer
for_eachovercountfor 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:
- resources/hcl-patterns.md - Advanced HCL patterns
- resources/project-scaffolding.md - Directory structure, .gitignore, next_steps output, security-first variables
- resources/post-provisioning.md - bin/setup-server scripts for post-infra, pre-deployment setup
- resources/state-management.md - State operations and encryption
- resources/provider-examples.md - Multi-cloud provider configs
- resources/makefile-automation.md - Makefile workflows for plan/apply/destroy
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-02-04
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!