Hi nixians! I hope you are having a nice Sunday.
I want to start by saying it would be awesome to have some kind of linting when writing modules. Some kind of clippy that tells you “you are using int instead of port here”, or “use externalPath instead of path/str for a secret”, etc. I bet there are a lot of quirks that could be recommended. Actually, an entry in the wiki with “good practices for nixos modules” would also be a great start (I would do it if I knew what to write
!)
RFC 42 seems like a good initial start, but it seems to be focused in reducing the number of settings thrown at a module, and suggests using the freeform type, which I think it’s great and prevents bugs from happening.
Anyway, I find myself wondering what are good practices when writing a service module and I couldn’t find much, not in the wiki, nor manual, or the internet. I’ve been reading different modules, trying to get inspiration, but I haven’t seen a clear line, so I’m opening this discussion.
Naming conventions
When designing the interface, should one try to (1) “fit” to nix, or (2) try to follow the naming conventions of the app you are trying to nixify?
Like a lot of nix services expose an address and a port, but if the app, you are trying to wrap, has listen_address. Should you use listenAddress like the app or address and port, like used in nix? What about making it easy to search in the original application’s documentation? you might not find listenAddress in camelCase.
My intuition tells me that the config should map the underlying software, to make it as familiar as possible, and to be able to follow the original software documentation without having to guess, while trying to maintain nix conventions (like camelCase).
How much should a service module provide?
Let’s take an Identity provider, how many other systems should it configure? should it also configure all reverse proxies, even though it’s not necessary to run it? I understand if it had to configure a database, without which it wouldn’t run. I feel like here the future rfc 189 could come really in handy.
What patterns do you use or recommend when writing nixos modules?
I was quite impressed by kanidm.provision, it’s like a very nice pattern for applications which cannot be configured via configuration files, and usually have settings in a database (e.g: jellyfin, *arrstack, etc)
How to deal with applications that are mostly provisioned via environment variables?
I’ve seen applications using settings to actually configure the app via environments, while at the same time, exposing an environmentFile to fill the environment.
Which is confusing, right?
Should we use settings for setting files and environment to fill the environment? some applications accept both, and it would map to what one would expect from the underlying app, leaving nix as a thin abstraction.
E.g:
app.settings = {}; // freeform
app.environment = {}; // freeform
At the same time, how much the name of the underlying configuration should change?
For example, netbird is configured via environment variables like NB_LISTEN_ADDRESS.
Should one try to leave it like the original so it can be easily looked up? or should we build an abstraction on top to make it more “fitting” to nix? Like listenAddress, which then becomes upper snake case with prefix NB?
I bet having netbird.environment.NB_LISTEN_ADDRESS would make it straightforward to find in netbird docs, and it tells you A LOT, like you already know it’s configured via env variables, and the real name. On the other hand, when you search on nixos search, it might look a bit weird.
Should we wrap secrets under a secret freeform attribute set?
This might make little sense, but grouping the secrets together would make it easier to locate them, and signal to the user that this values must be protected, a beginner nix user might not recognize that setupKeyFile should be protected.
app.secrets = {}; // These automatically either go to settings or are configured safely as env variables
Provisioning secrets
I haven’t found much explanation on how to do this, it would be nice to have a guide on how to handle secrets, and how to feed them to systemd.
systemd explains clearly (creds) how to do it if the application accepts a path to a secret, but that’s it
Should we always use types.externalPath instead of types.str or types.path? A lot of modules seem to use str or path for secrets.
Would you say this rough example is acceptable to feed a secret file that can only be configured via environments?
{ config, lib, pkgs, ... }:
{
options.services.myservice = {
# ...etc...
authSecretPath = mkOption {
type = types.externalPath; # Correct type for sensitive external files
description = "Path to the file containing the authentication secret.";
example = "/run/secrets/myservice-auth";
};
};
config = mkIf cfg.enable {
systemd.services.myservice = {
script = ''
export AUTH_SECRET="$(< "$CREDENTIALS_DIRECTORY/auth_secret")"
exec ${getExe cfg.package}
'';
serviceConfig = {
# ...etc...
LoadCredential = "auth_secret:${cfg.authSecretPath}";
};
};
};
}
Any other tips or suggestions would be appreciated.