# ๐ MAM ๊ฐ์ธ ์ ์ฉ ๋ธ๋ก์ปค(Private Broker) ๊ตฌ์ถ ๋ฐ ์ฐ๋ ๊ฐ์ด๋ (`PRIVATE_SERVER.md`)
- **์์ฑ์ผ**: 2026-08-20 (Rev.2)
- **๋ฌธ์ ๋ชฉ์ **: MAM(Multi-Agent Mux)์ ๊ณต๊ฐ ๋ธ๋ก์ปค ์์กด์ฑ ๋ฐ ๋ณด์ ๊ฒฐํจ(A-2)์ ํด์ํ๊ธฐ ์ํ ๊ฐ์ธ ์ ์ฉ ๋ธ๋ก์ปค(NATS / Mosquitto) ๊ตฌ์ถ, ์ด์, ๋ค๋ฅ์ฑ ํ์ฉ ๋ฐ MAM ํด๋ผ์ด์ธํธ ์ฐ๋ ๊ฐ์ด๋.
- **์ฐ๊ณ ๋ฌธ์**: [`NATS_REPORT.md`](NATS_REPORT.md), [`implementation_plan.md`](implementation_plan.md), [`IMPROVEMENTS.md`](IMPROVEMENTS.md)
---
## 1. ๊ฐ์ ๋ฐ ๋์
๋ฐฐ๊ฒฝ
ํ์ฌ MAM ํ๋ ์์ํฌ์ ๊ธฐ๋ณธ ๋ฉ์์ง ๋ธ๋ก์ปค๋ ๊ณต๊ฐ ์๋ฒ(`broker.hivemq.com:1883`)๋ก ์ค์ ๋์ด ์์ต๋๋ค. ๊ฐ์ธ ์ ์ฉ ๋ธ๋ก์ปค(Private Broker)๋ฅผ ๊ตฌ์ถํ์ฌ ์ฐ๊ฒฐํ๋ฉด **ํด๋ผ์ด์ธํธ ์ฝ๋ ๋ณ๊ฒฝ ์์ด(0์ค ๋ณ๊ฒฝ)** ๋ณด์ ์ํ์ ์์ฒ ์ฐจ๋จํ๊ณ ๋คํธ์ํฌ ์์ ์ฑ์ ๋ํญ ํฅ์์ํฌ ์ ์์ต๋๋ค.
```
[MAM Orchestrator / Agents]
โ
โผ (MQTT 3.1.1 / TLS)
[Private Dedicated Broker] โโโ> ์ฌ์ค๋ง/๊ฐ์ธ ์๋ฒ (NATS Server / Mosquitto)
โข ์ธ๋ถ ๋ถ๋ฒ ํธ๋ํฝ 100% ์ฐจ๋จ (A-2 ๋ณด์ ํด์)
โข JetStream ์์์ฑ ๋ฐ NKey/JWT ACL ์ง์
โข ์ด์ ์ง์ฐ (<1ms) ๋ฐ ๋ฌด์ ํ ๋์ญํญ
```
MAM์ ์ ์ด ํ๋ฉด(`run_loop.sh`์ `wait_for_job` ํ์ผ์์คํ
ํด๋ง)์ ๋ธ๋ก์ปค์ 100% ๋
๋ฆฝ์ ์ผ๋ก ์๋ํ๋ฏ๋ก, ๋ธ๋ก์ปค๋ **๋น๋๊ธฐ ๊ด์ธก(observability) ์ฌ์ด๋์นด** ์ญํ ์ ์ํํฉ๋๋ค.
---
## 2. ํด๊ฒฐ ์์ญ ๋งคํธ๋ฆญ์ค (๋ธ๋ก์ปค ์ ํ vs ์ฝ๋ ํจ์น)
์ ์ฉ ๋ธ๋ก์ปค ๊ตฌ์ถ์ผ๋ก ์ฆ์ ํด๊ฒฐ๋๋ ์์ญ๊ณผ, ๋ก์ปฌ ์ฝ๋ ํจ์น(Track 0)๊ฐ ๋ณํ๋์ด์ผ ํ๋ ์์ญ์ ๋ช
ํํ ๊ตฌ๋ถ์
๋๋ค.
| ๊ตฌ๋ถ | ๋น๋ฉด ๊ณผ์ | ๊ฐ์ธ ๋ธ๋ก์ปค ๊ตฌ์ถ ์ | ๋ก์ปฌ ์ฝ๋ ํจ์น ํ์ ์ฌ๋ถ (Track 0) |
|---|---|:---:|:---:|
| **๋ณด์ (A-2)** | ๊ณต๊ฐ ๋ธ๋ก์ปค ๋
ธ์ถ ๋ฐ ์ธ๋ถ ์
์์ ์ด๋ฒคํธ ์์ ์ํ | ๐ข **100% ์ฆ์ ํด์** (์ฌ์ค๋ง/ACL ๊ฒฉ๋ฆฌ) | Track 2์์ ํ ํฐ ๋ฐ๊ธ ๊ฐ์ |
| **์์ ์ฑ** | ๊ณต๊ฐ ๋ธ๋ก์ปค์ ์๊ณ ์๋ ์๋จ ๋ฐ ์๋ ์ ํ(Rate-limit) | ๐ข **100% ์ฆ์ ํด์** (์ ์ฉ ๋ฆฌ์์ค) | โ |
| **๋ด๊ฒฐํจ์ฑ (B-14)** | ๋ธ๋ก์ปค ์ผ์ ์ฅ์ ์ 65๋ถ ๋ฃจํ ์ ์ง(Hang) ๊ฒฐํจ | โ ๏ธ ๋ธ๋ก์ปค ์ ๊ฒ/์๋จ ์ ์ฌ์ ํ ์ํ | ๐ด **ํ์ (Track 0 Step 1 ์ ํ ํจ์น)** |
| **์ง์ฐ/์คํ (B-15)** | ๋ธ๋ก์ปค ๋ค์ด ์ 120์ด ์ง์ฐ ๋ฐ ์ ์ ์์
์ ์๋ฌ ์คํ์ | โ ๏ธ ๋ธ๋ก์ปค ์ ๊ฒ/์๋จ ์ ์ฌ์ ํ ์ํ | ๐ด **ํ์ (Track 0 Step 2 & 3 ์ ํ ํจ์น)** |
> [!IMPORTANT]
> **๋ฐฉํ ์ํคํ
์ฒ ์์น:**
> "Track 0(`B-14`, `B-15`) ํจ์น๋ฅผ ํตํด ๋ธ๋ก์ปค๊ฐ ๋ค์ด๋์ด๋ ๋ฃจํ๊ฐ 100% ์ ์ ์์ฃผํ๋๋ก ๋ก์ปฌ ๋์คํฌ ๋ด๊ฒฐํจ์ฑ์ ๋จผ์ ํ๋ณดํ๊ณ , ๊ฐ์ธ ๋ธ๋ก์ปค๋ฅผ ์ฐ๊ฒฐํ์ฌ A-2 ๋ณด์๊ณผ ์ฑ๋ฅ์ ์๊ฒฐํฉ๋๋ค."
---
## 3. ์ ์ฉ ๋ธ๋ก์ปค ์ถ์ฒ ๋ฐ ๋น๊ต
MAM ํด๋ผ์ด์ธํธ๋ ํ์ค `paho-mqtt`๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, MQTT 3.1.1์ ์ง์ํ๋ ๋ชจ๋ ๋ธ๋ก์ปค์ 100% ํธํ๋ฉ๋๋ค.
| ๋น๊ต ํญ๋ชฉ | ๐ `nats-server` (๊ฐ๋ ฅ ๊ถ์ฅ) | `eclipse-mosquitto` (๋์) |
|---|---|---|
| **์ํคํ
์ฒ** | Go ๋จ์ผ ์ ์ ๋ฐ์ด๋๋ฆฌ (Zero Dependency) | C ๊ธฐ๋ฐ ๊ฒฝ๋ ์คํ์์ค ๋ธ๋ก์ปค |
| **์ฃผ์ ํน์ง** | โข ๋ด์ฅ MQTT 3.1.1 ๋ฆฌ์ค๋ (`mqtt { port: 1883 }`)
โข JetStream ์์ง ๋ด์ฅ (์ด๋ฒคํธ ์์ํ ๋ฐ ๋ณต๊ตฌ)
โข NKey/JWT ๊ธฐ๋ฐ ๊ณ์ ๋ฐ Subject๋ณ ์ธ๋ถํ๋ ACL
โข WebSocket ๋ฐ NATS ๋ค์ดํฐ๋ธ ํ๋กํ ์ฝ ๋์ ์๋น | โข ๊ฐ์ฅ ๋๋ฆฌ ์ฐ์ด๋ ํ์ค ๊ฒฝ๋ MQTT ๋ธ๋ก์ปค
โข ๋ฎ์ ๋ฉ๋ชจ๋ฆฌ ์ ์ ์จ (~10MB) |
| **์ถ์ฒ ์ฉ๋** | ๋ชจ๋ ์ธํ๋ผ, ํ์ฅ์ฑ, ๊ฐ์ฌ ๋ก๊ทธ ์์ํ, ํ๋ฉ ํตํฉ | ์ ํต ์ด๊ฒฝ๋ ์๋ฒ ๋๋/๋จ์ผ ๋ชฉ์ ํ๊ฒฝ |
| **๋ฐฐํฌ ๋์ด๋** | ๐ข ๋ฐ์ด๋๋ฆฌ 1๊ฐ ์คํ ๋๋ Docker 1์ค | ๐ข ํจํค์ง ๋งค๋์ (`apt`, `brew`) ๋๋ Docker |
---
## 4. ๊ฐ์ธ ์๋ฒ ๋ธ๋ก์ปค ๋ฐฐํฌ ๊ฐ์ด๋
### 4.1 `nats-server` ๋ฐฐํฌ (๊ถ์ฅ)
`nats-server`์์ MQTT๋ฅผ ํ์ฑํํ๋ ค๋ฉด ์ค์ ํ์ผ(`nats.conf`)์ `mqtt { port: 1883 }` ๋ธ๋ก๊ณผ `jetstream { }` ๋ธ๋ก์ด ๋ฐ๋์ ํฌํจ๋์ด์ผ ํฉ๋๋ค.
> [!NOTE]
> `nats-server`์ `-m` ํ๋๊ทธ๋ HTTP ๋ชจ๋ํฐ๋ง ํฌํธ(`--http_port`)๋ฅผ ์ง์ ํ๋ ์ต์
์ด๋ฉฐ, MQTT๋ฅผ ์ผ๋ ํ๋๊ทธ๊ฐ ์๋๋๋ค. MQTT ํ์ฑํ๋ ๋ฐ๋์ `-c nats.conf` ์ค์ ํ์ผ์ ํตํด ๊ตฌ์ฑํด์ผ ํฉ๋๋ค.
#### 1) ๊ณตํต ์ค์ ํ์ผ (`nats.conf`)
```conf
# nats.conf
server_name: mam-hub
# JetStream ์์ ์คํ ๋ฆฌ์ง (MQTT QoS 1 ๋ฐ Retained ๋ฉ์์ง ์ฒ๋ฆฌ์ ํ์)
jetstream {
store_dir: "/data" # Docker ํ๊ฒฝ ๊ธฐ๋ณธ ์คํ ๋ฆฌ์ง (๋ค์ดํฐ๋ธ ์คํ ์ $HOME ์ ๊ฐ)
max_file: 10G # ํ๋ฉ ๋์คํฌ ์ํ ์ค์
}
# HTTP ๋ชจ๋ํฐ๋ง ์๋ํฌ์ธํธ (/varz, /jsz ๋์๋ณด๋)
http_port: 8222
# ํ๋ฉด A: MAM MQTT 3.1.1 ํ๋กํ ์ฝ ๋ฆฌ์ค๋
mqtt {
port: 1883
}
# ํ๋ฉด B: ํ๋ฉ/์น ๋ธ๋ผ์ฐ์ ๋์๋ณด๋์ฉ WebSocket ๋ฆฌ์ค๋ (์ ํ ์ฌํญ)
websocket {
port: 8080
no_tls: true # ๋ด๋ถ ์ฌ์ค๋ง ํ์
}
```
#### 2) ๋ฐฐํฌ ๋ฐฉ๋ฒ A. Docker / Docker Compose (๊ถ์ฅ)
**๋จ์ผ Docker ์คํ:**
```bash
# ํธ์คํธ์ nats.conf ์์ฑ ํ ์คํ (D-2: alpine ๊ณ ์ ํ, D-3: ๋ฃจํ๋ฐฑ/๋ฐ์ธ๋ ํ์ )
docker run -d \
--name mam-nats \
--restart unless-stopped \
-p "${MQTT_BIND:-127.0.0.1}:1883:1883" \
-p "${NATS_BIND:-127.0.0.1}:4222:4222" \
-p "127.0.0.1:8222:8222" \
-p "${WS_BIND:-127.0.0.1}:8080:8080" \
-v ./nats.conf:/etc/nats/nats.conf:ro \
-v nats-data:/data \
nats:2.12-alpine \
-c /etc/nats/nats.conf
```
**Docker Compose (`docker-compose.yml`):**
```yaml
version: '3.8'
services:
nats:
image: nats:2.12-alpine
container_name: mam-nats
restart: unless-stopped
command: ["-c", "/etc/nats/nats.conf"]
ports:
- "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 ํฌํธ (ํ๋ฉด A: MAM)
- "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS ๊ธฐ๋ณธ ํฌํธ (ํ๋ฉด B)
- "127.0.0.1:8222:8222" # HTTP ๋ชจ๋ํฐ๋ง (/varz, /jsz, /healthz)
- "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (ํ๋ฉด B)
volumes:
- ./nats.conf:/etc/nats/nats.conf:ro
- nats-data:/data
volumes:
nats-data:
```
#### 3) ๋ฐฐํฌ ๋ฐฉ๋ฒ B. ๋ค์ดํฐ๋ธ ๋ฐ์ด๋๋ฆฌ ์ค์น (macOS / Linux โ ๋น๋ฃจํธ ์ฌ์ฉ์ ๊ณต๊ฐ)
macOS์ sealed APFS ๋ฃจํธ ๋ณผ๋ฅจ(`/data`) ๊ถํ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฌ์ฉ์ ํ ๋๋ ํฐ๋ฆฌ(`~/.config/nats/`, `~/.local/share/nats/data`)๋ฅผ ๊ธฐ๋ณธ ์คํ ๋ฆฌ์ง๋ก ์ฌ์ฉํฉ๋๋ค.
```bash
# ์ค์ ๋ฐ ๋ฐ์ดํฐ ๋๋ ํฐ๋ฆฌ ์์ฑ (sudo ๋ถํ์)
mkdir -p ~/.config/nats ~/.local/share/nats/data
# ์ค์ ํ์ผ ์์ฑ (D-1: ๋น์ธ์ฉ heredoc < ~/.config/nats/nats.conf
server_name: mam-hub
jetstream {
store_dir: "$HOME/.local/share/nats/data"
max_file: 10G
}
http_port: 8222
mqtt {
port: 1883
}
websocket {
port: 8080
no_tls: true
}
EOF
# macOS (Homebrew ์ค์น ๋ฐ ์คํ)
brew install nats-server
nats-server -c ~/.config/nats/nats.conf &
# Linux (x86_64 ๋จ์ผ ๋ฐ์ด๋๋ฆฌ ์ค์น ๋ฐ ์คํ)
curl -L https://github.com/nats-io/nats-server/releases/download/v2.10.20/nats-server-v2.10.20-linux-amd64.tar.gz | tar xz
sudo mv nats-server-v2.10.20-linux-amd64/nats-server /usr/local/bin/
nats-server -c ~/.config/nats/nats.conf &
```
---
### 4.2 `mosquitto` ๋ฐฐํฌ (๋์)
#### Docker ์คํ:
```bash
docker run -d \
--name mam-mosquitto \
--restart unless-stopped \
-p 1883:1883 \
-v ./mosquitto.conf:/mosquitto/config/mosquitto.conf \
eclipse-mosquitto:latest
```
**๊ธฐ๋ณธ `mosquitto.conf` ์ค์ ํ์ผ ์์:**
```conf
listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
```
---
## 5. ํ๋์ ์๋ฒ๋ก ์ฌ๋ฌ ํ๋ก์ ํธ โ `nats-server` ๋ค๋ฅ์ฑ (Versatility)
`nats-server`์ ๋ค๋ฅ์ฑ์ **MAM์ ๋ค์ดํฐ๋ธ NATS๋ก ์ด๊ดํ ์ด์ ๊ฐ ์๋๋ผ, MAM ์ฝ๋๋ฅผ ํ ์ค๋ ๋ฐ๊พธ์ง ์๊ณ ๋ ์ป์ ์ ์๋ ๋ถ๊ฐ์ ์ด๋**์
๋๋ค.
### 5.1 ๋ ๊ฐ์ ์๋น ํ๋ฉด ๋ฐ ์ธ ๊ฐ์ง ์ ์ ๊ฒฝ๋ก (Consumption Planes & Transport Paths)
`nats-server`๋ ๋จ์ผ ํ๋ก์ธ์ค ๋ด์์ ์ฌ๋ฌ ํ๋กํ ์ฝ ๋ฆฌ์ค๋๋ฅผ ๋์์ ๊ตฌ๋ํ๋ฏ๋ก, MAM์ ๋จ์์ฑ๊ณผ ๊ฐ์ธ ํ๋ฉ์ ํ์ฅ์ฑ์ ์๋ฒฝํ ์๋ฆฝ์ํต๋๋ค.
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ nats-server (๋จ์ผ ์ธ์คํด์ค) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ํ๋ฉด A: MAM ์ํฌ๋ก๋ โ ํ๋ฉด B: ํ๋ฉ/๊ฐ์ธ ํ๋ก์ ํธ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
ํ๋กํ ์ฝ โ MQTT 3.1.1 (ํฌํธ 1883) โ NATS(4222), WebSocket(8080) โ
ํด๋ผ์ด์ธํธ โ paho-mqtt (์ฝ๋ ๋ณ๊ฒฝ 0์ค) โ nats-py, nats.js, MQTT.js ๋ฑโ
์ฌ์ฉ ๊ธฐ๋ฅ โ QoS 1, Retain, ์์ผ๋์นด๋, TLSโ JetStream ๋ฆฌํ๋ ์ด, KV, Objectโ
์ค๊ณ ์์น โ ์ด๊ฒฝ๋ ๋๊ธฐ CLI ํซํจ์ค ๋ณด์กด โ ๊ณ ๊ธ ๋น๋๊ธฐ ์ด๋ฒคํธ ์คํธ๋ฆฌ๋ฐ โ
๊ณต์ ์์ โ โโโโโโ ๋จ์ผ ์ ์ ๋ฐ์ด๋๋ฆฌ / JetStream ์คํ ๋ฆฌ์ง / ACL โโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
ํนํ **WebSocket ํฌํธ(8080)**๋ ์ ์ ๊ฒฝ๋ก(URL Path)์ ๋ฐ๋ผ ๋ ๊ฐ์ง ํ๋กํ ์ฝ์ ๋์์ ์๋นํฉ๋๋ค:
1. **MQTT 1883**: TCP ๋ค์ดํฐ๋ธ MQTT 3.1.1 (MAM CLI ์์ด์ ํธ ํ์ค ๊ฒฝ๋ก)
2. **WebSocket 8080 (`/mqtt` ๊ฒฝ๋ก)**: **MQTT-over-WebSocket** (`MQTT.js` ๋ฑ์ผ๋ก ์ ์). ์๋ฒ ๋ด๋ถ์์ 1883 ๋ฆฌ์ค๋์ ๋์ผํ๊ฒ ์ทจ๊ธ๋์ด **retained ์ข
๋ฃ ์ด๋ฒคํธ๋ฅผ ์์ ํ๊ฒ ์์ **ํฉ๋๋ค (N-7).
3. **WebSocket 8080 (`/` ๋๋ ๊ฒฝ๋ก ์์)**: **NATS ๋ค์ดํฐ๋ธ WebSocket** (`nats.ws` ๋ฑ์ผ๋ก ์ ์). ๋ผ์ด๋ธ ์คํธ๋ฆผ๋ง ์์ ํ๋ฉฐ retained ๋ฉ์์ง๋ ์์ ํ์ง ์์ต๋๋ค (N-1).
### 5.2 ๊ต์ฐจ ํ๋กํ ์ฝ ๋ธ๋ฆฌ์ง (Cross-Protocol Bridging)
- `nats-server`๋ ๋ด๋ถ์ ์ผ๋ก MQTT ํ ํฝ(`/`)์ NATS Subject(`.`)๋ก ์ค์๊ฐ ์๋ ๋ณํํฉ๋๋ค.
- MAM ์์ด์ ํธ๊ฐ MQTT ํ ํฝ `python/mqtt/jobs//events`๋ก ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋ฉด, ์น ๋ธ๋ผ์ฐ์ ๋ ํ ํ๋ก์ ํธ์ NATS ๊ตฌ๋
์๋ NATS Subject `python.mqtt.jobs..events` ๋๋ `python.mqtt.jobs.*.events`๋ก ์ฆ์ ์ค์๊ฐ ์์ ํ ์ ์์ต๋๋ค.
- **์ค์ฉ์ ์ด์ **: MAM ์์ค ์ฝ๋๋ฅผ ๋จ 1์ค๋ ์์ ํ์ง ์๊ณ ๋ React/Vue ์น ๋์๋ณด๋๋ ํฐ๋ฏธ๋ ๋ชจ๋ํฐ๋ง ํด์ ์ฆ์ ๋ถ์ฐฉํ ์ ์์ต๋๋ค.
- **๊ฒฝ๊ณ (ํ์ ์ธ์ง โ N-1 & N-7)**:
- NATS ๋ค์ดํฐ๋ธ(๊ฒฝ๋ก ์๋ WebSocket ๋๋ 4222)๋ก ์ ์ํ๋ ๊ฒฝ์ฐ: ๊ต์ฐจ ํ๋กํ ์ฝ ๋ธ๋ฆฌ์ง์ **๋ผ์ด๋ธ ์คํธ๋ฆฌ๋ฐ์ ํ์ **๋ฉ๋๋ค. ์ก์ด ๋๋ ๋ค ์ ์ํ ๋ค์ดํฐ๋ธ NATS ๋์๋ณด๋๋ ๊ทธ ์ก์ **์ข
๋ฃ ์ด๋ฒคํธ๋ฅผ ์์ ํ์ง ๋ชปํฉ๋๋ค** (retained ๋ฉ์์ง๋ MQTT SUBSCRIBE ๊ฒฝ๋ก ์ ์ฉ).
- ์น ๋์๋ณด๋์์ ์ฌํ ์ข
๋ฃ ์ด๋ฒคํธ๊น์ง ๋ฌด์์ผ๋ก ์์ ํ๋ ค๋ฉด **`ws://:8080/mqtt` ๊ฒฝ๋ก๋ก `MQTT.js` ํด๋ผ์ด์ธํธ๋ฅผ ์ฐ๊ฒฐ**ํ์ญ์์ค (N-7). ๋ฆฌํ๋ ์ด ์คํธ๋ฆผ ๊ตฌ์ถ ๋ฐ ๋์คํฌ ๊ด๋ฆฌ ๋ถ๋ด ์์ด ์์ ํ ์ข
๋ฃ ์ด๋ฒคํธ๋ฅผ ์ฆ์ ์์ ํ ์ ์์ต๋๋ค.
- **๊ณ์ ๋ฐฐ์น**: ๊ด์ธก ํด๋ผ์ด์ธํธ๋ ยง5.5 ์ฒ๋ฐฉ๋๋ก `MAM` ๊ณ์ ์์ ์ฝ๊ธฐ ์ ์ฉ ์ฌ์ฉ์(`mam_observer`)๋ก ์ ์ํด์ผ ํฉ๋๋ค. ๋ค๋ฅธ ๊ณ์ ์์๋ subject๊ฐ ๋ณด์ด์ง ์์ต๋๋ค.
- **์ฃผ์ ์ฌํญ**: ํ ํฝ ๋ ๋ฒจ ๋ด์ ๋ง์นจํ(`.`)๊ฐ ํฌํจ๋๋ฉด NATS ๊ณ์ธต์์ ํ ํฐ์ด ๋ถ๋ฆฌ๋ ์ ์์ผ๋, MAM์ `job_id`๋ 8์๋ฆฌ hex, ์ํฌ์คํ์ด์ค ์ง๋ฌธ์ 12์๋ฆฌ hex์ด๋ฏ๋ก ์์ ํฉ๋๋ค.
### 5.3 JetStream ์ด๋ฒคํธ ๋ฆฌํ๋ ์ด (Event Replay)
- `python.mqtt.jobs.>` Subject๋ฅผ ๊ตฌ๋
ํ๋ JetStream ์คํธ๋ฆผ์ ์์ฑํ๋ฉด, ์ง๋ ์์
์ ์ด๋ฒคํธ ์คํธ๋ฆผ ์ ์ฒด๋ฅผ ์์ ์ง์ (Time-based) ๋๋ ์ํ์ค ์ง์ (Sequence-based)์ผ๋ก ์ฌํ ๋ฆฌํ๋ ์ดํ ์ ์์ต๋๋ค.
- **์ฃผ์ ์ฌํญ**: ์ด ๊ธฐ๋ฅ์ ์ตํธ์ธ(Opt-in)์ด๋ฉฐ, MQTT QoS 1 ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ด๋ถ ์์คํ
์คํธ๋ฆผ(`$MQTT_*`)๊ณผ ๋ณ๊ฐ๋ก ๊ด๋ฆฌ๋ฉ๋๋ค. ๋์คํฌ ์ฉ๋ ๊ด๋ฆฌ๋ฅผ ์ํด `max_age`๋ `max_bytes` ์ํ์ ๋ฐ๋์ ์ค์ ํด์ผ ํฉ๋๋ค.
### 5.4 ๋ด์ฅ Key-Value (KV) ๋ฐ Object Store
- ํ๋ฉ ๋ฐ ๊ฐ์ธ ํ๋ก์ ํธ์์ Redis๋ MinIO ๊ฐ์ ๋ณ๋ ์ธํ๋ผ๋ฅผ ๋์ฐ์ง ์๊ณ ๋ `nats-server` ๋ด์ฅ KV ๋ฐ Object Store๋ฅผ ์ฆ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- **๊ธ์ง ์ฌํญ (Non-Goal)**: MAM์ ๋ก์ปฌ ๋ ์ง์คํธ๋ฆฌ(`.mam/jobs/*.json`)๋ฅผ JetStream KV๋ก ๋์ฒดํด์๋ ์ ๋ฉ๋๋ค (`wait_for_job`์ fcntl ๋ฐ ํ์ผ์์คํ
ํด๋ง ๊ณ์ฝ ์ ์ง).
### 5.5 ๋ฉํฐํ
๋ํธ ๊ณ์ ๋ถ๋ฆฌ ๋ฐ ๋ณด์
- ๋จ์ผ ์๋ฒ ๋ด์์ `MAM` ์ ์ฉ ๊ณ์ ๊ณผ `HOME` ๊ฐ์ธ ๊ณ์ ์ ๋ถ๋ฆฌํ์ฌ ๋ฆฌ์์ค ์ฟผํฐ์ ๊ถํ์ ์๋ฒฝํ ๊ฒฉ๋ฆฌํ ์ ์์ต๋๋ค.
- **๊ถ๊ณ ๋ฐฐ์น**: MAM๊ณผ ์ด๋ฅผ ๊ด์ธกํ๋ ๋์๋ณด๋๋ ๋์ผํ ๊ณ์ (`MAM`)์ ๋ฐฐ์นํ๊ณ (๊ด์ธก์๋ ์ฝ๊ธฐ ์ ์ฉ `mam_observer` ์ญํ ๋ถ์ฌ), ๋ฌด๊ดํ ํ๋ฉ ์๋น์ค๋ ๋ณ๋ ๊ณ์ (`HOME`)์ ๋ฐฐ์นํฉ๋๋ค.
---
## 6. MAM ํด๋ผ์ด์ธํธ ์ฐ๋ ์ค์ (`.mam.env`)
๊ฐ์ธ ์๋ฒ ๋ธ๋ก์ปค๊ฐ ๊ตฌ๋๋๋ฉด, MAM ์ ์ฅ์ ๋ฃจํธ์ [`.mam.env`](file:///.mam.env) ํ์ผ์ ๊ฐ์ธ ์๋ฒ ์ฃผ์๋ฅผ ๋ฑ๋กํฉ๋๋ค.
> [!NOTE]
> MAM ์ฝ๋(`mqtt_common.py`)๋ `MQTT_*` ์ ๋์ฌ์ ํ๊ฒฝ๋ณ์๋ฅผ ์ฝ์ต๋๋ค. ์ด์ ๋น๊ณต์ ๋ฌธ์์ `MAM_MQTT_*` ๋ณ์๋ ๋ฌดํจํ๋ฏ๋ก ๋ฐ๋์ ์๋์ ํ์ค ๋ณ์๋ช
์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
```bash
# ==============================================================================
# MAM Private MQTT Broker Configuration (.mam.env)
# ==============================================================================
# โโ ๋ชจ๋ธ T (Tailscale ์ฌ์ค ์ค๋ฒ๋ ์ด๋ง ๊ถ์ฅ) โโโโโโโโโโโโโโโโโโโโโโโ
MQTT_BROKER="mam-hub.tailXXXX.ts.net" # ๋๋ 100.x.y.z (ํ๋ฌธ์ด๋ฏ๋ก IP ์ฌ์ฉ ๊ฐ๋ฅ)
MQTT_PORT=1883
MQTT_TLS=0
MQTT_USERNAME=mam_agent
MQTT_PASSWORD=replace_me_with_token
MQTT_KEEPALIVE=60 # WAN ๊ตฌ๊ฐ ๊ถ์ฅ ์ฐ๊ฒฐ ์ ์ง ์๊ฐ
# โโ ๋ชจ๋ธ P (๊ณต๊ฐ TLS / Let's Encrypt ๋ชจ๋ธ) โโโโโโโโโโโโโโโโโโโโโโโ
# MQTT_BROKER="mam-broker.example.com" # D-4: ๋ฐ๋์ ์ธ์ฆ์ SAN ์ DNS ์ด๋ฆ (IP ๊ธ์ง)
# MQTT_PORT=8883
# MQTT_TLS=1
# MQTT_USERNAME=mam_agent
# MQTT_PASSWORD=replace_me_with_token
# MQTT_CA_CERTS ๋ Let's Encrypt ์ฌ์ฉ ์ '์ค์ ํ์ง ์์' (์์คํ
์ ๋ขฐ ์ ์ฅ์ ์ฌ์ฉ)
```
*์ฐธ๊ณ : OS ํ๊ฒฝ๋ณ์์ ๋์ผํ ์ด๋ฆ์ด ์ด๋ฏธ `export`๋์ด ์๋ ๊ฒฝ์ฐ OS ํ๊ฒฝ๋ณ์๊ฐ `.mam.env` ํ์ผ ์ค์ ๋ณด๋ค ์ฐ์ ํฉ๋๋ค.*
---
## 7. ์ฐ๋ ๋ฐ ๋์ ๊ฒ์ฆ ํ
์คํธ (4-Step Verification)
๊ฐ์ธ ์๋ฒ ๋ธ๋ก์ปค์์ ์ฐ๋ ์ํ๋ฅผ ์ ํํ๊ฒ ๊ฒ์ฆํ๋ 4๋จ๊ณ ์ ์ฐจ์
๋๋ค.
### Step 1. ๋ธ๋ก์ปค ๋ฆฌ์ค๋ ๋ฐ JetStream ์ํ ํ์ธ
```bash
# MQTT ๋ฆฌ์ค๋ ํ์ฑํ ํ์ธ
curl -s http://127.0.0.1:8222/varz | grep -i mqtt
# JetStream ์์ง ์ ์ ๊ตฌ๋ ํ์ธ
curl -s http://127.0.0.1:8222/jsz
```
### Step 2. ์์ ์ก ๋ฑ๋ก ๋ฐ ์ฐ๊ฒฐ ๊ฒ์ฆ ์ด๋ฒคํธ ๋ฐํ
`publish_event.py`๋ ๋ ์ง์คํธ๋ฆฌ์ ๋ฑ๋ก๋ ์ก์ ๋ํด์๋ง ๋ฐํ์ ์ํํ๋ฏ๋ก, ์์ ์ก์ ๋ฑ๋กํ๊ณ ๋ฐํํ ํ ์๋ฃ ์ฒ๋ฆฌํฉ๋๋ค.
```bash
# 1) ์์ ์ก ๋ฑ๋ก (์๋ ์ฑ๋ฒ๋ JID ์บก์ฒ)
JID=$(.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \
--registry-dir .mam/jobs \
register \
--prompt "Private broker connectivity test" \
--agent-session "herdr:test")
echo "registered test job: $JID"
# 2) ์ด๋ฒคํธ ๋ฐํ (์์ธ ๋ก๊ทธ ์ถ๋ ฅ ๋ฐ rc=0 ๋จ์ธ)
.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py \
--registry-dir .mam/jobs \
--job "$JID" \
--event progress \
--detail "Private broker connection verified" -v
# 3) ํ
์คํธ ์ก ์ข
๊ฒฐ ์ฒ๋ฆฌ (๋ฏธ์ข
๊ฒฐ ์ --wait-any ์ ๋ น ์ก ์์กด ๋ฐฉ์ง)
.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \
--registry-dir .mam/jobs status --job "$JID" --set completed
```
### Step 3. ์ ์ ๋์ ๋ธ๋ก์ปค IP ๋จ์ธ
Step 2์ `-v` ์ถ๋ ฅ ๋ก๊ทธ ๋๋ `.mam/delegate_job_logs/$JID/events.ndjson` ํ์ผ์์ ์ค์ ์ ์ ํธ์คํธ๊ฐ ๊ฐ์ธ ๋ธ๋ก์ปค IP๋ก ๋ํ๋๊ณ `broker.hivemq.com`์ด ํฌํจ๋์ง ์์๋์ง ํ์ธํฉ๋๋ค.
### Step 4. ๋จ์ ํ๊ท ํ
์คํธ ๊ฒ์ฆ
```bash
.venv/bin/python -m pytest tests/ -q
```
*์ฐธ๊ณ : MAM์ ๊ธฐ๋ณธ ๋จ์/์ปดํฌ๋ํธ ํ
์คํธ ์ค์ํธ๋ ๋ชจ์(Mock) ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋ธ๋ก์ปค ์ฐ๊ฒฐ ์ฌ๋ถ์ ๋ฌด๊ดํ๊ฒ 100% ํต๊ณผํฉ๋๋ค. ์ค์ ๋คํธ์ํฌ ์ฐ๋ ๊ฒ์ฆ์ Step 1~3์ด ๋ด๋นํฉ๋๋ค.*
---
## 8. ๊ถ์ฅ ์คํ ์์
```
[Phase 1: ๋ด๊ฒฐํจ์ฑ ํ๋ณด] โโ> [Phase 2: ๊ฐ์ธ ๋ธ๋ก์ปค ๊ฐ๋] โโ> [Phase 3: A-2 ๋ณด์ ์์ ์ข
๊ฒฐ]
Track 0 (B-14, B-15) nats-server (nats.conf) ์ง๋ฌธ ํ ํฝ ๋ฐ ์ธ์ฆ ํ ํฐ ๋ฐ๊ธ
๋ก์ปฌ ๋์คํฌ ํด๋ฐฑ ํจ์น .mam.env ํ๊ฒฝ๋ณ์ ์ฐ๋ ์ธ๋ถ ๊ฐ์ญ 100% ์ฐจ๋จ
```
1. **Phase 1 (Track 0 ์ ํ ํจ์น)**: `publish_event.py`์ `job_subscriber.py`์ ๋ก์ปฌ ๋์คํฌ ํด๋ฐฑ(`B-14`, `B-15`)์ ๋จผ์ ์ ์ฉํ์ฌ ๋ธ๋ก์ปค ๋ค์ด ์์๋ ๋ฃจํ๊ฐ ๋ฉ์ถ์ง ์๋ ๋ฐฉํ ๊ตฌ์กฐ๋ฅผ ํ๋ฆฝํฉ๋๋ค.
2. **Phase 2 (๊ฐ์ธ ๋ธ๋ก์ปค ๊ฐ๋)**: ๊ฐ์ธ ์๋ฒ์ `nats-server -c nats.conf`๋ฅผ ๊ตฌ๋ํ๊ณ `.mam.env`์ `MQTT_BROKER`๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค.
3. **Phase 3 (A-2 ๋ณด์ ์์ ์ข
๊ฒฐ)**: ์ํฌ์คํ์ด์ค ์ง๋ฌธ ํ ํฝ(`mam//jobs/...`) ๋ฐ ๋ฌด์กฐ๊ฑด ํ ํฐ ๋ฐ๊ธ์ ์ ์ฉํ์ฌ ๊ณต๊ฐ ๋ธ๋ก์ปค ์ํ์ ์์ ํ ์๊ตฌ ํ๊ธฐํฉ๋๋ค.
---
## 9. ์๊ฒฉ ์๋ฒ `nats-server` Docker ํ๋ก๋์
๋ฐฐํฌ ๊ฐ์ด๋ (Track 1R)
์๊ฒฉ VPS ๋๋ ์์ ๊ฐ๋ ํ๋ฉ ์๋ฒ์ ํ๋ก๋์
์์ค์ `nats-server`๋ฅผ Docker ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ถํ๊ณ MAM๊ณผ ์ฐ๋ํ๋ ํ์ค ์ ์ฐจ์
๋๋ค.
> [!NOTE]
> **์ ๋ณธ ์์ฐ ์๋ด**: ๋ณธ ์ ์ ์ค์ ์ [`docker/`](docker/) ๋๋ ํฐ๋ฆฌ์ ์ ๋ณธ(canonical) ํ์ผ(`docker/docker-compose.yaml`, `docker/nats.conf`, `docker/.env.example`, `docker/README.md`)๋ก ๊ด๋ฆฌ๋๋ฉฐ, ์๋ ์ฝ๋ ํ์ค๋ ๊ทธ ์ฌ๋ณธ์
๋๋ค. ๋ ๊ณณ์ด ์ด๊ธ๋๋ฉด `tests/test_deploy_freshness.py`์ D-22 ~ D-30 ํ๊ท ๊ฐ๋๊ฐ ์คํจํฉ๋๋ค.
### 9.1 ํ๋ก๋์
`nats.conf`
```conf
# ==============================================================================
# docker/nats.conf โ MAM ์๊ฒฉ ํ๋ก๋์
๋ธ๋ก์ปค (Track 1R)
#
# ์ ๋ณธ ๋ฌธ์: PRIVATE_SERVER.md ยง9.1
# ์ํฌ๋ฆฟ: ์ด ํ์ผ์๋ ์์ต๋๋ค. ๋ชจ๋ password ๋ docker/.env โ compose
# environment โ ์ปจํ
์ด๋ ํ๊ฒฝ๋ณ์๋ก ์ฃผ์
๋๋ $VAR ์ฐธ์กฐ์
๋๋ค.
#
# โ ์ํธ ๋ฌธ์ ์ ์ฝ: NATS ๋ ํ๊ฒฝ๋ณ์ ๊ฐ์ ์์ฒด ์ค์ ๋ ์๋ก ์ฌํ์ฑํฉ๋๋ค.
# ์ํธ์ [๊ณต๋ฐฑ ; , ] } # ' " $] ๊ฐ ๋ค์ด๊ฐ๋ฉด ์ค์ ์ด ๊นจ์ง๊ฑฐ๋ ๋ค๋ฅด๊ฒ ํด์๋ฉ๋๋ค.
# ๋ฐ๋์ `openssl rand -base64 32` (์ํ๋ฒณ A-Za-z0-9+/=) ๋ฅผ ์ฌ์ฉํ์ญ์์ค.
# ==============================================================================
server_name: mam-hub
# โโ JetStream: MQTT retained/QoS1 ์ ์ฅ์. ์ข
๋ฃ ์ด๋ฒคํธ ์ฌ์์ ์ด ์ฌ๊ธฐ์ ์์กด โโ
jetstream {
store_dir: "/data" # ์ ๋๊ฒฝ๋ก ๊ณ ์ . '~' ๋ ์ธ์ฉ๋ "$HOME" ๋ ํ์ฅ๋์ง ์์
max_file: 10G # ์ ๋ฏธ์ฌ๋ ๋๋ฌธ์๋ง ์ ํจ (K/M/G/T)
max_mem: 256M
}
http_port: 8222 # ๋ฌด์ธ์ฆ ๋ชจ๋ํฐ๋ง โ ํธ์คํธ ๊ฒ์๋ loopback ํ์ (compose)
mqtt {
port: 1883
ack_wait: 60s # WAN RTT ํก์ (๊ธฐ๋ณธ 30s)
max_ack_pending: 1024 # ๋ค์ค ์์ด์ ํธ ๋์ ๋ฐํ ์ฌ์ (์ํ 65535)
}
websocket {
port: 8080
no_tls: true # ์ฌ์ค๋ง/tailnet ํ์ . ์๋ตํ๋ฉด TLS ์ค์ ํ์๋ผ ๊ธฐ๋ ์คํจ
# โโ ์์ (Origin) ์ ์ฑ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# ๊ธฐ๋ณธ๊ฐ์ ์ด๋ฏธ '๋ชจ๋ ์ถ์ฒ ํ์ฉ'์
๋๋ค. same_origin ์ ๊ธฐ๋ณธ๊ฐ์ false ์ด๊ณ
# allowed_origins ๊ฐ ๋น์ด ์์ผ๋ฉด checkOrigin() ์ด Origin ํค๋๋ฅผ ์ฝ์ง๋ ์๊ณ
# ์ฆ์ nil ์ ๋ฐํํฉ๋๋ค (server/websocket.go:1039).
# โ http://localhost:3000 ์ ๋ธ๋ผ์ฐ์ ๋์๋ณด๋๋ ๋ณ๋ ์ค์ ์์ด ์ ์๋ฉ๋๋ค.
# โ `same_origin: false` ๋ฅผ ์ ๋ ๊ฒ์ no-op ์
๋๋ค.
#
# 8080 ์ tailnet ๋ฐ์ผ๋ก ๋
ธ์ถํ๋ค๋ฉด ์๋๋ฅผ ์ผ์ ์ถ์ฒ๋ฅผ ์ขํ์ญ์์ค.
# ์ฃผ์: allowed_origins ๋ฅผ ๋น์ฐ์ง ์๋ ์๊ฐ ์์ ๊ฒ์ฌ๊ฐ '์ผ์ง๋๋ค'.
# "*" ๋ ์ ๋ ์ฐ์ง ๋ง์ญ์์ค โ ์ต์
๊ฒ์ฆ ์คํจ๋ก ์๋ฒ๊ฐ ๊ธฐ๋ํ์ง ๋ชปํฉ๋๋ค
# (websocket.go:1142 "must be absolute URLs with http or https scheme").
# allowed_origins: ["https://dashboard.example", "http://localhost:3000"]
# โโ ์ด ํฌํธ๋ MQTT-over-WebSocket ๋ ์๋นํฉ๋๋ค โโโโโโโโโโโโโโโโโโโโโโโ
# ๊ฒฝ๋ก /mqtt ๋ก ๋ถ์ผ๋ฉด ์์ ํ MQTT ํด๋ผ์ด์ธํธ๊ฐ ๋ฉ๋๋ค (mqtt.go:193,
# websocket.go:1335 โ createMQTTClient, 1883 ๋ฆฌ์ค๋์ ๋์ผ ํจ์).
# ws://:8080/mqtt โ MQTT. retained ์ข
๋ฃ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ต๋๋ค.
# ws://:8080/ โ NATS ๋ค์ดํฐ๋ธ. retained ๋ฅผ ๋ฐ์ง ๋ชปํฉ๋๋ค (N-1).
# ๋ธ๋ผ์ฐ์ ๋์๋ณด๋๋ MQTT.js ๋ก /mqtt ์ ๋ถ์ด๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
}
# โโ ์ธ์ฆ ๋ฐ ๋ฉํฐํ
๋์ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
accounts {
MAM: {
jetstream: enabled # MQTT ๋ด๋ถ ์คํธ๋ฆผ์ด ์ด ๊ณ์ ์์ ์์ฑ๋จ
users: [
# ๋ฐํ์ ๊ฒธ ๊ตฌ๋
์ โ MAM ์์ด์ ํธ ๋ณธ์ฒด (.mam.env ์ MQTT_USERNAME)
{ user: mam_agent, password: $MAM_BROKER_PASS }
# ๊ด์ธก์ โ ๋์๋ณด๋/๋ชจ๋ํฐ๋ง. ๋ฐ๋์ MAM ๊ณ์ ์์ ์์น
{ user: mam_observer, password: $MAM_OBSERVER_PASS,
permissions: {
subscribe: { allow: ["python.mqtt.jobs.>"] } # M3 ์ดํ: "mam..jobs.>"
publish: { deny: [">"] }
}
}
]
}
# MAM ๊ณผ ๋ฌด๊ดํ ํ๋ฉ ์๋น์ค ์ ์ฉ. MAM subject ๋ ๋ณด์ด์ง ์์(์๋๋ ๊ฒฉ๋ฆฌ)
HOME: { jetstream: enabled, users: [ { user: home, password: $HOME_BROKER_PASS } ] }
SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] }
}
system_account: SYS
```
> [!IMPORTANT]
> **๊ด์ธก์๋ ๋ฐ๋์ `MAM` ๊ณ์ ์์ ๋ก๋๋ค.** NATS ๊ณ์ ์ ํ๋ ๊ฒฉ๋ฆฌ ๊ฒฝ๊ณ์ด๋ฏ๋ก `user: home`(๊ณ์ `HOME`)์ผ๋ก ์ ์ํ ํด๋ผ์ด์ธํธ๋ `python.mqtt.jobs.>`๋ฅผ ๊ตฌ๋
ํด๋ **0๊ฑด**์ ๋ฐ์ต๋๋ค. ์๋ก ๋ค๋ฅธ ์ ๋ขฐ ๋๋ฉ์ธ์ด๋ผ ๊ณ์ ์ ๋ฐ๋์ ๊ฐ๋ผ์ผ ํ๋ ์์ธ ์ํฉ์ **๋ถ๋ก X(Option A)** ๋ฅผ ๋ฐ๋ฅด์ญ์์ค.
### 9.2 ํ๋ก๋์
`docker/docker-compose.yaml`
```yaml
# ==============================================================================
# docker/docker-compose.yaml โ MAM ์๊ฒฉ ํ๋ก๋์
๋ธ๋ก์ปค (Track 1R)
# ์ ๋ณธ ๋ฌธ์: PRIVATE_SERVER.md ยง9.2
#
# ์ฌ์ฉ๋ฒ: cd docker && cp .env.example .env && <์ํฌ๋ฆฟ ์ฑ์ฐ๊ธฐ> && docker compose up -d
# ==============================================================================
services:
nats:
image: nats:2.12-alpine # alpine ํ์: healthcheck ์ wget ์ด ์ฌ๊ธฐ์๋ง ์์
container_name: mam-nats
restart: unless-stopped
command: ["-c", "/etc/nats/nats.conf"]
environment:
# ๋ฏธ์ค์ /๋น ๊ฐ์ด๋ฉด ์ปจํ
์ด๋ ์์ฑ ์ ์ compose ๊ฐ ์ค๋จ โ fail-closed
MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set MAM_BROKER_PASS in docker/.env}
MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set MAM_OBSERVER_PASS in docker/.env}
HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set HOME_BROKER_PASS in docker/.env}
SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set SYS_BROKER_PASS in docker/.env}
ports:
# โ Docker ์ published ํฌํธ๋ UFW ๋ฅผ ์ฐํํฉ๋๋ค. ๋
ธ์ถ ํต์ ๋ ๋ฐฉํ๋ฒฝ์ด ์๋๋ผ
# ์ฌ๊ธฐ์ ๋ฐ์ธ๋ ์ฃผ์๊ฐ ๋ด๋นํฉ๋๋ค. ๊ธฐ๋ณธ๊ฐ์ ์ ๋ถ loopback.
- "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 (ํ๋ฉด A: MAM)
- "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS ๋ค์ดํฐ๋ธ (ํ๋ฉด B)
- "127.0.0.1:8222:8222" # ๋ฌด์ธ์ฆ ๋ชจ๋ํฐ๋ง โ loopback ๊ณ ์
- "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (NATS + /mqtt ๊ฒฝ๋ก์ MQTT)
volumes:
- ./nats.conf:/etc/nats/nats.conf:ro
- nats-data:/data # nats.conf ์ store_dir ์ ์ผ์น
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
volumes:
nats-data:
```
> [!WARNING]
> **Docker์ published ํฌํธ๋ UFW๋ฅผ ์ฐํํฉ๋๋ค.** Docker๊ฐ ์ฝ์
ํ๋ NAT/FORWARD ๊ท์น์ด `ufw`์ INPUT ์ฒด์ธ๋ณด๋ค ๋จผ์ ํ๊ฐ๋๋ฏ๋ก, `ufw deny 1883`์ ๊ฑธ์ด๋ `-p 1883:1883`์ผ๋ก ๊ฒ์ํ ํฌํธ๋ ์ธํฐ๋ท์ ์ด๋ฆฝ๋๋ค. ๋ณธ ๊ตฌ์ฑ์ด ๋ฐฉํ๋ฒฝ์๋ง ์์กดํ์ง ์๊ณ **published ํฌํธ ์์ฒด์ ๋ฐ์ธ๋ ์ฃผ์๋ฅผ ๋ช
์**(`${MQTT_BIND}:1883:1883`)ํ๋ ์ด์ ์
๋๋ค.
### 9.3 ์๊ฒฉ ๋คํธ์ํน & ๋ณด์ ๊ฐ์ด๋
| ํญ๋ชฉ | ๐ **๋ชจ๋ธ T: Tailscale/WireGuard ์ค๋ฒ๋ ์ด (๊ถ์ฅ)** | ๋ชจ๋ธ P: ๊ณต๊ฐ TLS (Let's Encrypt) |
|---|---|---|
| ์ธํฐ๋ท ๋
ธ์ถ ๋ฉด์ | **0** (๊ณต๊ฐ ๋ฆฌ์ค๋ ์์) | 8883 1๊ฐ |
| ์ธ์ฆ์ ํ์ | ๋ถํ์ (`MQTT_TLS=0`) | ํ์ + 90์ผ ๊ฐฑ์ |
| **D-4 ํธ์คํธ๋ช
์ ์ฝ** | **ํด๋น ์์** | ๋๋ฉ์ธ ํ์, IP ๋ถ๊ฐ |
| ๋๋ฉ์ธ ํ์ | ๋ถํ์ | **ํ์** |
| ์ด๋์ฑ | ์๋ (Tailscale mesh) | ์๋ |
**๋ฐฉํ๋ฒฝ (UFW) ์ค์ (๋ชจ๋ธ T):**
```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
# tailnet ์ธํฐํ์ด์ค๋ง ํ์ฉ
sudo ufw allow in on tailscale0 to any port 1883 proto tcp
sudo ufw allow in on tailscale0 to any port 4222 proto tcp
sudo ufw allow in on tailscale0 to any port 8080 proto tcp
sudo ufw enable && sudo ufw status verbose
```
**์ํฌ๋ฆฟ ์์ฑ ๋ฐ ํ๊ฒฝ๋ณ์ ์ฃผ์
(`docker/.env`):**
```bash
# docker/.env.example ๋ณต์ฌ ๋ฐ ๊ถํ ์ ํ
cd docker && cp .env.example .env && chmod 600 .env
# ์ํฌ๋ฆฟ ์ํธ ์์ฑ (openssl rand -base64 32 ์ฌ์ฉ)
# .env ํ์ผ์ ์ด๊ณ ์์ฑ๋ ์ํฌ๋ฆฟ ๋ฐ ๋ฐ์ธ๋ ์ฃผ์ ์
๋ ฅ:
# MAM_BROKER_PASS=<์์ฑ๋_ํ ํฐ>
# MAM_OBSERVER_PASS=<์์ฑ๋_ํ ํฐ>
# HOME_BROKER_PASS=<์์ฑ๋_ํ ํฐ>
# SYS_BROKER_PASS=<์์ฑ๋_ํ ํฐ>
# MQTT_BIND=$(tailscale ip -4)
# WS_BIND=$(tailscale ip -4)
```
### 9.4 ์๊ฒฉ ๊ฒ์ฆ ํ๋ ์ด๋ถ (R-1 ~ R-10)
| ID | ๊ฒ์ฆ ํญ๋ชฉ | ๋ฐฉ๋ฒ | ํต๊ณผ ๊ธฐ์ค |
|---|---|---|---|
| **R-1** | ๋ธ๋ก์ปค ํฌ์ค | SSH ํฐ๋ ํ `curl -sf http://127.0.0.1:8222/healthz` | **HTTP 200** |
| **R-2** | ๋ฆฌ์ค๋ + TLS ์ ์ | `curl -s .../varz \| grep -i mqtt`; ๋ชจ๋ธ P๋ `openssl s_client -connect H:8883 -servername H` | MQTT ๋ฆฌ์ค๋ ๋
ธ์ถ, SAN์ `MQTT_BROKER` ํฌํจ |
| **R-3** | ๋
ธ์ถ ๋ฉด์ ๋จ์ธ | ์ธ๋ถ ๋ง์์ `nmap -Pn -p 1883,4222,8222,8080 <๊ณต๊ฐIP>` | ์ ๋ถ closed/filtered |
| **R-4** | ์๋ณต pub/sub + ๊ณ์ JetStream | ์์ ์ก ๋ฑ๋ก โ ๊ตฌ๋
์ ๊ธฐ๋ โ `progress` ๋ฐํ โ ์์ โ ์ข
๊ฒฐ | ์์ ์ฑ๊ณต, `JetStream not enabled for account` **๋ฏธ๋ฐ์** |
| **R-5** | **retained ์ข
๋ฃ ์ด๋ฒคํธ (MQTT)** | `--event completed` ๋ฐํ **ํ** ์ ๊ท `job_subscriber.py` ๊ธฐ๋ | ์ฆ์ ์ต์ข
์ด๋ฒคํธ ์์ |
| **R-6** | ๋ธ๋ก์ปค ์ ์ ๋จ์ธ | `grep -c 'broker.hivemq.com' .mam/delegate_job_logs/$JID/events.ndjson` | **0**, ์๊ฒฉ ํธ์คํธ ๋ฑ์ฅ |
| **R-7** | freeze ํ๊ท (H-1/H-4) | freeze ๊ฒฝ๋ก `publish_event.py`๋ฅผ `MAM_ENV_FILE` ์์ด ์คํ / ์คํ ๊ฒฝ๋ก ์คํ | ๊ณต๊ฐ ๋ธ๋ก์ปค๋ก ๋๊ฐ์ง ์์ |
| **R-8** | ์ ์ฒด ํ๊ท ์ค์ํธ | `.venv/bin/python -m pytest tests/ -q` | **์ ๊ฑด ํต๊ณผ** |
| **R-9** | **๊ด์ธก์ ๊ณ์ ๊ฒฝ๊ณ** | `mam_observer`๋ก NATS ๊ตฌ๋
โ ์์ ํ์ธ. ์ด์ด์ `home`์ผ๋ก ๋์ผ ๊ตฌ๋
| `mam_observer` **์์ **, `home` **0๊ฑด** |
| **R-10** | **retained ๊ฒฝ๊ณ ํ์ธ (N-1)** | ์ก ์ข
๋ฃ **ํ** NATS ๋ค์ดํฐ๋ธ ๊ตฌ๋
์๋ฅผ ์๋ก ๋ถ์ | **0๊ฑด ์์ ** (retained๋ MQTT ์ ์ฉ) |
**WAN ์ง์ฐ ์๊ฐ ์ธก์ (Latency Probe):**
```bash
.venv/bin/python - <<'PY'
import sys, time, statistics
sys.path.insert(0, '.agents/skills/multi-agent-mux-delegate-job/scripts')
import mqtt_common as m
cfg = m.broker_config_from_env()
print(f"target: {cfg.host}:{cfg.port} tls={cfg.tls}")
conn, rtt = [], []
for _ in range(5):
c = m.make_client("latency", cfg)
t0 = time.perf_counter(); c.connect(cfg.host, cfg.port, 10); c.loop_start()
conn.append((time.perf_counter() - t0) * 1000)
t1 = time.perf_counter()
info = c.publish("mam/latency/probe", b"x", qos=1); info.wait_for_publish(10)
rtt.append((time.perf_counter() - t1) * 1000)
c.loop_stop(); c.disconnect()
print(f"connect p50={statistics.median(conn):.1f}ms max={max(conn):.1f}ms")
print(f"qos1 rtt p50={statistics.median(rtt):.1f}ms max={max(rtt):.1f}ms")
PY
```
### 9.5 ์ ํ(Cutover) ์ ์ฐจ (H-2 ๋์)
```
[1 ๋๋ ์ธ] โโ> [2 ์์ฌ ์ค์บ] โโ> [3 .mam.env ๊ต์ฒด] โโ> [4 R-1~R-10] โโ> [5 ๋ ๊ฑฐ์ ์ฐจ๋จ]
```
1. **๋๋ ์ธ**: ์ ๊ท ์์ ์ค๋จ, ์งํ ์ค ์ก์ด ๋ชจ๋ terminal ๋ ๋๊น์ง ๋๊ธฐ.
2. **์์ฌ ์ค์บ**: ์ ๋ธ๋ก์ปค์ ํ ๊ณ ์ ๋ ๋ ์ฝ๋ ์ค์บ ํ ์ ๋ฆฌ.
3. **`.mam.env` ๊ต์ฒด**: ์๊ฒฉ ๋ธ๋ก์ปค ์ฃผ์ ๋ฐ ํ ํฐ ์ ์ฉ.
4. **๊ฒ์ฆ**: R-1 ~ R-10 ์ ๊ฑด ํต๊ณผ ํ์ธ.
5. **๋ ๊ฑฐ์ ์ฐจ๋จ**: ๊ณต๊ฐ ๋ธ๋ก์ปค ์ค์ ์์ ์ ๊ฑฐ.
---
### ๋ถ๋ก X. Option A (๊ณ์ ๊ฐ Export / Import) ์์ธ ๊ฒฝ๋ก
๊ด์ธก์๊ฐ **์๋ก ๋ค๋ฅธ ์ ๋ขฐ ๋๋ฉ์ธ**์ ์ํด ๊ณ์ ์ ์๊ฒฉํ ๋ถ๋ฆฌํด์ผ ํ๋ ๊ฒฝ์ฐ:
```conf
accounts {
MAM: {
jetstream: enabled
users: [ { user: mam_agent, password: $MAM_BROKER_PASS } ]
exports: [ { stream: "python.mqtt.jobs.>", accounts: [HOME] } ]
}
HOME: {
users: [ { user: home, password: $HOME_BROKER_PASS } ]
imports: [ { stream: { account: MAM, subject: "python.mqtt.jobs.>" } } ]
}
SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] }
}
system_account: SYS
```