Skip to main content

Opentofu vs Hashicorp Terraform

Shen Zhen, China

Previously named OpenTF, OpenTofu is a fork of Terraform that is open-source, community-driven, and managed by the Linux Foundation.

Installation Linux

There is an install script - but I just want to do it manually from the latest Github release:

wget https://github.com/opentofu/opentofu/releases/download/v1.6.1/tofu_1.6.1_linux_amd64.zip
wget https://github.com/opentofu/opentofu/releases/download/v1.6.1/tofu_1.6.1_SHA256SUMS
sha256sum tofu_1.6.1_linux_amd64.zip
sha256sum -c tofu_1.6.1_SHA256SUMS

> tofu_1.6.1_linux_amd64.zip: OK
unzip tofu_1.6.1_linux_amd64.zip
rm tofu_1.6.1_linux_amd64.zip

sudo mv tofu /usr/bin/tofu
tofu version
> OpenTofu v1.6.1
> on linux_amd64

Get Started - Docker

I will start where the Terraform Docker hello world example left off:

./main.tf

terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.1"
}
}
}

provider "docker" {}

resource "docker_image" "nginx" {
name = var.ingress_image_version
keep_locally = false
}

resource "docker_container" "nginx" {
image = docker_image.nginx.image_id
name = var.ingress_container_name
ports {
internal = var.ingress_http_internal
external = var.ingress_http_external
}
}

./variables.tf

variable "ingress_image_version" {
description = "Version of the NGINX ingress image"
type = string
}

variable "ingress_http_external" {
description = "External http port of the NGINX ingress"
type = number
}

variable "ingress_http_internal" {
description = "Internal http port of the NGINX ingress"
type = number
}

variable "ingress_container_name" {
description = "Name of the NGINX ingress container"
type = string
}

./terraform.tfvars

ingress_image_version="nginx:latest"
ingress_http_external=8888
ingress_http_internal=80
ingress_container_name= "ingress"

./outputs.tf

output "container_id" {
description = "ID of the Docker container"
value = docker_container.nginx.id
}

output "image_id" {
description = "ID of the Docker image"
value = docker_image.nginx.id
}

Running the App

tofu fmt
> outputs.tf
> terraform.tfvars
> variables.tf
tofu init
> OpenTofu has been successfully initialized!
tofu validate
> Success! The configuration is valid.
tofu apply
> Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

> Outputs:
> container_id = "ba637c54433152fa02351c5afe28d44feb376012e01b64e61d38b0788ba5bbf5"
> image_id = "sha256:a8758716bb6aa4d90071160d27028fe4eaee7ce8166221a97d30440c8eac2be6nginx:latest"

Inspect the current state using:

terraform show
terraform state list
> docker_container.nginx
> docker_image.nginx

Run docker ps to view the NGINX container running in Docker via Terraform:

docker ps

CONTAINER ID IMAGE STATUS PORTS NAMES
ba637c544331 a8758716bb6a Up 54 seconds 0.0.0.0:8888->80/tcp ingress
curl localhost:8888

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
tofu destroy
Destroy complete! Resources: 2 destroyed.

Real World

ntfy Server

Let's try to deploy ntfy using OpenTofu. The Docker-Compose file for it looks like this:

docker-compose.yml

version: "3"

services:
ntfy:
image: binwiederhier/ntfy
container_name: ntfy
command:
- serve
environment:
- TZ=UTC # optional: set desired timezone
user: 1002:1002 # replace with the user/group or uid/gid
volumes:
- ./cache:/var/cache/ntfy
- ./config:/etc/ntfy
- ./db:/var/lib/ntfy/
ports:
- 8000:80 # exposed on port 8000 (you can change it)
restart: unless-stopped

./variables.tf

variable "image_version" {
description = "Version of the ntfy Docker image"
type = string
}

variable "container_name" {
description = "Name of the ntfy Docker container"
type = string
}

variable "http_internal" {
description = "Internal http port of the nfty service"
type = number
}

variable "http_external" {
description = "External http port of the nfty service"
type = number
}

./terraform.tfvars

image_version  = "binwiederhier/ntfy:latest"
container_name = "ntfy"
http_internal = 80
http_external = 8080

./main.tf

terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.1"
}
}
}

provider "docker" {}

resource "docker_image" "ntfy" {
name = var.image_version
keep_locally = false
}

resource "docker_container" "ntfy" {
image = docker_image.ntfy.image_id
name = var.container_name
user = "1002:1002"
start = true
must_run = true
restart = "unless-stopped"

ports {
internal = var.http_internal
external = var.http_external
}

command = [
"serve"
]

env = [
"TZ=UTC"
]

upload {
file = "/var/cache/ntfy/cache.db"
source = "./cache/cache.db"
}

upload {
file = "/var/lib/ntfy/user.db"
source = "./lib/user.db"
}

upload {
content = <<EOF
base-url: "http://192.168.2.112"
listen-http: ":80"
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"
auth-file: /var/lib/ntfy/user.db
auth-default-access: "deny-all"
behind-proxy: false
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "5G"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"
web-root: /ntfy
enable-signup: false
enable-login: true
enable-reservations: true
log-level: info
log-format: json
log-file: /var/log/ntfy.log
EOF

file = "/etc/ntfy/server.yml"
}

healthcheck {
test = ["CMD", "curl", "-f", "http://localhost:8080/ntfy"]
interval = "15s"
timeout = "2s"
retries = 5
}
}

I was not able to get the volume mounts to work. I guess that Terraform handles the file system similar to Nomad - which means that you would have to configure Terraform to access the file system instead of being locked in inside the Docker overlay. But I cannot find any documentation for it.

So I replaced all volume mounts with file uploads. Note that you have to start ntfy once and create your users, subscriptions and permissions. Then copy the cache.db and user.db to where you want to Terraform to pick them up on your next deployment.

Run the Container

tofu init
tofu fmt
tofu validate
tofu apply
tofu destroy