인프라

[TInypilot] Tinypilot 도커로 컨테이너화 해보기

하루이2222 2024. 9. 1. 01:25

프로젝트: TinyPilot 모놀리식 애플리케이션의 서비스 분리 및 컨테이너화

1. 프로젝트 개요

TinyPilot은 라즈베리 파이를 활용한 오픈소스 KVM-over-IP 솔루션으로, 원격지에서 웹 브라우저를 통해 물리 서버의 화면, 키보드, 마우스를 제어할 수 있는 강력한 도구임. 하지만 공식적으로는 통합된 라즈베리 파이 이미지 형태로만 배포되어, 클라우드 환경이나 다양한 하드웨어에서의 이식성과 확장성에 한계가 있었음.

본 프로젝트는 TinyPilot의 각 구성 요소를 분석하여 개별 서비스로 분리하고, Docker를 통해 컨테이너화함으로써 이식성과 관리 효율성을 극대화하는 것을 목표로 진행했음.

주요 성과:

  • 공식 지원이 없는 TinyPilot의 Docker 이미지 개발에 성공함.
  • 주요 컴포넌트(Web UI, Streamer)를 독립적으로 배포 가능한 서비스로 분리하고 컨테이너화함.
  • Nginx 리버스 프록시를 통해 서비스 엔드포인트를 통합하고 관리 효율성을 증대시킴.
  • Docker Compose를 활용하여 전체 스택의 배포 및 오케스트레이션을 자동화함.

2. 아키텍처 분석 및 설계

기존의 모놀리식 구조를 각 기능별 독립적인 서비스로 분리하기 위해, TinyPilot의 핵심 구성 요소를 분석하고 분리하는 작업을 선행했음.

2.1. 핵심 구성 요소 식별

  • TinyPilot Web (포트 8000): Flask 기반의 웹 애플리케이션. 사용자 UI를 제공하고, WebSocket을 통해 키보드/마우스 입력을 처리하여 HID 장치(dev/hidg*)로 전달함.
  • uStreamer (포트 8001): C로 작성된 고성능 비디오 스트리밍 서버. 비디오 캡처 카드(dev/video*)의 화면을 실시간으로 웹에 스트리밍함.
  • Nginx (게이트웨이): 시스템의 단일 진입점(Entrypoint). 사용자 요청을 받아 /stream 등 특정 경로는 uStreamer로, 그 외 UI 및 API 요청은 TinyPilot Web으로 라우팅하는 리버스 프록시 역할을 수행함.

2.2. 코드 분석 및 분리 가능성 검증
TinyPilot 웹 애플리케이션의 소스 코드를 분석하여 독립적인 서비스로 실행 가능한지 검증했음.

  • main.py: Flask 앱 초기화 및 실행.
  • socket_api.py: 키보드/마우스 입력을 처리하는 WebSocket 이벤트 핸들러.
  • views.py: UI 템플릿 렌더링.

분석 결과, 웹 서버는 환경 변수를 통해 HID 장치 파일 경로(KEYBOARD_PATH, MOUSE_PATH)를 주입받는 구조로 설계되어 있어, 다른 컴포넌트와 의존성 없이 독립적으로 컨테이너화가 가능하다고 판단했음.

 

2.3. HID gadget 설정 과정
본 KVM 솔루션이 동작하기 위한 핵심 전제 조건은 라즈베리 파이가 키보드/마우스 장치로 인식되도록 하는 것입니다. 이 설정 과정은 별도의 포스팅에 상세히 기술하였음.


3. 구현: 컴포넌트별 컨테이너화

각 컴포넌트의 특성에 맞춰 Dockerfile을 작성하고 이미지를 빌드했음.

3.1. TinyPilot Web 서비스 Dockerfile
Flask 애플리케이션 구동에 필요한 Python 및 Node.js 의존성을 설치하고, 소스 코드를 컨테이너에 복사함.

FROM ubuntu:22.04

# 필요한 패키지 설치
RUN apt-get update && apt-get install -y \
    curl sudo git build-essential python3 python3-pip python3-dev \
    libevent-dev libjpeg-turbo8-dev uuid-dev libbsd-dev make gcc \
    libjpeg-dev libuuid1 libbsd0 nodejs npm

# TinyPilot 소스 코드 복사
COPY . /opt/tinypilot
WORKDIR /opt/tinypilot

# Python 및 Node.js 의존성 설치
RUN pip install --upgrade pip
RUN pip install greenlet==1.1.0
RUN pip install -r requirements.txt
RUN npm install prettier@2.0.5

RUN mkdir -p /home/tinypilot && chown -R $(whoami) /home/tinypilot

# 환경 변수 설정
ENV APP_SETTINGS_FILE=/opt/tinypilot/config.py

# 포트 설정
EXPOSE 8000

3.2. uStreamer 서비스 Dockerfile
C 기반의 uStreamer를 소스 코드로부터 직접 빌드하고 설치하는 과정을 자동화했음.

# 베이스 이미지로 Ubuntu 22.04 사용
FROM ubuntu:22.04

# 필요한 패키지 설치
RUN apt-get update && apt-get install -y \
    build-essential cmake pkg-config libevent-dev libjpeg-dev \
    libbsd-dev curl git

# ustreamer 소스 코드를 직접 빌드
WORKDIR /opt/ustreamer

# build context의 모든 내용을 현재 WORKDIR로 복사
COPY . .

# 빌드 
RUN make && make install

# 노출할 포트 설정
EXPOSE 8001

4. 서비스 통합 및 라우팅 (Nginx)

Nginx를 리버스 프록시로 구성하여 분리된 두 서비스를 단일 엔드포인트처럼 동작하도록 통합했음.

주요 라우팅 규칙:

  • /stream, /snapshot: 비디오 관련 요청은 uStreamer 서비스로 프록시함.
  • /socket.io: WebSocket 연결은 TinyPilot Web으로 프록시함 (업그레이드 헤더 포함).
  • 정적 파일 (.js, .css, .png 등): 컨테이너 분리 구조에서는 TinyPilot Web으로 프록시하여 UI 리소스를 올바르게 로드하도록 수정함.
  • 그 외 모든 요청: TinyPilot Web으로 프록시함.
server {
    listen [::]:9090;
    listen 9090;
    server_name tinypilot.tail2dac17.ts.net;

    # Docker 내부 DNS(127.0.0.11)를 사용하도록 설정
    resolver 127.0.0.11 valid=5s;

    # 각 프록시 목적지를 위한 변수 설정
    set $tinypilot_socket_upstream http://tinypilot;
    set $ustreamer_upstream        http://ustreamer:8001;
    set $tinypilot_main_upstream   http://tinypilot:8000/;
    set $tinypilot2_upstream       http://tinypilot:8000;

    root /opt/tinypilot;
    index index.html;

    proxy_buffers 16 16k;
    proxy_buffer_size 16k;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;

    location /socket.io {
        proxy_pass $tinypilot2_upstream;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        # ... (기타 헤더 설정 생략) ...
    }

    location /stream {
        postpone_output 0;
        proxy_buffering off;
        proxy_ignore_headers X-Accel-Buffering;
        proxy_pass $ustreamer_upstream;
    }

    location / {
        proxy_pass $tinypilot_main_upstream;
    }

    # ... (기타 정적 파일 location 블록 생략) ...
}

5. Tailscale을 이용한 보안 강화

KVM 서비스의 특성상 외부 노출을 최소화하고 인가된 사용자만 접근하도록 제어하기 위해, 내부망 VPN 솔루션인 Tailscale 컨테이너를 추가하여 전체 서비스를 프록시하도록 구성했음.

5.1. Tailscale & Nginx 통합 Dockerfile
Tailscale 공식 이미지에 Nginx를 설치하고, 컨테이너 시작 시 Tailscale 연결과 Nginx 프록시를 동시에 실행하도록 구성함.

# Base image from Tailscale
FROM tailscale/tailscale:latest

# Install nginx
RUN apk add --no-cache nginx

# Remove default nginx config and copy our custom configurations
RUN rm /etc/nginx/http.d/default.conf
COPY nginx.conf /etc/nginx/nginx.conf
COPY tailscale_tinypilot_nginx_proxy.conf /etc/nginx/conf.d/tailscale_tinypilot_nginx_proxy.conf

# Copy and set permissions for the startup script
COPY start.sh /start.sh
RUN chmod +x /start.sh

# Command to run when the container starts
CMD ["/start.sh"]

5.2. 컨테이너 시작 스크립트 (start.sh)
컨테이너 내부에서 Tailscale 데몬, tailscale up 인증, Nginx 프록시, tailscale serve를 순차적으로 실행함.

#!/bin/sh
set -e

# 1. Tailscale 데몬을 백그라운드에서 실행
tailscaled &
sleep 3

# 2. 환경 변수를 사용해 tailscale up 실행
tailscale up \
    --authkey=${TS_AUTHKEY} \
    --hostname=${TS_HOSTNAME} \
    --exit-node= \
    --accept-dns=false

# 3. Nginx를 백그라운드에서 실행
nginx &
sleep 3

# 4. Tailscale의 serve 기능을 사용해 외부 요청을 내부 Nginx 포트로 프록시
tailscale serve --bg --tcp 80 localhost:9090

# 5. 스크립트가 종료되지 않도록 유지
tail -f /dev/null

6. 배포 자동화 (Docker Compose)

docker-compose.yml 파일을 작성하여 전체 서비스를 정의하고, 의존성, 네트워크, 볼륨, 디바이스 매핑을 설정하여 배포를 자동화했음.

핵심 설정:

  • depends_on: Nginx의 역할을 흡수한 tailscale 컨테이너가 tinypilot, ustreamer 이후에 실행되도록 의존성을 설정함.
  • devices: 컨테이너가 호스트의 물리 장치(dev/video0, dev/hidg*)에 직접 접근하도록 매핑. 이는 KVM 기능의 핵심임.
  • volumes: 로컬 설정 파일을 컨테이너로 마운트하여 이미지 재빌드 없이 설정을 변경할 수 있도록 유연성을 확보함.
version: '3.6'
services:
  ustreamer:
    image: harbor.192.168.0.110.nip.io/tinypilot/ustreamer:latest
    container_name: ustreamer
    restart: always
    ports:
      - "8001:8001"
    devices:
      - /dev/video0:/dev/video0
    command: ["ustreamer", "--host=0.0.0.0", "--resolution=1920x1080", "--encoder=hw", "--port=8001", "/dev/video0"]

  tinypilot:
    image: harbor.192.168.0.110.nip.io/tinypilot/tinypilot:latest
    container_name: tinypilot
    restart: always
    ports:
      - "8000:8000"
    devices:
      - /dev/hidg0:/dev/hidg0
      - /dev/hidg1:/dev/hidg1
    volumes:
      - ./config/config.py:/opt/tinypilot/config.py
    command: ["python3", "/opt/tinypilot/app/main.py"]

  tailscale:
    image: harbor.192.168.0.110.nip.io/tinypilot/tailscale:latest
    container_name: tailscale
    restart: always
    hostname: ${TS_HOSTNAME}
    devices:
      - "/dev/net/tun:/dev/net/tun"
    cap_add:
      - net_admin
      - sys_module
    environment:
      - TS_AUTHKEY=${TS_AUTHKEY}
      - TS_HOSTNAME=${TS_HOSTNAME}

7. 실행 스크립트

HID 가젯 초기 설정과 Docker Compose 배포를 한 번에 실행하는 자동화 스크립트를 작성함.

#!/bin/bash
set -e

echo "=== [1/2] HID Gadget Setup 실행 ==="
if [ -f "./setup_hid_gadget.sh" ]; then
    sudo sh ./setup_hid_gadget.sh
else
    echo "❌ setup_hid_gadget.sh 파일이 없습니다."
    exit 1
fi

echo "=== [2/2] Docker Compose 실행 ==="
docker-compose up -d --remove-orphans

echo "✅ 배포 완료!"

8. 결론 및 고찰

이 프로젝트를 통해 공식 Docker 이미지가 없는 모놀리식 애플리케이션을 성공적으로 컨테이너화하고, 기능 단위로 분해하여 독립적인 서비스로 구성하는 경험을 쌓았음. 특히, 하드웨어 장치 제어가 필수적인 KVM 솔루션을 컨테이너 환경에서 완벽하게 동작시켰으며, Tailscale을 통합하여 보안성을 크게 향상시켰다는 점에서 큰 의미가 있음. 본 프로젝트의 결과물은 재사용 가능한 Docker 이미지로 빌드되어, 어떠한 Docker 환경에서도 재빌드 과정 없이 빠르고 일관되게 TinyPilot을 배포할 수 있음.