ng'stuff

🔭 🎶 🕺

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 :

nix
{ ... }:
{
  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-service lorsque nécessaire

Déploiements continus

Du côté du projet app, on peut tout matérialiser dans un flake :

nix
{
  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 :

sh
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.

Retour aux articles 👀