Migration 동기
"AMF가 CPU 50% 쓴다"는 알겠는데, "UE 몇 명 붙어 있는지"는 모르겠다
Prometheus + Grafana + Loki + Alloy를 클러스터에 올리고 나서,
Free5GC NF들의 애플리케이션 메트릭을 수집하려고 각 NF의 /metrics 엔드포인트를 찾았는데,
수집 가능한 metric이 아무것도 없는 상황이었다.
# 9090, 9091, 8000, 8080 전부 빈 응답
kubectl exec -n free5gc <amf-pod> -- wget -qO- http://localhost:9091/metrics
amfcfg.yaml을 열어보니 prometheus/metrics 섹션 자체가 없었고, config의 version: 1.0.9는 Free5GC v3.2~v3.3 계열이었다. 즉 현재 이미지는 Prometheus metrics를 아예 지원하지 않는다.
Free5GC v3에서 볼 수 있는 metric 종류
| 항목 | 상태 | 출처 |
| Pod CPU/MEM/Network I/O | ✅ | kubelet / cAdvisor |
| 노드 레벨 CPU/MEM/Disk | ✅ | node-exporter |
| namespace별 Pod 개수, restart 횟수, Deployment replica 상태 | ✅ | kube-state-metrics |
| MongoDB 연결 수 / ops / 스토리지 | ✅ | mongodb-exporter |
| 로그 (free5gc, oai-ran) | ✅ | Loki + Alloy |
| NF 애플리케이션 메트릭 (UE 수, PDU Session 수 등) | ❌ | — |
즉 통신 네트워크의 인프라 관점의 관측성은 충분한 상태였다.
rate(container_cpu_usage_seconds_total{namespace="free5gc", pod=~".*amf.*"}[5m]) 같은 쿼리로 AMF Pod의 CPU spike는 볼 수 있었다.
하지만 "5G 네트워크가 지금 얼마나 바쁜지" 를 나타내는 수치는 아직 없는 상태, 즉 다음과 같은 metric은 수집할 수 없었다.
- AMF가 현재 몇 개의 UE를 서빙 중인가?
- Registration이 초당 몇 건 발생하는가? 성공/실패율은?
- 활성 PDU Session이 몇 개인가?
- NF 간 SBI 요청 latency는 얼마인가?
Intent 기반 네트워크 Management 환경에서는 이런 컨트롤 플레인 부하 지표가 오히려 CPU 사용률보다 중요한 상황이었다.
워크로드를 정의하는 축이기 때문이다.
Free5GC 릴리스 노트
Free5GC v4.1.0 changelog에 다음과 같은 내용이 있었다.

chore/update-submodules-promotheus by @Alonza0314 in #709
Prometheus metrics exporter by @Niahh
v4.1.0부터 Prometheus metrics exporter가 공식 지원된다.
그리고 free5gc-compose 레포에는 docker-compose-prometheus.yaml까지 들어와 있었다.
prometheus.yml을 열어보니 모든 NF가 port 9091을 scrape target으로 잡고 있었다.
그런데 towards5gs-helm은 v3.x 전용이다
우리가 쓰던 배포 방식은 Orange-Opensource에서 제공하는 Free5GC Helm 파일이었다.
https://github.com/Orange-OpenSource/towards5gs-helm
GitHub - Orange-OpenSource/towards5gs-helm: Helm charts for deploying 5G network services on Kubernetes
Helm charts for deploying 5G network services on Kubernetes - Orange-OpenSource/towards5gs-helm
github.com
kubectl get pods -n free5gc -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'
free5gc-free5gc-amf-... towards5gs/free5gc-amf:latest
free5gc-free5gc-smf-... towards5gs/free5gc-smf:latest
...
towards5gs/free5gc-*:latest 이미지는 v3.3 계열이다.
그리고 v4.0.0은 Release/r17 (SBI only)로 전면 리팩토링된 버전이라, 단순 이미지 태그 교체로는 절대 안 된다.
config 포맷과 NF 간 인터페이스가 함께 바뀌었기 때문이다.
따라서 선택 가능한 옵션은 세 가지인 상황이었다.
| 옵션 | 난이도 | Prometheus 지원 |
| 현재 유지 (towards5gs v3.3) | 없음 | ❌ |
| free5gc-compose (Docker) v4로 전환 | 높음 | ✅ (단, K8s 포기) |
| v4.2.2용 Helm Chart 직접 작성 | 매우 높음 | ✅ |
K8s 기반 테스트베드(Multus, OAI RAN 연동, FlexRIC E2)를 유지해야 했으므로, 마지막 옵션을 선택했다.
무엇을 참고할 것인가
free5gc 메인 레포에는 Dockerfile이 없다
git clone --branch v4.2.2 --depth 1 https://github.com/free5gc/free5gc.git free5gc-v4
find free5gc-v4 -name "Dockerfile*" -o -name "docker-compose*"
# → 아무것도 없음
.gitmodules를 보니 각 NF가 개별 레포의 git submodule이었다. 이미지 빌드는 free5gc-compose 레포가 담당한다.
git clone --depth 1 https://github.com/free5gc/free5gc-compose.git free5gc-compose-v4
여기서 세 가지 핵심 정보를 얻을 수 있다
- 모든 NF가 port 9091에서 metrics 노출 — prometheus.yml의 scrape target 확인
- Docker Hub에 prebuilt 이미지 존재 — free5gc/<nf>:v4.2.2 형태
- config 포맷은 v3와 "거의" 호환된다.
최종 설계 결정
기존 towards5gs-helm의 구조(NF별 subchart)를 그대로 따르되, 내용물만 v4에 맞게 재작성하기로 했다.
항목 결정
| 항목 | 결정 |
| Chart 구조 | Parent chart + NF별 subchart (towards5gs 방식) |
| 이미지 | Docker Hub 공식 free5gc/<nf>:v4.2.2 |
| 배포 방식 | 별도 namespace(free5gc-v4)에 병렬 배포 후 전환 |
| NF 범위 | NRF, AMF, SMF, UPF, AUSF, NSSF, PCF, UDM, UDR, WebUI (+ CHF/NEF는 optional) |
| 메트릭 | 모든 NF port 9091 + ServiceMonitor 자동 등록 |
| 네트워크 | Multus ipvlan (N2/N3/N4/N6) conditional rendering |
| TLS | PV 대신 Secret 기반 (v3는 cert를 PV로 마운트했음) |
| MongoDB | bitnami subchart |
따라서 최종 구조는 다음과 같다.
free5gc-v4-helm/
├── Chart.yaml
├── values.yaml # global 설정 (PLMN, SBI, metrics, snssai...)
├── templates/
│ ├── _helpers.tpl # nrfUri, mongodbUri, labels 헬퍼
│ ├── tls-secret.yaml
│ └── networks/ # NetworkAttachmentDefinition (N2/N3/N4/N6)
└── charts/
├── free5gc-nrf/
│ └── templates/{deployment,configmap,service,servicemonitor}.yaml
├── free5gc-amf/
├── free5gc-smf/
├── free5gc-upf/
└── ... (ausf, nssf, pcf, udm, udr, webui)
NRF 하나를 제대로 만들면 나머지 CP NF는 사실상 복제 + config 차이만 반영하면 되므로, NRF를 레퍼런스 패턴으로 잡았다.
배포 절차의 변화
기존 v3의 배포 순서는 다음과 같다.
kubectl apply -f persistent-vol-for-mongodb.yaml # MongoDB PV
kubectl apply -f persistent-vol-for-cert.yaml # Cert PV
helm install free5gc . -n free5gc
Free5gc-v4의 배포는 다음과 같이 변경되었다.
kubectl create namespace free5gc-v4
kubectl apply -f free5gc-v4-helm/pv-mongodb.yaml # MongoDB PV만
helm dependency update free5gc-v4-helm/
helm install free5gc free5gc-v4-helm/ -n free5gc-v4
Cert PV가 사라진 이유는 v3는 인증서를 공유 PV에 두고 모든 NF가 마운트했다.
v4 chart에서는 TLS Secret으로 관리하고, global.sbi.scheme: http인 경우 Secret 자체가 렌더링되지 않으며 Deployment에서 optional: true로 마운트하므로 없어도 에러가 나지 않는다.
트러블슈팅 과정
다음은 시간순으로 만난 문제들이다.
문제 1 — WebUI 이미지 이름이 다르다
Failed to pull image "docker.io/free5gc/webconsole:v4.2.2":
reading manifest v4.2.2 in docker.io/free5gc/webconsole: manifest unknown
소스 레포의 바이너리 이름(webconsole)을 그대로 이미지 이름으로 썼는데, Docker Hub에는 다른 이름으로 올라와 있었다.
sudo crictl pull docker.io/free5gc/webconsole:v4.2.2 # ❌
sudo crictl pull docker.io/free5gc/webui:v4.2.2 # ✅
해결: 이미지를 free5gc/webui:v4.2.2로 변경. 그런데 이미지를 바꾸자 다음 에러가 나왔다.
Error: container create failed: executable file `/free5gc/./webconsole` not found
이미지 이름만 바꾸고 command는 그대로 뒀던 것. **이미지 안의 바이너리 이름도 webui**였다.
command: ["./webui"] # ./webconsole 아님
💡 레포의 모듈명 ≠ Docker 이미지명 ≠ 컨테이너 내부 바이너리명. 셋 다 따로 확인해야 한다.
문제 2 — bitnami/mongodb 태그가 사라졌다
Failed to pull image "docker.io/bitnami/mongodb:6.0.4-debian-11-r10":
reading manifest 6.0.4-debian-11-r10: manifest unknown
bitnami가 이미지 배포 정책을 바꾸면서 과거 버전 태그들이 정리된 것으로 보인다.
bitnami subchart의 기본 image.tag가 존재하지 않는 태그를 가리키고 있었다.
해결: values에서 MongoDB 이미지 태그를 명시적으로 오버라이드.
프로덕션이라면 latest가 아니라 실제로 존재하는 고정 태그를 미러링해서 쓰는 게 맞다.
우선 Demo 버전이므로 재현성 관점에서 이건 기술 부채로 남겨뒀다.
문제 3 — NRF: Scheme: non zero value required
가장 오래 헤맨 에러 중 하나.
[INFO][NRF][CFG] Read config from [./config/nrfcfg.yaml]
[ERRO][NRF][CFG] Scheme: non zero value required
[ERRO][NRF][Main] NRF Run Error: Config validate Error
렌더된 ConfigMap을 확인해보니 sbi.scheme: http는 분명히 들어가 있었다.
configuration:
sbi:
scheme: http # ← 있음!
registerIPv4: free5gc-free5gc-nrf-sbi
bindingIPv4: 0.0.0.0
port: 8000
...
metrics:
enable: true
bindingIPv4: 0.0.0.0 # ← 여기가 문제
port: 9091
free5gc-compose의 원본 nrfcfg.yaml을 열어보고 나서야 원인을 알았다.
v4.2.2에서는 metrics 블록에도 scheme이 필수(Required) 필드다.
에러 메시지가 sbi.Scheme인지 metrics.Scheme인지 구분해주지 않아서,
config 검증 실패의 원인이 SBI 쪽이라고 착각했던 것이다.
해결: 모든 NF ConfigMap의 metrics 블록에 scheme 추가.
metrics:
enable: true
scheme: http # ← 추가
bindingIPv4: 0.0.0.0
port: 9091
grep -rl "metrics:" free5gc-v4-helm/charts/*/templates/configmap.yaml
# amf, ausf, nrf, nssf, pcf, smf, udm, udr → 8개 전부 수정
💡 Free5GC의 config validator 에러 메시지는 필드명만 알려주고 경로를 알려주지 않는다.
같은 필드명이 여러 블록에 존재할 수 있다는 걸 항상 의심할 것.
그리고 config 문제는 항상 free5gc-compose/config/*.yaml 원본과 diff를 뜨는 게 가장 빠르다.
문제 4 — NRF: open cert/root.key: read-only file system
[WARN][NRF][Init] No root private key: privkey read: open cert/root.key:
no such file or directory; generate new one
[ERRO][NRF][Init] NRF init: generate rsa priv key create file:
open cert/root.key: read-only file system
v4의 NRF는 root key가 없으면 런타임에 직접 생성한다.
그런데 우리는 cert를 Secret으로 마운트했고, Secret 볼륨은 read-only다.
그래서 NRF가 키를 쓰려다 실패하고 죽는다.
v3(towards5gs)에서는 cert를 PV로 마운트했기 때문에 쓰기가 가능했고, 이 문제가 없었다.
Secret으로 바꾼 설계 결정이 여기서 문제가 된 것!
해결: http 스킴을 쓰는 현재 구성에서는 cert가 실질적으로 필요 없으므로, cert 마운트를 writable emptyDir로 변경.
volumes:
- name: tls-certs
emptyDir: {} # secret 대신
NRF가 필요하면 자기가 생성하도록 두고, Pod 재시작 시 새로 만들면 된다.
모든 CP NF(NRF, AMF, SMF, AUSF, NSSF, PCF, UDM, UDR)에 동일 적용.
💡 "인증서는 Secret에" 는 일반적으로 옳지만, 애플리케이션이 인증서를 스스로 생성하려 드는 경우 는 예외다.
마운트가 read-only인지 writable인지가 설계 제약이 된다.
문제 5 — SMF: uerouting.yaml: no such file or directory
[ERRO][SMF][Main] SMF Run error: ReadConfig [] Error:
open ./config/uerouting.yaml: no such file or directory
v4의 SMF는 smfcfg.yaml 외에 uerouting.yaml을 필수로 읽는다. ULCL을 안 쓰더라도 파일 자체는 있어야 한다.
해결: SMF ConfigMap에 uerouting.yaml 키를 추가.
그리고 곧바로 다음 에러:
[ERRO][SMF][CFG] invalid Version: 1.0.1 does not validate as in(1.0.7)
uerouting.yaml의 info.version이 정확히 1.0.7이어야 한다.
validator가 in(1.0.7) — 즉 허용값 목록에 정확히 매칭되는지 검사한다.
해결: version을 1.0.7로 수정.
💡 Free5GC의 config version 필드는 장식이 아니라 enum validator다. 원본 config의 version을 그대로 복사해야 한다.
문제 6 — PCF: invalid ServiceName: npcf-callback does not validate as service
[ERRO][PCF][CFG] invalid ServiceName: npcf-callback does not validate as service
재밌는 건, free5gc-compose의 원본 pcfcfg.yaml에는 npcf-callback이 실제로 들어 있다는 점이다. 문제는 구조였는데,
v4의 PCF config는 serviceNameList(문자열 배열)가 아니라 serviceList(객체 배열, serviceName 키를 가짐) 구조를 쓴다.
우리가 NRF 패턴을 복사하면서 serviceNameList 형식으로 잘못 렌더링했고,
validator가 이를 서비스명으로 해석하려다 실패한 것이다.
해결: PCF ConfigMap을 원본의 serviceList 구조로 수정.
💡 "NRF 패턴을 복제한다"는 전략은 효율적이지만, NF마다 config 스키마가 미묘하게 다르다.
복제 후에는 반드시 원본 config와 대조해야 한다.
문제 7 — 전부 Running인데 계속 Restart: Health Probe 404
이 시점에서 모든 NF의 로그가 정상이었다.
[INFO][AMF][SBI] Start SBI server (listen on 0.0.0.0:8000)
[INFO][AMF][Init] Start Metrics server (listen on 0.0.0.0:9091)
[INFO][AMF][Ngap] Listen on 192.168.56.240:38412
[INFO][SMF][Main] Received PFCP Association Setup Accepted Response from UPF
서비스는 완벽하게 동작하는데 Pod가 0/1 Running에서 계속 restart 카운트가 올라갔다.
Readiness probe failed: HTTP probe failed with statuscode: 404
Liveness probe failed: HTTP probe failed with statuscode: 404
그리고 NF 로그는 다음과 같이 찍혔다.
[INFO][AMF][GIN] | 404 | 172.16.106.131 | GET | / |
원인은 Free5GC NF들이 root path /에 핸들러가 없기 때문!
SBI는 /namf-comm/v1/... 같은 경로만 서빙한다.
probe가 GET /를 때리니 404가 오고, kubelet은 이를 실패로 판정해서 컨테이너를 죽인다.
해결: probe를 비활성화 (임시).
probes:
enabled: false
더 나은 해결: HTTP probe 대신 TCP socket probe를 쓰는 것.
readinessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 5
livenessProbe:
tcpSocket:
port: 8000
포트가 열려 있는지만 확인하므로 404 문제가 없고, "프로세스가 살아서 리스닝 중"이라는 최소한의 보장은 된다.
💡 "CrashLoopBackOff이 아닌데 restart가 쌓인다" = 거의 probe 문제다.
로그가 정상인데 Pod가 안 뜨면 로그가 아니라 kubectl describe pod의 Events를 봐야 한다.
문제 8 — subchart를 고쳤는데 반영이 안 된다
helm template으로 렌더링하면 NRF Deployment가 멀쩡히 나오는데, 실제 클러스터에는 NRF Deployment가 아예 생성되지 않는 상황이 발생했다.
kubectl get deployments -n free5gc-v4
# amf, nssf, pcf, udm, udr, webui는 있는데 nrf, smf, upf, ausf가 없음
helm template test ./free5gc-v4-helm/ | grep "free5gc-nrf"
# → 정상 렌더링됨 (!)
원인: subchart 파일을 수정한 뒤 helm dependency update를 실행하지 않았다.
Helm이 이전에 패키징된 charts/*.tgz 캐시를 쓰고 있었던 것.
해결:
helm uninstall free5gc -n free5gc-v4
helm dependency update ./free5gc-v4-helm/ # ← 필수
helm install free5gc ./free5gc-v4-helm/ -n free5gc-v4
💡 subchart를 직접 수정하는 개발 사이클에서는 helm dependency update를 매번 실행하는 습관을 들여야 한다.
helm template은 로컬 디렉토리를 읽지만 helm install은 패키징된 chart를 쓸 수 있어서, 둘의 결과가 갈릴 수 있다.
문제 9 — UPF: gtp5g 커널 모듈 버전 불일치
[INFO][UPF][Main] starting Gtpu Forwarder [gtp5g]
[ERRO][UPF][Main] UPF Cli Run Error: open Gtp5g: version mismatch:
gtp5g version(0.8.9) should be 0.9.5 <= version < 0.11.0, please update it
이건 Helm chart 문제가 아니다.
UPF는 커널 모듈 gtp5g에 의존하는데, free5gc-up 노드에 설치된 버전이 v3 시절의 0.8.9였다.
v4의 UPF는 0.9.5 이상을 요구한다.
해결: 노드에서 직접 커널 모듈 업데이트.
ssh user@
cd /tmp
git clone https://github.com/free5gc/gtp5g.git
cd gtp5g
git checkout v0.9.16
make && sudo make install
sudo rmmod gtp5g 2>/dev/null
sudo modprobe gtp5g
kubectl rollout restart deployment free5gc-free5gc-upf -n free5gc-v4
업데이트 후 UPF 로그:
[INFO][UPF][PFCP][LAddr:10.100.50.242:8805] handleAssociationSetupRequest
[INFO][UPF][PFCP][LAddr:10.100.50.242:8805][CPNodeID:10.100.50.244] New node
[INFO][UPF][PFCP][LAddr:10.100.50.242:8805] handleHeartbeatRequest
PFCP Association 성립. ✅
💡 컨테이너화해도 커널 모듈 의존성은 노드에 남는다.
UPF는 Kubernetes로 완전히 추상화되지 않는 컴포넌트다.
이건 Helm chart에 initContainer로 버전 체크를 넣어 조기에 fail-fast 시키는 게 좋겠다는 생각이 들었다.
문제 10 — gNB ↔ AMF SCTP 연결: hostNetwork 재현
v3에서 이미 한 번 겪었던 문제인데, v4 chart를 새로 짜면서 Multus 기반으로 되돌려 놓는 바람에 재발했다.
OAI gNB는 192.168.56.30/24에 있다.
반면 chart 기본값의 AMF N2 인터페이스는 Multus ipvlan으로 10.100.50.249/29가 할당된다.
Multus ipvlan L2 모드는 master interface와 같은 L2 네트워크에 Pod의 secondary interface를 붙이는 방식이다.
직접 L2 통신을 하려면 Pod IP도 실제 L2 네트워크와 같은 서브넷에 있어야 한다.
master interface(ens34)는 192.168.56.0/24인데 AMF N2 IP는 10.100.50.0/29 → 서로 다른 서브넷 → L2 직접 도달 불가 → SCTP 연결 실패.
oai-ran: 192.168.56.30/24
AMF N2: 10.100.50.249/29 ← 도달 불가
NodePort는 답이 아니다.
SCTP는 multi-homing과 association 관리 때문에 kube-proxy 기반 NodePort와 궁합이 나쁘고,
gNB가 바라보는 AMF 주소가 실제 N2 주소가 아니라 node IP가 되는 것도 구조적으로 깔끔하지 않다.
해결: AMF에 hostNetwork: true 적용. Docker Compose 환경에서 성공했던 구조를 K8s에서 재현하는 방향이다.
spec:
template:
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet # ← 필수!
dnsPolicy가 중요한데, hostNetwork Pod는 기본적으로 노드의 /etc/resolv.conf를 쓰기 때문에, free5gc-free5gc-nrf-sbi 같은 K8s 내부 service name을 resolve하지 못한다.
ClusterFirstWithHostNet으로 CoreDNS를 우선 사용하게 해야 SBI 통신이 정상 동작한다.
이렇게 하면 AMF가 free5gc-cp 노드의 네트워크 네임스페이스를 직접 사용하므로, 노드 IP에서 SCTP 38412를 직접 listen한다. 별도의 Multus N2 인터페이스도, NodePort 서비스도 불필요해진다.
AMF 로그:
[INFO][AMF][Ngap] Listen on 192.168.56.51:38412
[INFO][AMF][Ngap] [AMF] SCTP Accept from: 192.168.56.30:38103
[INFO][AMF][Ngap] Create a new NG connection for: 192.168.56.30:38103
[INFO][AMF][Ngap] Handle NGSetupRequest
[INFO][AMF][Ngap] Send NG-Setup response
gNB 로그:
[NGAP] Received NGSetupResponse from AMF
[GNB_APP] [gNB 0] Received NGAP_REGISTER_GNB_CNF: associated AMF 1
[E2-AGENT]: E2 SETUP RESPONSE rx ← FlexRIC 연동도 정상
N2/SCTP 연결 성립. ✅
부수 현상: AMF의 NRF 등록 실패 로그는 정상이다
[ERRO][AMF][Consumer] AMF register to NRF Error[Put "http://...nrf-sbi:8000/...":
dial tcp 10.101.234.57:8000: connect: connection refused]
... (반복) ...
[INFO][AMF][Main] OAuth2 setting receive from NRF: true
[INFO][AMF][SBI] Start SBI server (listen on 0.0.0.0:8000)
AMF가 NRF보다 먼저 뜨면 초반에 connection refused가 쏟아진다.
하지만 AMF는 자동으로 재시도하고, NRF가 Ready되면 등록에 성공한다. 이건 기동 순서로 인한 정상 동작이므로 무시 가능! (initContainer로 NRF wait를 넣으면 로그는 깨끗해지지만 본질적 문제는 아니다.)
문제 11 — WebUI 로그인이 계속 튕긴다: OAuth2/JWT
WebUI(포트 30500)에 접속해서 admin/free5gc로 로그인하면 로그인 화면으로 되돌아왔다.
로그를 보니 로그인 자체는 성공하고 있었다.
[INFO][WEBUI][Proc] Login success { username: admin, userid: 48e9414f-..., tenantid: ... }
[INFO][WEBUI][GIN] | 200 | POST | /api/login |
그런데 바로 다음 API 호출에서:
[INFO][WEBUI][Consumer] GetTokenCtx: NRF nnrf-disc
[INFO][WEBUI][Consumer] GetTokenCtx: AMF namf-oam
[ERRO][WEBUI][Proc] GetTenantId error: ParseJWT error: token is malformed
[INFO][WEBUI][GIN] | 401 | GET | /api/registered-ue-context |
원인: v4에서 NRF에 oauth: true가 설정되어 있으면, WebUI도 NRF에서 access token을 발급받아야 다른 NF(AMF 등)에 API를 호출할 수 있다. 토큰 발급/검증이 제대로 안 되니 401이 돌아오고, 프론트엔드가 401을 "세션 만료"로 해석해서 로그인 화면으로 리다이렉트하는 것이었다.
즉, "로그인 실패"가 아니라 "로그인 후 첫 API 호출 실패" 였다.
해결: 테스트베드 환경이므로 NRF config에서 OAuth를 끔.
# nrfcfg.yaml
sbi:
oauth: false
NRF가 oauth: false를 브로드캐스트하면 다른 NF들도 토큰 없이 동작한다.
[INFO][UDR][Main] OAuth2 setting receive from NRF: false
[INFO][UDR][Init] register to NRF successfully
⚠️ NRF의 oauth 설정을 바꾼 뒤에는 모든 NF Pod를 재시작해야 한다.
NF는 기동 시 NRF에서 oauth 설정을 받아 캐시하기 때문이다.kubectl rollout restart deployment -n free5gc-v4
프로덕션이라면 OAuth를 켜고 인증서를 제대로 셋업하는 게 맞다. 이건 우회한 것뿐!
문제 12 — UE Registration 실패: 슬라이스와 가입자 데이터의 미로
여기서부터가 핵심. 특히 Intent 중 Slice와 관련된 Intent를 다룰 때 어떻게 다뤄야 하는지 파악할 수 있었다.
트러블슈팅 과정에서는 각 단계마다 다른 에러가 나오면서 NAS 절차의 각 단계를 하나씩 통과해 나갔다.
12-1. 401 is not a valid status code — 가입자가 없다
[WARN][AMF][Gmm] Nausf_UEAU Authenticate Request Failed:
{Title:System failure Status:500 Detail:401 is not a valid status code in UeAuthenticationsPost}
[INFO][AMF][Gmm] Send Registration Reject
[WARN][AMF][Gmm] Authentication procedure failed
v4는 새 MongoDB를 쓰므로 가입자 정보가 비어 있다.
v3의 towards5gs-helm에는 dbpython이라는 Pod가 초기 가입자 데이터를 자동으로 넣어줬는데, 우리 chart에는 그게 없다.
해결: WebUI에서 가입자(IMSI/K/OPC) 등록.
12-2. MANDATORY_IE_MISSING — S-NSSAI가 안 맞는다
[INFO][AMF][Gmm] Handle event[SecurityMode Success],
transition from [SecurityMode] to [ContextSetup]
[ERRO][AMF][Gmm] communicateWithUDM error: MANDATORY_IE_MISSING
[ERRO][AMF][Gmm] communicateWithUDM failed: MANDATORY_IE_MISSING
Authentication은 통과했는데 UDM 통신에서 실패. UDR 로그를 보니 am-data 조회 자체는 200이 나왔다.
[INFO][UDR][GIN] | 200 | GET | /nudr-dr/v2/subscription-data/imsi-208930000000001/20893/provisioned-data/am-data |
MongoDB를 직접 열어봤다.
db.subscriptionData.provisionedData.amData.find().pretty()
{
ueId: 'imsi-208930000000001',
servingPlmnId: '20893',
gpsis: [ 'msisdn-' ],
subscribedUeAmbr: { uplink: '1 Gbps', downlink: '2 Gbps' },
nssai: { defaultSingleNssais: [ { sst: 1, sd: '000001' } ] } // ← SD가 000001
}
그런데 우리 Helm chart의 global.snssai는 sd: 010203이었다.
AMF는 sst:1, sd:010203을 지원하는데 가입자는 sst:1, sd:000001로 등록되어 있으니 매칭 실패.
towards5gs-helm의 값을 확인해보니:
grep -r "000001\|010203" towards5gs-helm/charts/free5gc/
charts/free5gc-amf/values.yaml: sd: 000001, 000002, 000003
charts/free5gc-nssf/values.yaml: sd: 000001, 000002, 000003
charts/free5gc-upf/values.yaml: cidr: 10.60.0.0/16 # Slice 1 (sst:1, sd:000001)
towards5gs-helm은 000001/000002/000003을 쓴다.
반면 free5gc-compose의 원본 config는 010203/112233을 쓴다.
나는 원본 config를 그대로 복사했고, WebUI는 towards5gs를 했던 상황.
해결: chart의 global.snssai를 000001/000002/000003으로 통일. AMF, SMF, NSSF, UPF의 S-NSSAI를 전부 맞춤.
💡 S-NSSAI(SST/SD)는 AMF config, SMF config, NSSF config, UPF config, 그리고 MongoDB 가입자 데이터 5곳에서 완전히 일치해야 한다. 한 곳이라도 어긋나면 MANDATORY_IE_MISSING이라는 에러가 출력된다.
12-3. FATA: overlap cidr value between UPFs — DNN 이름을 나눠야 한다
3개 슬라이스를 살리려고 했더니 SMF가 아예 기동을 거부했다.
[FATA][SMF][Init] overlap cidr value between UPFs
3개 슬라이스가 전부 dnn: internet 을 쓰면서 서로 다른 CIDR(10.60/16, 10.61/16, 10.62/16)을 할당하고 있었다.
v4의 SMF는 같은 DNN 이름에 대해 여러 UPF/CIDR이 등록되면 overlap으로 판단한다.
해결: 슬라이스별로 DNN 이름을 분리.
| Slice | S-NSSAI | DNN | CIDR | 5QI |
| eMBB | sst:1, sd:000001 | eMBB | 10.60.0.0/16 | 9 |
| URLLC | sst:2, sd:000002 | URLLC | 10.61.0.0/16 | 7 (priority 2) |
| mMTC | sst:3, sd:000003 | mMTC | 10.62.0.0/16 | 9 (AMBR 100Mbps) |
DNN 이름을 바꾸면 다음 5곳을 전부 수정해야 한다:
- SMF snssaiInfos / dnnInfos
- SMF userplaneInformation의 UPF interfaces / networkInstances
- UPF dnnList
- AMF supportDnnList
- MongoDB 가입자의 dnnConfigurations + OAI UE config의 DNN
12-4. 403 Forbidden → no subscription pdu session type info
Registration은 마침내 성공했다!
[INFO][AMF][Gmm] Handle Registration Complete
[INFO][AMF][Gmm] transition from [ContextSetup] to [Registered]
그런데 PDU Session 생성 단계에서:
[INFO][AMF][Gmm] Select SMF [snssai: {Sst:1 Sd:000001}, dnn: eMBB]
[ERRO][AMF][Gmm] CreateSmContextRequest Error: 403, Forbidden
AMF 로그만 보면 원인을 알 수 없다. SMF 로그를 봐야 한다.
[ERRO][SMF][CTX] this SMContext[urn:uuid:...] has no subscription pdu session type info
[ERRO][SMF][PduSess] PDU Session Establishment fail by gsm error cause[50]
[INFO][SMF][GIN] | 403 | POST | /nsmf-pdusession/v1/sm-contexts |
MongoDB의 smData를 확인:
db.subscriptionData.provisionedData.smData.find({ueId: "imsi-208930000000001"}).pretty()
{
singleNssai: { sst: 1, sd: '000001' },
ueId: 'imsi-208930000000001',
servingPlmnId: '20893'
// dnnConfigurations가 통째로 없다!
}
WebUI v4.2.2가 가입자를 등록할 때 dnnConfigurations를 채워주지 않았다.
S-NSSAI만 있고 session type, SSC mode, QoS, AMBR이 전부 비어 있다.
해결: MongoDB에 직접 주입.
db.subscriptionData.provisionedData.smData.updateOne(
{ ueId: "imsi-208930000000001", servingPlmnId: "20893" },
{ $set: {
dnnConfigurations: {
eMBB: {
pduSessionTypes: {
defaultSessionType: "IPV4",
allowedSessionTypes: ["IPV4"]
},
sscModes: {
defaultSscMode: "SSC_MODE_1",
allowedSscModes: ["SSC_MODE_1", "SSC_MODE_2", "SSC_MODE_3"]
},
"5gQosProfile": {
"5qi": 9,
arp: { priorityLevel: 8, preemptCap: "", preemptVuln: "" }
},
sessionAmbr: { uplink: "1000 Mbps", downlink: "1000 Mbps" }
}
}
}}
);
URLLC(5QI=7, priority 2), mMTC(5QI=9, AMBR 100Mbps)도 동일 패턴으로 주입.
12-5. Can't find UE SM Policy Data in UDR
[ERRO][SMF][PduSess] set sm policy association failed: 400, Bad Request
{ 400 Can't find UE SM Policy Data in UDR USER_UNKNOWN }
이번엔 PCF가 UDR에서 SM Policy Data를 찾지 못한다. 이것도 WebUI가 안 만들어준다.
해결: policyData.ues.smData 컬렉션에 직접 주입.
db.policyData.ues.smData.updateOne(
{ ueId: "imsi-208930000000001" },
{ $set: {
ueId: "imsi-208930000000001",
smPolicySnssaiData: {
"01000001": { // SST(2자리 hex) + SD
snssai: { sst: 1, sd: "000001" },
smPolicyDnnData: { eMBB: { dnn: "eMBB" } }
}
}
}},
{ upsert: true }
);
💡 v4.2.2의 WebUI는 가입자 데이터를 완전히 프로비저닝하지 않는다. 최소 3개 컬렉션이 필요하다:
- subscriptionData.provisionedData.amData — Access & Mobility (WebUI가 채움)
- subscriptionData.provisionedData.smData — Session Management (dnnConfigurations 누락!)
- policyData.ues.smData — SM Policy (WebUI가 아예 안 만듦)
towards5gs-helm의 dbpython Pod가 이걸 자동으로 해주던 이유가 있었다.
우리 chart에도 가입자 프로비저닝 Job을 추가하는 게 다음 과제다.
12-6. PDU Session 생성 성공, 그리고 PFCP
[INFO][SMF][PduSess] SDM Subscription Successful UE: imsi-208930000000001
[INFO][SMF][GIN] | 201 | POST | /nsmf-pdusession/v1/sm-contexts | ← 201 Created!
[INFO][AMF][Gmm] create smContext[pduSessionID: 10] Success
[INFO][AMF][Ngap] Send PDU Session Resource Setup Request
PDU Session 생성 성공 (201). IP도 할당됐다 (10.60.0.1).
이 시점에 나온 경고들:
[WARN][SMF][Consumer] BSF selection failed: no BSF instances found
[ERRO][SMF][PduSess] chf selection error: no CHF found in CHFSelection
→ BSF/CHF를 배포하지 않았기 때문. PDU Session 자체는 NRF fallback으로 진행되므로 무해하다.
그리고 UPF 쪽 PFCP 이슈:
[ERRO][UPF][PFCP] handleSessionModificationRequest: Sess: invalid lSeid:0
[WARN][SMF][PduSess] Sending PFCP Session Modification Request to AN UPF error:
received unexpected SEID response message
[ERRO][SMF][CTX] Can not get interface
Can not get interface는 SMF의 userplaneInformation에서 UPF의 N3 인터페이스 endpoint와 networkInstances(DNN)가 정합하지 않을 때 나온다.
DNN을 internet → eMBB로 바꾸면서 SMF의 UPF interface 정의를 함께 갱신해야 했다.
이 정합성을 맞추고 gtp5g를 업데이트한 뒤 UPF PFCP는 정상화됐다.
kubectl get configmap -n free5gc-v4 free5gc-free5gc-smf-config \
-o jsonpath='{.data.smfcfg\.yaml}' | grep -A10 "userplaneInformation"
결과: 실제로 무엇을 볼 수 있게 되었나
이게 지금까지의 Migration의 최종 목적이었다. AMF의 /metrics를 찔러봤다.
kubectl exec -n free5gc-v4 $(kubectl get pods -n free5gc-v4 \
-l app.kubernetes.io/name=free5gc-amf -o jsonpath='{.items[0].metadata.name}') \
-- wget -qO- http://localhost:9091/metrics
# HELP free5gc_amf_business_ue_connectivity Number of user equipment that are connected to the core network
free5gc_amf_business_ue_connectivity{access_type="3GPP_ACCESS"} 3
# HELP free5gc_amf_business_ue_gmm_state_count Current number of UEs in each 5GMM state in the AMF
free5gc_amf_business_ue_gmm_state_count{access_type="3GPP_ACCESS",gmm_state="Registered"} 3
free5gc_amf_business_ue_gmm_state_count{access_type="3GPP_ACCESS",gmm_state="Authentication"} 0
# HELP free5gc_amf_business_active_pdu_session_current_count Number of PDU Session currently active
free5gc_amf_business_active_pdu_session_current_count{access_type="3GPP_ACCESS"} 3
# HELP free5gc_amf_business_ue_gmm_state_duration_seconds Duration that UEs spend in a given GMM state
free5gc_amf_business_ue_gmm_state_duration_seconds_sum{gmm_state="Authentication"} 0.3875
free5gc_amf_business_ue_gmm_state_duration_seconds_count{gmm_state="Authentication"} 3
이제 "AMF가 현재 UE 3개 서빙 중, 활성 PDU Session 3개, Authentication 단계 평균 체류 시간 0.13초" 를 Prometheus 쿼리로 직접 볼 수 있게 됐다.
노출 메트릭 확인
모든 NF의 /metrics를 훑어서 정리했다.
AMF — 비즈니스 메트릭 (가장 풍부)
메트릭 타입 설명
| 메트릭 | 타입 | 설명 |
| free5gc_amf_business_ue_connectivity | gauge | 연결된 UE 수 (cm-connected + gmm-registered) |
| free5gc_amf_business_ue_cm_gmm_state_count | gauge | CM 상태별 UE 수 (CM_IDLE / CM_CONNECTED) |
| free5gc_amf_business_ue_gmm_state_count | gauge | 5GMM 상태별 UE 수 (Registered / Authentication / SecurityMode / ContextSetup) |
| free5gc_amf_business_ue_gmm_state_duration_seconds | histogram | 각 GMM 상태 체류 시간 |
| free5gc_amf_business_ue_gmm_transitions_total | counter | GMM 상태 전이 횟수 |
| free5gc_amf_business_active_pdu_session_current_count | gauge | 활성 PDU Session 수 (AMF 관점) |
| free5gc_amf_business_pdu_session_events_total | counter | PDU Session event (creation / release / modification) |
| free5gc_amf_business_handover_current_count | gauge | 핸드오버 진행 중 UE 수 (ngap / xn) |
AMF — 프로토콜 메트릭
메트릭 타입 설명
| 메트릭 | 타입 | 설명 |
| free5gc_nas_msg_received_total | counter | NAS 수신 메시지 수 |
| free5gc_nas_nas_msg_sent_total | counter | NAS 송신 메시지 수 |
| free5gc_ngap_msg_received_total | counter | NGAP 수신 메시지 수 |
| free5gc_ngap_msg_sent_total | counter | NGAP 송신 메시지 수 |
모든 NF 공통 — SBI 메트릭
메트릭 타입 설명
| 메트릭 | 타이 | 설명 |
| free5gc_sbi_inbound_request_total | counter | SBI 수신 요청 수 |
| free5gc_sbi_inbound_request_duration_seconds | histogram | SBI 수신 요청 latency |
| free5gc_sbi_outbound_request_total | counter | SBI 발신 요청 수 |
| free5gc_sbi_outbound_request_duration_seconds | histogram | SBI 발신 요청 latency |
Go Runtime — 모든 NF 기본 포함
go_goroutines, go_gc_duration_seconds, go_memstats_heap_alloc_bytes, process_cpu_seconds_total, process_resident_memory_bytes 등
여전히 확인할 수 없는 Metric
| NF | 비즈니스 메트릭 | SBI 메트릭 | 프로토콜 메트릭 |
| AMF | ✅ 풍부 (UE 수, PDU, handover, GMM 상태) | ✅ | ✅ (NAS, NGAP) |
| SMF | ❌ 미구현 | ✅ | ❌ |
| NRF | ❌ | ✅ (inbound만) | ❌ |
| AUSF / PCF / UDM / UDR / NSSF | ❌ | ✅ | ❌ |
| UPF | ❌ (metrics port 미노출) | ❌ | ❌ |
확인할 수 있게 된 Metric들:
- ✅ AMF의 UE 수, Registration/PDU 이벤트, GMM 상태 분포
- ✅ 모든 NF의 SBI HTTP latency / 성공·실패율
- ✅ Go runtime 메트릭
기대했지만 아직 없는 것:
- ❌ SMF의 활성 PDU Session 수, N4(PFCP) 메시지 처리량
- ❌ UPF의 GTP 터널 수, 패킷 처리량(PPS/BPS), drop 카운트
- ❌ NRF의 등록된 NF 인스턴스 수, Discovery 요청 수
Free5GC v4.2.2가 metrics framework는 도입했지만, NF-specific 비즈니스 메트릭은 현재 AMF에만 구현된 상태다. Prometheus metrics exporter by @Niahh PR이 AMF 중심으로 들어갔고, 나머지 NF는 공통 SBI 미들웨어 수준까지만 계측된 것으로 보인다.
그럼에도 이게 v3보다 압도적으로 낫다.
컨트롤 플레인 부하를 정량화할 수 있는 축이 생겼고, SBI latency 히스토그램만으로도 NF 간 병목을 짚어낼 수 있다.
사용 가능한 Grafana 쿼리
# 현재 연결된 UE 수
free5gc_amf_business_ue_connectivity{access_type="3GPP_ACCESS"}
# 활성 PDU Session 수
free5gc_amf_business_active_pdu_session_current_count
# PDU Session 생성률 (초당)
rate(free5gc_amf_business_pdu_session_events_total{event="creation"}[5m])
# GMM 상태 분포
free5gc_amf_business_ue_gmm_state_count
# Registration 절차 각 단계별 P95 소요 시간
histogram_quantile(0.95,
rate(free5gc_amf_business_ue_gmm_state_duration_seconds_bucket[5m]))
# NF별 SBI inbound P99 latency
histogram_quantile(0.99,
sum by (le, job) (rate(free5gc_sbi_inbound_request_duration_seconds_bucket[5m])))
# SBI 에러율
sum(rate(free5gc_sbi_inbound_request_total{status=~"4..|5.."}[5m]))
/ sum(rate(free5gc_sbi_inbound_request_total[5m]))
# NGAP 메시지 처리량
rate(free5gc_ngap_msg_received_total[1m])
잡음 하나
로그를 볼 때마다 마지막에 붙어 있던 이 메시지:
failed to create fsnotify watcher: too many open files
이건 Free5GC 에러가 아니다.
노드의 inotify watcher 한계에 도달한 것으로, kubectl logs가 로그를 tail할 때 발생한다.
Pod 수가 늘면서 kubelet, containerd, Alloy 등이 watcher를 소모한 결과다.
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances
sudo sysctl -w fs.inotify.max_user_watches=524288
sudo sysctl -w fs.inotify.max_user_instances=1024
echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf
echo "fs.inotify.max_user_instances=1024" | sudo tee -a /etc/sysctl.conf
NF 동작에는 영향이 없지만, 로그 tail이 간헐적으로 끊기므로 추후 수정하는 게 좋을듯.
정리하면
무엇이 가장 어려웠나
의외로 Helm chart 작성 자체는 어렵지 않았는데 까다로웠던 건 두 가지다.
- 에러 메시지가 원인을 가리키지 않는다.
Scheme: non zero value required는 어느 블록의 scheme인지 알려주지 않았고, MANDATORY_IE_MISSING은 어떤 IE인지 알려주지 않았으며, 403 Forbidden은 SMF 로그를 봐야만 진짜 원인(no subscription pdu session type info)이 나왔다.
NF 하나의 로그만 보면 절대 못 푼다. AMF → SMF → PCF → UDR을 체인으로 따라가야 한다. - 정합성이 여러 레이어에 걸쳐 있다. S-NSSAI 하나만 해도 AMF config, SMF config, NSSF config, UPF config, MongoDB amData, MongoDB smData, MongoDB policyData, 그리고 UE config까지 8곳이 일치해야 한다.
Helm의 global values로 앞의 넷은 묶었지만, MongoDB 쪽은 여전히 수동이다.
디버깅 체크리스트
Pod가 CrashLoopBackOff → 로그 첫 20줄. config validate 에러인지 확인
Pod가 Running인데 restart 누적 → probe 문제. describe pod의 Events 확인
NF는 뜨는데 UE registration 실패
├─ Authentication 실패 → 가입자(K/OPC) 미등록
├─ MANDATORY_IE_MISSING → S-NSSAI 불일치 (config ↔ MongoDB)
├─ 403 on CreateSmContext → SMF 로그 확인
│ ├─ no subscription pdu session type info → smData.dnnConfigurations 누락
│ └─ Can't find UE SM Policy Data → policyData.ues.smData 누락
└─ PFCP invalid lSeid → UPF gtp5g 버전 / SMF userplaneInformation 정합성
gNB SCTP 연결 실패 → Multus 서브넷 확인. hostNetwork 고려
subchart 수정이 반영 안 됨 → helm dependency update
남은 과제
- 가입자 프로비저닝 자동화 — towards5gs의 dbpython 같은 Helm Job으로 amData / smData / policyData를 chart values 기반으로 자동 주입. 지금은 MongoDB 수동 주입이라 재현성이 없다.
- Probe를 tcpSocket으로 복구 — 현재 probe를 꺼둔 상태라 hang된 NF를 감지하지 못한다.
- OAuth 복구 + TLS 셋업 — 현재 oauth: false, scheme: http. 보안 관점에서 부채.
- BSF / CHF subchart — SMF의 no BSF instances found / no CHF found 경고 제거.
- SMF / UPF 비즈니스 메트릭 — upstream에 기여할 여지가 있는 부분. UPF의 GTP 터널 수와 패킷 처리량은 사실상 upstream PR을 직접 쓰지 않으면 얻을 수 없다.
- v3 → v4 무중단 전환 — 현재는 별도 namespace 병렬 배포 후 NodePort(31412/30500) 충돌 때문에 v3를 내려야 한다.
결론
Free5GC v3.3(towards5gs-helm)에서 v4.2.2로 넘어가는 것은 "이미지 태그 교체"가 아니라 "Helm chart 재작성"
config 스키마, 인증서 처리 방식, OAuth, 가입자 데이터 스키마가 전부 바뀌었다.
결과는 꽤 만족스럽다. 이전에는 AMF Pod CPU 50%가 관측성의 전부였다면, 이제는 AMF가 UE 3개를 서빙 중이며, PDU Session 3개가 활성이고, Authentication 단계 평균 체류 시간은 0.13초라고 말할 수 있다.
5G 코어를 "리소스를 먹는 프로세스"가 아니라 "통신 시스템"으로 관측할 수 있게 된 것!
'Agentic AI 구축 > Agentic AI Intelligence Plane 구축' 카테고리의 다른 글
| Iperf3로 슬라이스별 UE 트래픽과 FlexRIC KPM 모니터링 구현하기 (0) | 2026.07.14 |
|---|---|
| 통신 도메인에서의 LLM의 한계와 GSMA의 Open-Telco 벤치마크 (0) | 2026.06.20 |
| Prometheus & Grafana & Loki를 활용한 RAN-CN E2E 모니터링 스택 구축 (0) | 2026.06.10 |
| Kubernetes 환경에서 OAI gNB와 Free5GC Cluster 연동 (0) | 2026.06.09 |
| Kubernetes 1.35 기반 서버 환경에서 Free5GC Cluster 구축 및 Troubleshooting (0) | 2026.06.08 |