From 449ddabce16d68d2b9fc6522bf03ff9a9bacfcec Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 6 Apr 2022 21:43:45 -0400 Subject: [PATCH] Nix flakes 3 (#453) * blog: add third nix flakes post Signed-off-by: Xe * make nix flakes post 3 better, thanks open Signed-off-by: Xe Iaso --- blog/nix-flakes-1-2022-02-21.markdown | 13 +- blog/nix-flakes-2-2022-02-27.markdown | 12 +- blog/nix-flakes-3-2022-04-07.markdown | 443 ++++++++++++++++++++++++++ 3 files changed, 456 insertions(+), 12 deletions(-) create mode 100644 blog/nix-flakes-3-2022-04-07.markdown diff --git a/blog/nix-flakes-1-2022-02-21.markdown b/blog/nix-flakes-1-2022-02-21.markdown index ae87f3d..7b20dc1 100644 --- a/blog/nix-flakes-1-2022-02-21.markdown +++ b/blog/nix-flakes-1-2022-02-21.markdown @@ -136,7 +136,10 @@ Then you can look at `flake.nix` to see what's up: pkgs = nixpkgsFor.${system}; in { - go-hello = pkgs.buildGoModule { + # The default package for 'nix build'. This makes sense if the + # flake provides only one package or there is a clear "main" + # package. + default = pkgs.buildGoModule { pname = "go-hello"; inherit version; # In 'nix develop', we don't need a copy of the source tree @@ -156,11 +159,6 @@ Then you can look at `flake.nix` to see what's up: vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo="; }; }); - - # The default package for 'nix build'. This makes sense if the - # flake provides only one package or there is a clear "main" - # package. - defaultPackage = forAllSystems (system: self.packages.${system}.go-hello); }; } ``` @@ -197,7 +195,6 @@ Let's take a closer look at the higher level things in the flake: outputs = { self, nixpkgs }: { packages = { ... }; - defaultPackage = { ... }; }; } ``` @@ -462,7 +459,7 @@ nixosModules.bot = { config, lib, ... }: { Group = "mara-bot"; Restart = "always"; WorkingDirectory = "/var/lib/mara-bot"; - ExecStart = "${self.defaultPackage."${system}"}/bin/mara"; + ExecStart = "${self.packages."${system}".default}/bin/mara"; }; }; }; diff --git a/blog/nix-flakes-2-2022-02-27.markdown b/blog/nix-flakes-2-2022-02-27.markdown index b820885..b163fcf 100644 --- a/blog/nix-flakes-2-2022-02-27.markdown +++ b/blog/nix-flakes-2-2022-02-27.markdown @@ -19,6 +19,10 @@ chance of bitrotting. I will make every attempt to update it if things change, however flakes have been fairly consistent for a few years now.](conversation://Cadey/coffee) +[EDIT(20220327 14:13): A previous version of this article said to use +`defaultPackage` for the default package. This is deprecated and you should use +`packages.default` instead.](conversation://Cadey/coffee) + [What is a package? I've seen this term thrown around with phrases like "Nix is a @@ -180,10 +184,10 @@ web server template by defining another package: ```nix # flake.nix -# after defaultPackage packages = { + default = ...; docker = let - web = self.defaultPackage.${system}; + web = self.packages.${system}.default; in pkgs.dockerTools.buildLayeredImage { name = web.pname; tag = web.version; @@ -373,7 +377,7 @@ the systemd unit: web-service = pkgs.substituteAll { name = "web-server.service"; src = ./systemd/web-server.service.in; - web = self.defaultPackage.${system}; + web = self.packages.${system}.default; }; ``` @@ -399,7 +403,7 @@ Then you can add the bit that builds the portable service: ```nix portable = let - web = self.defaultPackage.${system}; + web = self.packages.${system}.default; in pkgs.portableService { inherit (web) version; name = web.pname; diff --git a/blog/nix-flakes-3-2022-04-07.markdown b/blog/nix-flakes-3-2022-04-07.markdown new file mode 100644 index 0000000..342b29f --- /dev/null +++ b/blog/nix-flakes-3-2022-04-07.markdown @@ -0,0 +1,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 the other articles in +this series, 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 nixos-infect, a script you can +put into a cloudconfig when spinning up a new server. You can also install +NixOS manually 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; } +``` + + +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"; } +``` + + + +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 + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` + +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"; + }; + }; +}; +``` + + +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. + + + +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 :) +``` + + +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. + + + +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).