Skip to content

DEPLOYMENT

lazybrain80 edited this page Jun 30, 2026 · 3 revisions

배포 가이드 — 풀 로컬 실행 + 클라우드 빌드

모델: 모든 서비스는 전시장 로컬 PC에서 실행. 도커 이미지 빌드만 GitHub Actions(클라우드)에서 수행하고 컨테이너 레지스트리에 push. 로컬 PC는 핀(pin)된 버전을 pull해 실행.


0. 하드웨어 사양 (전시장)

0.1 권장 사양 (1세트)

본 시스템은 단일 사이트 풀 로컬 운영 + 동시 체험자 3명 + 카메라 ~8대 + 로봇 2대 기준.

노드 용도 권장 사양 추정 비용대
GPU 노드 Frigate(객체 탐지) CPU 8C16T (Intel i7/Ryzen 7), RAM 32GB, GPU NVIDIA RTX 4060 8GB VRAM 또는 USB Coral TPU, NVMe(NVM Express, 고속 저장 인터페이스) 1TB 중상
CPU 노드 mosquitto, HA, orchestrator, bridge, mcp-*, 관측 스택 CPU 4C8T (Intel i5/Ryzen 5), RAM 16GB, NVMe 512GB

또는 통합형(권장): 위 둘을 하나의 워크스테이션으로 묶어 단일 노드로 운영.

  • CPU 8C16T, RAM 32GB, GPU RTX 4060 8GB, NVMe 1TB, 1Gbps Ethernet ×2.
  • 컨테이너 리소스 분리는 docker compose의 deploy.resources 또는 Linux cgroup으로.

0.2 사양 산정 근거

컨테이너 RAM 추정 CPU 추정 GPU
Frigate (8 cam, 5fps detect) 1.5~2 GB 2 코어 TensorRT 또는 Coral 1개 — 8 GPU stream 가능
Home Assistant (recorder + entities) 600 MB ~ 1 GB 0.5 코어
Mosquitto 50 MB 0.1 코어
orchestrator (LangGraph) 400 MB 0.5 코어 (idle) ~ 2 코어 (Claude 호출 burst)
bridge (ReID 임베딩) 800 MB ~ 1.5 GB 1 코어 (5Hz position publish) (선택) GPU 0.5 GB for OSNet
mcp-robot-adapter 300 MB 0.5 코어
mcp-tablet-gateway 200 MB 0.2 코어
Prometheus + Loki + Grafana 1.5 GB 0.5 코어
합계 (헤드룸 30%) ~16 GB ~8 코어 ~8 GB VRAM

0.3 디스크 사용량

데이터 예상 사용 보존
HA recorder DB < 500 MB / 3일 (purge_keep_days: 3) 3일
Loki 로그 5 GB / 7일 7일
Prometheus 메트릭 10 GB / 15일 15일
Frigate 영상 클립 0 (record.enabled: false)
컨테이너 이미지 15~20 GB (9 컨테이너, cctv-relay 포함 — go2rtc 이미지 ~50MB)
시스템 + 여유 ~50 GB
합계 80100 GB → 512GB NVMe로 충분

0.4 네트워크

트래픽 추정 대역폭
RTSP(Real Time Streaming Protocol, 실시간 스트리밍 프로토콜) 입력 (8 카메라 × 720p × ~3Mbps) ~24 Mbps
MQTT(Message Queuing Telemetry Transport, 메시지 큐 텔레메트리 전송 프로토콜) (모든 도메인 토픽 합산, 피크) < 1 Mbps
Anthropic API (LLM(Large Language Model, 대규모 언어 모델) 호출) 가변, 보통 < 5 Mbps
ghcr.io(GitHub Container Registry, GitHub 컨테이너 레지스트리) pull (배포 시만) 가변
운영자 대시보드 viewing < 5 Mbps
cctv-relay 라이브 영상 (WebRTC → 전시 화면·태블릿, H.264 720p × 동시 뷰 n) ~2–3 Mbps / 뷰 (LAN egress)
RTSP 입력 중복 (cctv-relay 독립 pull, Frigate detect와 별개) +최대 ~24 Mbps (LAN ingress)

WebRTC 미디어는 브라우저↔cctv-relay P2P(LAN 내부)로 흐르므로 외부 회선 대역폭에는 영향이 없다. cctv-relay는 Frigate와 분리되어 RTSP를 독립 pull 하므로 카메라 입력이 detect 경로와 중복된다(장애 격리의 대가). 카메라 동시 연결·LAN 대역폭이 빡빡하면 단일-pull 변형 검토(SERVICES.md §2.5). 근거: SYSTEM_DESIGN.md §8.4.

권장: 1 Gbps Ethernet × 2 (카메라 segment + 일반 segment 분리) + 5GHz Wi-Fi AP 2대 (태블릿용 별도 SSID(Service Set Identifier, 무선 네트워크 식별자)).

개방 포트(전시장 LAN 내부): Frigate UI 5000, cctv-relay WebRTC 미디어 8555/tcp+udp(브라우저↔relay), cctv-relay API 1984/tcp(BFF 시그널링 — 외부 비공개). WebRTC가 LAN host candidate(8555)로 협상되므로 STUN/TURN 서버는 두지 않는다.

0.5 전원 / UPS

  • 메인 노드 + 네트워크 스위치 + AP는 UPS(Uninterruptible Power Supply, 무정전 전원공급장치) 보호 필수 (전시 운영 중 정전 → 데이터 손상 방지).
  • UPS 용량: 노드 ~150W + 네트워크 ~50W = 200W × 5분 백업 → 최소 600VA.
  • 카메라는 PoE(Power over Ethernet, 이더넷 전력 공급) 스위치 사용 시 자동 보호.
  • 로봇은 자체 배터리 — 영향 없음.

0.6 환경 (전시장 물리 조건)

  • 노드 발열·소음: 전시장 백오피스(관람객 비공개)에 설치, 직사광선·수분 회피.
  • 케이블: RTSP·로봇 통신은 가능한 유선, 태블릿만 무선.
  • 카메라 케이블 길이 > 50m 시 광 변환 또는 PoE+ 사용.

1. 토폴로지

┌──────────────────────────────────────────┐
│   GitHub (코드 저장소)                    │
│        │                                 │
│        │ git push                        │
│        ▼                                 │
│   GitHub Actions (CI/CD)                 │
│        │ docker build                    │
│        ▼                                 │
│   ghcr.io (Container Registry)           │
│        │  ghcr.io/OWNER/zerone/<svc>:TAG │
└────────┼─────────────────────────────────┘
         │
         │ docker pull (전시장에서 트리거)
         ▼
┌──────────────────────────────────────────┐
│  전시장 로컬 PC (Edge)                    │
│                                          │
│  docker-compose.yml                      │
│  ├── mosquitto         (외부 이미지)     │
│  ├── frigate           (외부 이미지)     │
│  ├── cctv-relay        (외부 이미지)     │
│  ├── home-assistant    (외부 이미지)     │
│  ├── bridge            (ghcr.io)         │
│  ├── orchestrator      (ghcr.io)         │
│  ├── mcp-robot-adapter (ghcr.io)         │
│  └── mcp-tablet-gateway(ghcr.io)         │
│                                          │
│  외부 의존: Anthropic API + 인터넷 회선   │
└──────────────────────────────────────────┘

원칙:

  • 빌드(클라우드)와 실행(로컬)이 완전히 분리.
  • 로컬은 이미지를 가져와 실행만 함 — 빌드 도구·소스코드 없음.
  • 버전은 명시적 태그로 핀(pin) — 전시 운영 중 의도치 않은 자동 갱신 방지.

2. 컨테이너 레지스트리 — GitHub Container Registry (ghcr.io)

왜 ghcr.io:

  • GitHub Actions와 네이티브 통합 (GITHUB_TOKEN만으로 push 인증).
  • 퍼블릭/프라이빗 모두 무료.
  • AWS ECR과 달리 IAM·VPC 설정 없이 바로 사용 가능.
  • 풀(pull) 인증도 PAT(Personal Access Token, 개인 액세스 토큰) 1개로 처리.

대안:

  • Docker Hub: 무료 티어 pull rate limit이 있어 로컬에서 자주 pull 시 막힐 수 있음 → 비추.
  • AWS ECR: AWS 계정·IAM이 필요 → 단독 사용엔 과대.

3. 이미지 태깅 전략

태그 의미 언제 생성
sha-<short-sha> 특정 커밋 모든 main 브랜치 push
main main의 최신 모든 main 브랜치 push (이전 main 태그 덮어씀)
vX.Y.Z 릴리즈 git tag v* push
latest 최신 안정 릴리즈 git tag v* push

전시 운영 권장: docker-compose에서 vX.Y.Z 또는 sha-<short-sha>명시적 핀. latest/main은 dev 환경에서만.

이유: 전시 도중 누군가 docker compose pull && up -d 실행했을 때 의도치 않은 신버전이 들어와 라이브 시연이 깨지는 사고를 방지.


4. CI/CD — GitHub Actions

4.1 워크플로 골격

# .github/workflows/build-images.yml
name: build-images

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_PREFIX: ${{ github.repository }}   # OWNER/zerone

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      services: ${{ steps.filter.outputs.changes }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            orchestrator:        services/orchestrator/**
            bridge:              services/bridge/**
            mcp-robot-adapter:   services/mcp-robot-adapter/**
            mcp-tablet-gateway:  services/mcp-tablet-gateway/**

  build:
    needs: detect-changes
    if: needs.detect-changes.outputs.services != '[]'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write       # ghcr.io push 권한
    strategy:
      fail-fast: false
      matrix:
        service: ${{ fromJSON(needs.detect-changes.outputs.services) }}
    steps:
      - uses: actions/checkout@v4

      - name: Log in to ghcr.io
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Compute tags
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix=sha-,format=short
            type=semver,pattern={{version}}
            type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}

      - uses: docker/setup-buildx-action@v3

      - name: Build & push
        uses: docker/build-push-action@v5
        with:
          context: services/${{ matrix.service }}
          platforms: linux/amd64                # 전시장 PC 아키텍처
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

4.2 동작

  • services/orchestrator/** 변경 + main push → ghcr.io/OWNER/zerone/orchestrator:main, :sha-abc1234 두 태그.
  • git tag v1.2.0 && git push --tags:v1.2.0, :latest 추가 push.
  • PR은 빌드만 하고 push 안 함 (dry-run 검증).

4.3 보안 스캔 (선택, 권장)

빌드 직후 Trivy로 취약점 스캔 추가:

      - name: Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}@${{ steps.meta.outputs.digest }}
          format: 'sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

5. 로컬 docker-compose.yml (전시장 PC)

레포의 deploy/ 디렉터리에 배치 — 빌드 컨텍스트나 소스코드 없이 compose 파일과 설정만.

# deploy/docker-compose.yml
version: "3.8"

x-image-prefix: &image-prefix ghcr.io/OWNER/zerone

networks:
  edge:
    driver: bridge

volumes:
  ha_config:
  frigate_media:
  mosquitto_data:

services:
  mosquitto:
    image: eclipse-mosquitto:2.0
    restart: always
    ports: ["1883:1883"]
    volumes:
      - ./mosquitto/config:/mosquitto/config:ro
      - mosquitto_data:/mosquitto/data
    networks: [edge]

  frigate:
    image: ghcr.io/blakeblackshear/frigate:0.14.1
    restart: always
    shm_size: "256mb"
    devices:
      - /dev/dri:/dev/dri
    env_file: [./.env, ./.env.secret]   # FRIGATE_MQTT_PASSWORD, FRIGATE_RTSP_USER/PASSWORD
    volumes:
      - ./frigate/config.yml:/config/config.yml:ro
      - frigate_media:/media/frigate
    ports: ["5000:5000"]
    networks: [edge]
    depends_on: [mosquitto]

  home-assistant:
    image: ghcr.io/home-assistant/home-assistant:2026.1
    restart: always
    network_mode: host
    volumes:
      - ha_config:/config

  bridge:
    image: ghcr.io/OWNER/zerone/bridge:${BRIDGE_TAG:-v1.0.0}
    restart: always
    env_file: [./.env, ./.env.secret]   # MOSQUITTO_PWD_BRIDGE
    networks: [edge]
    depends_on: [mosquitto]

  orchestrator:
    image: ghcr.io/OWNER/zerone/orchestrator:${ORCHESTRATOR_TAG:-v1.0.0}
    restart: always
    env_file: [./.env, ./.env.secret]
    extra_hosts:
      - "host.docker.internal:host-gateway"   # HA(host network)에 도달
    networks: [edge]
    depends_on: [mosquitto]

  mcp-robot-adapter:
    image: ghcr.io/OWNER/zerone/mcp-robot-adapter:${ROBOT_ADAPTER_TAG:-v1.0.0}
    restart: always
    env_file: [./.env, ./.env.secret]   # MOSQUITTO_PWD_ROBOT_ADAPTER, SPOT_USERNAME/PASSWORD
    networks: [edge]
    depends_on: [mosquitto]

  mcp-tablet-gateway:
    image: ghcr.io/OWNER/zerone/mcp-tablet-gateway:${TABLET_GATEWAY_TAG:-v1.0.0}
    restart: always
    ports: ["8080:8080"]    # 태블릿 WSS
    env_file: [./.env, ./.env.secret]   # MOSQUITTO_PWD_TABLET_GATEWAY
    networks: [edge]
    depends_on: [mosquitto]

버전은 .env로 분리 (배포 단위 관리):

# deploy/.env (git에 커밋, 시크릿 아님)
BRIDGE_TAG=v1.0.0
ORCHESTRATOR_TAG=v1.0.0
ROBOT_ADAPTER_TAG=v1.0.0
TABLET_GATEWAY_TAG=v1.0.0

시크릿은 별도 (.env.secret, gitignore):

ANTHROPIC_API_KEY=sk-ant-...
SPOT_USERNAME=...
SPOT_PASSWORD=...
HA_TOKEN=...
HA_URL=http://host.docker.internal:8123     # HA가 host network이므로 host gateway로 도달
FRIGATE_MQTT_PASSWORD=...
FRIGATE_RTSP_USER=...
FRIGATE_RTSP_PASSWORD=...
MOSQUITTO_PWD_ORCHESTRATOR=...
MOSQUITTO_PWD_BRIDGE=...
MOSQUITTO_PWD_ROBOT_ADAPTER=...
MOSQUITTO_PWD_TABLET_GATEWAY=...
MOSQUITTO_PWD_FRIGATE=...
MOSQUITTO_PWD_HOMEASSISTANT=...

docker-compose.yml에서 env_file: [./.env, ./.env.secret]로 둘 다 로드.

HA 네트워크 도달 주의: home-assistant 컨테이너는 mDNS·SSDP 호환을 위해 network_mode: host로 동작하므로, 같은 compose의 다른 서비스가 home-assistant 호스트명으로 접근 불가. 위처럼 모든 클라이언트는 host.docker.internal:8123을 사용해야 한다(extra_hosts로 매핑).

5.1 Mac 로컬 개발 스택 — CCTV 라이브 중계(cctv-relay) 포함

전시장 PC용 풀스택(위 §5)은 Linux·GPU·network_mode: host를 전제하므로 macOS 개발 머신에서 그대로 뜨지 않는다. 아래는 Mac에서 라이브 영상 중계 경로(브라우저 ↔ cctv-relay)를 빠르게 검증하기 위한 최소 스택이다 — mosquitto + home-assistant + cctv-relay(go2rtc) 세 개만 띄운다. CCTV "연계 서비스"는 detect/이벤트용 Frigate가 아니라 브라우저 라이브 영상을 책임지는 cctv-relay 다(SERVICES.md §2.5, 설계 근거 SYSTEM_DESIGN.md §8.4).

풀스택(§5)과의 차이

항목 전시장 풀스택 (§5) Mac 개발 스택 (§5.1)
서비스 수 mosquitto·frigate·HA·bridge·orchestrator·어댑터·gateway mosquitto·HA·cctv-relay 3개
CCTV 경로 Frigate(detect/이벤트) + cctv-relay(라이브) cctv-relay(라이브)만
HA 네트워크 network_mode: host 포트 매핑 8123:8123 (Mac은 host 네트워크 미지원)
HW 가속 /dev/dri 패스스루 불필요 — relay는 detect를 하지 않음(zero-copy 패스스루)
용도 운영 배포 라이브 중계·HA 대시보드 로컬 검증

Mac 개발 머신에서는 이 docker-compose.yml을 §5의 풀스택 대신 쓴다(같은 경로, 한 머신에 둘 중 하나만). 처음 구성한 최소 compose(mosquitto + home-assistant)에 cctv-relay 서비스를 더한 형태다.

디렉터리 레이아웃 (deploy/ 기준)

deploy/
├── docker-compose.yml            # 아래 compose (Mac 개발 머신용 최소 스택)
├── .env.secret                   # RTSP_USER / RTSP_PASSWORD (gitignore)
├── mosquitto/config/mosquitto.conf
├── config/cctv-relay.yaml        # go2rtc streams 맵
└── ha-config/                    # HA 첫 부팅 시 자동 생성

deploy/docker-compose.yml — 처음 구성한 최소 compose에 cctv-relay(go2rtc) 추가

# deploy/docker-compose.yml
services:
  mosquitto:
    image: eclipse-mosquitto:2.0
    restart: unless-stopped
    ports: ["1883:1883"]
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - mosquitto_data:/mosquitto/data
    networks: [edge]

  cctv-relay:                       # ← 추가: 브라우저용 CCTV 라이브 중계 (go2rtc)
    image: ghcr.io/alexxit/go2rtc:1.9.14
    restart: unless-stopped
    # command 미지정 — 이미지 기본 Cmd(go2rtc -config /config/go2rtc.yaml)를 그대로 사용.
    # ⚠ command를 덮어쓰면 go2rtc 바이너리까지 지워져 tini가 인자만 exec → FATAL.
    env_file: [./.env.secret]       # RTSP_USER, RTSP_PASSWORD
    volumes:
      - ./config/cctv-relay.yaml:/config/go2rtc.yaml:ro   # 컨테이너 기본 경로로 마운트
    ports:
      - "8555:8555/tcp"             # WebRTC 미디어 (브라우저 ↔ relay, ICE)
      - "8555:8555/udp"
      - "1984:1984"                 # go2rtc API/Web UI — 개발 중 직접 테스트용
    networks: [edge]
    # detect/MQTT를 하지 않으므로 mosquitto에 depends_on 하지 않음 (별도 미디어 평면)

  home-assistant:
    image: ghcr.io/home-assistant/home-assistant:2026.1
    restart: unless-stopped
    # network_mode: host  ← Mac에서는 제거
    ports: ["8123:8123"]          # ← 대신 명시적 포트 매핑
    volumes:
      - ./ha-config:/config
    networks: [edge]
    depends_on: [mosquitto]

networks:
  edge:
    driver: bridge

volumes:
  mosquitto_data:

설정 파일

deploy/mosquitto/config/mosquitto.conf (개발용 — 익명 허용, LAN 한정):

listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/

⚠️ allow_anonymous true로컬 개발 한정이다. 운영 ACL·비밀번호 정책은 SERVICES.md §1을 따른다.

deploy/config/cctv-relay.yaml (go2rtc — 소스 RTSP 한 줄당 스트림 1개):

# deploy/config/cctv-relay.yaml
streams:
  cafe_cam_01: rtsp://${RTSP_USER}:${RTSP_PASSWORD}@192.168.0.11:554/stream
  # H.265 카메라는 브라우저 호환(H.264)으로 트랜스코드:
  # lobby_cam_01:
  #   - rtsp://${RTSP_USER}:${RTSP_PASSWORD}@192.168.0.15:554/stream
  #   - 'ffmpeg:lobby_cam_01#video=h264#audio=opus'

webrtc:
  candidates:
    - 192.168.0.10:8555      # ← Mac의 LAN IP:8555 (브라우저가 닿는 주소). 같은 머신이면 127.0.0.1:8555

api:
  listen: ":1984"

⚠️ Mac WebRTC 주의: Docker Desktop은 컨테이너 네트워크를 NAT하므로 go2rtc가 자동 광고하는 host candidate(컨테이너 내부 IP)는 브라우저가 닿지 못한다. webrtc.candidates에 Mac의 실제 주소(ipconfig getifaddr en0로 확인한 LAN IP, 또는 같은 머신이면 127.0.0.1):8555를 명시해야 영상이 흐른다. 포트 8555/tcp+udp가 매핑돼 있어야 함.

deploy/.env.secret (gitignore — 평문 커밋 금지):

RTSP_USER=admin
RTSP_PASSWORD=...

사용법

cd deploy

# 1. 기동 (deploy/docker-compose.yml 자동 인식)
docker compose up -d

# 2. 상태 확인
docker compose ps

# 3. 각 서비스 검증
#  - cctv-relay: 브라우저로 go2rtc Web UI 열어 스트림 미리보기
open http://localhost:1984                       # streams 목록 → cafe_cam_01 → WebRTC 재생 확인
#  - Home Assistant: 온보딩 화면
open http://localhost:8123
#  - mosquitto: 구독/발행 왕복 (mosquitto-clients 필요)
mosquitto_sub -h localhost -p 1883 -t 'test/#' -v &
mosquitto_pub -h localhost -p 1883 -t test/ping -m hello

# 4. 브라우저 WebRTC 직접 확인 (BFF 없이 go2rtc로 바로)
open 'http://localhost:1984/webrtc.html?src=cafe_cam_01'

# 5. 로그 / 정리
docker compose logs -f cctv-relay
docker compose down            # 볼륨 유지
docker compose down -v         # 볼륨까지 삭제(초기화)
  • 카메라를 늘리려면 config/cctv-relay.yamlstreams:에 RTSP 한 줄 추가 후 docker compose restart cctv-relay — 코드 변경 불필요(SERVICES.md §2.5).
  • HA에서 MQTT를 쓰려면 통합 추가 시 브로커 호스트를 mosquitto(compose 내부 네트워크명), 포트 1883으로 지정한다.

⚠️ 로컬 브로커 대안(권장 환경에 따라): 개발용 mosquitto는 보통 원격 Mac Studio docker에서 상시 실행되므로, 그 브로커를 재사용한다면 위 compose에서 mosquitto 서비스를 빼고 HA·다른 클라이언트의 브로커 호스트를 Mac Studio 주소로 지정한다. 이 경우 cctv-relay는 브로커와 무관하므로 그대로 둔다.


6. 로컬 PC 초기 셋업 (1회)

# 1. Docker + Compose 설치
sudo apt-get install -y docker.io docker-compose-plugin

# 2. ghcr.io 프라이빗 이미지 pull 권한 (퍼블릭 레포면 생략 가능)
#    GitHub PAT 생성 (read:packages 권한만)
echo $GHCR_TOKEN | docker login ghcr.io -u GITHUB_USERNAME --password-stdin

# 3. 배포 디렉터리만 클론 (서비스 소스 불필요)
git clone --depth 1 --filter=blob:none --sparse \
  https://github.com/OWNER/zerone.git
cd zerone
git sparse-checkout set deploy

# 4. 시크릿 생성
cp deploy/.env.secret.example deploy/.env.secret
$EDITOR deploy/.env.secret

# 5. 첫 부팅
cd deploy
docker compose pull
docker compose up -d
docker compose logs -f

옵션: 소스코드 전체가 필요 없으면 deploy/ 디렉터리만 별도 레포로 분리해 더 단순화 가능.


7. 업데이트 절차

7.1 일반 업데이트 (권장)

cd deploy

# 1. 변경할 버전을 .env에서 명시적으로 갱신
$EDITOR .env       # ORCHESTRATOR_TAG=v1.1.0

# 2. 새 이미지만 pull
docker compose pull

# 3. 변경된 컨테이너만 교체 (의존성 자동)
docker compose up -d

# 4. 헬스 확인
docker compose ps
docker compose logs --tail=50 orchestrator

7.2 전시 운영 중 핫픽스 — 단일 서비스만 교체

docker compose pull orchestrator
docker compose up -d --no-deps orchestrator

7.3 롤백

$EDITOR .env       # 이전 버전 SHA 또는 태그로 되돌림
docker compose pull && docker compose up -d

이미지가 ghcr.io에 보존되므로 롤백은 1분 내.

7.4 자동 업데이트는 비추

Watchtower 등으로 자동 pull 금지 — 전시 도중 무인 갱신은 사고 위험. 항상 명시적 명령으로.


8. 외부 이미지(Frigate/HA/Mosquitto)도 핀(pin)

위 compose에서 2.0, 0.14.1, 2026.1처럼 명시적 버전을 적었다. 이유:

  • latest 태그는 외부 메인테이너의 릴리즈 시점에 따라 갑자기 갱신됨 → 전시 중 회귀 위험.
  • 업그레이드는 사전 검증 후 의도적으로.

업그레이드 절차:

  1. 스테이징 환경(또는 사무실 미러)에서 새 버전 검증.
  2. 동작 확인되면 compose 수정·커밋·전시장 PC에 배포.

9. 시크릿·자격증명 관리

9.1 비밀의 위치

환경 비밀 저장 위치 회전 주기
GitHub Actions (빌드) (없음)
GitHub Actions (push) GITHUB_TOKEN (자동) GitHub 내장 자동 (per-job)
로컬 PC (pull) ghcr PAT (read:packages) ~/.docker/config.json 90일
로컬 PC (실행) API key·로봇 자격·HA 토큰·MQTT 비번 deploy/.env.secret (git ignore, chmod 600) 항목별 (아래)
Mosquitto 비번 hashed in passwd 파일 ./mosquitto/config/passwd (root:1883, 0600) 분기 회전
Frigate UI 인증 기본 미지원 → reverse proxy 인증 HA Authelia 또는 LAN ACL(Access Control List, 접근 제어 목록)

원칙: 비밀은 빌드 이미지에 포함 금지 — 항상 런타임 환경변수로 주입.

9.2 항목별 발급·회전 절차

Anthropic API key (ANTHROPIC_API_KEY)

  • 발급: console.anthropic.com → API Keys → 신규 발급, name=zerone-prod.
  • 회전: 분기 1회. 신규 발급 → .env.secret 갱신 → docker compose up -d orchestrator 무중단 교체 → 구 키 폐기.
  • 모니터링: 사용량 알림 임계 설정(예: 일 $50 초과 시 이메일).

Home Assistant long-lived access token (HA_TOKEN)

  • 발급 절차 (HA 첫 부팅 후):
    1. HA에서 운영자 계정 생성 (zerone-ops).
    2. 우측 상단 프로필 아이콘 → "Long-Lived Access Tokens" 섹션.
    3. "Create Token" 클릭, name=orchestrator-prod 입력.
    4. 발급된 토큰 즉시 .env.secretHA_TOKEN에 기입 (한 번만 표시됨).
  • 회전: 6개월 1회 또는 운영자 인수인계 시. 신규 발급 → 갱신 → 구 토큰 revoke.
  • 권한: HA는 토큰을 발급한 사용자의 권한을 그대로 받으므로 zerone-ops 계정에 최소 권한만 부여.

ghcr.io PAT (이미지 pull)

  • 발급: github.com/settings/tokens (classic) → read:packages 단일 스코프, expiration 90일.
  • 갱신: 만료 7일 전 알림 → 신규 발급 → docker login ghcr.io 재실행 → ~/.docker/config.json 자동 갱신.
  • 운영 자동화: 만료일을 캘린더에 등록 + Slack/이메일 알림.

SPOT 자격 (SPOT_USERNAME, SPOT_PASSWORD)

  • service account 생성: SPOT 관리 콘솔에서 운영자 본인 계정이 아닌 dedicated service account 생성:
    • 이름: zerone-spot-svc
    • 권한: Operator(이동·동작 명령) + EStop(안전). Admin 권한 부여 금지.
  • 회전: 6개월 1회.

Mosquitto 사용자 비번

  • 생성: mosquitto_passwd -c /mosquitto/config/passwd <user> (대화형). 자동화 시 mosquitto_passwd -b passwd <user> <pwd>.
  • passwd 파일 권한: chown 1883:1883 passwd && chmod 0600 passwd.
  • 회전: 분기 1회. 사용자별 mosquitto_passwd -b 갱신 후 mosquitto reload (docker kill -s HUP mosquitto).

태블릿 페어링 토큰

  • 형식: opaque random 32-byte URL-safe (JWT 불필요 — 단일 사이트, 검증 단순).
  • TTL: 4시간 (전시 1세션 충분, 분실 시 짧은 윈도).
  • 발급: mcp-tablet-gateway/pair REST(Representational State Transfer, 표현 상태 전이) → Authorization: Bearer <token> 헤더로 후속 WSS(WebSocket Secure, 보안 웹소켓) 인증.
  • 만료 시: 자동 페어링 화면으로 복귀 → 재발급.
  • 저장: mcp-tablet-gateway in-memory만 (Redis 등 외부 저장 X). 컨테이너 재기동 시 모든 페어링 무효 — 운영 중 영향 미미.

Frigate UI 인증

  • Frigate 자체에는 기본 인증 없음. 다음 중 하나:
    • 권장: Frigate를 LAN 내부 IP에만 바인딩(127.0.0.1:5000) + HA의 ingress add-on을 통해 노출 → HA 인증으로 보호.
    • 또는 Cloudflare Tunnel + Cloudflare Access(이메일·SSO(Single Sign-On, 단일 로그인 인증)).
    • 또는 LAN 자체를 신뢰 영역으로 두고 미인증 (운영자만 LAN 접근).

운영자 대시보드 (HA) 인증 강도

  • HA 기본 username/password + MFA (HA 내장 TOTP(Time-based One-Time Password, 시간 기반 일회용 비밀번호) 지원 — 사용자 프로필에서 "Enable two-factor authentication").
  • 운영자 인원 5명 이하면 충분. 그 이상이면 Authelia add-on으로 SSO·LDAP(Lightweight Directory Access Protocol, 경량 디렉터리 접근 프로토콜) 연동 검토.
  • Lovelace 카드 단위 권한은 HA의 auth_provider + custom_card 권한 설정으로.

9.3 시크릿 노출 시 대응

  • ghcr PAT 노출 → 즉시 GitHub에서 revoke → 신규 발급.
  • HA 토큰 노출 → HA Profile에서 토큰 revoke.
  • Anthropic API key 노출 → console에서 즉시 revoke + 사용량 감사 → 신규 발급.
  • Mosquitto 비번 노출 → 모든 사용자 회전 + 짧은 시간 내 ACL 미경유 publish 로그 확인.
  • 사고 기록은 운영 일지에 남기고 회전 주기 일시 단축.

10. CI에 추가 권장 사항

10.1 PR에 빌드 검증

  • PR 시 빌드만 하고 push 안 함 (위 워크플로 이미 반영).
  • 빌드 실패 = 머지 차단 (branch protection rule).

10.2 SBOM 생성

      - uses: anchore/sbom-action@v0
        with:
          image: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:sha-${{ github.sha }}
          format: spdx-json

10.3 이미지 서명 (선택, 보안 강화 시)

  • Cosign + GitHub OIDC(OpenID Connect, OpenID 연결 인증 프로토콜)로 keyless signing.
  • 로컬 pull 시 cosign verify로 무결성 검증.

10.4 GitHub Secrets 등록 절차

CI가 외부 도구 호출 시 사용하는 시크릿은 GitHub repo Settings → Secrets and variables → Actions에 등록.

Secret 이름 용도 발급 위치 필수 여부
GITHUB_TOKEN ghcr.io push 인증 자동(GitHub Actions 내장) 필수 (자동)
TRIVY_DB_TOKEN 사설 vuln DB 접근 (사용 시) aquasec.com 선택
COSIGN_KEY (keyed signing 시) 로컬 cosign generate-key-pair 선택
COSIGN_PASSWORD 위 키 암호 위와 함께 선택
SLACK_WEBHOOK_URL 빌드 실패 알림 Slack 앱 → Incoming Webhook 선택
SBOM_UPLOAD_URL SBOM 외부 저장소 보안팀 endpoint 선택

Keyless cosign (권장): GitHub OIDC로 키 없이 서명 → COSIGN_KEY/PASSWORD 불필요.

permissions:
  id-token: write          # OIDC
  packages: write
  contents: read

# build job 마지막에
      - name: Sign image (keyless)
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          cosign sign --yes \
            ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}@${{ steps.build.outputs.digest }}

시크릿 회전:

  • 필요 시 GitHub UI에서 값 갱신 — 워크플로 변경 불필요.
  • 만료 모니터링은 운영자 캘린더에 기록.

환경별 분리 (운영/스테이징):

  • GitHub Environment 기능으로 production/staging 분리.
  • production 시크릿은 main 브랜치 push 시에만 노출 (require reviewers).

11. 모니터링 (전부 로컬)

docker-compose.yml에 관측 스택을 함께 추가:

  prometheus:
    image: prom/prometheus:v2.55.0
    volumes: [./prometheus.yml:/etc/prometheus/prometheus.yml:ro]
    ports: ["9090:9090"]
    networks: [edge]

  grafana:
    image: grafana/grafana:11.3.0
    volumes: [./grafana:/etc/grafana/provisioning:ro]
    ports: ["3000:3000"]
    networks: [edge]

  loki:
    image: grafana/loki:3.2.0
    volumes: [./loki-config.yml:/etc/loki/local-config.yaml:ro]
    networks: [edge]

운영자 PC가 같은 LAN이면 http://EDGE_IP:3000 으로 Grafana 접속.

원격 모니터링이 필요하면 Cloudflare Tunnel 또는 Tailscale로 위 포트만 외부 노출(무료 티어로 충분).


12. 디스크·백업·복구

12.1 자산 분류

자산 위치 휘발성 백업 우선순위
HA recorder DB ha_config 볼륨/home-assistant_v2.db 단기 영속 (3일 자동 purge) 낮음 (재구축 가능)
HA configuration (yaml·자동화·dashboard) ha_config 볼륨/* 영속 높음
Mosquitto passwd·acl·data mosquitto_data 볼륨 + ./mosquitto/config/ 영속 (passwd) / 휘발 (data) 높음 (passwd)
Frigate config ./frigate/config.yml (git에 커밋) git 관리 git만
Frigate clips (미저장)
캘리브레이션 (config/calibration/*.yaml) git에 커밋 git 관리 git만
.env·.env.secret 호스트 디스크 영속 높음 (시크릿)
컨테이너 이미지 ghcr.io 클라우드 보존 불필요 (registry가 SoT)
자체 서비스 메모리 상태 RAM only 휘발 불필요

12.2 백업 전략

  • 일일 백업 (cron 매일 03:00):
    # /etc/cron.d/zerone-backup
    0 3 * * * root /usr/local/bin/zerone-backup.sh
    # zerone-backup.sh
    set -euo pipefail
    STAMP=$(date +%Y%m%d-%H%M%S)
    DEST=/mnt/nas/zerone-backup
    tar -czf "$DEST/ha-$STAMP.tar.gz" -C /var/lib/docker/volumes/zerone_ha_config _data
    cp /opt/zerone/deploy/mosquitto/config/passwd "$DEST/passwd-$STAMP"
    cp /opt/zerone/deploy/mosquitto/config/acl "$DEST/acl-$STAMP"
    # .env.secret은 별도 vault(예: 운영자 1Password) 수동 보관 — cron 백업 X
    find "$DEST" -type f -mtime +14 -delete   # 14일 보존
  • NAS 미러: NAS 자체가 RAID + off-site sync (운영자 책임 외부 정책).
  • 개인정보 영향: HA recorder는 zone 점유·로봇 상태만 — 식별자 없음. 그대로 백업 가능. .env.secret은 별도 보관.

12.3 RPO·RTO

시나리오 RPO (데이터 손실 허용) RTO (복구 목표 시간)
컨테이너 단일 크래시 0 (in-memory만 휘발) < 30s (restart: always)
호스트 디스크 일부 손상 < 24시간 (마지막 cron 백업) < 1시간 (NAS에서 복원)
호스트 전체 장애 < 24시간 < 4시간 (대체 하드웨어 부팅 + 복원 + 캘리브레이션 일부)
시크릿 노출 즉시 < 30분 (회전 절차 §9.2)
전시 시작 D-day 장애 0 (시작 못 함) (해당 일 시연 취소)

12.4 복구 절차

호스트 단일 디스크 복구

# 1. 새 디스크에 OS·Docker 설치
# 2. NAS 마운트
sudo mount /mnt/nas
# 3. 가장 최근 백업 추출
LATEST=$(ls -t /mnt/nas/zerone-backup/ha-*.tar.gz | head -1)
docker volume create zerone_ha_config
tar -xzf "$LATEST" -C /var/lib/docker/volumes/zerone_ha_config/_data --strip 1
# 4. mosquitto 비밀
cp /mnt/nas/zerone-backup/passwd-* deploy/mosquitto/config/passwd
chown 1883:1883 deploy/mosquitto/config/passwd
# 5. .env.secret은 운영자가 수동 복구 (1Password 등)
# 6. 부팅
cd deploy && docker compose pull && docker compose up -d
# 7. 캘리브레이션 검증 (호스트가 같은 위치면 그대로 유효)

캘리브레이션 손실 시

  • config/calibration/cameras.yaml·robots.yaml은 git에 커밋되어 있어 git checkout으로 복구.
  • 카메라가 물리적으로 위치 안 바뀌었으면 그대로 사용. 재캘리브레이션 필요 시 SYSTEM_DESIGN.md §8.3 절차 따름.

12.5 복구 dry-run

  • 분기 1회 사무실 미러 환경에서 백업 → 복원 → 헬스체크 시퀀스 dry-run.
  • 결과를 운영 일지에 기록 (마지막 dry-run 일자 + 발견된 이슈).

13. 페일오버 시나리오 (요약)

장애 동작
Anthropic API 단절 HA 자동화의 rule-based 폴백, 운영자 알림
인터넷 단절 위와 동일 (LLM 외 기능은 영향 없음)
ghcr.io 단절 새 배포만 막힘, 실행 중 컨테이너는 정상
Edge PC 정전 UPS 후 systemd가 docker compose 재시작
컨테이너 크래시 restart: always로 자동 복구
로봇 fault 안전 인터록으로 명령 거부, 다른 로봇으로 시나리오 대체

14. 단계적 도입

  1. Phase 0 — 로컬 단독 (1주): 최소 컴포즈(MQTT + HA + bridge + orchestrator), 더미 카메라/로봇.
  2. Phase 1 — 실 카메라 + Frigate (1주): 1개 zone enter/exit 검증.
  3. Phase 2 — SPOT 통합 (2주): mcp-robot-adapter, 안전 인터록, e-stop(emergency stop, 긴급 정지).
  4. Phase 3 — LLM·태블릿 (2주): orchestrator + Claude tool use, 1개 zone playbook 종단.
  5. Phase 4 — 4 zone + 2번째 로봇 (3주): cross-camera ReID, 멀티 로봇.
  6. Phase 5 — CI/CD(Continuous Integration / Continuous Deployment, 지속 통합/지속 배포) 자동화 (1주): GitHub Actions·이미지 태깅 도입. 그 전까지는 로컬 빌드 OK.
  7. Phase 6 — 관측·운영 강화 (1주): Prometheus/Grafana/Loki, 알림.

CI/CD는 Phase 5에 도입해도 충분. 그 전엔 로컬에서 docker compose build로 작업.


15. 배포 체크리스트 (전시 오픈 D-1)

  • .env의 모든 태그가 릴리즈 버전(vX.Y.Z)으로 핀(pin)되어 있음
  • .env.secret이 채워져 있고 권한 600
  • docker compose ps 모든 서비스 healthy
  • Anthropic API key 잔여 한도 확인
  • HA 자동화 폴백 시나리오 dry-run
  • 로봇 e-stop 물리 테스트
  • 카메라별 zone polygon 캘리브레이션 검증
  • 태블릿 페어링 → 인터랙션 종단 시나리오 1회
  • UPS 잔여 용량 + 로봇 배터리 100%
  • Grafana 대시보드 운영자 PC에서 접근 가능
  • 롤백 절차 문서 운영자에게 공유 (.env 이전 태그로 변경 → pull → up -d)

Clone this wiki locally