# ๐Ÿณ MAM Remote Broker Production Deployment (`docker/`) ์ด ๋””๋ ‰ํ„ฐ๋ฆฌ๋Š” multi-agent-mux (MAM) ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ๊ด€์ธก ๋ฐฑํ”Œ๋ ˆ์ธ์„ ์œ„ํ•œ ์›๊ฒฉ `nats-server` ํ”„๋กœ๋•์…˜ ๋ธŒ๋กœ์ปค ๋ฐฐํฌ ์ž์‚ฐ์˜ **์ •๋ณธ(canonical)**์ž…๋‹ˆ๋‹ค. ์ƒ์„ธ ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๋ฐฐ๊ฒฝ ์ง€์‹์€ [`PRIVATE_SERVER.md`](../PRIVATE_SERVER.md) ยง9 ๋ฅผ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค. --- ## 1. ๋ฌด์—‡์ธ๊ฐ€ MAM ์€ ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ๊ฐ„ ๋น„๋™๊ธฐ ์ž‘์—… ์กฐ์ • ๋ฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฑํ”Œ๋ ˆ์ธ์œผ๋กœ `nats-server` (MQTT 3.1.1 + JetStream + WebSocket) ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ณธ ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ Compose ํŒŒ์ผ๊ณผ ์„ค์ •์€ ์‚ฌ์„ค ์˜ค๋ฒ„๋ ˆ์ด ๋„คํŠธ์›Œํฌ(Tailscale ๋“ฑ)๋ฅผ ํ†ตํ•ด 0๊ฐœ์˜ ๊ณต์ธ ํฌํŠธ ๊ฐœ๋ฐฉ์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์šด์˜๋˜๋Š” ๋‹จ์ผ ๋ธŒ๋กœ์ปค ํ—ˆ๋ธŒ(`mam-hub`)๋ฅผ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค. --- ## 2. ์‚ฌ์ „ ์š”๊ตฌ์‚ฌํ•ญ - **Docker Engine** (24.0+) ๋ฐ **Docker Compose V2** (`docker compose` CLI) - **Tailscale** ๋˜๋Š” WireGuard ์‚ฌ์„ค VPN (๊ถŒ์žฅ: ๋ชจ๋ธ T ์‚ฌ์„ค ์˜ค๋ฒ„๋ ˆ์ด) - ํ˜ธ์ŠคํŠธ ๋ฐฉํ™”๋ฒฝ (UFW ๋“ฑ) ๊ธฐ๋ณธ ์ฐจ๋‹จ ์ •์ฑ… (๊ณต์ธ IP ๊ฐœ๋ฐฉ ํฌํŠธ ์—†์Œ) --- ## 3. 5๋ถ„ ๋น ๋ฅธ ๋ฐฐํฌ ```bash # 1. docker/ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋Œ€์ƒ ์›๊ฒฉ ์„œ๋ฒ„๋กœ ๋ณต์‚ฌ rsync -avz ./docker/ user@your-server:~/mam-broker/ # 2. ์„œ๋ฒ„ ์ ‘์† ํ›„ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ ๋ณต์‚ฌ ๋ฐ ๊ถŒํ•œ ์ œํ•œ cd ~/mam-broker cp .env.example .env chmod 600 .env # 3. 4์ข…์˜ ์‹œํฌ๋ฆฟ ์•”ํ˜ธ ์ƒ์„ฑ ๋ฐ .env ์— ์ž…๋ ฅ # โš  ๋ฐ˜๋“œ์‹œ openssl rand -base64 32 ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค (๋ ‰์„œ ์ข…๊ฒฐ์ž ์ถฉ๋Œ ๋ฐฉ์ง€) openssl rand -base64 32 # MAM_BROKER_PASS openssl rand -base64 32 # MAM_OBSERVER_PASS openssl rand -base64 32 # HOME_BROKER_PASS openssl rand -base64 32 # SYS_BROKER_PASS # .env ํŒŒ์ผ ํŽธ์ง‘ ํ›„ ์‹œํฌ๋ฆฟ ์ฑ„์šฐ๊ธฐ: nano .env # (์„ ํƒ) Tailscale ์‚ฌ์„ค IP๋กœ ๋ฐ”์ธ๋“œ ์„ค์ •: # MQTT_BIND=$(tailscale ip -4) # NATS_BIND=$(tailscale ip -4) # WS_BIND=$(tailscale ip -4) # 4. ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋™ docker compose up -d # 5. ์ƒํƒœ ๋ฐ ํ—ฌ์Šค์ฒดํฌ ํ™•์ธ docker compose ps # STATUS ์—ด์— "(healthy)" ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ ``` --- ## 4. ๋„คํŠธ์›Œํฌ ์ž ๊ธˆ ๋ฐ ๋ฐฉํ™”๋ฒฝ ์„ค์ • > [!WARNING] > **Docker ์˜ published ํฌํŠธ(`ports:`)๋Š” Linux ํ˜ธ์ŠคํŠธ์˜ UFW ๋ฐฉํ™”๋ฒฝ ๊ทœ์น™์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์šฐํšŒ(Bypass)ํ•ฉ๋‹ˆ๋‹ค.** > ์‹ค์ œ ๋…ธ์ถœ ํ†ต์ œ๋Š” ๋ฐฉํ™”๋ฒฝ์ด ์•„๋‹Œ `docker-compose.yaml` ์˜ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ(`${MQTT_BIND:-127.0.0.1}`)๊ฐ€ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. UFW ํ˜ธ์ŠคํŠธ ๋ฐฉํ™”๋ฒฝ ๊ธฐ๋ณธ ๊ทœ์น™: ```bash sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow in on tailscale0 to any sudo ufw allow ssh sudo ufw enable ``` --- ## 5. ์™ธ๋ถ€ ๋…ธ์ถœ ๋ฉด์  ๊ฒ€์ฆ (R-3) ์™ธ๋ถ€ ์ธํ„ฐ๋„ท(Tailnet ๋ฐ–) ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ณต์ธ IP ํฌํŠธ ์Šค์บ”์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ธŒ๋กœ์ปค๊ฐ€ ์ธํ„ฐ๋„ท์— ๋…ธ์ถœ๋˜์ง€ ์•Š์•˜์Œ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค: ```bash nmap -Pn -p 1883,4222,8222,8080 <๊ณต๊ฐœ_IP> ``` ๋ชจ๋“  ํฌํŠธ๊ฐ€ `closed` ๋˜๋Š” `filtered` ๋กœ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. --- ## 6. ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ### (a) MAM ์—์ด์ „ํŠธ ํ™˜๊ฒฝ๋ณ€์ˆ˜ (`.mam.env`) ๋กœ์ปฌ ๊ฐœ๋ฐœ ๋จธ์‹ ์˜ MAM ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ตœ์ƒ์œ„ `.mam.env` ์— ๋ธŒ๋กœ์ปค ์ ‘์† ์ •๋ณด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค: ```bash MQTT_BROKER=100.x.y.z # ๋˜๋Š” mam-hub.your-tailnet.ts.net MQTT_PORT=1883 MQTT_TLS=0 MQTT_USERNAME=mam_agent MQTT_PASSWORD= ``` ### (b) ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ ์ ‘์† ๋ ˆ์‹œํ”ผ (N-7 ๋ฐ˜์˜) ```javascript // MQTT.js โ€” retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊นŒ์ง€ ์ •์ƒ ์ˆ˜์‹ ํ•˜๋Š” ๊ถŒ์žฅ ๊ฒฝ๋กœ (N-7) const client = mqtt.connect("ws://mam-hub.your-tailnet.ts.net:8080/mqtt", { username: "mam_observer", password: "", protocolVersion: 4, // MQTT 3.1.1 }); client.subscribe("python/mqtt/jobs/+/events"); // ์ด ํด๋ผ์ด์–ธํŠธ๋Š” nats-server ๋‚ด๋ถ€์—์„œ ์™„์ „ํ•œ MQTT ํด๋ผ์ด์–ธํŠธ๋กœ ์ทจ๊ธ‰๋˜๋ฏ€๋กœ // ์žก์ด ๋๋‚œ ๋’ค์— ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ retained ์ตœ์ข… ์ด๋ฒคํŠธ๋ฅผ ์ฆ‰์‹œ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค. // ์ฐธ๊ณ  (nats.ws): // ws://:8080/ (๊ฒฝ๋กœ ์—†์ด) ์ ‘์† ์‹œ NATS ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ๋™์ž‘ํ•˜๋ฉฐ, // ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆผ๋งŒ ์ˆ˜์‹ ํ•˜๊ณ  ๊ณผ๊ฑฐ ์ข…๋ฃŒ๋œ ์žก์˜ retained ์ด๋ฒคํŠธ๋Š” ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค (N-1). ``` --- ## 7. ์›๊ฒฉ ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ (R-1 ~ R-10) | ID | ๊ฒ€์ฆ ํ•ญ๋ชฉ | ๋ช…๋ น์–ด / ๋ฐฉ๋ฒ• | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | |---|---|---|---| | **R-1** | ํ—ฌ์Šค ์—”๋“œํฌ์ธํŠธ | `curl -f http://127.0.0.1:8222/healthz` (ํ˜ธ์ŠคํŠธ ๋‚ด๋ถ€) | HTTP 200 `{"status":"ok"}` | | **R-2** | ์™ธ๋ถ€ ๋ชจ๋‹ˆํ„ฐ๋ง ์ฐจ๋‹จ | ์›๊ฒฉ ๋จธ์‹ ์—์„œ `curl http://<๊ณต๊ฐœIP>:8222/healthz` | Connection refused / Timeout | | **R-3** | ํฌํŠธ ๋…ธ์ถœ ๋ฉด์  | `nmap -Pn -p 1883,4222,8222,8080 <๊ณต๊ฐœIP>` | 4๊ฐœ ํฌํŠธ ์ „๋ถ€ Closed / Filtered | | **R-4** | WAN ์ง€์—ฐ ์‹œ๊ฐ„ | `python latency_check.py` | RTT P95 < 150ms | | **R-5** | ์ธ์ฆ ๊ฑฐ๋ถ€ (๋ฌด์ธ๊ฐ€) | `mosquitto_pub -h -p 1883 -t test -m hi` | Connect return code 5 (Not authorized) | | **R-6** | ์ธ์ฆ ์„ฑ๊ณต (์ •์ƒ) | `mosquitto_pub -h -p 1883 -u mam_agent -P -t test -m hi` | ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ์„ฑ๊ณต (rc=0) | | **R-7** | Retained ์ด๋ฒคํŠธ ์ „๋‹ฌ | `mosquitto_sub -h -p 1883 -u mam_agent -P -t 'python/mqtt/jobs/+/events' -C 1` | Retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์ฆ‰์‹œ ๋คํ”„ | | **R-8** | ๋ธŒ๋กœ์ปค ์‹๋ณ„์ž ๊ฒ€์ฆ | `curl -s http://127.0.0.1:8222/varz \| jq .server_name` | `"mam-hub"` | | **R-9** | ํ…Œ๋„ŒํŠธ ๊ณ„์ • ๊ฒฉ๋ฆฌ | `mam_observer` ๋กœ ๊ตฌ๋… ์„ฑ๊ณต, `home` ๊ณ„์ •์œผ๋กœ ๊ตฌ๋… ์‹œ MAM ์ด๋ฒคํŠธ ์ˆ˜์‹  0๊ฑด | ํ…Œ๋„ŒํŠธ ์™„๋ฒฝ ๊ฒฉ๋ฆฌ | | **R-10** | Retained ๊ฒฝ๊ณ„ ๊ฒ€์ฆ | MQTT ๊ตฌ๋…์ž๋Š” retained ์ˆ˜์‹ , ๋„ค์ดํ‹ฐ๋ธŒ WS(๊ฒฝ๋กœ์—†์Œ) ๊ตฌ๋…์ž๋Š” 0๊ฑด | N-1 / N-7 ๊ฒฝ๊ณ„ ํ™•์ฆ | --- ## 8. ์šด์˜ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ - **๋กœ๊ทธ ํ™•์ธ**: `docker compose logs -f --tail=100 nats` (๊ธฐ๋ณธ json-file 10MB x 3 ๋กœํ…Œ์ด์…˜ ๋‚ด์žฅ) - **JetStream ๋ณผ๋ฅจ ๋ฐฑ์—…**: `docker run --rm -v mam-broker_nats-data:/data -v $(pwd):/backup alpine tar czf /backup/nats-data-$(date +%Y%m%d).tar.gz /data` - **๋ฒ„์ „ ์—…๊ทธ๋ ˆ์ด๋“œ**: `docker compose pull && docker compose up -d` - **๋ชจ๋‹ˆํ„ฐ๋ง ์ง€ํ‘œ ์กฐํšŒ**: SSH ํ„ฐ๋„๋ง์„ ํ†ตํ•ด `http://127.0.0.1:8222/varz`, `/connz`, `/jsz` ์•ˆ์ „ํ•˜๊ฒŒ ํ™•์ธ --- ## 9. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… | ์ฆ์ƒ | ์›์ธ | ํ•ด๊ฒฐ ์กฐ์น˜ | |---|---|---| | `required variable MAM_BROKER_PASS is missing` | `.env` ๋ฏธ์ƒ์„ฑ ๋˜๋Š” ๋นˆ ๊ฐ’ | ์˜๋„๋œ fail-closed ๋™์ž‘. `chmod 600 .env` ํ›„ ์‹œํฌ๋ฆฟ์„ ์ฑ„์šฐ์‹ญ์‹œ์˜ค. | | `variable reference for 'MAM_BROKER_PASS' โ€ฆ can not be found` | compose ๋Š” ํ†ต๊ณผํ–ˆ์œผ๋‚˜ ์ปจํ…Œ์ด๋„ˆ์— ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฏธ์ฃผ์ž… | `docker-compose.yaml` ์˜ `environment:` ๋ธ”๋ก ๋ˆ„๋ฝ ํ™•์ธ | | ์ปจํ…Œ์ด๋„ˆ๋Š” ๋œจ๋Š”๋ฐ ๊ณ„์† `unhealthy` | ์ด๋ฏธ์ง€๋ฅผ `latest`/`scratch`/non-alpine ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ `wget` ๋ถ€์žฌ | `image: nats:2.12-alpine` ๋กœ ๋ณต๊ตฌํ•˜์‹ญ์‹œ์˜ค. | | ์„ค์ •์ด ์•Œ ์ˆ˜ ์—†๋Š” ๊ฐ’์œผ๋กœ ํŒŒ์‹ฑ๋˜๊ฑฐ๋‚˜ ๊นจ์ง | ์•”ํ˜ธ์— NATS ๋ ‰์„œ ์ข…๊ฒฐ์ž(`; , ] } # ' " $` ๋“ฑ) ํฌํ•จ | `openssl rand -base64 32` ๋กœ ์•ŒํŒŒ๋ฒณ/์ˆซ์ž/๊ธฐ๋ณธ base64 ์•”ํ˜ธ๋ฅผ ์žฌ๋ฐœ๊ธ‰ํ•˜์‹ญ์‹œ์˜ค. | | `max_file` ๊ตฌ๋ฌธ ์—๋Ÿฌ | `10g` ๋“ฑ ์†Œ๋ฌธ์ž ํฌ๊ธฐ ์ ‘๋ฏธ์‚ฌ ์‚ฌ์šฉ | NATS ๋Š” ๋Œ€๋ฌธ์ž ์ ‘๋ฏธ์‚ฌ(`10G`, `256M`)๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. | | ์›๊ฒฉ(Tailnet)์—์„œ ์ ‘์† ๋ถˆ๊ฐ€ | ๋ฆฌ์Šค๋„ˆ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๊ฐ€ loopback(127.0.0.1) ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์œ ์ง€๋จ | `.env` ์— `MQTT_BIND=$(tailscale ip -4)` ๋“ฑ์œผ๋กœ Tailnet IP๋ฅผ ๋ช…์‹œํ•˜์‹ญ์‹œ์˜ค. | | `JetStream not enabled for account` | ๊ณ„์ • ์ •์˜์— `jetstream: enabled` ๋ˆ„๋ฝ | `nats.conf` ์˜ `accounts.MAM` ๋ธ”๋ก ํ™•์ธ | | **WebSocket `403 origin not allowed`** | **๊ธฐ๋ณธ ์„ค์ •์—์„œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** `allowed_origins` ๋ฅผ ์ฑ„์› ๊ฑฐ๋‚˜ `same_origin: true` ๋ฅผ ์ผ  ๊ฒฝ์šฐ์—๋งŒ ๋ฐœ๋™ | ํ•ด๋‹น ์„ค์ •์„ ์ง€์šฐ๊ฑฐ๋‚˜, ๋Œ€์‹œ๋ณด๋“œ URL์„ `allowed_origins` ์— ์ถ”๊ฐ€ํ•˜์‹ญ์‹œ์˜ค. | | **์„œ๋ฒ„๊ฐ€ `allowed origins must be absolute URLsโ€ฆ` ๋กœ ๊ธฐ๋™ ์‹คํŒจ** | `allowed_origins` ์— `"*"` ๋˜๋Š” scheme ์—†๋Š” ๊ฐ’ ์ž…๋ ฅ | `["https://dashboard.example", "http://localhost:3000"]` ์ฒ˜๋Ÿผ ์ ˆ๋Œ€ URL๋กœ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค (`"*"` ๋ถˆ๊ฐ€). | | **๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์˜ Mixed Content ์ฐจ๋‹จ** | `https://` ํŽ˜์ด์ง€์—์„œ `ws://` ์—ฐ๊ฒฐ ์‹œ๋„ | ๋Œ€์‹œ๋ณด๋“œ๋ฅผ tailnet ๋‚ด๋ถ€ `http://` ๋กœ ์„œ๋น™ํ•˜๊ฑฐ๋‚˜, ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋กœ `wss://` ์ข…๋‹จ์„ ์ œ๊ณตํ•˜์‹ญ์‹œ์˜ค. | | **๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ชป ๋ฐ›์Œ** | 8080 ํฌํŠธ์— **๊ฒฝ๋กœ ์—†์ด**(NATS ๋„ค์ดํ‹ฐ๋ธŒ) ์ ‘์†ํ•จ. retained ๋Š” MQTT ์ „์šฉ (N-1) | `ws://:8080/mqtt` ๋กœ MQTT.js ํด๋ผ์ด์–ธํŠธ๋กœ ์ ‘์†ํ•˜์‹ญ์‹œ์˜ค (N-7). |