Skip to content

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 the storage: entries in cluster.yaml and their IOMMU groups have nowhere to go.
  • Cpusets. cluster.yaml splits 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_ENDPOINT and _PROTOCOL were absent everywhere. They are what the SDK actually exports over; FERMIHDI_OTEL_URL is 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_STDOUT was absent everywhere, so kubectl logs showed startup and errors only.
  • LOG_LEVEL was absent on all four DMS services.
  • DMS-CA was published on 6989 — its container port, its Service, the NetworkPolicy and dmsCAURL all agreed with each other and disagreed with the binary, which listens on 6985. Nothing could reach the CA. It is now 6985, and CA_LISTEN_ADDR is 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_BATCHES and 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:6985 and their observability server to 8080. The engine keeps the defaults; the adapter takes renewPort and obsPort.
  • The adapter registers with DMS-Core in its own right, so it presents its own bootstrap token — OPA_ADAPTER_TOKEN in 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.