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