Using the Kubernetes API

ConfigMaps and Secrets kept current by a watch, k8s:// secret references, service discovery from EndpointSlices, and leader election — the k8s feature.

The k8s feature (18.35.0) gives an app running in Kubernetes the API itself: configuration that changes without a restart, secrets read where they live, the pods behind a Service, and one replica at a time for singleton work. It builds on kube-rs, which rust_web_server::k8s re-exports (kube, k8s_openapi, schemars).

rust-web-server = { version = "18.35", features = ["k8s"] }

A client

use rust_web_server::k8s::client;

let kube = client::client().await?;          // in a pod: its service account; elsewhere: ~/.kube/config
let ns = client::current_namespace();        // POD_NAMESPACE, the service account's, the kubeconfig's, else "default"

client::from_kubeconfig(path) reads a given file; client::offline() is a client that never connects (for an operator's --manifests, which describes its controllers without a cluster); client::identity() is this replica's name (POD_NAME, else HOSTNAME). rws gen deploy sets POD_NAME and POD_NAMESPACE from the downward API.

ConfigMaps and Secrets

config::ConfigWatch keeps one ConfigMap's or Secret's data current through a watch — no volume mount, no restart, and a change arrives in a second or two:

use rust_web_server::k8s::config::ConfigWatch;

let settings = ConfigWatch::config_map(kube.clone(), &ns, "app-settings").await?;   // fails if it can't be read now
let page_size: usize = settings.get("page_size").and_then(|v| v.parse().ok()).unwrap_or(20);
settings.on_change(|data| println!("now: {data:?}"));

// RWS_CONFIG_* keys become rws's own settings, re-read after each change:
settings.apply_to_env("RWS_CONFIG_");

let credentials = ConfigWatch::secret(kube.clone(), &ns, "db-credentials").await?;
Method What
get(key), snapshot() The value now, every key now
on_change(f) Called with the new data after each change
apply_to_env(prefix) Keys with prefix as environment variables, now and on change, then server_config::invalidate()
exists() false once the object was deleted (the last data is kept)

Binary data is included when it's UTF-8. A clone shares the watch; it stops when the last clone is dropped.

k8s:// secret references

let password = rust_web_server::k8s::config::resolve(&kube, "k8s://prod/db-credentials#password").await?;

k8s://<namespace>/<secret>#<key> (or k8s://<secret>#<key> for the current namespace) reads one key of a Secret, as vault://, aws-sm:// and azkv:// read other stores.

Service discovery

The ready endpoints of a Service, from its EndpointSlices, kept current in a BackendPool — for a ReverseProxy, or to reach each pod of a headless Service:

use rust_web_server::k8s::discovery;

let pool = discovery::service_endpoints(kube.clone(), &ns, "orders", Some("http")).await?;
pool.backends();    // ["10.42.0.17:8080", "10.42.1.9:8080"]

Endpoints whose ready condition is false are left out; Some("http") picks the Service port of that name (None: the first). IPv6 addresses come as [addr]:port.

Leader election

leader::LeaderElector holds a coordination.k8s.io/v1 Lease so one replica at a time does singleton work — a scheduler, a cleanup loop:

use rust_web_server::k8s::leader::LeaderElector;

let elector = LeaderElector::new(kube.clone(), &ns, "report-scheduler");
let mut leader = elector.handle();
tokio::spawn(elector.run());

loop {
    leader.wait_until_leader().await;
    // … work while leader.is_leader() …
}

The holder renews every 5 s; another replica takes over when a renewal is 15 s old (.timing(duration, renew)). Writes carry the Lease's resourceVersion, so two replicas can't both win. release() hands it over at once (on shutdown). Controllers get this built in: Operator::leader_election.

Permissions

The pod's service account needs:

For Rules
ConfigWatch::config_map / secret, k8s:// get, list, watch on configmaps / secrets in the namespace
discovery::service_endpoints list, watch on endpointslices (discovery.k8s.io)
LeaderElector get, create, update on leases (coordination.k8s.io)

manifests::Rbac writes them: see Manifests.

Last updated · Edit this page on GitHub · Markdown