Today, I find myself migrating from Digital Ocean to Linode (now Akamai Cloud but I’m never gonna really call it that) because, among other things, Digital Ocean recently donated $3 million USD to Omarchy, a slop distro for fascists. Raising money for free and open source software development is hard so it’s pretty cool when some guy who doesn’t even write code anymore gets dump trucks of money for nothing while honest projects compete for peanuts from small grant funding organizations. But anyway!
The Guix cookbook has some documentation about running Guix on Linode. It takes an approach where you start with one of Linode’s built-in Debian (RIP) images and then convert it to a Guix system. As a former devops guy, I found this unsatisfying. What I would really like is to upload a Guix image that is ready to go for use on Linode. I’m gonna save the story and just say: I figured it out.
My Linode disk image does the following:
- Allows use of virtual disks in the initial RAM disk
- Mounts the correct devices for
/and swap - Resizes the root file system upon boot to use all the space in the underlying Linode volume (needed a custom service for this as Guix’s own
resize-file-system-servicedidn’t work for this) - Starts an SSH server that recognizes my public key
- Allows passwordless
sudoso I can useguix deploylater - Installs the bootloader files somewhere that Linode will recognize
- Gets an IPv6 address over DHCP in addition to its IPv4 address
Here’s the code I ended up with:
(define-module (dthompson linode)
#:use-module (gnu)
#:use-module (gnu packages linux)
#:use-module (gnu packages ssh)
#:use-module (gnu services)
#:use-module (gnu services admin)
#:use-module (gnu services networking)
#:use-module (gnu services shepherd)
#:use-module (gnu services ssh)
#:use-module (gnu system linux-initrd)
#:use-module (guix gexp)
#:use-module (guix modules)
#:use-module (guix profiles)
#:use-module (nongnu packages linux)
#:export (%linode-base-services
linode-base-os))
(define ssh-authorized-keys
`(("dave" ,(local-file "../keys/dave.pub"))))
(define guix-signing-keys
(list (local-file "../keys/signing-key.pub")))
;; Ideally we'd just use Guix's resize-file-system-service but it
;; makes some assumptions that do not hold for our Linode setup.
(define (resize2fs-shepherd-service device)
(list (shepherd-service
(provision '(resize2fs))
(requirement '(user-processes))
(one-shot? #f)
(respawn? #f)
(start (with-imported-modules (source-module-closure
'((guix build utils)))
#~(lambda _
(invoke #$(file-append e2fsprogs "/sbin/resize2fs")
#$device))))
(documentation "Resize ext filesystem on boot."))))
(define resize2fs-service-type
(service-type
(name 'resize2fs)
(extensions
(list (service-extension shepherd-root-service-type
resize2fs-shepherd-service)))
(default-value #f)
(description "Resize ext filesystem on boot")))
(define %linode-base-services
(cons*
(service dhcpcd-service-type
(dhcpcd-configuration
;; Linode servers get their IPv6 address via SLAAC and
;; the default setting of "private" doesn't work.
(slaac "hwaddr")))
;; SSH access via public/private key pairs only.
(service openssh-service-type
(openssh-configuration
(password-authentication? #f)
(authorized-keys ssh-authorized-keys)))
;; Automatically resize root filesystem to take up entire allocated
;; space.
(service resize2fs-service-type "/dev/sda")
;; Firewall that blocks nearly everything by default.
(service nftables-service-type)
(modify-services %base-services
;; Allow other Guix machines to push store items over via
;; 'guix deploy'.
(guix-service-type config =>
(guix-configuration
(inherit config)
(authorized-keys
(append guix-signing-keys
%default-authorized-guix-keys)))))))
(define linode-base-os
(operating-system
(locale "en_US.utf8")
(timezone "America/New_York")
(host-name "linode")
(users
(cons (user-account
(name "dave")
(comment "David Thompson")
(group "users")
(home-directory "/home/dave")
(supplementary-groups '("wheel")))
%base-user-accounts))
(sudoers-file
(plain-file "sudoers"
(string-append (plain-file-content %sudoers-specification)
;; 'guix deploy' requires no password
;; sudo capability.
"%wheel ALL=NOPASSWD: ALL\n")))
(packages (cons openssh %base-packages))
(services %linode-base-services)
;; Need virtio_scsi for using virtual disk devices.
(initrd-modules (append '("virtio_scsi") (base-initrd-modules linux)))
;; Guix's default behavior of running grub-install on the boot
;; device doesn't work in the Linode environment. Instead, we
;; just do what Guix does for disk images: Install the font and
;; GRUB modules to the root file system.
(bootloader
(bootloader-configuration
(bootloader
(bootloader
(inherit grub-bootloader)
(installer
#~(lambda (bootloader device mount-point)
(let* ((install-dir (string-append mount-point "/boot"))
(fonts (string-append install-dir "/grub/fonts")))
(mkdir-p fonts)
(copy-file (string-append bootloader "/share/grub/unicode.pf2")
(string-append fonts "/unicode.pf2"))
(copy-recursively (string-append bootloader "/lib/")
install-dir))))))
(targets '("/dev/sda"))))
(file-systems
(cons (file-system
(device "/dev/sda")
(mount-point "/")
(type "ext4"))
%base-file-systems))
(swap-devices (list (swap-space (target "/dev/sdb"))))))
;; Allow for building an initial disk image with 'guix system image'.
(when (batch-mode?) linode-base-os)
This code can also be found in this Git repository.
Now this OS configuration needs to be turned into a usable disk image.
Guix can produce Linode-compatible images using the mbr-raw image
type. Linode images need to be gzip compressed before uploading. I
wrote a script to handle it:
#!/bin/sh
set -e
# First, we need to use Guix to generate a raw disk image that we can
# upload to Linode. Guix's 'mbr-raw' image type gets us most of the
# way there, but not quite. As stated in the name, these images have
# a master boot record. They also have GRUB installed in a post-MBR
# gap. After much trial and error we've discovered that what we want
# for Linode is an image that *only* contains the root partition.
image=$(guix system image -L . --image-type=mbr-raw dthompson/linode.scm)
# Guix places the root partition 1048576 bytes away from the beginning
# of the disk, so we need to skip over all of that when producing the
# final image.
#
# Somewhat arbitrarily, a 128K block size for 'dd' was chosen to speed
# up the operation vs. the default of 512 bytes. 8 blocks of 128K =
# 1048576 bytes, hence the 'skip=8' flag.
#
# Linode also requires uploaded images to be compressed with gzip.
dd if="$image" bs=128K skip=8 conv=sync,noerror | gzip -c > linode-base.gz
Once the disk image is created and uploaded, I can launch a new Linode instance using it. However, Linode’s default configuration profile doesn’t work and the new instance simply kernel panics. First, I stop the server. Then I edit its configuration profile and make the following changes:
- Open the “Select a kernel” dropdown under “Boot Settings” and choose “GRUB 2”
- Turn off all of the filesystem/boot helper switches
Then I save and boot the server. At this point I can use guix deploy over SSH for all OS updates. And that’s it!
It would probably be a good idea to update the Guix Cookbook with this improved process but I don’t have the energy for that right now so hopefully this blog post is helpful in the meanwhile.