• logo

      SeolMyeongTang

  • Pod 기반 원격 작업 환경 개발기

    2026년 4월 05일

    입출력 장치, 네트워크 환경만 있으면 언제 어디서든지 나만의 컴퓨팅 환경에 접속할 수 있다면 정말 매력적이지 않을까요? 웹 브라우저로 몇 번의 클릭만 하면 가상 환경을 사용할 수 있는 서비스. 이번 글은 웹에서 VNC 기반으로 pod에 접속해 가상 환경을 사용할 수 있는 서비스 구조 및 설계를 어떻게 구현했는지에 대한 시간을 가지도록 하겠습니다.

    제가 만들어보고 싶었던 서비스는 ‘네트워크 환경이라면 언제 어디서든 접속하여 사용할 수 있는 나만의 컴퓨팅 환경’(이하 NetCom 서비스)입니다. 사용자가 접속할 컴퓨터 환경은 pod로 대입하였고, noVNC로 pod에 VNC 원격 접속하여 웹 브라우저를 통해 접속할 수 있도록 구성하였습니다.


    왜 pod로 사용자의 컴퓨팅 환경을 구성하려고 했나요?

    NetCom 서비스는 사용자에게 독립되고 일관된 컴퓨팅 환경 제공이 필요했습니다. 쿠버네티스의 pod를 사용자의 하나의 컴퓨팅 환경으로 표현하기 적합하다고 판단했습니다.

    Container로도 충분히 서비스를 구현할 수 있었겠지만, 쿠버네티스는 클러스터 전체에 네트워크(NetworkPolicy), 스토리지(PV), 보안(RBAC) 제어할 수 있기에 container 환경(docker-compose)보다 장기적인 운영에 있어서 이득이 많을 거로 생각했습니다.



    왜 원격 접속으로 VNC을 선택했으며, 그중 왜 noVNC를 선택했나요?

    SSH, RDP, VNC 등 서버 장비에 원격 접속할 수 있는 여러 방법이 존재합니다. NetCom 서비스는 사용자에게 GUI 환경 제공을 하면서 별도의 프로그램 설치 없이 원격 접속할 수 있기를 희망했습니다.

    noVNC는 HTML VNC 클라이언트 오픈소스 라이브러리로 VNC 프로그램 설치 없이 웹 브라우저를 통해 VNC 접속을 할 수 있습니다. 웹 브라우저만 있다면 VNC 접속할 수 있는 편의성으로 noVNC 도구를 선정하였습니다.


    정리하여 NetCom 서비스를 한 줄로 정의한다면 다음과 같습니다.

    Kubernetes 기반으로 사용자에게 격리된 원격 작업 환경을 웹으로 제공하는 플랫폼

    컨셉은 단순했지만 이를 구현하기 위해 클라이언트, 서버, 인프라 모든 영역을 고려해야 했기 때문에 생각해야 할 지점들이 많았습니다.

    • 클라이언트
      • 기본적인 페이지 및 스타일 구성
        • 기존 설명탕 스타일 유지
      • 사용자 세션 관리 방법
        • 사용자마다의 환경을 제공해야 하므로 세션과 같이 사용자를 특정할 수 있어야 함
        • 회원가입은 사용자 입장에서는 개인정보를 제공해야 한다는 반감이 들고, 관리자 입장에서는 데이터를 안전하게 보관해야 하는 리스크가 존재
        • 개인정보를 서버에 저장하지 않고, 사용자마다 세션(이하 token) 값을 생성하고 localStorage에 저장하여 사용자를 구분
    • 서버
      • 쿠버네티스 리소스 생성 방법
        • 사용자의 환경 생성 요청이 들어올 때마다 pod 생성 필요
        • Golang의 k8s.io/api 라이브러리 활용
      • 특정 사용자 환경으로 VNC 접속을 위한 특정 pod 라우팅
        • websockify 라이브러리로 웹 브라우저(noVNC)와 VNC pod 사이 연결 중계 역할 수행
          • [noVNC] — (WebSocket) — [websockify] — (TCP) — [VNC pod]
        • Token 값을 기준으로 websockify가 특정 pod로 통신할 수 있도록 구현
          • websockify의 TokenPlugin class를 override
    • 인프라
      • 쿠버네티스 클러스터 구축 방법
        • 개인 블로그 수준의 소규모 서비스이고 bare metal 환경에서 사용할 경량 쿠버네티스 도구인 k3s 선택
      • 무분별한 환경(pod) 생성 문제
        • 불특정 다수 사용자를 대상으로 한 서비스이므로 pod의 갯수가 계속 늘어날 수 있고 이로 인한 서버 자원 부족 문제 발생 가능
        • 한 token 당 최대 4개 환경 생성 가능하도록 설정
        • 리소스(CPU, Memory, Disk) 제한 및 pod 10분 제한 시간 설정
        • 서버에서 주기적으로 pod의 제한 시간을 확인 후 10분 이상 된 pod 및 관련된 리소스 삭제 처리
      • dev, prod 환경 분리 운영
        • dev, prod 환경을 분리하고 환경별 운영, 배포를 위해 쿠버네티스 리소스를 kustomize로 관리
      • 외부 접속
        • 단순히 환경을 사용하는 거 뿐만 아니라 포트를 열어 외부에서도 접속이 가능하도록 설정
        • Cloudflare Tunnel을 활용하여 [id].tunnel.redundant4u.com로 접속하면 8080 포트가 열려져 있는 프로세스와 통신이 가능하도록 설정
    • 보안
      • root 권한 제공으로 인한 container breakout 문제
        • runC 컨테이너 런타임 도구는 호스트의 커널을 공유하는 구조
        • 커널 취약점으로 container breakout 공격으로 호스트 서버 장악 가능
        • 각각의 환경은 커널을 가상화하여 사용할 수 있도록 Kata Containers 컨테이너 런타임 사용
      • 인터넷 통신 시 호스트 서버의 NAT IP를 사용하는 문제
        • 사용자가 접속한 pod는 외부 통신 시 호스트의 NAT 공인 IP를 사용하므로 IP 평판 하락 혹은 IP 악용 가능
        • pod는 NetworkPolicy + Cloudflare WARP을 통해서만 외부 인터넷 통신하도록 설정
          • [pod] — (NetworkPolicy) — [gluetun] — [Cloudflare WARP] — [Internet]
        • 쿠버네티스 NetworkPolicy 리소스로 egress 강제
      • 의도하지 않은 사용자의 오남용으로 인한 리스크를 최소화하기 위해 책임 고지 내용 작성

    NetCom 서비스를 구성하면서 여러 부분을 신경 써서 만들었는데 그중 몇 가지 내용을 소개해 드리겠습니다.

    • Pod 만료시간 관리

    • Kata Containers 사용

    • Cloudflare 인프라를 활용한 inbound, outbound 제어

    • Pod 만료시간 관리

    무분별한 pod 생성 방지를 위해 pod 생성 시 annotation에 expired-at 항목과 함께 pod를 생성합니다.

    GO
    expiredAt := createdAt.Add(10 * time.Minute)
    
    podSpec := &corev1.Pod{
      ObjectMeta: metav1.ObjectMeta{
        Namespace: k.namespace,
        Annotations: map[string]string{
          "expired-at":  expiredAt.Format(time.RFC3339),
        },
      },
      ...
    }
    

    만료된 pod와 연관 리소스들을 정리하기 위해 goroutine을 사용하였습니다. Goroutine으로 Go 서버 실행 시 따로 스케줄러를 등록하지 않으면서 백그라운드에서 주기적으로 pod 만료 여부를 체크합니다.

    Goroutine은 1분 주기로 pod의 expired-at 값을 확인하여 pod 삭제 여부를 결정합니다.

    GO
    ctx := context.Background()
    go kube.Gc.Run(ctx)
    
    func (g *gc) Run(ctx context.Context) {
      ticker := time.NewTicker(time.Duration(g.interval) * time.Second)
      defer ticker.Stop()
      logger.Info("gc: starting garbage collector")
      for {
        select {
        case <-ticker.C:
          g.cleanup(ctx)
        case <-ctx.Done():
          logger.Info("gc: stopping garbage collector")
          return
        }
      }
    }
    
    func (g *gc) cleanup(ctx context.Context) {
      ...
      expiredAt, err := time.Parse(time.RFC3339, expiredAtStr)
    
      if now.After(expiredAt) {
        err := g.k8s.Clientset.CoreV1().Pods(g.namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{})
      }
      ...
    }
    
    • Kata Containers 사용

    runC 컨테이너 런타임 도구는 namespace와 cgroup을 통해 pod 내의 container를 격리시키지만 호스트 커널을 공유합니다. container에서 커널 취약점을 통해 container breakout 공격을 수행하면 서버 호스트를 장악할 수 있고 이는 큰 문제로 이어질 수 있습니다.

    이를 방지하기 위해 container를 VM 수준의 격리를 제공하는 Kata Containers 컨테이너 런타임 도구를 사용하였습니다.

    Kata Containers vs 기존 container 런타임 도구 비교
    Kata Containers vs 기존 container 런타임 도구 비교 출처

    위 그림처럼 Kata Containers를 사용하면 VM을 생성하고 그 안에 container를 실행합니다. 기존 runC 방식보다 성능상 손해를 있을 수 있지만, VM 수준의 보안 격리를 제공할 수 있어 사용자에게 root 권한을 부여하더라도 보안 위험을 줄일 수 있습니다.


    VirtualBox, QEMU 도구 혹은 OpenStack, Proxmox와 같은 플랫폼을 사용할 수 있겠지만, 컨테이너 런타임 도구로 쿠버네티스 환경에서 VM 수준의 격리를 제공해주는 Kata Containers가 적합하였습니다.


    Kata Containers를 사용하기 위해 한 가지 중요한 점은 nested virtualization 기능을 지원해야하는 장비여야 합니다. 일반적으로 클라우드에서 제공하는 가상 환경은 보안, 성능상의 제약으로 nested virtualization를 지원하지 않아 bare-metal 환경에서 사용하는 것을 추천합니다. 물론 bare-metal 장비의 CPU 또한 nested virtualization 지원하는지 꼭 확인하셔야 합니다.

    Kata Containers 런타임 도구를 사용하기 위해서는 다음의 작업이 필요합니다. 저는 kustomize으로 쿠버네티스 클러스터를 관리하여 해당 기준으로 작성하였습니다.

    1. kustomize의 helmCharts 기능을 통해 Kata Containers 설치

      YAML
      apiVersion: kustomize.config.k8s.io/v1beta1
      kind: Kustomization
      
      helmCharts:
        - name: kata-deploy
          releaseName: kata-deploy
          namespace: kube-system
          repo: "oci://ghcr.io/kata-containers/kata-deploy-charts"
          version: "3.27.0"
          valuesFile: values/kata-containers.yaml
      
    2. runtimeclass 확인

      • qemu-runtime-rs shims 사용
      BASH
      $ kubectl get runtimeclass
      NAME                       HANDLER                    AGE
      kata-qemu-runtime-rs       kata-qemu-runtime-rs       84d
      
    3. 사용자가 원격 접속할 pod의 runtimeClassName 변경

      YAML
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: smt
      spec:
        runtimeClassName: kata-qemu-runtime-rs
      
    4. Pod 생성 후 호스트 커널과 격리된 환경인지 확인

      BASH
      $ kubectl get po
      NAME             READY   STATUS    RESTARTS   AGE
      runc-sample      1/1     Running   0          44s
      kata-sample      1/1     Running   0          2m34s
      
      $ kubectl describe po kata-sample | grep Runtime
      Runtime Class Name:  kata-qemu-runtime-rs
      
      # host 서버 환경
      $ uname -a
      Linux smt X.8.X-XXX-generic #<build> <date> x86_64 x86_64 x86_64 GNU/Linux
      
      # runC 런타임 (host 서버 환경과 동일)
      $ kubectl exec -it runc-sample -- uname -a
      Linux runc-sample X.8.X-XXX-generic #<build> <date> x86_64 x86_64 x86_64 GNU/Linux
      
      # kata containers 런타임 (host 서버 환경과 다름)
      $ kubectl exec -it kata-sample -- uname -a
      Linux kata-sample X.18.15 #<build> <date> x86_64 GNU/Linux
      

    앞서 언급한 container 보안 문제와 더불어, 사용자에게 아무런 네트워크에 대한 제약 없이 환경을 제공하는건 보안적으로 위험하다고 판단했습니다. Pod에서 internet으로 가는 트래픽을 모니터링하고 필요한 경우 차단할 수 있도록 트래픽을 제어를 중앙화할 필요가 있었습니다. 아, 물론 사용자의 네트워크 요청을 기록하지는 않습니다.

    • Cloudflare 인프라를 활용한 inbound, outbound 제어

    쿠버네티스 NetworkPolicy를 사용하여 사용자 pod의 outbound 트래픽이 vnc-gateway pod로만 전달되도록 제한하였습니다. vnc-gateway pod에서는 gluetun이라는 VPN 클라이언트를 통해 Cloudflare WARP에 연결하고, WARP가 터널을 통해 외부 통신하도록 합니다.

    [vnc pod] — (NetworkPolicy) — [vnc-gateway pod] ([gluetun] — [Cloudflare WARP]) — Cloudflare Edge Server — [Internet]

    YAML
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: vnc-network-policy
    spec:
      podSelector:
        matchLabels:
          app: vnc
      policyTypes:
        - Egress
      egress:
        - to:
            - podSelector:
                matchLabels:
                  app: vnc-gateway
          ports: # for gluetun(forward proxy) port
            - protocol: TCP
              port: 3128
        - ports: # for DNS Resoultion port
            - protocol: UDP
              port: 53
            - protocol: TCP
              port: 53
    

    vnc-gateway pod에서 outbound 트래픽을 제어할 수 있으며, Cloudflare WARP를 통해 호스트 NAT IP가 아닌 Cloudflare IP로 외부와 통신할 수 있습니다. DNS resolution을 위해 53 포트를 허용하여 pod가 kube-dns를 통해 도메인을 해석할 수 있도록 구성했습니다. vnc pod에서는 HTTP_PROXY 설정을 통해 외부 트래픽이 vnc-gateway를 경유하도록 설정했습니다.

    BASH
    $ env
    http_proxy=VNC_GATEWAY_SERVICE_IP:3128
    https_proxy=VNC_GATEWAY_SERVICE_IP:3128
    HTTP_PROXY=VNC_GATEWAY_SERVICE_IP:3128
    HTTPS_PROXY=VNC_GATEWAY_SERVICE_IP:3128
    

    이 정도의 기능이 1차적으로 구현하고자 했던 범위였습니다. 다만 욕심을 조금만 내서 외부에서도 접속이 가능하다면 정말 끝내주지 않을까 생각했습니다. 하지만 이 부분은 보안적으로 민감한 영역이라 여러 가지 고민이 필요했습니다. 우선 외부 접속에만 의의를 두어 pod 마다 도메인을 할당하고 하나의 특정 프로세스와 연결될 수 있도록 했습니다.

    Pod가 생성될 때마다 호스트 서버의 포트포워딩(inbound) 설정을 자동화한다면, 서버의 네트워크 설정을 직접 변경해야 하기 때문에 위험할 수 있었고 실제로 안정적으로 구현이 될까에 대한 의문이 있었습니다. 놀랍게도 포트를 직접 열지 않고 Cloudflare 인프라를 활용하여 외부 접속이 되도록 구성할 수 있습니다.

    Cloudflare Tunnel 기능으로 이를 구현할 수 있는데, cloudflared CLI를 사용해 Cloudflare Tunnel을 생성하면 포트를 열지 않고도 Cloudflare 네트워크를 통해 외부에서 내부 서비스에 접근할 수 있습니다. cloudflared 설정 파일에서 내부 서비스의 포트를 지정하면 해당 서비스로 트래픽이 전달되도록 구성할 수 있습니다.


    Cloudflare WARP는 뭐고 Cloudflare Tunnel은 또 뭔가요?

    Cloudflare WARP는 서버나 클라이언트의 outbound 트래픽을 Cloudflare 네트워크를 통해 외부 인터넷에 전달할 수 있도록 도와주는 클라이언트 도구입니다.

    Cloudflare Tunnel은 내부 서비스로 들어오는 inbound 트래픽을 Cloudflare를 통해 전달받을 수 있도록 하는 기술입니다. 외부 사용자는 Cloudflare 네트워크를 통해 서버의 내부 서비스에 접근하게 됩니다.

    관련하여 더 알아보고 싶은 분들은 다음을 참고하시면 되겠습니다.


    1. cloudflared CLI 설치

      BASH
      brew install cloudflared
      apt install cloudflared
      
      cloudflared --version
      
    2. Cloudflare Tunnel 생성

      BASH
      cloudflared tunnel login
      cloudflared tunnel create CF_TUNNEL_NAME
      # ~/.cloudflared/<UUID>.json 생성 확인 가능
      
      • Cloudflare dashboard - Zero Trust - Networks - Connectors 에서 확인 가능
    3. Cloudflare Tunnel에 도메인 연결

      BASH
      cloudflared tunnel route dns CF_TUNNEL_NAME "*.tunnel.redundant4u.com"
      
    4. cloudflared 설정 파일 생성

      YAML
      tunnel: CF_TUNNEL_UUID
      credentials-file: /etc/cloudflared/credentials.json
      
      ingress:
        - hostname: POD_ID.tunnel.redundant4u.com
          service: http://localhost:8080
        - service: http_status:404
      
      • /etc/cloudflared/credentials.json는 앞서 Cloudflare Tunnel 생성할 때 나온 json 파일

    cloudflared 설정 내용은 pod에 파일로 작성해도 되고 API으로도 ingress 규칙을 설정을 할 수 있습니다.

    Cloudflare Tunnel를 사용한 외부 접근 흐름은 다음과 같습니다.

    1. POD_ID.tunnel.redundant4u.com 접속
      • POD_ID.tunnel.redundant4u.com는 Cloudflare에 설정된 DNS 규칙에 따라 xxx.cfargotunnel.com 터널 도메인을 가리킴
      • 브라우저는 사용자으로부터 가까운 Cloudflare Edge와 통신
    2. Cloudflare Edge에서 tunnel 라우팅 결정
      • Cloudflare Edge는 POD_ID.tunnel.redundant4u.com 기준으로 어떤 Cloudflare Tunnel로 전달할 지 결정
    3. cloudflared가 내부 서비스로 트래픽 전달
      • cloudflared가 트래픽을 받고, 설정 파일에 따라 내부 서비스로 트래픽 전달

    위의 기술과 고민으로 이전부터 구현하고 싶었던 서비스를 만들 수 있었습니다. 물론 부족한 점과 아쉬운 점도 많습니다.

    VNC는 RFB(Remote FrameBuffer) 프로토콜을 사용합니다. RFB 프로토콜은 TCP 기반에서 동작하며, 화면의 변경된 영역을 이미지로 인코딩 후 사용자한테 전송함으로써 화면의 변화를 전달합니다. 이러한 특징으로 VNC 방식의 원격 접속은 정적인 화면 위주의 작업에는 무리가 없지만 영상 재생과 같은 동적인 작업에는 쾌적하지 않았습니다. 또한 음성 전달을 지원하지 않아 영상 소리가 들리지 않았습니다.

    이러한 한계를 보완하기 위해 UDP 기반으로 동작하며 음성 전달을 지원하고 VNC보다 반응성이 더 좋은 WebRTC 방식의 원격 접속을 추가할 예정입니다. WebRTC 원격 접속 환경을 지원하려면 Signaling 서버 STUN/TURN 서버 구성 등 추가적인 작업이 필요하지만 VNC 원격 접속 방식보다 실시간성이 개선이 되지 않을까 하는 기대감이 있습니다.

    WebRTC 도입 외 추후 생각하고 있는 기능은 다음과 같습니다.

    • 환경 이미지 추가
      • Ubuntu 24.04와 같은 다양한 이미지 지원
      • GNOME 기반의 다른 스타일이 적용된 이미지
      • Windows, macOS도 제공 가능하지만 저작권 이슈로 불가능
    • SSH 접속 기능
    • PV, PVC 활용으로 영속성 보장
    • 사용자가 cloudflared ingress를 제어할 수 있도록 제공

    NetCom 서비스를 구축하면서 필요한 기능과 이를 구현할 도구들을 찾는데 AI 도움이 컸습니다. 특히 Cloudflare WARP, Cloudflare Tunnel로 외부 네트워크와의 outbound 통신과 외부에서 내부 서비스로의 접근 경로를 간편하게 구성할 수 있었습니다. root 권한을 사용할 수 있는 환경에서 호스트 서버를 지키기 위해 도입한 Kata Containers 런타임 존재 또한 알 수 있었고 클라이언트 구현도 제가 크게 관여하지 않을 만큼 훌륭하게 수행해줬습니다. AI가 개발 생태계에 큰 영향을 끼치고 있는데 정말 몸소 느낄 수 있었던 경험이였습니다.

    NetCom 서비스는 아래 링크에서 사용해보실 수 있습니다.

    https://redundant4u.com/session