Nix pour des déploiements modulaires
La nature déterministe de nix permet-elle de déployer par itérations rapides ?
Si on apprécie nix pour son aspect prévisible, il ne va pas nous laisser activer du code arbitraire n’importe comment. Voyons quelle interface nous avons à disposition pour déployer des services spécifiques sans redéployer le système entier.
Responsabilité et interface
Une configuration NixOS est un ensemble de déclarations cohérentes. Chaque option peut en impacter d’autres et c’est ce qui rend ce système si puissant pour s’adapter aux besoins logiciels réels.
En contrepartie on ne pourra pas permettre à une configuration séparée d’avoir un périmètre de responsabilités comparable. En effet, si deux profils de déploiement déclaraient une configuration système, comment réconcilier les deux tout en leur permettant de s’activer individuellement ? C’est incohérent by design.
À la place, cherchons la bonne interface pour s’intégrer de façon prévisible. Côté root, ajoutons ce module système pour notre app :
{ config, ... }:
{
users.users.app-user = {
isNormalUser = true; # uid ≥ 500
linger = true; # Permet à l'utilisateur de faire tourner
# des services sans session active
createHome = true;
# Autorise les administrateurs à deployer en tant que app-user
openssh.authorizedKeys.keys =
config.users.users.root.openssh.authorizedKeys.keys;
};
nix.settings.trusted-users = [ "app-user" ];
systemd.user.services.app-service = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "%h/.local/state/nix/profiles/app-profile/bin/app";
Restart = "on-failure";
};
};
}Contrat : on s’attend à la présence de l’exécutable app dans le profil app-profile de l’utilisateur app-user.
Déploiements continus
Côté applicatif, on aura donc accès au user service app-service :
app-userdevra maintenir son exécutableapp- Il pourra redémarrer
app-servicelorsque nécessaire
On peut tout matérialiser dans un flake :
{
inputs = {
deploy-rs.url = "github:serokell/deploy-rs";
deploy-rs.inputs.nixpkgs.follows = "nixpkgs";
nixpkgs.url = "nixpkgs/nixos-unstable";
};
outputs = { self, deploy-rs, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
package = pkgs.writeShellScriptBin "app" "echo Hello world!";
in
{
# Permet d'exécuter localement: `nix run .`
apps.${system}.default = {
type = "app";
program = "${package}/bin/run-app";
};
# Rend `deploy` dispo dans la dev shell. See https://nix.dev/guides/recipes/direnv.html
devShells.${system}.default = pkgs.mkShellNoCC {
packages = [ deploy-rs.packages.${system}.deploy-rs ];
};
deploy.nodes.default = {
hostname = "my.host.net";
profiles.app-profile = {
path = deploy-rs.lib.x86_64-linux.activate.custom
package "systemctl --user restart app-service.service";
remoteBuild = true; # Évite d'autoriser app-user à importer des dérivations arbitraires
sshUser = "app-user";
};
};
checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
};
}On utilise alors deploy-rs pour re-déployer à la volée :
deploy .Conclusion
Celle configuration peut sembler inutilement lourde. Soulignons donc qu’elle permet :
- De redéployer des services dynamiquement et indépendamment de la configuration système
- De délimiter la responsabilité d’une application à un utilisateur donné
On pourra donc, par exemple, l’envisager pour un système d’intégration continue ou pour simplement redéployer son blog.