1{ config, lib, pkgs, ... }:
2with lib;
3let
4 cfg = config.services.nomad;
5 format = pkgs.formats.json { };
6in
7{
8 ##### interface
9 options = {
10 services.nomad = {
11 enable = mkEnableOption "Nomad, a distributed, highly available, datacenter-aware scheduler";
12
13 package = mkPackageOption pkgs "nomad" { };
14
15 extraPackages = mkOption {
16 type = types.listOf types.package;
17 default = [ ];
18 description = ''
19 Extra packages to add to {env}`PATH` for the Nomad agent process.
20 '';
21 example = literalExpression ''
22 with pkgs; [ cni-plugins ]
23 '';
24 };
25
26 dropPrivileges = mkOption {
27 type = types.bool;
28 default = true;
29 description = ''
30 Whether the nomad agent should be run as a non-root nomad user.
31 '';
32 };
33
34 enableDocker = mkOption {
35 type = types.bool;
36 default = true;
37 description = ''
38 Enable Docker support. Needed for Nomad's docker driver.
39
40 Note that the docker group membership is effectively equivalent
41 to being root, see https://github.com/moby/moby/issues/9976.
42 '';
43 };
44
45 extraSettingsPaths = mkOption {
46 type = types.listOf types.path;
47 default = [ ];
48 description = ''
49 Additional settings paths used to configure nomad. These can be files or directories.
50 '';
51 example = literalExpression ''
52 [ "/etc/nomad-mutable.json" "/run/keys/nomad-with-secrets.json" "/etc/nomad/config.d" ]
53 '';
54 };
55
56 extraSettingsPlugins = mkOption {
57 type = types.listOf (types.either types.package types.path);
58 default = [ ];
59 description = ''
60 Additional plugins dir used to configure nomad.
61 '';
62 example = literalExpression ''
63 [ "<pluginDir>" pkgs.nomad-driver-nix pkgs.nomad-driver-podman ]
64 '';
65 };
66
67 credentials = mkOption {
68 description = ''
69 Credentials envs used to configure nomad secrets.
70 '';
71 type = types.attrsOf types.str;
72 default = { };
73
74 example = {
75 logs_remote_write_password = "/run/keys/nomad_write_password";
76 };
77 };
78
79 settings = mkOption {
80 type = format.type;
81 default = { };
82 description = ''
83 Configuration for Nomad. See the [documentation](https://www.nomadproject.io/docs/configuration)
84 for supported values.
85
86 Notes about `data_dir`:
87
88 If `data_dir` is set to a value other than the
89 default value of `"/var/lib/nomad"` it is the Nomad
90 cluster manager's responsibility to make sure that this directory
91 exists and has the appropriate permissions.
92
93 Additionally, if `dropPrivileges` is
94 `true` then `data_dir`
95 *cannot* be customized. Setting
96 `dropPrivileges` to `true` enables
97 the `DynamicUser` feature of systemd which directly
98 manages and operates on `StateDirectory`.
99 '';
100 example = literalExpression ''
101 {
102 # A minimal config example:
103 server = {
104 enabled = true;
105 bootstrap_expect = 1; # for demo; no fault tolerance
106 };
107 client = {
108 enabled = true;
109 };
110 }
111 '';
112 };
113 };
114 };
115
116 ##### implementation
117 config = mkIf cfg.enable {
118 services.nomad.settings = {
119 # Agrees with `StateDirectory = "nomad"` set below.
120 data_dir = mkDefault "/var/lib/nomad";
121 };
122
123 environment = {
124 etc."nomad.json".source = format.generate "nomad.json" cfg.settings;
125 systemPackages = [ cfg.package ];
126 };
127
128 systemd.services.nomad = {
129 description = "Nomad";
130 wantedBy = [ "multi-user.target" ];
131 wants = [ "network-online.target" ];
132 after = [ "network-online.target" ];
133 restartTriggers = [ config.environment.etc."nomad.json".source ];
134
135 path = cfg.extraPackages ++ (with pkgs; [
136 # Client mode requires at least the following:
137 coreutils
138 iproute2
139 iptables
140 ]);
141
142 serviceConfig = mkMerge [
143 {
144 DynamicUser = cfg.dropPrivileges;
145 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
146 ExecStart =
147 let
148 pluginsDir = pkgs.symlinkJoin
149 {
150 name = "nomad-plugins";
151 paths = cfg.extraSettingsPlugins;
152 };
153 in
154 "${cfg.package}/bin/nomad agent -config=/etc/nomad.json -plugin-dir=${pluginsDir}/bin" +
155 concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths +
156 concatMapStrings (key: " -config=\${CREDENTIALS_DIRECTORY}/${key}") (lib.attrNames cfg.credentials);
157 KillMode = "process";
158 KillSignal = "SIGINT";
159 LimitNOFILE = 65536;
160 LimitNPROC = "infinity";
161 OOMScoreAdjust = -1000;
162 Restart = "on-failure";
163 RestartSec = 2;
164 TasksMax = "infinity";
165 LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;
166 }
167 (mkIf cfg.enableDocker {
168 SupplementaryGroups = "docker"; # space-separated string
169 })
170 (mkIf (cfg.settings.data_dir == "/var/lib/nomad") {
171 StateDirectory = "nomad";
172 })
173 ];
174
175 unitConfig = {
176 StartLimitIntervalSec = 10;
177 StartLimitBurst = 3;
178 };
179 };
180
181 assertions = [
182 {
183 assertion = cfg.dropPrivileges -> cfg.settings.data_dir == "/var/lib/nomad";
184 message = "settings.data_dir must be equal to \"/var/lib/nomad\" if dropPrivileges is true";
185 }
186 ];
187
188 # Docker support requires the Docker daemon to be running.
189 virtualisation.docker.enable = mkIf cfg.enableDocker true;
190 };
191}