Developer’s Guide to OpenTofu: Setup, Policy, State

Posted on 20.10.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.

TL;DR

  • OpenTofu is a near drop-in replacement for most Terraform workflows (same HCL, providers, modules). Exceptions: Terraform Cloud/HCP-only features such as remote runs, Sentinel, and private registry.
  • A reliable setup depends on clean modular code, remote state and locking (with S3-compatible backends like UpCloud), and proper provider version management.
  • Migration from Terraform to OpenTofu is straightforward and reversible. You can run both tools side by side for a short time and roll back if needed.

When OpenTofu makes sense (and when not)

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.

Consider OpenTofu when

  • Your platform builds on top of IaC: HashiCorp’s BSL restricts using Terraform as a base for commercial products. OpenTofu, on the other hand, utilizes the Mozilla Public License 2.0, which ensures it will remain open-source and free to use.
  • Community-driven development matters to you: OpenTofu is governed by the Linux Foundation, where community interests take priority over commercial ones. Future decisions, roadmaps, and features will focus on contributors, collaborators, and users rather than profit, which often leads to faster updates and more transparent development.
  • You want to avoid vendor lock-in: OpenTofu is designed to integrate smoothly with a wide range of cloud-native tools and CI/CD pipelines, including GitHub Actions, GitLab CI, and Jenkins. It also supports multiple cloud providers such as UpCloud, AWS, and GCP, giving you the freedom to choose the best combination for your workflow.

Stay on Terraform if

  • Your team relies heavily on Terraform Cloud and its exclusive features.
  • You need a private module registry built directly into your IaC platform.

Or try a hybrid approach

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.

  1. It lets you test OpenTofu’s stability, ecosystem, and community support.
  2. It’s a means to gradually migrate your workspaces without risking downtime or compatibility issues.

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.

Core concepts of OpenTofu

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:

  • S3-compatible backends: use DynamoDB for locking.
  • GCS backend: uses Cloud Datastore for locking.
  • AzureRM backend: uses blob lease locks.
  • Note: locking mechanisms are backend-specific and not interchangeable.

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

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.

Wrap-up

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.

Continue with the hands-on tutorial

In the tutorial you will:

  • Provision an UpCloud VM with the OpenTofu UpCloud provider
  • Store state remotely in an S3-compatible bucket with DynamoDB locking
  • Enable client-side AES-GCM encryption for state and plans
  • Validate changes with Conftest + OPA policies
  • (Optional) Add a GitHub Actions workflow for plan on PR and apply on main
  • (Optional) Prove Terraform compatibility via import

Read it here: Build it step by step with OpenTofu on UpCloud.

Try out today!

Start your free 14-day trial today and discover why thousands of businesses trust UpCloud

  • Risk-free trial
  • Optimized performance
  • Scalable infrastructure
  • Top-tier security
  • Global availability

Sign up

See also

Guide about self-hosted vs Managed Databases

Self-Hosted vs. Managed Databases in 2026: A Guide on Which to Use When

Discover whether to self-host or choose managed databases in 2026 with this comprehensive guide for developers and CTOs.

Kumar Harsh

When Do You Need a Kubernetes Operator? A Practical Guide

Discover when a Kubernetes Operator is essential for effective container orchestration and streamlined application management.

Anita Ihuman

Edge Kubernetes and the Best Managed K8s Providers

Discover the advantages of Edge Kubernetes and explore the best managed K8s providers for optimal performance and reliability.

Anita Ihuman

You're viewing the EU site. Switch to the Global site
Back to top