Preparation: Setting it all up

OK, dear reader, I have to confess something. I am neither as agnostic nor am I more free with my choices than other hacker out there. Not to say that I am rather prejudiced and already knew what technology I will be using for this project. Sorry if I have lead you on. But - maybe, probably - unlike many others, I can come up with good reasoning for my choices and - attention, here comes the gist of it all - I can argue on why you should at least consider my reasoning for your next project. I will explain as we get going.

In this post I will set up my tooling, set up the project and - of course - explain why I do what in which way.

Environments

One issue in our line of work is the equivalence of environments. How long it takes to pin down a bug that happens in one environment (e.g. production) that does not happen in neither testing, staging, development or any of the others can be tough work of quite some man-hours. We are used to having the "same" tools available among different machines that are obviously not the same: from hardware via kernel all the way through specific versions of libraries and specific builds of their dependencies - managing environments means supply-chain management in software. And it is a mess.

That's why we should spare effort and opt for a toolbox that allows all of that at relative ease while guaranteeing bit-by-bit reproducibility for the whole dependency tree: GNU Guix.

Guix not only offers tools to quickly make software available without needing elevated privileges or clobbering a running system with unneeded programs and libraries, it also allows persisting environments, making them - through a clever, human-readable, text-based abstraction - available to others at ease. And, since (re-)producing environments is such a steal with Guix, running environments in docker environments comes at no cost, so does running whole system definitions in ad-hoc VMs.

I can't imagine anyone trying Guix and not choosing it to build their future upon.

Repositories

Of course, source gets to live in repositories. For this tiny project - but I guess for similar projects of any scale - three repos are needed. The first one of course holds the code of the Python/Django web application. The second one holds a Guix channel, which I will explain in more detail later. The last repository holds documentation material, the "machine" definition (for remote deployment) as well as the text you are reading right now: the blog.

Of course, all could possibly be contained in a mono-repo, but given the nature of the venture there are dependencies (e.g. the channel usually points to the most recent commit of the application's repository) which, in a mono-repo would mean that the most recent commit always points to the previous one. Which is not so nice.

Project Initialization

So, now that we're all set with our tooling, we initialize the Django project:

guix time-machine -C channels.scm -- shell -m manifest.scm
django-admin startproject mitteiler
django-admin startapp sharer

That's it. Now we get to the juicy part. From here on we program, test both unit-wise and interactively (using Django's development server) until we think we want to deploy first versions to the server.

Server setup

As probably mentioned before, Guix does not only allow to bit-by-bit reproduce single files and packages, it also allows to craft whole system definitions from single-file configurations in the same manner. To get an initial system on our VPS up and running we copy one of the bare-bones system definitions of the Guix repository and adjust it to our needs:

(use-modules (gnu))
(use-service-modules networking ssh)
(use-package-modules screen ssh)

(operating-system
  (host-name "dummy")
  (timezone "Europe/Zurich")
  (locale "en_US.utf8")

  (bootloader (bootloader-configuration
               (bootloader grub-bootloader)
               (targets '("/dev/sda"))
               (terminal-outputs '(console))))
  (kernel-arguments (list "console=ttyS0,115200"))
  (initrd-modules (cons* "virtio_scsi" %base-initrd-modules))
  (file-systems (cons (file-system
                        (device "/dev/sda2")
                        (mount-point "/")
                        (type "ext4"))
                      %base-file-systems))
  (services
   (cons*
    (service dhcp-client-service-type)
    (service ntp-service-type)
    (service unattended-upgrade-service-type)
    (service openssh-service-type
             (openssh-configuration
              (openssh openssh-sans-x)
              (permit-root-login #t)
              (port-number 2024)
              (authorized-keys
               `(("root" ,(local-file
                           (string-append (getenv "HOME")
                                          "/.ssh/id_ed25519.pub")))))))

    (modify-services %base-services
      (guix-service-type config =>
                         (guix-configuration
                          (inherit config)
                          (authorized-keys
                           (cons* (local-file "/etc/guix/signing-key.pub")
                                  %default-authorized-guix-keys)))))))

As you may guess from the human-readable definition above, we initialize a basic operating system with an SSH service answering to port 2024 with our personal key added to the root user's profile, so after initialization we can log in and work through ssh.

We build the initial system with this command:

guix system image src/guix/system.scm

and flash it onto the root disk of my VPS (check your VPS host on how to do that, exactly). And voilĂ : we boot into Guix System.

Adding a `machine.scm' file with (more or less) the following content to our workspace:

(use-modules (gnu)
             (teiler system))

(list (machine
       (operating-system %system)
       (environment managed-host-environment-type)
       (configuration (machine-ssh-configuration
                       (host-name "168.119.236.44")
                       (system "x86_64-linux")
                       (user "root")
                       (identity "~/.ssh/id_ed25519.pub")
                       (port 2024)
                       (host-key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIElr3OXvrmS7X7PA1BipPrl/jroTmVduMSK2Pf3IMODY")))))

allows us to deploy new system definitions with:

guix deploy machine.scm

So, kind of like Ansible and tools alike, except that Guix guarantees for system integrity and allows testing (through for example virtualization) before we deploy to production.

The Pipeline

It's somewhat pretentious to call it a pipeline, really, but having the workspace set up like this is all it takes. Defining staging, testing and other environments that track different branches of our repositories is as simple as writing some definitions in a channels.scm file:

(list (channel
       (name 'teil)
       (url "https://git.sr.ht/~gabber/teil-channel")
       (branch "trunk"))
      (channel
       (name 'guix)
       (url "https://git.savannah.gnu.org/git/guix.git")
       (branch "master")
       (introduction
        (make-channel-introduction
         "9edb3f66fd807b096b48283debdcddccfea34bad"
         (openpgp-fingerprint
          "BBB0 2DDF 2CEA F6A8 0D1D  E643 A2A0 6DF2 A33A 54FA")))))

Running guix deploy with a specific machine.scm file deploys for the specific machine. Whether this means a testing, staging, production or other environment does not actually matter.

No shell scripts are being run (as it usually does with Jenkins), no hidden Python libraries are used to evaluate the underlying system (I am looking at you, Ansible) - different stages, bit-by-bit reproducible environment, less headaches.

Last modified: 2025-04-09 Wed 19:08