How to Build a Kubernetes Operator in Rust (kube-rs)

You build a Kubernetes operator in Rust by using the `kube-rs` crate to define Custom Resource Definitions (CRDs) and implement a reconciliation loop that watches for changes and updates cluster state accordingly.

You build a Kubernetes operator in Rust by using the kube-rs crate to define Custom Resource Definitions (CRDs) and implement a reconciliation loop that watches for changes and updates cluster state accordingly. The process involves generating Rust types from your CRD YAML, setting up a controller with kube-runtime, and handling the logic to ensure the actual cluster state matches your desired state.

First, define your Custom Resource in YAML and generate the corresponding Rust structs using kubebuilder or k8s-openapi macros. Here is a minimal example of a MyResource CRD and its Rust representation:

# myresource.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: myresources.example.com
spec:
  group: example.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                replicas:
                  type: integer
            status:
              type: object
              properties:
                observedGeneration:
                  type: integer

In your Cargo.toml, add kube with the runtime feature. Then, use the kube::CustomResource derive macro to create the Rust type:

use kube::CustomResource;
use serde::{Deserialize, Serialize};

#[derive(CustomResource, Deserialize, Serialize, Clone, Debug)]
#[kube(group = "example.com", version = "v1", kind = "MyResource", plural = "myresources")]
#[kube(status = "MyResourceStatus")]
pub struct MyResourceSpec {
    pub replicas: i32,
}

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MyResourceStatus {
    pub observed_generation: i64,
}

The core of the operator is the reconciliation loop. You use kube_runtime::Controller to watch for events and run your logic whenever the resource changes. This function receives the current state and returns the desired state or an error.

use kube::{Api, Client, ResourceExt};
use kube_runtime::controller::{Action, Controller};
use k8s_openapi::api::apps::v1::Deployment;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = Client::try_default().await?;
    let my_resources: Api<MyResource> = Api::default_namespaced(client.clone());
    let deployments: Api<Deployment> = Api::default_namespaced(client.clone());

    let controller = Controller::new(my_resources, kube_runtime::config::Config::default())
        .shutdown_on_signal()
        .run(
            |my_resource, event| async move {
                // Reconciliation logic:
                // 1. Create or update a Deployment based on my_resource.spec.replicas
                // 2. Update my_resource.status.observed_generation
                
                let desired_replicas = my_resource.spec.replicas;
                // ... logic to sync Deployment ...
                
                Ok(Action::await_change())
            },
            kube_runtime::config::Config::default(),
        )
        .await;

    controller.await?;
    Ok(())
}

Deploy the operator by building the binary and running it inside a pod with appropriate RBAC permissions to read/write your CRDs and manage Deployments. The kube-rs ecosystem handles the heavy lifting of API interactions, making the operator pattern idiomatic and type-safe in Rust.