x lo posso fare anche con y!
Nix e' un DSL funzionale lazy pensato per restituire una struttura chiamata derivazione
stdenv.mkDerivation {
name = "hello";
src = ./src;
buildInputs = [ coreutils gcc ];
buildPhase = ''
gcc "$src/hello.c" -o ./hello
'';
installPhase = ''
mkdir -p "$out/bin"
cp ./hello "$out/bin/"
'';
}
stdenv.mkDerivation wrappa builtins.derivation
Quando l'evaluator Nix valuta una derivazione crea un file .drv
{
"/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv": {
"outputs": {
"out": {
"path": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
}
},
"inputSrcs": [],
"inputDrvs": {},
"platform": "mysystem",
"builder": "mybuilder",
"args": [],
"env": {
"builder": "mybuilder",
"name": "myname",
"out": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname",
"system": "mysystem"
}
}
}
Il file drv contiene tutte le informazioni necessarie per costruire l'output della derivazione.
Se la derivazione dipende da altre derivazioni prima vengono buildate queste.
Input addresssed: l'output della derivazione finira' in un path che contiene un hash ottenuto partendo alcune informazioni tra cui il nome, la versione, il builder e i nomi degli input (quindi dai loro hash).
In questo modo e' possibile sapere l'output di una derivazione prima di buildarla.
Le build avvengono in ambienti sandboxed senza accesso al sistema (in particolare a internet), in sostanza dipendono solo dalle derivazioni input.
Come possiamo buildare qualcosa se non possiamo scaricarne i sorgenti?
Fixed output: se siamo in grado di esplicitare l'hash del contenuto nel file drv (prima che avvenga la build) allora la sandbox dove avverra' la build avra' accesso a internet (git, curl, etc…)
Corollario: risalendo il grafo delle dipendenze di una derivazione alla fine troveremo sempre derivazioni fixed output.
Sono dette anche content addressed.
https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/games/umoria/default.nix
nix derivation show nixpkgs#umoria nix build nixpkgs#umoria -L --rebuild
Dove finiscono gli output delle derivazioni?
Nix quindi e' in grado di gestire contemporaneamente diverse versioni delle stesse dipendenze.
Potremo per esempio avere sullo stesso sistema versioni diverse di uno stesso software che pero' richiedono dipendenze differenti (dependency hell).
Data una derivazione, e' possibile considerare la chiusura rispetto alla relazione di dipendenza di tale derivazione, ovvero la closure.
Pertanto possiamo copiare un programma con tutte le sue dipendenze da una macchina all'altra semplicemente copiando la closure.
Nix prima di buildare una derivazione (un file drv) interroga un substituter (detto anche cache) ed eventualmente scarica l'output della derivazione.
Cio' e' possibile poiche' gli output sono (generalmente) input addressed.
E se l'intero sistema operativo fosse l'output di una derivazione?
In ordine sparso:
systemdNix non e' strong typed, i moduli aggiungono un type system dentro il linguaggio stesso.
Sono estremamente componibili.
Sostanzialmente un modulo fa due cose:
Il linguaggio diventa veramente puro (e.g. non puo' leggere path fuori dallo store).
Forniscono un modo unificato di dichiarare le dipendenze dei propri progetti ed interagire con essi da CLI.
Schema di un flake:
{ self, ... }@inputs:
{
# Executed by `nix flake check`
checks."<system>"."<name>" = derivation;
# Executed by `nix build .#<name>`
packages."<system>"."<name>" = derivation;
# Executed by `nix build .`
packages."<system>".default = derivation;
# Executed by `nix run .#<name>`
apps."<system>"."<name>" = {
type = "app";
program = "<store-path>";
};
# Executed by `nix run . -- <args?>`
apps."<system>".default = { type = "app"; program = "..."; };
# Formatter (alejandra, nixfmt or nixpkgs-fmt)
formatter."<system>" = derivation;
# Used for nixpkgs packages, also accessible via `nix build .#<name>`
legacyPackages."<system>"."<name>" = derivation;
# Overlay, consumed by other flakes
overlays."<name>" = final: prev: { };
# Default overlay
overlays.default = final: prev: { };
# Nixos module, consumed by other flakes
nixosModules."<name>" = { config, ... }: { options = {}; config = {}; };
# Default module
nixosModules.default = { config, ... }: { options = {}; config = {}; };
# Used with `nixos-rebuild switch --flake .#<hostname>`
# nixosConfigurations."<hostname>".config.system.build.toplevel must be a derivation
nixosConfigurations."<hostname>" = {};
# Used by `nix develop .#<name>`
devShells."<system>"."<name>" = derivation;
# Used by `nix develop`
devShells."<system>".default = derivation;
# Hydra build jobs
hydraJobs."<attr>"."<system>" = derivation;
# Used by `nix flake init -t <flake>#<name>`
templates."<name>" = {
path = "<store-path>";
description = "template description goes here?";
};
# Used by `nix flake init -t <flake>`
templates.default = { path = "<store-path>"; description = ""; };
}
Contiene sia pacchetti (derivazioni) che moduli di NixOS.
https://github.com/nixos/nixpkgs
nixos-rebuild in realta' permette anche il deploy remoto.
nixos-rebuild switch --flake github:aciceri/nixfleet#kirk --target-host X.X.X.X --build-host Y.Y.Y.Y
Esistono numeri altri tool:
nixopscolmenadeploy-rsE se volessimo generare immagini per provider cloud specifici?
Niente.
Dico davvero.
Ok, quasi niente.
NixOS non e':
Se i progetti su cui lavorano gli sviluppatori sono flake, la CI puo' semplicmente buildare un sottoinsieme dei suoi output (packages, checks, devShells, etc…).
I progetti non richiedono nessuna configurazione ad hoc per abilitare la CI.
Se lo store del sistema di CI persiste avremo build molto veloci. Altrimenti e' possibile istruire la CI a pushare cio' che builda su una cache (e interrogarla prima di buildare).
Se esponiamo questa cache agli sviluppatori minimizzeremo di molto il tempo di build in locale.
Esistono diverse soluzioni:
Il continuous deployment viene facile, basta eseguire uno dei tool visti prima in CI.
Esistono soluzioni piu' strutturate per gestire il deploy di sistemi multipli (come Hercules CI).
Alternativamente si puo' anche istruire il sistema stesso ad aggiornarsi da solo:
system.autoUpgrade = {
enable = true;
flake = "github:aciceri/nixfleet#${config.networking.hostName}";
dates = "daily";
allowReboot = false;
};
Nix permette di "cross-buildare" le derivazioni in due modi diversi, e funzionano entrambi molto bene.
binfmt (QEMU)Nix e' probabilmente un migliore build system per Docker di Docker stesso.
docker buildx 🤮)
Come possiamo avere segreti se le derivazioni nello store sono leggibili da tutti?
Criptiamoli!
agenixsops-nixMettiamo nello store solo i segreti criptati e installiamo degli script che li decriptino a runtime.
Questo richiede comunque un meccanismo per distrubuire la chiave privata in fase di deploy.
NixOS fornisce opzioni che definiscono i punti di mount del sistema, ma durante l'installazione queste partizioni devono essere create manualmente.
Disko e' un modulo NixOS che permette di dichiarare come devono essere create le partizioni (dimensione, file system, encryption, etc…)
Tale modulo fa due cose:
fileSystems)NixOS e' stateless?
Purtroppo no…
Ma possiamo domare lo stato!
https://github.com/nix-community/impermanence
tmpfs come / (tranne /boot e /nix)Il mio cloud provider non supporta NixOS… ðŸ˜
https://github.com/nix-community/nixos-anywhere
Per gli amanti di terraform esiste un anche un interessante modulo.
e' un flake
nix build github:aciceri/nixos-devops-talk
https://github.com/aciceri/nixos-devops-talk
Non troppo difficili per favore!