444 lines
14 KiB
Markdown
444 lines
14 KiB
Markdown
---
|
|
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).
|