learn-flakes-the-fun-way/post.md
2024-07-08 09:35:53 -05:00

6 KiB
Raw Blame History

This post is a Flake-based rewrite of [Learn Nix the Fun Way on fzakaria.com] 0. I really enjoyed the content of the post and wanted to write it as a Nix user who is just using flakes. It does add a few extra steps and complexity, but I think it's still valuable and perhaps reveals a bit more about Nix and why it's pretty fantastic.

what-is-my-ip

Let's walk through a single example of a shell script one may write: what-is-my-ip

#! /usr/bin/env bash
curl -s http://httpbin.org/get | \
    jq --raw-output .origin

Sure, it's sort of portable, if you tell the person running it to have curl and jq. What if you relied on a specific version of either though?

Nix guarantees portability.

We might leverage Nixpkgs' trivial builders (specifically, writeShellScriptBin) in a basic Nix flake to turn this into a Nix derivation (i.e. build recipe).

# flake.nix
{
  inputs.nixpkgs.url = "nixpkgs/nixos-24.05";
  outputs = {nixpkgs, ...}: let
    systems = ["aarch64-linux" "aarch64-darwin" "x86_64-darwin" "x86_64-linux"];
    pkgsFor = func: (nixpkgs.lib.genAttrs systems (system: (func (import nixpkgs {inherit system;}))));
  in {
    packages = pkgsFor (pkgs: {
      what-is-my-ip = pkgs.callPackage ./what-is-my-ip.nix {};
    });
  };
}
# what-is-my-ip.nix
{pkgs}:
pkgs.writeShellScriptBin "what-is-my-ip" ''
  ${pkgs.curl}/bin/curl -s http://httpbin.org/get | \
    ${pkgs.jq}/bin/jq --raw-output .origin
''

😬 Avoid over-focusing on the fact I just introduced a new language and a good chunk of boilerplate. Just come along for the ride.

Here we are pinning our package to dependencies which come from NixOS/Nixpkgs release branch 24.05.

If we nix build .#what-is-my-ip and readlink result we get:

/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip

And, of course, we can run our result:

$ ./result/bin/what-is-my-ip 
24.5.113.148

Or run it from the Flake:

$ nix run .#what-is-my-ip
24.5.113.148

Now that this is in Nix and we've modeled our dependencies, we can do fun things like generate graph diagrams to view them (click the image to view larger).

$ nix-store --query --graph $(readlink result) | nix shell nixpkgs#graphviz -c dot -Tpng -o what-is-my-ip-deps.png

Image of what-is-my-ip dependencies as a graph

Let's create a developer environment and bring in our new tool. This is a great way to create developer environment with reproducible tools

let
  pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz") {};
  what-is-my-ip = import ./what-is-my-ip.nix {inherit pkgs;};
in
  pkgs.mkShell {
    packages = [what-is-my-ip];
    shellHook = ''
      echo "Hello, Nix!"
    '';
  }
 nix-shell what-is-my-ip-shell.nix
Hello, Nix!

[nix-shell:~/tutorial]$ which what-is-my-ip
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip

🕵️ Notice that the hash lr6wlz2652r35rwzc79samg77l6iqmii is exactly the same which we built earlier.

We can now do binary or source deployments 🚀🛠️📦 since we know the full dependency closure of our tool. We simply copy the necessary /nix/store paths to another machine with Nix installed.

 nix copy --to ssh://nixie.tail9f4b5.ts.net \
    $(nix-build what-is-my-ip.nix) --no-check-sigs

 ssh nixie.tail9f4b5.ts.net

[fmzakari@nixie:~]$ /nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
98.147.178.19

Maybe though you are stuck with Kubernetes or Docker. Let's use Nix to create an OCI compatible image.

let
  pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz") {};
  what-is-my-ip = import ./what-is-my-ip.nix {inherit pkgs;};
in
  pkgs.dockerTools.buildImage {
    name = "what-is-my-ip-docker";
    config = {
      Cmd = ["${what-is-my-ip}/bin/what-is-my-ip"];
    };
  }
 docker load < $(nix-build what-is-my-ip-docker.nix)
Loaded image: what-is-my-ip-docker:c9g6x30invdq1bjfah3w1aw5w52vkdfn

 docker run -it what-is-my-ip-docker:c9g6x30invdq1bjfah3w1aw5w52vkdfn
24.5.113.148

Cool! Nix + Docker integration perfectly. The image produced has only the files exactly necessary to run the tool provided, effectively distroless.

Finally, let's take the last step and create a reproducible operating system using NixOS to contain only the programs we want.

let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz";
  pkgs = import nixpkgs {};
  what-is-my-ip = import ./what-is-my-ip.nix {inherit pkgs;};
  nixos = import "${nixpkgs}/nixos" {
    configuration = {
      users.users.alice = {
        isNormalUser = true;
        # enable sudo
        extraGroups = ["wheel"];
        packages = [
          what-is-my-ip
        ];
        initialPassword = "swordfish";
      };

      system.stateVersion = "24.05";
    };
  };
in
  nixos.vm
 nix-build what-is-my-ip-vm.nix

 QEMU_KERNEL_PARAMS=console=ttyS0 ./result/bin/run-nixos-vm -nographic; reset

<<< Welcome to NixOS 24.05pre-git (x86_64) - ttyS0 >>>

Run 'nixos-help' for the NixOS manual.

nixos login: alice
Password: 

[alice@nixos:~]$ which what-is-my-ip
/etc/profiles/per-user/alice/bin/what-is-my-ip

[alice@nixos:~]$ readlink $(which what-is-my-ip)
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip

[alice@nixos:~]$ what-is-my-ip
24.5.113.148

💥 Hash lr6wlz2652r35rwzc79samg77l6iqmii present again!

We took a relatively simple script through a variety of applications in the Nix ecosystem: build recipe, shell, docker image and finally NixOS VM.

Hopefully, seeing the fun things you can do with Nix might inspire you to push through the hard parts.

There is a golden pot 💰 at the end of this rainbow 🌈 awaiting you.

Learn Nix the fun way.