Building a Telco Test Lab Using srsRAN – #8 Kubernetes Deployment (Part 1)

This chapter walks through how to deploy a complete RAN setup on a Kubernetes cluster using the srsRAN gNB together with Open5GS as the core network. I am using a single-node cluster on the machine I optimized in earlier chapters. I assume that all earlier chapters have been completed successfully, because I rely on the configuration and files created there.

srsRAN Helm Charts

In this post I use Helm charts to deploy workloads on a Kubernetes cluster. Make sure you have access to your cluster and Helm installed and working.

For the most recent updates clone the srsRAN Helm chart repository directly from GitHub:

git clone https://github.com/srsran/srsRAN_Project_helm

k9s

k9s is a terminal UI for Kubernetes that helps me a lot with quick debugging and navigating logs. Install instructions: https://k9scli.io/topics/install/

Preparing the Kubernetes Cluster

As a first step, I show how I set up Kubernetes nodes for real-time workloads such as the srsRAN gNB. I assume you already have a Kubernetes cluster (v1.26 or higher) and a basic understanding of the environment.

Configure the CPU Manager (Static Policy)

For real-time workloads like the srsRAN gNB, it is important that the scheduler places Pods only on isolated CPU cores to avoid jitter. To achieve this, I configure the kubelet to use the static CPU Manager policy.

With this configuration, all Pods running with the Guaranteed QoS class are placed on isolated CPU cores. In order for the scheduler to distinguish between isolated cores and housekeeping cores, we must explicitly define which CPUs are reserved for system tasks.

In this example, I reserve cores 0 and 12 for the system. Core 0 is the physical core, and core 12 is its corresponding hyperthreading sibling. I also enable full-pcpus-only because I do not want the gNB to share CPU resources with any other process. This option ensures that physical CPU cores are always allocated together with their hyperthreading siblings.

The downside of enabling this option is that only an even number of CPU cores can be allocated to Pods running with the Guaranteed QoS class on this system.

If hyperthreading is disabled, I sometimes run into issues when reserving a single CPU using reservedSystemCPUs. As a workaround, I specify the same core twice, for example reservedSystemCPUs: 0,0.

An example kubelet configuration enabling the static CPU manager:

cpuManagerPolicy: static
reservedSystemCPUs: 0,12
featureGates:
  CPUManagerPolicyOptions: true
cpuManagerPolicyOptions:
  full-pcpus-only: "true"

What these parameters do

  • cpuManagerPolicy: static
    Enables the static CPU Manager policy. QoS Guaranteed pods receive full, isolated CPU cores.
  • reservedSystemCPUs: 0,12
    These cores are reserved for system tasks only. No pods run there.
  • featureGates
    Required to enable the CPU manager functions like full-pcpus-only.
  • full-pcpus-only: "true"
    Forces Kubernetes to assign full physical CPUs including hyperthreading siblings if enabled.

How to update the kubelet config

  1. Drain the node
kubectl drain <node> --ignore-daemonsets
  1. Remove the CPU Manager state file
sudo rm -rf /var/lib/kubelet/cpu_manager_state
  1. Edit the kubelet config
    Add the CPU manager snippet to the bottom of:
/etc/kubernetes/kubelet-config.yaml
  1. Restart kubelet
sudo systemctl restart kubelet
  1. Uncordon the node
kubectl uncordon <node>

(Optional) SR-IOV Network Device Plugin

If you don’t want to give gNB pods full root access to network devices on the host, you can use the SR-IOV Network Device Plugin.

This plugin enables you to allocate Virtual Functions (VFs) or physical interfaces on the fly. This way you dont have to grant privileged access to the host. The srsRAN Helm chart supports dynamically updating the gNB config file to use the dynamically allocated network interfaces.

In the following section I will show how to configure a VF of a E810 bound to vfio-pci.

Configuration

All configuration files are packed here:

You can also find all of these files on the official Github repo.

The SR-IOV plugin creates device plugin endpoints based on the configMap.yaml definition. You need to specify the vendor and device ID.

Example VF:

lspci -nn -s 01:01.0
01:01.0 Ethernet controller [0200]: Intel Corporation Ethernet Adaptive Virtual Function [8086:1889] (rev 02)

Vendor = 8086
Device = 1889

Create the ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: sriovdp-config
  namespace: kube-system
data:
  config.json: |
    {
      "resourceList": [
        {
          "resourceName": "intel_sriov_dpdk",
          "resourcePrefix": "intel.com",
          "selectors": [
            {
              "vendors": ["8086"],
              "devices": ["1889"],
              "drivers": ["vfio-pci"],
              "needVhostNet": true
            }
          ]
        }
      ]
    }

This groups all Intel E810 VFs that are bound to vfio_pci into the resource intel.com/intel_sriov_dpdk. If you have used a different driver in Chapter 3 you want to use that driver here.

The SRIOV DaemonSets can be restricted using nodeAffinity if needed. In my example configs, they are pinned to nodes with hostname srskit1. Remove the affinity if you are using a single node cluster or adjust it to your node’s name.

Deployment

Multus is required for the SR-IOV provider to work. I’ve included a manifest for it in the example config package, which you can apply directly. If you prefer using a newer release, check the SR-IOV project’s GitHub page for the latest installation instructions.

kubectl apply -f multus-daemonset-thick.yml

After that we can deploy the remaining manifests for the SRIOV provider.

kubectl apply -f configMap.yaml
kubectl apply -f sriov-cni-daemonset.yaml
kubectl apply -f sriov-crd.yaml
kubectl apply -f sriovdp-daemonset.yaml

Infrastructure Pods (TuneD & LinuxPTP)

At SRS we created two Helm charts that help manage system-level configuration as code. They are inspired by the OpenShift linuxptp and tuned operators but designed to work on any Kubernetes cluster without requiring OpenShift. These charts make it possible to version-control system tuning and time-sync settings and redeploy them reliably. If a node fails, you can reapply the same charts and restore most of your environment once its replaced.

Below I show how I use the configuration from earlier chapters (TuneD from Chapter 1 and LinuxPTP from Chapter 5).

TuneD

The TuneD Helm chart deploys a TuneD profile onto the node and restarts the TuneD service automatically when the profile content changes.

For the configuration of the TuneD chart, I copy the profile I created in Chapter 1 and place it under the profileContent section inside the chart’s values.yaml. Make sure to overwrite the example configuration with your own.

Important parameters

  • hostPathTuned
    Path on the host where TuneD profiles live.
  • securityContext
    Must be set to privileged so the Helm chart can restart the server.
  • restartOnConfigChange
    If enabled, nodes reboot when the profile changes.
  • reboot
    Defines how and when the reboot is executed.

Example deployment command

helm install tuned-srskit1 ./ -n infra -f ./values.yaml --create-namespace

(Replace ./ with the folder of the TuneD chart from the cloned srsRAN Helm repo.)

LinuxPTP

The LinuxPTP Helm chart deploys ptp4l, phc2sys, and optionally ts2phc inside Kubernetes instead of running them directly on the host. This allows you to keep all timing-related configuration in a Git-driven IaC workflow. The chart also includes liveness/readiness probes that check PTP health by parsing logs in real time.

I reuse the same configuration I created in Chapter 5. From the example config provided in the values.yaml of the chart, I remove everything I don’t use and set:

ts2phc:
  enabled: false

Key chart features explained

  • ntp
    Optionally resets the NIC and the system’s PHC to NTP time on container restart (helpful for LLS-C1 without GPS).
  • Probes (liveness, readiness, startup)
    Validate Pod health automatically by monitoring logs.
  • interfaceNameList
    Allows defining multiple interfaces for multi-cell LLS-C1 setups, or a single interface otherwise.
  • securityContext
    Must run privileged due to PHC access requirements.
  • config
    Your ptp4l and phc2sys configuration copied from Chapter 5.

Example deployment command

helm install linuxptp-srskit1 ./ -n infra -f ./values.yaml --create-namespace

RT Tests

To verify a deployment, we at SRS have created a Helm chart that runs cyclictest together with stress-ng.
cyclictest measures thread wake-up latency and gives a clear indication of how deterministic the system behaves under load. The maximum latency a node can tolerate depends on the SCS used in the deployment:

  • 60 kHz SCS → ~17.8 µs per symbol
  • 15 kHz SCS → ~71.4 µs per symbol
  • 30 kHz SCS → ~35.7 µs per symbol

For cyclictest, the important thing is that the maximum measured wake-up time stays below the symbol duration for the SCS you are using.

Configuration

You can use the default values file included in the RT Tests chart. Adjust the values according to your CPU configuration. Below is a short explanation of the defaults:

config:
  rt_tests.yml: |-
    stress-ng: "--cpu 16 --iomix 4 --vm 2 --vm-bytes 128M --fork 4 --timeout 12h"
    cyclictest: "--mlockall --priority 95 --distance 0 --threads 16 --histogram 25 --quiet --duration 12h"

Explanation of the parameters

stress-ng:

  • --cpu 16 runs 16 CPU stress workers
  • --iomix 4 adds mixed I/O load
  • --vm 2 --vm-bytes 128M creates two memory-stress workers allocating 128 MB each
  • --fork 4 stresses process creation
  • --timeout 12h keeps the stress load running for 12 hours

cyclictest:

  • --mlockall locks memory to avoid page faults
  • --priority 95 assigns a high RT priority to the threads
  • --distance 0 wakes all threads at the same time
  • --threads 16 launches 16 test threads
  • --histogram 25 collects latency histogram data
  • --quiet suppresses detailed output
  • --duration 12h runs cyclictest for 12 hours

Running the test

Deploy the chart using:

helm install rt-test ./ -f values.yaml

The test results will be written to the output directory you configured (default: /var/lib/rt-tests). Inside that directory, you will find a diagram similar to the one shown in this post, summarizing the wake-up latency distribution.

Example configs for all 3 chats:

Summary

In this chapter we prepared the Kubernetes environment for running real-time RAN workloads. We cloned the srsRAN Helm repository, set up the tuning and timing infrastructure using TuneD and LinuxPTP Helm charts, and created a repeatable IaC setup. These components ensure that every node in the cluster runs with consistent CPU isolation, PTP timing, and system tuning, all are critical for stable srsRAN gNB performance.

In the next chapter, I’ll continue by deploying the srsRAN gNB and integrating Open5GS using Helm. At the end of the next post you will have your whole RAN running inside of K8s! Stay tuned!

Comments
Join the Discussion and Share Your Opinion
Add a Comment

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert