Every 5G core vendor ships Helm charts. Most are bad. Some are very bad. If you are integrating, customizing, or building your own, this is what good looks like.
Chart layout for a 5G core
The pattern that has won out by 2026 is an umbrella chart with one sub-chart per NF:
5gc/
Chart.yaml
values.yaml
values-prod-eu.yaml
values-prod-us.yaml
values-lab.yaml
charts/
nrf/
amf/
smf/
upf/
ausf/
udm/
udr/
pcf/
nssf/
common/ # library chart
templates/
namespace.yaml
network-attachment-definitions.yaml
The common sub-chart is a Helm library chart — no installable resources, just named templates that the other sub-charts include. Things that belong there:
- Standard labels and selectors
- Resource block helpers (CPU/memory with sane defaults)
- Multus annotation builders
- Liveness/readiness probe templates for HTTP/2 SBA endpoints
Without a library chart, you copy-paste the same Multus annotation across nine NFs and they drift.
values.yaml structure
A flat values file is unmanageable past the third NF. Use this structure:
global:
registry: registry.example.com/5gc
imagePullSecrets:
- name: registry-creds
plmn:
mcc: "310"
mnc: "260"
region: eu-west-1
multus:
n2:
networkName: n2-sriov
subnet: 10.20.2.0/24
n3:
networkName: n3-sriov-dpdk
subnet: 10.20.3.0/24
n4:
networkName: n4-macvlan
subnet: 10.20.4.0/24
nrf:
enabled: true
replicaCount: 2
image:
repository: nrf
tag: 2026.04.1
resources:
requests: { cpu: "500m", memory: "1Gi" }
limits: { cpu: "2", memory: "2Gi" }
amf:
enabled: true
replicaCount: 3
guami:
- { plmnId: { mcc: "310", mnc: "260" }, amfId: "cafe00" }
servedTai:
- tac: "000001"
...
Three rules:
globalis for cross-NF values only. PLMN, image registry, region. If only one NF reads it, it does not belong in global.- NF-specific values live under the NF key, mirroring the sub-chart name. Helm will pass
nrf:to thenrfsub-chart automatically. - Never repeat values. If you find yourself writing
mcc: "310"in five places, move it toglobal.plmn.
Multi-environment overlays
One chart, many environments. Do not fork charts per region.
helm install 5gc ./5gc \
-f values.yaml \
-f values-prod-eu.yaml \
--set global.region=eu-west-1
Later overlays override earlier ones. values-prod-eu.yaml should contain only deltas:
global:
region: eu-west-1
amf:
replicaCount: 6
resources:
requests: { cpu: "2", memory: "4Gi" }
upf:
replicaCount: 4
dpdk:
hugepages: 8Gi
cores: [4,5,6,7]
Keep base values.yaml as a working lab config. Production overlays are short.
Init containers for dependency ordering
A 5G core has hard ordering requirements:
- NRF up first.
- UDR before UDM.
- UDM before AUSF.
- AUSF, UDM, UDR registered with NRF before AMF/SMF start serving.
Helm hooks help at install time, but pods can restart anytime. The right pattern is init containers that block until dependencies are ready:
initContainers:
- name: wait-nrf
image: curlimages/curl:8.5.0
command:
- sh
- -c
- |
until curl -sf http://nrf:8000/nnrf-nfm/v1/nf-instances; do
echo "waiting for NRF"; sleep 2;
done
For data plane NFs, the init container often does more — verifying SR-IOV VF availability, checking hugepages, validating DPDK driver binding.
Do not use helm.sh/hook: post-install for runtime dependencies. Hooks run once at install. Init containers run on every pod start, which is what you actually need.
Sub-chart conditional enablement
Not every deployment runs every NF. Lab deployments often skip NSSF. Edge UPF deployments are UPF-only.
In the umbrella Chart.yaml:
dependencies:
- name: nrf
condition: nrf.enabled
- name: amf
condition: amf.enabled
- name: upf
condition: upf.enabled
Then --set amf.enabled=false removes AMF entirely. This is cleaner than commenting out values.
Configuration: ConfigMap vs Secret vs file
| Data | Storage | Why |
|---|---|---|
| NF YAML config | ConfigMap, mounted as file | Operator-edited, version-controlled |
| TLS certs, OAuth client secrets | Secret, mounted as file | Sensitive, may rotate |
| Subscriber keys (UDR) | External KMS or sealed-secrets | Never in plain values |
| PLMN, AMF ID, region | Helm values -> ConfigMap | Config-as-code |
Mount config as files, not env vars, for anything beyond a handful of values. NF binaries reload config on file change far more reliably than they parse env vars.
For secrets, never put real secrets in values.yaml checked into Git. Use Sealed Secrets, External Secrets Operator pulling from Vault, or SOPS-encrypted overlays.
Probes that actually work for SBA NFs
Default Helm probes are HTTP/1.1 GET on /. SBA NFs speak HTTP/2 on the SBI port. Use the right probe:
livenessProbe:
httpGet:
path: /nnrf-nfm/v1/nf-instances
port: 8000
scheme: HTTP
httpHeaders:
- { name: Accept, value: application/json }
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["/usr/local/bin/healthcheck", "--check=nrf-registered"]
periodSeconds: 5
Readiness should reflect NRF registration state, not just process liveness. A pod with the process running but not registered with NRF is not ready to serve.
Versioning discipline
- Chart version bumps when the chart changes (template, values structure).
- App version matches the NF software release.
- Sub-chart versions are pinned in
Chart.yaml, not floating.
When you ship a chart change without bumping the version, ArgoCD will not detect drift and your fleet silently diverges. This has bitten real operators. Bump the version on every change, even cosmetic.
Testing charts before they hit a cluster
helm lint 5gc/
helm template 5gc ./5gc -f values-prod-eu.yaml | kubeval --strict
helm template 5gc ./5gc -f values-prod-eu.yaml | conftest test -
conftest with Rego policies catches things like "every UPF must have hugepages set" or "no NF runs as root" before they ship.
> A good Helm chart for a 5G core is mostly boring. The cleverness should live in the NF, not in the templates.
When your chart looks like everyone else's chart, you have probably written it correctly.