"Variables" for a system


#1

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

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.


#3

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.


#4

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.


#5

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!


#6
      variables = mkOption {
        type = types.attrs;
        default = { };
      };

#7

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
    ];
  [...]
}

#8

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


#9

if is not lazy enough in this case. You should consider mkIf as per https://nixos.org/nixos/manual/#sec-option-definitions-delaying-conditionals.


#10

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


#11

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;
    };
}

#12

@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;
}

#13

What does this line do?


#14

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;
}