기존에는 OpenAirInterface(OAI) gNB와 free5GC Core를 Docker Compose 환경에서 연동하여 사용하였다.
Docker Compose 기반 구성에서는 free5GC가 단일 VM 안에서 실행되었고,
AMF와 UPF는 Docker port publishing을 통해 외부 gNB와 통신하였다.
따라서 이 방식에서는 gNB가 free5GC 호스트의 IP와 포트로 직접 접근할 수 있었기 때문에 N2/SCTP, N3/GTP-U 연결이 비교적 단순하게 구성되었다.
https://gksyb4235.tistory.com/162
VMware 환경에서 OAI gNB ↔ free5GC 연동하기 (NG Setup)
OAI에 Free5GC Core 연결하기개요OpenAirInterface(OAI) 5G RAN은 기본적으로 OAI-CN5G 코어와 연동되도록 설정되어 있다. 기본 구성에서는 OAI-CN5G의 모든 컴포넌트가 같은 docker 네트워크(192.168.70.128/26) 위에서
gksyb4235.tistory.com
현재는 free5GC를 Kubernetes 환경으로 이전하였다.
Kubernetes에서는 free5GC를 Helm chart로 배포하고, Control Plane NF와 User Plane NF를 서로 다른 node에 분리하였다.
또한 Calico를 기본 Pod network로 사용하고, N2/N3/N4/N6와 같은 5G 특화 인터페이스는 Multus CNI를 통해 별도의 secondary interface로 구성하였다.
두 환경을 비교하면 다음과 같다.
| 항목 | 이전 (Docker Compose) | 현재 (Kubernetes/Helm) |
| Free5GC 실행 방식 | docker-compose (단일 VM) | Helm Chart (멀티 노드 k8s) |
| AMF 접근 방식 | 포트 포워딩 (호스트IP:31412/sctp) | NodePort 또는 Multus ipvlan |
| UPF 접근 방식 | 포트 포워딩 (호스트IP:2152/udp) | Multus ipvlan (L2 직접 접근) |
| NF간 통신 | Docker bridge (10.100.200.0/24) | K8s Pod Network + Multus |
| 네트워크 격리 | Docker bridge 1개 | N2/N3/N4/N6 각각 별도 ipvlan |
따라서 구축하고자 하는 Topology는 다음과 같다.
## 현재 Kubernetes 클러스터 네트워크 토폴로지
┌─────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
│ oai-ran 노드 │ │ free5gc-cp 노드 │ │ free5gc-up 노드 │
│ (OAI gNB) │ │ (AMF, NRF, SMF 등) │ │ (UPF) │
│ │ │ │ │ │
│ ens33: 172.16.106.129│ │ ens33: 172.16.106.131 │ │ ens33: 172.16.106.132 │
│ ens34: 192.168.56.30│ │ ens34: 192.168.56.51 │ │ ens34: 192.168.56.52 │
└─────────┬───────────┘ └──────────┬───────────────┘ └──────────┬───────────────┘
│ │ │
│ VMnet (192.168.56.0/24 — ens34) │
├────────────────────────────┼────────────────────────────────┤
│ │ │
│ ┌──────┴──────┐ ┌──────┴──────┐
│ │ AMF Pod │ │ UPF Pod │
│ │ N2(ipvlan): │ │ N3(ipvlan): │
│ │ 10.100.50.249│ │ 192.168.56.19│
│ │ (ens34) │ │ (ens34) │
│ │ │ │ N6(ipvlan): │
│ │ NodePort: │ │ 172.16.106.10│
│ │ 31412/SCTP │ │ (ens33) │
│ └──────────────┘ └──────────────┘
│
│ ★ gNB → AMF: 192.168.56.51:31412 (NodePort/SCTP)
│ ★ gNB ← SMF에서 전달받는 UPF N3: 192.168.56.19 (L2 직접 도달)
OpenAirInterface 쪽 수정해야 되는 파일 -
/home/user/openairinterface5g/targets/PROJECTS/GENERIC-NR-5GC/CONF/gnb.sa.band78.fr1.106PRB.usrpb210.conf
이때 gNB에 3개의 Network Slice가 정의한다.
snssaiList = ({ sst = 1; sd = 0x000001}, { sst = 2; sd = 0x000002; }, { sst = 3; sd = 0x000003; })
이 값을 기준으로 free5GC 쪽으로 맞춰야 한다.
NSSF 부분 수정

SMF 부분 수정

UPF 부분 수정

이후 Free5GC webconsole에서 OAI-RAN에서 설정한 Subscriber를 추가한다



Troubleshooting
OAI gNB와 AMF 간 SCTP 연결 실패
이 과정에서 OAI gNB에서 AMF로 NG Setup을 시도했지만 SCTP 연결이 정상적으로 수립되지 않았다.
Docker Compose 환경에서는 -p 38412:38412/sctp 또는 host network 기반으로 AMF에 연결할 수 있었지만,
Kubernetes 환경에서는 동일한 방식이 바로 동작하지 않은 상황.
초기 Kubernetes 구성에서는 AMF N2 인터페이스가 Multus ipvlan으로 생성되어 있었고, 다음과 같은 IP가 할당되어 있었다.
AMF N2 IP: 10.100.50.249/29
반면 OAI gNB는 다음 대역에 있었다. (즉, gNB와 AMF N2 인터페이스가 서로 다른 서브넷에 존재하였다.)
OAI gNB: 192.168.56.30/24
문제의 원인은 다음과 같다.
Multus ipvlan L2 모드는 master interface와 같은 L2 네트워크에 Pod의 secondary interface를 붙이는 방식이다.
이때 통신 상대와 직접 L2로 통신하려면, Pod에 할당된 IP 역시 실제 L2 네트워크와 같은 서브넷에 있어야 한다.
현재 AMF의 N2 인터페이스는 free5gc-cp 노드의 ens34를 master interface로 사용하고 있었다.
ens34는 192.168.56.0/24 대역에 속한다. 따라서 AMF의 N2 IP도 192.168.56.0/24 대역이어야 했다.
하지만 기존 설정에서는 AMF N2 IP가 10.100.50.249/29로 지정되어 있었다.
이 주소는 gNB가 위치한 192.168.56.0/24와 다른 대역이므로 gNB에서 직접 도달할 수 없었다.
oai-ran: 192.168.56.30/24
AMF N2: 10.100.50.249/29
→ 서로 다른 서브넷
→ L2 직접 도달 불가
→ SCTP 연결 실패
이 문제를 해결하기 위해 처음에는 Kubernetes NodePort를 통해 AMF SCTP 포트를 노출하는 방식을 고려하였다.
gNB → 192.168.56.51:31412/SCTP → NodePort → AMF Pod
하지만 SCTP는 일반적인 TCP/UDP와 달리 multi-homing, association 관리 등에서 kube-proxy 기반 NodePort와 잘 맞지 않는 경우가 있다. 또한 gNB가 바라보는 AMF 주소가 실제 AMF Pod의 N2 주소가 아니라 node IP가 되는 점도 구조적으로 깔끔하지 않았다.
따라서 Docker Compose 환경에서 성공했던 구조를 Kubernetes에서 재현하는 방향으로 접근하였다.
핵심 아이디어는 다음과 같다.
gNB와 AMF가 같은 192.168.56.0/24 L2 네트워크에서 직접 SCTP 통신하도록 구성
이를 위해 두 가지 방식을 검토하였다.
첫 번째는 AMF의 Multus N2 IP를 192.168.56.0/24 대역으로 변경하는 방식이다.
이 경우 AMF Pod에 별도의 N2 인터페이스가 생성되고, gNB는 해당 N2 IP로 직접 접속한다.
두 번째는 AMF Pod를 hostNetwork: true로 실행하는 방식이다. 이 경우 AMF Pod가 free5gc-cp 노드의 네트워크 네임스페이스를 직접 사용하므로, AMF가 192.168.56.51:38412에서 직접 SCTP를 listen할 수 있다.
이번 구성에서는 Docker Compose 환경과 가장 유사한 효과를 내기 위해 AMF에 hostNetwork: true를 적용하였다.
따라서 AMF Deployment에 다음 patch를 적용하였다.
kubectl patch deployment free5gc-free5gc-amf-amf -n free5gc --type='json' \
-p='[
{"op":"add","path":"/spec/template/spec/hostNetwork","value":true},
{"op":"add","path":"/spec/template/spec/dnsPolicy","value":"ClusterFirstWithHostNet"}
]'
적용 후 AMF Pod는 free5gc-cp 노드의 네트워크 네임스페이스를 직접 사용한다.
따라서 AMF는 노드의 Host-only IP인 192.168.56.51에서 SCTP 포트 38412를 직접 listen할 수 있다.
OAI gNB: 192.168.56.30
AMF: 192.168.56.51:38412/SCTP
이 경우 AMF의 별도 Multus N2 인터페이스는 필요하지 않으므로 n2network.enabled는 비활성화할 수 있다.
NodePort 서비스 역시 더 이상 필요하지 않지만, 남아 있어도 직접 연결에는 영향을 주지 않는다.
hostNetwork: true를 사용하는 Pod는 기본적으로 노드의 /etc/resolv.conf를 사용한다.
이 경우 Kubernetes 내부 서비스명, 예를 들어 NRF나 SMF의 service name을 정상적으로 resolve하지 못할 수 있다.
이를 방지하기 위해 다음 설정을 함께 적용하였다.
dnsPolicy: ClusterFirstWithHostNet
이 설정은 hostNetwork Pod에서도 CoreDNS를 우선 사용하게 하여 Kubernetes 내부 service discovery가 정상적으로 동작하도록 한다.
OAI gNB 설정에서는 AMF 주소를 다음과 같이 지정하였다.

포트는 NGAP 기본 포트인 38412를 사용하므로 별도로 지정하지 않았다.
정상 연결 후 AMF 로그에서는 다음과 같은 내용을 확인할 수 있었다.
[INFO][AMF][Ngap] Listen on 127.0.0.1/.../192.168.56.51/...:38412
[INFO][AMF][Ngap] SCTP Accept from: 192.168.56.30:45614
[INFO][AMF][Ngap] Create a new NG connection for: 192.168.56.30:45614
[INFO][AMF][Ngap] Handle NGSetupRequest
[INFO][AMF][Ngap] Send NG-Setup response
이를 통해 OAI gNB와 AMF 간 N2/SCTP 연결은 정상적으로 수립되었다.

N3/GTP-U 데이터 경로 실패
SCTP 연결이 성공한 뒤 UE는 PDU Session을 수립하고 IP를 할당받았다.
그러나 UE 인터페이스를 통해 외부로 ping을 수행하면 실패하는 문제가 발생했다.
ping -I oaitun_ue1 -c 4 8.8.8.8
오류는 다음과 같이 나타났다.
From 20.96.53.179 icmp_seq=1 Destination Host Unreachable

여기서 20.96.53.179는 UPF Pod의 Calico Pod network IP였다.
즉, UE 트래픽이 의도한 N6 인터페이스가 아니라 Calico Pod network 쪽으로 빠지고 있었다.
UPF Pod에는 여러 인터페이스가 존재한다.
| 인터페이스 | IP | 용도 |
| eth0 | 20.96.53.179 | Calico Pod network, SBI 통신 |
| n3 | 192.168.56.230 | gNB GTP-U 수신 |
| n4 | 10.100.50.241 | SMF PFCP 제어 |
| n6 | 172.16.106.10 | 외부 DN/인터넷 |
| upfgtp | - | GTP-U decapsulation 후 UE 트래픽 처리 |
문제는 UPF Pod 내부의 default route였다.
ip route get 8.8.8.8
결과는 다음과 같았다.
8.8.8.8 via 169.254.1.1 dev eth0 src 20.96.53.179
즉, UPF에서 외부 인터넷으로 나가는 기본 경로가 N6 인터페이스가 아니라 Calico의 eth0으로 잡혀 있었다.
그 결과 UE 트래픽이 다음과 같이 잘못 흘렀다.
UE traffic
→ N3 GTP-U
→ UPF
→ upfgtp
→ eth0, Calico Pod network
→ Destination Host Unreachable
원래 의도한 경로는 다음과 같아야 한다.
UE traffic
→ N3 GTP-U
→ UPF
→ upfgtp
→ n6
→ 172.16.106.2 gateway
→ Internet
문제는 UPF Pod 안에서 모든 트래픽이 동일한 main routing table을 참조한다는 점이었다.
따라서 UE IP 대역에서 출발하는 패킷만 별도 routing table로 분리하였다.
UE IP 대역은 다음과 같이 구성되어 있었다.
10.60.0.0/16
10.61.0.0/16
10.62.0.0/16
이 대역에서 출발하는 패킷은 table 100을 참조하도록 policy routing rule을 추가하였다.
kubectl exec -n free5gc <upf-pod> -- ip rule add from 10.60.0.0/16 table 100
kubectl exec -n free5gc <upf-pod> -- ip rule add from 10.61.0.0/16 table 100
kubectl exec -n free5gc <upf-pod> -- ip rule add from 10.62.0.0/16 table 100
그리고 table 100의 default route를 N6 gateway로 설정하였다.
kubectl exec -n free5gc <upf-pod> -- ip route add default via 172.16.106.2 dev n6 table 100
마지막으로 UE 트래픽이 외부 인터넷으로 나갈 수 있도록 NAT를 추가하였다.
kubectl exec -n free5gc <upf-pod> -- iptables -t nat -A POSTROUTING -s 10.60.0.0/16 -o n6 -j MASQUERADE
kubectl exec -n free5gc <upf-pod> -- iptables -t nat -A POSTROUTING -s 10.61.0.0/16 -o n6 -j MASQUERADE
kubectl exec -n free5gc <upf-pod> -- iptables -t nat -A POSTROUTING -s 10.62.0.0/16 -o n6 -j MASQUERADE
각 설정의 의미는 다음과 같다.
| 명령 | 의미 |
| ip rule add from 10.60.0.0/16 table 100 | UE IP 대역에서 출발하는 패킷은 main table 대신 table 100 사용 |
| ip route add default via 172.16.106.2 dev n6 table 100 | table 100의 default route를 N6 gateway로 설정 |
| MASQUERADE -o n6 | UE source IP를 N6 인터페이스 IP로 NAT |
이 설정을 통해 UE 트래픽은 Calico Pod network가 아니라 N6 인터페이스를 통해 외부로 나가게 된다.
Ping Test
정책 라우팅과 NAT 적용 후 UE 인터페이스에서 외부 ping을 다시 수행하였다.
ping -I oaitun_ue1 -c 4 8.8.8.8
결과는 다음과 같이 정상적으로 응답이 돌아왔다.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=127 time=441 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=127 time=362 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=127 time=511 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=127 time=461 ms

이를 통해 N2/SCTP 연결뿐 아니라 N3/GTP-U 기반 UE 데이터 경로도 정상적으로 동작하는 것을 확인하였다.
OAI gNB/UE를 Docker에서 Kubernetes Pod로 전환하기
이제 이를 기반으로 gNB와 UE를 k8s pod로 띄우고 모니터링 Scope에 포함시킨다.
기존에는 oai-ran VM(192.168.56.30)에 SSH로 접속하여 터미널에서 직접 Docker 컨테이너를 실행했다.
각 명령어는 아래와 같다.
# gNB 실행
docker run --rm -it \
--name oai-gnb \
--privileged \
--cap-add NET_ADMIN \
--cap-add NET_RAW \
--device /dev/net/tun:/dev/net/tun \
--network host \
-v /home/user/openairinterface5g/targets/PROJECTS/GENERIC-NR-5GC/CONF/gnb.sa.band78.fr1.106PRB.usrpb210_docker.conf:/opt/oai-gnb/etc/gnb.conf \
oai-gnb:latest \
/opt/oai-gnb/bin/nr-softmodem \
-O /opt/oai-gnb/etc/gnb.conf \
--gNBs.[0].min_rxtxtime 6 \
--rfsim
# UE 실행
docker run --rm -it \
--name oai-nr-ue1 \
--privileged \
--cap-add NET_ADMIN \
--cap-add NET_RAW \
--device /dev/net/tun:/dev/net/tun \
--network host \
-v /home/user/openairinterface5g/targets/PROJECTS/GENERIC-NR-5GC/CONF/ue1.conf:/opt/oai-nr-ue/etc/nr-ue.conf \
oai-nr-ue:latest \
/opt/oai-nr-ue/bin/nr-uesoftmodem \
-r 106 --numerology 1 --band 78 -C 3619200000 \
--rfsim \
--uicc0.imsi 208930000000001 \
--rfsimulator.serveraddr 127.0.0.1 \
--telnetsrv --telnetsrv.listenport 9095
이 방식의 경우 k8s control plane이 해당 컨테이너의 존재를 파악할 수 없다.
이는 곧 Headlamp, Prometheus, Grafana 등 모니터링 도구에서 감지할 수 없음을 뜻한다.
따라서 위 Docker 명령어를 K8s Pod manifest(YAML)로 변환하여,
- K8s가 gNB/UE의 라이프사이클을 관리
- 모니터링 스택에 자동 노출
- kubectl로 통합 관리 가능
위를 가능하도록 한다.
우선 Docker에서 K8s YAML로 변환 매핑은 다음과 같다.
| Docker 옵션 | K8s 대응 | 설명 |
| --network host | hostNetwork: true | 노드의 네트워크 네임스페이스 공유 |
| --priviledged | securityContext.priviledged: true | 모든 커널 기능 허용 |
| --cap-add NET_ADMIN | capabilities.add: ["NET_ADMIN"] | 네트워크 설정 변경 권한 |
| --device /dev/net/tun | hostPath volume + VolumeMount | TUN 디바이스 접근 |
| -v host:container | hostPath volume + volumeMount | 설정 파일 마운트 |
| 이미지명 | image: 필드 | 컨테이너 이미지 |
| 실행 명령어 | command: + args: | 엔트리포인트 + 인자 |
또한 oai-ran 노드의 container runtime은 CRI-O이다.
Docker 이미지를 직접 인식하지 못하므로, 로컬 registry를 통해 이미지를 제공했다.
# 1. 로컬 registry 실행
docker run -d --restart=always --name registry -p 5000:5000 registry:2
# 2. 이미지 태그 변경 및 push
docker tag oai-gnb:latest localhost:5000/oai-gnb:latest
docker tag oai-nr-ue:latest localhost:5000/oai-nr-ue:latest
docker push localhost:5000/oai-gnb:latest
docker push localhost:5000/oai-nr-ue:latest
# 3. CRI-O에 insecure registry 등록
echo '[[registry]]
location = "localhost:5000"
insecure = true' | sudo tee /etc/containers/registries.conf.d/local.conf
# 4. CRI-O 재시작
sudo systemctl restart crio
이후 RAN 관련 Pod를 분리 관리하기 위해 전용 namespace를 생성했다.
kubectl create namespace oai-ran
이후 gNB와 UE에 대한 Pod YAML을 작성한다.


이후 모든 Free5GC Pod가 떠있는 상태에서 gNB를 띄우고, AMF 연결 확인 후 UE를 띄운다.
# 1. gNB 배포
kubectl apply -f /home/ubuntu/oai-ran-pods/oai-gnb.yaml
# 2. gNB → AMF 연결 확인
kubectl logs -n oai-ran oai-gnb | grep "NGSetupResponse"
# "Received NGSetupResponse from AMF" 확인 후 진행
# 3. UE 배포
kubectl apply -f /home/ubuntu/oai-ran-pods/oai-nr-ue1.yaml
각 Pod를 내리는 명령어는 다음과 같다.
kubectl delete -f /home/ubuntu/oai-ran-pods/oai-nr-ue1.yaml
kubectl delete -f /home/ubuntu/oai-ran-pods/oai-gnb.yaml
이때 UE2, UE3도 동일한 image에서 config 파일과 IMSI 인자만 다르게 넣는 구조라서,
이 이미지 하나만 있으면 UE가 몇 개든 추가 push 없이 바로 실행할 수 있다.
kubectl apply 실행 시 일어나는 일
master-node에서 kubectl apply 실행
│
├── API Server가 Pod 생성 요청 수신
│
├── Scheduler가 nodeSelector 확인 → oai-ran 노드에 배치 결정
│
├── oai-ran의 kubelet이 Pod 생성 명령 수신
│
├── CRI-O가 localhost:5000에서 이미지 pull (또는 캐시 사용)
│
├── 컨테이너 생성 (hostNetwork이므로 노드 네트워크 사용)
│
└── command/args로 지정된 바이너리 실행
물리적으로 컨테이너가 실행되는 위치는 oai-ran VM 내부이며, Docker로 실행했을 때와 동일하다.
차이는 라이프사이클을 K8s가 관리한다는 점이다.
최종 아키텍처
oai-ran (192.168.56.30)
│
│ ens34 (192.168.56.0/24, L2)
│
├─── N2/SCTP ──→ free5gc-cp (192.168.56.51:38412)
│ └── AMF Pod (hostNetwork: true)
│
└─── N3/GTP-U ──→ UPF Pod N3 (192.168.56.230, Multus ipvlan)
│
│ upfgtp (decapsulation)
│
│ ip rule → table 100
│
└──→ N6 (172.16.106.10) ──NAT──→ 인터넷
via 172.16.106.2

이제 VMware 안에 있는 OAI와 Free5GC Cluster는 구축되었고,
이제 본격적인 Intelligence Plane (Agentic AI Plane)을 구축할 차례!

'Agentic AI 구축 > Agentic AI Intelligence Plane 구축' 카테고리의 다른 글
| Free5GC v3.3 → v4.2.2 마이그레이션 — Free5GC v4 Helm Chart 생성하기 (0) | 2026.07.13 |
|---|---|
| 통신 도메인에서의 LLM의 한계와 GSMA의 Open-Telco 벤치마크 (0) | 2026.06.20 |
| Prometheus & Grafana & Loki를 활용한 RAN-CN E2E 모니터링 스택 구축 (0) | 2026.06.10 |
| Kubernetes 1.35 기반 서버 환경에서 Free5GC Cluster 구축 및 Troubleshooting (0) | 2026.06.08 |
| Agentic AI로 Mobile Network를 운영하기 위한 실험 환경 (0) | 2026.06.02 |