GitOps-Driven 5G Core on Kubernetes
Deploy a 5G Standalone Core on Kubernetes using ArgoCD and the Operator pattern.
Introduction
30 Second Summary
Every time you stream a video or make a video call, a 5G core network is routing your data through specialized network functions. The world's largest telcos now manage these networks entirely through Git repositories, treating infrastructure like code.
In this project, you will deploy a production-style 5G Core network on a local Kubernetes cluster using ArgoCD and the Kubernetes Operator pattern. You will manage network slices declaratively through Git and monitor everything with Prometheus and Grafana.
What You'll Build
A GitOps platform that deploys and manages an open-source 5G Core (Open5GS) on Kubernetes, mirroring the exact architecture KT Cloud uses in production.
Git stores the desired state for this platform. ArgoCD synchronizes that state into Kubernetes, which runs the operator-managed 5G Core alongside its Prometheus and Grafana monitoring stack.
By the end of this project, you'll have:
- A Docker Desktop Kubernetes cluster provisioned with Kind, running a full 5G Standalone Core reconciled by ArgoCD.
- Declarative network slices managed through Kubernetes Custom Resources in Git.
- A Prometheus and Grafana observability pipeline that displays CNF metrics while a Git push triggers the complete ArgoCD reconciliation workflow.
- Secret Mission: Add a simulated subscriber and a second network slice, then build a Grafana dashboard showing per-slice resource allocation.
Are there any prerequisites?
This project assumes familiarity with Kubernetes concepts (pods, namespaces, deployments), Helm basics, and CI/CD fundamentals. A basic understanding of telecom network architecture is helpful but not required.
Before We Start
Before deploying a 5G Core on Kubernetes, you need Docker Desktop running as your local container platform.
In this step, you will confirm Docker Desktop is ready, install the ArgoCD CLI, and create the directory that holds your GitOps files.
In this step, get ready to:
- Confirm Docker Desktop is running and responding to container commands.
- Install the ArgoCD CLI.
- Create your project directory.
Confirm Docker Desktop is running
Docker Desktop provides the container engine that the integrated Kind provisioner uses to create your cluster. Confirm the engine is ready before you continue.
- Check the system tray for the Docker Desktop icon.
- Open the Applications menu if Docker Desktop is not running.
- Select Docker Desktop and wait until the engine reports that it is running.
- Verify Docker Desktop is working by running this command:
docker info
You should see output showing Docker Desktop as the active context, with no errors.
Cannot connect to the Docker daemon?
Make sure Docker Desktop is running. The engine can take up to a minute to start, so wait until Docker Desktop reports that it is ready.
If the connection still fails, restart Docker Desktop and repeat the verification command above.
Help me fix my Docker Desktop connection.
Install the ArgoCD CLI
Docker Desktop handles local cluster creation through its integrated provisioner. The ArgoCD CLI lets you manage GitOps deployments from your terminal.
- Check whether the ArgoCD CLI is installed by running this command:
argocd version --client
✔️ I see a version number
The ArgoCD CLI is ready. Move on to creating your project directory.
ⓧ Command not found
- Install the ArgoCD CLI by running:
curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 && sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd && rm argocd-linux-amd64
- Confirm the installation by running:
argocd version --client
You should see a version number in the output.
Permission denied during install?
The install commands use sudo to move binaries into /usr/local/bin/. Enter your system password when prompted.
If sudo is not available, you can install to a user-local path like ~/.local/bin/ instead, and make sure that directory is in your PATH.
Help me fix permission issues while installing the ArgoCD CLI.
Create the project working directory
You need a dedicated folder to store the Git repository you will build throughout this project.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir telco-gitops
cd telco-gitops
- Confirm the folder exists by running:
ls ~/Desktop | grep telco-gitops
You should see telco-gitops in the output.
Don't see the folder?
Make sure you ran cd ~/Desktop before the mkdir command. Without it, the folder is created in whatever directory your terminal opened to.
help me find or create my project directory on Linux
Your environment is ready. Next up, you will create the Kubernetes cluster and deploy ArgoCD into it.
Install Kind and Create the Telco Cluster
Create the Telco Cluster with Docker Desktop
To deploy a 5G Core through GitOps, you first need a Kubernetes cluster that ArgoCD can manage. Docker Desktop can provision this local cluster with Kind running underneath it.
In this step, you will use Docker Desktop’s integrated Kind provisioner instead of installing the standalone kind CLI. You will use Kubernetes 1.36 with Argo CD 3.5.2, then verify that both the cluster and GitOps platform are healthy.
In this step, get ready to:
- Provision a Kubernetes 1.36 cluster through Docker Desktop’s Kind provisioner.
- Install Argo CD 3.5.2 into the cluster and configure local access through port-forwarding.
- Verify the cluster is healthy and the Argo CD UI is accessible.
Create the cluster in Docker Desktop
Docker Desktop 4.51 and later can create its integrated Kubernetes cluster with the Kind provisioner. Docker Desktop manages Kind for you, so this workflow does not require the standalone kind CLI.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Docker Desktop and press Enter.
- Select the Kubernetes view in the Docker Desktop Dashboard.
- Select Create cluster. If a cluster already exists, select Edit cluster instead.
- Choose Kind as the cluster type.
- Set the cluster to one node.
- Select the latest available Kubernetes 1.36 patch release for Argo CD 3.5 compatibility.
- Select Create.
Cluster provisioning takes about 30 seconds while Docker Desktop creates the Kind node container. A longer first run usually means Docker Desktop is downloading the Kubernetes images.
Why use Docker Desktop's Kind provisioner?
Docker Desktop handles the cluster lifecycle while Kind supplies the container-based Kubernetes nodes. You still get Kind’s faster provisioning and version selector without maintaining another CLI binary.
- Confirm that kubectl points to Docker Desktop by running this command:
kubectl config current-context
You should see docker-desktop.
- Check the node status and Kubernetes version by running this command:
kubectl get nodes -o wide
You should see one node in the Ready state with a Kubernetes 1.36 version.
Kind option missing or cluster not ready?
- Update Docker Desktop to version 4.51 or later if the Kind cluster type is unavailable.
- Confirm Docker Desktop uses the containerd image store because its integrated Kind provisioner does not work with the Docker image store.
- Stop any other local container runtime before retrying the cluster creation.
Help me troubleshoot Docker Desktop's Kind-provisioned Kubernetes cluster.
Install ArgoCD into the cluster
With the cluster running, you can now deploy ArgoCD into it. ArgoCD will manage the entire 5G Core lifecycle from Git.
- Install ArgoCD by running this command:
kubectl create namespace argocd && kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.2/manifests/install.yaml
What does this command do?
- kubectl create namespace argocd creates a dedicated namespace for all Argo CD components.
- kubectl apply with --server-side --force-conflicts is required because some Argo CD CRDs exceed the client-side apply size limit.
- The version-pinned manifest URL installs Argo CD 3.5.2, which is tested with Kubernetes 1.36.
- Wait for all ArgoCD pods to reach Running state by running:
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=120s
- You should see each pod listed as meeting the condition. This may take 60-90 seconds while images download.
- Start the port-forward to access the ArgoCD UI by running:
kubectl port-forward svc/argocd-server -n argocd 8080:443 &
Why the ampersand?
The & at the end runs the port-forward in the background so you can continue using the same terminal. The ArgoCD UI will be accessible at localhost:8080 for the rest of this project.
- Retrieve the ArgoCD admin password by running:
argocd admin initial-password -n argocd
- Copy the password that prints to your terminal. You will use it to log in to the ArgoCD UI with the username admin.
Seeing connection refused on the password command?
- The ArgoCD pods may still be starting. Run kubectl get pods -n argocd and confirm all pods show Running before retrying.
- If pods are stuck in ContainerCreating, wait another 30 seconds for image pulls to finish.
Why can't I retrieve the ArgoCD admin password?
Verify the cluster and ArgoCD are healthy
Before moving on, confirm that your Kubernetes cluster and ArgoCD installation are both fully operational.
- Check the cluster node is healthy by running:
kubectl get nodes
- You should see the Docker Desktop node in the Ready state with a Kubernetes 1.36 version.
- Confirm all ArgoCD pods are running by running:
kubectl get pods -n argocd
- You should see pods for argocd-server, argocd-repo-server, argocd-application-controller, argocd-dex-server, and argocd-redis all in Running state.
- Verify the kubectl context is set correctly by running:
kubectl config current-context
- You should see docker-desktop printed.
- Open your browser and navigate to https://localhost:8080.
- Accept the self-signed certificate warning.
- Log in with username admin and the password you retrieved earlier.
You should see the ArgoCD dashboard with no applications listed yet.
Can't access localhost:8080?
- Run jobs to check if the port-forward is still running in the background. If it stopped, re-run kubectl port-forward svc/argocd-server -n argocd 8080:443 &.
- Make sure no other process is using port 8080. You can check with lsof -i :8080.
Why can't I access the ArgoCD UI at localhost:8080?
Your local telco platform is ready. Next up, you will set up the GitOps repository structure that ArgoCD will use to deploy your 5G Core.
Set Up the GitOps Repository Structure
Your cluster is ready and ArgoCD is watching for work. Now you need to give it something to watch.
In a GitOps workflow, the Git repository IS the desired state of your infrastructure. ArgoCD continuously compares what is in Git against what is running in the cluster. Any difference is treated as drift and corrected automatically.
In this step, get ready to:
- Create a Git repository with separate application, base, and local-overlay directories.
- Define ArgoCD Applications for the Open5GS Operator and observability stack.
- Build a local Kustomize overlay for the Open5GS Custom Resource.
Create the GitOps repository and directory structure
KT Cloud uses four separate repositories in production. For a 60-minute project, you will use a single repo with the same logical separation inside three directories.
- Create a new project directory and initialize a Git repository by running these commands:
cd ~/Desktop
mkdir telco-gitops && cd telco-gitops
git init
- Create the KT Cloud-style directory structure by running these commands:
mkdir -p apps
mkdir -p base/open5gs-cr
mkdir -p overlays/local
- Confirm the directories exist by running:
find . -type d | sort
You should see ./apps, ./base/open5gs-cr, and ./overlays/local in the output.
What are these directories for?
- apps/ holds the ArgoCD Application manifests that deploy the Helm charts and the Open5GS Custom Resource.
- base/open5gs-cr/ holds the reusable Open5GS Custom Resource and its base kustomization.yaml.
- overlays/local/ selects that base and adds settings for your local Kind cluster.
A directory used as a Kustomize base needs its own kustomization.yaml. Helm values stay inside their ArgoCD Application because they are not Kubernetes resources that Kustomize can render.
Define the Open5GS Custom Resource
The Kubernetes Operator pattern gives you a controller that watches for Custom Resource Definitions (CRDs) and reconciles the cluster to match them. The Open5GS Operator manages the full lifecycle of your 5G Core network functions.
- Create the Open5GS Custom Resource with one network slice by running:
cat << 'EOF' > base/open5gs-cr/open5gs-sample.yaml
# Open5GS Custom Resource - defines the 5G Core deployment
# The Operator reads this CR and spins up all network functions
apiVersion: net.gradiant.org/v1
kind: Open5GS
metadata:
name: open5gs-sample
namespace: open5gs
spec:
# Enable Prometheus ServiceMonitors for CNF metrics
serviceMonitor: true
configuration:
mcc: "999"
mnc: "70"
tac: "0001"
slices:
- sst: "1"
sd: "0x111111"
EOF
What does the Open5GS CR define?
This Custom Resource is your entire 5G Core network defined in 15 lines of YAML. The Operator reads it and creates all the necessary pods (AMF, SMF, UPF, NRF, UDM, AUSF, PCF, NSSF) plus MongoDB.
- serviceMonitor: true tells the Operator to create Prometheus ServiceMonitor resources for components that expose metrics (AMF, PCF, UPF).
- The slices array defines network slices using SST (Slice/Service Type) and SD (Slice Differentiator). You start with one slice. In Step 5, you will add a second.
- Confirm the Custom Resource file exists by running:
ls base/open5gs-cr/open5gs-sample.yaml
You should see base/open5gs-cr/open5gs-sample.yaml in the output.
File not showing up?
Make sure you are inside the telco-gitops directory when running the cat commands. Run pwd to confirm your current directory ends with telco-gitops.
help me figure out why my file was not created
Define the observability stack and ArgoCD Application manifests
The kube-prometheus-stack installs Prometheus, Grafana, and the ServiceMonitor CRD. The ServiceMonitor CRD must exist in the cluster before the Open5GS Operator can create ServiceMonitor objects for your CNFs.
- Create the ArgoCD Application manifest for the observability stack by running:
cat << 'EOF' > apps/observability-app.yaml
# ArgoCD Application for the observability stack
# Deploys kube-prometheus-stack with ServiceMonitor CRDs
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: observability
namespace: argocd
spec:
project: default
source:
chart: kube-prometheus-stack
repoURL: https://prometheus-community.github.io/helm-charts
targetRevision: "*"
helm:
releaseName: kube-prometheus-stack
valueFiles: []
valuesObject:
prometheus:
prometheusSpec:
serviceMonitorSelectorNilUsesHelmValues: false
grafana:
enabled: true
service:
type: ClusterIP
defaultRules:
create: true
destination:
server: "https://kubernetes.default.svc"
namespace: monitoring
syncPolicy:
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
EOF
Why keep the values here?
The valuesObject belongs inside this Helm-based ArgoCD Application. It passes chart values directly to Helm without treating the values as standalone Kubernetes resources.
serviceMonitorSelectorNilUsesHelmValues: false lets Prometheus discover ServiceMonitors created outside its own Helm release, including those in the open5gs namespace.
- Create the ArgoCD Application manifest for the Open5GS Operator by running:
cat << 'EOF' > apps/open5gs-operator-app.yaml
# ArgoCD Application for the Open5GS Operator Helm chart
# Installs the Operator controller into the open5gs namespace
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: open5gs-operator
namespace: argocd
spec:
project: default
source:
chart: open5gs-operator
repoURL: registry-1.docker.io/gradiantcharts
targetRevision: "1.0.7"
helm:
releaseName: open5gs-operator
destination:
server: "https://kubernetes.default.svc"
namespace: open5gs
syncPolicy:
syncOptions:
- CreateNamespace=true
EOF
- Create the ArgoCD Application manifest for the Open5GS Custom Resource by running:
cat << 'EOF' > apps/open5gs-cr-app.yaml
# ArgoCD Application for the local Open5GS Kustomize overlay
# ArgoCD renders the overlay before applying the Custom Resource
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: open5gs-cr
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/YOURUSER/telco-gitops.git
targetRevision: HEAD
path: overlays/local
destination:
server: "https://kubernetes.default.svc"
namespace: open5gs
syncPolicy:
syncOptions:
- CreateNamespace=true
EOF
Why does this app point at an overlay?
The Operator and observability Applications use Helm charts from external registries. The Open5GS Custom Resource lives in your Git repository, so its Application uses path: overlays/local.
ArgoCD finds kustomization.yaml at that path and renders the local overlay. Any later change to the base Custom Resource flows through this overlay during the next sync.
- Create the Open5GS base and its local overlay by running these commands:
cat << 'EOF' > base/open5gs-cr/kustomization.yaml
# Reusable base for the Open5GS Custom Resource
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- open5gs-sample.yaml
EOF
cat << 'EOF' > overlays/local/kustomization.yaml
# Local Kind-cluster variant of the Open5GS Custom Resource
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base/open5gs-cr
labels:
- pairs:
environment: local
EOF
- Render the local overlay without applying it by running this command:
kubectl kustomize overlays/local
You should see one Open5GS resource. Its metadata includes environment: local, which proves the overlay loaded and transformed the base.
Overlay failed to render?
Check that base/open5gs-cr/kustomization.yaml lists open5gs-sample.yaml. Confirm that the path in overlays/local/kustomization.yaml is exactly ../../base/open5gs-cr.
Help me debug the local Kustomize overlay.
- List every YAML file in the repository by running this command:
find . -name '*.yaml' | sort
You should see six YAML files across apps/, base/, and overlays/.
- Commit all files to Git by running:
git add .
git commit -m "Initial GitOps structure: Open5GS + observability"
Push to GitHub for full GitOps
If you have a GitHub repository ready, push now with git remote add origin <your-repo-url> followed by git push -u origin main. Update the repoURL in apps/open5gs-cr-app.yaml to match your actual GitHub URL. If you prefer local-only, the next step will show you how to register a local path with ArgoCD.
🙋♀️ Git commit failing?
If Git asks for your identity, run git config user.email "you@example.com" and git config user.name "Your Name" inside the telco-gitops directory, then retry the commit.
help me fix my git commit error
✔️ Awesome, I've got everything!
Great. Make sure you have saved all files and committed them to Git.
ⓧ I'd like to double check the full code
Here is the full content of each file in your repository:
# apps/observability-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: observability
namespace: argocd
spec:
project: default
source:
chart: kube-prometheus-stack
repoURL: https://prometheus-community.github.io/helm-charts
targetRevision: "*"
helm:
releaseName: kube-prometheus-stack
valueFiles: []
valuesObject:
prometheus:
prometheusSpec:
serviceMonitorSelectorNilUsesHelmValues: false
grafana:
enabled: true
service:
type: ClusterIP
defaultRules:
create: true
destination:
server: "https://kubernetes.default.svc"
namespace: monitoring
syncPolicy:
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
# apps/open5gs-operator-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: open5gs-operator
namespace: argocd
spec:
project: default
source:
chart: open5gs-operator
repoURL: registry-1.docker.io/gradiantcharts
targetRevision: "1.0.7"
helm:
releaseName: open5gs-operator
destination:
server: "https://kubernetes.default.svc"
namespace: open5gs
syncPolicy:
syncOptions:
- CreateNamespace=true
# apps/open5gs-cr-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: open5gs-cr
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/YOURUSER/telco-gitops.git
targetRevision: HEAD
path: overlays/local
destination:
server: "https://kubernetes.default.svc"
namespace: open5gs
syncPolicy:
syncOptions:
- CreateNamespace=true
# base/open5gs-cr/open5gs-sample.yaml
apiVersion: net.gradiant.org/v1
kind: Open5GS
metadata:
name: open5gs-sample
namespace: open5gs
spec:
serviceMonitor: true
configuration:
mcc: "999"
mnc: "70"
tac: "0001"
slices:
- sst: "1"
sd: "0x111111"
# base/open5gs-cr/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- open5gs-sample.yaml
# overlays/local/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base/open5gs-cr
labels:
- pairs:
environment: local
Your GitOps repository is now the single source of truth for your 5G Core platform. Next up, you will point ArgoCD at this repository and watch it deploy everything automatically.
Deploy the 5G Core via ArgoCD GitOps
Your Git repository is structured and all your manifests are committed. Now it is time to let ArgoCD take over and deploy the entire 5G Core stack from Git into your cluster.
This is the moment where GitOps becomes real. Instead of running helm install or kubectl apply manually, you will point ArgoCD at your repository and let it continuously reconcile desired state. Deployment order matters here because the Open5GS Operator depends on ServiceMonitor CRDs that the observability stack installs.
In this step, get ready to:
- Register your Git repository with ArgoCD and deploy the observability stack first.
- Deploy the Open5GS Operator and trigger the 5G Core network functions via the Custom Resource.
- Verify all Cloud-Native Network Functions are running and the Operator has reconciled.
Register the repository and deploy the observability stack
ArgoCD needs to know where your Git repository lives before it can pull manifests from it. Once registered, you will deploy the kube-prometheus-stack first because it installs the ServiceMonitor Custom Resource Definitions that the Open5GS Operator relies on for metrics.
- Register your GitHub repository with ArgoCD by running this command:
argocd repo add [[YOUR_REPO_URL="https://github.com/your-username/telco-platform-gitops.git"]]
What does this command do?
The argocd repo add command registers a Git repository URL with ArgoCD so it can pull manifests during sync operations. For public repositories, no credentials are needed.
- Create the monitoring namespace and apply the observability ArgoCD Application manifest by running:
kubectl create namespace monitoring
kubectl apply -f apps/observability-app.yaml
- Trigger ArgoCD to sync the observability application by running:
argocd app sync observability
- Wait for the observability stack to become healthy by running:
argocd app wait observability --timeout 300
Why deploy observability first?
The kube-prometheus-stack Helm chart installs the ServiceMonitor CRD into your cluster. The Open5GS Custom Resource sets serviceMonitor: true, which tells the Operator to create ServiceMonitor objects for each network function.
If those CRDs do not exist when the Operator tries to create ServiceMonitors, the deployment will fail. This ordering guarantees all dependencies are satisfied.
- Verify the observability pods are running by checking the monitoring namespace:
kubectl get pods -n monitoring
You should see Prometheus and Grafana pods in Running state. This may take 2-3 minutes as the images are pulled and containers start.
Pods stuck in Pending or CrashLoopBackOff?
If pods are stuck in Pending, your cluster may need more resources. Check that Docker Desktop memory allocation is at least 8GB.
If pods show CrashLoopBackOff, run kubectl describe pod <pod-name> -n monitoring to inspect the events for specific error messages.
Help me troubleshoot my monitoring pods
Deploy the Open5GS Operator and 5G Core network functions
With ServiceMonitor CRDs now installed, you can safely deploy the Open5GS Kubernetes Operator. The Operator watches for Open5GS Custom Resources and automatically spins up all the 5G Core network functions (AMF, SMF, UPF, NRF, UDM, AUSF, PCF, NSSF) when it detects one.
- Create the open5gs namespace and apply the Operator ArgoCD Application manifest by running:
kubectl create namespace open5gs
kubectl apply -f apps/open5gs-operator-app.yaml
- Sync and wait for the Operator to become healthy by running:
argocd app sync open5gs-operator
argocd app wait open5gs-operator --timeout 300
- Confirm the Operator controller-manager is running:
kubectl get pods -n open5gs
You should see the open5gs-operator-controller-manager pod in Running state. This is the pod that watches for Open5GS Custom Resources and reconciles them.
- Display every container image in the Operator Deployment by running this command:
kubectl get deployment open5gs-operator-controller-manager -n open5gs -o=jsonpath='{range .spec.template.spec.containers[*]}{.name}{"\t"}{.image}{"\n"}{end}'
You should see one manager container using gradiant/open5gs-operator:1.0.7. This confirms that the active Deployment matches the pinned chart.
Still seeing the retired GCR proxy?
If the output includes gcr.io/kubebuilder/kube-rbac-proxy, the live Deployment still contains a sidecar from an older manifest. Chart 1.0.7 renders only the manager container, so a controllerManager.kubeRbacProxy override cannot fix this chart.
Replacing the stale Deployment restarts only the Operator pod. ArgoCD rebuilds it from the pinned chart while the existing 5G Core resources remain in the cluster.
- Replace the Operator Deployment from ArgoCD's desired state by running these commands:
argocd app sync open5gs-operator --resource apps:Deployment:open5gs-operator-controller-manager --replace
argocd app wait open5gs-operator --resource apps:Deployment:open5gs/open5gs-operator-controller-manager --sync --health --timeout 300
Run the image check again. You should now see only gradiant/open5gs-operator:1.0.7.
Help me remove the retired proxy image from my Operator Deployment
- Now deploy the Open5GS Custom Resource that tells the Operator to create the actual 5G Core network functions by running:
kubectl apply -f apps/open5gs-cr-app.yaml
argocd app sync open5gs-cr
argocd app wait open5gs-cr --timeout 600
What happens when the Custom Resource is applied?
The Operator detects the new Open5GS Custom Resource and begins its reconciliation loop. It reads the desired state (which network functions to deploy, how many slices to configure, whether to enable metrics) and creates all the necessary Deployments, Services, and ConfigMaps.
This single CR triggers the creation of 8 separate network function pods plus a MongoDB pod for subscriber data. The Operator also creates ServiceMonitor objects for AMF, PCF, and UPF since serviceMonitor: true is set in the CR.
This deployment may take 3-5 minutes as multiple container images are pulled. The Operator starts pods in dependency order.
Seeing ImagePullBackOff errors?
This guidance applies to the Open5GS network-function pods created from the Custom Resource, not the Operator's controller-manager pod. If the controller-manager references gcr.io/kubebuilder/kube-rbac-proxy, use the retired-proxy recovery steps above instead.
The network functions pull docker.io/gradiant/open5gs:2.7.5 from Docker Hub. A corporate proxy or Docker Hub rate limit can leave those pods in ImagePullBackOff.
Wait a few minutes for Kubernetes to retry the pull. If the error continues, check your Docker Desktop proxy settings.
Help me fix image pull errors in my Open5GS network functions
Verify all 5G Core CNFs are running and the Operator has reconciled
- Check that all 5G Core network function pods are in Running state by running:
kubectl get pods -n open5gs
You should see all of the following pods in Running state: open5gs-sample-amf, open5gs-sample-smf, open5gs-sample-upf, open5gs-sample-nrf, open5gs-sample-udm, open5gs-sample-ausf, open5gs-sample-pcf, and open5gs-sample-nssf.
- Verify the Operator successfully reconciled by checking its logs:
kubectl logs deployment/open5gs-operator-controller-manager -f
Look for log entries showing successful reconciliation of the open5gs-sample resource. Press Ctrl+C to stop following the logs once you confirm reconciliation succeeded.
- Confirm all three ArgoCD Applications show as Synced and Healthy by running:
argocd app get observability
argocd app get open5gs-operator
argocd app get open5gs-cr
All three applications should display Synced for sync status and Healthy for health status. You can also verify this visually in the ArgoCD UI at localhost:8080 where all application cards should show green checkmarks.
What is the Kubernetes Operator pattern?
A Kubernetes Operator extends the cluster with custom logic. Instead of you manually creating 8 separate Deployments and their Services, the Open5GS Operator handles all of that from a single Custom Resource definition.
The Operator also performs drift correction. If someone manually deletes one of the network function pods, the Operator detects the discrepancy between desired and actual state and recreates it automatically. This is exactly the pattern KT Cloud uses in production for their 5G Core lifecycle management.
🙋♀️ ArgoCD shows OutOfSync or Degraded?
If an application shows OutOfSync, it means the live cluster state does not match what is in your Git repository. Run argocd app sync <app-name> again to force reconciliation.
If the health status shows Degraded, one or more pods have failed to start. Check individual pod status with kubectl describe pod <pod-name> -n open5gs to identify the specific issue.
Help me debug my ArgoCD application sync issues
Your entire 5G Core is now deployed and managed through GitOps. Every network function is running as a container in your cluster, reconciled by the Operator, and tracked by ArgoCD. Next up, you will verify that the observability pipeline is flowing metrics from your CNFs into Grafana.
Verify the Observability Pipeline
Your 5G Core network functions are deployed and ArgoCD is keeping them in sync. But running CNFs without monitoring is like driving without a dashboard.
In this step, you will confirm that the observability pipeline is working end-to-end. You will port-forward Grafana and Prometheus to your local machine, then verify that the Open5GS ServiceMonitors are actively scraping metrics from your AMF, PCF, and UPF network functions.
In this step, get ready to:
- Port-forward Grafana and verify CNF metrics are flowing from Prometheus.
- Check the Prometheus targets page to confirm Open5GS ServiceMonitors are active and scraping successfully.
Port-forward Grafana and check CNF metrics
Prometheus collects metrics from your 5G Core pods via ServiceMonitors. Grafana visualizes those metrics. Both services run inside your cluster with ClusterIP services, so you need port-forwarding to access them from your browser.
- First, check which services are available in the monitoring namespace by running:
kubectl get svc -n monitoring
You should see services including one ending in -grafana and one ending in -prometheus.
- Start a port-forward for Grafana by running this command:
kubectl port-forward svc/kube-prometheus-stack-grafana 3000:80 -n monitoring
What does this command do?
This maps port 3000 on your local machine to port 80 on the Grafana service inside the cluster. Grafana's internal web server runs on port 3000, but the Kubernetes service exposes it on port 80.
After running this, http://localhost:3000 reaches Grafana.
- Open a browser and navigate to http://localhost:3000.
- Log in with username admin and password prom-operator.
Why prom-operator?
The kube-prometheus-stack Helm chart sets prom-operator as the default Grafana admin password. This is defined in the chart's values.yaml under grafana.adminPassword.
- In Grafana, click the hamburger menu (three lines) in the top left, then select **Connections**, then **Data sources**.
- Confirm that a Prometheus data source is listed and connected.
- Navigate to **Explore** from the left sidebar.
- In the metrics browser, type amf and check that AMF-related metrics appear in the autocomplete suggestions.
If you see AMF metrics appearing in the Explore view, your observability pipeline is flowing correctly from the 5G Core pods through Prometheus into Grafana.
Not seeing any metrics?
The ServiceMonitors may need a minute to be discovered by Prometheus. Wait 60 seconds and try again.
If metrics still do not appear, verify the Prometheus data source URL in Grafana points to the correct internal service address. Check that the Open5GS CR has serviceMonitor: true set.
Help me troubleshoot why Grafana is not showing Open5GS CNF metrics from Prometheus.
Check Prometheus targets for Open5GS ServiceMonitors
Seeing metrics in Grafana confirms the end-to-end flow. Now verify the source of truth directly in Prometheus to confirm each CNF endpoint is being scraped.
- Open a new terminal tab (keep the Grafana port-forward running in the existing tab).
- Start a port-forward for Prometheus by running:
kubectl port-forward svc/kube-prometheus-stack-prometheus 9090:9090 -n monitoring
What does this command do?
This maps port 9090 on your local machine to port 9090 on the Prometheus service. Prometheus natively serves its web UI on port 9090.
- Open your browser and navigate to http://localhost:9090/targets.
This is the Prometheus targets page. It shows every scrape target grouped by its ServiceMonitor configuration.
- Look for scrape pools related to Open5GS. You should see entries for the AMF, PCF, and UPF endpoints.
- Confirm each target shows a green UP status. This means Prometheus successfully scraped metrics from that endpoint on the last attempt.
What does UP mean on the targets page?
Prometheus displays three possible states for each scrape target. UP means the last scrape was successful. DOWN means the last scrape failed, with an error message explaining why. UNKNOWN means no scrape has occurred yet.
The Open5GS Operator enables metrics by default on AMF, PCF, and UPF. When you set serviceMonitor: true in the Open5GS Custom Resource, the Operator creates ServiceMonitor resources that tell Prometheus where to scrape.
🙋♀️ Seeing DOWN or missing targets?
If targets show DOWN, read the error column for details. Common causes include network policies blocking Prometheus from reaching the open5gs namespace, or a port name mismatch in the ServiceMonitor.
If targets are completely missing from the list, the ServiceMonitor may not have the correct labels for Prometheus to discover it. Check that the kube-prometheus-stack Prometheus resource has serviceMonitorNamespaceSelector configured to watch all namespaces.
Help me debug why my Open5GS ServiceMonitor targets are not appearing in Prometheus.
Your observability pipeline is confirmed. Prometheus is scraping live metrics from your 5G Core network functions, and Grafana can visualize them. Next up, you will use the power of GitOps to reconfigure your network slices by pushing a change to Git and watching ArgoCD reconcile it.
GitOps-Driven Network Slice Reconfiguration
In the previous step, you confirmed that Prometheus and Grafana are scraping live metrics from your 5G Core network functions. Now comes the real power move: reconfiguring your network through Git.
In production, telcos add network slices to serve different service types. You will add a second slice to your 5G Core by editing the Open5GS Custom Resource in your Git repository, then watch ArgoCD and the Operator handle the rollout automatically.
In this step, get ready to:
- Add a second network slice to your Open5GS Custom Resource in Git.
- Observe ArgoCD detect drift and sync the updated configuration.
- Verify the Operator reconciles the change by restarting AMF and NSSF pods.
Modify the Open5GS Custom Resource to add a second network slice
A network slice in 5G is identified by two values: SST (Slice/Service Type) and SD (Slice Differentiator). Your current Open5GS CR defines one slice. You will now add a second slice for a different service type.
- Open the file base/open5gs-cr/open5gs-sample.yaml in your text editor.
- Find the slices section under spec.configuration. It currently looks like this:
configuration:
slices:
- sst: "1"
sd: "0x111111"
- Add a second slice entry below the first one, so the slices section now looks like this:
configuration:
slices:
- sst: "1"
sd: "0x111111"
- sst: "2"
sd: "0x222222"
What do SST and SD mean?
SST (Slice/Service Type) defines the category of service a slice provides. SST 1 is eMBB (enhanced Mobile Broadband) for high-bandwidth applications. SST 2 is URLLC (Ultra-Reliable Low-Latency Communications) for mission-critical applications.
SD (Slice Differentiator) further distinguishes slices within the same SST. Together, SST and SD form a unique identifier called S-NSSAI that the 5G Core uses to route traffic to the correct slice.
- Save base/open5gs-cr/open5gs-sample.yaml.
- Commit and push the change to your Git repository by running these commands:
git add base/open5gs-cr/open5gs-sample.yaml
git commit -m "Add URLLC network slice (SST 2, SD 0x222222)"
git push
What does this commit do?
This commit changes the desired state of your 5G Core in Git. The cluster still has only one slice. ArgoCD will detect the difference between Git (desired state) and the cluster (actual state) during its next polling cycle.
- After pushing, verify your commit landed by checking git log --oneline -1. You should see your commit message at the top.
Push rejected or authentication failed?
If you are using a local Git repository without a remote, skip the git push command. ArgoCD can track a local path if configured that way.
If using GitHub, ensure your personal access token or SSH key is configured. Help me troubleshoot my Git push failure.
Observe ArgoCD detect drift and sync the change
ArgoCD polls your Git repository at regular intervals. When it detects that the manifest in Git differs from what is running in the cluster, it marks the application as OutOfSync.
- Trigger a manual refresh so you do not have to wait for the polling interval by running:
argocd app get open5gs-cr --refresh
You should see the Sync Status change to OutOfSync in the output. This confirms ArgoCD detected the drift between Git and the cluster.
- Trigger the sync to apply the updated manifest by running:
argocd app sync open5gs-cr
What happens during sync?
ArgoCD applies the updated Open5GS Custom Resource to the cluster. The Open5GS Operator watches for changes to the CR. When it detects the new slice configuration, it restarts the affected pods (AMF and NSSF) to load the updated parameters.
This is the Operator pattern in action: you declare the desired state, and the controller reconciles reality to match.
- Wait for the sync to complete, then confirm the application is healthy by running:
argocd app get open5gs-cr
You should see Sync Status as Synced and Health Status as Healthy.
- Verify that the Operator restarted the AMF and NSSF pods by running:
kubectl get pods -n open5gs -l app.kubernetes.io/name=amf
kubectl get pods -n open5gs -l app.kubernetes.io/name=nssf
Both pods should show a recent AGE value (seconds or a few minutes) and a Running status. This confirms the Operator detected the configuration change and restarted them.
Pods not restarting or still showing old age?
Check the Operator controller-manager logs for errors by running:
kubectl logs deployment/open5gs-operator-controller-manager -n open5gs --tail=30
If you see reconciliation errors, ensure your YAML is valid (no indentation issues in the slices array). Help me debug why my Open5GS Operator is not restarting pods after a slice change.
Verify the new network slice is active in AMF logs
The AMF (Access and Mobility Management Function) is responsible for slice selection. When it starts with the new configuration, its logs will show both slices loaded.
- Check the AMF pod logs for slice configuration by running:
kubectl logs deployment/open5gs-sample-amf -n open5gs | grep -i "nssai\|sst\|slice"
You should see log entries referencing both SST 1 (eMBB) and SST 2 (URLLC) slices. This confirms the 5G Core is now serving two network slices.
Why is this significant?
You just completed a full GitOps lifecycle for network infrastructure. The change flowed from Git to ArgoCD to the Operator to the running pods, with zero manual kubectl apply commands against the cluster.
This is exactly how KT Cloud manages their production 5G Core. Every network change is a Git commit, auditable and reversible.
- You can also verify the full reconciliation from the Operator side by running:
kubectl logs deployment/open5gs-operator-controller-manager -n open5gs --tail=20
You should see log lines indicating successful reconciliation of the open5gs-sample resource.
Not seeing slice info in AMF logs?
The AMF may not print slice details at the default log level. Try viewing the full logs without the grep filter:
kubectl logs deployment/open5gs-sample-amf -n open5gs --tail=50
Look for configuration-related entries near the start of the log. If the pod has been running for a while, you may need to add --previous to see the startup logs from the most recent restart. Help me find the slice configuration in my AMF logs.
You have now proven a complete GitOps-driven reconfiguration of a 5G Core network. Every change is declarative, auditable, and automatically reconciled. Next up: the Secret Mission challenges you to take this further with subscriber provisioning and per-slice observability.
Secret mission
Multi-Slice Subscriber Provisioning with Per-Slice Observability
You've deployed a GitOps-managed 5G Core with two network slices. Now take it further: provision a simulated subscriber to a specific slice using the Open5GSUser CRD, then build a custom Grafana dashboard that visualizes per-slice resource allocation and CNF health. This completes the full declarative telecom lifecycle.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker Desktop Kubernetes cluster provisioned with Kind, using approximately 8 GB of RAM.
- Container images cached by Docker, using approximately 4–6 GB of disk space.
- Local telco-gitops repository with ArgoCD manifests and Open5GS configurations.
- ArgoCD CLI binary at /usr/local/bin/argocd.
Keep everything running
No action is needed. Choose this if you are still actively experimenting with your 5G Core platform.
- Keep Docker Desktop running so its Kind-provisioned Kubernetes cluster remains available.
- Keep ArgoCD reconciling your Git repository against the cluster state.
- Start the Grafana and Prometheus port-forwards whenever you want to inspect metrics.
- Keep the ArgoCD CLI binary at /usr/local/bin/argocd for future projects.
Pause - I'll come back to this later
Pause Docker Desktop to freeze the current cluster state while reducing CPU and memory use. Your containers resume from the same in-memory state.
- Select the Pause icon to the left of the footer in the Docker Desktop Dashboard.
- Confirm that Docker Desktop displays a paused status in the Dashboard.
- Select Resume from the Docker menu when you return to the project.
- Run kubectl get pods -A after resuming. You should see your ArgoCD, Open5GS, Prometheus, and Grafana pods return to the Running state.
Your Git repository is your safety net
Your Git repository records the desired state of the platform. If the local cluster ever needs to be reset, you can recreate it in Docker Desktop and let ArgoCD rebuild the workloads.
Delete - I don't want to use this again
Resetting the cluster deletes every Kubernetes workload in Docker Desktop. Your Git repository remains available until you remove it separately.
- Open the Troubleshoot menu in Docker Desktop.
- Select Reset Kubernetes cluster.
- Confirm the reset to delete all Kubernetes stacks, resources, and Kind node containers.
- Reclaim space used by unused container images by running this command:
docker system prune -a
This command can remove images from other local projects. Review the confirmation prompt before entering y.
- Remove the local telco-gitops repository by running this command:
rm -rf ~/Desktop/telco-gitops
- Remove the standalone ArgoCD CLI by running this command:
sudo rm /usr/local/bin/argocd
- Verify that the Docker Desktop Kubernetes containers are gone by running this command:
docker ps
You should see no Kubernetes cluster node containers in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-style GitOps-managed 5G Core platform on local Kubernetes, using the same architecture that KT Cloud runs in production.
You've learned how to:
- Deploy a full 5G Standalone Core (AMF, SMF, UPF, NRF, UDM, AUSF, PCF) as Cloud-Native Network Functions on Kubernetes using the Operator pattern with automatic drift correction.
- Manage network slices declaratively through Custom Resources in Git, with ArgoCD syncing changes and the Operator reconciling desired state automatically.
- Build a telco-grade observability pipeline with Prometheus and Grafana monitoring CNF metrics via ServiceMonitors.
- Secret Mission: Provisioned simulated subscribers to specific network slices using the Open5GSUser CRD and built per-slice Grafana dashboards, completing the full declarative telecom lifecycle.
Ready to quiz yourself?