• 2 Posts
  • 46 Comments
Joined 2 years ago
cake
Cake day: June 14th, 2023

help-circle







  • At their most general, they are “data processors.” In common parlance, they’re often called “algorithms,” although some folks insist that that is reserved for programs with trivial control flow. For disambiguation and comparison:

    • A service is an API surface and a contract promising that the surface has certain behaviors; data processing may be part of how the API is implemented. In practice, a service is e.g. an HTTPS endpoint and an OpenAPI specification.
    • A capability is a copyable token which simultaneously authorizes its holder to perform an action and designates the holder as having the authority to perform that action. This won’t be part of your normal curriculum and training; see this post for an introduction, or this story for motivation.
    • A controller is a modulator for a (distributed) system. Typically a controller is anything which is actuated by a control loop, although sometimes a controller can sit outside of the system. Common examples include MVC patterns, k8s components, and video-game controllers.


  • Oh, right, monoids! Yes, you understand correctly.

    A monoid is a collection of objects that has some sort of addition and zero. (Depending on your maths background, it might equivalently have some sort of multiplication and unit.) Addition must be associative, and addition with zero must not have any effect. Monoids let us think of a system as built from a sequence of operations; each operation adds to the system, preparing its state incrementally.

    Sometimes monoids are commutative, which means that the order of additions is irrelevant to the result. Commutative monoids let us think of a system as built from a collection of operations without worrying about the order in which those operations are applied.

    NixOS modules (and HM modules, etc.) are commutative monoids. The zero is {}. The module system lets options declare their own monoids which ride along, like my example of allowedTCPPorts. Because we can combine sets of port numbers (with set union) and get more sets, we can factor a set of ports into many smaller subsets and put each one in their own file. Here’s my shortest module, for an internal Docker registry, docker-registry.nix:

    {
      networking.firewall.allowedTCPPorts = [ 5000 ];
      services.dockerRegistry = {
        enable = true;
        enableGarbageCollect = true;
      };
    }
    

  • I’m adding some code snippets from my homelab’s flake. Minor details are changed. Note how I have a core.nix and also separate files for adding Avahi (zeroconf) and SSH, and for fixing bufferbloat. I could have them as one file, but it’s easier to come back to them after several years this way. (bufferbloat.nix was last changed in December 2021, for example.)

    I know that some of this code style probably seems weird. Think of it as heavily inspired by Puppet, Chef, Ansible, HCL, etc.; when we are configuring a system, it is very very nice to be able to comment out a single line at a time.

    Click to see code!

    Some common modules, bundled into a NixOS module:

        commonModules = {
          imports = [
            nixpkgs.nixosModules.notDetected
            ./avahi.nix
            ./bufferbloat.nix
            ./core.nix
            ./ssh.nix
          ];
          nix.registry.nixpkgs.flake = self.inputs.nixpkgs;
          nixpkgs.config.packageOverrides = pkgs: {
            mumble = pkgs.mumble.override {
              pulseSupport = true;
            };
          };
          users.users.corbin = {
            isNormalUser = true;
            extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
          };
        };
    

    A NixOS machine definition:

          joker = nixpkgs.lib.nixosSystem {
            inherit system;
            modules = [
              commonModules
              ./joker/configuration.nix
              ./gl.nix
              ./sound.nix
              ./wifi.nix
              ./xserver.nix
            ];
          };
    

  • At scale, you’ll appreciate explicitly spelling out your imports. I currently have 23 importable files, of which two are mutually incompatible (headless vs. Xorg). I don’t want a glob over these files because no machine can have all of them; indeed, most machines only have like five imports from the list.

    What might be more interesting to you is a common collection of modules which must be imported everywhere. To achieve this, I explicitly declare a commonModules at the top of my flake and reuse it in each machine definition. Another approach might be a common.nix module which recursively contains the common modules as its own imports.

    Finally, it doesn’t “defeat[] the point of separating” expressions into multiple files to avoid globbing over them. Because NixOS/HM modules are monoidal, they often factor nicely. When you have a dozen different services, you could stuff all of them into one file with one networking.firewall.allowedTCPPorts if you wanted, or you could put each service into its own file and let each module bring its own port to the combined configuration. The latter is easier at scale; I have nine modules declaring TCP ports and five machine-specific TCP ports as well, and it would be a pain to put all of them in one location.