"Variables" for a system

When developing a new system I regularly find myself struggling with details that differ between a deployment in a VM (e.g. through the awesome nixos-shell or a nixops virtualbox deployment) and a real productive deployment.
Usually it’s just a string or a simple boolean that needs to be replaced or altered to fit the environment:

services.nginx.virtualHosts."example.com" = {
enableACME = true;
forceSSL = true;
root = "/srv/www/example.com";
}

I would like to change that to something like the following and just have a vm.nix and prod.nix declare these variables:

services.nginx.virtualHosts."${variables.baseDomain}" = {
enableACME = ${variables.useSSL};
forceSSL = ${variables.useSSL};
root = "/srv/www/example.com";
}

with for example a prod.nix looking like the following:

{
includes = [ ./mywebserver.nix ];
variables = {
baseDomain = "example.com";
useSSL = true; }
}

and a vm.nix looking like the following:

{ 
includes = [ ./mywebserver.nix ];
variables = {
baseDomain = "mydevsystem.example.org";
useSSL = false; }
}

Did I miss some easy way to implement something like this? How do others solve this?

2 Likes

You can create a module for that: https://github.com/Mic92/dotfiles/blob/c7a6da01b9e93d836eafebc49d61232efc823416/nixos/vms/modules/retiolum.nix

then just refer to its value via config.optionname.

1 Like

That means I’d have to additionally create an option with every possible variable I’d use, so everytime I find a new value that differs, I’d have an additional point where I have to change it, instead of just the point where I use it and the actual machine definitions.

Well you can also have a single option of type attribute set that contains all variables. Then you would not have default values or type-checking but a little less to type.

2 Likes

So I’d basically just declare a module variables = mkOption {type = types.attributes} and then just set and use variables.baseDomain = “whatever” and access them that way as well.
That sounds good. Thanks!

      variables = mkOption {
        type = types.attrs;
        default = { };
      };
1 Like

Just for the sake of documentation, here’s what I’m using now:
variables.nix:

{ ... }:
{
  options = {
    variables = lib.mkOption {
      type = lib.types.attrs;
      default = { };
    };
  };
}

prod.nix:

{ config, ... }:
{
  imports = [ ./configuration.nix ];
  config.variables.useSSL = true;
}

vm.nix:

{ config, ... }:
{
  imports = [ ./configuration.nix ];
  config.variables.useSSL = false;
}

and configuration.nix:

{ config, pkgs, ... }:
{
  imports =
    [
      ./variables.nix
      ./users.nix
      ./myservice.nix
    ];
  [...]
}
4 Likes

Thank you, @tokudan for the code samples. I think I am going to reorganize my nixos dotfiles based on them.

It’d be preferable to be able to import files based on config values. For example, if config.enableVPN == true, I’d like to import ./vpn.nix.

When I try to do so, however I get:

error: infinite recursion encountered, at /home/jdoe/.nixpkgs/lib/modules.nix:62:71
(use '--show-trace' to show detailed location information)

Following your sample style, my configuration.nix [1] would look like this:

{ config, pkgs, ... }:
{
  imports = buildins.concatLists [
      [
        ./variables.nix
        ./users.nix
        ./myservice.nix
      ]
      # the problematic line:
      (if config.enableVPN then [./vpn.nix] else [])
    ];
  [...]
}

[1] not the NixOS /etc/nixos/configuration.nix but rather the file holding the configurable nix expressions

if is not lazy enough in this case. You should consider mkIf as per NixOS 23.11 manual | Nix & NixOS.

Hmmm, @layus, that makes sense, but would you mind providing a code sample?

If I had to do it myself, I would make the import unconditional, and add the condition in the module itself, like most modules do. It would be easier if you had a minimal (non) working example :wink:

in configuration.nix:

    imports = [ ./vpn.nix ];
    
    variables.vpn.enable = true;

and in vpn.nix

let
    cfg = config.variables.vpn;
in { 
    options = {
        variables.vpn.enable = mkEnable ...;
    };

    config = mkIf cfg.enable {
        // configure the vpn here;
    };
}

@tokudan example is nice, but can be improved even more

{ config, ... }:
{
  options.variables = lib.mkOption {
    type = lib.types.attrs;
    default = { };
  };
  config._module.args.variables = config.variables;
}

vm.nix:

# this can even be JSON (if you don't use secrets here), loaded with `builtins.fromJSON`
{
  variables.useSSL  = false;
}

prod.nix

{
  variables.useSSL  = true;
}

myservice.nix:

{ config, pkgs, variables, ... }:
{
  services.nginx.useSSL = variables.useSSL;
}
1 Like

What does this line do?

It puts variables into scope of a module, alongside with config, lib, pkgs. For example, pkgs is put into scope in

Another example is relatively unknown utils module arg:

You can use this to completely forbid changing variables inside modules, allow only read access, for example:

let
  vars_staging = {
    useSSL = false;
  };

  vars_prod = {
    useSSL = true;
  };

  my_service = { vars, ...}: {
     services.nginx.useSSL = vars.useSSL;
  };

  my_config = vars: {
    imports = [ my_service.nix ];
    config._module.args.vars = vars;
  };

in {
  staging = (import <nixpkgs/nixos> { configuration = my_config vars_staging; }).system;
  production = (import <nixpkgs/nixos> { configuration = my_config vars_prod; }).system;
}
5 Likes

I am not 100% sure but for others reading this- I couldn’t get this approach working for imports. I still get the infinite recursion error.

It’s impossible to make imports depend on anything from config, because everything in config depends on imports itself (since any module can change any other option). This is where the infinite recursion comes from.

You can think of the module system working like this:

  1. First all modules are resolved by recursively looking at imports
  2. Then all options are determined by looking at options of all the collected modules
  3. Finally the resulting config is evaluated by combining all the config definitions according to the options that were defined

And generally these have to happen sequentially. Especially step 1 has to happen before 2 and 3.

3 Likes