Skip to main content
The Kubernetes rules follow the same split as container pushes: the build is pure — manifests render into .rbs/bin/ — and the apply is a deliberate run-time side effect:
There is no rbs deploy subcommand; deploying is running a deploy target. Two rules cover the surface:
  • k8s_workload — a typed wrapper that generates correct YAML for the common shapes: Deployment + Service, StatefulSet + volume claims + headless Service, a Secret populated from your environment, and an Ingress.
  • k8s_deploy — the underlying rule: hand-written manifest templates with ${VAR} substitution. Anything beyond the wrapper’s shapes belongs here.

Declaring a workload

k8s_workload generates the YAML and declares a k8s_deploy target named <name>_deploy carrying it:
A stateful example — a database with persistent storage:

k8s_workload parameters

The wrapper is deliberately small: kinds beyond deployment/statefulset, extra pod settings, network policies, or multi-path ingresses belong in hand-written YAML passed to k8s_deploy — not in more wrapper options.

Hand-written manifests with k8s_deploy

When you need full control, write the YAML yourself and declare it:
In the YAML, reference the image by its placeholder:
At least one of manifests or manifest_content must be non-empty.

The substitution model

Placeholders are ${VAR}, resolved in two phases:
  1. Build timesubstitutions (static strings) render into the output manifests under .rbs/bin/. Anything unresolved stays as-is.
  2. Deploy timeimages (push targets or literal refs), uses (exported values of applied infra resources), and env (names read from the deploying shell). Substituted manifests are piped to kubectl on stdin, never written to disk — which is exactly why secrets flow through env.
Substitution is one pass with no nesting, and a placeholder still unresolved at apply time fails loudly, naming the manifest and the placeholders. Manifests apply in sorted filename order. k8s_workload prefixes its generated files (00-secret, 05-pvc-*, 10-deployment, 20-service, 30-ingress) so Secrets land before the pods that mount them — name hand-written files the same way when ordering matters.
Image references that point at an oci_push target resolve through the same repository resolution the push itself uses (explicit repository, or infra state via uses). Push and deploy therefore cannot disagree about which registry an image lives in.

How the apply actually runs

rbs run on a deploy target builds it (rendering manifests), resolves the deploy-time substitutions, then drives kubectl:
  • kubectl is hermetically pinned. rbs downloads its pinned kubectl release on first deploy and verifies it against the SHA-256 Kubernetes publishes. PATH is never consulted — whatever kubectl a machine happens to have is exactly the non-hermeticity the pin removes. No kubectl installation is a prerequisite.
  • KUBECONFIG passes through untouched; the cluster you talk to is the one your kubeconfig selects.
  • RBS_KUBE_CONTEXT overrides the target’s context attribute per invocation.
  • RBS_K8S_ACTION=delete turns the run into kubectl delete (with --ignore-not-found), tearing down everything the target applied. Only apply and delete are accepted.
  • The target namespace is created idempotently before the apply.
  • RBS_KUBECTL is an explicit operator override (air-gapped mirrors, test shims). The executor logs loudly whenever it is in effect.

Pinning a kubectl version

rbs embeds a default kubectl version, so nothing is required for deploys to work. To pin a different release, register the toolchain in WORKSPACE.rbs:
Any release published on dl.k8s.io works; the download is verified against its published checksum.

A full deploy sequence

A realistic order for a stack of services (images first, then data layer, then services, then edge):
Tear any piece down with RBS_K8S_ACTION=delete rbs run //deploy:<target>. PersistentVolumeClaims survive deletion — remove them explicitly if you mean to lose the data.