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