aboutsummaryrefslogtreecommitdiff
path: root/blog/nix-flakes-3-2022-04-07.markdown
blob: 342b29f8ed228e089b754c08ba2f3399acde09ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
---
title: "Nix Flakes: Exposing and using NixOS Modules"
date: 2022-04-07
series: nix-flakes
tags:
 - nixos
vod:
  twitch: https://www.twitch.tv/videos/1437346416
  youtube: https://youtu.be/wCZ9SwmgSck
---

Nix flakes allow you to expose NixOS modules. NixOS modules are templates for
system configuration and they are the basis of how you configure NixOS. Today
we're going to take our Nix flake [from the last
article](/blog/nix-flakes-2-2022-02-27) and write a NixOS module for it so that
we can deploy it to a container running locally. In the next post we will deploy
this to a server.

[If you haven't read <a href="/blog/series/nix-flakes">the other articles in
this series</a>, you probably should. This article builds upon the previous
ones.](conversation://Mara/hacker)

NixOS modules are building blocks that let you configure NixOS servers. Modules
expose customizable options that expand out into system configuration.
Individually, each module is fairly standalone and self-contained, but they
build up together into your server configuration like a bunch of legos build
into a house. Each module describes a subset of your desired system
configuration and any options relevant to that configuration.

[You can think about them like Ansible playbooks, but NixOS modules describe the
desired end state instead of the steps you need to get to that end
state. It's the end result of evaluating all of your options against all of the
modules that you use in your configuration.](conversation://Mara/hacker)

NixOS modules are functions that take in the current state of the system and
then return things to add to the state of the system. Here is a basic NixOS
module that enables [nginx](https://nginx.org/):

```nix
{ config, pkgs, lib, ... }:

{
  config = {
    services.nginx.enable = true;
  };
}
```

This function takes in the state of the world and returns additions to the state
of the world. This will use the nginx module that ships with NixOS to give you a
basic nginx setup that has the upstream default configuration in it.

NixOS has a way to run other instances of NixOS with [NixOS
containers](https://nixos.org/manual/nixos/stable/index.html#ch-containers). We
can use them to test our NixOS module as we write it.

[This probably won't work on a non-NixOS machine. You will need to
install NixOS in order to test this. For an easy way to do this, see <a
href="https://github.com/elitak/nixos-infect">nixos-infect</a>, a script you can
put into a cloudconfig when spinning up a new server. You can also <a
href="https://nixos.org/manual/nixos/stable/index.html#sec-installation">install
NixOS manually</a> in a VM, but for now it may be better to use a cloud server
as the path of least resistance. Installing NixOS with a flake will be a part of
a future article in this series.](conversation://Mara/hacker)

In Nix you can merge two attribute sets using the `//` operator. This allows you
to add two attribute sets into one larger one, such as like this:

```
nix-repl> { foo = 1; } // { bar = 2; }
{ bar = 2; foo = 1; }
```

<xeblog-conv name="Mara" mood="hacker">
Important pro tip: the merge operator is NOT recursive. If you try to do
something like:

```
nix-repl> foo = { bar = { baz = "foo"; }; }
nix-repl> (foo // { bar = { spam = "eggs"; }; }).bar
```

You will get:

```
{ spam = "eggs"; }
```

And not:

```
{ baz = "foo"; spam = "eggs"; }
```

This is because the `//` operator prefers things in the right hand side over the
left hand side if both conflict. To recursively merge two attribute sets (using
all elements from both sides), use
[lib.recursiveUpdate](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.attrsets.recursiveUpdate):

```
nix-repl> (pkgs.lib.recursiveUpdate foo bar).bar
{ baz = "foo"; spam = "eggs"; }
```

</xeblog-conv>

We will use this to add the container configuration to the flake at the end of
the flake.nix file. We need to do this because the upper part of the flake with
the `forAllSystems` call will generate a bunch of system-specific attributes for
each system we support. NixOS configurations don't support this level of
granularity.

At the end of your flake.nix (just before the final closing `}`), there should
be a line that looks like this:

```nix
      });
```

This is what terminates the `outputs` declaration from all the way at the top.
In order to add the container configuration, you should change this to look like
this:

```nix
      }) // {
      
      };
```

Then we can add the container configuration to the flake:

```nix
}) // {
  nixosConfigurations.container = nixpkgs.lib.nixosSystem {
    system = "x86_64-linux";
    modules = [
      ({pkgs, ...}: {
        # Only allow this to boot as a container
        boot.isContainer = true;
        networking.hostName = "gohello";

        # Allow nginx through the firewall
        networking.firewall.allowedTCPPorts = [ 80 ];

        services.nginx.enable = true;
      })
    ];
  };
};
```

This will create a container (with the hostname "gohello") that starts nginx and
allows traffic to go to nginx on TCP port 80. You can start up the container
with the `nixos-container` command:

```console
$ sudo nixos-container create gohello --flake .#container
host IP is 10.233.1.1, container IP is 10.233.1.2
```

Then you can start the container with this command:

```console
$ sudo nixos-container start gohello
```

And then we can try to connect to nginx to see if it's working:

```console
$ curl http://10.233.1.2
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {}
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
                                                                           
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
                                                                           
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

We have nginx!

Now that we have our container to test with, let's write the configuration for
the service. At a basic level we need the following things:

- A systemd unit for orchestrating the HTTP server process
- nginx configuration to reverse proxy to that HTTP server

Above the container definition, add this basic NixOS module template:

```nix
nixosModule = { config, lib, pkgs, ... }:
  with lib;
  let cfg = config.xeserv.services.gohello;
  in {
    options.xeserv.services.gohello = {
      enable = mkEnableOption "Enables the gohello HTTP service";
    };

    config = mkIf cfg.enable {
    };
  };
```

This will create a NixOS module that will only be enabled when the configuration
setting `xeserv.services.gohello.enable` is set to `true`. Everything else we do
here will build on this.

[You can and probably do want to change the namespace `xeserv` here, it is a
placeholder that is not likely to conflict with anything
else.](conversation://Mara/happy)

Create a basic systemd service with this template:

```nix
config = mkIf cfg.enable {
  systemd.services."xeserv.gohello" = {
    wantedBy = [ "multi-user.target" ];

    serviceConfig = let pkg = self.packages.${system}.default;
    in {
      Restart = "on-failure";
      ExecStart = "${pkg}/bin/web-server";
      DynamicUser = "yes";
      RuntimeDirectory = "xeserv.gohello";
      RuntimeDirectoryMode = "0755";
      StateDirectory = "xeserv.gohello";
      StateDirectoryMode = "0700";
      CacheDirectory = "xeserv.gohello";
      CacheDirectoryMode = "0750";
    };
  };
};
```

<xeblog-conv name="Mara" mood="hacker">
NOTE: If you have been following along since before this article was published,
you will want to be sure to do the following things to your copy of gohello: 

* Move the definition of `defaultPackage` into the `packages` attribute set with
  the name `default`
* Update `defaultApp` and the other entries to point to
  `self.packages.${system}.default` instead of `self.defaultPackage.${system}`

We have updated previous articles and the template accordingly. Annoyingly it
seems that this change is new enough that it isn't totally documented on the
NixOS wiki. We are working on fixing this.

</xeblog-conv>

This will do the following things:

- Start the service on boot (`multi-user.target` fires once the system is "fully
  booted" and the network is active)
- Automatically restarts the service when it crashes
- Starts our `web-server` binary when running the service
- Creates a random, unique user account for the service (see
  [here](http://0pointer.net/blog/dynamic-users-with-systemd.html) for more
  information on how/why this works)
- Creates temporary, home and cache directories for the service, makes sure that
  random user has permission to use them (with the specified directory modes
  too)
- Enables the service automatically

Then you need to add the nginx configuration. We want this application to have
its own virtual host, so we will need to add that as a configuration option
under the `enable` option:

```nix
domain = mkOption rec {
  type = types.str;
  default = "gohello.local.cetacean.club";
  example = default;
  description = "The domain name for gohello";
};
```

[Pro tip: `anything.local.cetacean.club` points to `127.0.0.1`. You can use this
when testing things.](conversation://Mara/happy)

And then we can add the nginx configuration under the systemd service definition:

```nix
services.nginx.virtualHosts.${cfg.domain} = {
  locations."/" = { proxyPass = "http://127.0.0.1:3031"; };
};
```

Your module should look like this:

```nix
nixosModule = { config, lib, pkgs, ... }:
  with lib;
  let cfg = config.xeserv.services.gohello;
  in {
    options.xeserv.services.gohello = {
      enable = mkEnableOption "Enables the gohello HTTP service";

      domain = mkOption rec {
        type = types.str;
        default = "gohello.local.cetacean.club";
        example = default;
        description = "The domain name for gohello";
      };
    };

    config = mkIf cfg.enable {
      systemd.services."xeserv.gohello" = {
        wantedBy = [ "multi-user.target" ];

        serviceConfig = let pkg = self.packages.${pkgs.system}.default;
        in {
          Restart = "on-failure";
          ExecStart = "${pkg}/bin/web-server";
          DynamicUser = "yes";
          RuntimeDirectory = "xeserv.gohello";
          RuntimeDirectoryMode = "0755";
          StateDirectory = "xeserv.gohello";
          StateDirectoryMode = "0700";
          CacheDirectory = "xeserv.gohello";
          CacheDirectoryMode = "0750";
        };
      };

      services.nginx.virtualHosts.${cfg.domain} = {
        locations."/" = { proxyPass = "http://127.0.0.1:3031"; };
      };
    };
  };
```

[The service name is overly defensive. It's intended to avoid conflicting with
any other unit on the system named `gohello.service`. Feel free to remove this
part, it is really just defensive devops by design to avoid name
conflicts.](conversation://Mara/hacker)

Then you can add it to the container by importing our new module in its
configuration and activating the gohello service:

```nix
nixosConfigurations.container = nixpkgs.lib.nixosSystem {
  system = "x86_64-linux";
  modules = [
    self.nixosModule
    ({ pkgs, ... }: {
      # Only allow this to boot as a container
      boot.isContainer = true;

      # Allow nginx through the firewall
      networking.firewall.allowedTCPPorts = [ 80 ];

      services.nginx.enable = true;

      xeserv.services.gohello.enable = true;
    })
  ];
};
```

Then you can update the container's configuration with this command:

```console
$ sudo nixos-container update gohello --flake .#container
reloading container...
```

And finally make a request to the gohello service running in that container:

```console
$ curl http://10.233.1.2 -H "Host: gohello.local.cetacean.club"
hello world :)
```

<xeblog-conv name="Mara" mood="hacker">
Exercises for the reader:

Try adding a [nixos
option](https://nixos.org/manual/nixos/stable/index.html#sec-writing-modules)
that correlates to the `--bind` flag that `gohello` uses as the TCP
address to serve HTTP from. You will want to have the type be
`types.port`. If you are stuck, see
[here](https://github.com/Xe/nixos-configs/tree/master/common/services) for inspiration.

Also try adding `AmbientCapabilities = "CAP_NET_BIND_SERVICE"` and
`CapabilityBoundingSet = "CAP_NET_BIND_SERVICE"` to your `serviceConfig` and
bind `gohello` to port 80 without nginx involved at all.

</xeblog-conv>

You can delete this container with `sudo nixos-container destroy gohello` when
you are done with it.

These are the basics on how to use NixOS modules. Everything else you can do
with them builds off of these fundamental ideas. Modules are templates that
coordinate packages and configuration into your desired system state. Containers
can let you test out modules without having to add them to your currently
running system. Modules declare options and emit configuration based on those
options.

You can also consume NixOS modules from flakes using the input system, however I
will go into more details about this at a later date. If you want more examples
of NixOS modules, I would suggest checking out my
[nixos-configs](https://github.com/Xe/nixos-configs) repository. I have nearly
everything neatly modularized and configurable. If you see anything in there
that is confusing to you, please [reach out](/contact) and ask. I am happy to
answer your questions and your feedback will help me write future posts in this
series.

I also have my "next generation" flakes-based configuration experiments
[here](https://tulpa.dev/cadey/nixos-configs) if you want to read through those.
I have still been porting over things piecemeal, so it is not a complete replica
of my existing configuration.

Next time I will cover how to install NixOS to a server and deploy system
configurations using [deploy-rs](https://github.com/serokell/deploy-rs). This
will allow you to have your workstation build configuration for your servers and
push out all the changes from there.

---

Many thanks to Open Skies for being my fearless editor that helps make these
things shine.

In part of this post I use my new Xeact-powered HTML component for some of the
conversation fragments, but the sizing was off on my iPhone when I tested it. If
you know what I am doing wrong, please [get in touch](/contact).