Files
multi-agent-mux/docker/README.md
T

159 lines
8.2 KiB
Markdown

# 🐳 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=<MAM_BROKER_PASS_값>
```
### (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: "<MAM_OBSERVER_PASS_값>",
protocolVersion: 4, // MQTT 3.1.1
});
client.subscribe("python/mqtt/jobs/+/events");
// 이 클라이언트는 nats-server 내부에서 완전한 MQTT 클라이언트로 취급되므로
// 잡이 끝난 뒤에 대시보드를 새로고침해도 retained 최종 이벤트를 즉시 수신합니다.
// 참고 (nats.ws):
// ws://<host>: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 <host> -p 1883 -t test -m hi` | Connect return code 5 (Not authorized) |
| **R-6** | 인증 성공 (정상) | `mosquitto_pub -h <host> -p 1883 -u mam_agent -P <pw> -t test -m hi` | 메시지 발행 성공 (rc=0) |
| **R-7** | Retained 이벤트 전달 | `mosquitto_sub -h <host> -p 1883 -u mam_agent -P <pw> -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://<host>:8080/mqtt` 로 MQTT.js 클라이언트로 접속하십시오 (N-7). |