Quick sketch of an idea for a more modular nixops

I was wondering if there’s been any discussions on making nixops itself a little bit more modular in a similar sense to https://www.youtube.com/watch?v=I_KCd46B8Mw?

I’ve been trying out something that seems like it’s been working quite well in my remote-builder-network definition.

Specifically this includes:

  • lib.nixopsNetwork is a function similar to lib.nixosSystem that builds a regular nixops configuration, but can include imports = [ ... ] and options in the network definition.
  • nixopsModules (similar to nixosModules), exposes network modules from a flake.

Curious if others feel that something like this might help with modernizing nixops in the new world of flakes?

To support spot fleets, auto-scaling groups etc in future, I feel as though having instances explicit in the schema could be helpful (see example below)?

Also, it might be useful to pave the way for e.g. lib.terraformNetwork in the future.


Rough example

Roughly the layout I have looks like this:

Flake:

# flake.nix

{
  description = "Flake with nixops modules";

  inputs = {
    # ...
  };

  outputs = { self, ... }: {

    nixosModules = {
      myNode = ./nixos-modules/my-node;
    };

    # nixopsModules is similar to nixosModules
    nixopsModules = {
      myNetwork = ./nixops-modules/my-network;
    };
    
    nixopsConfigurations.default =
      let
        # lib.nixopsNetwork is similar to lib.nixosSystem
        lib = { nixopsNetwork = import ./lib/nixops-network.nix; };
      in
        lib.nixopsNetwork {
          modules = [ ./nixops-configurations ];
          specialArgs.flake = self;
          inherit nixpkgs;
        };

  };
}

Lib

# lib/nixops-network.nix

{
  nixopsNetwork = { nixpkgs, modules, specialArgs }:
    let
      baseModule = {
        options = with lib.types; {
          network = lib.mkOption { type = attrsOf anything; };
          resources = lib.mkOption { type = attrsOf anything; };
          instances = lib.mkOption { type = attrsOf anything; };
          nixpkgs = lib.mkOption { type = anything; };
        };
      };

      networkConfig =
        (lib.evalModules {
          modules = [ baseModule ] ++ modules;
          inherit specialArgs;
        }).config;
    in
      { inherit (networkConfig) network resources nixpkgs; } // networkConfig.instances;
}

Configuration

# nixops-configurations/default.nix

{ flake, ... }:
{
  imports = [
    flake.nixopsModules.myNetwork
  ];

  myNetwork = {
    name = "my-network";
    nodeConfigurations = {
      node-1 = flake.nixosModules.node;
    };
  };

  instances.defaults = { pkgs, lib, ... }: {
    deployment.keys = {
      # ...
    };
  };

  nixpkgs = flake.inputs.nixpkgs;
}

Modules

# nixos-modules/my-node.nix
{ pkgs
, config
, lib
, ...
}:

let
  myNetwork = config.myNetwork;
  myNode = config.myNode;

  myNodeOptions =  {
    name = lib.mkOption {
      type = lib.types.str;
      default = myNetwork.name;
    };
  };
in
{
  options.myNode = myNodeOptions;

  config = {
    networking.firewall = {
      enable = true;
    };

    boot.loader.grub.device = "/dev/xvda";

    fileSystems."/" = {
      label = "nixos";
      fsType = "ext4";
    };

    # more config ...
  };
}
# nixops-modules/my-network.nix

{ flake
, config
, lib
, ...
}:

let
  myNetwork = config.myNetwork;

  myNetworkOptions =  {
    name = lib.mkOption {
      type = lib.types.str;
      default = "my-network";
    };

    nodeConfigurations = lib.mkOption {
      type = lib.types.attrsOf lib.types.anything; # One or more nixos configurations
      description = "NixOS configuration of each node.";
    };
  };

  awsInstances = import ./aws/instances.nix {
    networkName = myNetwork.name;
    inherit (myNetwork.aws) region zone;
  };

  mkInstance = configuration: { resources, lib, ... }@args: {
      imports = [
        configuration
      ];
      options.myNetwork = {
        inherit (myNetworkOptions) name;
      };
      config = {
        deployment = awsInstances.myNode args;
        myNetwork = {
          inherit (config.myNetwork) name;
        };
      };
    };
in
{
  # Import AWS resources
  imports = [
    ./aws/resources
  ];

  options.myNetwork = myNetworkOptions;

  config = {
    network.description = lib.mkDefault "${myNetwork.name} network";

    instances = lib.mapAttrs (_: mkInstance) myNetwork.nodeConfigurations;
  };
}
1 Like