Building Kubernetes operators with OCaml
TL;DR#
I created ocaml-kube, a Kubernetes client and controller library for OCaml, published on opam as kube. This post shows how to use it through three operator tutorials, following the familiar Kubebuilder workflow: scaffold a project, define an API, generate the CRD, implement reconciliation, and run it against a cluster.
The library and Kubernetes support#
The kube.0.1.3 release used here supports Kubernetes 1.34, 1.35, 1.36, and 1.37, with integration tests against real API servers. Older clusters may work through the generic client but aren’t supported, and the supported range doesn’t imply coverage of every alpha feature.
ocaml-kube is a native OCaml 5 implementation, not a binding to Go’s client-go. Its HTTP/1.1 transport uses Unix sockets and the OCaml TLS library. Watches and controller workers run on system threads with cooperative cancellation.
Built-in resource records and JSON codecs are generated from checksum-pinned Kubernetes OpenAPI schemas. Each minor has a separate schema library, from kube.api.v1_34 through kube.api.v1_37, sharing the same runtime. These tutorials use Kube_api_v1_36 for 1.36 resource types; that doesn’t restrict the operators to a 1.36 cluster.
Above the client sit LIST/WATCH caches, a work queue, and retries. Kube.Operator handles process startup, credentials, shutdown, diagnostics, and optional leader election. The application code describes what should exist.
Same pattern, different language#
An operator watches Kubernetes resources and repeatedly brings actual state towards the desired state in spec. Changing the language doesn’t change that contract.
The interesting part here is how OCaml expresses it. Records describe the API, a PPX deriver generates JSON codecs and schemas, and module functors build clients and controllers for a particular resource type. For example:
module Controller = Kube.Controller.Make (Custom_resource)
module Config_maps =
Kube.Reconcile.Make (Kube_api_v1_36.Core_v1.ConfigMap)
From scaffold to CRD#
With OCaml 5.1 or newer and opam installed:
opam install kube.0.1.3
opam exec -- ocaml-kube init \
--output greeting-operator \
--group tutorial.ocaml.dev \
--version v1alpha1 \
--kind Greeting
This covers the project-and-first-API setup normally split between kubebuilder init and kubebuilder create api. Here, --group takes the full API group. The generated project includes a model, controller skeleton, CRD, RBAC, sample resource, Deployment, and Dockerfile. The reconciler still needs application logic.
In the completed Greeting tutorial, the desired state is one field:
module C = Kube_crd
module Spec = struct
type t = {
message : string;
[@kube.schema C.Schema.string ~min_length:1 ~max_length:200 ()]
}
[@@deriving kube]
end
Those attributes become validation in the CRD. Kube_crd.Resource.Make combines the spec, status, and resource identity into the typed resource module.
After editing the model, generate the manifest from inside the project:
opam exec -- dune exec tools/generate_crd.exe > deploy/crd.yaml
opam exec -- dune build @codegen-check
That’s the equivalent of make manifests. The second command catches a checked-in CRD that no longer matches the OCaml model.
Three tutorials#
Each example is an independent Dune project with a walkthrough, tests, and deployment files.
- Greeting writes
spec.messageinto a ConfigMap. It introduces Server-Side Apply, owner references, and aReadystatus condition. - WebApp manages a Deployment and Service. It watches both child kinds and reports the Deployment’s ready replica count.
- Project is cluster-scoped and manages a Namespace and ResourceQuota. A finalizer keeps the Project around until Namespace deletion completes.
The Greeting’s child write is small:
Config_maps.apply_owned ~cancel:request.cancel client
~field_manager:"greeting-operator"
~owner_api:Custom_resource.api
~owner:greeting.metadata
(desired_config_map greeting)
The helper attaches the owner reference and applies the desired fields. An owned-resource watch enqueues the parent when a child changes. Kubernetes handles garbage collection when the owner is deleted; the Project example adds explicit cleanup to demonstrate finalizers.
Try it on kind#
With Docker running, plus kind, kubectl, and the OCaml toolchain available:
git clone https://github.com/thevilledev/kube-ocaml-examples.git
cd kube-ocaml-examples
opam install kube.0.1.3 alcotest
make build
make test
make check-generated
make kind-smoke
I ran all three controllers locally against a Kubernetes 1.37 kind cluster. The smoke test checks the ConfigMap contents, waits for the WebApp rollout and status, checks the Project quota, and verifies that finalization removes its Namespace. The five unit tests and CRD drift checks also passed.
The smoke test stops the controllers and deletes the sample resources, but leaves the cluster available. When finished:
kind delete cluster --name ocaml-kube-examples
These are small examples for learning the API and controller patterns. For the complete commands, including container builds and in-cluster deployment, start with the repository README.