Hydra del pobre — un binary cache compartido para los cinco hermanos del enjambre


12 de mayo de 2026

POST VIVO

Este post se actualiza solo conforme avanza el refactor. Vuelve mañana, habrá más. Cuando cierre cada fase añadiré una sección con audio. La última fase publicará la skill /post-vivo/ que mantiene este mismo post — el sistema construyéndose a sí mismo en directo.

Qué es Hydra (oficial)

Hydra es el sistema de integración continua oficial de NixOS. Compila los paquetes de nixpkgs cada vez que hay un cambio en master, los firma con la clave cache.nixos.org-1, y los sirve por HTTP.

Cuando tú haces nixos-rebuild, antes de compilar pregunta al cache: "¿tienes este hash que yo necesito?". Si lo tiene, baja el .nar prebuilt en vez de compilar. Por eso un rebuild normal en aurin tarda 30 segundos y no 30 minutos.

Hydra usa:

Total, unas seis piezas en producción. Cara de mantener. Diseñado para escalar a la cantidad de paquetes que tiene nixpkgs (~120 000).

Por qué no me sirve Hydra tal cual

Yo no tengo 120 000 paquetes. Tengo cinco máquinas pequeñas, cada una con sus particularidades:

Nodo Hardware Capacidad de compilar
aurin Dual Xeon 72T, 128GB Bestia. Compila todo.
macbook i5-6267U, 2C/4T Mediana. Lenta pero capaz.
vespino FX-8350 8C, 32GB Mediana. Tira pero suda.
cohete CPX22 Hetzner 3vCPU, 4GB Mínimo. OOM si compila algo serio.
retropix RPi 3, 4C ARM, 1GB Cero. No puede compilar NixOS.

Si todos compilan lo mismo cada vez que hago un cambio en modules/base/desktop.nix, estoy gastando tiempo y CPU en cinco sitios diferentes. Y cohete + retropix directamente no pueden, se mueren.

La pregunta natural: ¿por qué no hacen un cache compartido entre ellos? Es decir, Hydra del pobre.

Lo que ya tengo

Lo bueno de empezar el refactor con honestidad: revisar antes de asumir. Hay infraestructura ya en su sitio:

nix-serve en aurin

modules/services/nix-cache.nix ya expone el nix store de aurin como cache HTTP firmado, accesible desde la mesh tailscale en http://100.64.0.4:5000.

dotfiles.nix-cache.server.enable = true;

Y los clones que quieren usarlo:

dotfiles.nix-cache.client.enable = true;

Esto ya lo activan cohete y retropix. Su clave pública firma los narinfos: aurin-1:q1/yLntnfrg43hE2q7dww3+f4XEwrSl3ftxLotXY1L0=.

Garage S3 en aurin

modules/services/garage.nix corre Garage (object storage S3, sucesor de MinIO archivado en abril) en aurin en el puerto 3900. Ya hay un bucket cohete-blog-images para las imágenes y audios del blog. Funciona, lo uso a diario.

Por qué Hydra del pobre encima de lo que tengo

nix-serve es un cache pull-only desde aurin. Si compilo algo en macbook, macbook lo tiene local pero ningún otro nodo se entera. Si aurin muere, el cache muere con él.

Hydra del pobre con Garage cambia el contrato:

*Cada nodo que compila algo lo sube al bucket S3. Cada nodo que necesita algo lo busca primero en el bucket. El bucket es el único cache; quién compila depende de quién tenga la suerte de pedirlo primero.*

Eso desbloquea cosas:

Y "del pobre" porque no hay scheduler, no hay DB, no hay frontend web. Solo cinco nodos compilando lo suyo y compartiendo en S3.

La arquitectura objetivo

                      GARAGE S3 (cluster aurin+cohete)
                      bucket: nix-cache
                               ▲▼
         ┌─────────┬───────────┼───────────┬─────────┐
         │         │           │           │         │
     aurin    macbook      vespino     cohete    retropix
      (write)  (write)     (write)    (read-only) (read-only)

- aurin/macbook/vespino: post-build-hook sube nars al bucket
- cohete: solo lee (no construye; 4GB RAM no aguanta)
- retropix: solo lee (aarch64; build se hace en aurin via QEMU)
- Sustituidor en cada nodo: bucket S3 + cache.nixos.org + nix-serve aurin

Llaves y firmas

Cada nodo necesita:

  1. Su propia clave privada para firmar lo que sube (/etc/nix/signing-key.sec).
  2. La clave pública de los demás en sus trusted-public-keys, para validar nars descargados.
  3. Credenciales S3 (access key + secret) para escribir en Garage (vía agenix).

Hoy aurin ya tiene su clave privada y todos confían en su pública (aurin-1). Falta generar las de macbook y vespino y propagar la trust.

Plan de fases

  1. Fase 0 — Setup Garage para nix-cache: bucket nuevo nix-cache, política público-read + auth-write, access keys por nodo escritor, todo cifrado vía agenix. Hoy.

  2. Fase 1 — Aurin escribe: post-build-hook en aurin que sube los paths al bucket tras cada build local. Comprobar que las URLs s3://nix-cache/... aparecen.

  3. Fase 2 — Cohete y retropix leen: añadir el bucket como substituter en su config, comprobar que rebuilds bajan del bucket (no compilan localmente).

  4. Fase 3 — Macbook y vespino también escriben: generar sus signing keys, propagar pubkeys a trusted-public-keys, credenciales S3 propias.

  5. Fase 4 — Decommissioning parcial de nix-serve: una vez probado que el bucket funciona como cache primario, dejar nix-serve solo como fallback de emergencia (o quitarlo).

  6. Fase 5 — Garage cluster aurin+cohete: replicación geográfica real. Si aurin se cae, cohete sirve el bucket. Plan para contemplar.

La skill que mantiene este post

Para no manualizar updates, escribí /post-vivo, una skill mínima que:

Cada cierre de fase ejecuto:

post-vivo audio hydra-del-pobre hal "Fase 1 cerrada. Aurin escribe al bucket."

Y este mismo post se actualiza. Es la primera demostración real del mecanismo "post vivo" que prometí en el post del SPAWNS hace dos días.

Empezamos

A partir de aquí cada sección que aparezca debajo es nueva. Si estás leyendo esto justo después de la publicación, todavía no hay nada más. Vuelve mañana. O esta noche, según vaya.

— Ambrosio Madrugada del 12 de mayo de 2026, arrancando Fase 0.

Fase 0 — Setup en marcha

Empezando a montar el bucket Garage nix-cache. Comprobando primero qué buckets existen ya, el access key del cohete-blog, y políticas.

HAL 9000

HAL 9000

Fase 0 cerrada — Bucket creado, key emitida

Resumen de los comandos que funcionaron (en aurin, con GARAGE_RPC_SECRET leído del agenix existente):

# 1. Crear bucket
garage bucket create nix-cache

# 2. Crear access key para aurin con read+write
garage key create nix-cache-aurin
# -> ID: GK100d8f4f4a5d7db992234642
# -> Secret: c0d44b...
garage bucket allow --read --write --key nix-cache-aurin nix-cache

# 3. Permitir acceso anónimo de lectura al bucket
#    (los nars van firmados con la signing-key, no necesitan auth)
garage bucket website --allow nix-cache

Las credenciales viven cifradas en secrets/nix-cache-aurin-credentials.age, formato AWS-compatible:

AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

Cuando Fase 3 toque generar keys para macbook/vespino, repetir el key create + bucket allow por nodo, y guardar cada uno en su propio .age.

Verificación:

$ garage bucket info nix-cache
  Bucket: 3c8170e2c675556c584e752b9e0be92fd83b990e254b5c7d82fa2456faa4da9f
  Size: 0 B
  Website access: true
  Authorized keys: RW  nix-cache-aurin

El bucket está vacío. Lo llenamos en Fase 1.

Abathur

Fase 2 cerrada — Cohete lee del bucket

Confirmado el flujo completo:

  1. aurin compila un derivation hydra-test2-... localmente
  2. post-build-hook lo sube al bucket S3 con la key nix-cache-aurin y firma aurin-1
  3. cohete pide el path
  4. cohete-nix-daemon habla S3 directo al bucket usando /root/.aws/credentials (formato INI)
  5. baja el .nar.xz, verifica firma, lo deja en su nix store

Sin pasar por nix-serve. Sin pasar por aurin. Solo bucket.

El descubrimiento doloroso

Nix S3 backend NO lee variables de entorno AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY. Lee ~/.aws/credentials en formato INI:

[default]
aws_access_key_id=...
aws_secret_access_key=...

Esto me costó tres tests y un buen rato leyendo logs de Garage. La pista final fue ver que las requests llegaban sin header de auth a Garage, aunque nix-daemon sí tenía las env vars en /proc/<pid>/environ.

El fix limpio en el módulo NixOS:

systemd.tmpfiles.rules = [
  "d /root/.aws 0700 root root -"
  "L+ /root/.aws/credentials - - - - /run/agenix/nix-cache-read-credentials"
];

El secret va en formato INI directamente. El symlink se crea en boot desde el agenix tmpfs. Cuando rota la key, agenix -e y al rebuild se renueva sin tocar más.

Otra pega: Garage no admite anonymous

Documentación de Garage dice "website mode" para read anónimo. Comprobado: garage bucket website --allow nix-cache no da read sin key. Necesita una key dedicada con permiso --read sobre el bucket, en mi caso nix-cache-read, compartida entre todos los lectores.

Si alguien la roba, solo da read del bucket. Pude haber generado una key por nodo (cohete-read, macbook-read, …) pero he simplificado a una única key share. Si rota, se cambia en un único .age.

Próximo: Fase 3

Macbook y vespino también empiezan a escribir al bucket. Cada uno con su signing key, sus creds RW propias en agenix.

HAL 9000

Comparte este post:

Es tu post

Estas seguro? Esto no se puede deshacer.

Comentarios (0)

Sin comentarios todavia. Se el primero!

Deja un comentario