-
Notifications
You must be signed in to change notification settings - Fork 0
DEPLOYMENT
모델: 모든 서비스는 전시장 로컬 PC에서 실행. 도커 이미지 빌드만 GitHub Actions(클라우드)에서 수행하고 컨테이너 레지스트리에 push. 로컬 PC는 핀(pin)된 버전을 pull해 실행.
본 시스템은 단일 사이트 풀 로컬 운영 + 동시 체험자 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으로.
| 컨테이너 | 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 |
| 데이터 | 예상 사용 | 보존 |
|---|---|---|
| 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 | — |
| 합계 |
| 트래픽 | 추정 대역폭 |
|---|---|
| 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-relayP2P(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 서버는 두지 않는다.
- 메인 노드 + 네트워크 스위치 + AP는 UPS(Uninterruptible Power Supply, 무정전 전원공급장치) 보호 필수 (전시 운영 중 정전 → 데이터 손상 방지).
- UPS 용량: 노드 ~150W + 네트워크 ~50W = 200W × 5분 백업 → 최소 600VA.
- 카메라는 PoE(Power over Ethernet, 이더넷 전력 공급) 스위치 사용 시 자동 보호.
- 로봇은 자체 배터리 — 영향 없음.
- 노드 발열·소음: 전시장 백오피스(관람객 비공개)에 설치, 직사광선·수분 회피.
- 케이블: RTSP·로봇 통신은 가능한 유선, 태블릿만 무선.
- 카메라 케이블 길이 > 50m 시 광 변환 또는 PoE+ 사용.
┌──────────────────────────────────────────┐
│ 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) — 전시 운영 중 의도치 않은 자동 갱신 방지.
왜 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이 필요 → 단독 사용엔 과대.
| 태그 | 의미 | 언제 생성 |
|---|---|---|
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 실행했을 때 의도치 않은 신버전이 들어와 라이브 시연이 깨지는 사고를 방지.
# .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-
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 검증).
빌드 직후 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'레포의 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로 매핑).
전시장 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.yaml의streams:에 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는 브로커와 무관하므로 그대로 둔다.
# 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/디렉터리만 별도 레포로 분리해 더 단순화 가능.
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 orchestratordocker compose pull orchestrator
docker compose up -d --no-deps orchestrator$EDITOR .env # 이전 버전 SHA 또는 태그로 되돌림
docker compose pull && docker compose up -d이미지가 ghcr.io에 보존되므로 롤백은 1분 내.
Watchtower 등으로 자동 pull 금지 — 전시 도중 무인 갱신은 사고 위험. 항상 명시적 명령으로.
위 compose에서 2.0, 0.14.1, 2026.1처럼 명시적 버전을 적었다. 이유:
-
latest태그는 외부 메인테이너의 릴리즈 시점에 따라 갑자기 갱신됨 → 전시 중 회귀 위험. - 업그레이드는 사전 검증 후 의도적으로.
업그레이드 절차:
- 스테이징 환경(또는 사무실 미러)에서 새 버전 검증.
- 동작 확인되면 compose 수정·커밋·전시장 PC에 배포.
| 환경 | 비밀 | 저장 위치 | 회전 주기 |
|---|---|---|---|
| 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, 접근 제어 목록) | — |
원칙: 비밀은 빌드 이미지에 포함 금지 — 항상 런타임 환경변수로 주입.
-
발급: console.anthropic.com → API Keys → 신규 발급, name=
zerone-prod. -
회전: 분기 1회. 신규 발급 →
.env.secret갱신 →docker compose up -d orchestrator무중단 교체 → 구 키 폐기. - 모니터링: 사용량 알림 임계 설정(예: 일 $50 초과 시 이메일).
-
발급 절차 (HA 첫 부팅 후):
- HA에서 운영자 계정 생성 (
zerone-ops). - 우측 상단 프로필 아이콘 → "Long-Lived Access Tokens" 섹션.
- "Create Token" 클릭, name=
orchestrator-prod입력. - 발급된 토큰 즉시
.env.secret의HA_TOKEN에 기입 (한 번만 표시됨).
- HA에서 운영자 계정 생성 (
- 회전: 6개월 1회 또는 운영자 인수인계 시. 신규 발급 → 갱신 → 구 토큰 revoke.
-
권한: HA는 토큰을 발급한 사용자의 권한을 그대로 받으므로
zerone-ops계정에 최소 권한만 부여.
-
발급: github.com/settings/tokens (classic) →
read:packages단일 스코프, expiration 90일. -
갱신: 만료 7일 전 알림 → 신규 발급 →
docker login ghcr.io재실행 →~/.docker/config.json자동 갱신. - 운영 자동화: 만료일을 캘린더에 등록 + Slack/이메일 알림.
-
service account 생성: SPOT 관리 콘솔에서 운영자 본인 계정이 아닌 dedicated service account 생성:
- 이름:
zerone-spot-svc - 권한:
Operator(이동·동작 명령) +EStop(안전).Admin권한 부여 금지.
- 이름:
- 회전: 6개월 1회.
-
생성:
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의/pairREST(Representational State Transfer, 표현 상태 전이) →Authorization: Bearer <token>헤더로 후속 WSS(WebSocket Secure, 보안 웹소켓) 인증. - 만료 시: 자동 페어링 화면으로 복귀 → 재발급.
- 저장:
mcp-tablet-gatewayin-memory만 (Redis 등 외부 저장 X). 컨테이너 재기동 시 모든 페어링 무효 — 운영 중 영향 미미.
- Frigate 자체에는 기본 인증 없음. 다음 중 하나:
-
권장: Frigate를 LAN 내부 IP에만 바인딩(
127.0.0.1:5000) + HA의 ingress add-on을 통해 노출 → HA 인증으로 보호. - 또는 Cloudflare Tunnel + Cloudflare Access(이메일·SSO(Single Sign-On, 단일 로그인 인증)).
- 또는 LAN 자체를 신뢰 영역으로 두고 미인증 (운영자만 LAN 접근).
-
권장: Frigate를 LAN 내부 IP에만 바인딩(
- 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 권한 설정으로.
- ghcr PAT 노출 → 즉시 GitHub에서 revoke → 신규 발급.
- HA 토큰 노출 → HA Profile에서 토큰 revoke.
- Anthropic API key 노출 → console에서 즉시 revoke + 사용량 감사 → 신규 발급.
- Mosquitto 비번 노출 → 모든 사용자 회전 + 짧은 시간 내 ACL 미경유 publish 로그 확인.
- 사고 기록은 운영 일지에 남기고 회전 주기 일시 단축.
- PR 시 빌드만 하고 push 안 함 (위 워크플로 이미 반영).
- 빌드 실패 = 머지 차단 (branch protection rule).
- uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:sha-${{ github.sha }}
format: spdx-json- Cosign + GitHub OIDC(OpenID Connect, OpenID 연결 인증 프로토콜)로 keyless signing.
- 로컬 pull 시
cosign verify로 무결성 검증.
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).
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로 위 포트만 외부 노출(무료 티어로 충분).
| 자산 | 위치 | 휘발성 | 백업 우선순위 |
|---|---|---|---|
| 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 | 휘발 | 불필요 |
-
일일 백업 (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은 별도 보관.
| 시나리오 | RPO (데이터 손실 허용) | RTO (복구 목표 시간) |
|---|---|---|
| 컨테이너 단일 크래시 | 0 (in-memory만 휘발) | < 30s (restart: always) |
| 호스트 디스크 일부 손상 | < 24시간 (마지막 cron 백업) | < 1시간 (NAS에서 복원) |
| 호스트 전체 장애 | < 24시간 | < 4시간 (대체 하드웨어 부팅 + 복원 + 캘리브레이션 일부) |
| 시크릿 노출 | 즉시 | < 30분 (회전 절차 §9.2) |
| 전시 시작 D-day 장애 | 0 (시작 못 함) | (해당 일 시연 취소) |
# 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 절차 따름.
- 분기 1회 사무실 미러 환경에서 백업 → 복원 → 헬스체크 시퀀스 dry-run.
- 결과를 운영 일지에 기록 (마지막 dry-run 일자 + 발견된 이슈).
| 장애 | 동작 |
|---|---|
| Anthropic API 단절 | HA 자동화의 rule-based 폴백, 운영자 알림 |
| 인터넷 단절 | 위와 동일 (LLM 외 기능은 영향 없음) |
| ghcr.io 단절 | 새 배포만 막힘, 실행 중 컨테이너는 정상 |
| Edge PC 정전 | UPS 후 systemd가 docker compose 재시작 |
| 컨테이너 크래시 |
restart: always로 자동 복구 |
| 로봇 fault | 안전 인터록으로 명령 거부, 다른 로봇으로 시나리오 대체 |
- Phase 0 — 로컬 단독 (1주): 최소 컴포즈(MQTT + HA + bridge + orchestrator), 더미 카메라/로봇.
- Phase 1 — 실 카메라 + Frigate (1주): 1개 zone enter/exit 검증.
- Phase 2 — SPOT 통합 (2주): mcp-robot-adapter, 안전 인터록, e-stop(emergency stop, 긴급 정지).
- Phase 3 — LLM·태블릿 (2주): orchestrator + Claude tool use, 1개 zone playbook 종단.
- Phase 4 — 4 zone + 2번째 로봇 (3주): cross-camera ReID, 멀티 로봇.
- Phase 5 — CI/CD(Continuous Integration / Continuous Deployment, 지속 통합/지속 배포) 자동화 (1주): GitHub Actions·이미지 태깅 도입. 그 전까지는 로컬 빌드 OK.
- Phase 6 — 관측·운영 강화 (1주): Prometheus/Grafana/Loki, 알림.
CI/CD는 Phase 5에 도입해도 충분. 그 전엔 로컬에서 docker compose build로 작업.
-
.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)