Deploy and Manage a Guix VM in OpenStack: A Comprehensive Guide

date 2025-05-21 read 11 min tags #guix #infra

Introduction

It's been a few months since I started using Guix as the operating system on my personal laptop, and I've been continuously impressed by its unique approach to system management. Unlike traditional package managers, Guix offers a fully declarative, functional approach to system configuration. This means that your entire system—from packages to services to system parameters—can be defined in code and reproducibly deployed.

What makes Guix particularly powerful is its use of Guile Scheme as the configuration language, offering the full expressiveness of a proper programming language rather than a limited DSL. The ability to treat your system configuration as code, with all the benefits of version control, reproducibility, and atomicity, sets Guix apart from other Linux distributions.

After becoming more comfortable with Guile as a programming language, I decided to extend my Guix experience to the cloud by deploying a virtual machine in OpenStack (through OVHcloud's public cloud offering) and managing it with guix deploy. This post documents my experience and provides a step-by-step guide to help others do the same.

Prerequisites

Before we begin, you'll need:

If you're new to Guix, I recommend first spending some time with the official manual to understand the basic concepts.

Building the QCow2 Image Locally

Creating a QCow2 image with Guix is surprisingly straightforward—comparable to writing a Dockerfile for creating a container image. The process begins with defining an operating-system configuration.

Here's the configuration I used:

;; image.scm
(use-modules (gnu)
             (nongnu packages linux)
             (guix packages)
             (guix build-system copy))

(use-service-modules networking ssh web)
(use-package-modules bootloaders ssh web version-control)

(operating-system
  (kernel linux)
  (firmware (list linux-firmware))
  (host-name "guix-1")
  (locale "en_US.utf8")
  (timezone "Europe/Paris")
  (keyboard-layout (keyboard-layout "fr"))

  (bootloader (bootloader-configuration
               (bootloader grub-bootloader)
               (target "/dev/vda")
               (terminal-outputs '(console))))
  (file-systems (cons (file-system
                        (mount-point "/")
                        (device "/dev/vda1")
                        (type "ext4"))
                      %base-file-systems))
  (services
   (append
    (list
     (service dhcp-client-service-type)
     (service openssh-service-type
              (openssh-configuration
               (openssh openssh-sans-x)
               (permit-root-login #t)
               (password-authentication? #f)  ;; More secure: SSH keys only
               (authorized-keys
                ;; Authorize our SSH key
                `(("root" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))
                  ("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub")))))))
    (modify-services %base-services
      (guix-service-type config =>
                         (guix-configuration
                          (inherit config)
                          (authorized-keys (append (list (local-file "/etc/guix/signing-key.pub")) %default-authorized-guix-keys))))))))

This file declares an operating system that:

- A DHCP client to get the IP automatically - An OpenSSH daemon that authorizes connections to the root user with the public key from my local laptop

With this configuration ready, building the image is as simple as:

guix system image --root=guix-result --image-type=qcow2 image.scm

The --root flag creates a symlink named guix-result that points to the generated artifact.

Since guix-result is a read-only artifact, you'll need to copy it to a writable location if you want to test it locally:

cp guix-result guix.qcow2

You can then start the virtual machine using QEMU:

guix shell qemu -- sudo qemu-system-x86_64 \
  -nic user,model=virtio-net-pci \
  -enable-kvm \
  -m 2048 \
  -device virtio-blk,drive=guix-demo \
  -drive if=none,file=guix.qcow2,id=guix-demo

Uploading the Image and Starting a VM

To work with OpenStack, you'll first need to install the CLI tools:

pip install python-openstackclient

After configuring your OpenStack credentials (typically by sourcing an environment file provided by your cloud provider), you can upload the image:

openstack image create \
  --progress \
  --disk-format qcow2 \
  --container-format bare \
  --file guix-result \
  --property hypervisor_type=kvm \
  --property os_type=linux \
  --property os_admin_user=root \
  --property os_distro=guixsystem \
  --property os_version=unknown \
  guix

Now you can launch a VM from this image. The following command creates a VM named guix-1 with 8GB of memory, connected to the Ext-Net network (which provides public IPs in OVHcloud's OpenStack):

openstack server create \
  --image guix \
  --network Ext-Net \
  --flavor b3-8 \
  --key-name laptop \
  guix-1

After a minute or two, you can check the status and IP address of your VM:

$ openstack server list
+--------------------------------------+--------+--------+--------------------------------------------------+-------+--------+
| ID                                   | Name   | Status | Networks                                         | Image | Flavor |
+--------------------------------------+--------+--------+--------------------------------------------------+-------+--------+
| af308c4e-8064-44c3-8ced-ed7c112478eb | guix-1 | ACTIVE | Ext-Net=2001:41d0:304:300::627d, 217.182.210.193 | guix  | b3-8   |
+--------------------------------------+--------+--------+--------------------------------------------------+-------+--------+

And connect to it:

$ ssh root@217.182.210.193
Last login: Fri May 16 17:50:33 2025 from 81.65.157.5
root@guix-1 ~#

Cloud-Init Integration

One limitation of our initial image is the lack of cloud-init, which means features like automatic disk resizing don't work. Let's address this by integrating cloud-init with our Guix system.

First, update the image.scm file to include cloud-init:

;; Updated image.scm with cloud-init
(use-modules (gnu)
             (nongnu packages linux)
             (gnu packages cloud)
             (guix packages)
             (guix build-system copy))

(use-service-modules networking ssh web cloud)  ;; Added cloud
(use-package-modules bootloaders ssh web version-control)

(operating-system
  ;; Previous configuration...
  
  (services
   (append
    (list
     (service dhcp-client-service-type)
     (service openssh-service-type
              (openssh-configuration
               (openssh openssh-sans-x)
               (permit-root-login #t)
               (password-authentication? #f)
               (authorized-keys
                `(("root" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))
                  ("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))))))
     ;; Add cloud-init service
     (service cloud-init-service-type
              (cloud-init-configuration
               (datasources '("ConfigDrive" "OpenStack" "NoCloud")))))
    ;; Rest of configuration...
    )))

With cloud-init integrated, your VM will now handle cloud-specific tasks automatically, including:

Build the updated image and upload it to OpenStack following the same steps as before.

Managing the VM with guix deploy

One of Guix's most powerful features is guix deploy, which allows you to manage remote systems using the same declarative approach used for local system configuration. This provides a consistent experience across all your machines and simplifies ongoing maintenance.

To use guix deploy, create a deployment configuration file that includes your target machine:

;; deploy.scm
(use-service-modules networking ssh)
(use-package-modules bootloaders ssh)

(define os (load "image.scm"))

(list (machine
       (operating-system os)
       (environment managed-host-environment-type)
       (configuration (machine-ssh-configuration
                       (host-name "217.182.210.193")
                       (system "x86_64-linux")
                       (user "root")
                       (host-key #f)
                       (port 22)))))

This file defines a single machine managed through SSH.

To apply your configuration to the remote machine, simply run:

guix deploy deploy.scm

By default, Guix will build all required packages and system components locally, then transfer them to the server and activate the new configuration.

Extending Your Configuration

Now let's enhance our system by adding a user account, installing additional packages, and setting up a web server:

;; Updated image.scm with additional configuration
(operating-system
  ;; Previous configuration...

  ;; Add a non-root user
  (users (cons* (user-account
                 (name "polyedre")
                 (comment "Polyedre")
                 (group "users")
                 (home-directory "/home/polyedre")
                 (supplementary-groups '("wheel" "netdev" "audio" "video")))
                %base-user-accounts))

  ;; Add some useful packages
  (packages (cons* git
                   curl
                   htop
                   %base-packages))

  ;; Services configuration 
  (services
   (append
    (list
     (service dhcp-client-service-type)
     (service openssh-service-type
              (openssh-configuration
               (openssh openssh-sans-x)
               (permit-root-login #f)  ;; Disable root login for security
               (password-authentication? #f)
               (authorized-keys
                `(("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))))))
     
     ;; Add cloud-init service
     (service cloud-init-service-type
              (cloud-init-configuration
               (datasources '("ConfigDrive" "OpenStack" "NoCloud"))))
     
     ;; Add nginx web server
     (service nginx-service-type
              (nginx-configuration
               (server-blocks
                (list
                 (nginx-server-configuration
                  (listen '("80"))
                  (server-name '("www.polyed.re"))
                  (root (file-union "nginx-root"
                                   `(("index.html" ,(local-file "./index.html"))))))))))
     
     ;; Add a basic firewall
     (service iptables-service-type
              (iptables-configuration
               (ipv4-rules (plain-file "iptables.rules"
                                      "
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp --dport 80 -j ACCEPT
-A INPUT -p tcp --dport 443 -j ACCEPT
COMMIT
"))
               (ipv6-rules (plain-file "ip6tables.rules"
                                      "
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -p ipv6-icmp -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp --dport 80 -j ACCEPT
-A INPUT -p tcp --dport 443 -j ACCEPT
COMMIT
")))))
    
    (modify-services %base-services
      (guix-service-type config =>
                         (guix-configuration
                          (inherit config)
                          (authorized-keys (append (list (local-file "/etc/guix/signing-key.pub")) %default-authorized-guix-keys))))))))

This extended configuration:

  1. Creates a non-root user with sudo privileges 2. Installs additional useful packages 3. Improves security by disabling root SSH login 4. Adds an Nginx web server 5. Configures a basic firewall to control inbound traffic

Deploy the updated configuration with guix deploy deploy.scm, and all these changes will be applied to your VM.

Security Hardening

Security is critical for any public-facing server. Here are some additional security measures to consider:

SSH Hardening

Update your openssh-configuration to include:

(openssh-configuration
 (openssh openssh-sans-x)
 (permit-root-login #f)
 (password-authentication? #f)
 (x11-forwarding? #f)
 (allow-agent-forwarding? #f)
 (max-auth-tries 3)
 (log-level 'verbose)
 (authorized-keys
  `(("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub")))))

Automatic Security Updates

Add a service to periodically update security-critical packages:

(service unattended-upgrade-service-type
         (unattended-upgrade-configuration
          (schedule "0 4 * * *")))  ;; Run at 4 AM daily

System Auditing

Enable system auditing with auditd:

(service auditd-service-type)

Advanced Configuration Examples

Setting Up PostgreSQL Database

Add a PostgreSQL database server to your VM:

(use-service-modules databases)
;; ...

(service postgresql-service-type
         (postgresql-configuration
          (port 5432)
          (locale "en_US.utf8")
          (create-user-groups? #t)
          (authentication
           (list (postgresql-authentication-rule
                  (auth-name "local")
                  (auth-method "peer")
                  (auth-matches '("all")))))
          (extension-packages
           (list postgresql-postgis))))

Setting Up a Reverse Proxy with Let's Encrypt

For secure web services with automatic TLS certificate management:

(use-modules (gnu services certbot))
;; ...

(service nginx-service-type
         (nginx-configuration
          (server-blocks
           (list
            (nginx-server-configuration
             (server-name '("www.polyed.re"))
             (listen '("443 ssl"))
             (ssl-certificate "/etc/letsencrypt/live/www.polyed.re/fullchain.pem")
             (ssl-certificate-key "/etc/letsencrypt/live/www.polyed.re/privkey.pem")
             (root (file-union "nginx-root"
                              `(("index.html" ,(local-file "./index.html")))))
             (locations
              (list
               (nginx-location-configuration
                (uri "/")
                (body '("try_files $uri $uri/ =404;")))))))
          (server-tokens? #f)))

(service certbot-service-type
         (certbot-configuration
          (email "your.email@example.com")
          (webroot "/var/www/certbot")
          (certificates
           (list
            (certificate-configuration
             (domains '("www.polyed.re"))
             (deploy-hook "systemctl reload nginx"))))))

Troubleshooting

Common Issues and Solutions

VM Fails to Boot in OpenStack

  1. Issue: The VM shows as "Error" or "Shut Off" state in OpenStack. Solution: Check the console log for errors: ``bash openstack console log show guix-1 `` Common problems include incorrect bootloader configuration or filesystem issues.
  2. Issue: Network connectivity fails in the VM. Solution: Ensure the DHCP client service is enabled and the network device is correctly configured. Check if the VM is receiving DHCP offers: ``bash # Inside the VM ip addr journalctl -u dhclient ``

SSH Connection Problems

  1. Issue: Cannot connect via SSH. Solution: Verify the security group allows port 22 and check if the SSH service is running: ``bash # From your local machine openstack security group rule list default # Inside the VM (if you can access console) systemctl status sshd ``
  2. Issue: SSH key authentication fails. Solution: Ensure the correct public key is in the authorized_keys file and permissions are correct: ``bash # Inside the VM ls -la ~/.ssh/ chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys ``

Guix Deploy Errors

  1. Issue: "Failed to connect to host" error during deployment. Solution: Verify the hostname/IP and SSH configuration in your deploy.scm file.
  2. Issue: Build failures during deployment. Solution: Try building the failing packages locally first to diagnose the issue: ``bash guix build package-name ``

Comparison with Other Tools

Guix Deploy vs. Ansible

FeatureGuix DeployAnsible
Configuration StyleDeclarativeMostly procedural
LanguageGuile SchemeYAML + Jinja2
ReproducibilityVery strongLimited
RollbacksBuilt-inManual
Learning CurveSteeper (requires Scheme)Gentler
EcosystemSmallerLarger

Guix Deploy vs. Terraform + Configuration Management

While Terraform excels at provisioning cloud infrastructure, Guix Deploy focuses on system configuration. They can be complementary:

The main advantage of Guix is the unified approach—the same language and principles apply from package management to system configuration to deployment.

Conclusion

Deploying and managing Guix VMs in OpenStack offers a powerful, reproducible approach to cloud infrastructure. The declarative nature of Guix provides several key benefits:

  1. Reproducibility: Your entire system configuration is captured in code, making environments easy to reproduce 2. Transactional upgrades and rollbacks: System changes are atomic, and you can easily roll back if something goes wrong 3. Unified approach: The same principles and tools apply from package management to system configuration to deployment 4. Programmability: Using a full programming language (Guile Scheme) for configuration offers great flexibility

While there is a learning curve, particularly if you're not familiar with Scheme, the benefits are substantial for those who value reproducibility and a truly declarative approach to system management.

Guix may not be the most common choice for cloud deployments, but it offers a refreshing alternative to traditional tools. By combining the functional principles of Guix with the flexibility of cloud infrastructure, you can build systems that are both powerful and maintainable.

← back home