FreeBSD on Hetzner Cloud
So I like weird, Linux is cool and everything, but I’ve always gravitated to FreeBSD for personal projects. I like the fact the FreeBSD team build the entire OS from the kernel to the userland tools. It’s cohesive as a system and the handbook is unlike any other resource for configuring an OS. (Ok maybe the ArchWiki comes close)
I’m also rather fond of NixOS so the similarity of chucking config settings into /etc/rc.conf is also cool.
Anyway, rambling. FreeBSD provide a VM image with ZFS for root and BASIC-CLOUDINIT which I’ve yet to figure out. That’s for next time.
I adapted a borrowed Packer build for Talos that I was messing with on Hetzner for k8s a while ago. This build spins up a little instance, reboots it into the Hetzner rescue system then wget’s the FreeBSD arm/x86 image and dd’s it to the instances /dev/sda disk. It’ll then take a snapshot you can use to spin up Hetzner Cloud servers.
Setup & Build
You’ll need to add a valid HCLOUD_TOKEN to your env
export HCLOUD_TOKEN="blah"
Then init the packer build to pull down the hcloud plugin
packer init freebsd-hcloud.pkr.hcl
To build both the ARM and x86 images simultaneously run
packer build freebsd-hcloud.pkr.hcl
To build just the ARM snapshot:
packer build -only hcloud.freebsd-arm freebsd-hcloud.pkr.hcl
freebsd-hcloud.pkr.hcl
packer {
required_plugins {
hcloud = {
version = "~> v1.6.0"
source = "github.com/hetznercloud/hcloud"
}
}
}
variable "freebsd_version" {
type = string
default = "15.0-RELEASE"
}
variable "image_url_arm" {
type = string
default = null
}
variable "image_url_x86" {
type = string
default = null
}
variable "server_location" {
type = string
default = "hel1"
}
locals {
timestamp = formatdate("YYYY-MM-DD-hh-mm", timestamp())
image_arm = var.image_url_arm != null ? var.image_url_arm : "https://download.freebsd.org/releases/VM-IMAGES/${var.freebsd_version}/aarch64/Latest/FreeBSD-${var.freebsd_version}-arm64-aarch64-BASIC-CLOUDINIT-zfs.raw.xz"
image_x86 = var.image_url_x86 != null ? var.image_url_x86 : "https://download.freebsd.org/releases/VM-IMAGES/${var.freebsd_version}/amd64/Latest/FreeBSD-${var.freebsd_version}-arm64-aarch64-BASIC-CLOUDINIT-zfs.raw.xz"
# Add local variables for inline shell commands
download_image = "wget --timeout=5 --waitretry=5 --tries=5 --retry-connrefused --inet4-only -O /tmp/freebsd.raw.xz "
write_image = <<-EOT
set -ex
echo 'FreeBSD image loaded, writing to disk... '
xz -d -c /tmp/freebsd.raw.xz | dd of=/dev/sda && sync
echo 'done.'
EOT
clean_up = <<-EOT
set -ex
echo "Cleaning-up..."
rm -rf /etc/ssh/ssh_host_*
EOT
}
source "hcloud" "freebsd-arm" {
rescue = "linux64"
image = "debian-13"
location = "hel1"
server_type = "cax11"
ssh_username = "root"
snapshot_name = "FreeBSD ${var.freebsd_version} ARM - ${local.timestamp}"
snapshot_labels = {
os = "freebsd",
version = var.freebsd_version,
arch = "arm",
}
}
source "hcloud" "freebsd-x86" {
rescue = "linux64"
image = "debian-13"
location = "hel1"
server_type = "cx22"
ssh_username = "root"
snapshot_name = "FreeBSD ${var.freebsd_version} x86 - ${local.timestamp}"
snapshot_labels = {
os = "freebsd",
version = var.freebsd_version,
arch = "x86",
}
}
build {
sources = [
"source.hcloud.freebsd-arm",
"source.hcloud.freebsd-x86",
]
provisioner "shell" {
inline = ["${local.download_image}${local.image_arm}"]
only = ["source.hcloud.freebsd-arm"]
}
provisioner "shell" {
inline = ["${local.download_image}${local.image_x86}"]
only = ["source.hcloud.freebsd-x86"]
}
provisioner "shell" {
inline = [local.write_image]
}
provisioner "shell" {
inline = [local.clean_up]
}
}
Terraform
To then use the snapshot created with packer here’s some example terraform
data "hcloud_image" "freebsd" {
with_selector = "os=freebsd"
with_architecture = "arm"
most_recent = true
}
resource "hcloud_server" "this" {
name = "freebsd"
image = data.hcloud_image.freebsd.id
server_type = "cax11"
location = "hel1"
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
<snip>
}