chaudron

Infrastructure as plain Guile programs.

A deployment is a Scheme script. Each resource call converges the real thing, a server, a DNS record, a whole Guix System, and hands back its outputs. No CLI, no plan to apply, no daemon.

deploy.scm
(use-modules (chaudron)
             (chaudron hetzner)
             (chaudron cloudflare))

(with-stage "prod"
  (define forge
    (hetzner-server "forge" #:type "cx23"
                            #:location "fsn1"))

  (cloudflare-dns-record "git"
                         #:zone "example.com"
                         #:content (output forge 'ipv4)))
$ guile deploy.scm
chaudron: create hetzner-server/forge
chaudron: create cloudflare-dns-record/git

One language, from the cloud to the system

Infrastructure usually takes two tools: one for the servers and the DNS, another, with its own language, to configure the machines. Here both are Scheme, in the same program.

  • The machines are declared too

    With Guix System, a server is not configured step by step: it is described whole. Deploying the same description gives the same system, and each deployment can be rolled back.

  • Procedures, not templates

    A website, its server, its DNS record and its system are one procedure. Two websites are two calls, with loops and modules for the rest.

  • Code is data

    The system of a machine is a Scheme expression: the program builds it, with its values in place, rather than filling text templates.

  • Everything pinned

    The versions of Guix, of chaudron and of every package on the servers are fixed by the project's lock: the same code, the same result.

deploy.scm
(define (website name key)
  (let* ((domain (string-append name ".example.com"))
         (server (hetzner-server name #:type "cx23"
                                      #:location "fsn1"
                                      #:ssh-keys (list key))))
    (cloudflare-dns-record name #:zone "example.com"
                           #:content (output server 'ipv4))
    (hetzner-guix-system name
                         #:server server
                         #:os (write-system! name domain)
                         #:ssh-key key)))

(website "blog" key)
(website "shop" key)
the system, built by the program
(define (system-of domain)
  `(operating-system
     (inherit %base)
     (services
      (cons* (service nginx-service-type
               (nginx-configuration
                (server-blocks
                 (list (nginx-server-configuration
                        (server-name (list ,domain))
                        (listen '("443 ssl")))))))
             (service certbot-service-type
               (certbot-configuration
                (certificates
                 (list (certificate-configuration
                        (domains (list ,domain)))))))
             (operating-system-user-services %base)))))

Why Scheme, with the whole example

What a run does

Running the script is the deployment. A stage is one environment, like "prod", and its body is ordinary Guile: loops, procedures and modules all work.

  1. Load the stage

    with-stage reads what chaudron knows about the stage from .chaudron/prod/: one small file per resource.

  2. Converge each call, in order

    Each resource call looks at the real resource, brings it back if it drifted, then creates, updates or replaces it, and saves its state right away. Wrap independent calls in concurrently to run them in parallel.

  3. Delete what is gone

    When the body returns, resources the script no longer declares are deleted, each before what it depends on. If the body raised an error, nothing is deleted.

Nothing hidden

Everything chaudron knows and does is plain text you can read, diff and commit.

The state is data

One s-expression per resource. It is read, never evaluated.

.chaudron/prod/hetzner-volume/data.scm
((kind . hetzner-volume)
 (name . "data")
 (seq . 4)
 (props (name . "data-prod")
        (size . 10)
        (location . "fsn1")
        (format . "ext4")
        (labels)
        (protected? . #f))
 (outputs
  (id . 104729301)
  (linux-device . "/dev/disk/by-id/scsi-0HC_Volume_104729301"))
 (depends-on))

Dry runs say what would change

The same script with #:dry-run? #t reads the real resources and changes nothing.

dry run
chaudron: create hetzner-volume/data (dry run)
chaudron: update hetzner-server/forge (volumes) (dry run)
chaudron: delete hetzner-firewall/web (dry run)

Drift is reported, then fixed

A change made by hand in the console is undone by the next run, out loud.

drift
chaudron: hetzner-server/forge has drifted: type is "cx33", expected "cx23"
chaudron: update hetzner-server/forge (type)

From a bare server to guix deploy

Give hetzner-guix-system a server and an operating-system file. The first run installs Guix System on it; later runs deploy again whenever the file, or the Guix you run, changes.

  1. Boots the server into Hetzner's rescue system.
  2. Erases its disk and installs a minimal Guix System.
  3. Deploys your configuration with guix deploy.
deploy.scm
(hetzner-guix-system "forge"
                     #:server forge
                     #:os "forge-os.scm"
                     #:ssh-key key)

Providers

Hetzner Cloud
  • hetzner-server
  • hetzner-ssh-key
  • hetzner-firewall
  • hetzner-primary-ip
  • hetzner-volume
  • hetzner-network
  • hetzner-load-balancer
  • hetzner-snapshot
Guix System
  • hetzner-guix-system
Cloudflare
  • cloudflare-dns-record
  • cloudflare-worker
  • cloudflare-kv-namespace
  • cloudflare-d1-database
Livebox experimental
  • livebox-port-forward
  • livebox-dhcp-lease

Write your own

A provider is a module with four procedures, of which only create and delete are required. Props and outputs are association lists. Without update, any change replaces the resource.

acme.scm
(define thing-provider
  (register-provider!
   (make-provider 'acme-thing
                  #:create (lambda (name props) ...)
                  #:read   (lambda (outputs) ...)
                  #:update (lambda (outputs old new) ...)
                  #:delete (lambda (outputs) ...)
                  #:replace-on '(region))))

Get started

With Guix, a project pins chaudron in its own channels.scm and runs it with guix time-machine: nothing to install. On Debian and Ubuntu, apt has all it needs.

Installation ยท Your first deployment

chaudron is young: its interface may still change.

shell
$ guix time-machine -C channels.scm -- \
    describe -f channels > channels.lock.scm
$ guix time-machine -C channels.lock.scm -- \
    shell guile chaudron
$ guile deploy.scm