install/helm/fermihdi
The chart the installer deploys to Kubernetes. install/fermihdi-k8s renders its
values from the same cluster.yaml everything else in install/ reads, so a
Kubernetes install and a bare-metal one are described by one file:
install/fermihdi-k8s cluster.yaml -o values.yaml
helm install fermihdi install/helm/fermihdi -f values.yaml
It began as docs/examples/k8s_example,
which is still there and still a reference. This copy is the one that is kept
current; where the two disagree, this is the one to trust.
What it deploys
| Namespace | Runs |
|---|---|
fermihdi-dms |
DMS-CA, DMS-Core, the OPA engine, the WebUX, the OTEL collector |
fermihdi-sc |
the SCC, and the Storage Nodes as a StatefulSet |
fermihdi-edge |
the Ingester and the HD Proxy |
fermihdi-extra |
hd_mcp, hd_graphql, the data explorer |
Each group is reached from the others through ExternalName Services rather
than by fully-qualified names spread through the templates — which is what makes
a DMS outside the cluster a value rather than an edit:
dms:
externalHost: dms.corp.example.com # already installed, or on bare metal
core: { enabled: false } # ... and nothing here deploys one
What differs from the bare-metal install
A Pod is not a host, and four things do not survive the trip. The generator says so per cluster file rather than leaving it to be discovered:
- DPDK. Every node here runs with
FERMIHDI_FORCE_POSIX=1. A DPDK data path needs SR-IOV or a host-network Pod with the NIC bound to vfio, neither of which this renders. - Devices. Storage Nodes write to a PVC-backed file, sized by
sc.sn.storageFileSize. Nothing passes an NVMe through, so thestorage:entries incluster.yamland their IOMMU groups have nowhere to go. - Cpusets.
cluster.yamlsplits poll cores from overflow cores to keep the container runtime off the reactors. The equivalent here is the static CPU manager policy with integer CPU limits, which is what the generated resources ask for — the node pool has to be configured for it or the request is just a number. - 1 GB hugepages. Requested on bare metal for SPDK's NVMe DMA. Nothing here DMAs, so only the 2 MB pool is asked for.
IPC_LOCK is granted to the SCC, the SNs and the Ingester: the HD engine and DPDK pin
their arenas, and a container's default RLIMIT_MEMLOCK of 64 KB stops them at
startup. hd_mcp and hd_graphql get a medium: Memory emptyDir at /dev/shm
instead — they allocate from shared memory rather than hugepages, and the 64 MiB
a container gets by default is not enough to start with.
Secrets
The chart reads everything from three Secret objects — fermihdi-dms-secrets,
fermihdi-sc-secrets, fermihdi-edge-secrets — so anything that creates them
with the right keys will do: External Secrets, Sealed Secrets, or your own
pipeline. secrets.create=true renders them from values instead, which is what
fermihdi-k8s --secrets writes and which is for development.
Keeping it honest
The services' environments are checked against tests/sandbox_dms/compose.yml,
which is the deployment as it is actually run. When something moves there, it
moves here. The last sweep (2026-08-20) closed these:
OTEL_EXPORTER_OTLP_ENDPOINTand_PROTOCOLwere absent everywhere. They are what the SDK actually exports over;FERMIHDI_OTEL_URLis a routing probe the C++ nodes use to find their own observability address and configures no exporter. Every service looked configured and exported nothing.FERMIHDI_LOG_STDOUTwas absent everywhere, sokubectl logsshowed startup and errors only.LOG_LEVELwas absent on all four DMS services.- DMS-CA was published on 6989 — its container port, its Service, the
NetworkPolicy and
dmsCAURLall agreed with each other and disagreed with the binary, which listens on 6985. Nothing could reach the CA. It is now 6985, andCA_LISTEN_ADDRis set explicitly so the four cannot drift apart again. global.dataPlaneClIDR— misspelled in the values and read by that misspelling in every template, so setting the obvious name did nothing.- The SCC's
FANOUT_MAX_INFLIGHT_BATCHESand the SN's mode, write-path and engine-memory settings had no values at all.
To check it yourself:
helm lint install/helm/fermihdi
helm template hd install/helm/fermihdi -f values.yaml | kubectl apply --dry-run=server -f -
The OPA adapter is a sidecar
dms.opaAdapter.enabled=true adds it to the OPA engine's Pod rather than
deploying it separately, because the handover between the two is a directory:
the adapter polls an OPA control plane that cannot reach into the deployment and
writes bundles into dms.opaEngine.bundleDir; the engine, which reaches nothing
outward, validates and serves what appears there. Two Pods would need a
ReadWriteMany volume for what an emptyDir does inside one.
Three things follow from sharing a Pod, and each is a value rather than a surprise:
- Containers in a Pod share one network namespace, and both binaries default
their certificate-renewal listener to
0.0.0.0:6985and their observability server to8080. The engine keeps the defaults; the adapter takesrenewPortandobsPort. - The adapter registers with DMS-Core in its own right, so it presents its own
bootstrap token —
OPA_ADAPTER_TOKENin the DMS Secret, falling back to the engine's. - It is the one service in the chart that reads its trust bundle only as a
file. There is no PEM environment variable for it, so the CA bundle is
projected out of the Secret to
/etc/fermihdi/certs/fermihdi_root_ca.pem.
Without styraApiUrl and a STYRA_TOKEN it registers and then idles: there is
nothing for it to poll.
Not deployed here
The public front door — the Traefik that terminates TLS for the Proxy and the
WebUX, and the forward-auth service behind it — and hd_ipflow_graphql.
docs/examples/k8s_example is no further ahead on either; they are genuinely
not written yet.