Skip to content

DEPLOYMENT

lazybrain80 edited this page May 23, 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 (8 ์ปจํ…Œ์ด๋„ˆ) โ€”
์‹œ์Šคํ…œ + ์—ฌ์œ  ~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

๊ถŒ์žฅ: 1 Gbps Ethernet ร— 2 (์นด๋ฉ”๋ผ segment + ์ผ๋ฐ˜ segment ๋ถ„๋ฆฌ) + 5GHz Wi-Fi AP 2๋Œ€ (ํƒœ๋ธ”๋ฆฟ์šฉ ๋ณ„๋„ SSID(Service Set Identifier, ๋ฌด์„  ๋„คํŠธ์›Œํฌ ์‹๋ณ„์ž)).

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           (์™ธ๋ถ€ ์ด๋ฏธ์ง€)     โ”‚
โ”‚  โ”œโ”€โ”€ 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๋กœ ๋งคํ•‘).


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.secret์˜ HA_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