battery-operator is a Kubernetes operator for battery, which keeps warm pools of flintlock MicroVMs. With it, you declare a pool of Firecracker MicroVMs and claim one the way you use anything else in a cluster:
This recording is one Kubernetes Node with KVM. A Pool fills with two
Firecracker MicroVMs. A Consumer claims one with the Client Library, and
runs a command in it through the Exec Agent. The Pool starts a new MicroVM
in place of the one claimed. Then a claim made with kubectl binds, is
renewed, and goes Expired when nobody renews it. Deleting the Pool deletes
its MicroVMs.
Nothing in it is a fake: the Host runs Firecracker, flintlockd and
battery’s own poolmgrd, in a Lima VM with nested virtualization on a Mac.
The real hosts trial
brings it up, and hack/real-hosts/demo.sh --record records it again.
Kubernetes API Operator pod each Host (a Node)
┌─────────────────┐ ┌──────────────────────────────────┐ ┌───────────────────────────┐
│ Pool │◀───▶│ Pool, Claim, Inventory │ │ Exec Agent (DaemonSet) │
│ MicroVMClaim │ │ Controllers, certificate signer │ │ checks the claim, then │
│ Nodes, CSRs │ │ │ gRPC, loopback │ │ relays exec ───────┐ │
└─────────────────┘ │ ▼ │ │ ▼ │
│ battery poolmgrd (sidecar) ──────┼─────┼─▶ flintlockd ──▶ Firecracker
└──────────────────────────────────┘ mTLS└───────────────────────────┘
flintlockd over mutual TLS with them.flintlockd’s exec API.You need a Kubernetes cluster whose Hosts meet the
Host prerequisites:
KVM, containerd’s thin pool, and flintlockd with its exec API. The
quickest way is the real hosts trial, which makes one Lima VM on a Mac, or
any Ubuntu machine with KVM, into such a cluster:
hack/real-hosts/up.sh # the Host, k3s, cert-manager and the Operator
export KUBECONFIG=hack/real-hosts/.state/kubeconfig
kubectl apply -f examples/demo/demo.yaml # a namespace, a Holder and a Pool
kubectl -n demo wait --for=jsonpath={.status.available}=2 pool/demo
The demo manifests say what each object is for.
To install a release on your own cluster, with Flux or with kubectl, see
Install.
Every claim is a MicroVMClaim in the cluster, so kubectl get shows which
MicroVMs are held, on which Node, and until when:
apiVersion: battery.liquidmetal-x.dev/v1alpha1
kind: MicroVMClaim
metadata:
name: build-1
namespace: demo
spec:
poolRef:
name: demo
serviceAccountName: demo-holder # the Holder: only it can use the MicroVM
$ kubectl -n demo get microvmclaims
NAME POOL PHASE NODE EXPIRES AGE
build-1 demo Bound bo-host-1 2026-09-27T20:46:09Z 6s
$ kubectl -n demo get microvmclaim build-1 -o jsonpath='{.status}' | jq '{phase, microVM, host, leaseExpiresAt}'
{
"phase": "Bound",
"microVM": { "uid": "01M3J9T8R6826GNNC6PJ3TDTSC" },
"host": { "agentAddress": "192.168.5.15:10270", "nodeName": "bo-host-1" },
"leaseExpiresAt": "2026-09-27T20:46:09Z"
}
EXPIRES is the time the claim’s Lease runs out. The Lease lasts the Pool’s
lease.expiryThreshold. The holder renews it by setting spec.renewTime,
and the Client Library does this for its claims. A claim nobody renews goes
Expired, and battery deletes its MicroVM. Deleting a claim releases its
MicroVM at once.
A Consumer uses the Client Library,
pkg/claimclient:
cl, err := claimclient.New(claimclient.Config{
Client: kubeClient, // a controller-runtime client, as the Holder
Namespace: "demo",
ServingCA: servingCA, // the Operator's serving CA
})
// Claim blocks until the claim is Bound.
claim, err := cl.Claim(ctx, claimclient.Request{
Pool: "demo",
ServiceAccountName: "demo-holder",
})
defer claim.Release(ctx)
// Dial reaches the Exec Agent on the MicroVM's Host.
conn, err := claim.Dial(ctx)
The demo’s Consumer,
hack/real-hosts/smoke,
is a complete example.
flintlockd. The e2e suite
runs the Manifests on kind, with battery’s real poolmgrd.v1alpha1. The Operator, the Exec Agent and the Client Library work
end to end on a real Host. The project lives outside battery so it can be
proven without disturbing battery, with the aim of being adopted by
liquidmetal-dev. The design was discussed in
liquidmetal-dev/battery#46.