learn-flakes-the-fun-way/readme.md

227 lines
6.6 KiB
Markdown
Raw Normal View History

2024-07-08 09:35:53 -05:00
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.
<!--more-->
## what-is-my-ip
Let's walk through a single example of a shell script one may write: _what-is-my-ip_
```bash
2024-07-08 12:18:24 -05:00
#!/usr/bin/env bash
curl -s http://httpbin.org/get \
| jq --raw-output .origin
2024-07-08 09:35:53 -05:00
```
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](https://ryantm.github.io/nixpkgs/builders/trivial-builders/)_ (specifically, `writeShellScriptBin`) in a basic Nix flake to turn this into a Nix derivation (i.e. build recipe).
```nix
# 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: {
2024-07-08 12:18:24 -05:00
default = pkgs.callPackage ./what-is-my-ip.nix {};
2024-07-08 09:35:53 -05:00
});
};
}
```
```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.
2024-07-08 12:18:24 -05:00
If we `nix build` and `readlink result` we get:
2024-07-08 09:35:53 -05:00
**/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip**
2024-07-08 12:18:24 -05:00
And, of course, we can run our built result:
2024-07-08 09:35:53 -05:00
2024-07-08 12:18:24 -05:00
```console
2024-07-08 09:35:53 -05:00
$ ./result/bin/what-is-my-ip
24.5.113.148
```
2024-07-08 12:18:24 -05:00
Or run it from the Flake directly:
2024-07-08 09:35:53 -05:00
2024-07-08 12:18:24 -05:00
```console
$ nix run
2024-07-08 09:35:53 -05:00
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).
```console
2024-07-08 09:35:53 -05:00
$ nix-store --query --graph $(readlink result) | nix shell nixpkgs#graphviz -c dot -Tpng -o what-is-my-ip-deps.png
```
2024-07-08 12:41:46 -05:00
[![Image of what-is-my-ip dependencies as a graph](./what-is-my-ip-deps.png)](./what-is-my-ip-deps.png)
2024-07-08 09:35:53 -05:00
Let's create a _developer environment_ and bring in our new tool.
2024-07-08 12:18:24 -05:00
This is a great way to create developer environments with reproducible tools.
```diff
diff --git a/flake.nix b/flake.nix
index 2a99357..ab32421 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,11 +1,24 @@
{
inputs.nixpkgs.url = "nixpkgs/nixos-24.05";
- outputs = {nixpkgs, ...}: let
+ outputs = {
+ self,
+ 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 {};
});
+
+ devShells = pkgsFor (pkgs: {
+ default = pkgs.mkShell {
+ packages = [self.outputs.packages.${pkgs.system}.what-is-my-ip];
+ shellHook = ''
+ echo "Hello, Nix!"
+ '';
+ };
+ });
};
}
2024-07-08 09:35:53 -05:00
```
2024-07-08 12:18:24 -05:00
```console
$ nix develop -c $SHELL
2024-07-08 09:35:53 -05:00
Hello, Nix!
2024-07-08 12:18:24 -05:00
$ which what-is-my-ip
2024-07-08 09:35:53 -05:00
/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.
2024-07-08 12:18:24 -05:00
```console
$ nix copy --to ssh://beefcake $(nix build --print-out-paths)
2024-07-08 12:38:39 -05:00
$ ssh beefcake /nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
2024-07-08 09:35:53 -05:00
98.147.178.19
```
2024-07-08 12:38:39 -05:00
Maybe though you are stuck with Kubernetes or Docker. Let's use Nix to create an OCI-compatible image.
2024-07-08 09:35:53 -05:00
2024-07-08 12:38:39 -05:00
```diff
diff --git a/flake.nix b/flake.nix
index 99d6d52..81e98c9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -10,6 +10,12 @@
in {
packages = pkgsFor (pkgs: {
default = pkgs.callPackage ./what-is-my-ip.nix {};
+ container = pkgs.dockerTools.buildImage {
+ name = "what-is-my-ip-container";
+ config = {
+ Cmd = ["${self.outputs.packages.${pkgs.system}.default}/bin/what-is-my-ip"];
+ };
+ };
});
devShells = pkgsFor (pkgs: {
2024-07-08 09:35:53 -05:00
```
2024-07-08 12:40:25 -05:00
```console
2024-07-08 12:38:39 -05:00
$ docker load < $(nix build .#docker-image --print-out-paths)
2024-07-08 09:35:53 -05:00
Loaded image: what-is-my-ip-docker:c9g6x30invdq1bjfah3w1aw5w52vkdfn
2024-07-08 12:38:39 -05:00
$ docker run -it what-is-my-ip-docker:c9g6x30invdq1bjfah3w1aw5w52vkdfn
2024-07-08 09:35:53 -05:00
24.5.113.148
```
2024-07-08 12:38:39 -05:00
Cool! Nix + Docker integration perfectly. The image produced has only the files exactly necessary to run the tool provided, effectively **distroless**. You may also note that if you are following along, your image digest is exactly the same. **Reproducibility!**
2024-07-08 09:35:53 -05:00
Finally, let's take the last step and create a reproducible operating system using NixOS to contain only the programs we want.
2024-07-08 12:38:39 -05:00
```diff
diff --git a/flake.nix b/flake.nix
index 99d6d52..81e98c9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,5 +26,27 @@
'';
};
});
+
+ nixosConfigurations = let
+ system = "x86_64-linux";
+ in {
+ default = nixpkgs.lib.nixosSystem {
+ inherit system;
+ modules = [
+ {
+ users.users.alice = {
+ isNormalUser = true;
+ # enable sudo
+ extraGroups = ["wheel"];
+ packages = [
+ self.outputs.packages.${system}.default
+ ];
+ initialPassword = "swordfish";
+ };
+ system.stateVersion = "24.05";
+ }
+ ];
+ };
+ };
};
}
2024-07-08 09:35:53 -05:00
```
```console
2024-07-08 12:38:39 -05:00
$ nixos-rebuild build-vm --flake .#default
$ ./result/bin/run-nixos-vm
2024-07-08 09:35:53 -05:00
2024-07-08 12:38:39 -05:00
# I/O snippet from QEMU
2024-07-08 09:35:53 -05:00
nixos login: alice
Password:
2024-07-08 12:40:25 -05:00
$ readlink $(which what-is-my-ip)
2024-07-08 09:35:53 -05:00
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
2024-07-08 12:40:25 -05:00
$ what-is-my-ip
2024-07-08 09:35:53 -05:00
24.5.113.148
```
💥 Hash **lr6wlz2652r35rwzc79samg77l6iqmii** present again!
2024-07-08 12:38:39 -05:00
We took a relatively simple script through a variety of applications in the Nix ecosystem: build recipe, shell, docker image, and finally NixOS VM.
2024-07-08 09:35:53 -05:00
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.**
[0]: https://fzakaria.com/2024/07/05/learn-nix-the-fun-way.html