Server

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

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

TinyPilot Docker 이미지로 배포하기

TinyPilot은 원격으로 컴퓨터를 관리할 수 있는 오픈소스 KVM-over-IP (Keyboard, Video, Mouse over IP) 솔루션이다. TinyPilot을 사용하면 원격지에서 웹 브라우저를 통해 물리적인 컴퓨터의 키보드와 마우스를 제어하고 화면을 실시간으로 볼 수 있다. 주로 네트워크에 연결된 기기의 문제를 해결하거나, 운영체제를 설치하거나, 원격에서 컴퓨터를 유지보수할 때 유용하다.

1. TinyPilot의 개요

일단 TinyPilot은 라즈베리 파이와 올인원으로 설치되는 소프트웨어 패키지 + 하드웨어인 통합 KVM 솔루션이다. 그래서인지 공식 Docker 이미지는 지원하지 않는다. 그래서 걍 내가 만들기로 했다
이전에 계획을 세웠던 글을 작성한 적이 있는데, 이번에 드디어 성공하게 되어 그 과정을 남겨보고자 한다.

2. TinyPilot의 구성 요소

TinyPilot은 크게 3가지로 구성되어 있다:

  1. TinyPilot의 웹 프로그램: 사용자 인터페이스(UI) 제공 및 키보드, 마우스 입력을 캡처하는 TinyPilot 소프트웨어 패키지.
  2. uStreamer: 캡처 카드를 통해 화면을 캡처하여 스트리밍하는 비디오 스트리밍 서버.
  3. Nginx: TinyPilot과 uStreamer의 API를 하나로 통합하는 웹 서버.

이러한 구성 요소를 통해 LAN을 통해 외부 인터넷에서도 KVM에 접속하여 키보드, 마우스, 모니터를 사용할 수 있도록 하는 KVM 서비스를 제공한다.

3. TinyPilot Docker 이미지

TinyPilot은 위와 같이 구성되어 있으며, 이를 Docker 이미지로 배포하기 위해 몇 가지 과정이 필요하다. 특히, TinyPilot의 공식 패키지를 분석하여 Docker 환경에 맞는 빌드 스크립트를 새로 작성해야 했다. 이를 위해 TinyPilot의 소스 코드를 분석한 결과, 다음과 같은 주요 구성 요소를 발견했다:

TinyPilot 웹 서버
TinyPilot의 웹 서버는 Flask 기반의 웹서버로 이루어져 있으며, 중요한 파일들은 다음과 같다:

  • main.py: Flask 애플리케이션 초기화 및 설정, 블루프린트 등록, 오류 핸들러 정의, 애플리케이션 실행.
  • socket_api.py: WebSocket 설정 및 이벤트 핸들링, 클라이언트의 키보드와 마우스 입력 처리.
  • views.py: TinyPilot의 사용자 인터페이스(UI) 제공, 메인 페이지와 여러 HTML 페이지 렌더링.

이 세 파일이 TinyPilot 웹 서버에서 가장 중요한 부분들을 담고 있다.

의 역할과 분석

  • 주요 기능과 역할:
    • Flask 애플리케이션 초기화 및 설정: Flask 앱을 초기화하고 필요한 설정(비밀 키, 템플릿 자동 재로드, CSRF 보호 등)을 적용한다.
    • 블루프린트 등록: 다양한 기능을 제공하는 여러 블루프린트(API, CLI 명령어, 라이선스 공지, 뷰 등)를 애플리케이션에 등록한다.
    • 오류 핸들러 정의: 애플리케이션 전반에 걸쳐 발생하는 예외를 처리하고, 적절한 응답을 반환하는 오류 핸들러를 정의한다.
    • 애플리케이션 실행: socketio를 사용하여 Flask 애플리케이션을 실행하고, WebSocket 연결을 활성화한다.

의 역할

  • WebSocket 설정 및 이벤트 핸들링:
    • SocketIO 인스턴스 생성: flask_socketio.SocketIO를 통해 Flask 애플리케이션을 WebSocket 프로토콜을 지원하도록 확장한다.
    • WebSocket 이벤트 핸들러 등록: @socketio.on 데코레이터를 사용하여 클라이언트와의 실시간 통신을 처리하기 위한 다양한 이벤트 핸들러(keystroke, mouse-event, keyRelease, connect, disconnect)를 정의한다.
  • 키보드 입력 처리 (on_keystroke 핸들러):
    • 기능: 사용자가 클라이언트(브라우저)에서 키 입력을 할 때마다, 그 입력을 서버로 전송하고 이를 물리적인 키보드 장치로 전달한다.
  • 마우스 이벤트 처리 (on_mouse_event 핸들러):
    • 기능: 사용자가 브라우저에서 마우스 움직임이나 클릭을 할 때마다, 그 입력을 서버로 전송하고 이를 물리적인 마우스 장치로 전달한다.
  • 클라이언트 연결 및 해제 이벤트 처리 (on_connect, on_disconnect 핸들러):
    • 기능: 클라이언트가 서버에 연결하거나 연결을 해제할 때 로그에 기록하는 역할을 한다.

의 역할

  • TinyPilot의 웹 인터페이스 제공:
    • 이 파일은 사용자 인터페이스(UI)를 구성하고, 사용자가 TinyPilot 웹 애플리케이션에 접근할 때 표시되는 여러 페이지를 정의한다.
    • 메인 페이지, 스타일 가이드, 전용 창 자리 표시자 등 다양한 HTML 페이지를 제공하여 사용자에게 원격 제어 기능을 제공한다.
  • 환경에 따라 동작 변경:
    • 이 파일은 애플리케이션이 디버그 모드로 실행 중인지 여부에 따라 다른 동작을 수행한다. 예를 들어, 개발 모드에서만 특정 페이지(예: 스타일 가이드)를 제공하거나, 테스트용 이미지를 반환한다.
  • 템플릿 렌더링과 설정 전달:
    • Flask의 render_template 함수와 함께 다양한 설정 값을 템플릿에 전달하여, 페이지가 동적으로 표시되도록 한다. 예를 들어, WebRTC 설정이나 스트리밍 서버 정보가 페이지에 포함된다.

3.4 분석 결과

코드를 분석해본 결과, TinyPilot 웹 서버는 사용자에게 UI를 제공하는 부분과 키보드, 마우스 입력, 캡처, 데이터 스트리밍을 위한 API로 구성되어 있는 것을 확인했고 , 별도로 분리 하여 띄워도 문제가 없다고 판단해 웹 서버 부분에 대한 도커 파일을 작성했다.

3.5 Dockerfile

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.6 TinyPilot 웹 서버 설정

TinyPilot 웹 서버는 키보드 디바이스 파일과 마우스 디바이스 파일을 환경 변수로 등록하여 사용한다.
이부분운 이포스트 에서 이미 자세히 다루었다.
https://developsvai5096.tistory.com/43
그 이후 tinypilot은 해당 디바이스 파일들을 환경변수 를 통해 받아온다.

파일

DEBUG = False
SECRET_KEY = 'secret key'
FLASK_RUN_HOST = '0.0.0.0'
KEYBOARD_PATH = "/dev/hidg0"  # /dev/hidg1
MOUSE_PATH = "/dev/hidg1"  # /dev/hidg0
# Add other configuration variables here

위와 같이 환경 변수를 설정해 주어야 TinyPilot 웹 서버에서 정상적으로 키보드와 마우스 신호를 전송할 수 있다.
이거 알아낸다고 엄청난 삽질을 ...어우 토나와

4. uStreamer Docker 이미지

uStreamer는 TinyPilot이 원격 컴퓨터의 화면을 캡처하고 이를 실시간으로 전송하기 위해 사용하는 비디오 스트리밍 서버이다. 따라서 패키지만 클론한 다음, Docker 파일을 작성해 빌드한다.

# 베이스 이미지로 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 소스 코드 복사

COPY . /opt/ustreamer

WORKDIR /opt/ustreamer

# ustreamer 소스 코드 ->  https://github.com/pikvm/ustreamer.git . 

# ustreamer 빌드
RUN make && make install

# 노출할 포트 설정
EXPOSE 8001

5. Nginx 설정

Nginx는 클라이언트로부터 요청을 받아 각 엔드포인트마다 필요한 곳으로 프록시 역할을 한다. TinyPilot의 요청 순서는 다음과 같다:

5.1 TinyPilot 요청 순서 정리

  1. 클라이언트와 서버 간 연결 설정
    • 클라이언트가 TinyPilot 웹 인터페이스에 접속하여 기본 UI를 로드한다.
    • GET / 요청을 통해 index.html 페이지를 서버로부터 받아온다.
  2. WebSocket 연결 수립
    • 클라이언트가 원격 제어를 위해 WebSocket 연결을 수립한다.
    • connect 이벤트: 클라이언트가 서버와 WebSocket 연결을 설정하면 서버는 on_connect 핸들러를 호출하여 연결된 클라이언트 정보를 기록한다.
  3. 비디오 스트림 설정
    • 클라이언트는 비디오 스트림을 수신하기 위해 uStreamer와의 연결을 설정한다.
    • GET /stream 요청: 클라이언트는 스트림 경로를 요청한다. (개발 모드에서 테스트용 이미지가 반환될 수 있다.)
  4. 키보드 입력 처리
    • 사용자가 키보드 입력을 할 때마다 WebSocket을 통해 서버로 keystroke 이벤트가 전송된다.
    • keystroke 이벤트 핸들러: 서버는 on_keystroke 핸들러를 호출하여 입력을 파싱하고 변환한 후, 이를 물리적 키보드 장치로 전달한다.
  5. 마우스 이벤트 처리
    • 사용자가 마우스를 움직이거나 클릭할 때마다 WebSocket을 통해 서버로 mouse-event 이벤트가 전송된다.
    • mouse-event 이벤트 핸들러: 서버는 on_mouse_event 핸들러를 호출하여 입력을 파싱하고 변환한 후, 이를 물리적 마우스 장치로 전달한다.
  6. 키 릴리스 이벤트 처리
    • 사용자가 키 입력을 해제할 때 WebSocket을 통해 서버로 keyRelease 이벤트가 전송된다.
    • keyRelease 이벤트 핸들러: 서버는 on_key_release 핸들러를 호출하여 눌린 모든 키를 해제한다.
  7. 시스템 상태 및 설정 요청
    • 클라이언트는 TinyPilot의 다양한 상태 및 설정 정보를 얻기 위해 여러 RESTful API를 호출한다:
      • GET /api/version: TinyPilot의 현재 버전을 요청한다.
      • GET /api/update: 최신 업데이트 상태를 요청한다.
      • GET /api/hostname: 현재 호스트 이름을 요청한다.
      • GET /api/network/status: 네트워크 상태(이더넷, Wi-Fi)를 요청한다.
      • GET /api/settings/video: 비디오 스트리밍 설정을 요청한다.
  8. 설정 변경 및 명령 실행
    • 사용자가 TinyPilot의 설정을 변경하거나 특정 명령을 실행하기 위해 POST 또는 PUT 요청을 보낸다:
      • POST /api/shutdown: TinyPilot 시스템을 종료한다.
      • POST /api/restart: TinyPilot 시스템을 재시작한다.
      • PUT /api/hostname: 호스트 이름을 변경한다.
      • PUT /api/update: TinyPilot 소프트웨어를 업데이트한다.
      • PUT /api/settings/video: 비디오 스트리밍 설정을 변경한다.
      • POST /api/paste: 클립보드 내용을 원격 컴퓨터에 붙여넣기 한다.
  9. 연결 종료
    • WebSocket 연결 해제: 사용자가 브라우저 탭을 닫거나, 연결이 끊어질 때 disconnect 이벤트가 서버에 전송된다.
    • disconnect 이벤트 핸들러: 서버는 on_disconnect 핸들러를 호출하여 연결이 해제된 클라이언트 정보를 기록한다.

이와 같은 요청 순서로 동작하며, 이를 프록시하는 것이 Nginx의 역할이다.

5.2

user www-data;  
error\_log /var/log/nginx/error.log warn;  
pid /run/nginx.pid;  
worker\_processes "4";

events {  
worker\_connections 1024;  
multi\_accept off;  
}

http {  
include /etc/nginx/mime.types;  
default\_type application/octet-stream;  
server\_names\_hash\_bucket\_size 64;  
client\_max\_body\_size 64m;

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                   '$status $body_bytes_sent "$http_referer" '
                   '"$http_user_agent" "$http_x_forwarded_for"';
access_log  /var/log/nginx/access.log main buffer=16k flush=2m;

sendfile        on;
tcp_nopush      on;
tcp_nodelay     on;
keepalive_timeout  65;
keepalive_requests 100;
server_tokens on;

upstream tinypilot2 {
    server tinypilot:8000 fail_timeout=1s max_fails=600;
}
upstream ustreamer {
    server ustreamer:8001 fail_timeout=1s max_fails=600;
}

server {
    listen [::]:8900 default_server;
    listen 8900 default_server;
    server_name tinypilot;
    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 http://tinypilot2;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";

            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $http_host;
            proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /state {
            proxy_pass http://ustreamer;
    }

    location /stream {
            postpone_output 0;
            proxy_buffering off;
            proxy_ignore_headers X-Accel-Buffering;
            proxy_pass http://ustreamer;
    }

    location /snapshot {
            proxy_pass http://ustreamer;
    }

    location / {
            proxy_pass http://tinypilot:8000/;
    }


    location ~* ^/.+\.(html|js|js.map|css|woff|woff2)$ { 
    //여기서 중요한게 있는데 tinypilot 코드에서는 이부분에서 root workdir 에서 파일을 찾아 반환 하게 되었는데 , 분리 시켰기 때문에 tinypilot 컨테이너로 프록시 해주어야한다.
        proxy_pass http://tinypilot2;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        add_header Last-Modified $date_gmt;
        add_header Cache-Control 'public, max-age=10s';
    }


    location ~* ^/.+\.(jpg|jpeg|png|ico)$ { // 이하동문 
         proxy_pass http://tinypilot2;
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         add_header Last-Modified $date_gmt;
         add_header Cache-Control 'public, max-age=10s';
        }
    }
}

6. 도커 컴포즈

위에서 설정판 도커 파일로 생성된 이미지들을 통합 실행 하기 위해 도커 컴포즈 파일을 작성한다.

6.1 docker-compose.yml

version: '3.6'
services:
  nginx:
    image: nginx
    container_name: nginx
    restart: always
    depends_on:
      - ustreamer
      - tinypilot
    ports:
      - "8900:8900"
    volumes:
      - ./nginx/kvn.conf.template:/etc/nginx/nginx.conf  # nginx 설정 파일을 직접 nginx.conf로 마운트

  ustreamer:
    build:
      context: ./images/ustreamer/
    container_name: ustreamer
    restart: always
    ports:
      - "8001:8001"
    devices:
      - /dev/video0:/dev/video0
    command: ["ustreamer", "--host=0.0.0.0", "--resolution=1280x720", "--port=8001", "/dev/video0"] # ustreamer 실행, 호스트 설정, 해상도 설정 , 포트 , 캡쳐 카드 디바이스 파일 

  tinypilot:
    build:
      context: ./images/tinypilot/
    container_name: tinypilot
    restart: always
    ports:
      - "8000:8000"
    devices:
     - /dev/hidg0:/dev/hidg0
     - /dev/hidg1:/dev/hidg1
    volumes:
      - ./images/tinypilot/config/config.py:/opt/tinypilot/config.py  # Tinypilot 설정 파일 마운트

    command: ["python3", "/opt/tinypilot/app/main.py"]

컴포즈 가 정상적으로 빌드되고 실행 되는것이 확인 되면 각 이미지를 추출(하거나 또는 도커 허브에 올리거나)해서 다른 인스턴스 에서 띄울때 재빌드 를 방지하는것이 좋다.