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. Côté root, ajoutons ce module système pour notre app :

nix
{ 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-user devra maintenir son exécutable app
  • Il pourra redémarrer app-service lorsque nécessaire

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; };
      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 :

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

Retour aux articles 👀