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:

  1. global is for cross-NF values only. PLMN, image registry, region. If only one NF reads it, it does not belong in global.
  2. NF-specific values live under the NF key, mirroring the sub-chart name. Helm will pass nrf: to the nrf sub-chart automatically.
  3. Never repeat values. If you find yourself writing mcc: "310" in five places, move it to global.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:

  1. NRF up first.
  2. UDR before UDM.
  3. UDM before AUSF.
  4. 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

DataStorageWhy
NF YAML configConfigMap, mounted as fileOperator-edited, version-controlled
TLS certs, OAuth client secretsSecret, mounted as fileSensitive, may rotate
Subscriber keys (UDR)External KMS or sealed-secretsNever in plain values
PLMN, AMF ID, regionHelm values -> ConfigMapConfig-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.