On Kubernetes
Run bcd as a privileged DaemonSet on the GPU nodes of a cluster you already operate, so the control plane adopts them without owning their host configuration.
Most machines run bcd the way the install script puts it there: a binary and a systemd unit
on a host the control plane provisions and configures. A cluster you already operate is different. Its GPU nodes
have a driver, a container toolkit and a MIG layout that something else manages, usually the NVIDIA GPU operator,
and you want the control plane to adopt them without taking that over. For that, bcd runs inside the cluster as a
DaemonSet: one privileged pod per GPU node, which joins the node once and then runs bcd node.
This is the opposite direction from a Kubernetes pool. There, bcd is installed on
machines the control plane owns, and the k8s-join config unit joins them to a cluster as workers. Here the cluster
and its nodes exist first, and bcd runs inside the cluster to report their GPUs and hand the bound ones to pods.
What you get
- One pod per selected node. The DaemonSet runs on every node, or on those
--node-selectornames (for examplenvidia.com/gpu.present=true, the label NVIDIA's GPU Feature Discovery sets), tolerates every taint by default, runs atsystem-node-criticalpriority and rolls one node at a time. - The host's view, and only what bcd uses of it. The
bcdcontainer is privileged, with host networking, and mounts exactly the host paths listed in host mounts:/dev,/sys,/var/lib/bcd, the CDI directories, the kubelet sockets bcd talks to, the kubelet's own credentials, and the container runtime's and NVIDIA toolkit's configuration, read-only. Never/var/lib/kubeletitself and never the host's/etc/kubernetes. The pod does not share the host's PID namespace (hostPID: false). It runs under the RuntimeClassnvidia(the GPU operator's;nvidiaRuntimeClass), and thebcdcontainer asks forNVIDIA_VISIBLE_DEVICES=allwithNVIDIA_DRIVER_CAPABILITIES=utilityonly, so bcd getsnvidia-smiand the driver's libraries to read and reset GPUs (b6d5548); workload pods still get CDI devices only. - No Kubernetes identity of its own. No service account token: bcd talks to its gateway, and what it does in the
cluster (its Node's labels, deleting pods, DRA claims and slices) it does by running the image's
kubectlwith the kubelet's own credentials, which NodeRestriction confines to its Node (see API access). - One join per node. The identity lives in the host's
/var/lib/bcd, so it survives pod restarts and image rollouts; every later start finds it and needs no token. - No host configuration.
config.tomlsets[host] units = falseby default. bcd applies noNodeSpec.hostunit and reports thehostsectionUNSUPPORTEDwith reasonHostManagedExternally(see host configuration), which the gateway records as an informational node condition. The inventory still reports what the node has, so capacity books only the GPUs, MIG instances and huge pages that the cluster's own tooling set up. - GPUs for pods. bcd hands the GPUs of
binding: 'kubernetes'reservations to the kubelet, with nok8s-joinrecord: see GPUs for pods. It gives pods only those GPUs only when bcd is the only source of GPUs on the node: see GPU isolation. - No self-update and no reboots. The configuration has no
[update] release_pins, so bcd refuses every self-update and you upgrade by rolling out a new image; bcd never reboots a node.
Host mounts
The bcd container's host paths follow what daemon/internal/kube uses and nothing more (27f2c1b, a5eee84,
d11810c). Mounting all of /var/lib/kubelet would give a compromised bcd pod every pod's volumes, secrets and service
account tokens and the kubelet's checkpoints; the host's /etc/kubernetes holds admin.conf and the cluster CA's
keys on a kubeadm control-plane node. tests/provider-kubernetes.test.ts asserts the exact set, with and without DRA.
| Host path | Mode | hostPath type | Why |
|---|---|---|---|
/dev, /sys | read-write | Directory | GPUs, cgroups, device nodes |
/var/lib/bcd | read-write | DirectoryOrCreate | The node key, identity and journal, kept across pod restarts |
/etc/cdi | read-write | DirectoryOrCreate | bcd writes each hold's CDI spec |
/var/run/cdi | read-only | DirectoryOrCreate | bcd only reads NVIDIA's generated spec there |
/var/lib/kubelet/device-plugins | read-write | Directory | bcd's plugin sockets, and the kubelet's registration socket it dials |
/var/lib/kubelet/pod-resources | read-only | Directory | bcd only connects to the socket; a read-only bind mount still allows connect() |
/var/lib/kubelet/plugins_registry, /var/lib/kubelet/plugins/gpu.bcd.dev | read-write | DirectoryOrCreate | Only with gpuApi: 'dra': the DRA registration and driver sockets, and the driver's prepared.json |
The kubelet's kubeconfig: /etc/kubernetes/kubelet.conf (kubeadm, the default), /var/lib/rancher/k3s/agent/kubelet.kubeconfig (k3s) or /var/lib/rancher/rke2/agent/kubelet.kubeconfig (rke2) | read-only | File | Mounted where bcd reads it, /etc/kubernetes/kubelet.conf (see API access) |
The kubelet's certificate directories: /var/lib/kubelet/pki (kubeadm), /var/lib/rancher/k3s/agent (k3s) or /var/lib/rancher/rke2/agent (rke2) | read-only | Directory | Each at its own path, so the paths inside the kubeconfig resolve |
The container runtime's configuration, default /etc/containerd/config.toml | read-only | File or Directory | bcd reads which NVIDIA runtime the container runtime runs (see GPU isolation) |
The NVIDIA toolkit's configuration, default /usr/local/nvidia/toolkit/.config | read-only | Directory | bcd reads whether the toolkit accepts NVIDIA_VISIBLE_DEVICES from unprivileged pods |
Besides these, the pod has the ConfigMap's config.toml, a memory-backed emptyDir at /run/bcd and an emptyDir
at /tmp. The container runtime's and toolkit's configuration are never created: a missing path stays missing, so the
pod does not start on a node that lacks it rather than seeing an empty directory, and hosted bcd treats a runtime
configuration it cannot read as ForeignGpuExposure. The kubelet credentials refuse a certificate directory that holds
more than the kubelet's own: /etc, /etc/kubernetes, /etc/kubernetes/pki, /var/lib/kubelet or anything under it
but pki, /var/lib, /var and /. Which paths these are is set with the options in
generate the manifests from TypeScript, or with --kubelet-credentials,
--container-runtime and --nvidia-toolkit.
Deploy it
Since 3341e50 there is one path: the manifests are rendered for your deployment by the CLI, with
@better-compute/provider-kubernetes's bcdDaemonSetManifests, so they cannot drift from what bcd and the control
plane expect. The hand-kept manifests that deploy/kubernetes used to carry are gone; its README walks the same steps.
Build the image
daemon/Containerfile builds the cgo-free host build (-tags nonvml; GPUs through nvidia-smi) on
debian:bookworm-slim with ca-certificates, nftables, kmod, pciutils and util-linux. Build and push it,
and note its digest:
docker build -f daemon/Containerfile -t registry.example.com/better-compute/bcd:0.1.0 \
--build-arg VERSION=0.1.0 daemon
docker push registry.example.com/better-compute/bcd:0.1.0RELEASE_SEQUENCE is a second build argument (default 0). No published image exists; build your own. Since 9bdd43a
the image also carries kubectl v1.34.1 at /usr/local/bin/kubectl, which bcd runs for its Node's labels, for
deleting pods of released devices and for DRA. The build downloads it from dl.k8s.io for TARGETARCH (default
amd64; arm64 too) and checks it against the pinned SHA-256 of that release; a mismatch, or another architecture,
fails the build. bcd runs it as the kubelet (see API access). CI builds this image for linux/amd64 on
every change to daemon/**, proto/** or packages/provider-kubernetes/** (the container job, ac1ca4b), but
publishes nothing.
Add the command
The CLI never imports providers, so the application adds kubernetes manifest (a2eb28f) to its configuration:
import { kubernetesManifestCommand } from '@better-compute/provider-kubernetes';
export const commands = [kubernetesManifestCommand()];An application may pass kubernetesManifestCommand({ pins }) with the trust anchors from its own PKI bundle
(readPkiPins of @better-compute/pki); otherwise the pins are flags.
Render the manifests and the join token
better-compute kubernetes manifest --tenant root --pool pool_01k… \
--image registry.example.com/better-compute/bcd@sha256:<64 hex digits> \
--serving-ca-pin sha256/… --join-signing-pin sha256/… --join-encryption-pin sha256/… \
--max-uses 8 --ttl 1h \
--secret-out bcd-join-secret.json --out bcd.jsonThe three pins are trust anchors, and they come only from you (d2cd813): the three flags together (all or none),
from better-compute pki show on the control-plane host, or the application's pins. A run without them is refused
before any call. The command never pins what the network says: from the control plane's /.well-known/better-compute
(at --control-plane, which must be https, else the configured deployment in process) it takes only data that
carries no trust, the gateway's URL and name and the join key, and it refuses a join key that does not match
--join-encryption-pin before a token is minted.
It renders the manifests first, so a bad image or pin fails before any token exists: a Namespace (bcd-system,
labelled pod-security.kubernetes.io/enforce: privileged), a ConfigMap with config.toml, and the DaemonSet.
Then it mints a multi-use join token with hosted: 'kubernetes' and writes it only as the join Secret's manifest,
to --secret-out, a new file created 0600, never the output. The call is decided as compute:join-tokens:create.
After writing the Secret, the command reminds you (ab9339b) to revoke the token (join-tokens revoke <id>) and
delete the Secret once every node has joined, with a fresh token for a later batch of nodes, and prints the
GPU isolation requirements.
| Flag | Default | |
|---|---|---|
--tenant | BETTER_COMPUTE_TENANT | The tenant that owns the pool (required) |
--pool | required | The pool the cluster's nodes join |
--image | required | The bcd image, pinned by digest (registry/repo[:tag]@sha256:<64 hex>) |
--serving-ca-pin, --join-signing-pin, --join-encryption-pin | BCD_SERVING_CA_PIN, BCD_JOIN_SIGNING_PIN, BCD_JOIN_ENCRYPTION_PIN | The gateway serving CA's and the JoinService keys' sha256/… pins, from the PKI bundle; all three, unless the application passes pins |
--secret-out | required | A new file (0600) for the join token Secret manifest |
--out | the output | A new file for the manifests |
--control-plane | the configuration | The API's public https URL, where the well-known document is served: the origin for better-compute serve, or the origin and the API's base path when compute is mounted inside an application |
--namespace, --name | bcd-system, bcd | |
--node-selector | every node | key=value pairs, such as nvidia.com/gpu.present=true |
--host-units | off | Let bcd apply NodeSpec.host units |
--gpu-api | device-plugin | dra for the DRA driver (see GPUs for pods) |
--container-runtime | containerd | containerd (/etc/containerd/config.toml), k3s (/var/lib/rancher/k3s/agent/etc/containerd/config.toml), rke2 (/var/lib/rancher/rke2/agent/etc/containerd/config.toml) or cri-o (/etc/crio/crio.conf and /etc/crio/crio.conf.d): the nodes' container runtime, whose configuration is mounted read-only for the GPU isolation check (containerRuntime; 3775f46) |
--kubelet-credentials | kubeadm | kubeadm (/etc/kubernetes/kubelet.conf and /var/lib/kubelet/pki), k3s (/var/lib/rancher/k3s/agent/kubelet.kubeconfig and /var/lib/rancher/k3s/agent) or rke2 (the same under /var/lib/rancher/rke2/agent): the kubelet's kubeconfig and client certificates, mounted read-only, which bcd acts with (kubeletCredentials; 3d6f79a, a96b238) |
--nvidia-toolkit | gpu-operator | gpu-operator (/usr/local/nvidia/toolkit/.config) or packages (/etc/nvidia-container-runtime): where the NVIDIA container toolkit's configuration lives, mounted read-only for the same check (nvidiaToolkit; 3775f46) |
--nvidia-runtime-class | nvidia | The RuntimeClass bcd's own container runs under, which gives it nvidia-smi and the driver's libraries (nvidiaRuntimeClass; ab9339b); none for no RuntimeClass, when the image carries them |
--ttl | 24h | How long the token admits nodes, 1 minute to 24 hours |
--max-uses | 100 | How many nodes it admits |
--key | a new UUID | The token's idempotency key, to retry the same request |
Distribution, runtime and toolkit. Pick --container-runtime and --kubelet-credentials for the distribution the
nodes run (k3s or rke2 for those; cri-o for CRI-O, else containerd, with kubeadm credentials), and
--nvidia-toolkit packages when the toolkit comes from NVIDIA's distribution packages rather than the GPU operator. Every path must exist on every selected node: the mounts are never
created, so a wrong choice leaves the pod unable to start, and hosted bcd withdraws every GPU when it cannot read the
runtime's configuration. Other paths (containerd's conf.d, another toolkit.installDir) need
bcdDaemonSetManifests with { files, dirs }, and so do kubelet credentials outside the three presets
(kubeletCredentials: { kubeconfig, certificateDirs }).
Token hygiene (d6f8549). A DaemonSet cannot use one single-use token per node, so the token is multi-use, and
anyone who reads the Secret while it is live can join a machine to the pool. Give it --max-uses equal to the node
count and a short --ttl (an hour covers a rollout).
Apply
Configure the GPU stack as GPU isolation requires, then apply the Secret first and the manifests after:
kubectl apply -f bcd-join-secret.json
kubectl apply -f bcd.jsonOn each node the join init container runs bcd join with the token Secret mounted read-only (mode 0400), as root
with no capabilities, no privilege escalation and a read-only root filesystem; on a node that joined before it
succeeds without spending a use. bcd join --token-file refuses a token file that group or others can read, one
that is not a regular file (a Secret volume's symlink into ..data is followed) and one over 4 KiB (c29bb16). The
bcd container then runs bcd node, and never sees the token. The nodes appear in
nodes.list as adopting, as any joined machine does (see
joining nodes).
Revoke the token and delete the Secret
Once every node has joined, revoke the token (joinTokens.revoke) and delete
the Secret:
kubectl -n bcd-system delete secret <name>The pods keep running and restarting without it, since a joined node keeps its identity in /var/lib/bcd. Since
27f2c1b the join-token Secret volume is optional: true, so a pod of a joined node that restarts or is rescheduled
after the Secret is gone starts instead of waiting in ContainerCreating: its bcd join finds the identity, exits 0
without a token file and spends nothing. For nodes added later, mint a new token and Secret, and remove both again.
Trust
The cluster's admins have root on its machines: anyone who may run a privileged pod there can read what bcd holds.
So a node that joins with a hosted token lands tenant-controlled, with a node-tenant certificate that never
receives platform secrets, unless the pool's policy.kubernetes.platformOperated is true, which says the platform
itself operates the cluster (a27156a). Since 259d82c that flag counts only on a pool the platform owns: the pool's
owner must be the owner of its site, the site must not be custom, and, when the deployment names a root tenant, the
owner must be it. A tenant's pool on a platform site cannot vouch for its own cluster by editing its policy. The token
decides this, never what the node claims, and it only ever lowers trust: a node on a tenant's own custom site stays
tenant-owned, and a reclaim through a hosted token lowers an existing platform node the same way. Replaying
joinTokens.create with the same key and another hosted value is CONFLICT.
A tenant-controlled node adds no capacity: placement excludes it, as for a
dedicated-root lease. For the platform's own clusters, give the pool
policy.kubernetes: { platformOperated: true }, with none of apiServer, caCertSha256 and joinTokenRef: those
three together mean the machines should join a cluster through the k8s-join unit, and a partial set is
INVALID_INPUT (7db110a).
Host paths and Pod Security
bcd keeps its node key and journal in the hostPath /var/lib/bcd (files 0600, owned by root). Any pod allowed to
mount host paths as root can read them, and with the key it can act as the node. So restrict host paths for
everything else (d6f8549): label workload namespaces with Pod Security Admission baseline or restricted, which
both forbid hostPath and privileged pods. Only bcd-system, which the manifests label
pod-security.kubernetes.io/enforce: privileged, and your platform's own system namespaces should run privileged.
bcd's pod runs without hostPID and with no service account token.
API access
bcd reaches the Kubernetes API only by running the image's pinned kubectl as the kubelet: every call passes
--kubeconfig /etc/kubernetes/kubelet.conf. Since 27f2c1b the DaemonSet mounts only that file there (read-only,
hostPath type File, from the kubelet's own kubeconfig, which --kubelet-credentials or kubeletCredentials names),
and the kubelet's client-certificate directories read-only at their own paths (default /var/lib/kubelet/pki), never the host's
whole /etc/kubernetes (see host mounts). The pod has automountServiceAccountToken: false
and the manifests create no RBAC. That is deliberate (e7d0765, which reverted 9bdd43a's service-account credentials):
RBAC cannot be scoped to one node, so a service account would have given every privileged bcd pod cluster-wide
patch on Nodes and delete on pods, while the NodeRestriction admission plugin confines the kubelet's identity, and
so bcd, to its own Node and that Node's pods. What bcd does with it:
| Work | kubectl |
|---|---|
Its Node's bcd.dev/* labels | get node, label node --overwrite |
| The reaper | delete pod --wait=false --grace-period=10, forced a minute later |
DRA (gpu_api = "dra") | api-versions, get resourceclaim, apply --server-side --field-manager=bcd and delete of its ResourceSlice |
| Other GPU DRA drivers (GPU isolation) | get resourceslices --field-selector spec.nodeName=<node>, at most once a minute (f88bcdb) |
GPU isolation
bcd hands a pod exactly the GPUs its reservation holds, and it can enforce that only when nothing else hands out GPUs
on the node (d6f8549). With the NVIDIA GPU operator its defaults are unsafe here: its own device plugin advertises
every GPU, and its container runtime gives an unprivileged pod any GPU it names in NVIDIA_VISIBLE_DEVICES, so any
pod could take every GPU. Required:
| Setting | Why |
|---|---|
devicePlugin.enabled=false in the GPU operator | bcd's device plugins (or its DRA driver) are the only source of GPUs for the kubelet |
accept-nvidia-visible-devices-envvar-when-unprivileged = false in the NVIDIA container toolkit's config.toml (the operator's toolkit env ACCEPT_NVIDIA_VISIBLE_DEVICES_ENVVAR_WHEN_UNPRIVILEGED=false) | Devices arrive by CDI only |
CDI specs generated with --device-name-strategy=uuid | bcd's device plugin then answers nvidia.com/gpu=<uuid>; without that spec it answers bcd.dev/gpu=<uuid> plus NVIDIA_VISIBLE_DEVICES, which is safe only with the setting above |
The generated DaemonSet carries these requirements, and the Pod Security and token rules above, in its
bcd.dev/requires annotation (gpuIsolationRequirements), since JSON manifests have no comments.
bcd enforces the first two itself (9f860ad, widened in f88bcdb). It offers no GPUs while any of these holds: its
device plugins stop, its DRA slice is deleted, and it reports the condition ForeignGpuExposure naming what to close.
- Another device plugin. The kubelet can also allocate an
nvidia.com/*resource (pod-resources' allocatable resources). - Another GPU DRA driver. The node's ResourceSlices, read with the kubelet's credentials at most once a minute,
show a driver other than
gpu.bcd.devwhose name marks it as a GPU driver (for examplegpu.nvidia.com) offering devices. An API server withoutresource.k8s.io, or one that refuses the list, is no evidence either way. - The toolkit the container runtime runs. bcd reads the container runtime's configuration (containerd's
/etc/containerd/config.toml, k3s's and RKE2's under/var/lib/rancher/<k3s|rke2>/agent/etc/containerd/, CRI-O's/etc/crio/crio.conf, and the.tomland.conffiles of/etc/containerd/conf.d,/etc/containerd/config.dand/etc/crio/crio.conf.d) for aBinaryNameorruntime_paththat runsnvidia-container-runtime(the.cdivariant, which takes CDI names only, excepted). For each, it derives the configuration that binary reads: the GPU operator's<installDir>/toolkit/.config/nvidia-container-runtime/config.tomlwhen the binary sits in atoolkitdirectory, else/etc/nvidia-container-runtime/config.toml. That file must exist and setaccept-nvidia-visible-devices-envvar-when-unprivileged = false: missing or unreadable, the toolkit takes its default and acceptsNVIDIA_VISIBLE_DEVICESfrom any pod, so that is exposure too. - Any known toolkit configuration.
/etc/nvidia-container-runtime/config.tomlor the operator's/usr/local/nvidia/toolkit/.config/nvidia-container-runtime/config.toml, when present, accepts the variable (the keytrue, or absent, the toolkit's default). - Hosted, no readable runtime configuration. On a DaemonSet node bcd cannot rule the toolkit out when it can read
none of the container runtime's configuration files, so it counts that as exposure. That is why the manifests mount
them read-only as
FileorDirectoryand neverDirectoryOrCreate, which would make a missing directory look empty (d11810c; see host mounts).
The gateway records the condition as a node condition (b91dcff). Since ebd9806 it is NoSchedule while true (advisory
only on a tenant-controlled node), and the node's active reservations turn Degraded with reason ForeignGpuExposure
(4570dfe), since their GPUs are no longer isolated. So a cluster with the GPU operator's defaults gets no GPUs
through bcd, and its nodes take no new work, until you apply the settings above.
bcd doctor's gpu-isolation check (35534e5) runs the same checks. Since f88bcdb it also runs on a DaemonSet node
([host] units = false): it fails when no container runtime configuration is readable and when a toolkit
configuration lets unprivileged pods pick GPUs. On a node bcd joined with k8s-join it also fails on another GPU DRA
driver in the node's ResourceSlices while the kubelet is up.
GPUs for pods
With [host] units = false the node is a worker because the cluster runs bcd there, not because k8s-join joined
it, so bcd's Kubernetes side runs in hosted mode (1e5f96b). It needs no k8s-join record: the joined-mode files in
/var/lib/bcd-hostconfig are not read here.
- Device plugins. bcd runs its kubelet device plugins whenever
/var/lib/kubelet/device-plugins/kubelet.sockexists, advertising the devices of the node'sbinding: 'kubernetes'holds asbcd.dev/gpu,bcd.dev/gpu.migandbcd.dev/gpu.slot, as on a joined worker (see the device plugin). - Labels. bcd keeps its Node's
bcd.dev/*labels itself (bcd.dev/node-id,bcd.dev/site,bcd.dev/gpu.count,bcd.dev/gpu.product,bcd.dev/gpu.driver), naming the Node fromNODE_NAME(the DaemonSet'sspec.nodeNamefield reference) and using the kubelet's kubeconfig,/etc/kubernetes/kubelet.conf(see API access). It updates them at most once a minute, touches onlybcd.dev/*, sets no taints, and removesbcd.dev/*labels it no longer sets. - The reaper. Pods that keep a released device are deleted after 30 seconds with a 10-second grace period, and
forced a minute later if they still hold it, as on a joined worker, DaemonSet pods included (8bcbe92), through the
kubelet's pod-resources socket in the read-only
/var/lib/kubelet/pod-resourcesmount; without that socket bcd reportsPodResourcesUnavailable, which is NoSchedule for the whole node while true (ebd9806; advisory on a tenant-controlled node). A released allocation is reported to the control plane only once nothing holds its devices (cd5fc82, 8bcbe92), and one still held after 5 minutes raisesKubernetesReleaseStuck, also NoSchedule while true (see release). A hosted node always counts as a worker there, so a kubelet bcd cannot ask means the devices stay in use. The process backstop a joined worker has (a pod process with a released GPU's device node open) is off here: withouthostPIDbcd's/procis its own pod's, where its ownnvidia-smiwould look like a holder (f88bcdb). A hosted node relies on pod-resources and, with DRA, the driver'sprepared.jsonrecord in the/var/lib/kubelet/plugins/gpu.bcd.devmount. The same holds for clearing an unreadablereleasing.json, which keeps every GPU claimed and raisesKubernetesReleasesUnreadable(NoSchedule while true, 4884a65) until no pod may hold a GPU of an allocation the node does not know (a18f423, b01130f; see an unreadable releases file). - Device plugin or DRA.
[kubernetes] gpu_apiinconfig.tomlchooses how the GPUs reach pods:"device-plugin"(the default) or"dra", bcd's DRA drivergpu.bcd.dev(7d24836), never both. Any other value stops bcd from starting.--gpu-apiandbcdDaemonSetManifests({ gpuApi })write it (e4b0c01). See device plugin or DRA for what the driver publishes.
All of these run the kubectl the image carries, as the kubelet (see API access).
Generate the manifests from TypeScript
kubernetes manifest calls bcdDaemonSetManifests (2c9f953). An application can call it itself; it is not a VPS
provider and creates no machines, it produces manifests you apply, and it mints no token.
import { writeFileSync } from 'node:fs';
import { bcdDaemonSetManifests, renderKubernetesManifests } from '@better-compute/provider-kubernetes';
const manifests = bcdDaemonSetManifests({
image: 'registry.example.com/better-compute/bcd@sha256:<64 hex digits>',
gatewayUrl: 'https://gw.eu-central.example.com',
siteId: 'site_01k…',
pins: { servingCaPin, joinSigningPin, joinEncryptionPin }, // from your PKI bundle
controlPlaneUrl: 'https://compute.example.com', // where /.well-known/better-compute is served, or joinEncryptionJwk
joinTokenSecret: { name: 'bcd-join' },
nodeSelector: { 'nvidia.com/gpu.present': 'true' },
});
writeFileSync('bcd.json', renderKubernetesManifests(manifests)); // kubectl apply -f bcd.jsonbcdDaemonSetManifests returns a Namespace, a ConfigMap (config.toml, and the pinned JoinService key when you pass
joinEncryptionJwk) and a DaemonSet; renderKubernetesManifests turns them into one v1 List for
kubectl apply -f. It refuses (INVALID_CONFIG) an image not pinned by digest, a gateway or control-plane URL that
is not https, malformed pins, names or resource quantities, a gpuApi other than device-plugin or dra, a JWK
with a private part, and host paths that are not absolute or that host mounts rules out.
| Option | Default | |
|---|---|---|
image | required | registry/repo[:tag]@sha256:<64 hex> |
gatewayUrl, siteId, pins | required | As the pool's site and the PKI bundle give them |
joinEncryptionJwk or controlPlaneUrl | one required | The JoinService encryption key, or where bcd join --server fetches it; pinned either way |
joinTokenSecret | required | { name, key = 'token' }, a Secret with a hosted: 'kubernetes' token |
namespace, name, createNamespace | bcd-system, bcd, true | |
nodeSelector, tolerations | every node, every taint | |
priorityClassName | system-node-critical | null for none |
imagePullSecrets, resources | none; requests 100m and 128Mi, limit 512Mi | |
hostUnits | false | true writes no [host] units = false, so the pool's host units apply |
gpuApi | none (bcd's default, device-plugin) | 'dra' writes [kubernetes] gpu_api = "dra" (e4b0c01) |
nvidiaRuntimeClass | nvidia | The RuntimeClass that gives bcd's container the driver's user space; null when the image carries it (b6d5548) |
kubeletCredentials | 'kubeadm': /etc/kubernetes/kubelet.conf and /var/lib/kubelet/pki | A preset (3d6f79a; kubeletCredentialPresets): 'k3s' (/var/lib/rancher/k3s/agent/kubelet.kubeconfig, /var/lib/rancher/k3s/agent) or 'rke2' (the same under /var/lib/rancher/rke2/agent), or { kubeconfig, certificateDirs } with 1 to 4 directories. The kubeconfig is mounted read-only at /etc/kubernetes/kubelet.conf, each directory at its own path; a directory holding more than the kubelet's certificates, or an unknown preset, is refused (27f2c1b) |
containerRuntime | 'containerd' | 'k3s', 'rke2', 'cri-o', or { files, dirs } (1 to 8 absolute paths in all): the runtime configuration mounted read-only (containerRuntimeConfigs; d11810c) |
nvidiaToolkit | 'gpu-operator' | 'packages', or { files, dirs } for another toolkit.installDir: the toolkit configuration mounted read-only (nvidiaToolkitConfigs; d11810c). A path both options name is refused |
gatewayServerName, logLevel | none, info |
The pod's bcd container gets NODE_NAME and has bcd status as its liveness probe, and the pod rolls when
config.toml changes (annotation bcd.dev/config-sha256). Both sides check the contract:
scripts/daemon-k8s-contract.mjs writes the generated pod to daemon/cmd/bcd/testdata/k8s-daemonset.json, and bcd's
TestKubernetesDaemonSetContract parses its config.toml (the gpu_api key included) and command lines with bcd's
own configuration parser and flag sets.
Upgrades
Render the manifests again with the new image digest and apply them. The update strategy replaces one pod at a time.
The identity and the journal are on the host, so a new pod resumes where the old one stopped. Rollouts and
daemon.upgrade do not apply to these nodes, since bcd takes no self-update without release pins.
Limits
- No VPS instances. The image carries neither
virshnorcloud-hypervisor, so these nodes run no instance runtime. - No reboots.
node.rebootrunssystemctl reboot, which the image does not have. - The pod sees its own filesystem. Only the mounted host paths are the host's; that is why the host converger is off by default.
- Host resources are the kubelet's. A
binding: 'kubernetes'hold gets no cgroup slice: the kubelet enforces its pods' requests and limits (e01d7ff). See Kubernetes workers.
Better Compute is created by Sean Filimon
Last updated
Host configurationexperimental
The config units bcd converges from NodeSpec.host, in order, with their settings, files, refusals and reboot cases, and how bcd helper applies them as root.
Deliveryexperimental
How NodeSpec generations and one-shot commands reach bcd, the acceptance checks, lanes and credits, cancellation, signatures, and the journal that makes delivery safe.