실시간 영상은 왜 쿠버네티스에서 어려웠을까: WebRTC 스트리밍 아키텍처 개선기
글쓴이: 서지호 | DevOps Engineer, Plaidlabs
누비온 프로젝트에서 맡았던 과제 중 하나는 WebRTC 기반 영상을 웹에서 실시간으로 안정적으로 전달하는 환경을 만드는 일이었습니다.
요구사항만 보면 단순해 보입니다.
카메라에서 올라오는 영상을 받아 웹에서 보여주면 됩니다.
하지만 실제로는 WebRTC, NAT, 포트, 네트워크 경로, 쿠버네티스 운영 모델이 한 번에 얽히는 문제였습니다.
특히 우리가 풀어야 했던 문제는 단순히 "영상이 나온다"가 아니었습니다.
쿠버네티스 환경 안에서 운영 가능해야 했고, 연결 실패나 품질 저하가 발생했을 때 원인을 추적할 수 있어야 했으며, 장기적으로 유지보수 가능한 구조여야 했습니다.
이 글에서는 Kurento를 쿠버네티스에 직접 올리려다 겪었던 문제, Kurento 전용 VM으로 우회했던 이유, 그리고 이후 STUNner를 통해 더 쿠버네티스 네이티브한 방향으로 WebRTC 스트리밍 아키텍처를 개선해간 과정을 정리해보려 합니다.
문제는 영상 자체보다 네트워크였다
초기에는 미디어서버로 Kurento를 사용해 쿠버네티스 안에서 WebRTC 스트리밍 환경을 직접 구성하려고 했습니다.
WebRTC를 다루는 데 필요한 미디어 처리 기능을 고려하면 자연스러운 선택이었습니다. 하지만 구현을 진행할수록 진짜 어려운 지점은 미디어 처리 자체보다 네트워크에 있다는 사실이 분명해졌습니다.
일반적인 웹 서비스는 고정된 포트와 HTTP 중심의 트래픽을 기준으로 구성할 수 있습니다.
쿠버네티스에서도 Service, Ingress, Load Balancer 같은 익숙한 방식으로 외부 노출과 라우팅을 설명할 수 있습니다.
반면 WebRTC는 완전히 다른 성격의 트래픽을 가집니다.
연결을 맺는 과정에서 ICE candidate를 교환하고, NAT 환경을 넘기 위한 처리도 필요하며, 미디어 트래픽이 오가는 포트 역시 고정적으로 설명하기 어렵습니다.
Kurento를 실제로 쿠버네티스에 올려보면서 가장 크게 부딪힌 부분도 이 지점이었습니다. 영상 연결을 맺을 때 사용하는 포트가 계속 바뀌었고, 이를 클러스터 외부와 내부 네트워크 구조 안에서 안정적으로 제어하는 것이 쉽지 않았습니다.
컨테이너로 실행하는 것 자체는 가능했지만, 운영 환경 전체를 고려했을 때 예측 가능하고 관리 가능한 구조를 만드는 일은 전혀 다른 문제였습니다.
이때 처음 체감한 것은 "쿠버네티스에 배포할 수 있다"와 "쿠버네티스에서 운영하기 좋다"는 전혀 같은 말이 아니라는 점이었습니다. 특히 WebRTC처럼 네트워크 경로와 포트 전략이 중요한 워크로드는 더 그랬습니다.
그래서 처음에는 Kurento를 VM으로 분리했다
프로젝트 초기에 가장 현실적인 선택은 Kurento만 별도의 VM에서 운영하는 것이었습니다. 쿠버네티스 안에는 Spring Boot 애플리케이션을 두고, 실제 미디어 처리는 VM 위의 Kurento가 담당하도록 역할을 나눴습니다.
이 구조를 선택한 이유는 명확했습니다. VM에서는 미디어용 포트 범위를 더 직접적으로 관리할 수 있었고, 쿠버네티스 내부 네트워크 제약을 우회하면서 빠르게 기능을 붙일 수 있었습니다.
일단 동작하는 구조를 만드는 것이 우선이었기 때문에, 이 선택은 당시로서는 합리적이었습니다.
실제로 이 방식은 초기에 시스템을 안정화하는 데 도움이 됐습니다. 애플리케이션 로직과 영상 처리 영역을 분리함으로써 문제를 더 단순하게 다룰 수 있었고, 적어도 "왜 연결이 안 되는가"를 쿠버네티스 네트워크와 Kurento 내부 동작 중 어디에서 먼저 봐야 하는지 구분할 수 있었습니다.
하지만 시간이 지나면서 이 구조의 한계도 분명해졌습니다. 애플리케이션은 쿠버네티스에 있고, 미디어서버는 VM에 따로 있는 구조는 운영 모델을 이원화합니다.
확장 방식도 다르고, 장애 대응 포인트도 다르며, 배포와 관측의 기준도 달라집니다.
이 시점의 아키텍처는 아래와 같았습니다.
특정 컴포넌트 하나만 클러스터 밖에 두는 구조는 시간이 갈수록 예외를 늘리고 운영 복잡도를 키우는 방향으로 작동했습니다.
다시 쿠버네티스 네이티브한 구조를 고민하게 된 이유
결국 다음 단계의 목표는 분명했습니다. 실시간 영상 송출 경로를 다시 쿠버네티스 안으로 가져오되, 이전처럼 무리하게 억지로 넣는 방식이 아니라 쿠버네티스 환경에 맞는 형태로 재설계하는 것이었습니다.
이 과정에서 중요했던 것은 "어떤 미디어서버를 쓸 것인가"보다 "WebRTC 트래픽을 어떤 네트워크 모델로 운영할 것인가"였습니다.
우리가 계속 겪고 있던 문제는 미디어 변환 자체보다도 연결 수립과 미디어 경로 제어에 가까웠기 때문입니다.
즉, 문제의 본질은 기능보다 운영 방식에 있었습니다.
그래서 주목하게 된 것이 STUNner였습니다.
STUNner는 Kurento 같은 미디어서버를 대체하는 도구라기보다, 쿠버네티스 환경 안에서 WebRTC/TURN 트래픽을 더 자연스럽게 다룰 수 있도록 도와주는 인프라 레이어에 가까웠습니다.
우리가 원했던 것도 정확히 그 지점이었습니다.
미디어 워크로드를 쿠버네티스에서 설명 가능하게 만들고, 예외적인 VM 운영을 줄이며, 네트워크 경로를 더 선언적이고 예측 가능하게 가져가는 것 말입니다.
STUNner를 통해 연결 경로를 다시 설계했다
STUNner를 도입하면서 가장 크게 달라진 점은 실시간 미디어 트래픽을 쿠버네티스의 운영 모델 안에서 다시 설명할 수 있게 되었다는 점이었습니다.
이전에는 Kurento VM이 실질적인 예외 지점으로 남아 있었고, 운영 관점에서도 "저 영역은 별도로 관리해야 하는 것"처럼 분리되어 있었습니다.
반면 STUNner를 적용한 이후에는 WebRTC 연결 경로를 클러스터 중심으로 재구성할 수 있었고, 외부 노출과 내부 연결을 더 일관된 방식으로 다룰 수 있게 됐습니다.
이 변화는 단순히 구성 요소를 바꾼 수준이 아니었습니다.
운영자가 제어할 수 있는 범위가 넓어졌고, 어떤 구간에서 병목이나 실패가 발생하는지 파악하기 쉬워졌습니다.
실시간 스트리밍 환경에서는 연결 성공 여부만큼이나, 실패했을 때 어디를 먼저 봐야 하는지가 중요합니다.
그런 점에서 STUNner 기반 구조는 네트워크 문제를 더 잘 드러내고 더 쉽게 운영할 수 있는 방향이었습니다.
무엇보다 의미 있었던 것은 "이 컴포넌트만 왜 VM에 있어야 하는가"라는 질문에서 어느 정도 벗어날 수 있었다는 점입니다. 실시간 영상 송출이 여전히 어려운 워크로드인 것은 맞지만, 적어도 운영 구조 자체가 예외가 되지는 않도록 만들 수 있었습니다.
아키텍처를 정리한 뒤, 진짜 과제는 WebRTC 품질 개선이었다
아키텍처를 Kubernetes 안으로 다시 정리하고 나서야, 비로소 품질 문제를 제대로 볼 수 있게 됐습니다.
이제부터는 관심사가 달라졌습니다. “연결이 되느냐”보다 “얼마나 빨리 붙고, 얼마나 안정적으로 유지되느냐”가 더 중요한 과제가 됐습니다.
웹브라우저 경로는 WebRTC -> STUNner -> mediasoup, nuv-agent 장치 uplink 경로는 WebRTC -> STUNner -> ingest-bridge -> mediasoup로 정리됐고, 그 위에서 연결 품질과 재생 품질을 함께 다듬기 시작했습니다.
가장 먼저 손본 부분은 연결 안정성이었습니다. WebRTC에서는 단순히 세션을 생성하는 것보다, 실제로 어떤 ICE candidate가 선택되고 어떤 경로로 미디어가 흐르느냐가 더 중요합니다. 그래서 relay 경로가 기대한 대로 선택되는지, 불필요한 재시도나 중복 offer가 발생하지 않는지, 세션이 불안정한 상태에서 반복적으로 다시 맺어지지는 않는지를 먼저 점검했습니다. 실제로 이런 종류의 문제는 겉으로 보면 단순한 “끊김”처럼 보이지만, 내부적으로는 시그널링 흐름이나 ICE 협상 과정의 작은 불안정성이 누적된 결과인 경우가 많았습니다.
그 다음에는 인코딩과 전송 파라미터를 조정했습니다. WebRTC 품질은 네트워크만으로 결정되지 않았습니다. 비트레이트가 너무 높으면 패킷 손실과 지연이 커졌고, 반대로 너무 낮으면 화면이 쉽게 깨졌습니다. 키프레임 간격은 첫 화면 노출 시간과 복구 속도에 직접적인 영향을 줬고, 프레임레이트와 해상도 역시 장치 성능과 네트워크 상태에 따라 적절한 타협점이 필요했습니다. 결국 중요한 것은 “좋아 보이는 설정”이 아니라, 현재 환경에서 얼마나 안정적으로 유지되는 설정인가였습니다.
이 과정에서 특히 중요했던 것은 감이 아니라 지표를 기준으로 조정하기 시작했다는 점입니다. 연결 수립 시간, 첫 화면 노출 시간, 재접속 빈도, packet loss, jitter, candidate selection 결과 같은 값을 함께 보기 시작하면서부터는 품질 문제를 구조적으로 설명할 수 있게 됐습니다.
이전에는 사용자의 체감이나 로그 일부를 보고 원인을 추측하는 경우가 많았다면, 이후에는 어떤 구간에서 품질 저하가 두드러지는지, 조정한 설정이 실제로 효과가 있었는지를 훨씬 더 명확하게 판단할 수 있었습니다.
결국 품질 개선은 하나의 설정으로 해결되지 않았습니다. 외부 진입 경로를 STUNner 중심으로 정리하고, 장치 uplink를 WebRTC 기반으로 재구성하고, 시그널링 안정성을 높이고, 인코딩 파라미터를 조정하고, 운영 지표를 함께 보면서 병목을 줄여가는 과정이 필요했습니다.
실시간 영상 품질은 특정 미디어서버 하나가 아니라, 네트워크 경로, 세션 제어, 인코딩 전략, 운영 모델 전체가 함께 만드는 결과에 더 가까웠습니다.
마치며
이번 경험을 통해 가장 크게 배운 점은, 쿠버네티스에서 실시간 영상을 다룬다는 것은 단순히 미디어서버를 컨테이너로 배포하는 문제가 아니라는 사실이었습니다. WebRTC는 애플리케이션 레벨의 기능 구현만으로 해결되지 않고, 네트워크 경로와 운영 모델까지 함께 설계해야 비로소 안정적인 구조가 됩니다.
초기에는 쿠버네티스 안에서 다루기 어려운 미디어 경로를 우회하기 위해 예외적인 구성을 선택할 수밖에 없었습니다. 하지만 장기적인 운영 관점에서는 결국 그 예외를 줄이고, 미디어 경로 자체를 Kubernetes 안에서 설명 가능한 구조로 바꾸는 것이 더 중요했습니다.
mediasoup를 클러스터 안으로 가져오고, STUNner를 통해 외부 UDP 진입점을 정리하고, ingest-bridge를 통해 장치 uplink까지 WebRTC 중심으로 재구성하면서 비로소 전체 구조가 하나의 운영 모델 안에 들어오기 시작했습니다.
아직도 실시간 영상은 Kubernetes에서 쉬운 주제는 아니지만, 적어도 이번 개선 과정을 통해 한 가지는 분명해졌습니다. 이런 시스템은 무엇을 쓰느냐보다, 그 경로를 얼마나 운영 가능하게 만드느냐가 더 중요하다는 점입니다.