🐳 MAM Remote Broker Production Deployment (docker/)
이 디렉터리는 multi-agent-mux (MAM) 멀티 에이전트 관측 백플레인을 위한 원격 nats-server 프로덕션 브로커 배포 자산의 **정본(canonical)**입니다. 상세 아키텍처 및 배경 지식은 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 composeCLI) - Tailscale 또는 WireGuard 사설 VPN (권장: 모델 T 사설 오버레이)
- 호스트 방화벽 (UFW 등) 기본 차단 정책 (공인 IP 개방 포트 없음)
3. 5분 빠른 배포
# 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 호스트 방화벽 기본 규칙:
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 포트 스캔을 수행하여 브로커가 인터넷에 노출되지 않았음을 검증합니다:
nmap -Pn -p 1883,4222,8222,8080 <공개_IP>
모든 포트가 closed 또는 filtered 로 표시되어야 합니다.
6. 클라이언트 연결
(a) MAM 에이전트 환경변수 (.mam.env)
로컬 개발 머신의 MAM 워크스페이스 최상위 .mam.env 에 브로커 접속 정보를 설정합니다:
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 반영)
// 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). |