Getting started¶
In this tutorial, we will deploy and run Charmed OAI RAN (Radio Access Network) using Juju and Terraform. The Charmed OAI RAN consists of two Juju charms representing the CU (Central Unit) and the DU (Distributed Unit).
As part of this tutorial, we will also deploy additional components:
Charmed Aether SD-Core: A 5G core network which will manage our RAN network
SD-Core Router: a software router facilitating communication between the 5G Core and the RAN
User Equipment (UE) simulator: A simulated cellphone which will allow us to validate the correctness of the entire deployment
To complete this tutorial, you will need a machine which meets the following requirements:
A recent
x86_64
CPU (Intel 4ᵗʰ generation or newer, or AMD Ryzen or newer)At least 4 cores (8 recommended)
At least 8 GB of RAM (16 GB recommended)
50GB of free disk space
1. Deploy Charmed Aether SD-Core¶
1. Install Canonical K8s¶
From your terminal, install Canonical K8s and bootstrap it:
sudo snap install k8s --classic --channel=1.33-classic/stable
cat << EOF | sudo k8s bootstrap --file -
containerd-base-dir: /opt/containerd
cluster-config:
network:
enabled: true
dns:
enabled: true
load-balancer:
enabled: true
local-storage:
enabled: true
annotations:
k8sd/v1alpha1/cilium/sctp/enabled: true
EOF
Add the Multus plugin.
sudo k8s kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset-thick.yml
We must give MetalLB an address range that has at least 3 IP addresses for Charmed Aether SD-Core.
sudo k8s set load-balancer.cidrs="10.0.0.2-10.0.0.4"
Bootstrap a Juju controller¶
From your terminal, install Juju:
sudo snap install juju --channel=3.6/stable
Save the K8s credentials to allow bootstrapping Juju controller.
mkdir -p ~/.kube
sudo k8s config > ~/.kube/config
mkdir -p ~/.local/share/juju/
sudo k8s config > ~/.local/share/juju/credentials.yaml
Bootstrap a Juju controller:
juju bootstrap k8s
Note
There is a bug in Juju that occurs when
bootstrapping a controller on a new machine. If you encounter it, create the following
directory:
mkdir -p /home/ubuntu/.local/share
Install Terraform¶
From your terminal, install Terraform:
sudo snap install terraform --classic
Create Terraform module¶
On the host machine create a new directory called terraform
:
mkdir terraform
Inside newly created terraform
directory create a terraform.tf
file:
cd terraform
cat << EOF > versions.tf
terraform {
required_providers {
juju = {
source = "juju/juju"
version = ">= 0.20.0"
}
}
}
EOF
Create a Terraform module containing the Charmed Aether SD-Core and a router:
cat << EOF > core.tf
resource "juju_model" "sdcore" {
name = "sdcore"
}
module "sdcore-router" {
source = "git::https://github.com/canonical/sdcore-router-k8s-operator//terraform"
model = juju_model.sdcore.name
depends_on = [juju_model.sdcore]
}
module "sdcore" {
source = "git::https://github.com/canonical/terraform-juju-sdcore//modules/sdcore-k8s"
model = juju_model.sdcore.name
depends_on = [module.sdcore-router]
traefik_config = {
routing_mode = "subdomain"
}
}
EOF
Note
You can get a ready example by cloning this Git repository.
All necessary files are in the examples/terraform/getting_started
directory.
Deploy 5G Core network¶
Initialize Juju Terraform provider:
terraform init
Deploy SD-Core by applying your Terraform configuration:
terraform apply -auto-approve
The deployment process should take approximately 10-15 minutes.
Monitor the status of the deployment:
juju switch sdcore
juju status --watch 1s --relations
The deployment is ready when all the charms are in the active/idle
state.
It is normal for grafana-agent
and traefik
to be in blocked
state.
Example:
ubuntu@host:~/terraform $ juju status
Model Controller Cloud/Region Version SLA Timestamp
sdcore k8s k8s 3.6.7 unsupported 09:45:02+02:00
App Version Status Scale Charm Channel Rev Address Exposed Message
amf 1.6.4 active 1 sdcore-amf-k8s 1.6/edge 937 10.152.183.27 no
ausf 1.6.2 active 1 sdcore-ausf-k8s 1.6/edge 741 10.152.183.81 no
grafana-agent 0.40.4 blocked 1 grafana-agent-k8s 1/stable 116 10.152.183.232 no Missing ['grafana-cloud-config']|['logging-consumer'] for logging-provider; ['grafana-cloud-config']|['send-remote-wr...
mongodb active 1 mongodb-k8s 6/stable 61 10.152.183.151 no
nms 1.8.5 active 1 sdcore-nms-k8s 1.6/edge 889 10.152.183.87 no
nrf 1.6.2 active 1 sdcore-nrf-k8s 1.6/edge 825 10.152.183.106 no
nssf 1.6.1 active 1 sdcore-nssf-k8s 1.6/edge 685 10.152.183.158 no
pcf 1.6.1 active 1 sdcore-pcf-k8s 1.6/edge 729 10.152.183.56 no
router active 1 sdcore-router-k8s 1.6/edge 482 10.152.183.192 no
self-signed-certificates active 1 self-signed-certificates 1/stable 308 10.152.183.63 no
smf 2.0.2 active 1 sdcore-smf-k8s 1.6/edge 829 10.152.183.222 no
traefik 2.11.0 blocked 1 traefik-k8s latest/stable 236 10.152.183.208 no "external_hostname" must be set while using routing mode "subdomain"
udm 1.6.1 active 1 sdcore-udm-k8s 1.6/edge 691 10.152.183.37 no
udr 1.6.2 active 1 sdcore-udr-k8s 1.6/edge 671 10.152.183.95 no
upf 2.0.1 active 1 sdcore-upf-k8s 1.6/edge 797 10.152.183.253 no
Unit Workload Agent Address Ports Message
amf/0* active idle 10.1.0.253
ausf/0* active idle 10.1.0.33
grafana-agent/0* blocked idle 10.1.0.215 Missing ['grafana-cloud-config']|['logging-consumer'] for logging-provider; ['grafana-cloud-config']|['send-remote-wr...
mongodb/0* active idle 10.1.0.217 Primary
nms/0* active idle 10.1.0.209
nrf/0* active idle 10.1.0.186
nssf/0* active idle 10.1.0.32
pcf/0* active idle 10.1.0.254
router/0* active idle 10.1.0.28
self-signed-certificates/0* active idle 10.1.0.249
smf/0* active idle 10.1.0.100
traefik/0* blocked idle 10.1.0.11 "external_hostname" must be set while using routing mode "subdomain"
udm/0* active idle 10.1.0.141
udr/0* active idle 10.1.0.108
upf/0* active idle 10.1.0.221
Offer Application Charm Rev Connected Endpoint Interface Role
amf amf sdcore-amf-k8s 937 0/0 fiveg-n2 fiveg_n2 provider
nms nms sdcore-nms-k8s 889 0/0 fiveg_core_gnb fiveg_core_gnb provider
upf upf sdcore-upf-k8s 797 0/0 fiveg_n3 fiveg_n3 provider
Configure the ingress¶
Get the external IP address of Traefik’s traefik-lb
LoadBalancer service:
sudo k8s kubectl -n sdcore get svc | grep "traefik-lb"
The output should look similar to below:
ubuntu@host:~/terraform $ sudo k8s kubectl -n sdcore get svc | grep "traefik-lb"
traefik-lb LoadBalancer 10.152.183.83 10.0.0.2 80:30462/TCP,443:30163/TCP 9m4s
In this tutorial, the IP is 10.0.0.2
. Please note it, as we will need it in the next step.
Configure Traefik to use an external hostname. To do that, edit traefik_config
in the core.tf
file:
:caption: core.tf
(...)
module "sdcore" {
(...)
traefik_config = {
routing_mode = "subdomain"
external_hostname = "10.0.0.2.nip.io"
}
(...)
}
(...)
Apply new configuration:
terraform apply -auto-approve
2. Deploy Charmed OAI RAN CU and DU¶
Create a Terraform module for the Radio Access Network and add Charmed OAI RAN CU and Charmed OAI RAN DU to it:
cat << EOF > ran.tf
resource "juju_model" "oai-ran" {
name = "ran"
}
module "cu" {
source = "git::https://github.com/canonical/oai-ran-cu-k8s-operator//terraform"
model = juju_model.oai-ran.name
config = {
"n3-interface-name": "ran"
}
}
module "du" {
source = "git::https://github.com/canonical/oai-ran-du-k8s-operator//terraform"
model = juju_model.oai-ran.name
config = {
"simulation-mode": true,
"bandwidth": 40,
"frequency-band": 77,
"sub-carrier-spacing": 30,
"center-frequency": "4060",
}
}
resource "juju_integration" "cu-amf" {
model = juju_model.oai-ran.name
application {
name = module.cu.app_name
endpoint = module.cu.requires.fiveg_n2
}
application {
offer_url = module.sdcore.amf_fiveg_n2_offer_url
}
}
resource "juju_integration" "cu-nms" {
model = juju_model.oai-ran.name
application {
name = module.cu.app_name
endpoint = module.cu.requires.fiveg_core_gnb
}
application {
offer_url = module.sdcore.nms_fiveg_core_gnb_offer_url
}
}
resource "juju_integration" "du-cu" {
model = juju_model.oai-ran.name
application {
name = module.du.app_name
endpoint = module.du.requires.fiveg_f1
}
application {
name = module.cu.app_name
endpoint = module.cu.provides.fiveg_f1
}
}
EOF
Update Juju Terraform provider:
terraform init
Deploy Charmed OAI RAN CU and DU:
terraform apply -auto-approve
Monitor the status of the deployment:
juju switch ran
juju status --watch 1s --relations
At this stage both the cu
and the du
applications are expected to be in the waiting/idle
state and the messages should indicate they’re waiting for network configuration.
Example:
ubuntu@host:~/terraform $ juju status
Model Controller Cloud/Region Version SLA Timestamp
ran k8s k8s 3.6.7 unsupported 11:43:43+02:00
SAAS Status Store URL
amf active local admin/sdcore.amf
nms active local admin/sdcore.nms
App Version Status Scale Charm Channel Rev Address Exposed Message
cu waiting 1 oai-ran-cu-k8s 2.2/edge 74 10.152.183.220 no Waiting for TAC and PLMNs configuration
du waiting 1 oai-ran-du-k8s 2.2/edge 109 10.152.183.124 no Waiting for F1 information
Unit Workload Agent Address Ports Message
cu/0* waiting idle 10.1.194.194 Waiting for TAC and PLMNs configuration
du/0* waiting idle 10.1.194.196 Waiting for F1 information
3. Configure the 5G core network through the Network Management System¶
Retrieve the NMS credentials (username
and password
):
juju switch sdcore
juju show-secret NMS_LOGIN --reveal
The output looks like this:
cvmg6h7mp25c7619i89g:
revision: 2
checksum: 68cb0ef846164496a7b4233933c339b667563b1ad93351f1a3e43ceec0dc3d39
owner: nms
label: NMS_LOGIN
created: 2025-04-02T09:28:37Z
updated: 2025-04-02T09:41:02Z
content:
password: 8lR4jyOKQQz
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDM1OTA0NjEsInVzZXJuYW1lIjoiY2hhcm0tYWRtaW4tRFhRVCIsInJvbGUiOjF9.OPd4zEjNqhxkptRrGeybfSqwU78epk2tz7o69zLQnq8
username: charm-admin-DXQT
Retrieve the NMS address:
juju run traefik/0 show-proxied-endpoints
The output should be https://sdcore-nms.10.0.0.2.nip.io/
.
Navigate to this address in your browser and use the username
and password
to login.
Assign Tracking Area Code (TAC) to the gNodeB¶
In the Network Management System (NMS) navigate to the Inventory
tab. Click the Edit
button next to the integrated gNB name and set TAC
to 1
:

Confirm new TAC
value by clicking the Submit
button.
Create a Network Slice¶
Navigate to the Network slices
tab and create a network slice with the following attributes:
Name:
default
MCC:
001
MNC:
01
UPF:
upf-external.sdcore.svc.cluster.local:8805
gNodeB:
ran-cu-cu (tac:1)
You should see the following network slice created:

Create a Device Group¶
Navigate to the Device groups
tab and create a device group with the following attributes:
Name:
device-group
Network Slice:
default
Subscriber IP pool:
172.250.1.0/16
DNS:
8.8.8.8
MTU (bytes):
1456
Maximum bitrate (Mbps):
Downstream:
200
Upstream:
20
QoS:
5QI:
1: GBR - Conversational Voice
ARP:
6
You should see the following device group created:

Create a Subscriber¶
Navigate to Subscribers
tab and click the Create
button. Fill in the following:
Network Slice:
default
Device Group:
device-group
Click the two Generate
buttons to automatically fill in the values in the form. Note the IMSI, OPC, Key and Sequence Number; we are going to use them shortly.
After clicking the Submit
button you should see the subscriber created:

Note
Due to current limitations in the network configuration procedure, it is required to restart the CU Pod after configuring the network.
This limitation will be addressed in the future.
To restart the CU Pod execute:
sudo k8s kubectl -n ran delete pod cu-0
After adding the network configuration the CU and the DU should change their state to active/idle
.
To verify that run:
juju switch ran
juju status
Output should be similar to:
ubuntu@host:~/terraform $ juju status
Model Controller Cloud/Region Version SLA Timestamp
ran k8s k8s 3.6.7 unsupported 09:58:23+02:00
SAAS Status Store URL
amf active local admin/sdcore.amf
nms active local admin/sdcore.nms
App Version Status Scale Charm Channel Rev Address Exposed Message
cu active 1 oai-ran-cu-k8s 2.2/edge 74 10.152.183.233 no
du active 1 oai-ran-du-k8s 2.2/edge 109 10.152.183.254 no
Unit Workload Agent Address Ports Message
cu/0* active idle 10.1.0.196
du/0* active idle 10.1.0.231
5. Deploy Charmed OAI RAN UE Simulator¶
Add Charmed OAI RAN UE Terraform module to ran.tf
. Please replace the imsi
, key
and opc
with the subscriber values from previous step:
cat << EOF >> ran.tf
module "ue" {
source = "git::https://github.com/canonical/oai-ran-ue-k8s-operator//terraform"
model = juju_model.oai-ran.name
config = {
"imsi": "001010100007487", # Use the IMSI generated in the previous step
"key": "5122250214c33e723a5dd523fc145fc0", # Use the Key generated in the previous step
"opc": "981d464c7c52eb6e5036234984ad0bcf", # Use the OPC generated in the previous step
"simulation-mode": true,
}
}
resource "juju_integration" "ue-du" {
model = juju_model.oai-ran.name
application {
name = module.ue.app_name
endpoint = module.ue.requires.fiveg_rf_config
}
application {
name = module.du.app_name
endpoint = module.du.provides.fiveg_rf_config
}
}
EOF
Update Juju Terraform provider:
terraform init
Deploy the UE simulator:
terraform apply -auto-approve
Monitor the status of the deployment:
juju status --watch 1s --relations
The deployment is ready when the ue
application is in the active/idle
state.
6. Run 5G network traffic simulation¶
Run the simulation:
juju run ue/leader ping
The simulation executed successfully if you see success: "true"
as one of the output messages:
ubuntu@host:~$ juju run ue/leader ping
Running operation 1 with 1 task
- task 2 on unit-ue-0
Waiting for task 2...
result: |
PING 8.8.8.8 (8.8.8.8) from 172.250.0.2 oaitun_ue1: 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=13.2 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=116 time=15.3 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=116 time=13.8 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=116 time=12.6 ms
64 bytes from 8.8.8.8: icmp_seq=5 ttl=116 time=14.1 ms
64 bytes from 8.8.8.8: icmp_seq=6 ttl=116 time=14.8 ms
64 bytes from 8.8.8.8: icmp_seq=7 ttl=116 time=14.6 ms
64 bytes from 8.8.8.8: icmp_seq=8 ttl=116 time=14.6 ms
64 bytes from 8.8.8.8: icmp_seq=9 ttl=116 time=14.6 ms
64 bytes from 8.8.8.8: icmp_seq=10 ttl=116 time=14.5 ms
--- 8.8.8.8 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9010ms
rtt min/avg/max/mdev = 12.561/14.217/15.294/0.772 ms
success: "true"
7. Destroy the environment¶
Destroy Terraform deployment:
terraform destroy -auto-approve
Note
Terraform does not remove anything from the working directory. If needed, please clean up
the terraform
directory manually by removing everything except for the core.tf
and terraform.tf
files.
Destroy the Juju controller and all its models:
juju kill-controller k8s