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. Dans un premier temps nous définierons côté root/système une interface qui permettra dans un second temps à un projet séparé de maintenir son app à jour.
Commençons donc par ce module système :
{ ... }:
{
users.users.app-user = {
isNormalUser = true; # uid ≥ 500
createHome = true;
openssh.authorizedKeys.keys = [ "ssh-ed25519 000000 app@boite.com" ];
};
# Autoriser app-user à importer les dérivations construites
nix.settings.trusted-users = [ "app-user" ];
systemd.services.app-service = {
description = "Service de l'app piloté par app-user";
serviceConfig = {
DynamicUser = true; # Sandboxing à la systemd
ExecStart = "/home/app-user/.local/state/nix/profiles/app-profile/bin/app";
Restart = "on-failure";
};
};
security.sudo.extraRules = [ {
users = [ "app-user" ];
commands = [ {
# Permettre à app-user de piloter app-service
command = "/run/current-system/sw/bin/systemctl ^(restart|start|stop) app-service\\.service$";
# Sans mot de passe - OK car on contrôle précisément la permission
options = [ "NOPASSWD" ];
} ];
} ];
}Contrat : on s’attend à la présence de l’exécutable app dans le profil app-profile de l’utilisateur app-user.
Ici on a donc bien délimité le rôle de app-user :
- Maintenir son exécutable
app - Redémarrer
app-servicelorsque nécessaire
Déploiements continus
Du côté du projet app, 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; };
app-package = pkgs.writeShellScriptBin "app" ''
exec echo Je suis une app badass
'';
in
{
# Permet d'exécuter app via l'API flake: `nix run .`
apps.${system}.default = {
type = "app";
program = "${app-package}/bin/run-app";
};
# Rend `deploy` dispo dans la dev shell. Voir https://nix.dev/guides/recipes/direnv.html
devShells.${system}.default = pkgs.mkShellNoCC {
packages = [ deploy-rs.packages.${system}.deploy-rs ];
};
deploy.nodes.my-host = {
hostname = "my.host.net";
profiles.app-profile = {
path = deploy-rs.lib.x86_64-linux.activate.custom
app-package "sudo systemctl restart app-service.service";
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
Cette configuration peut sembler inutilement lourde. Soulignons donc qu’elle permet :
- De redéployer des services indépendamment de la configuration système
- De délimiter la responsabilité d’une application/projet à 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.