> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reasonos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying to Kubernetes

> Declare Kubernetes workloads and manifests as build targets, then apply them to a cluster with rbs run — using a hermetically pinned kubectl.

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**:

```bash theme={null}
rbs run //deploy:myservice_deploy                        # kubectl apply
RBS_K8S_ACTION=delete rbs run //deploy:myservice_deploy  # kubectl delete (tear down)
```

There is no `rbs deploy` subcommand; deploying is running a deploy target. Two
rules cover the surface:

```python theme={null}
load("@rbs//k8s/rules.rbs", "k8s_deploy", "k8s_workload")
```

* **`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:

```python theme={null}
load("@rbs//k8s/rules.rbs", "k8s_workload")

k8s_workload(
    name = "myservice",
    image = "//services/myservice:push",   # an oci_push target — or a literal "host/repo:tag"
    kind = "deployment",
    replicas = 1,
    ports = [{"name": "http", "port": 8090}],
    env = {"LOG_LEVEL": "info"},
    secret_env = ["DATABASE_URL", "API_KEY"],   # values from the deployer's shell at apply time
    namespace = "myapp",
)
```

```bash theme={null}
rbs run //services/myservice:push      # push the image first
rbs run //deploy:myservice_deploy      # then apply
```

A stateful example — a database with persistent storage:

```python theme={null}
k8s_workload(
    name = "postgres",
    image = "postgres:17-alpine",      # literal public image
    kind = "statefulset",
    replicas = 1,
    ports = [5432],
    env = {"POSTGRES_USER": "myapp", "POSTGRES_DB": "myapp"},
    secret_env = ["POSTGRES_PASSWORD"],
    volumes = [{"name": "data", "mount_path": "/var/lib/postgresql/data", "size": "5Gi"}],
    namespace = "myapp",
)
```

### k8s\_workload parameters

| Parameter          | Type          | Default        | Required | Meaning                                                                                                                                     |
| ------------------ | ------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`             | string        | —              | yes      | Workload name. The Deployment/StatefulSet, Service, and Secret all derive from it; the deploy target is `<name>_deploy`.                    |
| `image`            | string        | —              | yes      | An `oci_push` target label (`":push"`, `"//services/x:push"`) resolved at deploy time, or a literal `host/repo:tag`.                        |
| `kind`             | string        | `"deployment"` | no       | `"deployment"` or `"statefulset"`. StatefulSets get a headless Service and turn sized volumes into `volumeClaimTemplates`.                  |
| `replicas`         | int           | `1`            | no       | Replica count.                                                                                                                              |
| `ports`            | list          | `[]`           | no       | Ints (`[8090]`) or dicts: `{"name": "http", "port": 8090, "target_port": 8090}`.                                                            |
| `env`              | string dict   | `{}`           | no       | Plain container environment values. May contain `${VAR}` deploy-time placeholders.                                                          |
| `secret_env`       | string list   | `[]`           | no       | Variable **names** delivered via a generated Secret; values come from the deploying shell at apply time and never touch disk.               |
| `volumes`          | list of dicts | `[]`           | no       | Each needs `"name"` and `"mount_path"`, plus one of: `"size"` (PVC / claim template), `"secret"` (mounted Secret), or neither (`emptyDir`). |
| `command` / `args` | string list   | `[]`           | no       | Container command and arguments.                                                                                                            |
| `labels`           | string dict   | `{}`           | no       | Extra workload labels.                                                                                                                      |
| `service`          | bool          | `True`         | no       | Generate a Service. A deployment with `service = True` needs at least one port. StatefulSets always get their headless Service.             |
| `service_type`     | string        | `"ClusterIP"`  | no       | Service type for deployments.                                                                                                               |
| `ingress`          | dict          | `None`         | no       | `{"host": ..., "path"?: "/", "port"?: ..., "class"?: ..., "tls_secret"?: ...}`.                                                             |
| `namespace`        | string        | `""`           | no       | Target namespace (created if missing at apply). Empty leaves it to the manifests / current context.                                         |
| `context`          | string        | `""`           | no       | kubeconfig context; `RBS_KUBE_CONTEXT` overrides at deploy time.                                                                            |
| `uses`             | string list   | `[]`           | no       | Applied infra resources whose exported values join the deploy-time substitutions.                                                           |
| `workspace`        | string        | `"default"`    | no       | Infra workspace for `uses` resolution.                                                                                                      |
| `substitutions`    | string dict   | `{}`           | no       | Static `${KEY}` values rendered at build time.                                                                                              |

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:

```python theme={null}
load("@rbs//k8s/rules.rbs", "k8s_deploy")

k8s_deploy(
    name = "gitdataplane_deploy",
    manifests = [
        "k8s/gitdataplane-statefulset.yaml",
        "k8s/gitdataplane-service.yaml",
        "k8s/gitdataplane-networkpolicy.yaml",
    ],
    images = {"IMAGE_GITDATAPLANE": "//services/gitdataplane:push"},
    namespace = "myapp",
)
```

In the YAML, reference the image by its placeholder:

```yaml theme={null}
containers:
- name: gitdataplane
  image: "${IMAGE_GITDATAPLANE}"
```

| Attribute          | Type        | Default     | Required | Meaning                                                                                                                     |
| ------------------ | ----------- | ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `manifests`        | label list  | `[]`        | no       | YAML manifest template files, package-relative. `${VAR}` placeholders allowed.                                              |
| `manifest_content` | string dict | `{}`        | no       | Inline manifests as `{filename: yaml}` — how `k8s_workload` feeds generated YAML in; usable directly too.                   |
| `substitutions`    | string dict | `{}`        | no       | Static `${KEY}` → value substitutions rendered at **build** time.                                                           |
| `images`           | string dict | `{}`        | no       | `${KEY}` → image substitutions resolved at **deploy** time: an `oci_push` target label, or a literal `host/repo:tag`.       |
| `env`              | string list | `[]`        | no       | Variable **names** resolved from the deploying shell at apply time (the secrets path). A missing variable fails the deploy. |
| `uses`             | string list | `[]`        | no       | Applied infra resources whose exported values (e.g. a registry's `REGISTRY_URL`) join the deploy-time substitutions.        |
| `namespace`        | string      | `""`        | no       | Target namespace, ensured to exist and passed as `-n`.                                                                      |
| `context`          | string      | `""`        | no       | kubeconfig context; empty uses the current context.                                                                         |
| `workspace`        | string      | `"default"` | no       | Infra workspace to resolve `uses` against.                                                                                  |

At least one of `manifests` or `manifest_content` must be non-empty.

## The substitution model

Placeholders are `${VAR}`, resolved in two phases:

1. **Build time** — `substitutions` (static strings) render into the output
   manifests under `.rbs/bin/`. Anything unresolved stays as-is.
2. **Deploy time** — `images` (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.

<Note>
  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.
</Note>

## 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`:

```python theme={null}
load("@rbs//k8s/toolchain.rbs", "k8s_toolchain")

k8s_toolchain(version = "v1.30.14")
```

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):

```bash theme={null}
export RBS_KUBE_CONTEXT=my-cluster
export POSTGRES_PASSWORD=... DATABASE_URL=...   # everything listed in secret_env / env

rbs run //services/api:push            # images → registry
rbs run //deploy:postgres_deploy       # database
rbs run //deploy:api_deploy            # services
rbs run //deploy:ingress_deploy        # edge routing
```

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.
