26 KiB
🔒 MAM 개인 전용 브로커(Private Broker) 구축 및 연동 가이드 (PRIVATE_SERVER.md)
- 작성일: 2026-08-20 (Rev.2)
- 문서 목적: MAM(Multi-Agent Mux)의 공개 브로커 의존성 및 보안 결함(A-2)을 해소하기 위한 개인 전용 브로커(NATS / Mosquitto) 구축, 운영, 다능성 활용 및 MAM 클라이언트 연동 가이드.
- 연계 문서:
NATS_REPORT.md,implementation_plan.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)
# 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 실행:
# 호스트에 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):
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)를 기본 스토리지로 사용합니다.
# 설정 및 데이터 디렉터리 생성 (sudo 불필요)
mkdir -p ~/.config/nats ~/.local/share/nats/data
# 설정 파일 작성 (D-1: 비인용 heredoc <<EOF 으로 $HOME 을 파일 생성 시점에 절대경로로 고정)
cat <<EOF > ~/.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 실행:
docker run -d \
--name mam-mosquitto \
--restart unless-stopped \
-p 1883:1883 \
-v ./mosquitto.conf:/mosquitto/config/mosquitto.conf \
eclipse-mosquitto:latest
기본 mosquitto.conf 설정 파일 예시:
listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
5. 하나의 서버로 여러 프로젝트 — nats-server 다능성 (Versatility)
nats-server의 다능성은 MAM을 네이티브 NATS로 이관할 이유가 아니라, MAM 코드를 한 줄도 바꾸지 않고도 얻을 수 있는 부가적 이득입니다.
5.1 두 개의 소비 평면 (Two Consumption Planes)
nats-server는 단일 프로세스 내에서 여러 프로토콜 리스너를 동시에 구동하므로, MAM의 단순성과 개인 홈랩의 확장성을 완벽히 양립시킵니다.
┌───────────────────────────────────────────────────────────┐
│ nats-server (단일 인스턴스) │
├─────────────────────────────┬─────────────────────────────┤
│ 평면 A: MAM 워크로드 │ 평면 B: 홈랩/개인 프로젝트 │
├─────────────────────────────┼─────────────────────────────┤
프로토콜 │ MQTT 3.1.1 (포트 1883) │ NATS(4222), WebSocket(8080) │
클라이언트 │ paho-mqtt (코드 변경 0줄) │ nats-py, nats.js, CLI 등 자유 │
사용 기능 │ QoS 1, Retain, 와일드카드, TLS│ JetStream 리플레이, KV, Object│
설계 원칙 │ 초경량 동기 CLI 핫패스 보존 │ 고급 비동기 이벤트 스트리밍 │
공유 자원 │ └───── 단일 정적 바이너리 / JetStream 스토리지 / ACL ─────┘│
└───────────────────────────────────────────────────────────┘
5.2 교차 프로토콜 브리징 (Cross-Protocol Bridging)
nats-server는 내부적으로 MQTT 토픽(/)을 NATS Subject(.)로 실시간 자동 변환합니다.- MAM 에이전트가 MQTT 토픽
python/mqtt/jobs/<job_id>/events로 이벤트를 발행하면, 웹 브라우저나 타 프로젝트의 NATS 구독자는 NATS Subjectpython.mqtt.jobs.<job_id>.events또는python.mqtt.jobs.*.events로 즉시 실시간 수신할 수 있습니다. - 실용적 이점: MAM 소스 코드를 단 1줄도 수정하지 않고도 React/Vue 웹 대시보드나 터미널 모니터링 툴을 즉시 부착할 수 있습니다.
- 경계 (필수 인지 — N-1): 교차 프로토콜 브리징은 라이브 스트리밍에 한정됩니다. MQTT의 retained 메시지는 MQTT 구독자에게만 전달되므로(
mqttSendRetainedMsgsToNewSubs가 MQTT SUBSCRIBE 경로 전용), 잡이 끝난 뒤 접속한 NATS/WebSocket 대시보드는 그 잡의 종료 이벤트를 수신하지 못합니다. 사후 상태가 필요하면 (a) 대시보드를 MQTT(1883)로 연결하거나 (b) §5.3 JetStream 리플레이 스트림을 옵트인하십시오. - 계정 배치: 관측 클라이언트는 §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 파일에 개인 서버 주소를 등록합니다.
Note
MAM 코드(
mqtt_common.py)는MQTT_*접두사의 환경변수를 읽습니다. 이전 비공식 문서의MAM_MQTT_*변수는 무효하므로 반드시 아래의 표준 변수명을 사용해야 합니다.
# ==============================================================================
# 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 상태 확인
# 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는 레지스트리에 등록된 잡에 대해서만 발행을 수행하므로, 임시 잡을 등록하고 발행한 후 완료 처리합니다.
# 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. 단위 회귀 테스트 검증
.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% 차단
- Phase 1 (Track 0 선행 패치):
publish_event.py와job_subscriber.py의 로컬 디스크 폴백(B-14,B-15)을 먼저 적용하여 브로커 다운 시에도 루프가 멈추지 않는 방탄 구조를 확립합니다. - Phase 2 (개인 브로커 가동): 개인 서버에
nats-server -c nats.conf를 구동하고.mam.env에MQTT_BROKER를 연결합니다. - Phase 3 (A-2 보안 완전 종결): 워크스페이스 지문 토픽(
mam/<sha256[:12]>/jobs/...) 및 무조건 토큰 발급을 적용하여 공개 브로커 위험을 완전히 영구 폐기합니다.
9. 원격 서버 nats-server Docker 프로덕션 배포 가이드 (Track 1R)
원격 VPS 또는 상시 가동 홈랩 서버에 프로덕션 수준의 nats-server를 Docker 기반으로 구축하고 MAM과 연동하는 표준 절차입니다.
9.1 프로덕션 nats.conf
# nats.conf — 원격 프로덕션 (컨테이너 내부 절대경로 기준)
server_name: mam-hub
# ── JetStream: MQTT retained/QoS1 저장소. MAM 종료 이벤트 재수신이 여기에 의존 ──
jetstream {
store_dir: "/data" # D-1: 절대경로 고정. '~' 도, 인용된 "$HOME" 도 확장되지 않음
max_file: 10G
max_mem: 256M
}
http_port: 8222 # D-3: 호스트 게시는 loopback 한정 (§9.3)
mqtt {
port: 1883 # 공개 TLS 모델에서는 8883 + tls 블록 (§9.3)
ack_wait: 60s # WAN RTT 흡수 (기본 30s)
max_ack_pending: 1024 # 기본 100 → 다중 에이전트 동시 발행 여유
}
websocket {
port: 8080
no_tls: true # 사설망/tailnet 한정
}
# ── 인증 및 멀티테넌시 (Rev.2: C1/C2 반영) ────────────────────────────────
accounts {
MAM: {
jetstream: enabled # MQTT 내부 스트림이 이 계정 안에 생성됨
users: [
# 발행자 겸 구독자 — MAM 에이전트 본체
{ user: mam_agent, password: $MAM_BROKER_PASS }
# 관측자 — 대시보드/모니터링. PRIVATE_SERVER.md §5.5 의 '동일 계정 배치' 처방
{ user: mam_observer, password: $MAM_OBSERVER_PASS,
permissions: {
subscribe: { allow: ["python.mqtt.jobs.>"] } # M3 이후: "mam.<fp>.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-compose.yml
version: '3.8'
services:
nats:
image: nats:2.12-alpine # D-2: latest(=scratch)는 healthcheck 불가
container_name: mam-nats
restart: unless-stopped
command: ["-c", "/etc/nats/nats.conf"]
environment:
# 미해결 $VAR 는 파싱 에러 → 시크릿 누락 시 '기동 실패'로 fail-closed
MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set in .env}
MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set in .env}
HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set in .env}
SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set in .env}
ports:
- "${MQTT_BIND:-127.0.0.1}:1883:1883"
- "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS 네이티브 프로토콜 (Plane B)
- "127.0.0.1:8222:8222" # D-3: 무인증 모니터링은 loopback 한정
- "${WS_BIND:-127.0.0.1}:8080:8080"
volumes:
- ./nats.conf:/etc/nats/nats.conf:ro
- nats-data:/data
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):
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
시크릿 생성 및 바인드 주소 주입 (.env):
# 서버 측 compose 옆 .env
cat <<EOF > .env
MQTT_BIND=$(tailscale ip -4)
WS_BIND=$(tailscale ip -4)
MAM_BROKER_PASS=$(openssl rand -base64 32)
MAM_OBSERVER_PASS=$(openssl rand -base64 32)
HOME_BROKER_PASS=$(openssl rand -base64 32)
SYS_BROKER_PASS=$(openssl rand -base64 32)
EOF
chmod 600 .env
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):
.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 레거시 차단]
- 드레인: 신규 위임 중단, 진행 중 잡이 모두 terminal 될 때까지 대기.
- 잔여 스캔: 옛 브로커에 핀 고정된 레코드 스캔 후 정리.
.mam.env교체: 원격 브로커 주소 및 토큰 적용.- 검증: R-1 ~ R-10 전건 통과 확인.
- 레거시 차단: 공개 브로커 설정 완전 제거.
부록 X. Option A (계정 간 Export / Import) 예외 경로
관측자가 서로 다른 신뢰 도메인에 속해 계정을 엄격히 분리해야 하는 경우:
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