OpenTofu overview: Installation, migration from Terraform, and key features

OpenTofu is a community fork of Terraform, the most famous IaC tool in the world. It was created in response to a controversial change in HashiCorp’s licensing policy and became one of the Linux Foundation projects backed by numerous companies. As you may recall, HashiCorp has adopted Business Source License (BSL) v1.1 for future releases of their core products, diverging away from a fully Open Source model.

In this article, I will take a closer look at OpenTofu v1.7, released on April 30th, 2024. Specifically, I will install it, migrate a simple infrastructure from Terraform, and go over the key features of this version.

Installing OpenTofu

The OpenTofu distribution is available for a number of platforms. In our case, we are going to install it on macOS using the Homebrew package manager:

$ brew update
$ brew install opentofu

Ensure that OpenTofu has been installed correctly:

$ tofu -version
OpenTofu v1.7.0
on darwin_arm64

Migrating from Terraform

Now, let’s migrate the Terraform resources. We will use AWS as an example, yet the following instructions are pretty basic and may be easily applied to other cloud/infrastructure providers of your choice.

Our infrastructure will include a separate VPC, two virtual machines, and a DNS zone with two A records:

The provider configuration looks like this:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

The official docs describe the Terraform-to-OpenTofu migration process very well. Depending on the Terraform version you used to deploy your original infrastructure, you might need to migrate to a corresponding OpenTofu version. For example, you need OpenTofu 1.6 for Terraform 1.5.x and 1.6.x deployments, but OpenTofu 1.7 for Terraform 1.7.x and 1.8.x.

Our target infrastructure was deployed using Terraform 1.8.3, so OpenTofu 1.7 will do just fine. Here’s what we need to migrate to OpenTofu:

1. Ensure that the Terraform state matches the infrastructure:

$ terraform plan
[..]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

2. Apply the configuration using terraform apply.

3. Create a backup for your Terraform state file.

4. Make changes to your Terraform code as suggested in the migration plan linked above, i.e.:

  • S3 backend:
    • Remove the skip_s3_checksum option for the S3 backend if you use it.
    • Remove the endpointssso option and the AWS_ENDPOINT_URL environment variable if you use them. Ensure that your code still works as intended.
  • Removed blocks:
    • To perform the migration, the lifecycle block must be deleted. If the lifecycledestroy = true option is used, the entire removed block must be deleted.
  • Testing changes:
    • There is currently no support for the mock provider (yet it should emerge very soon). Thus, you need to revise the tests if mock_provider is used in your code. You’ll have to do the same for override_resource, override_data, and override_module.

5. Once you have made the necessary code changes, initialize OpenTofu:

$ tofu init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.48.0...
- Installed hashicorp/aws v5.48.0 (signed, key ID 0C0AF313E5FD9F80)

[..]

OpenTofu has been successfully initialized!

[..]

6. Do resource planning:

$ tofu plan
aws_vpc.main: Refreshing state... [id=vpc-abe54f4e421hc41a2]
[..]

No changes. Your infrastructure matches the configuration.

OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

7. Once OpenTofu says that there are no changes, we are ready to apply the configuration:

$ tofu apply
[..]

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

vm_1_address = "<IP-1>"
vm_1_name = "vm-1"
vm_2_address = "<IP-2>"
vm_2_name = "vm-2"

8. The migration is complete! Enjoy using OpenTofu for your infrastructure. As a final touch, we can also compare the Terraform state we backed up earlier with the current state:

❯ diff state tofu_state
3,4c3,4
<   "terraform_version": "1.8.3",
<   "serial": 4,
---
>   "terraform_version": "1.7.0",
>   "serial": 5,
29c29
<       "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
---
>       "provider": "provider[\"registry.opentofu.org/opentofu/aws\"]",
59c59
<       "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
---
>       "provider": "provider[\"registry.opentofu.org/opentofu/aws\"]",
[..]

There should be no surprises: all the difference boils down to the terraform_version and the providers’ registry addresses since OpenTofu uses its own registry.

While this OpenTofu migration was simplistic and made solely for illustrative purposes, you can be sure that real-world migrations from Terraform are happening, and sometimes, they are huge. Here’s one example Matt Gowie from Masterpoint shared in February 2024:

Key OpenTofu features

Now, we’re ready to take a practical look at the features that have been introduced in OpenTofu 1.7.

Terraform state encryption

Encryption is done either through the passphrase or through cloud storage tools, such as AWS KMS, GCP KMS, OpenBao, and so on. To encrypt the existing Terraform state using the passphrase, add the following configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  encryption {
    method "unencrypted" "migrate" {}

    key_provider "pbkdf2" "mykey" {
      passphrase = "my1unique2secure3phrase4"
    }

    method "aes_gcm" "new_method" {
      keys = key_provider.pbkdf2.mykey
    }

    state {
      method = method.aes_gcm.new_method

      fallback {
        method = method.unencrypted.migrate
      }
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

Applying this will encrypt the Terraform state. You can see it for yourself by executing:

$ cat terraform.tfstate

To decrypt the file, modify the configuration by specifying fallback as the encryption method and add the unencrypted method to state:

terraform {
  [..]

  encryption {

    [..]

    state {
      method = method.unencrypted.migrate

      fallback {
        method = method.aes_gcm.new_method
      }

      enforced = false     
    }
  }
}

[..]

Applying the modified configuration will cause the Terraform state file to revert back to plain text.

Support for dynamic provider-defined functions

On top of adding support for dynamic provider-defined functions, the OpentTofu developers have created their own Lua and Go providers. These now enable you to write custom functions in Go and Lua right in the Terraform configuration.

Here’s an example of sorting IPv4/IPv6 and MAC addresses using Go in a Terraform configuration:

1. Add the Go provider to required_providers:

required_providers {
  aws = {
    source  = "opentofu/aws"
    version = "~> 5.0"
  }
  go = {
    source = "registry.opentofu.org/opentofu/go"
    version = "0.0.3"
  }
}

2. Create a network.go file next to the configurations and define the sorting functions in it:

package lib

import (
	"bytes"
	"net"
	"sort"
)

func Sort_IP(ips []string) []string {
	parsed := make([]net.IP, len(ips))

	for i, ip := range ips {
		parsed[i] = net.ParseIP(ip)
	}

	sort.Slice(parsed, func(i, j int) bool {
		return bytes.Compare(parsed[i], parsed[j]) < 0
	})

	result := make([]string, len(ips))

	for i, ip := range parsed {
		result[i] = ip.String()
	}

	return result
}

func Sort_Mac(macs []string) ([]string, error) {
	parsed := make([]net.HardwareAddr, len(macs))

	var err error

	for i, mac := range macs {
		parsed[i], err = net.ParseMAC(mac)
		if err != nil {
			return nil, err
		}
	}

	sort.Slice(parsed, func(i, j int) bool {
		return bytes.Compare(parsed[i], parsed[j]) < 0
	})

	result := make([]string, len(macs))

	for i, mac := range parsed {
		result[i] = mac.String()
	}

	return result, nil
}

3. In network.tf, add the required section to invoke the provider-defined function:

provider "go" {
  alias = "net"
  go = file("./network.go")
}

locals {
  target_ips = [
    "100.100.0.1",
    "192.168.2.2",
    "192.168.1.2",
    "192.168.2.3",
    "10.0.10.10",
  ]
  target_macs = [
    "fe:df:43:af:3c:74",
    "bb-67-2c-b2-30-fe",
    "5c-7a-be-cc-a1-1a",
  ]

  sorted_ips = provider::go::net::sort_ip(local.target_ips)
  sorted_macs = provider::go::net::sort_mac(local.target_macs)
}

resource "local_file" "ip_configs" {
  filename = "./ip.txt"
  content = join("\n", local.sorted_ips)
}

resource "local_file" "mac_configs" {
  filename = "./mac.txt"
  content = join("\n", local.sorted_macs)
}

4. Run the tofu plan command:

OpenTofu will perform the following actions:

  # local_file.ip_configs will be created
  + resource "local_file" "ip_configs" { 
      + content              = <<-EOT
	        10.0.10.10
	        100.100.0.1
	        192.168.1.2
	        192.168.2.2
	        192.168.2.3
        EOT
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5 = (known after apply)
      + content_sha1 = (known after apply)
      + content_sha256 = (known after apply)
      + content_sha512 = (known after apply)
      + directory_permission = "0777"
      + file_permission = "0777"
      + filename = "./ip.txt"
      + id = (known after apply) 
    }

  # local_file.mac_configs will be created
  + resource "local_file" "mac_configs" {
      + content              = <<-EOT
            5c:7a:be:cc:a1:1a
            bb:67:2c:b2:30:fe
            fe:df:43:af:3c:74
        EOT
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      [..]

As you can see, it is now possible to invoke functions written in Go right in the Terraform configuration. For more details on provider-defined functions, refer to the Native Lua (+more) in OpenTofu 1.7.0 video.

Support for removed blocks

This allows you to remove resources from the state file and configuration while keeping them in the deployed infrastructure. For example, let’s remove the aws_instance.vm_2. To do this, add the following removed block:

removed {
  from = aws_instance.vm_2
}

During planning, OpenTofu will flag the resource as ready to be removed from the Terraform state without actually deleting it from the infrastructure.

Note: It was precisely the code for this feature that caused HashiCorp to accuse the OpenTofu developers of plagiarism. Luckily, the answer from OpenTofu was wholly compelling.

Loopable import blocks

This feature allows you to use the for_each condition to import resources into Terraform. See the code snippet below (provided by the developers) to learn how to use it.

variable "server_ids" {
  type = list(string)
}

resource "random_id" "test_id" {
  byte_length = 8
  count = 2
}

import {
  to = random_id.test_id[tonumber(each.key)]
  id = each.value
  for_each = {
    for idx, item in var.server_ids: idx => item
  }
}

output "id" {
  value = random_id.test_id.*.b64_url
}

Running this will result in a randomly generated sequence of IDs:

Outputs:

id = [
  "6Lgv5nUSpvt",
  "w5txF9kO4Xv",
]

Note that when using the for_each construct, you cannot generate configurations using the -generate-config-out flag:

$ tofu plan -generate-config-out=gen.tf
var.vm_ids
 Enter a value: [..]

╷
│ Error: Configuration generation for count and for_each resources not supported

More new features

While this article was written based on the OpenTofu v1.7.x releases, the developers are getting v1.8.0 ready for production users. Its first alpha was released at the end of June, and as of today, we have v1.8.0-rc1 available.

This update will bring more features to OpenTofu users, including:

  • Variables and locals in module sources and backend configurations.
  • Support for new .tofu file extensions enabling OpenTofu-specific overrides of .tf files.
  • Support for the abovementioned override_resource / override_data / override_module and mock_provider / mock_resource / mock_data blocks in the testing framework.

To keep informed on further changes, refer to the project’s milestones.

Afterword

While OpenTofu was created to be fully compatible with Terraform, new extra features make it different, and eventually, these tools would deviate only more. As the developers say, this compatibility for future versions is a matter of community preference.

A number of prominent Terraform users have already opted for OpenTofu for their infrastructure management. For example, VMware Tanzu switched to OpenTofu, Oracle migrated its EBS Cloud Manager, and OpenSteetMap moved its cloud services management.

However, despite the worldwide community’s vast endorsement and support for OpenTofu, an alternative take on this project exists as well. Particularly, Cristian Măgherușan-Stanciu claims that OpenTofu is developing at a slower pace than Terraform, based on the analysis of the activity in their repositories. He found that the bulk of the commits made in the OpenTofu repo over the last year and a half can be attributed to a mere six developers. Moreover, their activity peaked around the time OpenTofu was announced.

Still, OpenTofu is a ready-to-use and promising Open Source alternative to Terraform. We’re excited to see its emergence and have no doubt it will thrive and contribute a lot to the modern IaC and DevOps ecosystem. Feel free to share your own experience using OpenTofu in the comments section below!

Comments

Your email address will not be published. Required fields are marked *