Hydra del pobre — el enjambre con su propia fábrica binaria
El problema (otra vez)
Hoy aurin lleva cuatro horas y media cross-compilando el closure de retropix (Pi 3, aarch64). Kernel del Pi + RetroArch con todos los cores + miscelánea. Si aurin se reinicia mañana, hay que volver a hacerlo: cuatro horas y media tiradas a la basura.
Y peor: cuando quieras meter una segunda Pi al enjambre, o un emulador Switch ARM, o el Nixondroid del móvil… vuelta a empezar desde cero. Cada nodo nuevo paga el peaje completo.
La pregunta es simple:
¿Por qué no guardamos los binarios compilados una vez y los reusamos?
Esto es lo que hace todo el mundo Nix desde hace años. Tiene nombre: binary cache. Y la implementación de referencia se llama Hydra. Vamos a entender qué es y por qué nosotros podemos tener nuestra propia versión sin levantar Hydra completo.
Capítulo 1 — ¿Qué es un binary cache?
La intuición
Cuando NixOS construye un paquete, el resultado es una carpeta
inmutable en /nix/store/HASH-nombre-version/. El HASH viene calculado desde:
- código fuente exacto
- versión del compilador
- todas las dependencias transitivas
- flags de compilación
- arquitectura objetivo (x8664-linux, aarch64-linux, …)
Si dos máquinas distintas calculan el mismo HASH, el resultado de compilar es bit a bit
idéntico. Eso es la magia de Nix: el hash es la
prueba de reproducibilidad.
Ahora la consecuencia importante: si yo (aurin) ya compilé HASH-linux_rpi-bcm2711-6.12.47, ese mismo
binario sirve para cualquier máquina aarch64 que pida
exactamente ese hash. No tienen que volver a compilarlo.
Un binary cache es exactamente eso: un
servidor HTTP/S3 que guarda los /nix/store/HASH-... ya compilados, y los sirve a
quien los pida.
El cache público
que ya usas: cache.nixos.org
Cada vez que haces nixos-rebuild switch, antes de compilar nada,
Nix le pregunta a https://cache.nixos.org
si tiene el path. La mayoría de las veces SÍ lo tiene (lo construyó la
infra oficial), y te lo descarga. Por eso instalar Firefox no tarda 2
horas.
Hay dos paths de la conversación:
- Aurin: "¿Tienes
9rpism89...-systemd-260.1?" - cache.nixos.org: "Sí, toma" → manda el .nar.xz comprimido.
Sin cache, aurin compilaría systemd cada vez que rebuildeas.
¿Por qué no me sirve cache.nixos.org para el kernel rpi?
Porque cache.nixos.org SOLO tiene paquetes de nixpkgs "oficial". El kernel del Raspberry Pi
viene del flake nixos-raspberrypi
(externo). Sus HASH son distintos a los de
nixpkgs. Nadie los ha metido en cache.nixos.org. Por tanto, se
compila desde source.
Y compilar el kernel del Pi en QEMU aarch64 son horas.
Capítulo 2 — ¿Qué es Hydra?
Hydra es el "CI/CD de NixOS"
Imagina Jenkins, GitHub Actions, GitLab CI… pero específicamente para construir paquetes de Nix.
+-------------------+
| HYDRA |
| (build farm CI) |
+---------+---------+
|
+-------------------+-------------------+
| | |
v v v
+---------+ +---------+ +---------+
| builder | | builder | | builder |
| x86_64 | | aarch64 | | i686 |
+---------+ +---------+ +---------+
| | |
+---------+---------+-------------------+
|
v
+---------------+
| binary cache |
| (S3 / HTTP) |
+---------------+
|
v
clientes (tu aurin)
Lo que hace:
- Jobsets: defines qué quieres construir
(
nixpkgsentero, tu flake, lo que sea). - Schedules: cuándo se construye (cada commit, cada noche, etc.).
- Builders: máquinas que ejecutan los
nix build. Pueden ser nativas x8664, aarch64 reales, o QEMU emulado. - Cache output: el resultado de cada build se firma y se sube a un binary cache (S3 o HTTP).
NixOS oficial tiene su propio Hydra en hydra.nixos.org. Construye nixpkgs entero en x8664 y aarch64, y
publica los binarios en cache.nixos.org.
Eso es por lo que Firefox se descarga, no se compila.
¿Necesitas Hydra para tener un binary cache propio?
No. Hydra es la infraestructura completa (scheduler + builders
- web UI + DB Postgres + queue runner). Es muchísimo. Para nuestro
caso es matar moscas a cañonazos.
Lo único que necesitas para tener un binary cache propio es:
- Un sitio donde guardar paths firmados (HTTP o S3).
- Una forma de publicar paths nuevos al sitio.
- Una forma de descargar paths firmados desde el sitio.
Las tres ya las tienes. Vamos.
Capítulo 3 — Las piezas que ya tienes en el enjambre
Pieza 1: nix-serve en aurin
Ya tienes el módulo modules/services/nix-cache.nix y aurin lo
expresa con server.enable = true. Eso
levanta nix-serve en el puerto 5000:
ss -tlnp | grep 5000
# LISTEN 0 1024 0.0.0.0:5000 ... nix-serveEs un servidor HTTP minúsculo (Haskell, ~10 MiB) que sirve los paths
del /nix/store local de aurin como un
binary cache.
Si retropix le dice "dame HASH-foo",
aurin responde con el .nar.xz
correspondiente, firmado con la clave aurin-1.
Pieza 2: la signing key aurin-1
En /etc/nix/signing-key.sec hay una
clave privada (NUNCA sale de aurin). La parte pública es:
aurin-1:q1/yLntnfrg43hE2q7dww3+f4XEwrSl3ftxLotXY1L0=
Cuando aurin compila /nix/store/X,
calcula una firma con su clave privada. Cualquier nodo que tenga la
clave pública en sus trusted-public-keys puede
verificar esa firma.
Esto es lo que hace que el binary cache sea seguro: nadie puede inyectar binarios maliciosos a nuestro nombre porque solo aurin tiene la clave privada.
Pieza 3: Tailscale mesh
Aurin escucha en 100.64.0.4:5000 (su IP
de la mesh). Solo los nodos del enjambre pueden hablar con él. Cero
exposición a Internet.
Pieza 4: Garage (S3 propio en cohete)
Tienes un cluster Garage corriendo en cohete: backend de almacenamiento S3-compatible. Las URLs se ven así:
s3://mi-bucket?endpoint=https://garage.pascualmg.dev®ion=us-east-1
Cualquier cosa que hable S3 puede hablar con Garage.
Y Nix habla S3 nativo (nix copy --to s3://..., nix.settings.substituters = [ "s3://..." ]).
Capítulo 4 — La diferencia entre lo que tienes y lo que falta
Hoy
+----------+ HTTP nix-serve +----------+
| AURIN |<------------------+ retropix |
| /nix/ | | pulls |
| store/ | | paths |
+----------+ +----------+
|
| (los paths viven SOLO en el RAM de aurin
| hasta que el GC los borre)
Limitaciones:
- Si aurin está apagado, retropix no puede pull → tiene que compilar.
- Si aurin se reinstala, se va todo el work compilado.
- Si quieres compartir con macbook → cada nodo lo descarga otra vez desde aurin (consume bandwidth de aurin).
Lo que falta: persistencia y compartido
+----------------------+
| GARAGE en cohete |
| bucket: nix-cache |
| (S3-compatible) |
+----------+-----------+
|
| (paths firmados, persistentes)
|
+--------------+--------------+
| |
v v
+----------+ +----------+
| AURIN | --(push tras --| retropix |
| | build aarch64) | | |
+----------+ +----------+
^ ^
| |
| (substituter S3) | (substituter S3)
| |
+--------------+--------------+
|
OTROS CLONES
(macbook, vespino, futuras Pi...)
Ahora:
- Aurin compila → push a Garage
- Cualquier nodo: pulls desde Garage
- Si aurin muere → no se ha perdido nada (cohete tiene los paths)
- Si cohete muere → aurin sigue teniendo sus paths locales (degrada a "como antes")
Capítulo 5 — ¿Por qué es compatible?
Esta es la parte más bonita de Nix. La compatibilidad viene de tres protocolos abiertos:
5.1. El formato de paquete: .nar
Un paquete de Nix se serializa como .nar (Nix Archive). Es un formato textual
deterministico (como tar, pero ordenado y
sin metadatos volátiles). El mismo input produce el mismo .nar bit a bit.
Comprimido suele ir como .nar.xz o
.nar.zst.
5.2. La estructura del
cache: store-path-info
Un binary cache, sea HTTP o S3, expone dos tipos de archivos:
https://cache.example.com/
├── nix-cache-info (metadatos del cache)
├── HASH.narinfo (info de UN path)
├── nar/HASH.nar.xz (contenido)
├── nar/HASH2.nar.xz
└── ...
El .narinfo es texto plano con la info
clave:
StorePath: /nix/store/HASH-foo
URL: nar/abc123.nar.xz
Compression: xz
FileHash: sha256:...
FileSize: 12345
References: HASH-bar HASH-baz
Sig: aurin-1:BASE64FIRMA
Cuando cache.nixos.org sirve esto, hace
GET .narinfo. Cuando nix-serve lo sirve, hace lo mismo.
Cuando Garage lo sirve via S3, el cliente Nix interpreta
exactamente la misma estructura.
Por eso da igual el "transporte":
- HTTP plano (
cache.nixos.org,nix-serve) - S3 nativo (
s3://bucket?endpoint…=) - Local file (
file:///mnt/cache)
Todos hablan el mismo idioma de paths firmados.
5.3. La verificación: firmas
Cuando un nodo descarga HASH-foo.narinfo, encuentra el campo Sig: aurin-1:BASE64. Verifica esa firma contra
su lista trusted-public-keys. Si está
aurin-1 ahí: OK, instala. Si no: rechaza.
La firma es del CONTENIDO, no del transporte. Por
eso un path firmado por aurin en su nix-serve local tiene la misma validez que ese
mismo path subido a Garage. La firma viaja con el path.
Capítulo 6 — El plan concreto
Paso 1: Crear bucket en Garage
# En cohete (donde corre garage):
garage bucket create nix-cache
garage key new --name nix-cache-writer
garage bucket allow --read --write --owner nix-cache --key nix-cache-writer
# Read público (sin key) para que cualquier nodo pueda pull:
garage bucket website --allow nix-cacheSale una access-key + secret-key que guardamos en agenix.
Paso 2: Credenciales en agenix de aurin
# En aurin:
cd ~/dotfiles/secrets
echo -n "AKIAxxxxxx" > nix-cache-s3-key.tmp
echo -n "secretxxxxx" > nix-cache-s3-secret.tmp
agenix -e nix-cache-s3-credentials.age # editor: ambas líneas
rm *.tmpY en hosts/aurin/default.nix:
age.secrets.nix-cache-s3-credentials = {
file = ../../secrets/nix-cache-s3-credentials.age;
owner = "passh";
};Paso 3: Script nix-cache-push
#!/usr/bin/env bash
# nix-cache-push - Sube un store path a Garage cache, firmado.
set -euo pipefail
PATH_TO_PUSH="${1:?store path}"
BUCKET="nix-cache"
ENDPOINT="https://garage.pascualmg.dev"
REGION="us-east-1"
# Cargar credenciales agenix
set -a
source <(grep -E "AWS_" /run/agenix/nix-cache-s3-credentials)
set +a
# Firmar path con aurin-1 (idempotente)
sudo nix store sign --key-file /etc/nix/signing-key.sec -r "$PATH_TO_PUSH"
# Push a Garage
nix copy --to "s3://${BUCKET}?endpoint=${ENDPOINT}®ion=${REGION}" "$PATH_TO_PUSH"
echo "OK: $PATH_TO_PUSH publicado en Garage cache"Uso:
nix-cache-push /nix/store/csansdji33...-linux_rpi-bcm2711-6.12.47Paso 4: Substituter en cada host
En modules/services/nix-cache.nix
(cliente):
nix.settings = {
substituters = [
"https://cache.nixos.org"
"http://100.64.0.4:5000" # aurin nix-serve (rápido si vivo)
"s3://nix-cache?endpoint=https://garage.pascualmg.dev®ion=us-east-1"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"aurin-1:q1/yLntnfrg43hE2q7dww3+f4XEwrSl3ftxLotXY1L0="
];
};Nix prueba los substituters por orden. Lo encuentre primero, gana.
Paso 5: Hook post-deploy retropix
Modificar scripts/deploy-retropix: tras
el nix copy --to ssh-ng://retropix
exitoso, lanzar también nix-cache-push
sobre el toplevel.
# 4. Switch + push a cache compartido
ssh "$RETROPIX_HOST" "$RESULT/bin/switch-to-configuration switch"
nix-cache-push "$RESULT" || log "WARN: push a cache fallo (no critico)"Capítulo 7 — Qué ganas
Hoy
| Operación | Tiempo |
|---|---|
| Compilar kernel rpi en aurin | 1-2 h |
| Compilar retroarch+cores en aarch64 | 2-3 h |
| Total deploy retropix desde cero | 4-5 h |
Tras montar Garage cache
| Operación | Tiempo |
|---|---|
| Primera vez: compilar + push | 4-5 h |
| Segunda vez (sin cambios): pull | 2 min |
| Otro nodo aarch64 (Switch, Pi 4…) | 2 min |
| Aurin reinstalado: pull desde cohete | 2 min |
Multiplicador 100x en cualquier deploy posterior. La inversión de 1-2h de setup se amortiza la primera vez que reusas un closure.
Capítulo 8 — Hydra vs Cachix vs Garage propio
Tres niveles de "binary cache":
8.1. Hydra (la pesada)
- Para quién: distribuciones grandes (NixOS oficial, Nixpkgs).
- Coste: setup complejo (Postgres + jobset YAML + builders).
- Ventaja: scheduler + UI + métricas + auth.
- Conclusion: para nuestro enjambre, overkill.
8.2. Cachix.org (SaaS)
- Para quién: proyectos open source, equipos.
- Coste: gratis para públicos, $$ para privados.
- Ventaja: cero setup.
cachix push, listo. - Pega: tus binarios viven en infra de Cachix (ordenador de otros). El "lo local manda" se rompe.
8.3. Garage propio (la nuestra)
- Para quién: nosotros (5 clones, infra propia).
- Coste: 1-2h de setup. Almacenamiento = disco de cohete.
- Ventaja: 100% bajo nuestro control. Cero dependencia externa para infra crítica. Latencia mínima (mesh tailscale).
- Pega: si cohete muere, no hay cache. Pero los paths siguen
en
aurin /nix/store(degrada a antes).
Para nuestra escala (5 nodos, infra clone-first), la tercera es la que encaja con la filosofía. El ordenador de otros sigue siendo el ordenador de otros.
Capítulo 9 — Cosas que NO cambian
- Compilar sigue siendo exactamente igual. Nix no nota que ahora los paths van a S3 cuando termina.
- Las firmas son idénticas (
aurin-1). Cualquier path firmado vale igual. - Los demás substituters (
cache.nixos.org) siguen funcionando. Garage es además, no en vez de. - Si Garage no responde, Nix se va al siguiente substituter de la lista. Tolerante a fallos por diseño.
Capítulo 10 — Próximos pasos lógicos
Una vez tengamos esto montado, abre:
Auto-build nocturno: cron en aurin que rebuildea
nixosde cada host y pushea al cache. Pi compila mientras dormimos.Build matrix: GitHub Actions construye la flake en aarch64 nativo (runners ARM gratis para open source) y pushea al cache. Aurin se libra hasta del cross-build.
Servir pública el bucket: cualquier internauta podría usar tu cache. Si publicas el
trusted-public-keysen el README, cualquiera con tu flake compila gratis.Cache offline: copiar el bucket de Garage a un disco USB antes de viajar. Tu enjambre instala desde el USB, sin internet.
Cierre
Hydra es la "fábrica binaria" oficial de NixOS. Lo que vamos a montar es la fábrica binaria del enjambre: 100x más pequeña, exactamente lo que necesitamos, 100% bajo nuestro control.
La gracia es que no hace falta inventar nada nuevo. Las piezas ya existen y son compatibles entre sí por diseño. Solo hay que conectarlas.
Cuando termine el deploy retropix de hoy y subamos el closure a Garage, no hablaremos de "esperar 5 horas por compilación". Hablaremos de "el path ya está en cache, descarga".
El enjambre evoluciona también en su pipeline de build. Es parte del mismo principio que el refactor genético: *cada nodo expresa solo lo que necesita, y comparte el resto a través de la infraestructura común*.
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario