feat(deploy): create production Docker assets in docker/ (compose, nats.conf, env template, README) with D-22~D-30 freshness guards
This commit is contained in:
+88
-41
@@ -193,7 +193,7 @@ persistence_location /mosquitto/data/
|
||||
|
||||
`nats-server`의 다능성은 **MAM을 네이티브 NATS로 이관할 이유가 아니라, MAM 코드를 한 줄도 바꾸지 않고도 얻을 수 있는 부가적 이득**입니다.
|
||||
|
||||
### 5.1 두 개의 소비 평면 (Two Consumption Planes)
|
||||
### 5.1 두 개의 소비 평면 및 세 가지 접속 경로 (Consumption Planes & Transport Paths)
|
||||
|
||||
`nats-server`는 단일 프로세스 내에서 여러 프로토콜 리스너를 동시에 구동하므로, MAM의 단순성과 개인 홈랩의 확장성을 완벽히 양립시킵니다.
|
||||
|
||||
@@ -204,18 +204,25 @@ persistence_location /mosquitto/data/
|
||||
│ 평면 A: MAM 워크로드 │ 평면 B: 홈랩/개인 프로젝트 │
|
||||
├─────────────────────────────┼─────────────────────────────┤
|
||||
프로토콜 │ MQTT 3.1.1 (포트 1883) │ NATS(4222), WebSocket(8080) │
|
||||
클라이언트 │ paho-mqtt (코드 변경 0줄) │ nats-py, nats.js, CLI 등 자유 │
|
||||
클라이언트 │ 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/<job_id>/events`로 이벤트를 발행하면, 웹 브라우저나 타 프로젝트의 NATS 구독자는 NATS Subject `python.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 리플레이 스트림을 옵트인하십시오.
|
||||
- **경계 (필수 인지 — N-1 & N-7)**:
|
||||
- NATS 네이티브(경로 없는 WebSocket 또는 4222)로 접속하는 경우: 교차 프로토콜 브리징은 **라이브 스트리밍에 한정**됩니다. 잡이 끝난 뒤 접속한 네이티브 NATS 대시보드는 그 잡의 **종료 이벤트를 수신하지 못합니다** (retained 메시지는 MQTT SUBSCRIBE 경로 전용).
|
||||
- 웹 대시보드에서 사후 종료 이벤트까지 무상으로 수신하려면 **`ws://<host>:8080/mqtt` 경로로 `MQTT.js` 클라이언트를 연결**하십시오 (N-7). 리플레이 스트림 구축 및 디스크 관리 부담 없이 완전한 종료 이벤트를 즉시 수신할 수 있습니다.
|
||||
- **계정 배치**: 관측 클라이언트는 §5.5 처방대로 `MAM` 계정 안의 읽기 전용 사용자(`mam_observer`)로 접속해야 합니다. 다른 계정에서는 subject가 보이지 않습니다.
|
||||
- **주의 사항**: 토픽 레벨 내에 마침표(`.`)가 포함되면 NATS 계층에서 토큰이 분리될 수 있으나, MAM의 `job_id`는 8자리 hex, 워크스페이스 지문은 12자리 hex이므로 안전합니다.
|
||||
|
||||
@@ -332,41 +339,74 @@ Step 2의 `-v` 출력 로그 또는 `.mam/delegate_job_logs/$JID/events.ndjson`
|
||||
|
||||
원격 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
|
||||
# nats.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 저장소. MAM 종료 이벤트 재수신이 여기에 의존 ──
|
||||
# ── JetStream: MQTT retained/QoS1 저장소. 종료 이벤트 재수신이 여기에 의존 ──
|
||||
jetstream {
|
||||
store_dir: "/data" # D-1: 절대경로 고정. '~' 도, 인용된 "$HOME" 도 확장되지 않음
|
||||
max_file: 10G
|
||||
store_dir: "/data" # 절대경로 고정. '~' 도 인용된 "$HOME" 도 확장되지 않음
|
||||
max_file: 10G # 접미사는 대문자만 유효 (K/M/G/T)
|
||||
max_mem: 256M
|
||||
}
|
||||
|
||||
http_port: 8222 # D-3: 호스트 게시는 loopback 한정 (§9.3)
|
||||
http_port: 8222 # 무인증 모니터링 → 호스트 게시는 loopback 한정 (compose)
|
||||
|
||||
mqtt {
|
||||
port: 1883 # 공개 TLS 모델에서는 8883 + tls 블록 (§9.3)
|
||||
ack_wait: 60s # WAN RTT 흡수 (기본 30s)
|
||||
max_ack_pending: 1024 # 기본 100 → 다중 에이전트 동시 발행 여유
|
||||
port: 1883
|
||||
ack_wait: 60s # WAN RTT 흡수 (기본 30s)
|
||||
max_ack_pending: 1024 # 다중 에이전트 동시 발행 여유 (상한 65535)
|
||||
}
|
||||
|
||||
websocket {
|
||||
port: 8080
|
||||
no_tls: true # 사설망/tailnet 한정
|
||||
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://<host>:8080/mqtt → MQTT. retained 종료 이벤트를 받습니다.
|
||||
# ws://<host>:8080/ → NATS 네이티브. retained 를 받지 못합니다 (N-1).
|
||||
# 브라우저 대시보드는 MQTT.js 로 /mqtt 에 붙이는 것을 권장합니다.
|
||||
}
|
||||
|
||||
# ── 인증 및 멀티테넌시 (Rev.2: C1/C2 반영) ────────────────────────────────
|
||||
# ── 인증 및 멀티테넌시 ────────────────────────────────────────────────────
|
||||
accounts {
|
||||
MAM: {
|
||||
jetstream: enabled # MQTT 내부 스트림이 이 계정 안에 생성됨
|
||||
jetstream: enabled # MQTT 내부 스트림이 이 계정 안에 생성됨
|
||||
users: [
|
||||
# 발행자 겸 구독자 — MAM 에이전트 본체
|
||||
# 발행자 겸 구독자 — MAM 에이전트 본체 (.mam.env 의 MQTT_USERNAME)
|
||||
{ user: mam_agent, password: $MAM_BROKER_PASS }
|
||||
|
||||
# 관측자 — 대시보드/모니터링. PRIVATE_SERVER.md §5.5 의 '동일 계정 배치' 처방
|
||||
# 관측자 — 대시보드/모니터링. 반드시 MAM 계정 안에 위치
|
||||
{ user: mam_observer, password: $MAM_OBSERVER_PASS,
|
||||
permissions: {
|
||||
subscribe: { allow: ["python.mqtt.jobs.>"] } # M3 이후: "mam.<fp>.jobs.>"
|
||||
@@ -385,31 +425,37 @@ system_account: SYS
|
||||
> [!IMPORTANT]
|
||||
> **관측자는 반드시 `MAM` 계정 안에 둡니다.** NATS 계정은 하드 격리 경계이므로 `user: home`(계정 `HOME`)으로 접속한 클라이언트는 `python.mqtt.jobs.>`를 구독해도 **0건**을 받습니다. 서로 다른 신뢰 도메인이라 계정을 반드시 갈라야 하는 예외 상황은 **부록 X(Option A)** 를 따르십시오.
|
||||
|
||||
### 9.2 프로덕션 `docker-compose.yml`
|
||||
### 9.2 프로덕션 `docker/docker-compose.yaml`
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
# ==============================================================================
|
||||
# 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 # D-2: latest(=scratch)는 healthcheck 불가
|
||||
image: nats:2.12-alpine # alpine 필수: healthcheck 의 wget 이 여기에만 있음
|
||||
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}
|
||||
# 미설정/빈 값이면 컨테이너 생성 전에 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:
|
||||
- "${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"
|
||||
# ⚠ 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-data:/data # nats.conf 의 store_dir 와 일치
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"]
|
||||
interval: 30s
|
||||
@@ -451,18 +497,19 @@ sudo ufw allow in on tailscale0 to any port 8080 proto tcp
|
||||
sudo ufw enable && sudo ufw status verbose
|
||||
```
|
||||
|
||||
**시크릿 생성 및 바인드 주소 주입 (`.env`):**
|
||||
**시크릿 생성 및 환경변수 주입 (`docker/.env`):**
|
||||
```bash
|
||||
# 서버 측 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
|
||||
# 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)
|
||||
|
||||
Reference in New Issue
Block a user