Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modular services #372170

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Modular services #372170

wants to merge 6 commits into from

Conversation

roberth
Copy link
Member

@roberth roberth commented Jan 8, 2025

I invite you to collaborate on modular services.

A lot is described in nixos/doc/manual/development/modular-services.md (see PR files), but in a nutshell:

As an example, I've ported one service, ghostunnel to have a modular service.
Some services will be more complicated, requiring more than the current facilities. These can be added.

This PR does not require that all services become modular services.

Things done

  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • For non-Linux: Is sandboxing enabled in nix.conf? (See Nix manual)
    • sandbox = relaxed
    • sandbox = true
  • Tested, as applicable:
  • Tested compilation of all packages that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage
  • Tested basic functionality of all binary files (usually in ./result/bin/)
  • 25.05 Release Notes (or backporting 24.11 and 25.05 Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
  • Fits CONTRIBUTING.md.

Add a 👍 reaction to pull requests you find important.

@github-actions github-actions bot added 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: documentation This PR adds or changes documentation 8.has: module (update) This PR changes an existing module in `nixos/` labels Jan 8, 2025
@aanderse
Copy link
Member

aanderse commented Jan 8, 2025

Some services will be more complicated, requiring more than the current facilities. These can be added.

let's elaborate on that so people who may not be as familiar with the history of this PR up to this point stand a chance at understanding this

the new system.services module in this PR currently does not currently have, but could require the ability to:

  • create system users
  • place files under /etc
  • set runtime parameters of the linux kernel, as set by sysctl
  • add packages to the global system environment
  • open ports in the system firewall

the above is just a list of the most common/useful patterns that many nixos service oriented modules use, but anyone else should feel free to edit this comment and add to it if they feel it useful to do so - as well as check off the functionality if they contribute it to this PR


thanks for getting the ball rolling on this one @roberth 🙇‍♂️

Copy link

@ibizaman ibizaman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the idea and the way services can be imported in the top-level attrsOf. I'm already looking forward to having multiple instances of a database!

From a theoretical point of view, I understand the nesting of services but I'm not sure to understand the use in practice. Would this be used to provide opinionated coupling between services? For example there would be a service grouping Nextcloud and Postgres and others to provide an out-of-the-box experience? And this would allow to split that big module into multiple ones?

You mentioned the pre-RFC on decoupling services and I'm the one that wrote it. There are some common goals here and I'd love to collaborate. One way would be to write the systemd implementation of the generic service using the structural typing I propose in the pre-RFC. But there are still questions to be elucidated there so not (yet) sure it's the ideal solution. And also, I don't want to pull the rug on "my" side for no good reason. I know you're way more proficient in the module system than I am so I'd love to get your input on how we could work together (us - the community) so that both RFCs can cooperate, merge? At least not hinder one another.

I had also a few noob questions about modules. I've been using modules a lot but not that deep.

Thanks for progressing on this!


- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521

- For now, do not add an `enable` option, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to keep the distinction. I had cases where I wanted one or the other.

options.services = mkOption {
type = types.attrsOf (
types.submoduleWith {
class = "service";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noob question: what does class do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very basic "type checking" so that you don't load a normal NixOS module where a service is expected or vice versa.
The typing is "gradual" - if you don't specify it in a module or submodule, that counts as a match. In practice, this means modules have to specify _class to get this benefit. It's not so widely used yet. I think it's a 2 or 3 year old feature, but limited usefulness and not widely advertized. Flake-parts flake.modules does set it, but that's more recent.
See also evalModules class in the manual.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense now. The manual helped also, specifically:

If the class attribute is set and non-null, the module system will reject imports with a different _class declaration.

'';
type = types.lazyAttrsOf types.deferredModule;
default = { };
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose there will also be systemd.timers and the other?

};
config = {
# Note that this is the systemd.services option above, not the system one.
systemd.services."" = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this empty service?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're prefixed by the service attribute path, or you could say "" is the suffix.

system.services."foo".systemd.service."bar" => systemd.services.foo-bar
system.services."foo".systemd.service."" => systemd.services.foo
system.services."foo".services."db".systemd.service."autovacuum" => systemd.services.foo-db-autovacuum

types.submoduleWith {
class = "service";
modules = [
./service.nix
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking I'm understanding this correctly, this is importing itself? It's to support sub-services, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. This constructs a self-referential type - see visible = "shallow"; which makes that practical. It makes arbitrarily nested services possible. "Infinite types" are ok in the module system, with no noteworthy disadvantages.

In practice, 4 is already very deep, I think, but we don't need to enforce anything here.

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/pre-rfc-decouple-services-using-structured-typing/58257/16

@roberth
Copy link
Member Author

roberth commented Jan 8, 2025

I understand the nesting of services but I'm not sure to understand the use in practice. Would this be used to provide opinionated coupling between services? For example there would be a service grouping Nextcloud and Postgres and others to provide an out-of-the-box experience? And this would allow to split that big module into multiple ones?

This can be used for groupings of multiple "off-the-shelf" services into a common configuration, or for non-trivial services that consist of multiple executables, such as apache cassandra, or even a mix, such as pulsar.

I think the distinguishing factor is that when a sub-service is an off the shelf one, such as a database, admins may prefer to have a single db service with multiple apps that consume it. @ibizaman this is where I think the ideas you've worked out apply.
This could be a configurable thing as well - a boolean in the "parent" service that controls whether sub-services are generated to fill the requests.

I'd love to get your input

Of course! I will have a look soon-ish.

a few [...] questions

Happy to help :)

@ibizaman
Copy link

ibizaman commented Jan 8, 2025

I think the distinguishing factor is that when a sub-service is an off the shelf one, such as a database, admins may prefer to have a single db service with multiple apps that consume it.

Agreed. The end user should have the choice of either an instance per service or one instance for all of them.

This can be used for groupings of multiple "off-the-shelf" services into a common configuration, or for non-trivial services that consist of multiple executables, such as apache cassandra, or even a mix, such as pulsar.

For non-trivial packages with multiple binaries, I indeed don't see how contracts can apply. But if we're talking about multiple services working together, this is where our work could be merged and become even more powerful. If one write a group of services, like you allow, and the link between those services are contracts, then we can easily swap one of those services for another one.

@ElvishJerricco
Copy link
Contributor

system.services is probably too close to systemd.services. People already get confused by toplevel services and systemd.services. Not sure what would be better though :/

@roberth
Copy link
Member Author

roberth commented Jan 9, 2025

Perhaps systemServices is sufficiently distinct?

Adding any extra words is probably a net negative.

Explaining extra words

System

It's the place where service modules are connected to the system (as in e.g. systemd --system).
Perhaps this term could be replaced, but I can't think of a synonym for "system services" that is nearly as good.
Host comes close, but that gets weird with VMs or "hosting a configuration" in the sense of it being deployed onto something.

Module

Putting the word module or modular in there would be wrong, because that option is where the modules are consumed and turned into a concrete configuration, such that the modules become irrelevant.
For example, this would be valid naming:

{ serviceModules, ... }:
{
  systemServices.etcd = { imports = [ serviceModules.etcd ]; };
}

Portable

Portability is similarly irrelevant, because it is a property of a module, and it is not even a required property for these services.

NixOS

Putting NixOS in the name, besides being redundant, would seem to imply that the modules are not portable, which is also not necessarily the case. (though I did shift the goalposts a bit for that second argument)

An intriguing thought is that "subsystem" could replace "service", which seems appropriate when these things encompass more than a process and its execution settings, while also giving space for multiple services to exist within it.
However, "subsystem" hardly gives any intuition for what the feature is for, and the "system" part of "subsystem" conflicts with the system role of these services.
It also reminds me that a current flaw is that process is required, even if you want to just compose a "subsystem" from multiple services instead of a custom process. I guess a nullOr submodule would fix that.

@bew
Copy link
Contributor

bew commented Jan 10, 2025

About naming, it seems you haven't considered changing the 'services' part 🤔

Systemd base resource names are units not services, and this terminology works for all resource types that systemd supports, like services, timer, target, socket, mount...

So in our case we could have system.units ?

Although this might easily increase the coverage support for types of manageable systemd resources, it might be too much for this proposal 🤔

@ElvishJerricco
Copy link
Contributor

So in our case we could have system.units ?

Similar problem because we do have systemd.units :P

@bjornfor
Copy link
Contributor

So in our case we could have system.units ?

Similar problem because we do have systemd.units :P

systemUnits then?

@roberth
Copy link
Member Author

roberth commented Jan 10, 2025

units

This feature isn't really about systemd units. For instance, the process options are applicable to any init system or process supervisor, and it is possible to write modular services that support all such systems (by sticking to portable options) or supporting specific systems by feature testing (config = ... // optionalAttrs (options?systemd) ...; or other, nicer mechanisms).
Similarly, assertions and warnings are a configuration management facility that goes beyond systemd, but more significantly, so are the capabilities listed by @aanderse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: documentation This PR adds or changes documentation 8.has: module (update) This PR changes an existing module in `nixos/` 10.rebuild-darwin: 1-10 10.rebuild-linux: 1-10
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants