Managing Servers with GNU Guix: A Tutorial

The outcome of this year's GSoC is a Guile-based programming interface named guix deploy for automatically creating, upgrading, and changing the configurations of machines running the Guix System. The tool is comparable to Ansible or NixOps, but makes use of the system configuration facilities provided by Guix. A post from earlier this summer described an early version of the programming interface, but we're already a few months into autumn, so it's time for guide on how you can use guix deploy in production.

Simple case: managing a home server

If the machine you need to manage is already running the Guix System, it shouldn't be too hard to incorporate guix deploy into your workflow. All that's needed is the <operating-system> declaration you've been passing to guix system reconfigure and some information about the machine (specifically its IP address and architecture). The guix deploy command is invoked with the filename of a "deployment specification" as an argument, whose contents should look something like this:

;; Module imports
(use-modules (gnu) (guix))
(use-service-modules networking ssh)
(use-package-modules bootloaders)

;; Operating system description
(define os
  (operating-system
    (locale "en_US.utf8")
    (timezone "America/New_York")
    (keyboard-layout (keyboard-layout "us" "altgr-intl"))
    (bootloader (bootloader-configuration
                 (bootloader grub-bootloader)
                 (target "/dev/sda")
                 (keyboard-layout keyboard-layout)))
    (file-systems (cons* (file-system
                          (mount-point "/")
                          (device "/dev/sda1")
                          (type "ext4"))
                         %base-file-systems))
    (host-name "alyssas-home-server")
    (users (cons* (user-account
                   (name "alyssa")
                   (comment "Alyssa")
                   (group "users")
                   (home-directory "/home/alyssa")
                   (supplementary-groups
                    '("wheel" "netdev" "audio" "video")))
                  %base-user-accounts))
    (sudoers-file (plain-file "sudoers" "\
root ALL=(ALL) ALL
%wheel ALL=NOPASSWD: ALL\n"))
    (services (append
               (list (service openssh-service-type
                              (openssh-configuration
                               (permit-root-login #t)))
                     (service dhcp-client-service-type))
               %base-services))))

;; List of machines to deploy
(list (machine
       (operating-system os)
       (environment managed-host-environment-type)
       (configuration (machine-ssh-configuration
                       (host-name "alyssa-p-hacker.tld")
                       (system "i686-linux")
                       (identity "/path/to/ssh-key")))))

Even if Scheme isn't your forté, parts of this should look familiar if you've used Guix before. The "operating system description" section in particular is something you might use with guix system reconfigure. What's new is the last part: We construct a list containing one machine of the managed-host-environment-type, for which we've specified that os is the operating-system declaration that we want to install on it, and that we can connect to it using the parameters specified by the machine-ssh-configuration.

Let's take a step back for a moment and explain what a machine is. guix deploy aims to support a number of different use-cases, which we abstract as "environment types". We'll see other environment types later in this article, but the general idea is that these environments specify how resources should be "provisioned" or created. For example, an environment type designed for working with a Virtual Private Server (VPS) provider might make calls the provider's API to request a virtual machine before installing the machine's operating-system declaration on it.

The environment type used in this example, managed-host-environment-type, is intended for machines that are already running Guix System and are accessible over SSH. It expects that the configuration field of the machine be an instance of machine-ssh-configuration, whose available fields are described in the manual. This gives guix deploy the information it needs to connect to the machine's SSH daemon.

Running guix deploy with this file would build the "operating system closure" of os -- a bundle of the packages, configuration files, and other dependencies necessary to realize that configuration -- for the architecture specified by system (in this case i686-linux), send it over SSH to alyssa-p-hacker.tld, and then remotely "activate" the configuration by creating a new system generation and upgrading running services. Sweet! Upgrading our single server setup has been reduced to an endeavour involving just over a dozen keystrokes.

More advanced case: managing a virtual private server deployment

One server not cutting it for you? guix deploy can still help. Suppose we run a web service that we'd like to split up across multiple machines for performance reasons.

(define %forum-server-count 4)

(define (forum-server n)
  (operating-system
    (host-name (format #f "forum-server-~a" n))
    ...
    (services (append (list (service httpd-service-type
                                     (httpd-configuration
                                      ...)))
                      %base-services))))

(map (lambda (n)
       (machine
        (system (forum-server n))
        (environment digital-ocean-environment-type)
        (configuration (digital-ocean-configuration
                        (region "nyc3")
                        (size "s-1vcpu-1gb")
                        (enable-ipv6 #t)))))
     (iota %forum-server-count))

This example isn't as concrete as the first one; I'm intentionally omitting parts of the configuration to make the example clearer. Here, we automate the creation of %forum-server-count Digital Ocean "droplets" in their NYC3 region by creating a list of 4 machines.

Assuming that the environment variable GUIX_DIGITAL_OCEAN_TOKEN is properly set, running guix deploy with this file will do much of the same as the previous example. The difference is that four virtual machines will be automatically created on Digital Ocean.

One important thing to note about the digital-ocean-environment-type is that, currently, it does not automatically clean up unused virtual machines. If you change something in the deployment specification and run guix deploy again, the virtual machines from the previous deployment will remain until you destroy them yourself.

A quick peek into the internals of digital-ocean-environment-type

It would be an overstatement to say that the process of implementing a new environment type is easy, but a fair amount of the work has already been done for you. We'll use the definition of digital-ocean-environment-type as an example.

(define digital-ocean-environment-type
  (environment-type
   (machine-remote-eval digital-ocean-remote-eval)
   (deploy-machine      deploy-digital-ocean)
   (roll-back-machine   roll-back-digital-ocean)
   (name                'digital-ocean-environment-type)
   (description         "Provisioning of \"droplets\": virtual machines
 provided by the Digital Ocean virtual private server (VPS) service.")))

The environment-type record specifies a small amount of metadata (name and description), as well as the names of three procedures: one for remotely evaluating a G-Expression on the host (machine-remote-eval), one for deploying an operating-system declaration to the host, and one for rolling the host back one generation.

This might sound like a lot, but the pattern for these high-level environment types is to somehow obtain a machine running Guix System, set up an SSH daemon, and then delegate to managed-host-environment-type. digital-ocean-remote-eval is a pretty good example of this:

(define (digital-ocean-remote-eval target exp)
  "Internal implementation of 'machine-remote-eval' for MACHINE instances with
an environment type of 'digital-ocean-environment-type'."
  (mlet* %store-monad ((name (droplet-name target))
                       (network -> (droplet-public-ipv4-network name))
                       (address -> (hash-ref network "ip_address"))
                       (ssh-key -> (digital-ocean-configuration-ssh-key
                                    (machine-configuration target)))
                       (delegate -> (machine
                                     (inherit target)
                                     (environment managed-host-environment-type)
                                     (configuration
                                      (machine-ssh-configuration
                                       (host-name address)
                                       (identity ssh-key)
                                       (system "x86_64-linux"))))))
    (machine-remote-eval delegate exp)))

As you can see, you could reasonably go about implementing an environment type without ever having to learn what a G-Expression is. Here, droplet-name derives the name of the droplet from the machine's operating-system declaration, the information necessary to connect to the droplet is found using droplet-public-ipv4-network, and that's used to create machine of managed-host-environment-type.

In conclusion

I sincerely hope that guix deploy proves to be a useful to anyone dealing with system administration or software development. Transactional upgrades should provide peace of mind to those managing servers (it's worth noting that few existing tools are capable of recovering from failed deployments), and I believe that procedurally-generated deployment configurations could very well be the future of distribution for software such as web services: Imagine if setting up a Mastodon instance were as easy as downloading a Scheme file and handing it off to guix deploy. The ease of writing code that generates code isn't the only benefit of using Guile for something like this. Guile is a general-purpose programming language, so more advanced tooling can reasonably be built atop guix deploy. A GTK or Emacs DevOps interface, perhaps? (If that idea sounds outlandish, consider that the latter has already happened for the package management interface.)

It's been a great summer working alongside everyone in the Guix community. guix deploy is brand new (and a little unstable!), but we've had enthusiastic adoption by several on the mailing lists who were quick to report any issues they found. I'd like to thank everyone on the #guix IRC channel and the mailing lists who got me up to speed with the code, answered my questions, gave feedback when I submitted my patches, and put guix deploy under the pressure of use in production. And of course, I want to thank my mentors Christopher Lemmer Webber and David Thompson. I had to make some hard design decisions, but this was made easier thanks to the guidance of two experienced Guix veterans.

Oh, and this isn't a goodbye. I really feel I've found my place as a Guix contributor, and I can't wait to see what the future will bring for guix deploy. Catch ya on the mailing lists!

Editor's note

Thank you for all of your hard work, Jakob!

About GNU Guix

GNU Guix is a transactional package manager and an advanced distribution of the GNU system that respects user freedom. Guix can be used on top of any system running the kernel Linux, or it can be used as a standalone operating system distribution for i686, x86_64, ARMv7, and AArch64 machines.

In addition to standard package management features, Guix supports transactional upgrades and roll-backs, unprivileged package management, per-user profiles, and garbage collection. When used as a standalone GNU/Linux distribution, Guix offers a declarative, stateless approach to operating system configuration management. Guix is highly customizable and hackable through Guile programming interfaces and extensions to the Scheme language.

Unless otherwise stated, blog posts on this site are copyrighted by their respective authors and published under the terms of the CC-BY-SA 4.0 license and those of the GNU Free Documentation License (version 1.3 or later, with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts).