Self-hosted service
saiyam1814/kiac avatar
saiyam1814/kiac

kiac gives every Kubernetes node its own VM, and a systemd loop for LoadBalancer

Local Kubernetes on Apple's container framework - every node is its own lightweight VM. Metrics, storage, and LoadBalancer included.

374 stars24 forksGoMIT

At a glance

What is it?
A Go CLI that builds local Kubernetes clusters on Apple's container framework so each node boots as a lightweight virtual machine with its own kernel. Storage, metrics and a LoadBalancer controller are preinstalled, and the GPU path is marked alpha.
Who is it for?
kiac fits a developer who needs to rehearse node failure, read real metrics or watch a LoadBalancer get an address on a Mac, because those are the three things a shared-kernel local cluster cannot show you. It does not fit anyone on Intel Macs or on Linux, since the runtime is Apple's own container framework.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The hypervisor is the boundary, not namespaces

The argument is about what a Kubernetes node is. A node wants its own kernel, its own kubelet, its own cgroups and an IP that can come and go by itself, and most local Kubernetes tools give you containers sharing one kernel inside a single hidden virtual machine.

The failure described is mundane and specific. The illusion holds until you try to test a node failure, run kubectl top, or use a service of type LoadBalancer, and then it cracks.

With kiac the boundary between nodes is the hypervisor. Four consequences are claimed from that one change. Blast radius shrinks, because an escape reaching a shared kernel would reach every node, while a per-node virtual machine contains it. Failure domains separate, because one panic or runaway sysctl takes down only the machine that caused it. Node failure becomes real, so stopping a node VM produces NotReady detection, eviction and rescheduling rather than the killing of one process among several sharing a kernel. And each node has its own proc filesystem, sysfs, modules and sysctls.

The project is explicit that it still depends on containers. The narrower claim is that when the workload being isolated is a machine, a machine-grade boundary is the right one.

The whole thing is two commands:

bash
brew install --cask saiyam1814/tap/kiac
kiac create cluster --workers 2

vminitd is PID 1 and the host drives it over vsock

The runtime underneath is Apple's own container framework, and four properties of it are what make a node possible.

The image becomes a disk. The container image is turned into an EXT4 filesystem and handed to the virtual machine as its root block device, rather than being layered as an overlay on a shared host kernel.

A dedicated kernel boots. Each container gets its own minimal Linux kernel, shared with neither the host nor any other container.

vminitd runs as PID 1. A small init system written in Swift comes up first and then launches and supervises the process inside, with the host driving it through a gRPC API over vsock.

Devices are virtio and networking is direct. With no BIOS and no legacy device emulation, the virtual machine boots in about a second and receives an address the Mac can reach.

The claim in the project's own framing is that you get the developer experience of containers with the isolation boundary of a virtual machine, and that combination is what a node wants. There is no Docker Desktop, no Lima and no QEMU in the path.

kiac-lb is a systemd loop, not a controller

A service of type LoadBalancer is where most local clusters stall, and the solution here is deliberately small.

kiac-lb ships by default as a systemd loop running inside the control-plane virtual machine. It assigns node IPs to services in about two seconds, shares one IP across services when ports do not collide, and heals itself after node restarts.

What it is not doing matters as much as what it does. No pods, no admission webhooks, nothing left pending, no tunnels.

That is a much smaller surface than a controller with a CRD and a webhook path, and it is consistent with the rest of the design. A loop that assigns an address inside the control-plane VM cannot fail independently of the node it is assigning.

Storage follows the same preinstalled philosophy. A default StorageClass backed by the local path provisioner is installed at create time, so StatefulSets and volume claim templates bind without extra setup, and metrics-server ships preconfigured so kubectl top nodes works as soon as the cluster is up.

Host directories are exposed with repeatable mount options that place macOS paths at matching paths inside every kubeadm or k3s node, which is what makes hostPath volumes usable rather than theoretical.

Two distros, and a 22 to 54 second k3s cluster

The default is kubeadm on the kindest node image, which is the conventional path and the one to use when you want to see the same components a managed cluster runs.

The alternative is k3s, selected with a distro flag and running the rancher image under a lightweight supervisor in every virtual machine. Its footprint is quoted as a sqlite datastore, a two-node cluster in 22 to 54 seconds, and about 3.7GB of host memory in total.

That is the choice to make deliberately. A k3s cluster starts quickly enough to rebuild from scratch, which changes how you think about experiments, while kubeadm gives you the standard control-plane components and a slower startup.

Scaling is a flag rather than a project. A workers count gives a real topology with scheduling, cross-node pod networking and node failures you can practise on, so a two-node cluster is one command and a longer one is a different number.

Observability is an opt-in flag that installs Prometheus and Grafana on a real LoadBalancer address, with cluster overview and node dashboards already provisioned. Address family is also a flag, with dual or IPv6 giving pods, services and nodes real IPv6 rather than a mapped address.

Cilium runs on a sha-pinned custom kernel

The eBPF path is opt-in and comes as a pair of flags rather than a single switch, because two things have to change together.

With Cilium selected and a full kernel requested, the tool downloads a published kernel build pinned by hash, carrying VXLAN, eBPF and br_netfilter, and then drives the official Cilium installer rather than a bundled variant of it.

The reason for the custom kernel is stated plainly: those features have to be present in the kernel before the CNI can use them, and the host kernel on a Mac cannot be assumed to have them.

Throughput figures are quoted for the vxlan datapath: cross-node pod traffic at about 285MB per second and Mac-to-pod at about 1GB per second. Those are the numbers to check against your own workload rather than assume.

Direct networking is a separate feature from the CNI choice. Every node gets a routable address on macOS 26 and later, so NodePorts can be hit without a port mapping, with a small node-local edge proxy terminating external TCP first so large uploads from sibling virtual machines do not hit a known vmnet segmentation offload forwarding problem.

GPU workers advertise a resource name of their own

The Apple GPU path is marked alpha and reached with a flag that creates worker virtual machines backed by krunkit, exposing the Mac's GPU through virtio-gpu and Venus.

What is interesting is the honesty about the resource. Rather than claiming a standard device name, the cluster advertises only a resource under the project's own domain, through either a device plugin or dynamic resource allocation. Anything scheduling against a conventional GPU request will therefore not match.

There is a bench command to prove the path rather than assert it. It exercises Vulkan with a pinned llama.cpp workload and can compare that figure against native Metal on the same machine, which is the only way to tell whether the virtualised path is worth using for a given model.

The example directory reflects how broad the surface has become: separate labs for GPU clusters, GPU inference, DRA memory and DRA metadata, Vulkan, Cilium, Gateway API, observability scraping, chaos drills, and shell-driven setups for Portainer, Rancher, k8gb and OpenChoreo.

The build cross-compiles two arm64 sidecars and checks they are current

The module targets Go 1.26 with a pinned toolchain, and its direct dependencies are few: cobra for the command line, a YAML library, the Kubernetes API and machinery modules, and a qcow2 reader from the Lima project.

The Makefile reveals the part of the design that is easiest to miss. Two binaries do not run on the Mac at all. An edge proxy and a GPU agent run inside the node virtual machines, so both are cross-compiled for linux on arm64 with cgo disabled, built into a bin directory and then gzipped into an assets directory inside the cluster package, from where they are shipped into the nodes.

Each has a matching check target that matters more than it looks. The check rebuilds the binary into a temporary directory, gzips it and compares it byte for byte with the committed asset, failing with a message telling the contributor the asset is stale and to regenerate it. Without that step, an asset committed from a slightly different tree would be shipped silently into every node.

Beyond that the target list covers the usual ground plus a runtime smoke test, and release versioning comes from git describe with a dirty flag, so a build from an untagged tree is labelled as one.

Editorial conclusion

kiac fits a developer who needs to rehearse node failure, read real metrics or watch a LoadBalancer get an address on a Mac, because those are the three things a shared-kernel local cluster cannot show you. It does not fit anyone on Intel Macs or on Linux, since the runtime is Apple's own container framework. Two caveats before planning around it: the Apple GPU worker path is labelled alpha and advertises a resource name of its own rather than pretending to be a standard device, and direct networking with routable node IPs is stated for macOS 26 and later. Otherwise the operational story is short, since resume after a reboot is idempotent and upgrades k3s clusters in place.

Frequently asked questions

How do I create a kiac cluster?

Install the cask with brew install --cask saiyam1814/tap/kiac, then run kiac create cluster --workers 2. The workers flag gives a real multi-node topology with scheduling, cross-node pod networking and node failures you can practise on.

Does kiac need Docker Desktop, Lima or QEMU?

None of them. It runs natively on Apple silicon on top of the Apple container framework, with opt-in Apple GPU worker nodes reached through krunkit and Venus.

How does kiac make type LoadBalancer work?

kiac-lb ships by default as a small systemd loop inside the control-plane VM. It assigns node IPs to services in about two seconds, shares one IP across services when ports do not collide, and heals after node restarts, with no pods, webhooks or tunnels.

What do the Cilium and kernel flags do in kiac?

Selecting Cilium with a full kernel downloads a published, hash-pinned kernel build carrying VXLAN, eBPF and br_netfilter, then runs the official Cilium installer. Quoted figures are about 285MB/s for cross-node pod traffic and about 1GB/s Mac to pod on the vxlan datapath.

Does a kiac cluster survive a Mac reboot?

Yes. The resume cluster command restarts the kubeadm or k3s VMs and heals stale control-plane, node, kubeconfig and networking addresses. It is idempotent and upgrades existing k3s clusters in place.

Which GPU resource does kiac advertise to Kubernetes?

Only a resource under the project's own domain, exposed through a device plugin or dynamic resource allocation, rather than a standard device name. A bench command exercises the Vulkan path with a pinned llama.cpp workload and compares it against native Metal.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. saiyam1814/kiac on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/saiyam1814-kiac.svg)](https://hysenlabs.com/projects/saiyam1814-kiac)