From bf38ecdba82d4659bf3542abf20b66e7a9adda02 Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 19:19:52 -0300 Subject: [PATCH 1/9] feat: add k8s CI benchmark note (kind vs k3d vs k3s) --- src/pages/notes/k8s-ci-benchmark/index.astro | 103 +++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/pages/notes/k8s-ci-benchmark/index.astro diff --git a/src/pages/notes/k8s-ci-benchmark/index.astro b/src/pages/notes/k8s-ci-benchmark/index.astro new file mode 100644 index 0000000..927954e --- /dev/null +++ b/src/pages/notes/k8s-ci-benchmark/index.astro @@ -0,0 +1,103 @@ +--- +export const metadata = { + title: "Benchmarking kind, k3d, and k3s on GitHub Actions", + date: "2026-04-29", + tag: "platform", + description: "Measuring cluster startup, resource footprint, and image load across the three main local K8s tools to pick a standard for a shared Nebari CI action.", +}; + +import ArticleLayout from "@/layouts/ArticleLayout.astro"; +--- + + + +## Background + +Across the `nebari-dev` org, every component repo that needs integration testing rolls +its own local cluster setup. `nebari-operator` uses kind with custom shell scripts in +`dev/scripts/`. `nebari-landing` uses minikube with Makefile targets. Pack repos like +`nebari-mlflow-pack` work around this by checking out the operator repo at CI time just +to reuse those scripts — a fragile cross-repo dependency that breaks silently when the +operator refactors. + +The goal is to replace all of that with a single reusable composite action backed by +NIC itself. Before committing to one tool, the question was: **which local K8s runtime +should the shared action standardize on?** + +See [nebari-dev/nebari-infrastructure-core#243](https://github.com/nebari-dev/nebari-infrastructure-core/issues/243) +for the full proposal. + +## What was measured + +The benchmark runs three parallel jobs — one per tool — on `ubuntu-latest` with a +common configuration: + +- **Kubernetes version**: v1.32 (same image/tag across all three) +- **Extras disabled**: Traefik, ServiceLB, metrics-server — to isolate cluster overhead, + not bundled add-ons +- **No load balancer** for k3d (`--no-lb`) + +Each job records wall-clock time for every phase using `date +%s%N` brackets: + +| Phase | What it measures | +|-------|-----------------| +| Cluster create | `kind create` / `k3d cluster create` / k3s install script, including `--wait` | +| Nodes ready | `kubectl wait --for=condition=Ready nodes --all` after create returns | +| System pods ready | `kubectl wait --for=condition=Ready pods --all -n kube-system` | +| Image load | Loading `nginx:1.27-alpine` into the cluster's image store | +| Deployment ready | `kubectl wait --for=condition=Available deployment/nginx` | +| Cluster delete | Teardown time | + +Resource usage (memory RSS, VSZ, CPU%) is sampled once after the cluster stabilizes. + +## Tool differences worth knowing + +**kind** wraps the control plane in a Docker container. Image loads go through +`kind load docker-image`, which tars and imports via the container runtime. Lifecycle +events are visible in `docker logs` on the control-plane container. + +**k3d** also runs inside Docker (it's k3s-in-Docker), but the cluster config adds a +server node via `k3d-bench-server-0`. Image loads use `k3d image import`. Kubernetes +events via `kubectl get events` give a useful second timeline alongside docker logs. + +**k3s** (bare) installs directly onto the runner via the upstream install script and +runs as a systemd service. This means: +- `journalctl -u k3s` gives **microsecond-precision** lifecycle timestamps — more + granular than docker log timestamps +- Image loading requires `docker save | sudo k3s ctr images import` rather than a + native CLI command +- Teardown is `/usr/local/bin/k3s-uninstall.sh` + +The k3s process runs as root on the host, so resource sampling uses `ps` against the +`k3s server` PID rather than `docker stats`. + +## Why this matters for the reusable action + +The shared action needs to be fast (developers run it on every PR), lightweight +(GH Actions runners have 7 GB RAM), and easy to configure without per-repo +customization. The benchmark results directly answer: + +- Which tool has the shortest cold-start wall time? +- Which has the lowest idle memory footprint after stabilization? +- How fast is image injection — relevant because NIC bootstraps several images + (ArgoCD, Keycloak, Envoy Gateway, cert-manager) during environment setup? + +Live results for each run are in the +[GitHub Actions job summaries](https://github.com/viniciusdc/k8s-local-benchmark/actions) +of the benchmark repo. + +
+ Note + k3s bare gives the most precise timing data via journalctl, but its rootful install + means any image injection step has to go through k3s ctr rather than + Docker CLI — worth factoring in if the action needs to pre-load images before NIC + runs. +
+ +
From 0f8b32a5678dd1b77abe9cc130d808dff05bd242 Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 19:27:37 -0300 Subject: [PATCH 2/9] fix: add journalctl to cspell dictionary and use redirect.github.com links --- cspell.json | 1 + src/pages/notes/k8s-ci-benchmark/index.astro | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index 0be4e1a..49283ec 100644 --- a/cspell.json +++ b/cspell.json @@ -14,6 +14,7 @@ "woff", "woff2", "oklch", "rgb", "rgba", "hsl", "clamp", "minmax", "prefers", "nebari", "jmespath", "snistrict", + "journalctl", "systemd", "firewalld", "libvirt", "vagrantfile", "syscall", "seccomp", "apparmor", "selinux", "etcd", "kube", "kube-proxy", "coredns", diff --git a/src/pages/notes/k8s-ci-benchmark/index.astro b/src/pages/notes/k8s-ci-benchmark/index.astro index 927954e..6474357 100644 --- a/src/pages/notes/k8s-ci-benchmark/index.astro +++ b/src/pages/notes/k8s-ci-benchmark/index.astro @@ -30,7 +30,7 @@ The goal is to replace all of that with a single reusable composite action backe NIC itself. Before committing to one tool, the question was: **which local K8s runtime should the shared action standardize on?** -See [nebari-dev/nebari-infrastructure-core#243](https://github.com/nebari-dev/nebari-infrastructure-core/issues/243) +See [nebari-dev/nebari-infrastructure-core#243](https://redirect.github.com/nebari-dev/nebari-infrastructure-core/issues/243) for the full proposal. ## What was measured @@ -89,7 +89,7 @@ customization. The benchmark results directly answer: (ArgoCD, Keycloak, Envoy Gateway, cert-manager) during environment setup? Live results for each run are in the -[GitHub Actions job summaries](https://github.com/viniciusdc/k8s-local-benchmark/actions) +[GitHub Actions job summaries](https://redirect.github.com/viniciusdc/k8s-local-benchmark/actions) of the benchmark repo.
From 4d33504a49b988c480721756ebd9301f678f1957 Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 19:56:47 -0300 Subject: [PATCH 3/9] chore: add .nvmrc pinning Node 22 --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 From f0e01ec4d682003dc2d766ef8535eec962aab0dd Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 20:02:33 -0300 Subject: [PATCH 4/9] chore: add pixi env for isolated Node 22 local dev --- .gitignore | 1 + Makefile | 17 +++++++++++------ pixi.toml | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 pixi.toml diff --git a/.gitignore b/.gitignore index 4de9cb6..9f24b11 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ # dependencies node_modules/ +.pixi/ # logs npm-debug.log* diff --git a/Makefile b/Makefile index 63acab0..010e3fb 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,26 @@ .PHONY: install dev build preview check banner post -# ── npm ─────────────────────────────────────────────────────────────────────── +# ── env ─────────────────────────────────────────────────────────────────────── + +env: + pixi install + +# ── npm (runs inside pixi env) ──────────────────────────────────────────────── install: - npm install + pixi run install dev: - npm run dev + pixi run dev build: - npm run build + pixi run build preview: - npm run preview + pixi run preview check: - npm run check + pixi run check # ── assets ──────────────────────────────────────────────────────────────────── diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000..19b9886 --- /dev/null +++ b/pixi.toml @@ -0,0 +1,16 @@ +[project] +name = "blog" +version = "0.1.0" +channels = ["conda-forge"] +platforms = ["osx-arm64", "osx-64", "linux-64"] + +[dependencies] +nodejs = ">=22.12,<23" + +[tasks] +install = "npm install" +dev = "npm run dev" +build = "npm run build" +preview = "npm run preview" +check = "npm run check" +spell = "npm run spell" From 4cf628f947f2e0ffbba03c89d12c3a27bfa589ae Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 20:07:37 -0300 Subject: [PATCH 5/9] fix: convert note to MDX and add table and link styles to ArticleLayout --- pixi.lock | 273 ++++++++++++++++++ src/layouts/ArticleLayout.astro | 37 +++ .../{index.astro => index.mdx} | 6 +- 3 files changed, 312 insertions(+), 4 deletions(-) create mode 100644 pixi.lock rename src/pages/notes/k8s-ci-benchmark/{index.astro => index.mdx} (99%) diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..78a1013 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,273 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.22.2-h273caaf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-22.22.2-h5d72c05_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-22.22.2-h2e6c367_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + sha256: c9dbcc8039a52023660d6d1bbf87594a93dd69c6ac5a2a44323af2c92976728d + md5: e18ad67cf881dcadee8b8d9e2f8e5f73 + depends: + - __unix + license: ISC + size: 131039 + timestamp: 1776865545798 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + sha256: 1294117122d55246bb83ad5b589e2a031aacdf2d0b1f99fd338aa4394f881735 + md5: 627eca44e62e2b665eeec57a984a7f00 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 12273764 + timestamp: 1773822733780 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + sha256: 3a7907a17e9937d3a46dfd41cffaf815abad59a569440d1e25177c15fd0684e5 + md5: f1182c91c0de31a7abd40cedf6a5ebef + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 12361647 + timestamp: 1773822915649 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda + sha256: 596a0bdd5321c5e41a4734f18b35bcbc5d116079d13bc40d765fd93c32b285d1 + md5: 4394b1ba4b9604ac4e1c5bdc74451279 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 567125 + timestamp: 1776815441323 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda + sha256: 25a0d02148a39b665d9c2957676faf62a4d2a58494d53b201151199a197db4b0 + md5: 448a1af83a9205655ee1cf48d3875ca3 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 569927 + timestamp: 1776816293111 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 + md5: 0aa00f03f9e39fb9876085dee11a85d4 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 he0feb66_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 1041788 + timestamp: 1771378212382 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 + md5: 239c5e9546c38a1e884d69effcf4c882 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 603262 + timestamp: 1771378117851 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e + md5: 1b08cd684f34175e4514474793d44bcb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 5852330 + timestamp: 1771378262446 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b + md5: 0f03292cc56bf91a077a134ea8747118 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + size: 895108 + timestamp: 1753948278280 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda + sha256: d90dd0eee6f195a5bd14edab4c5b33be3635b674b0b6c010fb942b956aa2254c + md5: fbfc6cf607ae1e1e498734e256561dc3 + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 422612 + timestamp: 1753948458902 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + sha256: 042c7488ad97a5629ec0a991a8b2a3345599401ecc75ad6a5af73b60e6db9689 + md5: c0d87c3c8e075daf1daf6c31b53e8083 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 421195 + timestamp: 1753948426421 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + sha256: 4c6da089952b2d70150c74234679d6f7ac04f4a98f9432dec724968f912691e7 + md5: 30439ff30578e504ee5e0b390afc8c65 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + size: 59000 + timestamp: 1774073052242 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 + md5: bc5a5721b6439f2f62a84f2548136082 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + size: 47759 + timestamp: 1774072956767 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.22.2-h273caaf_0.conda + sha256: 96b2973e280a867f1ad96fdb4413e4a24e2953487fd4cfce579214712b92f452 + md5: 81a057418ed2da9bd0463429d94ee140 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.28,<3.0.a0 + - openssl >=3.5.5,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - icu >=78.3,<79.0a0 + - libuv >=1.51.0,<2.0a0 + license: MIT + license_family: MIT + size: 24131706 + timestamp: 1774516679471 +- conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-22.22.2-h5d72c05_0.conda + sha256: 23f85cb1e1bd6fc16fbfa4440f7d3909e8daa587dce6c19764a4638a5dfa3df1 + md5: 6be8f1395dfe143ce1d2e71da3a9cddf + depends: + - __osx >=10.15 + - libcxx >=19 + - libzlib >=1.3.2,<2.0a0 + - icu >=78.3,<79.0a0 + - libuv >=1.51.0,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + size: 17639577 + timestamp: 1774518173221 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-22.22.2-h2e6c367_0.conda + sha256: 2e8b09512a2a6a196c632c3b4c51d29cf90127b5e08e165448d22eb636ca9931 + md5: 898ab7075e4194872ee28742645de9ea + depends: + - __osx >=11.0 + - libcxx >=19 + - libuv >=1.51.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - icu >=78.3,<79.0a0 + license: MIT + license_family: MIT + size: 16675444 + timestamp: 1774518175001 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb + md5: da1b85b6a87e141f5140bb9924cecab0 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + size: 3167099 + timestamp: 1775587756857 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda + sha256: 334fd49ea31b99114f5afb1ec44555dc8c90640648302a4f8f838ee345d1ec50 + md5: 5cf0ece4375c73d7a5765e83565a69c7 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 2776564 + timestamp: 1775589970694 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea + md5: 25dcccd4f80f1638428613e0d7c9b4e1 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 3106008 + timestamp: 1775587972483 diff --git a/src/layouts/ArticleLayout.astro b/src/layouts/ArticleLayout.astro index 7d94f35..2d0347c 100644 --- a/src/layouts/ArticleLayout.astro +++ b/src/layouts/ArticleLayout.astro @@ -188,6 +188,43 @@ const { title, description, meta, subtitle, backHref, backLabel, nextHref, nextL text-transform: uppercase; letter-spacing: 0.06em; } + .article-content :global(a) { + color: var(--link); + text-decoration: underline; + text-decoration-color: rgba(155, 209, 223, 0.35); + text-underline-offset: 3px; + } + .article-content :global(a:hover) { + text-decoration-color: var(--link); + } + .article-content :global(table) { + width: 100%; + border-collapse: collapse; + font-size: 15px; + margin: 20px 0; + } + .article-content :global(thead tr) { + border-bottom: 1px solid var(--border-strong); + } + .article-content :global(tbody tr) { + border-bottom: 1px solid var(--border); + } + .article-content :global(th) { + text-align: left; + padding: 8px 14px 8px 0; + font-size: 12px; + font-family: var(--font-mono); + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--faint); + white-space: nowrap; + } + .article-content :global(td) { + padding: 9px 14px 9px 0; + color: #c3cbd6; + line-height: 1.55; + vertical-align: top; + } .article-content :global(figure) { margin: 36px 0; border: 1px solid var(--border); diff --git a/src/pages/notes/k8s-ci-benchmark/index.astro b/src/pages/notes/k8s-ci-benchmark/index.mdx similarity index 99% rename from src/pages/notes/k8s-ci-benchmark/index.astro rename to src/pages/notes/k8s-ci-benchmark/index.mdx index 6474357..394118f 100644 --- a/src/pages/notes/k8s-ci-benchmark/index.astro +++ b/src/pages/notes/k8s-ci-benchmark/index.mdx @@ -1,4 +1,5 @@ ---- +import ArticleLayout from "@/layouts/ArticleLayout.astro"; + export const metadata = { title: "Benchmarking kind, k3d, and k3s on GitHub Actions", date: "2026-04-29", @@ -6,9 +7,6 @@ export const metadata = { description: "Measuring cluster startup, resource footprint, and image load across the three main local K8s tools to pick a standard for a shared Nebari CI action.", }; -import ArticleLayout from "@/layouts/ArticleLayout.astro"; ---- - Date: Wed, 29 Apr 2026 20:08:44 -0300 Subject: [PATCH 6/9] feat: add conclusions comparison table to k8s benchmark note --- src/pages/notes/k8s-ci-benchmark/index.mdx | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pages/notes/k8s-ci-benchmark/index.mdx b/src/pages/notes/k8s-ci-benchmark/index.mdx index 394118f..d0517fd 100644 --- a/src/pages/notes/k8s-ci-benchmark/index.mdx +++ b/src/pages/notes/k8s-ci-benchmark/index.mdx @@ -90,6 +90,29 @@ Live results for each run are in the [GitHub Actions job summaries](https://redirect.github.com/viniciusdc/k8s-local-benchmark/actions) of the benchmark repo. +## Conclusions + +Across runs on `ubuntu-latest`, the tools split into a clear order for cold-start speed +and footprint, while differing sharply in operational model: + +| | kind | k3d | k3s (bare) | +|---|---|---|---| +| **Startup** | slowest | mid | fastest | +| **Idle memory** | highest | mid | lowest | +| **Image load** | `kind load docker-image` | `k3d image import` | `k3s ctr images import` | +| **Timing data** | docker logs | docker logs + k8s events | journalctl (µs precision) | +| **Root required** | no | no | yes | +| **Teardown** | `kind delete cluster` | `k3d cluster delete` | `k3s-uninstall.sh` | + +For a shared CI action where the environment is ephemeral and controlled, **k3d** is the +pragmatic middle ground: faster and lighter than kind, no rootful install requirement, +and Docker-based so image injection stays consistent with the rest of the workflow. +k3s is worth revisiting if startup shaves enough time to matter at scale, but the +rootful constraint complicates image pre-loading. + +Exact timing numbers for each run are in the +[GitHub Actions job summaries](https://redirect.github.com/viniciusdc/k8s-local-benchmark/actions). +
Note k3s bare gives the most precise timing data via journalctl, but its rootful install From 573f91ef700b66f76258478c7931e1958abb9edd Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 20:11:18 -0300 Subject: [PATCH 7/9] feat: add actual benchmark results table and h3 heading styles --- src/pages/notes/k8s-ci-benchmark/index.mdx | 39 ++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/pages/notes/k8s-ci-benchmark/index.mdx b/src/pages/notes/k8s-ci-benchmark/index.mdx index d0517fd..1598659 100644 --- a/src/pages/notes/k8s-ci-benchmark/index.mdx +++ b/src/pages/notes/k8s-ci-benchmark/index.mdx @@ -92,26 +92,39 @@ of the benchmark repo. ## Conclusions -Across runs on `ubuntu-latest`, the tools split into a clear order for cold-start speed -and footprint, while differing sharply in operational model: +Benchmarks ran on `ubuntu-latest` (2 vCPUs, 7 GB RAM), K8s v1.32, extras disabled +across all three tools. Full lifecycle logs: +[benchmark run — April 2026](https://redirect.github.com/viniciusdc/k8s-local-benchmark/actions/runs/24283780667). + +### Timing results + +| | kind v0.31.0 | k3d v5.8.3 | k3s v1.32.13 | +|---|---:|---:|---:| +| **Cluster create** | 39.8s | 12.6s | 8.6s | +| **Nodes ready** | 1.0s | 1.3s | 1.7s | +| **Image load** | 1.4s | 3.3s | 0.7s | +| **Deployment ready** | 1.2s | 2.2s | 18.0s | +| **Cluster delete** | 0.6s | 0.4s | 3.7s | + +k3s wins on raw cluster create (8.6s) but loses on first deployment (18.0s) — the +install script returns before controllers are fully stable, so the workload wait absorbs +the gap. k3d's `--wait` flag blocks until the node and controllers are ready, making +its total time to first workload (~18s create + deploy) faster than k3s (~27s). + +### Operational characteristics | | kind | k3d | k3s (bare) | |---|---|---|---| -| **Startup** | slowest | mid | fastest | -| **Idle memory** | highest | mid | lowest | -| **Image load** | `kind load docker-image` | `k3d image import` | `k3s ctr images import` | +| **Idle memory** | ~463 MiB | ~423 MiB | ~275 MiB | +| **Image load CLI** | `kind load docker-image` | `k3d image import` | `k3s ctr images import` | | **Timing data** | docker logs | docker logs + k8s events | journalctl (µs precision) | | **Root required** | no | no | yes | | **Teardown** | `kind delete cluster` | `k3d cluster delete` | `k3s-uninstall.sh` | -For a shared CI action where the environment is ephemeral and controlled, **k3d** is the -pragmatic middle ground: faster and lighter than kind, no rootful install requirement, -and Docker-based so image injection stays consistent with the rest of the workflow. -k3s is worth revisiting if startup shaves enough time to matter at scale, but the -rootful constraint complicates image pre-loading. - -Exact timing numbers for each run are in the -[GitHub Actions job summaries](https://redirect.github.com/viniciusdc/k8s-local-benchmark/actions). +For a shared CI action, **k3d** is the pragmatic pick: fastest end-to-end from create +to first workload, no rootful install, Docker-based image injection, and already in use +across half the nebari-dev repos. k3s is worth revisiting if startup time becomes a +bottleneck at scale, but the rootful constraint complicates image pre-loading.
Note From d314cdb7a4ee62427e965955c7d087000e12259c Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 20:11:46 -0300 Subject: [PATCH 8/9] refactor: move results tables before conclusions as part of argumentation --- src/pages/notes/k8s-ci-benchmark/index.mdx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/notes/k8s-ci-benchmark/index.mdx b/src/pages/notes/k8s-ci-benchmark/index.mdx index 1598659..a5a293f 100644 --- a/src/pages/notes/k8s-ci-benchmark/index.mdx +++ b/src/pages/notes/k8s-ci-benchmark/index.mdx @@ -86,17 +86,13 @@ customization. The benchmark results directly answer: - How fast is image injection — relevant because NIC bootstraps several images (ArgoCD, Keycloak, Envoy Gateway, cert-manager) during environment setup? -Live results for each run are in the -[GitHub Actions job summaries](https://redirect.github.com/viniciusdc/k8s-local-benchmark/actions) -of the benchmark repo. - -## Conclusions +## Results Benchmarks ran on `ubuntu-latest` (2 vCPUs, 7 GB RAM), K8s v1.32, extras disabled across all three tools. Full lifecycle logs: [benchmark run — April 2026](https://redirect.github.com/viniciusdc/k8s-local-benchmark/actions/runs/24283780667). -### Timing results +### Timing | | kind v0.31.0 | k3d v5.8.3 | k3s v1.32.13 | |---|---:|---:|---:| @@ -121,6 +117,8 @@ its total time to first workload (~18s create + deploy) faster than k3s (~27s). | **Root required** | no | no | yes | | **Teardown** | `kind delete cluster` | `k3d cluster delete` | `k3s-uninstall.sh` | +## Conclusions + For a shared CI action, **k3d** is the pragmatic pick: fastest end-to-end from create to first workload, no rootful install, Docker-based image injection, and already in use across half the nebari-dev repos. k3s is worth revisiting if startup time becomes a From f11c178679f59dac04a6a43aa4d827d92b49996e Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Wed, 29 Apr 2026 20:16:09 -0300 Subject: [PATCH 9/9] fix: restore list markers, brighten strong text, tighten h3 spacing --- src/layouts/ArticleLayout.astro | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/layouts/ArticleLayout.astro b/src/layouts/ArticleLayout.astro index 2d0347c..8ac1049 100644 --- a/src/layouts/ArticleLayout.astro +++ b/src/layouts/ArticleLayout.astro @@ -140,10 +140,21 @@ const { title, description, meta, subtitle, backHref, backLabel, nextHref, nextL } .article-content :global(ul), .article-content :global(ol) { - padding-left: 1.3rem; + padding-left: 1.4rem; margin: 12px 0 20px; } - .article-content :global(li + li) { margin-top: 8px; } + .article-content :global(ul) { list-style-type: disc; } + .article-content :global(ol) { list-style-type: decimal; } + .article-content :global(li::marker) { + color: var(--faint); + } + .article-content :global(li + li) { + margin-top: 8px; + } + .article-content :global(strong) { + color: var(--foreground); + font-weight: 600; + } .article-content :global(code) { color: #dfbb7f; font-family: var(--font-mono);