Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.