Developer’s Guide to OpenTofu: Setup, Policy, State
-
About
- Type
- Blog
- Category
- Cloud Infrastructure
About
Table of contents
Posted on 20 October 2025
It’s common knowledge that Terraform’s move from an open license to a Business Source License (BSL) has limited companies’ ability to build on top of it. In response, OpenTofu was created as a fully open alternative, yet many teams still struggle to find practical guidance on how to use it correctly. Clear explanations on structuring modules, managing state files, and following best practices that keep infrastructure secure are scarce.This guide covers all of that. You’ll learn when OpenTofu makes sense, its core concepts, how to work with modules and state files, and how to avoid common pitfalls.
Although OpenTofu is a direct fork of Terraform, there are specific situations where adopting it makes sense, and others where staying with Terraform might be better.
Since both tools use the same HCL language, you can keep critical workloads on Terraform while using OpenTofu for non-critical projects. This approach gives you two advantages.
Just like Terraform, OpenTofu has core components that let you interact with cloud-native tools to create, update, and destroy infrastructure. These components form the foundation on which OpenTofu is built.
Below are some of the most important OpenTofu concepts that will make it easier to understand how the tool works and how to use it in practice.
Providers and versions
The provider component acts as the bridge between OpenTofu and the API of the platform you want to manage. Think of it as a messenger that tells cloud providers, such as UpCloud or AWS, or tools like vSphere and Helm, that you want to create infrastructure objects within their environment.
If your code and permissions are correctly configured, the provider allows you to create specific resources or fetch information from existing ones using data sources. It’s like a handshake that says, “I have valid credentials such as API keys, SSH keys, or temporary OIDC tokens, and I’m ready to manage resources through your API.”
For example, connecting to the UpCloud provider gives you access to create virtual machines, databases, and other managed resources.
terraform {
required_version = ">= 1.7.0" # OpenTofu version requirement
required_providers {
upcloud = {
source = "upcloudltd/upcloud"
version = "~> 5.0"
}
}
}
Notice how the block name remains terraform. This maintains backward compatibility with the HCL syntax, allowing you to roll back to Terraform if needed. OpenTofu version 1.7.0 or higher is required for full compatibility with current HCL syntax and providers.
When you run tofu init, OpenTofu generates a dependency lock file named .tofu.lock.hcl that records the exact provider versions used in your project. So, when another engineer runs the init command on the same codebase, OpenTofu will automatically use those same versions. This ensures that everyone works with an identical set of dependencies, helping to prevent version drift across environments.
Here is what the lockfile looks like:
provider "registry.opentofu.org/upcloudltd/upcloud" {
version = "5.0.1"
constraints = "~> 5.0"
hashes = [
"h1:vVqEPr4Z9Zy8dBzBxXzH8KzM3A1bD9jLqV1V2w3Xv0Y=",
]
}
Resources and data sources
Resources are the infrastructure components that OpenTofu can manage within any provider you authenticate into. These include virtual machines, databases, Kubernetes clusters, and virtual networks, among others. With OpenTofu, you can create, update, and delete instances of these resources as part of your infrastructure lifecycle.
resource "upcloud_server" "web_server" {
hostname = "web-server-01"
zone = "us-nyc1"
plan = "2xCPU-4GB"
template {
storage = "Ubuntu Server 22.04 LTS (Jammy Jellyfish)"
size = 50
}
}
Data sources, on the other hand, allow you to fetch existing configurations or information from a provider. The retrieved data can then be referenced within other resources you’re managing. For example, you can import existing networking information or IP addresses into a new virtual machine configuration.
data "upcloud_ip_addresses" "available_ips" {
zone = "us-nyc1"
}
This flexibility helps you reuse existing cloud resources and dynamically link configurations between new and existing infrastructure components.
Variables and Outputs
Variables let you customize configuration inputs without changing the underlying HCL code. For example, you can define a variable block for a virtual machine plan instead of hardcoding it. This makes your configuration reusable and easier to modify later.
variable "vm_type" {
description = "Virtual machine type or plan"
type = string
default = "2xCPU-4GB"
# Common options: "1xCPU-1GB", "1xCPU-2GB", "2xCPU-4GB", "4xCPU-8GB"
}
You can override this variable in a separate variable file (e.g., tofu.tfvars) or directly within the main configuration:
vm_type = "2xCPU-4GB"
Outputs are used to extract useful information from the infrastructure OpenTofu creates and manages. For example, you can define an output block to display the IPv4 address of the virtual machine you just provisioned.
output "server_ipv4" {
description = "IPv4 address of the server"
value = upcloud_server.web_server.network_interface[0].ip_address
}
Outputs make it easy to retrieve and share important deployment details, especially in automated pipelines or chained environments where one resource output becomes the input for another resource.
Modules: inputs, outputs, and versioning
When you need to create similar infrastructure across multiple environments, such as development, staging, and production, modules are the best way to organize your code. Instead of writing separate configurations for each environment, a module allows you to define a single, reusable block and pass in different environment-specific variables.
Outputs from one module can also serve as inputs for another. For example, the IPv4 address output from a networking module can be passed as an input to a virtual machine module.
Modules can be versioned using Git tags in your repository, allowing you to track and manage different iterations of your code safely and securely. This makes it easier to test changes in staging before promoting them to production.
Here is an example of how a module can be called:
module "network" {
source = "./modules/network"
region = "us-nyc1"
}
module "server" {
source = "./modules/server"
vm_type = "2xCPU-4GB"
network_ip = module.network.server_ipv4
version = "v1.1.0"
}
This setup defines two reusable modules, one for networking and one for servers, while passing the output of the first as input to the second.
State: backends, locking, and drift
The state file is OpenTofu’s record of all the virtual resources you’ve defined and how they map to real infrastructure on your provider. Below is a simplified example of what an OpenTofu state file looks like after provisioning a virtual machine:
{
"version": 4,
"terraform_version": "1.7.0",
"serial": 3,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"outputs": {
"server_ipv4": {
"value": "185.12.34.56",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "upcloud_server",
"name": "web_server",
"provider": "provider[\"registry.opentofu.org/upcloudltd/upcloud\"]",
"instances": [
{
"attributes": {
"id": "00abc123-4567-89de-f012-3456789abcde",
"hostname": "web-server-01",
"zone": "us-nyc1",
"plan": "2xCPU-4GB",
"network_interface": [
{
"type": "public",
"ip_address": "185.12.34.56"
}
],
"template": {
"storage": "Ubuntu Server 22.04 LTS (Jammy Jellyfish)",
"size": 50
}
}
}
]
}
]
}
Each resource entry captures its attributes, metadata, and current state of what actually exists. OpenTofu references this file on every run to detect changes and determine what needs to be created, updated, or destroyed.
The lock file is located alongside the state file and prevents conflicting operations. When commands like tofu apply or tofu destroy run, the lock file temporarily locks the state so that no one else can modify the same infrastructure at the same time. This avoids corruption and conflicting writes between teammates.
Storage options:
You can store both the state file and the lock file locally or remotely. When using remote storage, you can pair object storage with an external lock store. For example you can:
Remote storage is recommended for collaboration, as it provides a shared, versioned, and recoverable source of truth for your team.
Infrastructure drift occurs when your real infrastructure no longer matches the state file. For example, when a resource is deleted manually outside OpenTofu. Drift can be fixed by either deleting the virtual resource from the configuration or importing the manually created resource back into the state file using the tofu import command.
Outside of managing infrastructure drift and preventing conflicting writes, a common challenge with state files is that they grow more complex as your infrastructure expands. They also contain sensitive data about your environment. These issues can quickly become major concerns, so it’s important to find effective ways to address them.
Remote state patterns outline the best practices for managing your OpenTofu state files. Two of the most common challenges are file complexity and security. Let’s examine each one separately.
File complexity
Your state file becomes increasingly complex as your infrastructure grows. The situation becomes even more complicated when tracking resources across multiple environments, including development, staging, and production. One effective way to manage this is by splitting your state across environments. You can configure your backend to point to different paths or keys based on the environment.
s3://my-bucket/app/**dev**/terraform.tfstate
s3://my-bucket/app/**staging**/terraform.tfstate
s3://my-bucket/app/**prod**/terraform.tfstate
For larger setups within a single environment, it can be helpful to divide the state by resource groups, such as networking, databases, and application services. You can then use terraform_remote_state to share outputs between these groups.
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-bucket"
key = "app/prod/networking/terraform.tfstate"
region = "eu-1"
}
}
This pattern keeps state files smaller, easier to manage, and modular across environments or teams.
Security
State files are stored in plain text, often containing sensitive information. It’s vital that encryption is part of your workflow to prevent data exposure in the event of unauthorized access.
Starting from version 1.7.0, OpenTofu introduced end-to-end client-side state encryption, which protects your state regardless of where it’s stored. Data is encrypted before it leaves your machine using a secure passphrase or a key management service (such as AWS KMS or GCP KMS).
Example of OpenTofu state encryption configuration:
terraform {
# Backend configuration (UpCloud Object Storage as an example)
backend "s3" {
bucket = "my-terraform-state"
key = "state/terraform.tfstate"
endpoint = "https://<endpoint>.upcloudobjects.com"
region = "eu-1"
skip_credentials_validation = true
force_path_style = true
}
# Native encryption configuration
encryption {
key_provider "pbkdf2" "mykey" {
passphrase = env.TF_ENCRYPTION_PASSPHRASE
}
method "aes_gcm" "default" {
keys = key_provider.pbkdf2.mykey
}
state { method = method.aes_gcm.default }
plan { method = method.aes_gcm.default }
}
}
This setup encrypts your state and plan files locally before upload, providing consistent protection whether you use UpCloud Object Storage or any other compatible backend.
You now have the “why” and the core concepts: providers, resources, modules, remote state, locking, and client-side encryption. The next step is to apply them.
In the tutorial you will:
Read it here: Build it step by step with OpenTofu on UpCloud.