This commit is contained in:
Pim Kunis 2024-11-02 16:30:24 +01:00
commit b023369500
39 changed files with 717 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.direnv/
result
ideas.txt
notes.txt

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

BIN
assets/helm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

BIN
assets/kubenix-github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

162
assets/kubenix.svg Normal file
View file

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="722.8457"
height="701.96637"
id="svg2"
version="1.1"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
sodipodi:docname="logo2.svg"
inkscape:export-filename="/home/thockin/src/kubernetes/new.png"
inkscape:export-xdpi="460.95001"
inkscape:export-ydpi="460.95001"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
id="defs4"><linearGradient
inkscape:collect="always"
id="main"><stop
style="stop-color:#ffffff;stop-opacity:0"
offset="0"
id="stop915" /><stop
id="stop917"
offset="0.23168644"
style="stop-color:#ffffff;stop-opacity:1" /><stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop919" /></linearGradient><linearGradient
y2="880.37714"
x2="-414.38654"
y1="782.33563"
x1="-584.19934"
gradientTransform="translate(864.69589,-1491.3405)"
gradientUnits="userSpaceOnUse"
id="linearGradient1299"
xlink:href="#main"
inkscape:collect="always" /><linearGradient
y2="460.51822"
x2="389.57562"
y1="351.41116"
x1="200.59668"
gradientTransform="translate(210.82018,-765.27605)"
gradientUnits="userSpaceOnUse"
id="linearGradient1713"
xlink:href="#main"
inkscape:collect="always" /></defs><sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.69351872"
inkscape:cx="390.03994"
inkscape:cy="303.52461"
inkscape:document-units="px"
inkscape:current-layer="g3052"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1248"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:snap-global="false"
fit-margin-top="10"
fit-margin-left="10"
fit-margin-right="10"
fit-margin-bottom="10"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" /><metadata
id="metadata7"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-6.3260942,-174.7524)"><g
id="g3052"><path
style="fill:#326ce5;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 365.31239,184.81252 a 46.724621,46.342246 0 0 0 -17.90625,4.53125 l -244.34375,116.75 a 46.724621,46.342246 0 0 0 -25.281248,31.4375 l -60.28125,262.25 a 46.724621,46.342246 0 0 0 6.34375,35.53125 46.724621,46.342246 0 0 0 2.65625,3.6875 L 195.62489,849.28127 a 46.724621,46.342246 0 0 0 36.53125,17.4375 l 271.21875,-0.0625 a 46.724621,46.342246 0 0 0 36.53125,-17.40625 l 169.0625,-210.3125 a 46.724621,46.342246 0 0 0 9.03125,-39.21875 l -60.375,-262.25 a 46.724621,46.342246 0 0 0 -25.28125,-31.4375 l -244.375,-116.6875 a 46.724621,46.342246 0 0 0 -22.65625,-4.53125 z"
id="path3055"
inkscape:connector-curvature="0"
inkscape:export-filename="new.png"
inkscape:export-xdpi="250.55"
inkscape:export-ydpi="250.55" /><g
id="g321"
transform="matrix(1.0666666,0,0,1.0666666,100.31931,303.73558)"><g
style="display:none"
transform="matrix(0.09048806,0,0,0.09048806,-14.15991,84.454917)"
inkscape:label="(pdf) background"
id="layer1-3"><rect
y="-2102.4253"
x="-1045.6049"
height="7145.4614"
width="7947.0356"
id="rect995"
style="opacity:1;fill:#040404;fill-opacity:1;stroke-width:10.3605" /></g><g
transform="translate(-156.48372,537.56136)"
style="display:inline;opacity:1"
inkscape:label="gradient-logo"
id="layer3"><g
style="stroke-width:11.0512"
transform="matrix(0.09048806,0,0,0.09048806,142.32381,-453.10644)"
id="g955"><g
transform="matrix(11.047619,0,0,11.047619,-1572.2888,9377.7107)"
id="g869"><g
transform="rotate(-60,226.35754,-449.37199)"
id="g932"
style="stroke-width:11.0512"><path
sodipodi:nodetypes="cccccccccc"
inkscape:connector-curvature="0"
id="path3336-6"
d="m 449.71876,-420.51322 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8257 z"
style="opacity:1;fill:url(#linearGradient1713);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:33.1535;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></g><path
sodipodi:nodetypes="cccccccccc"
inkscape:connector-curvature="0"
id="path4260-0"
d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8256 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient1299);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:33.1535;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /><use
x="0"
y="0"
xlink:href="#path3336-6"
inkscape:transform-center-x="124.43045"
inkscape:transform-center-y="151.59082"
id="use3439-6"
transform="rotate(60,728.23563,-692.24036)"
width="100%"
height="100%"
style="stroke-width:11.0512" /><use
x="0"
y="0"
xlink:href="#path3336-6"
inkscape:transform-center-x="59.669705"
inkscape:transform-center-y="-139.94592"
id="use3449-5"
transform="rotate(180,477.5036,-570.81898)"
width="100%"
height="100%"
style="stroke-width:11.0512" /><use
style="display:inline;stroke-width:11.0512"
x="0"
y="0"
xlink:href="#path4260-0"
id="use4354-5"
transform="rotate(120,407.33916,-716.08356)"
width="100%"
height="100%" /><use
style="display:inline;stroke-width:11.0512"
x="0"
y="0"
xlink:href="#path4260-0"
id="use4362-2"
transform="rotate(-120,407.28823,-715.86995)"
width="100%"
height="100%" /></g></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
assets/kubernetes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

BIN
assets/me.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
assets/nix-snapshotter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

BIN
assets/nixng.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
assets/openapi-spec-gen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

BIN
assets/servers-k8s-kill.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

BIN
assets/servers-k8s.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

BIN
assets/servers-kill.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

BIN
assets/servers-services.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

BIN
assets/servers.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

27
flake.lock Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1730200266,
"narHash": "sha256-l253w0XMT8nWHGXuXqyiIC/bMvh1VRszGXgdpQlfhvU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "807e9154dcb16384b1b765ebe9cd2bba2ac287fd",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

36
flake.nix Normal file
View file

@ -0,0 +1,36 @@
{
description = "Presentation for NixCon 2024";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs = { self, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
packages = forEachSupportedSystem ({ pkgs }: {
default = pkgs.stdenv.mkDerivation {
name = "nixcon-2024-slides";
buildInputs = with pkgs; [ marp-cli ];
src = self;
dontUnpack = true;
buildPhase = ''
marp $src --output $out/presentation.html --html
cp -r $src/assets $out/assets
'';
};
});
devShells = forEachSupportedSystem ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [ marp-cli ];
};
});
};
}

487
presentation.md Normal file
View file

@ -0,0 +1,487 @@
---
marp: true
theme: gaia
paginate: true
footer: "October 26th, 2024"
---
# Kubenix: Leveraging NixOS modules to generate Kubernetes manifests
![bg right:35% 100% Kubenix logo](assets/kubenix.svg)
**Pim Kunis**
---
# Agenda
![bg right:60% 80%](assets/kubenix-github.png)
1. My background
2. Kubernetes
3. Kubenix
---
# `$ whoami`
![bg right 60% drop-shadow:0,5px,10px,rgba(0,0,0,.7)](assets/me.jpg)
- Pim Kunis
- Matrix: @pim:envs.net
- GitHub: @pizzapim
- Consultant @ [Sue](https://sue.nl)
- Currently doing Linux + Ansible stuff
- Relatively new to Nix
- Avid self-hoster
---
# Self-hosting
![bg Logos of services I self-host](assets/self-hosting/logos.png)
---
# Self-hosting
3 mini PCs (Gigabyte Brix with Intel Celeron J4105)
![w:900 My servers in their closet](assets/servers-services.jpg)
---
# NixOS for a cluster?
![w:900 My servers in their closet](assets/servers-kill.jpg)
---
# NixOS for a cluster?
![bg right assets/cat-chewing-cable.jpg](assets/cat-chewing-cable.jpg)
- Balance services
- Maintenance
- Services are tied to a particular host
- Pets vs cattle
---
# Agenda
1. My background
2. **Kubernetes**
3. Kubenix
---
# Enter: Kubernetes
![bg left:40% 100% assets/cat-chewing-cable.jpg](assets/kubernetes.png)
- Abstracts infrastructure
- Container orchestration
- Workloads are containers (pods)
- Distributed state (etcd)
- Actual state
- Desired state
- Reconciliation
---
# Kubernetes
![w:900 My servers in their closet](assets/servers-k8s.jpg)
---
# Kubernetes
![w:900 My servers in their closet](assets/servers-k8s-kill.jpg)
---
# Kubernetes
![w:900 My servers in their closet](assets/servers-k8s-revive.jpg)
---
# Mutating state in Kubernetes
![bg 80% hi](assets/yaml-manifest/base.png)
---
# Mutating state in Kubernetes
![bg 80% hi](assets/yaml-manifest/highlight1.png)
---
# Mutating state in Kubernetes
![bg 80% hi](assets/yaml-manifest/highlight2.png)
---
# Mutating state in Kubernetes
![bg 80% hi](assets/yaml-manifest/highlight3.png)
---
# Helm
![bg right:50% 60%](assets/helm.png)
- Package manager
- Templating YAML
- Cursed
---
# Helm
![bg 80% hi](assets/helm-example/base.png)
---
# Helm
![bg 80% hi](assets/helm-example/highlight.png)
---
# Helm
![bg 80% hi](assets/helm-example/watermark.png)
---
# Agenda
1. My background
2. Kubernetes
3. **Kubenix**
---
# Kubenix ✨
- Generate manifests using NixOS modules!
- Benefits:
- No ugly YAML templates
- Functional language
- Type and structure checking
- Modularity
---
# How Kubenix works
1. My background
2. Kubernetes
3. **Kubenix**
- **OpenAPI specification**
- Generate NixOS module
- Use generated module
- Generate manifest
---
# The OpenAPI spec
- Standard for describing APIs
- YAML or JSON
- Specifies how to interact with Kubernetes API
---
# The OpenAPI spec
![bg 75% hi](assets/openapi-spec-example/base.png)
---
# The OpenAPI spec
![bg 75% hi](assets/openapi-spec-example/highlight1.png)
---
# The OpenAPI spec
![bg 75% hi](assets/openapi-spec-example/highlight2.png)
---
# How Kubenix works
1. My background
2. Kubernetes
3. **Kubenix**
- OpenAPI specification
- **Generate NixOS module**
- Use generated module
- Generate manifest
---
# Generating the NixOS module
- Generate Nix code
- Templating?
---
# Generating the NixOS module
```nix
''
definitions = {
# Here we loop over each "definition"
${concatStrings (mapAttrsToList (name: value: ''
"${name}" = {
${optionalString (hasAttr "options" value) "
options = {${concatStrings (mapAttrsToList (name: value: ''
"${name}" = ${value};
'')
value.options)}};
"}
${optionalString (hasAttr "config" value) ''
config = {${concatStrings (mapAttrsToList (name: value: ''
"${name}" = ${value};
'')
value.config)}};
''}
};
'')
definitions)}
};
''
```
---
# The generated NixOS module
```nix
"io.k8s.api.core.v1.Pod" = {
options = {
"apiVersion" = mkOption {
description = "...";
type = (types.nullOr types.str);
};
"kind" = mkOption {
description = "...";
type = (types.nullOr types.str);
};
"metadata" = mkOption {
description = "...";
type = (types.nullOr (submoduleOf "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"));
};
"spec" = mkOption {
description = "...";
type = (types.nullOr (submoduleOf "io.k8s.api.core.v1.PodSpec"));
};
"status" = mkOption {
description = "...";
type = (types.nullOr (submoduleOf "io.k8s.api.core.v1.PodStatus"));
};
};
};
```
---
# How Kubenix works
1. My background
2. Kubernetes
3. **Kubenix**
- OpenAPI specification
- Generate NixOS module
- **Use generated module**
- Generate manifest
---
# Using the module
![bg 70% hi](assets/kubenix-example/base.png)
---
# Using the module
![bg 70% hi](assets/kubenix-example/highlight1.png)
---
# Using the module
![bg 70% hi](assets/kubenix-example/highlight2.png)
---
# Using the module
![bg 70% hi](assets/kubenix-example/highlight3.png)
---
# Using the module
![bg 70% hi](assets/kubenix-example/highlight4.png)
---
# Example of type checking
```nix
{
kubernetes.resources.pods.example-pod = {
spec.containers.jellyfin-container = {
image = "jellyfin/jellyfin:latest";
ports = [{ containerPort = "foo"; }];
};
};
};
```
```
error: A definition for option `kubernetes.(...).containerPort' is not of type `signed integer'.
```
---
# An undefined option
```nix
{
kubernetes.resources = {
foo.bar = "baz";
}
}
```
```
error: The option `kubernetes.api.resources.foo' does not exist.
```
---
# How Kubenix works
1. My background
2. Kubernetes
3. **Kubenix**
- OpenAPI specification
- Generate NixOS module
- Use generated module
- **Generate manifest**
---
# `kubenix.evalModules`
```nix
{
inputs.kubenix.url = "github:hall/kubenix";
outputs = {self, kubenix, ... }@inputs: let
system = "x86_64-linux";
in {
packages.${system}.default = (kubenix.evalModules.${system} {
module = { kubenix, ... }: {
imports = [ kubenix.modules.k8s ];
kubernetes.resources.pods.example.spec.containers.jellyfin.image = "jellyfin/jellyfin:latest";
};
}).config.kubernetes.result;
};
}
```
---
# `kubenix.evalModules`
```json
{
"apiVersion": "v1",
"items": [
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"annotations": {
"kubenix/k8s-version": "1.30",
"kubenix/project-name": "kubenix"
},
"labels": {
"kubenix/hash": "84130bb6a26b9066eb2bdcaf2e68148a6e0648b0"
},
"name": "example"
},
"spec": {
"containers": [
{
"image": "jellyfin/jellyfin:latest",
"name": "jellyfin"
}
]
}
}
],
"kind": "List",
"labels": {
"kubenix/hash": "84130bb6a26b9066eb2bdcaf2e68148a6e0648b0",
"kubenix/k8s-version": "1.30",
"kubenix/project-name": "kubenix"
}
}
```
---
# Summary
- Kubenix generates Kubernetes manifests
- Converts OpenAPI spec to a NixOS module
- Build Kubernetes deployments with:
- No ugly YAML templates
- Modularity
- In a functional language
- Type and structure checking
---
# Terraform: OpenAPI Provider Spec Generator
![hoi](assets/openapi-spec-gen.png)
---
# Bonus: nix-snapshotter
![bg right:50% 100%](assets/nix-snapshotter.png)
- Images are in Nix store
- Fully declarative
---
# Bonus: NixNG
![bg right:50% 100%](assets/nixng.png)
- Nix~~OS~~ in a container
- init-system independent