OpenTofu
OpenTofu is an open-source tool for building, changing, and versioning infrastructure safely and efficiently. It can manage existing and popular service providers as well as custom in-house solutions. It originally derived from HashiCorp Terraform after its license change to BSL in 2023.
Key features:
- Infrastructure as Code: Infrastructure is described using a high-level configuration syntax (HCL), allowing your datacenter blueprint to be versioned, shared, and reused like any other code.
- Execution Plans: OpenTofu has a “planning” step that generates an execution plan showing exactly what will happen before
applyis run — no surprises. - Resource Graph: OpenTofu builds a dependency graph of all resources and parallelizes creation/modification of independent resources for maximum efficiency.
- Change Automation: Complex changesets can be applied with minimal human interaction. The execution plan and resource graph together make changes predictable and auditable.
Usage
Define your infrastructure with providers and resources, then deploy:
# Initialize OpenTofu (downloads providers, sets up backend)
tofu init
# Preview the changes that will be made
tofu plan
# Apply the actual changes
tofu apply
# Refresh external state with local state
tofu refresh
# Show the current state of your infrastructure
tofu show
# Export a dependency graph in Graphviz format
tofu graphTip: Use
tofu plan -out=tfplanto save a plan file, thentofu apply tfplanto apply it exactly — useful in CI/CD pipelines.
State
OpenTofu maintains a state file that tracks the current state of your infrastructure. This allows it to know what resources have been created and what changes need to be made. State is stored in .tfstate files.
⚠️ NEVER commit state files to version control. They contain sensitive information like secrets, IPs, and resource IDs.
Add to .gitignore:
.terraform
.terraform.lock.hcl
terraform.tfstate
terraform.tfstate.backup
terraform.tfstate.d/
*.tfvarsBackends
By default, state is stored via the local backend — a plain-text terraform.tfstate file in your working directory. This is fine for solo development, but not suitable for teams or production.
In a team setting you need a centralized, lockable state backend. The two most common options are S3 and HTTP.
S3 Backend
The S3 backend is the most popular choice for AWS users. It supports state locking via DynamoDB.
terraform {
backend "s3" {
bucket = "my-tofu-state"
key = "prod/terraform.tfstate"
region = "eu-central-1"
# State locking (strongly recommended)
dynamodb_table = "tofu-state-lock"
encrypt = true
}
}Setting up the DynamoDB lock table (one-time, can be done via the AWS CLI):
aws dynamodb create-table \
--table-name tofu-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUESTThe S3 bucket itself should have versioning enabled so you can recover from accidental state corruption:
aws s3api put-bucket-versioning \
--bucket my-tofu-state \
--versioning-configuration Status=EnabledHTTP Backend
A generic HTTP backend suitable for self-hosted state servers (e.g. GitLab, Terraform HTTP backend implementations):
terraform {
backend "http" {
address = "https://example.com/state/hydra-prod"
lock_address = "https://example.com/lock/hydra-prod"
unlock_address = "https://example.com/lock/hydra-prod"
lock_method = "POST"
unlock_method = "DELETE"
}
}After configuring a new backend, run tofu init to migrate your existing state to it.
Syntax
OpenTofu configuration is written in HCL (HashiCorp Configuration Language). It’s also possible to write raw HCL JSON, or use Nix via Terranix.
Variables
Variables allow you to parameterize your configuration and are referenced via var.<name>:
variable "cloudflare_api_token" {
type = string
description = "Cloudflare API token"
sensitive = true # Prevents the value from being shown in logs
}
variable "environment" {
type = string
default = "dev"
}Variable values are resolved in this priority order (highest to lowest):
-var/-var-fileCLI arguments*.auto.tfvarsfilesterraform.tfvars- Environment variables:
TF_VAR_<name> - Default values defined in the variable block
Locals
Locals are computed values you can reuse across your config without exposing them as inputs:
locals {
common_tags = {
environment = var.environment
managed_by = "opentofu"
}
}
resource "aws_instance" "web" {
# ...
tags = local.common_tags
}Outputs
Outputs expose values after apply, useful for passing data between modules or displaying results:
output "instance_ip" {
value = aws_instance.web.public_ip
description = "Public IP of the web server"
}Providers
Providers are plugins that let OpenTofu interact with external services and APIs. They’re declared in a required_providers block and configured separately:
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}Some useful providers from the registry:
- Telmate/proxmox
- cloudflare/cloudflare
- goauthentik/authentik
- gavinbunney/kubectl
- kfkonrad/forgejo
- grafana/grafana
- jkossis/garage
Resources
Resources represent actual infrastructure objects. They’re the core building block of OpenTofu configuration:
resource "cloudflare_dns_record" "www" {
name = "www"
value = "1.2.3.4"
type = "A"
ttl = 3600
}The key syntax is resource "<type>" "<name>" {} where <type> is provider-specific and <name> is your local identifier for referring to this resource elsewhere (e.g. cloudflare_dns_record.www.id).
Resources can be imported from existing infrastructure if the provider supports it:
tofu import <resource_type>.<resource_name> <external_id>
# Example
tofu import cloudflare_dns_record.www abc123Data Sources
Data sources let you fetch read-only information from providers without managing it:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
# ...
}Modules
Modules are reusable, self-contained packages of OpenTofu configuration:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
}Modules can be sourced from the registry, a local path (./modules/vpc), or a Git URL.
Full Example (Cloudflare)
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5"
}
}
backend "s3" {
bucket = "my-tofu-state"
key = "cloudflare/terraform.tfstate"
region = "eu-central-1"
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
variable "cloudflare_api_token" {
type = string
sensitive = true
}
resource "cloudflare_dns_record" "www" {
name = "www"
value = "1.2.3.4"
type = "A"
ttl = 3600
}
output "record_hostname" {
value = cloudflare_dns_record.www.hostname
}