From 60b96a6acd3870e48f8f60b8bb95cd806fefe406 Mon Sep 17 00:00:00 2001
From: Collin J. Doering
Date: Thu, 10 Oct 2024 12:56:57 -0400
Subject: Early draft of blog article
---
docs/blog-Tennnessee-build-farm.org | 499 ++++++++++++++++++++++++++++++++++++
1 file changed, 499 insertions(+)
create mode 100644 docs/blog-Tennnessee-build-farm.org
diff --git a/docs/blog-Tennnessee-build-farm.org b/docs/blog-Tennnessee-build-farm.org
new file mode 100644
index 0000000..8b3ede9
--- /dev/null
+++ b/docs/blog-Tennnessee-build-farm.org
@@ -0,0 +1,499 @@
+#+TITLE: Setup of a simple Guix build farm and substitute server
+#+AUTHOR: Collin J. Doering
+
+A few months ago [[https://lists.gnu.org/archive/html/guix-devel/2024-07/msg00033.html][I announced on the guix mailing list]] that there was a new North American
+based Guix substitute server and build farm, cuirass.genenetwork.org. This article provides
+further information about how the build farm and substitute server was setup, and how you can
+do so for yourself or your organization. Having more Guix substitutes servers available
+improves build diversity (which can be checked with [[https://guix.gnu.org/manual/en/html_node/Invoking-guix-challenge.html][guix challenge]]), as well as substitute
+availability and improved response times due to server locality.
+
+* TODO note inspiration, and in cases direct copy from https://git.savannah.gnu.org/cgit/guix/maintenance.git/tree/hydra/berlin.scm
+
+* Setting up a Minimal Guix Build Farm and Substitute Server
+
+Though a Guix build farm and substitute server could be deployed on any distribution, we
+naturally chose to use Guix itself. There are a variety of components that provide the
+necessary functionality:
+
+- [[https://guix.gnu.org/cuirass/][Cuirass]] :: Watches the the guix channel repository for changes, and manages building of
+ derivations, packages, etc..
+- [[https://guix.gnu.org/manual/en/html_node/Invoking-guix-publish.html][guix-publish]] :: Provides substitute archives for consumption by users (indirectly via nginx
+ as a local proxy).
+- nginx :: Acts as a reverse proxy for Cuirass and guix-publish.
+- certbot :: Fetches ssl certificates so cuirass and substitutes can be served over https.
+- anonip :: Anatomizes http logs to preserver user privacy.
+
+How each of these components are setup is detailed below, component-by-component. You can see
+the full source-code for the Tennessee build farm at
+https://git.genenetwork.org/guix-north-america/.
+
+** Cuirass - building packages
+
+*** Define Cuirass Specs
+
+#+begin_src scheme
+ (define %cuirass-specs
+ #~(list (specification
+ (name "guix")
+ (priority 0)
+ (build '(channels guix))
+ (channels %default-channels))))
+#+end_src
+
+*** Setup Cuirass Service
+
+#+begin_src scheme
+ (service cuirass-service-type
+ (cuirass-configuration
+ (host "localhost")
+ (specifications %cuirass-specs)))
+#+end_src
+
+** Provide Substitutes using Guix Publish
+
+#+begin_src scheme
+ (service guix-publish-service-type
+ (guix-publish-configuration
+ (port 3000)
+ (cache "/var/cache/guix/publish")))
+#+end_src
+
+** Anonomize IPs (anonip)
+
+#+begin_src scheme
+ (define (anonip-service file)
+ (service anonip-service-type
+ (anonip-configuration
+ (input (format #false "/var/run/anonip/~a" file))
+ (output (format #false "/var/log/anonip/~a" file)))
+
+ (define %anonip-log-files
+ ;; List of files handled by Anonip
+ '("http.access.log"
+ "https.access.log"))
+
+ (define (log-file->anonip-service-name file)
+ "Return the name of the Anonip service handling FILE, a log file."
+ (symbol-append 'anonip-/var/log/anonip/ (string->symbol file)))
+#+end_src
+
+** Certbot
+
+#+begin_src scheme
+ (define* (le host #:optional privkey)
+ (string-append "/etc/letsencrypt/live/"
+ host "/"
+ (if privkey "privkey" "fullchain")
+ ".pem"))
+#+end_src
+
+** Nginx Reverse Proxy
+
+*** abc
+
+#+begin_src scheme
+ (define publish-robots.txt
+ ;; Try to prevent good-faith crawlers from downloading substitutes. Allow
+ ;; indexing the root—which is expected to be static or cheap—to remain visible
+ ;; in search engine results for, e.g., 'Guix CI'.
+ "\
+ User-agent: *\r
+ Disallow: /\r
+ Allow: /$\r
+ \r
+ ")
+#+end_src
+
+#+begin_src scheme
+ (define (publish-locations url)
+ "Return the nginx location blocks for 'guix publish' running on URL."
+ (list (nginx-location-configuration
+ (uri "/nix-cache-info")
+ (body
+ (list
+ (string-append
+ "proxy_pass " url "/nix-cache-info;")
+ ;; Cache this file since that's always the first thing we ask
+ ;; for.
+ "proxy_cache static;"
+ "proxy_cache_valid 200 100d;" ; cache hits for a looong time.
+ "proxy_cache_valid any 5m;" ; cache misses/others for 5 min.
+ "proxy_ignore_client_abort on;"
+
+ ;; We need to hide and ignore the Set-Cookie header to enable
+ ;; caching.
+ "proxy_hide_header Set-Cookie;"
+ "proxy_ignore_headers Set-Cookie;")))
+
+ (nginx-location-configuration
+ (uri "/nar/")
+ (body
+ (list
+ (string-append "proxy_pass " url ";")
+ "client_body_buffer_size 256k;"
+
+ ;; Be more tolerant of delays when fetching a nar.
+ "proxy_read_timeout 60s;"
+ "proxy_send_timeout 60s;"
+
+ ;; Enable caching for nar files, to avoid reconstructing and
+ ;; recompressing archives.
+ "proxy_cache nar;"
+ "proxy_cache_valid 200 30d;" ; cache hits for 1 month
+ "proxy_cache_valid 504 3m;" ; timeout, when hydra.gnu.org is overloaded
+ "proxy_cache_valid any 1h;" ; cache misses/others for 1h.
+
+ "proxy_ignore_client_abort on;"
+
+ ;; Nars are already compressed.
+ "gzip off;"
+
+ ;; We need to hide and ignore the Set-Cookie header to enable
+ ;; caching.
+ "proxy_hide_header Set-Cookie;"
+ "proxy_ignore_headers Set-Cookie;"
+
+ ;; Provide a 'content-length' header so that 'guix
+ ;; substitute-binary' knows upfront how much it is downloading.
+ ;; "add_header Content-Length $body_bytes_sent;"
+ )))
+
+ (nginx-location-configuration
+ (uri "~ \\.narinfo$")
+ (body
+ (list
+ ;; Since 'guix publish' has its own caching, and since it relies
+ ;; on the atime of cached narinfos to determine whether a
+ ;; narinfo can be removed from the cache, don't do any caching
+ ;; here.
+ (string-append "proxy_pass " url ";")
+
+ ;; For HTTP pipelining. This has a dramatic impact on
+ ;; performance.
+ "client_body_buffer_size 128k;"
+
+ ;; Narinfos requests are short, serve many of them on a
+ ;; connection.
+ "keepalive_requests 600;"
+
+ ;; Do not tolerate slowness of hydra.gnu.org when fetching
+ ;; narinfos: better return 504 quickly than wait forever.
+ "proxy_connect_timeout 10s;"
+ "proxy_read_timeout 10s;"
+ "proxy_send_timeout 10s;"
+
+ ;; 'guix publish --ttl' produces a 'Cache-Control' header for
+ ;; use by 'guix substitute'. Let it through rather than use
+ ;; nginx's "expire" directive since the expiration time defined
+ ;; by 'guix publish' is the right one.
+ "proxy_pass_header Cache-Control;"
+
+ "proxy_ignore_client_abort on;"
+
+ ;; We need to hide and ignore the Set-Cookie header to enable
+ ;; caching.
+ "proxy_hide_header Set-Cookie;"
+ "proxy_ignore_headers Set-Cookie;")))
+
+ ;; Content-addressed files served by 'guix publish'.
+ (nginx-location-configuration
+ (uri "/file/")
+ (body
+ (list
+ (string-append "proxy_pass " url ";")
+
+ "proxy_cache cas;"
+ "proxy_cache_valid 200 200d;" ; cache hits
+ "proxy_cache_valid any 5m;" ; cache misses/others
+
+ "proxy_ignore_client_abort on;")))
+
+ ;; Try to prevent good-faith crawlers from downloading substitutes.
+ (nginx-location-configuration
+ (uri "= /robots.txt")
+ (body
+ (list
+ #~(string-append "try_files "
+ #$(plain-file "robots.txt" publish-robots.txt)
+ " =404;")
+ "root /;")))))
+#+end_src
+
+#+begin_src scheme
+ (define (balg02-locations publish-url)
+ "Return nginx location blocks with 'guix publish' reachable at
+ PUBLISH-URL."
+ (append (publish-locations publish-url)
+ (list
+ ;; Cuirass.
+ (nginx-location-configuration
+ (uri "/")
+ (body (list "proxy_pass http://localhost:8081;"
+ ;; ;; See
+ ;; ;; .
+ ;; (string-append
+ ;; "add_header Onion-Location http://" %ci-onion
+ ;; "$request_uri;")
+ )))
+ (nginx-location-configuration
+ (uri "~ ^/admin")
+ (body
+ (list "if ($ssl_client_verify != SUCCESS) { return 403; } proxy_pass http://localhost:8081;")))
+
+ (nginx-location-configuration
+ (uri "/static")
+ (body
+ (list
+ "proxy_pass http://localhost:8081;"
+ ;; Cuirass adds a 'Cache-Control' header, honor it.
+ "proxy_cache static;"
+ "proxy_cache_valid 200 2d;"
+ "proxy_cache_valid any 10m;"
+ "proxy_ignore_client_abort on;")))
+
+ (nginx-location-configuration
+ (uri "/download") ;Cuirass "build products"
+ (body
+ (list
+ "proxy_pass http://localhost:8081;"
+ "expires 10d;" ;override 'Cache-Control'
+ "proxy_cache static;"
+ "proxy_cache_valid 200 30d;"
+ "proxy_cache_valid any 10m;"
+ "proxy_ignore_client_abort on;")))
+
+ (nginx-location-configuration ;certbot
+ (uri "/.well-known")
+ (body (list "root /var/www;"))))))
+#+end_src
+
+#+begin_src scheme
+ (define %publish-url "http://localhost:3000")
+
+ (define %tls-settings
+ (list
+ ;; Make sure SSL is disabled.
+ "ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;"
+ ;; Disable weak cipher suites.
+ "ssl_ciphers HIGH:!aNULL:!MD5;"
+ "ssl_prefer_server_ciphers on;"
+
+ ;; Use our own DH parameters created with:
+ ;; openssl dhparam -out dhparams.pem 2048
+ ;; as suggested at .
+ "ssl_dhparam /etc/dhparams.pem;"))
+#+end_src
+
+#+begin_src scheme
+ (define %balg02-servers
+ (list
+ ;; Redirect domains that don't explicitly support HTTP (below) to HTTPS.
+ (nginx-server-configuration
+ (listen '("80"))
+ (raw-content
+ (list "return 308 https://$host$request_uri;")))
+
+ ;; Domains that still explicitly support plain HTTP.
+ (nginx-server-configuration
+ (listen '("80"))
+ (server-name `("cuirass.genenetwork.org"
+ ;;
+ "~[0-9]$"
+ ; TODO: onion
+ ; ,(regexp-quote %ci-onion)
+ ))
+ (locations (balg02-locations %publish-url))
+ (raw-content
+ (list
+ "access_log /var/run/anonip/http.access.log;"
+ "proxy_set_header X-Forwarded-Host $host;"
+ "proxy_set_header X-Forwarded-Port $server_port;"
+ "proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;")))
+
+ ;; HTTPS servers
+ (nginx-server-configuration
+ (listen '("443 ssl"))
+ (server-name '("cuirass.genenetwork.org"))
+ (ssl-certificate (le "cuirass.genenetwork.org"))
+ (ssl-certificate-key (le "cuirass.genenetwork.org" 'key))
+ (locations (balg02-locations %publish-url))
+ (raw-content
+ (append
+ %tls-settings
+ (list
+ "access_log /var/run/anonip/https.access.log;"
+ "proxy_set_header X-Forwarded-Host $host;"
+ "proxy_set_header X-Forwarded-Port $server_port;"
+ "proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
+ ;; TODO:
+ ;; For Cuirass admin interface authentication
+ ;; "ssl_client_certificate /etc/ssl-ca/certs/ca.crt;"
+ ;; "ssl_verify_client optional;"
+ ))))))
+#+end_src
+
+#+begin_src scheme
+ (define %extra-content
+ (list
+ "default_type application/octet-stream;"
+ "sendfile on;"
+
+ (accept-languages)
+
+ ;; Maximum chunk size to send. Partly this is a workaround for
+ ;; , but also the nginx docs mention that
+ ;; "Without the limit, one fast connection may seize the worker
+ ;; process entirely."
+ ;;
+ "sendfile_max_chunk 1m;"
+
+ "keepalive_timeout 65;"
+
+ ;; Use HTTP 1.1 to talk to the backend so we benefit from keep-alive
+ ;; connections and chunked transfer encoding. The latter allows us to
+ ;; make sure we do not cache partial downloads.
+ "proxy_http_version 1.1;"
+
+ ;; The 'inactive' parameter for caching is not very useful in our
+ ;; case: all that matters is that LRU sweeping happens when 'max_size'
+ ;; is hit.
+
+ ;; cache for nar files
+ "proxy_cache_path /var/cache/nginx/nar"
+ " levels=2"
+ " inactive=8d" ; inactive keys removed after 8d
+ " keys_zone=nar:4m" ; nar cache meta data: ~32K keys
+ " max_size=10g;" ; total cache data size max
+
+ ;; cache for content-addressed files
+ "proxy_cache_path /var/cache/nginx/cas"
+ " levels=2"
+ " inactive=180d" ; inactive keys removed after 180d
+ " keys_zone=cas:8m" ; nar cache meta data: ~64K keys
+ " max_size=50g;" ; total cache data size max
+
+ ;; cache for build logs
+ "proxy_cache_path /var/cache/nginx/logs"
+ " levels=2"
+ " inactive=60d" ; inactive keys removed after 60d
+ " keys_zone=logs:8m" ; narinfo meta data: ~64K keys
+ " max_size=4g;" ; total cache data size max
+
+ ;; cache for static data
+ "proxy_cache_path /var/cache/nginx/static"
+ " levels=1"
+ " inactive=10d" ; inactive keys removed after 10d
+ " keys_zone=static:1m" ; nar cache meta data: ~8K keys
+ " max_size=200m;" ; total cache data size max
+
+ ;; If Hydra cannot honor these delays, then something is wrong and
+ ;; we'd better drop the connection and return 504.
+ "proxy_connect_timeout 10s;"
+ "proxy_read_timeout 10s;"
+ "proxy_send_timeout 10s;"
+
+ ;; Cache timeouts for a little while to avoid increasing pressure.
+ "proxy_cache_valid 504 30s;"))
+#+end_src
+
+#+begin_src scheme
+ (define %nginx-configuration
+ (nginx-configuration
+ (server-blocks %balg02-servers)
+ (server-names-hash-bucket-size 128)
+ (modules
+ (list
+ ;; Module to redirect users to the localized pages of their choice.
+ (file-append nginx-accept-language-module
+ "/etc/nginx/modules/ngx_http_accept_language_module.so")))
+ (global-directives
+ '((worker_processes . 16)
+ (pcre_jit . on)
+ (events . ((worker_connections . 1024)))))
+ (extra-content
+ (string-join %extra-content "\n"))
+ (shepherd-requirement
+ (map log-file->anonip-service-name
+ %anonip-log-files))))
+#+end_src
+
+*** Cache activation
+
+#+begin_src scheme
+ (define %nginx-cache-activation
+ ;; Make sure /var/cache/nginx exists on the first run.
+ (simple-service 'nginx-/var/cache/nginx
+ activation-service-type
+ (with-imported-modules '((guix build utils))
+ #~(begin
+ (use-modules (guix build utils))
+ (mkdir-p "/var/cache/nginx")))))
+#+end_src
+
+*** Deploy hook
+
+#+begin_src scheme
+ (define %nginx-deploy-hook
+ (program-file
+ "nginx-deploy-hook"
+ #~(let ((pid (call-with-input-file "/var/run/nginx/pid" read)))
+ (kill pid SIGHUP))))
+#+end_src
+
+** Setup guix-daemon
+
+- Allow for substitutes from this server
+- Adjust guix-daemon configuration (timeouts, # of build accounts, # of cores to use)
+
+#+begin_src scheme
+ (define* (guix-daemon-config #:key (max-jobs 5) (cores 4)
+ (build-accounts-to-max-jobs-ratio 4)
+ (authorized-keys '())
+ (substitute-urls '()))
+ (guix-configuration
+ (substitute-urls substitute-urls)
+ (authorized-keys authorized-keys)
+
+ ;; We don't want to let builds get stuck for too long, but we still want
+ ;; to allow building things that can take a while (eg. 3h). Adjust as necessary.
+ (max-silent-time 3600)
+ (timeout (* 6 3600))
+
+ (log-compression 'gzip) ;be friendly to 'guix publish' users
+
+ (build-accounts (* build-accounts-to-max-jobs-ratio max-jobs))
+ (extra-options (list "--max-jobs" (number->string max-jobs)
+ "--cores" (number->string cores)
+ "--gc-keep-derivations"))))
+#+end_src
+
+#+begin_src scheme
+ (modify-services %base-services
+ (guix-service-type config => (guix-daemon-config
+ #:substitute-urls
+ '("https://cuirass.genenetwork.org")
+ #:max-jobs 20
+ #:cores 4
+ #:authorized-keys
+ (cons
+ (local-file "../../../.pubkeys/guix/cuirass.genenetwork.org.pub")
+ %default-authorized-guix-keys)
+ #:build-accounts-to-max-jobs-ratio 5)))
+#+end_src
+
+** Optional
+
+*** Onion service
+
+* Setup
+
+TODO: talk about setup of Tennessee Guix Build Farm and Substitute Server specifics (eg.
+remote install)
+
+* Conclusion
+
+TODO: ...
+
+- In the future, we hope to work with Guix maintainers to include this substitute server as
+ one of the provided Guix System defaults.
--
cgit v1.2.3