Tutorial 25: Kubernetes Manifests & Health Probes ☸️
cargo-rullst can generate a Kubernetes starter set. The files contain project
name/port defaults and must be reviewed for the target cluster, registry,
secrets, storage, network policy, ingress, workload identity, and security
policy before deployment.
Step 1: Generate the starter manifests
cargo rullst make:k8s
The command writes deployment.yaml, service.yaml, configmap.yaml,
hpa.yaml, ingress.yaml, and all-in-one.yaml under k8s/. It refuses to
run when any of those files already exists, so move customized manifests aside
to regenerate them, and review the diff.
Resource names, the image and the ingress host use the [package] name as a
lowercase RFC 1123 label, so a package named my_app becomes my-app; tag
your image with the same name. Replace the placeholder
image: <name>:latest with an immutable registry reference (preferably a
digest). The generated ConfigMap contains non-secret
settings only; use a Kubernetes Secret/external secret manager for credentials.
The Ingress selects ingressClassName: nginx and has a tls section, so
cert-manager’s ingress-shim asks the letsencrypt-prod ClusterIssuer for a
certificate and stores it in the <project>-tls Secret. Replace the placeholder
<project>.local host in both tls.hosts and rules with a public DNS name you
control (an ACME issuer cannot certify .local), and create the issuer or
change the annotation. Without TLS, the production configuration’s Secure
session cookies, HSTS and Nexus Basic Auth over verified TLS do not work.
Step 2: Mount lifecycle-aware health routes
Projects created by cargo rullst new already merge
rullst::health::health_router() into their router, so the generated probes
answer from the first deployment: /health and /ready both return 200 while
the process serves requests. To report startup, dependency and drain state on
/ready instead, replace that merge with the lifecycle-aware router:
use rullst::{ApplicationLifecycle, Router, Server};
use rullst::health::{health_router_with_lifecycle, init_health_boot_time};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
init_health_boot_time();
let lifecycle =
ApplicationLifecycle::with_required_components(["database", "queue"])?;
// Set these only after the application's own bounded checks succeed.
lifecycle.set_component_ready("database", true)?;
lifecycle.set_component_ready("queue", true)?;
let app = Router::new()
.merge_axum(health_router_with_lifecycle(lifecycle.clone()));
Server::new(app)
.with_lifecycle(lifecycle)
.run(3000)
.await?;
Ok(())
}
/health remains process-only so an external database outage does not create a
restart loop. /ready returns 503 during startup, while any registered
component is unavailable, during drain, and after stop. The JSON carries only
aggregate counts; it never emits the component labels or their error messages.
The version field of both probes is the Rullst framework version. To see
which build of your application a Pod runs, use
health_router_with_lifecycle_and_version(lifecycle.clone(), env!("CARGO_PKG_VERSION"))
(or health_router_with_version instead of health_router), which adds that
string as app_version.
The component registry is immutable, accepts at most 32 validated labels, and
does not execute probes by itself. The application must perform bounded,
timeout-protected checks and update each bit. Server marks startup complete
after binding, closes new application admission before graceful shutdown, and
waits through Axum for accepted requests. run_with_shutdown accepts a
supervisor future when OS signals are not the desired trigger.
This is one process contract. Kubernetes removes an unready Pod from service
according to its own timing; Rullst does not coordinate replica consensus,
load-balancer propagation, dependency failover, preStop, or the Pod’s
terminationGracePeriodSeconds. Measure those together in staging. The legacy
health_router() remains the simpler process-only pair when dependency-aware
admission is not requested.
Step 3: validate before applying
kubectl apply --dry-run=client -f k8s/all-in-one.yaml
kubectl diff -f k8s/all-in-one.yaml
kubectl apply -f k8s/all-in-one.yaml
The generated HPA uses autoscaling/v2 and requires a working resource metrics
pipeline. Validate against the actual cluster version and admission policies.
Release checklist
- add pod/container security contexts compatible with the reviewed image user;
- define CPU/memory requests and limits from measurements;
- add PodDisruptionBudget, topology spread/anti-affinity, NetworkPolicy, and service account/workload identity as required;
- terminate TLS with a configured issuer and real hostname;
- provision durable storage only where application state truly needs it; and
- verify probes, graceful termination, migrations, rollback, and autoscaling in a staging cluster.