Opentofu vs Hashicorp Terraform
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