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