# ๐Ÿ“ ๊ตฌํ˜„ ๊ณ„ํš์„œ Rev.2: `docker/` ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ž์‚ฐ ์ •๋ณธํ™” (Job `2168e631`) - **์ž‘์„ฑ์ผ**: 2026-08-23 - **์—ญํ• **: Planner (`.agents/MULTI_AGENT_RULES.md` ยง1 โ€” Planner ๋Š” ์ €์žฅ์†Œ ์ฝ”๋“œ/๋ฌธ์„œ๋ฅผ **์ˆ˜์ •ํ•˜์ง€ ์•Š์œผ๋ฉฐ**, ์‚ฐ์ถœ๋ฌผ์€ ๋ณธ ๊ณ„ํš์„œ์ž…๋‹ˆ๋‹ค) - **๊ธฐ์ค€ ์ปค๋ฐ‹**: `3523b9b` (ํ…Œ์ŠคํŠธ **297๊ฑด ์ˆ˜์ง‘** ์‹ค์ธก) - **์„ ํ–‰ ๋ฆฌ๋น„์ „**: `810987f6` (Rev.1) โ† ๋ณธ ๋ฌธ์„œ๊ฐ€ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค - **ํŒ์ • ๋Œ€์ƒ ๋ฆฌ๋ทฐ**: `2934740e` (agy, `[VERDICT: PASS WITH CHALLENGE]`) โ€” WebSocket `same_origin` ๊ธฐ๋ณธ๊ฐ’ ์ด์˜์ œ๊ธฐ - **๋Œ€์ƒ ์‚ฐ์ถœ๋ฌผ**: `docker/docker-compose.yaml`, `docker/nats.conf`, `docker/.env.example`, `docker/README.md`, `tests/test_deploy_freshness.py` (D-22 ~ **D-30**) - **ํ…Œ์ŠคํŠธ ์ˆ˜ ์˜ˆ์ƒ**: 297 โ†’ **306** (Rev.1 ์˜ 305 ์—์„œ D-30 ์ถ”๊ฐ€) --- ## A. ๋ฆฌ๋ทฐ ํŒ์ • (Adjudication of Challenge `2934740e`) ### A-1. ํŒ์ • ์š”์•ฝ | ํ•ญ๋ชฉ | ํŒ์ • | ๊ทผ๊ฑฐ | |---|---|---| | **์ „์ œ**: "`websocket {}` ๋ฅผ ์›์  ์„ค์ • ์—†์ด ์ •์˜ํ•˜๋ฉด NATS ๊ฐ€ `same_origin: true` ๋กœ ๊ธฐ๋ณธ ๋™์ž‘ํ•œ๋‹ค" | โŒ **๊ธฐ๊ฐ (์‚ฌ์‹ค๊ณผ ๋ฐ˜๋Œ€)** | `SameOrigin` ์€ ํ‰๋ฒ”ํ•œ `bool` ํ•„๋“œ์ด๊ณ  **๊ธฐ๋ณธ๊ฐ’์„ `true` ๋กœ ์„ค์ •ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์ €์žฅ์†Œ ์–ด๋””์—๋„ ์—†์Œ**. `checkOrigin()` ์€ `!checkSame && listEmpty` ์ผ ๋•Œ **์ฆ‰์‹œ `nil` ๋ฐ˜ํ™˜** | | **๊ท€๊ฒฐ**: "๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ 403 Forbidden ์œผ๋กœ ๊ฑฐ๋ถ€๋œ๋‹ค" | โŒ **๊ธฐ๊ฐ** | 403 ๊ฒฝ๋กœ๋Š” ์‹ค์žฌํ•˜๋‚˜(`websocket.go:869`) ๋„๋‹ฌ ์กฐ๊ฑด์ด ์„ฑ๋ฆฝํ•˜์ง€ ์•Š์Œ. ํ˜„ ์„ค์ •์—์„œ ๊ต์ฐจ ์ถœ์ฒ˜ ๋ธŒ๋ผ์šฐ์ € ์ ‘์†์€ **๊ทธ๋Œ€๋กœ ์„ฑ๊ณต** | | **์ฒ˜๋ฐฉ 1**: `same_origin: false` ์ถ”๊ฐ€ | โš ๏ธ **๋ถ€๋ถ„ ์ˆ˜์šฉ (๋ฌดํ•ดํ•˜๋‚˜ no-op)** | ์œ ํšจํ•œ ํ‚ค์ด๋‚˜ ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋™์ผ. "๊บผ์•ผ๋งŒ ๋™์ž‘ํ•œ๋‹ค"๋Š” ์ž˜๋ชป๋œ ์„œ์‚ฌ๋ฅผ ์„ค์ • ํŒŒ์ผ์— ์ƒˆ๊ธฐ๊ฒŒ ๋จ โ†’ **์ฃผ์„์œผ๋กœ ์‚ฌ์‹ค์„ ๊ธฐ๋ก**ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ ์ˆ˜์šฉ | | **์ฒ˜๋ฐฉ 2**: `allowed_origins: ["*"]` | ๐Ÿ”ด **๊ฐ•๋ ฅ ๊ธฐ๊ฐ โ€” ์„œ๋ฒ„๊ฐ€ ๊ธฐ๋™ํ•˜์ง€ ๋ชปํ•จ** | `validateWebsocketOptions` ๊ฐ€ `"*"` ์˜ scheme ์ด http/https ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉฐ **์—๋Ÿฌ ๋ฐ˜ํ™˜**(`websocket.go:1142-1143`). ์ด ์ฒ˜๋ฐฉ์„ ๋”ฐ๋ฅด๋ฉด ๋ธŒ๋กœ์ปค๊ฐ€ **์•„์˜ˆ ๋œจ์ง€ ์•Š์Œ** | | **์ฒ˜๋ฐฉ 3**: Mixed Content(`https://` โ†’ `ws://`) ๋ฌธ์„œํ™” | โœ… **์ „๋ฉด ์ˆ˜์šฉ** | NATS ์™€ ๋ฌด๊ด€ํ•œ ๋ธŒ๋ผ์šฐ์ € ์ •์ฑ…์ด๋ฉฐ ์‹ค์ œ๋กœ ํ™ˆ๋žฉ์—์„œ ์ž์ฃผ ๋ฐœ์ƒ. ยง3.4 ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ํ‘œ์— ๋ฐ˜์˜ | | **๋ถ€์ˆ˜ ํšจ๊ณผ** | ๐ŸŸข **์‹ ๊ทœ ๋ฐœ๊ฒฌ N-7** | ์ฑŒ๋ฆฐ์ง€๊ฐ€ ์ง€๋ชฉํ•œ "Plane B ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ" ์˜์—ญ์„ ํŒŒ๋‹ค๊ฐ€, **8080 ์ด `/mqtt` ๊ฒฝ๋กœ๋กœ MQTT-over-WebSocket ๋„ ์„œ๋น™**ํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์„ ํ™•์ธ. Rev.2 ์ด๋ž˜ ๋ฏธํ•ด๊ฒฐ์ด๋˜ ์—ด๋ฆฐ ์งˆ๋ฌธ์ด ์ด๊ฑธ๋กœ **ํ•ด์†Œ**๋จ | ### A-2. ์ „์ œ๊ฐ€ ์™œ ์‚ฌ์‹ค๊ณผ ๋ฐ˜๋Œ€์ธ๊ฐ€ โ€” ์‹ค์ธก **(1) ๊ตฌ์กฐ์ฒด ํ•„๋“œ ์ฃผ์„์ด ๋ช…์‹œ์ ์œผ๋กœ ๋ฐ˜๋Œ€๋ฅผ ๋งํ•ฉ๋‹ˆ๋‹ค.** `server/opts.go:672-677`: ```go // If true, the Origin header must match the request's host. SameOrigin bool // Only origins in this list will be accepted. If empty and // SameOrigin is false, any origin is accepted. AllowedOrigins []string ``` **(2) ๊ธฐ๋ณธ๊ฐ’์„ `true` ๋กœ ์„ธ์šฐ๋Š” ์ฝ”๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.** `SameOrigin = true` / `SameOrigin:` (๊ตฌ์กฐ์ฒด ๋ฆฌํ„ฐ๋Ÿด) ๋กœ grep ํ•˜๋ฉด `opts.go`ยท`websocket.go` ์–‘์ชฝ์—์„œ **0๊ฑด**์ž…๋‹ˆ๋‹ค. ๊ฐ’์€ ์˜ค์ง ์„ค์ • ํŒŒ์„œ์—์„œ๋งŒ ๋Œ€์ž…๋ฉ๋‹ˆ๋‹ค(`opts.go:5545-5546`). ๋”ฐ๋ผ์„œ Go ์ œ๋กœ๊ฐ’ `false` ๊ฐ€ ๊ทธ๋Œ€๋กœ ์œ ํšจ ๊ธฐ๋ณธ๊ฐ’์ž…๋‹ˆ๋‹ค. **(3) ๊ฒ€์‚ฌ ์ž์ฒด๊ฐ€ ๋‹จ๋ฝ(short-circuit)๋ฉ๋‹ˆ๋‹ค.** `server/websocket.go:1034-1041`: ```go func (w *srvWebsocket) checkOrigin(r *http.Request) error { checkSame := w.sameOrigin listEmpty := len(w.allowedOrigins) == 0 if !checkSame && listEmpty { return nil // โ† ์šฐ๋ฆฌ ์„ค์ •์ด ์—ฌ๊ธฐ์„œ ๋๋‚ฉ๋‹ˆ๋‹ค } ... ``` ์šฐ๋ฆฌ `websocket { port: 8080, no_tls: true }` ๋Š” `sameOrigin=false`, `allowedOrigins` ๋น„์–ด ์žˆ์Œ โ†’ ์ฒซ ์กฐ๊ฑด์—์„œ `nil` ๋ฐ˜ํ™˜. `Origin` ํ—ค๋”๋ฅผ ์ฝ์ง€๋„ ์•Š์Šต๋‹ˆ๋‹ค. **`http://localhost:3000` ์—์„œ ๋œฌ React ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ `ws://100.x.y.z:8080` ๋กœ ๋ถ™๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์•„๋ฌด ๋ณ€๊ฒฝ ์—†์ด ์„ฑ๊ณตํ•ฉ๋‹ˆ๋‹ค.** ์ฑŒ๋ฆฐ์ง€๊ฐ€ ์ธ์šฉํ•œ 403 ๊ฒฝ๋กœ๋Š” ์‹ค์žฌํ•ฉ๋‹ˆ๋‹ค(`websocket.go:868-870`, `StatusForbidden`, `"origin not allowed: %v"`). ๋‹ค๋งŒ ๊ทธ ๋ฌธ์€ `checkSame || !listEmpty` ์ผ ๋•Œ๋งŒ ์—ด๋ฆฝ๋‹ˆ๋‹ค. ์ฆ‰ **๋ฌธ์€ ์žˆ์œผ๋‚˜ ์šฐ๋ฆฌ ์„ค์ •์—์„œ๋Š” ๊ทธ ์•ž๊นŒ์ง€ ๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ### A-3. ์ฒ˜๋ฐฉ 2 ๋ฅผ ๋”ฐ๋ฅด๋ฉด ๋ธŒ๋กœ์ปค๊ฐ€ ์ฃฝ๋Š”๋‹ค โ€” ์‹ค์ธก `allowed_origins: ["*"]` ๋Š” ๋‹จ์ง€ ๋ถˆํ•„์š”ํ•œ ๊ฒŒ ์•„๋‹ˆ๋ผ **๊ธฐ๋™ ์ฐจ๋‹จ** ์„ค์ •์ž…๋‹ˆ๋‹ค. `server/websocket.go:1136-1150` (`validateWebsocketOptions`): ```go for _, ao := range wo.AllowedOrigins { u, err := url.ParseRequestURI(ao) if err != nil { return fmt.Errorf("unable to parse allowed origin: %v", err) } if u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("unable to parse allowed origin %q: allowed origins must be "+ "absolute URLs with http or https scheme", ao) } if u.Host == _EMPTY_ { ... } ``` `"*"` ๋Š” scheme ์ด ๋น„์–ด ์žˆ์œผ๋ฏ€๋กœ ๋‘ ๋ฒˆ์งธ ๋ถ„๊ธฐ์—์„œ ์—๋Ÿฌ. ์˜ต์…˜ ๊ฒ€์ฆ ์‹คํŒจ๋Š” ๊ธฐ๋™ ์‹คํŒจ์ž…๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ๋…ผ๋ฆฌ์ ์œผ๋กœ๋„ ์—ญํšจ๊ณผ์ž…๋‹ˆ๋‹ค โ€” `AllowedOrigins` ๋ฅผ ๋น„์šฐ์ง€ ์•Š๋Š” ์ˆœ๊ฐ„ `listEmpty` ๊ฐ€ `false` ๊ฐ€ ๋˜์–ด **์›์  ๊ฒ€์‚ฌ๊ฐ€ ์ผœ์ง‘๋‹ˆ๋‹ค**. "๋ชจ๋‘ ํ—ˆ์šฉ"์„ ์˜๋„ํ•œ ์„ค์ •์ด "๊ฒ€์‚ฌ ํ™œ์„ฑํ™”"๋ฅผ ์œ ๋ฐœํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. > [!IMPORTANT] > ์ด ํ•ญ๋ชฉ์€ ์ฑŒ๋ฆฐ์ง€์˜ ์ฒ˜๋ฐฉ์„ ๊ทธ๋Œ€๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค๋ฉด **ํ”„๋กœ๋•์…˜ ๋ธŒ๋กœ์ปค๊ฐ€ ๊ธฐ๋™์กฐ์ฐจ ๋ชป ํ–ˆ์„** ์‚ฌ์•ˆ์ž…๋‹ˆ๋‹ค. ๋ฆฌ๋ทฐ๋ฅผ ์ง€์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฐ€์„ค๋กœ ์ทจ๊ธ‰ํ•˜๊ณ  ์ธก์ •ํ•œ ๊ฒฐ๊ณผ์ด๋ฉฐ, `MULTI_AGENT_RULES.md` ยง1 ์˜ '[REBUT:]' ์ ˆ์ฐจ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ### A-4. ๊ทธ๋Ÿผ์—๋„ ์ฑŒ๋ฆฐ์ง€๊ฐ€ ์˜ณ๊ฒŒ ์งš์€ ๊ฒƒ 1. **Mixed Content ๋Š” 100% ์‹ค์žฌํ•˜๋Š” ์ œ์•ฝ**์ž…๋‹ˆ๋‹ค. NATS ์„ค์ •๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ, `https://` ๋กœ ์„œ๋น™๋œ ํŽ˜์ด์ง€๋Š” `ws://` ์—ฐ๊ฒฐ์„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. ํ™ˆ๋žฉ์—์„œ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ HTTPS ๋กœ ์˜ฌ๋ฆฌ๋Š” ์ˆœ๊ฐ„ 8080 ํ‰๋ฌธ WS ๋Š” ๋ชป ์”๋‹ˆ๋‹ค. ยง3.4 ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. 2. **์šด์˜์ž๊ฐ€ ๋ฐ˜๋“œ์‹œ ๊ถ๊ธˆํ•ดํ•  ์ง€์ ์„ ์ •ํ™•ํžˆ ์ง€๋ชฉ**ํ–ˆ์Šต๋‹ˆ๋‹ค. Rev.1 ์˜ `websocket { port: 8080, no_tls: true }` ๋Š” ์›์  ์ •์ฑ…์— ๋Œ€ํ•ด **์•„๋ฌด ๋ง๋„ ํ•˜์ง€ ์•Š์•˜๊ณ **, ๊ทธ๋ž˜์„œ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ •๋ฐ˜๋Œ€๋กœ ์ถ”์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์„ค์ • ํŒŒ์ผ์ด ์นจ๋ฌตํ•˜๋ฉด ๋…์ž๊ฐ€ ์ตœ์•…์„ ๊ฐ€์ •ํ•œ๋‹ค๋Š” ์ฆ๊ฑฐ์ž…๋‹ˆ๋‹ค โ€” ์ฃผ์„์œผ๋กœ ์‚ฌ์‹ค์„ ๋ช…๋ฌธํ™”ํ•ฉ๋‹ˆ๋‹ค(ยง3.1). 3. **๋ณด์•ˆ ๋ฐฉํ–ฅ์€ ์˜คํžˆ๋ ค ๋ฐ˜๋Œ€๋กœ ์—ด๋ ค ์žˆ์Šต๋‹ˆ๋‹ค.** ๊ธฐ๋ณธ๊ฐ’์ด ๊ด€๋Œ€ํ•˜๋ฏ€๋กœ, 8080 ์„ tailnet ๋ฐ–์œผ๋กœ ๋‚ด๋ณด๋‚ด๋Š” ์ˆœ๊ฐ„ ์ž„์˜ ์›น ํŽ˜์ด์ง€๊ฐ€ ํ•ธ๋“œ์…ฐ์ดํฌ๋ฅผ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์ธ์ฆ์€ ๋ณ„๋„๋กœ ๋ง‰์ง€๋งŒ). ์™„ํ™”๊ฐ€ ์•„๋‹ˆ๋ผ **๊ฐ•ํ™”** ์ฒ˜๋ฐฉ(`allowed_origins` ์— ์‹ค์ œ ๋Œ€์‹œ๋ณด๋“œ URL)์ด ํ•„์š”ํ•˜๋ฉฐ, ์ด๋ฅผ ์ฃผ์„ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์ œ๊ณตํ•˜๊ณ  D-30 ๊ฐ€๋“œ๋กœ `"*"` ํšŒ๊ท€๋ฅผ ๋ด‰์ธํ•ฉ๋‹ˆ๋‹ค. ### A-5. ์‹ ๊ทœ ๋ฐœ๊ฒฌ N-7 โ€” 8080 ์€ MQTT-over-WebSocket ๋„ ์„œ๋น™ํ•œ๋‹ค (์—ด๋ฆฐ ์งˆ๋ฌธ ํ•ด์†Œ) ์ฑŒ๋ฆฐ์ง€์˜ ์ฃผ์ œ(Plane B ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ)๋ฅผ ์ธก์ •ํ•˜๋‹ค ํ™•์ธํ•œ ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค. ```go // server/websocket.go:820-833 if r.URL != nil { ep := r.URL.EscapedPath() if strings.HasSuffix(ep, leafNodeWSPath) { kind = LEAF } else if strings.HasSuffix(ep, mqttWSPath) { kind = MQTT } // mqttWSPath = "/mqtt" } // Reject MQTT-over-WebSocket upgrades unless MQTT is enabled. if kind == MQTT && opts.MQTT.Port == 0 { ... 404 ... } // server/websocket.go:1333-1335 case MQTT: s.createMQTTClient(res.conn, res.ws) // โ† 1883 ๋ฆฌ์Šค๋„ˆ์™€ ๋™์ผ ํ•จ์ˆ˜ ``` - `mqttWSPath = "/mqtt"` (`server/mqtt.go:193`). - ๊ฒŒ์ดํŠธ๋Š” `opts.MQTT.Port != 0` ๋ฟ์ด๋ฉฐ, ์šฐ๋ฆฌ ์„ค์ •์€ `mqtt { port: 1883 }` ์ด๋ฏ€๋กœ **์ด๋ฏธ ์ถฉ์กฑ**์ž…๋‹ˆ๋‹ค. - ์ƒ์„ฑ ํ•จ์ˆ˜๊ฐ€ ๋„ค์ดํ‹ฐ๋ธŒ 1883 ๋ฆฌ์Šค๋„ˆ์™€ **๋™์ผํ•œ `createMQTTClient`** (`mqtt.go:552` vs `websocket.go:1335`) ์ด๋ฏ€๋กœ, ์ด ์—ฐ๊ฒฐ์€ ํŠธ๋žœ์ŠคํฌํŠธ๋งŒ WebSocket ์ธ **์™„์ „ํ•œ MQTT ํด๋ผ์ด์–ธํŠธ**์ž…๋‹ˆ๋‹ค. **์ด๊ฒƒ์ด ์™œ ์ค‘์š”ํ•œ๊ฐ€**: Rev.2(`b11d499d`) ์ด๋ž˜ ๋‚จ์•„ ์žˆ๋˜ ์—ด๋ฆฐ ์งˆ๋ฌธ โ€” *"๋Œ€์‹œ๋ณด๋“œ๋ฅผ MQTT ๋กœ ๋ถ™์ผ ๊ฒƒ์ธ๊ฐ€, ์•„๋‹ˆ๋ฉด N-1(retained ๋Š” MQTT ์ „์šฉ) ์ œ์•ฝ์„ ๋ฐ›์•„๋“ค์ด๊ณ  JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ๋งŒ๋“ค ๊ฒƒ์ธ๊ฐ€"* โ€” ๊ฐ€ **์ œ3์˜ ๋‹ต์œผ๋กœ ํ•ด์†Œ**๋ฉ๋‹ˆ๋‹ค. > ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ **MQTT.js ๋กœ `ws://mam-hub:8080/mqtt` ์— ์ ‘์†ํ•˜๋ฉด**, ๊ทธ๊ฒƒ์€ MQTT ํด๋ผ์ด์–ธํŠธ์ด๋ฏ€๋กœ `mqttSendRetainedMsgsToNewSubs` ๊ฒฝ๋กœ๋ฅผ ๊ทธ๋Œ€๋กœ ํƒ€๊ณ  **retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค**. ๋ณ„๋„ ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ๋„, ๋””์Šคํฌ ๊ด€๋ฆฌ ๋ถ€๋‹ด๋„ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด ๊ฐ™์€ 8080 ํฌํŠธ๋ผ๋„ **NATS ๋„ค์ดํ‹ฐ๋ธŒ WebSocket**(๊ฒฝ๋กœ ์—†์Œ ๋˜๋Š” `/`)์œผ๋กœ ๋ถ™์œผ๋ฉด N-1 ์ด ๊ทธ๋Œ€๋กœ ์ ์šฉ๋˜์–ด ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ชป ๋ฐ›์Šต๋‹ˆ๋‹ค. **๊ฐ™์€ ํฌํŠธ, ๋‹ค๋ฅธ ๊ฒฝ๋กœ, ๋‹ค๋ฅธ ๊ฒฐ๊ณผ** โ€” ์ด ํ•จ์ •์€ ๋ฐ˜๋“œ์‹œ ๋ฌธ์„œํ™”๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ถ€์ˆ˜ ํšจ๊ณผ๋กœ ๋…ธ์ถœ ๋ชจ๋ธ ์„œ์ˆ ๋„ ์ •์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค: 8080 ์€ "Plane B ์ „์šฉ"์ด ์•„๋‹ˆ๋ผ **MQTT ํ”„๋กœํ† ์ฝœ ํ‘œ๋ฉด์„ ํ•จ๊ป˜ ๋…ธ์ถœ**ํ•ฉ๋‹ˆ๋‹ค(์ธ์ฆ์€ ๊ณ„์ • ์„ค์ •์ด ๋™์ผํ•˜๊ฒŒ ๊ฐ•์ œ). --- ## B. Rev.1 โ†’ Rev.2 ๋ณ€๊ฒฝ ์š”์•ฝ | # | ๋ณ€๊ฒฝ | ์ถœ์ฒ˜ | |---|---|---| | C-1 | `docker/nats.conf` `websocket {}` ๋ธ”๋ก์— **์›์  ์ •์ฑ… ์‚ฌ์‹ค ์ฃผ์„ + `allowed_origins` ๊ฐ•ํ™” ํ…œํ”Œ๋ฆฟ(์ฃผ์„)** ์ถ”๊ฐ€. `same_origin: false` ๋Š” **ํ™œ์„ฑ ๋ผ์ธ์œผ๋กœ ๋„ฃ์ง€ ์•Š์Œ** | ์ฑŒ๋ฆฐ์ง€ ยง2.3-1 ๋ณ€ํ™˜ ์ˆ˜์šฉ | | C-2 | `docker/nats.conf` `websocket {}` ์— **`/mqtt` ๊ฒฝ๋กœ = MQTT-over-WS** ์‚ฌ์‹ค ์ฃผ์„ ์ถ”๊ฐ€ | N-7 | | C-3 | `docker/README.md` ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์— **WS 403(์ •ํ™•ํ•œ ๋ฐœ๋™ ์กฐ๊ฑด)** ยท **Mixed Content** ยท **`/mqtt` vs `/` ๊ฒฝ๋กœ ์ฐจ์ด** 3ํ–‰ ์ถ”๊ฐ€ | ์ฑŒ๋ฆฐ์ง€ ยง2.3-2 + N-7 | | C-4 | `docker/README.md` 6์ ˆ(ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ)์— **๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ ์ ‘์† ๋ ˆ์‹œํ”ผ** ์‹ ์„ค | N-7 | | C-5 | ์‹ ๊ทœ ๊ฐ€๋“œ **D-30**(WebSocket ์›์  ์ •์ฑ…์ด ๊ธฐ๋™ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ์ธ์ง€) ์ถ”๊ฐ€ โ†’ 305 โ†’ **306** | A-3 | | C-6 | ๋ฌธ์„œ ๋™๊ธฐํ™” ์ž‘์—…์— **T-5** ์‹ ์„ค: `PRIVATE_SERVER.md` ยง5.1/ยง5.2 ์˜ '๋‘ ์†Œ๋น„ ํ‰๋ฉด' ์„œ์ˆ ์— ์„ธ ๋ฒˆ์งธ ๊ฒฝ๋กœ(MQTT-over-WS) ๋ฐ˜์˜ | N-7 | | C-7 | ์‹ค์ธก ์›์žฅ์— **M-19 ~ M-24** ์ถ”๊ฐ€ | A-2 / A-3 / N-7 | | C-8 | ์—ด๋ฆฐ ์งˆ๋ฌธ์—์„œ 'Rev.2 ์ด์›” ์งˆ๋ฌธ' **์‚ญ์ œ(ํ•ด์†Œ๋จ)**, ๋Œ€์‹  Q-5(๋Œ€์‹œ๋ณด๋“œ ํ”„๋กœํ† ์ฝœ ์„ ํƒ ๊ถŒ๊ณ ) ๋กœ ๋Œ€์ฒด | N-7 | Rev.1 ์˜ ยง2 ์„ค๊ณ„ ๊ฒฐ์ • D-1 ~ D-8, ยง3.2 compose, ยง3.3 `.env.example`, ยง4 ๊ฐ€๋“œ D-22 ~ D-29, ยง5 T-1 ~ T-4, ยง7 ๋ฐœ๊ฒฌ N-2 ~ N-6 ์€ **๋ฆฌ๋ทฐ์—์„œ ์ „๋ถ€ ์Šน์ธ**๋˜์—ˆ์œผ๋ฉฐ ๋ณ€๊ฒฝ ์—†์ด ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. --- ## 0. ์š”์•ฝ โ€” ์ด ๊ณ„ํš์ด ๋ฌด์—‡์„ ํ™•์ •ํ•˜๋Š”๊ฐ€ `PRIVATE_SERVER.md` ยง9 ๋Š” ์ง€๊ธˆ๊นŒ์ง€ **๋ฌธ์„œ ์•ˆ์˜ ์ฝ”๋“œ ํŽœ์Šค**๋กœ๋งŒ ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค. ํŽœ์Šค๋Š” ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ ๋Œ€์ƒ์ด์ง€ ๋ฐฐํฌ ์ž์‚ฐ์ด ์•„๋‹ˆ๋ฏ€๋กœ, 1. ์„œ๋ฒ„์— ์‹ค์ œ๋กœ ์˜ฌ๋ผ๊ฐ„ ์„ค์ •์ด ๋ฌธ์„œ์™€ ๊ฐˆ๋ผ์ ธ๋„ ์•„๋ฌด๋„ ์•Œ ์ˆ˜ ์—†๊ณ , 2. `test_deploy_freshness.py` ์˜ D-15 ~ D-19 ๊ฐ€๋“œ๋Š” **๋ฌธ์„œ๋งŒ** ๊ฒ€์‚ฌํ•˜๋ฏ€๋กœ ์‹ค์ œ ๋ฐฐํฌ๋ฌผ์˜ ํšŒ๊ท€๋ฅผ ์žก์ง€ ๋ชปํ•˜๋ฉฐ, 3. ์‹œํฌ๋ฆฟ์„ ์–ด๋””์— ๋‘๋Š”์ง€๊ฐ€ ๊ทœ์•ฝ์ด ์•„๋‹ˆ๋ผ ๊ด€์Šต์œผ๋กœ ๋‚จ์Šต๋‹ˆ๋‹ค. ๋ณธ ๊ณ„ํš์€ ยง9 ์˜ ํŽœ์Šค๋ฅผ `docker/` ํ•˜์œ„์˜ **์ •๋ณธ(canonical) ํŒŒ์ผ**๋กœ ์Šน๊ฒฉ์‹œํ‚ค๊ณ , ๋ฌธ์„œโ†”ํŒŒ์ผ ๋“œ๋ฆฌํ”„ํŠธ๋ฅผ 9์ข…์˜ ์‹ ๊ทœ ๊ฐ€๋“œ(D-22 ~ D-30)๋กœ ๋ด‰์ธํ•ฉ๋‹ˆ๋‹ค. > [!IMPORTANT] > ํ˜„์žฌ ์ €์žฅ์†Œ์—๋Š” **0 ๋ฐ”์ดํŠธ์งœ๋ฆฌ `docker/docker-compose.yaml`** ์ด untracked ์ƒํƒœ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค(`git status` = `?? docker/`). ์‹ ๊ทœ ๊ฐ€๋“œ D-22 ๋Š” ์ด ์ƒํƒœ์—์„œ **์ฆ‰์‹œ FAIL** ํ•˜๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค โ€” ์ฆ‰ ๊ฐ€๋“œ๊ฐ€ ๊ณตํ—ˆํ•˜๊ฒŒ ํ†ต๊ณผํ•˜์ง€ ์•Š์Œ์ด ์ฐฉ์ˆ˜ ์‹œ์ ์— ์ž๋™์œผ๋กœ ์ฆ๋ช…๋ฉ๋‹ˆ๋‹ค. --- ## 1. ์‹ค์ธก ๊ธฐ๋ฐ˜ (Measurement Ledger) | # | ๊ฒ€์ฆ ํ•ญ๋ชฉ | ๋ฐฉ๋ฒ• | ์‹ค์ธก ๊ฒฐ๊ณผ | |---|---|---|---| | M-1 | ํ…Œ์ŠคํŠธ ๋ฒ ์ด์Šค๋ผ์ธ | `pytest tests/ -q --collect-only` | **297 collected** | | M-2 | `nats:2.12-alpine` ํƒœ๊ทธ ์‹ค์žฌ | Docker Hub API `library/nats/tags?name=2.12` | ์กด์žฌ. ์ตœ์‹  ํŒจ์น˜ **2.12.15** (2026-08-12) | | M-3 | alpine ์ด๋ฏธ์ง€ ๋ฒ ์ด์Šค | `nats-docker/main/2.12.x/alpine3.22/Dockerfile` | `FROM alpine:3.22` โ†’ **busybox `wget` ๋‚ด์žฅ** | | M-4 | ์—”ํŠธ๋ฆฌํฌ์ธํŠธ ์ธ์ž ์ฒ˜๋ฆฌ | ๋™ ๋””๋ ‰ํ„ฐ๋ฆฌ `docker-entrypoint.sh` | `[ "${1#-}" != "$1" ] && set -- nats-server "$@"` โ†’ `command: ["-c", โ€ฆ]` **๋™์ž‘** | | M-5 | ์ด๋ฏธ์ง€ HEALTHCHECK ์œ ๋ฌด | ๋™ Dockerfile | **์—†์Œ** โ†’ compose ๊ฐ€ ๋ฐ˜๋“œ์‹œ ์ •์˜ | | M-6 | ๋ฏธํ•ด๊ฒฐ `$VAR` ๋™์ž‘ | `conf/parse.go:390` | ํŒŒ์‹ฑ ์—๋Ÿฌ = **fail-closed** | | M-7 | ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’ ์žฌํŒŒ์‹ฑ | `conf/parse.go` `lookupVariable` โ†’ `parseEnv(...)` | ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์ด **NATS ๋ ‰์„œ๋กœ ์žฌํŒŒ์‹ฑ** (N-5) | | M-8 | ๋น„์ธ์šฉ ๋ฌธ์ž์—ด ์ข…๊ฒฐ์ž | `conf/lex.go:958-960` | NL, EOF, `;`, `,`, `]`, `}`, ๊ณต๋ฐฑ. `=` `+` `/` ๋Š” **๋น„์ข…๊ฒฐ์ž** | | M-9 | `mqtt {}` ์œ ํšจ ํ‚ค | `server/opts.go` `parseMQTT` | `port`/`ack_wait`/`max_ack_pending` **์ „๋ถ€ ์œ ํšจ** | | M-10 | `max_ack_pending` ์ƒํ•œ | `opts.go:5673-5679` | `[0..65535]`. **1024 ์œ ํšจ** | | M-11 | `jetstream {}` ์œ ํšจ ํ‚ค | `opts.go` `parseJetStream` | `store_dir`, `max_file`, `max_mem` **์ „๋ถ€ ์œ ํšจ** | | M-12 | ํฌ๊ธฐ ์ ‘๋ฏธ์‚ฌ ๋Œ€์†Œ๋ฌธ์ž | `opts.go:2507` `suffixMap` | `{"K","M","G","T"}` **๋Œ€๋ฌธ์ž ์ „์šฉ** (N-6) | | M-13 | `websocket {}` ์œ ํšจ ํ‚ค | `opts.go` `parseWebsocket` | `port`, `no_tls`, `same_origin`, `allowed_origins` ๋“ฑ ์œ ํšจ | | M-14 | `.gitignore` ๊ฑฐ๋™ | `git check-ignore -v` | `docker/.env` **ignored**(`:21`), `docker/.env.example` **tracked**(`:23`) | | M-15 | ๋น„์ธ์šฉ `${VAR:?msg}` YAML | PyYAML 6.0.3 ํŒŒ์‹ฑ | ํ‰๋ฌธ ์Šค์นผ๋ผ โ†’ **๋ฌธ์„œ ์›๋ฌธ ๊ทธ๋Œ€๋กœ ํŒŒ์ผํ™” ๊ฐ€๋Šฅ** | | M-16 | PyYAML ์„ ์–ธ ์—ฌ๋ถ€ | `requirements.txt`=1ํ–‰, `tests/test_sanity.py:3`=`import yaml` | **๋ฏธ์„ ์–ธ ํ•˜๋“œ ์˜์กด** (N-2) | | M-17 | ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ๋ฐฐํฌ ๋ฒ”์œ„ | `deploy/install.sh:365-374` | `docker/`ยท`PRIVATE_SERVER.md` **๋ฏธ๋ฐฐํฌ** | | M-18 | `DEFAULT_TOPIC_ROOT` | `mqtt_common.py:119` | `"python/mqtt/jobs"` | | **M-19** | **`SameOrigin` ๊ธฐ๋ณธ๊ฐ’** | `opts.go` ์ „์—ญ grep `SameOrigin = true` / `SameOrigin:` | **0๊ฑด** โ†’ Go ์ œ๋กœ๊ฐ’ `false`. ์ฃผ์„(`opts.go:676-677`)๋„ "empty and SameOrigin is false โ†’ any origin is accepted" ๋ช…์‹œ | | **M-20** | **`checkOrigin` ๋‹จ๋ฝ ์กฐ๊ฑด** | `websocket.go:1034-1041` | `!checkSame && listEmpty` โ†’ **์ฆ‰์‹œ `nil`**. `Origin` ํ—ค๋”๋ฅผ ์ฝ์ง€ ์•Š์Œ | | **M-21** | **403 ๋ฐœ๋™ ์ง€์ ** | `websocket.go:868-870` | `StatusForbidden "origin not allowed"` โ€” `checkOrigin` ์ด ์—๋Ÿฌ์ผ ๋•Œ๋งŒ | | **M-22** | **`allowed_origins: ["*"]`** | `websocket.go:1136-1150` `validateWebsocketOptions` | scheme ์ด http/https ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉฐ **์˜ต์…˜ ๊ฒ€์ฆ ์‹คํŒจ โ†’ ๊ธฐ๋™ ์‹คํŒจ** | | **M-23** | **`no_tls` ํ•„์š”์„ฑ** | `websocket.go:1132-1134` | `TLSConfig == nil && !NoTLS` โ†’ `websocket requires TLS configuration`. ํ˜„ ์„ค์ •์˜ `no_tls: true` ๋Š” **ํ•„์ˆ˜** | | **M-24** | **MQTT-over-WebSocket** | `mqtt.go:193` `mqttWSPath="/mqtt"`; `websocket.go:824,832,1334-1335` | 8080 ์˜ `/mqtt` ๊ฒฝ๋กœ๊ฐ€ `createMQTTClient` ๋กœ ๋ถ„๊ธฐ. ๊ฒŒ์ดํŠธ๋Š” `MQTT.Port != 0` ๋ฟ โ†’ **์ด๋ฏธ ํ™œ์„ฑ** (N-7) | --- ## 2. ์„ค๊ณ„ ๊ฒฐ์ • (Design Decisions) ### D-1. ํŒŒ์ผ๋ช…์€ `docker/docker-compose.yaml` + ๋ฌธ์„œ ์ฐธ์กฐ ์ •์ • ๋ธŒ๋ฆฌํ”„๋Š” `.yaml`, `PRIVATE_SERVER.md` ยง9.2 ์ œ๋ชฉ์€ `.yml` ์„ ์”๋‹ˆ๋‹ค. Compose ๋Š” ๋‘˜ ๋‹ค ์ธ์‹ํ•˜๋ฏ€๋กœ ๊ธฐ๋Šฅ ์ฐจ๋Š” ์—†์Šต๋‹ˆ๋‹ค. **๋ธŒ๋ฆฌํ”„๋ฅผ ์ •๋ณธ์œผ๋กœ ์ฑ„ํƒ**ํ•˜๊ณ  ๋ฌธ์„œ ์ฐธ์กฐ๋ฅผ ์ •์ •ํ•ฉ๋‹ˆ๋‹ค(T-3). ๋‘ ๊ณณ์ด ๋‹ค๋ฅธ ์ฑ„๋กœ ๋‚จ์œผ๋ฉด D-23 docโ†”file ๊ฐ€๋“œ๊ฐ€ ๋ฌด์—‡์„ ๋น„๊ตํ•˜๋Š”์ง€ ๋ชจํ˜ธํ•ด์ง‘๋‹ˆ๋‹ค. ### D-2. ๊ณ„์ • ์‚ฌ์šฉ์ž๋ช…์€ `mam_agent` / `mam_observer` ์œ ์ง€ ๋ธŒ๋ฆฌํ”„์˜ `MAM with mam/observer` ์ถ•์•ฝ์€ **๊ณ„์ • ๊ตฌ์กฐ**๋ฅผ ๊ฐ€๋ฆฌํ‚จ ๊ฒƒ์œผ๋กœ ์ฝ์Šต๋‹ˆ๋‹ค. ์‹๋ณ„์ž๋ฅผ ๋ฐ”๊พธ๋ฉด `PRIVATE_SERVER.md` ยง6 (`MQTT_USERNAME=mam_agent`) ๊ณผ ยง9.4 R-9 ํ”Œ๋ ˆ์ด๋ถ์ด ์กฐ์šฉํžˆ ๊นจ์ง‘๋‹ˆ๋‹ค. ### D-3. `version: '3.8'` ์ œ๊ฑฐ Compose V2 ๋Š” ๋งค `up` ๋งˆ๋‹ค `the attribute 'version' is obsolete` ๊ฒฝ๊ณ ๋ฅผ ๋ƒ…๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ์ž์‚ฐ์ด ์ƒ์‹œ ๊ฒฝ๊ณ ๋ฅผ ๋ฟœ์œผ๋ฉด ์šด์˜์ž๊ฐ€ ๊ฒฝ๊ณ ๋ฅผ ๋ฌด์‹œํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์ž…๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ ์˜ํ–ฅ 0. ### D-4. `.env.example` ์˜ ์‹œํฌ๋ฆฟ์€ **๋นˆ ๊ฐ’**์œผ๋กœ ์ถœํ•˜ (fail-closed ์˜ ํ•ต์‹ฌ) | ๋ฐฉ์‹ | `cp .env.example .env` ํ›„ ๊ฒฐ๊ณผ | |---|---| | `MAM_BROKER_PASS=changeme` | ๋ธŒ๋กœ์ปค **์ •์ƒ ๊ธฐ๋™**. ์ „ ์„ธ๊ณ„๊ฐ€ ์•„๋Š” ์•”ํ˜ธ๋กœ ํ”„๋กœ๋•์…˜ ๊ฐ€๋™ = **fail-open** | | `MAM_BROKER_PASS=` (๋นˆ ๊ฐ’) โœ… | `${VAR:?โ€ฆ}` ๋Š” ์ฝœ๋ก  ํ˜•ํƒœ๋ผ **๋นˆ ๊ฐ’์—์„œ๋„ ์ค‘๋‹จ** โ†’ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ ์ „ ๋น„์˜์  ์ข…๋ฃŒ | ๋ฐฉ์–ด๋Š” ์ด์ค‘์ž…๋‹ˆ๋‹ค: Compose ๋ณด๊ฐ„ ์‹คํŒจ(1์ฐจ) โ†’ ๊ทธ๋ž˜๋„ ๋–ด๋‹ค๋ฉด `nats.conf` ๋ฏธํ•ด๊ฒฐ `$VAR` ํŒŒ์‹ฑ ์—๋Ÿฌ(M-6, 2์ฐจ). ### D-5. `nats.conf` ๋Š” ์‹œํฌ๋ฆฟ์„ ํ•œ ๊ธ€์ž๋„ ๋‹ด์ง€ ์•Š๋Š”๋‹ค ๋ชจ๋“  `password:` ๋Š” `$VAR` ์ฐธ์กฐ. ๋”ฐ๋ผ์„œ `docker/nats.conf` ๋Š” ์ปค๋ฐ‹ ๊ฐ€๋Šฅํ•˜๊ณ , ์‹œํฌ๋ฆฟ์€ gitignore ๋œ `docker/.env` ์—๋งŒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค(M-14). D-25(e) ๊ฐ€ ๋ด‰์ธํ•ฉ๋‹ˆ๋‹ค. ### D-6. ์•”ํ˜ธ ์ƒ์„ฑ๊ธฐ๋Š” `openssl rand -base64 32` ๋กœ ๊ณ ์ •ํ•˜๊ณ  ์ด์œ ๋ฅผ ๋ฌธ์„œํ™” ํŒŒ์„œ ์ œ์•ฝ์ž…๋‹ˆ๋‹ค(M-7/M-8). ์•”ํ˜ธ์— `๊ณต๋ฐฑ ; , ] } # ' " $` ๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์„ค์ •์ด ๊นจ์ง€๊ฑฐ๋‚˜ ์กฐ์šฉํžˆ ๋‹ค๋ฅธ ๊ฐ’์ด ๋ฉ๋‹ˆ๋‹ค. base64 ์•ŒํŒŒ๋ฒณ(`A-Za-z0-9+/=`)์€ ์ด ์ง‘ํ•ฉ๊ณผ ๊ต์ง‘ํ•ฉ์ด 0์ž…๋‹ˆ๋‹ค. ### D-7. healthcheck ๋Š” `wget` ์œ ์ง€ โ€” ์ด๋ฏธ์ง€ ๊ณ„์—ด๊ณผ **์ปคํ”Œ๋ง**ํ•ด์„œ ๋ด‰์ธ `wget` ์€ alpine ๋ฒ ์ด์Šค์—๋งŒ ์žˆ์Šต๋‹ˆ๋‹ค(M-3). D-28 ์€ healthcheck ์กด์žฌ์™€ ์ด๋ฏธ์ง€ alpine ์—ฌ๋ถ€๋ฅผ **ํ•œ ํ…Œ์ŠคํŠธ ์•ˆ์—์„œ** ๋‹จ์–ธํ•ฉ๋‹ˆ๋‹ค. ๋ถ„๋ฆฌํ•˜๋ฉด ์ด๋ฏธ์ง€๋งŒ ๋ฐ”๊พธ๋Š” ์ปค๋ฐ‹์ด ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. ### D-8. `docker/` ๋Š” ํ•˜์œ„ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋กœ ๋ฐฐํฌํ•˜์ง€ ์•Š๋Š”๋‹ค `install.sh` ๋Š” `PRIVATE_SERVER.md` ์กฐ์ฐจ ๋ฐฐํฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค(M-17). `docker/` ๋Š” **์ด ์ €์žฅ์†Œ๊ฐ€ ์šด์˜ํ•˜๋Š” ์„œ๋ฒ„**์˜ ์ž์‚ฐ์ด๋ฏ€๋กœ ๋™์ผํ•˜๊ฒŒ ์ €์žฅ์†Œ ์ „์šฉ์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค. `install.sh` ๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š” ๊ฒƒ์ด ๋ช…์‹œ์  ๊ฒฐ์ •์ž…๋‹ˆ๋‹ค(Q-3). ### D-9 (์‹ ๊ทœ). WebSocket ์›์  ์ •์ฑ…์€ **ํ™œ์„ฑ ์„ค์ •์ด ์•„๋‹ˆ๋ผ ์ฃผ์„์œผ๋กœ** ๋‹ค๋ฃฌ๋‹ค ์„ธ ๊ฐ€์ง€ ์„ ํƒ์ง€๋ฅผ ๊ฒ€ํ† ํ–ˆ์Šต๋‹ˆ๋‹ค. | ์„ ํƒ | ๊ฒฐ๊ณผ | |---|---| | `same_origin: false` ํ™œ์„ฑ ์ถ”๊ฐ€ (์ฑŒ๋ฆฐ์ง€ ์ฒ˜๋ฐฉ 1) | ๋™์ž‘์ƒ **no-op**(M-19). ๊ทธ๋Ÿฌ๋‚˜ ์„ค์ • ํŒŒ์ผ์ด "์ด๊ฑธ ๊บผ์•ผ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ถ™๋Š”๋‹ค"๋Š” **๊ฑฐ์ง“ ์„œ์‚ฌ**๋ฅผ ํ›„์ž„์ž์—๊ฒŒ ์ „๋‹ฌ | | `allowed_origins: ["*"]` (์ฑŒ๋ฆฐ์ง€ ์ฒ˜๋ฐฉ 2) | ๐Ÿ”ด **๊ธฐ๋™ ์‹คํŒจ**(M-22) | | **์ฃผ์„์œผ๋กœ ๊ธฐ๋ณธ ๋™์ž‘์„ ๋ช…๋ฌธํ™” + ๊ฐ•ํ™” ํ…œํ”Œ๋ฆฟ์„ ์ฃผ์„ ์ œ๊ณต** โœ… | ๋™์ž‘ ๋ถˆ๋ณ€, ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์‹ค์ œ๋กœ ๊ฒช์€ ์ •๋ณด ๊ณต๋ฐฑ์„ ๋ฉ”์›€, 8080 ์„ tailnet ๋ฐ–์œผ๋กœ ๋‚ผ ๋•Œ ํ•„์š”ํ•œ **๊ฐ•ํ™”** ๊ฒฝ๋กœ๋ฅผ ์ฆ‰์‹œ ์ œ๊ณต | ์„ธ ๋ฒˆ์งธ๋ฅผ ์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ๋ทฐ์–ด์˜ ๊ด€์ฐฐ(์„ค์ •์ด ์›์  ์ •์ฑ…์— ์นจ๋ฌตํ•œ๋‹ค)์€ ํƒ€๋‹นํ–ˆ๊ณ , ์ฒ˜๋ฐฉ(๋„๊ธฐ)๋งŒ ๋ฐฉํ–ฅ์ด ๋ฐ˜๋Œ€์˜€์Šต๋‹ˆ๋‹ค. ์นจ๋ฌต์„ ๋ฉ”์šฐ๋˜ ์‚ฌ์‹ค๋Œ€๋กœ ๋ฉ”์›๋‹ˆ๋‹ค. --- ## 3. ์‚ฐ์ถœ๋ฌผ ๋ช…์„ธ (Creator ๊ตฌํ˜„ ์‚ฌ์–‘) ### 3.1 `docker/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 ``` **๊ณ„์•ฝ ๊ฐ’** (๊ฐ€๋“œ ๊ฒ€์‚ฌ ๋Œ€์ƒ): `store_dir` = `/data` ยท `mqtt.port` = `1883` ยท `websocket.port` = `8080` ยท `http_port` = `8222` ยท ๊ณ„์ • `MAM`/`HOME`/`SYS` ยท ์‚ฌ์šฉ์ž `mam_agent`/`mam_observer`/`home`/`sys` ยท subject ์ ‘๋‘ `python.mqtt.jobs` ยท ๋ชจ๋“  `password:` ๋Š” `$` ์‹œ์ž‘ ยท ํ™œ์„ฑ `allowed_origins` ์— `"*"` ์—†์Œ(D-30). > [!NOTE] > `accounts {}` ๋ฅผ ์ •์˜ํ•˜๊ณ  `no_auth_user` ๋ฅผ ๋‘์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ **์ต๋ช… ์ ‘์†์€ MQTTยทNATSยทWebSocket ์ „ ๊ฒฝ๋กœ์—์„œ ๊ฑฐ๋ถ€**๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ยง9.4 R-3(๋…ธ์ถœ ๋ฉด์  0)๊ณผ ๋…๋ฆฝ๋œ ๋‘ ๋ฒˆ์งธ ๋ฐฉ์–ด์„ ์ด๋ฉฐ, N-7 ๋กœ ๋“œ๋Ÿฌ๋‚œ `/mqtt` ํ‘œ๋ฉด์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ### 3.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: ``` **์ฃผ์˜ ์‚ฌํ•ญ** - `8222` ๋งŒ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๊ฐ€ ํ•˜๋“œ์ฝ”๋”ฉ์ž…๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€ 3๊ฐœ๋Š” `${*_BIND:-127.0.0.1}` ๋กœ tailnet IP ์ฃผ์ž…์„ ํ—ˆ์šฉํ•˜๋˜ ๊ธฐ๋ณธ๊ฐ’์ด loopback ์ž…๋‹ˆ๋‹ค. D-24 ๊ฐ€ ์ด ๋น„๋Œ€์นญ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. - `./nats.conf` ๋Š” compose ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ธฐ์ค€ ์ƒ๋Œ€๊ฒฝ๋กœ์ด๋ฏ€๋กœ `docker/` ์•ˆ์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - ๋น„์ธ์šฉ `${VAR:?msg}` ๋Š” PyYAML ๋กœ ํ‰๋ฌธ ์Šค์นผ๋ผ ํŒŒ์‹ฑ๋จ์„ ์‹ค์ธก(M-15). ์ธ์šฉ๋ถ€ํ˜ธ ์ถ”๊ฐ€ ๋ถˆํ•„์š”. ### 3.3 `docker/.env.example` ```bash # ============================================================================== # docker/.env.example โ€” ์›๊ฒฉ ๋ธŒ๋กœ์ปค ์„œ๋ฒ„ ์ธก ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ # # ์ด ํŒŒ์ผ์€ git ์— ์ปค๋ฐ‹๋ฉ๋‹ˆ๋‹ค (.gitignore:23 ์˜ `!.env.example`). # ๋ณต์‚ฌ๋ณธ docker/.env ๋Š” git ์—์„œ ์ œ์™ธ๋ฉ๋‹ˆ๋‹ค (.gitignore:21 ์˜ `.env`). # # cd docker && cp .env.example .env && chmod 600 .env # # โš  ์•„๋ž˜ ์‹œํฌ๋ฆฟ 4์ข…์€ ์˜๋„์ ์œผ๋กœ **๋นˆ ๊ฐ’**์ž…๋‹ˆ๋‹ค. ์ฑ„์šฐ์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•˜๋ฉด # `docker compose up` ์ด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค (fail-closed). # ํ”Œ๋ ˆ์ด์Šคํ™€๋” ๋ฌธ์ž์—ด์„ ๋„ฃ์ง€ ๋งˆ์‹ญ์‹œ์˜ค โ€” '์•Œ๋ ค์ง„ ์•”ํ˜ธ๋กœ ๊ฐ€๋™'์ด ์ตœ์•…์ž…๋‹ˆ๋‹ค. # ============================================================================== # โ”€โ”€ ์‹œํฌ๋ฆฟ (ํ•„์ˆ˜) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ์ƒ์„ฑ: openssl rand -base64 32 # # โš  ๋ฐ˜๋“œ์‹œ ์œ„ ๋ช…๋ น์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. NATS ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์„ ์„ค์ • ๋ ‰์„œ๋กœ ์žฌํŒŒ์‹ฑํ•˜๋ฏ€๋กœ # ์•”ํ˜ธ์— [๊ณต๋ฐฑ ; , ] } # ' " $] ๊ฐ€ ํฌํ•จ๋˜๋ฉด ์„ค์ •์ด ๊นจ์ง€๊ฑฐ๋‚˜ ๋‹ค๋ฅด๊ฒŒ ํ•ด์„๋ฉ๋‹ˆ๋‹ค. # base64 ์•ŒํŒŒ๋ฒณ(A-Za-z0-9+/=)์€ ์ด ๋ฌธ์ž๋“ค๊ณผ ๊ต์ง‘ํ•ฉ์ด ์—†์–ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. # MAM ์—์ด์ „ํŠธ ๋ฐœํ–‰/๊ตฌ๋… ๊ณ„์ • (.mam.env ์˜ MQTT_PASSWORD ์™€ ๋™์ผ ๊ฐ’) MAM_BROKER_PASS= # ๊ด€์ธก ์ „์šฉ ๊ณ„์ • (๊ตฌ๋… allow: python.mqtt.jobs.>, ๋ฐœํ–‰ ์ „๋ฉด deny) MAM_OBSERVER_PASS= # MAM ๊ณผ ๋ฌด๊ด€ํ•œ ํ™ˆ๋žฉ ์„œ๋น„์Šค์šฉ ๋ณ„๋„ ๊ณ„์ • HOME_BROKER_PASS= # ์‹œ์Šคํ…œ ๊ณ„์ • ($SYS). ์šด์˜ ์ด๋ฒคํŠธ/๋ชจ๋‹ˆํ„ฐ๋ง ์ „์šฉ SYS_BROKER_PASS= # โ”€โ”€ ๋ฆฌ์Šค๋„ˆ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ (์„ ํƒ โ€” ๋ฏธ์„ค์ • ์‹œ ์ „๋ถ€ 127.0.0.1) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # โš  Docker ์˜ published ํฌํŠธ๋Š” UFW ๋ฅผ ์šฐํšŒํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ๊ฐ’์ด ์‹ค์งˆ์ ์ธ ๋…ธ์ถœ ํ†ต์ œ์ž…๋‹ˆ๋‹ค. # ๋ชจ๋ธ T(Tailscale) ๊ถŒ์žฅ ์„ค์ •: MQTT_BIND=$(tailscale ip -4) # # MQTT 1883 ๋ฆฌ์Šค๋„ˆ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ # MQTT_BIND=127.0.0.1 # # NATS 4222 ๋„ค์ดํ‹ฐ๋ธŒ ๋ฆฌ์Šค๋„ˆ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ # NATS_BIND=127.0.0.1 # # WebSocket 8080 ๋ฐ”์ธ๋“œ ์ฃผ์†Œ. # ์ฐธ๊ณ : ์ด ํฌํŠธ๋Š” NATS WebSocket ๊ณผ MQTT-over-WebSocket(/mqtt)์„ ํ•จ๊ป˜ ์„œ๋น™ํ•ฉ๋‹ˆ๋‹ค. # WS_BIND=127.0.0.1 # # HTTP ๋ชจ๋‹ˆํ„ฐ 8222 ๋Š” ๋ฌด์ธ์ฆ์ด๋ฏ€๋กœ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๋ฅผ ๋ณ€์ˆ˜ํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (loopback ๊ณ ์ •). ``` **์„ค๊ณ„ ํฌ์ธํŠธ**: ์‹œํฌ๋ฆฟ 4์ข…์€ **์ฃผ์„ ํ•ด์ œ ์—†์ด ๋นˆ ๊ฐ’์œผ๋กœ ํ™œ์„ฑ**, ๋ฐ”์ธ๋“œ ์ฃผ์†Œ 3์ข…์€ **์ฃผ์„ ์ฒ˜๋ฆฌ**. ์ด ๋น„๋Œ€์นญ์ด "์‹œํฌ๋ฆฟ์€ ๋ฐ˜๋“œ์‹œ ์ฑ„์›Œ์•ผ ํ•˜๊ณ , ๋ฐ”์ธ๋“œ๋Š” ์•ˆ ์ฑ„์›Œ๋„ ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’"์ด๋ผ๋Š” ์˜๋„๋ฅผ ํŒŒ์ผ ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. ### 3.4 `docker/README.md` | ์ ˆ | ๋‚ด์šฉ | ์ •๋ณธ | |---|---|---| | 1. ๋ฌด์—‡์ธ๊ฐ€ | ์ด ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ MAM ๊ด€์ธก ๋ฐฑํ”Œ๋ ˆ์ธ ๋ธŒ๋กœ์ปค์˜ ์ •๋ณธ์ž„. `PRIVATE_SERVER.md` ยง9 ๋งํฌ | ยง9 | | 2. ์‚ฌ์ „ ์š”๊ตฌ | Docker Engine + Compose V2, (๋ชจ๋ธ T) Tailscale, ๊ฐœ๋ฐฉ ํฌํŠธ ์—†์Œ | ยง9.3 | | 3. 5๋ถ„ ๋ฐฐํฌ | `docker/` ๋ณต์‚ฌ โ†’ `cp .env.example .env` โ†’ `openssl rand -base64 32` ร—4 โ†’ `chmod 600 .env` โ†’ `docker compose up -d` โ†’ `docker compose ps` **healthy** | ยง9.2 | | 4. ๋„คํŠธ์›Œํฌ ์ž ๊ธˆ | UFW ๊ทœ์น™ ์ „๋ฌธ + **published ํฌํŠธ๊ฐ€ UFW ๋ฅผ ์šฐํšŒ**ํ•œ๋‹ค๋Š” ๊ฒฝ๊ณ ๋ฅผ ์ตœ์ƒ๋‹จ์— | ยง9.3 | | 5. ๋…ธ์ถœ ๊ฒ€์ฆ | ์™ธ๋ถ€ ๋ง์—์„œ `nmap -Pn -p 1883,4222,8222,8080 <๊ณต๊ฐœIP>` โ†’ ์ „๋ถ€ closed/filtered (R-3) | ยง9.4 | | 6. ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ | (a) MAM ์ €์žฅ์†Œ `.mam.env` ๊ธฐ์ž… (b) **๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ ๋ ˆ์‹œํ”ผ** (์•„๋ž˜) | ยง6 + N-7 | | 7. ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ | R-1 ~ R-10 ํ‘œ + ์ง€์—ฐ ์ธก์ • ์Šค๋‹ˆํŽซ ๋งํฌ | ยง9.4 | | 8. ์šด์˜ | ๋กœ๊ทธ ๋กœํ…Œ์ด์…˜(๋‚ด์žฅ), JetStream ๋ณผ๋ฅจ ๋ฐฑ์—…, `docker compose pull && up -d`, `/varz`ยท`/jsz` ๋Š” SSH ํ„ฐ๋„ ๊ฒฝ์œ  | ยง9 P6 | | 9. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… | ์•„๋ž˜ ํ‘œ | ์‹ ๊ทœ | **6์ ˆ (b) ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ ์ ‘์† ๋ ˆ์‹œํ”ผ** โ€” N-7 ๋ฐ˜์˜, ํ•„์ˆ˜ ์‹ ์„ค: ```js // MQTT.js โ€” retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊นŒ์ง€ ๋ฐ›๋Š” ๊ฒฝ๋กœ (๊ถŒ์žฅ) const client = mqtt.connect("ws://mam-hub.tailXXXX.ts.net:8080/mqtt", { username: "mam_observer", password: "", protocolVersion: 4, // MQTT 3.1.1 }); client.subscribe("python/mqtt/jobs/+/events"); // ์ด ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„์—์„œ ์™„์ „ํ•œ MQTT ํด๋ผ์ด์–ธํŠธ๋กœ ์ทจ๊ธ‰๋˜๋ฏ€๋กœ // ์žก์ด ๋๋‚œ ๋’ค์— ์ ‘์†ํ•ด๋„ retained ์ตœ์ข… ์ด๋ฒคํŠธ๋ฅผ ์ฆ‰์‹œ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค. // nats.ws โ€” ๊ฒฝ๋กœ ์—†์ด ๋ถ™์œผ๋ฉด NATS ๋„ค์ดํ‹ฐ๋ธŒ. ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆผ๋งŒ ์ˆ˜์‹ ํ•˜๋ฉฐ // ์ด๋ฏธ ๋๋‚œ ์žก์˜ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋Š” ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค (N-1). ``` **9์ ˆ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ํ‘œ(ํ•„์ˆ˜ ํ•ญ๋ชฉ)** | ์ฆ์ƒ | ์›์ธ | ์กฐ์น˜ | |---|---|---| | `required variable MAM_BROKER_PASS is missing` | `.env` ๋ฏธ์ƒ์„ฑ ๋˜๋Š” ๋นˆ ๊ฐ’ | ์˜๋„๋œ fail-closed. ์‹œํฌ๋ฆฟ์„ ์ฑ„์šธ ๊ฒƒ | | `variable reference for 'MAM_BROKER_PASS' โ€ฆ can not be found` | compose ํ†ต๊ณผํ–ˆ์œผ๋‚˜ ์ปจํ…Œ์ด๋„ˆ์— ๋ณ€์ˆ˜ ๋ฏธ์ฃผ์ž… | `environment:` ๋ธ”๋ก ๋ˆ„๋ฝ ํ™•์ธ | | ์ปจํ…Œ์ด๋„ˆ๋Š” ๋œจ๋Š”๋ฐ ๊ณ„์† `unhealthy` | ์ด๋ฏธ์ง€๋ฅผ `latest`/`scratch`/non-alpine ๋กœ ๋ณ€๊ฒฝ โ†’ `wget` ๋ถ€์žฌ | `nats:2.12-alpine` ๋กœ ๋ณต๊ท€ | | ์„ค์ •์ด ์•Œ ์ˆ˜ ์—†๋Š” ๊ฐ’์œผ๋กœ ํ•ด์„๋จ | ์•”ํ˜ธ์— ๋ ‰์„œ ์ข…๊ฒฐ์ž ํฌํ•จ | `openssl rand -base64 32` ๋กœ ์žฌ๋ฐœ๊ธ‰ | | `max_file` ์—๋Ÿฌ | `10g` ๋“ฑ ์†Œ๋ฌธ์ž ์ ‘๋ฏธ์‚ฌ | ๋Œ€๋ฌธ์ž `10G` | | ์›๊ฒฉ์—์„œ ์ ‘์† ๋ถˆ๊ฐ€ | ๋ฐ”์ธ๋“œ๊ฐ€ loopback ๊ธฐ๋ณธ๊ฐ’ | `.env` ์— `MQTT_BIND=$(tailscale ip -4)` | | `JetStream not enabled for account` | ๊ณ„์ •์— `jetstream: enabled` ๋ˆ„๋ฝ | `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"]` ์ฒ˜๋Ÿผ ์ ˆ๋Œ€ URL ๋กœ | | **๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์˜ Mixed Content ์ฐจ๋‹จ** | `https://` ํŽ˜์ด์ง€์—์„œ `ws://` ์—ฐ๊ฒฐ ์‹œ๋„ | ๋Œ€์‹œ๋ณด๋“œ๋ฅผ tailnet ๋‚ด๋ถ€ `http://` ๋กœ ์„œ๋น™ํ•˜๊ฑฐ๋‚˜, ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋กœ `wss://` ์ข…๋‹จ ์ œ๊ณต | | **๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ชป ๋ฐ›์Œ** | 8080 ์— **๊ฒฝ๋กœ ์—†์ด**(NATS ๋„ค์ดํ‹ฐ๋ธŒ) ์ ‘์†ํ•จ. retained ๋Š” MQTT ์ „์šฉ (N-1) | `ws://host:8080/**mqtt**` ๋กœ MQTT.js ์ ‘์† (N-7) | --- ## 4. ์‹ ๊ทœ ํšŒ๊ท€ ๊ฐ€๋“œ D-22 ~ D-30 `tests/test_deploy_freshness.py` ๋ง๋ฏธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด D-11 ~ D-21 ์Šคํƒ€์ผ(๋ชจ๋“ˆ ์ƒ๋‹จ ์ƒ์ˆ˜, ํŽœ์Šค ์Šค์ฝ”ํ•‘, ๊ณตํ—ˆ ํ†ต๊ณผ ๋ฐฉ์ง€ ๋‹จ์–ธ)์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ### 4.1 ๊ณตํ†ต ํ—ฌํผ ```python DOCKER_DIR = os.path.join(REPO_ROOT, "docker") COMPOSE_PATH = os.path.join(DOCKER_DIR, "docker-compose.yaml") NATS_CONF_PATH = os.path.join(DOCKER_DIR, "nats.conf") ENV_EXAMPLE_PATH = os.path.join(DOCKER_DIR, ".env.example") DOCKER_README_PATH = os.path.join(DOCKER_DIR, "README.md") SECRET_VARS = {"MAM_BROKER_PASS", "MAM_OBSERVER_PASS", "HOME_BROKER_PASS", "SYS_BROKER_PASS"} def _load_compose(): """compose ํŒŒ์ผ์„ dict ๋กœ. ํŒŒ์ผ์ด ๋น„์—ˆ๊ฑฐ๋‚˜ nats ์„œ๋น„์Šค๊ฐ€ ์—†์œผ๋ฉด ์ฆ‰์‹œ ์‹คํŒจ.""" import yaml with open(COMPOSE_PATH, encoding="utf-8") as f: doc = yaml.safe_load(f) assert isinstance(doc, dict) and doc.get("services"), ( "docker/docker-compose.yaml is empty or has no services: โ€” the docker/ " "assets were never populated") svc = doc["services"].get("nats") assert svc, "compose file defines no 'nats' service" return doc, svc def _active_conf_lines(): """nats.conf ์—์„œ ์ฃผ์„์„ ์ œ์™ธํ•œ 'ํ™œ์„ฑ' ๋ผ์ธ๋งŒ. ์ฃผ์„ ํ…œํ”Œ๋ฆฟ์„ ์˜คํƒํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•จ.""" with open(NATS_CONF_PATH, encoding="utf-8") as f: lines = [ln.split("#", 1)[0].rstrip() for ln in f] return [ln for ln in lines if ln.strip()] ``` > `_active_conf_lines()` ๋Š” D-30 ์˜ ์ •ํ™•๋„๋ฅผ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ยง3.1 ์€ `allowed_origins` ๋ฅผ **์ฃผ์„ ํ…œํ”Œ๋ฆฟ**์œผ๋กœ ์ œ๊ณตํ•˜๋ฏ€๋กœ, ์ฃผ์„์„ ๊ทธ๋Œ€๋กœ ์Šค์บ”ํ•˜๋ฉด ๊ฐ€๋“œ๊ฐ€ ์ž๊ธฐ ๋ฌธ์„œ๋ฅผ ์˜คํƒํ•ฉ๋‹ˆ๋‹ค. ### 4.2 ๊ฐ€๋“œ ๋ช…์„ธ | ID | ์ด๋ฆ„ | ๋‹จ์–ธ | ๊ณตํ—ˆ ํ†ต๊ณผ ๋ฐฉ์ง€ | ์žก์•„๋‚ด๋Š” ํšŒ๊ท€ | |---|---|---|---|---| | **D-22** | `docker_assets_exist_and_are_populated` | 4๊ฐœ ํŒŒ์ผ ์กด์žฌ + ํฌ๊ธฐ > 0, compose ๊ฐ€ `nats` ์„œ๋น„์Šค๋ฅผ ๊ฐ–๋Š” dict ๋กœ ํŒŒ์‹ฑ | `_load_compose()` ๊ฐ€ `services` ๋น„๋ฉด ์‹คํŒจ | **ํ˜„์žฌ์˜ 0๋ฐ”์ดํŠธ compose**, ์ž์‚ฐ ์‚ญ์ œ | | **D-23** | `compose_image_matches_doc_and_is_alpine` | compose ์˜ `nats:` == `PRIVATE_SERVER.md` ํŽœ์Šค์—์„œ ์ถ”์ถœํ•œ ํƒœ๊ทธ, `tag != "latest"`, `"alpine" in tag` | ์–‘์ชฝ์—์„œ ํƒœ๊ทธ ์ถ”์ถœ ์‹คํŒจ ์‹œ ์‹คํŒจ | ํ•œ์ชฝ๋งŒ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋Š” ๋“œ๋ฆฌํ”„ํŠธ, scratch ํšŒ๊ท€ | | **D-24** | `compose_port_exposure_contract` | ์ปจํ…Œ์ด๋„ˆ ํฌํŠธ ์ง‘ํ•ฉ == `{1883,4222,8222,8080}`; ๋ชจ๋“  ๊ฒŒ์‹œ๊ฐ€ 3ํ•„๋“œ โ†’ **bare `"1883:1883"` ๊ธˆ์ง€**; `8222` ๋Š” ์ •ํ™•ํžˆ `127.0.0.1:8222:8222` | `ports` ๋น„๋ฉด ์‹คํŒจ | ๋ฐ”์ธ๋“œ ๋ˆ„๋ฝ์œผ๋กœ ์ธํ„ฐ๋„ท ๋…ธ์ถœ, ํฌํŠธ ์˜คํƒ€ | | **D-25** | `secrets_are_fail_closed` | (a) `nats.conf` ์˜ ๋ชจ๋“  `$VAR` โІ compose `environment:` ํ‚ค (b) ๊ฐ’์ด ์ „๋ถ€ `:?` ํ˜•ํƒœ (c) `SECRET_VARS` ์ „๋ถ€ `.env.example` ์— ์กด์žฌ (d) `.env.example` ์˜ ์‹œํฌ๋ฆฟ 4์ข… ๊ฐ’์ด **๋นˆ ๋ฌธ์ž์—ด** (e) `nats.conf` ์˜ ๋ชจ๋“  `password:` ๊ฐ’์ด `$` ์‹œ์ž‘ | `$VAR` 0๊ฑด์ด๋ฉด ์‹คํŒจ | ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์•”ํ˜ธ, ํ‰๋ฌธ ์‹œํฌ๋ฆฟ, `:?`โ†’`:-` ์™„ํ™” | | **D-26** | `nats_conf_jetstream_and_mqtt_contract` | `store_dir` ์ ˆ๋Œ€๊ฒฝ๋กœ + compose ๋ณผ๋ฅจ ํƒ€๊นƒ๊ณผ **๋™์ผ**; `max_file`/`max_mem` ์ด `^\d+[KMGT]$`; `mqtt {` + `port: 1883`; `MAM` ๊ณ„์ • `jetstream: enabled` | ์ถ”์ถœ ์‹คํŒจ ์‹œ ์‹คํŒจ | `store_dir`โ†”๋ณผ๋ฅจ ๋ถˆ์ผ์น˜(๋ฐ์ดํ„ฐ ์œ ์‹ค), ์†Œ๋ฌธ์ž ์ ‘๋ฏธ์‚ฌ, ๊ณ„์ • JS ๋ˆ„๋ฝ | | **D-27** | `observer_permissions_match_topic_root` | subject ๋ฆฌํ„ฐ๋Ÿด์ด `mqtt_common.DEFAULT_TOPIC_ROOT.replace("/",".")` ๋กœ ์‹œ์ž‘; `mam_observer` ์— `publish` `deny` ์กด์žฌ | ๋ฆฌํ„ฐ๋Ÿด 0๊ฑด์ด๋ฉด ์‹คํŒจ | M3 ์ง€๋ฌธ ํ† ํ”ฝ ์ „ํ™˜ ์‹œ ๊ด€์ธก์ž ๊ถŒํ•œ ๊ณ ์•„ํ™” | | **D-28** | `healthcheck_contract_and_image_coupling` | healthcheck ์กด์žฌ + `/healthz` + `127.0.0.1:8222` + `wget`; **๋™์ผ ํ…Œ์ŠคํŠธ์—์„œ** ์ด๋ฏธ์ง€๊ฐ€ alpine ๊ณ„์—ด์ž„์„ ๋‹จ์–ธ | healthcheck ํ‚ค ์—†์œผ๋ฉด ์‹คํŒจ | healthcheck ์‚ญ์ œ, wget ์—†๋Š” ์ด๋ฏธ์ง€ | | **D-29** | `env_secrets_never_tracked` | `git check-ignore docker/.env` rc == 0; `.env.example` ์€ not-ignored; `git ls-files docker/.env` ๋นˆ ์ถœ๋ ฅ | โ€” | `.gitignore` ์™„ํ™”๋กœ ์‹ค์ œ ์‹œํฌ๋ฆฟ ์ปค๋ฐ‹ | | **D-30** ๐Ÿ†• | `websocket_origin_policy_is_startable` | **ํ™œ์„ฑ**(๋น„์ฃผ์„) ๋ผ์ธ ๊ธฐ์ค€: (a) `websocket {` ๋ธ”๋ก์— `no_tls: true` ์กด์žฌ(M-23); (b) `allowed_origins` ๊ฐ€ ํ™œ์„ฑ์ด๋ผ๋ฉด ๊ทธ ํ•ญ๋ชฉ์ด ์ „๋ถ€ `http://` ๋˜๋Š” `https://` ๋กœ ์‹œ์ž‘ โ€” ํŠนํžˆ **`"*"` ๊ธˆ์ง€**(M-22); (c) `nats.conf` ๊ฐ€ `/mqtt` ๊ฒฝ๋กœ ์ฃผ์„์„ ํฌํ•จํ•ด N-7 ์ง€์‹์ด ์œ ์‹ค๋˜์ง€ ์•Š์Œ | `websocket {` ๋ธ”๋ก์„ ๋ชป ์ฐพ์œผ๋ฉด ์‹คํŒจ | **์ฑŒ๋ฆฐ์ง€ ์ฒ˜๋ฐฉ 2๋ฅผ ๊ทธ๋Œ€๋กœ ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ์˜ ๊ธฐ๋™ ๋ถˆ๋Šฅ**, `no_tls` ๋ˆ„๋ฝ์œผ๋กœ ์ธํ•œ TLS ์š”๊ตฌ ์—๋Ÿฌ, N-7 ๋ฌธ์„œ ์†Œ์‹ค | ### 4.3 ๋ฎคํ…Œ์ด์…˜ ์ˆ˜์šฉ ๊ธฐ์ค€ (๊ตฌํ˜„ ์™„๋ฃŒ์˜ ์ •์˜) ๊ฐ€๋“œ๋Š” **๊นจ์ ธ์•ผ ํ•  ๋•Œ ๊นจ์ง€๋Š” ๊ฒƒ์ด ์ฆ๋ช…๋˜์–ด์•ผ** ํ†ต๊ณผ๋กœ ์ธ์ •ํ•ฉ๋‹ˆ๋‹ค. Creator ๋Š” ์•„๋ž˜๋ฅผ ๊ฐ๊ฐ ์ ์šฉ โ†’ ํ•ด๋‹น ํ…Œ์ŠคํŠธ FAIL ํ™•์ธ โ†’ ์›๋ณตํ•˜๊ณ , ๋กœ๊ทธ๋ฅผ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ๋˜๋Š” ๋ฆฌ๋ทฐ ์š”์ฒญ์— ์ฒจ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. | ๋ฎคํ…Œ์ด์…˜ | FAIL ํ•ด์•ผ ํ•˜๋Š” ๊ฐ€๋“œ | |---|---| | `docker-compose.yaml` ์„ 0๋ฐ”์ดํŠธ๋กœ ๋˜๋Œ๋ฆผ | D-22 | | ์ด๋ฏธ์ง€๋ฅผ `nats:latest` ๋กœ ๋ณ€๊ฒฝ | D-23, D-28 | | `- "1883:1883"` (๋ฐ”์ธ๋“œ ์ฃผ์†Œ ์ œ๊ฑฐ) | D-24 | | `8222` ๋ฅผ `0.0.0.0:8222:8222` ๋กœ ๋ณ€๊ฒฝ | D-24 | | `.env.example` ์˜ `MAM_BROKER_PASS=` โ†’ `=changeme` | D-25 | | compose ์˜ `:?` โ†’ `:-` ๋กœ ์™„ํ™” | D-25 | | `store_dir: "/data"` โ†’ `"/var/lib/nats"` (๋ณผ๋ฅจ ์œ ์ง€) | D-26 | | `subscribe.allow` ๋ฅผ `"mam.>"` ๋กœ ๋ณ€๊ฒฝ | D-27 | | healthcheck ๋ธ”๋ก ์‚ญ์ œ | D-28 | | `.gitignore` ์— `!docker/.env` ์ถ”๊ฐ€ | D-29 | | **`allowed_origins: ["*"]` ๋ฅผ ํ™œ์„ฑ ๋ผ์ธ์œผ๋กœ ์ถ”๊ฐ€** | **D-30** | | **`websocket {}` ์—์„œ `no_tls: true` ์‚ญ์ œ** | **D-30** | > [!IMPORTANT] > **D-22 ๋Š” ์ฐฉ์ˆ˜ ์‹œ์ ์— ์ด๋ฏธ FAIL ํ•ฉ๋‹ˆ๋‹ค** (0๋ฐ”์ดํŠธ compose ํŒŒ์ผ ๋•Œ๋ฌธ). ๊ฐ€๋“œ๋ฅผ ๋จผ์ € ์ปค๋ฐ‹ํ•˜๊ณ  ์ž์‚ฐ์„ ์ฑ„์šฐ๋Š” **ํ…Œ์ŠคํŠธ ์šฐ์„  ์ˆœ์„œ**๋กœ ์ง„ํ–‰ํ•˜๋ฉด, ์ด ๊ฐ€๋“œ๊ฐ€ ๊ณตํ—ˆํ•˜๊ฒŒ ํ†ต๊ณผํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์‚ฌ์‹ค์ด ๋ณ„๋„ ๋ฎคํ…Œ์ด์…˜ ์—†์ด ์ž๋™ ์ฆ๋ช…๋ฉ๋‹ˆ๋‹ค. ### 4.4 ์˜ˆ์ƒ ํ…Œ์ŠคํŠธ ์ˆ˜ | ์‹œ์  | ์ˆ˜ | |---|---| | ํ˜„์žฌ (`3523b9b`) โ€” ์‹ค์ธก | **297** | | D-22 ~ D-30 ์ถ”๊ฐ€ ํ›„ | **306** | --- ## 5. ๋ฌธ์„œ ๋™๊ธฐํ™” ์ž‘์—… (Creator ๋ฒ”์œ„) `docker/` ๋งŒ ๋งŒ๋“ค๊ณ  ๋ฌธ์„œ๋ฅผ ๋‘๋ฉด ๋‹ค์Œ ๋ฆฌ๋ทฐ์—์„œ "๋ฌด์—‡์ด ์ •๋ณธ์ธ๊ฐ€"๊ฐ€ ๋‹ค์‹œ ๋…ผ์Ÿ์ด ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ 5๊ฑด์„ **๊ฐ™์€ ์ปค๋ฐ‹**์— ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. | ID | ํŒŒ์ผ | ์ž‘์—… | |---|---|---| | **T-1** | `PRIVATE_SERVER.md` ยง9 ์„œ๋‘ | "๋ณธ ์ ˆ์˜ ์„ค์ •์€ [`docker/`](docker/) ์— ์ •๋ณธ ํŒŒ์ผ๋กœ ์กด์žฌํ•˜๋ฉฐ ์•„๋ž˜ ํŽœ์Šค๋Š” ๊ทธ ์‚ฌ๋ณธ์ž…๋‹ˆ๋‹ค. ์–ด๊ธ‹๋‚˜๋ฉด `tests/test_deploy_freshness.py` ์˜ D-22~D-30 ์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค." 1๋ฌธ๋‹จ ์ถ”๊ฐ€ | | **T-2** | `PRIVATE_SERVER.md` ยง9.1 / ยง9.2 | ํŽœ์Šค๋ฅผ `docker/nats.conf`ยท`docker/docker-compose.yaml` ์ตœ์ข…๋ณธ๊ณผ **์ผ์น˜**์‹œํ‚ด (`version: '3.8'` ์ œ๊ฑฐ, **ยง3.1 ์˜ websocket ์ฃผ์„ ๋ธ”๋ก ํฌํ•จ**) | | **T-3** | `PRIVATE_SERVER.md` ยง9.2 ์ œ๋ชฉ ยท ยง9.3 `.env` ๋ธ”๋ก | `docker-compose.yml` โ†’ `docker/docker-compose.yaml`; `.env` ์ƒ์„ฑ ์ ˆ์ฐจ๋ฅผ `docker/.env.example` ๋ณต์‚ฌ ๋ฐฉ์‹์œผ๋กœ ๊ต์ฒด | | **T-4** | `implementation_plan.md` ยง5 / ยง8 | ยง5 ๋กœ๋“œ๋งต์— **P0.5 `docker/` ์ž์‚ฐ ์ •๋ณธํ™”** ์‚ฝ์ž…; ยง8 M2b ์—์„œ ์ด๋ฏธ ์™„๋ฃŒ๋œ `G-D5~G-D9, G-R1, G-R2` ๋ฅผ `[x]` ๋กœ ์ •์ •(N-4)ํ•˜๊ณ  `docker/` ์ž์‚ฐ + D-22~D-30 (297 โ†’ 306) ํ•ญ๋ชฉ ์‹ ์„ค | | **T-5** ๐Ÿ†• | `PRIVATE_SERVER.md` ยง5.1 / ยง5.2 | '๋‘ ๊ฐœ์˜ ์†Œ๋น„ ํ‰๋ฉด' ์„œ์ˆ ์— **์„ธ ๋ฒˆ์งธ ๊ฒฝ๋กœ**๋ฅผ ๋ช…๋ฌธํ™”: 8080 ์€ NATS WebSocket ๊ณผ **MQTT-over-WebSocket(`/mqtt`)** ์„ ํ•จ๊ป˜ ์„œ๋น™ํ•˜๋ฉฐ, ํ›„์ž๋งŒ retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๋Š”๋‹ค(N-1 โ†” N-7 ์—ฐ๊ฒฐ). ยง5.2 ์˜ ๋ธŒ๋ฆฌ์ง• ๋ฌธ๋‹จ์ด ํ˜„์žฌ ์ด ๊ตฌ๋ถ„ ์—†์ด ์„œ์ˆ ๋˜์–ด ์žˆ์–ด ์ •์ • ๋Œ€์ƒ | T-2 ์ˆ˜ํ–‰ ์‹œ ์ฃผ์˜: ยง9.1/ยง9.2 ํŽœ์Šค๋Š” ๊ธฐ์กด D-15 ~ D-19 ๊ฐ€๋“œ์˜ ๊ฒ€์‚ฌ ๋Œ€์ƒ์ด๊ธฐ๋„ ํ•˜๋ฏ€๋กœ, ํŽธ์ง‘ ํ›„ **์ „์ฒด ์Šค์œ„ํŠธ๋ฅผ ๋Œ๋ ค** ๊ธฐ์กด 7์ข… ๊ฐ€๋“œ์˜ ํšŒ๊ท€ ์—†์Œ์„ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. --- ## 6. ์‹คํ–‰ ์ˆœ์„œ ๋ฐ ์™„๋ฃŒ ์ •์˜ ``` [1] ๊ฐ€๋“œ ์„ ํ–‰ ์ปค๋ฐ‹ โ”€โ”€> [2] docker/ ์ž์‚ฐ ์ž‘์„ฑ โ”€โ”€> [3] ๋ฌธ์„œ ๋™๊ธฐํ™” โ”€โ”€> [4] ๋ฎคํ…Œ์ด์…˜ ๊ฒ€์ฆ โ”€โ”€> [5] ์„œ๋ฒ„ ์‹ค๋ฐฐํฌ D-22~D-30 ์ถ”๊ฐ€ 3.1~3.4 (4๊ฐœ ํŒŒ์ผ) T-1~T-5 ยง4.3 12๊ฑด ์ „๊ฑด R-1~R-10 (+R-11) D-22 FAIL ํ™•์ธ 306 ์ „๊ฑด GREEN ๊ธฐ์กด D-15~D-19 ๊ฐ FAILโ†’์›๋ณต (๋ณ„๋„ ์žก) ํšŒ๊ท€ ์—†์Œ ``` **DoD (๋ณธ ์žก์˜ ์™„๋ฃŒ ์ •์˜)** 1. `docker/` 4๊ฐœ ํŒŒ์ผ์ด ยง3 ์‚ฌ์–‘๋Œ€๋กœ ์กด์žฌํ•˜๊ณ , `docker/.env` ๋Š” ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค. 2. `pytest tests/ -q` ๊ฐ€ **306๊ฑด ์ „๊ฑด ํ†ต๊ณผ**. 3. ยง4.3 ๋ฎคํ…Œ์ด์…˜ 12๊ฑด์ด ๊ฐ๊ฐ ์ง€์ •๋œ ๊ฐ€๋“œ๋ฅผ FAIL ์‹œํ‚ด์ด ๋กœ๊ทธ๋กœ ํ™•์ธ๋œ๋‹ค. 4. T-1 ~ T-5 ๋ฌธ์„œ ๋™๊ธฐํ™”๊ฐ€ ๋™์ผ ์ปค๋ฐ‹์— ํฌํ•จ๋œ๋‹ค. 5. `git status` ์— `docker/.env` ๊ฐ€ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š๋Š”๋‹ค(D-29 ๋กœ ์ž๋™ ๋ณด์ฆ). **๊ฒŒ์ดํŠธ**: 3๋ฒˆ(๋ฎคํ…Œ์ด์…˜ ์ „๊ฑด FAIL) ๋ฏธ์ถฉ์กฑ ์‹œ ์ปค๋ฐ‹ ๊ธˆ์ง€. ํ†ต๊ณผํ•˜์ง€ ์•Š๋Š” ๊ฐ€๋“œ๋Š” ๊ฐ€๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ฃผ์„์ž…๋‹ˆ๋‹ค. **์‹ ๊ทœ ์›๊ฒฉ ๊ฒ€์ฆ ํ•ญ๋ชฉ** (์„œ๋ฒ„ ๋ฐฐํฌ ์žก์œผ๋กœ ์ด์›”): | ID | ๊ฒ€์ฆ | ๋ฐฉ๋ฒ• | ํ†ต๊ณผ ๊ธฐ์ค€ | |---|---|---|---| | **R-11** | healthcheck ์˜ ์Œ์„ฑ ๋Œ€์กฐ | ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ `wget -q -O /dev/null http://127.0.0.1:8222/healthzX` (404) | **๋น„์˜์  ์ข…๋ฃŒ** โ€” 200 ์ด ์•„๋‹ ๋•Œ ์‹ค์ œ๋กœ ์‹คํŒจํ•จ์„ ์ฆ๋ช… | | **R-12** | ๊ต์ฐจ ์ถœ์ฒ˜ ๋ธŒ๋ผ์šฐ์ € ์ ‘์† | ๋‹ค๋ฅธ ์˜ค๋ฆฌ์ง„์˜ ํŽ˜์ด์ง€์—์„œ `new WebSocket("ws://:8080/")` | **ํ•ธ๋“œ์…ฐ์ดํฌ ์„ฑ๊ณต** (A-2 ์˜ ๋ฐ˜์ฆ ๊ฐ€๋Šฅ ํ˜•ํƒœ โ€” 403 ์ด ๋‚˜์˜ค๋ฉด M-19/M-20 ํŒ์ •์ด ํ‹€๋ฆฐ ๊ฒƒ) | | **R-13** | MQTT-over-WS retained | ์žก ์ข…๋ฃŒ **ํ›„** MQTT.js ๋กœ `ws://:8080/mqtt` ์ ‘์† โ†’ ๊ตฌ๋… | **retained ์ตœ์ข… ์ด๋ฒคํŠธ ์ˆ˜์‹ ** (N-7 ํ™•์ฆ. 0๊ฑด์ด๋ฉด N-7 ์ด ํ‹€๋ฆฐ ๊ฒƒ) | R-12 ์™€ R-13 ์€ **์˜๋„์ ์œผ๋กœ ๋ฐ˜์ฆ ๊ฐ€๋Šฅํ•˜๊ฒŒ** ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ธก์ด ํ‹€๋ ธ๋‹ค๋ฉด ์ด ๋‘ ํ•ญ๋ชฉ์—์„œ ๋“œ๋Ÿฌ๋‚˜๋ฉฐ, ๊ทธ ๊ฒฝ์šฐ ยงA-2 ์™€ ยงA-5 ๋ฅผ ํ๊ธฐํ•˜๊ณ  ์ฑŒ๋ฆฐ์ง€์˜ ์ฒ˜๋ฐฉ 1์„ ์žฌ๊ฒ€ํ† ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. **๋ณธ ์žก ๋ฒ”์œ„ ๋ฐ–**: ์‹ค์ œ ์„œ๋ฒ„ ํ”„๋กœ๋น„์ €๋‹, Tailscale ๊ฐ€์ž…, `.mam.env` ์ „ํ™˜, R-1 ~ R-13 ์‹คํ–‰. --- ## 7. ๋ฐœ๊ฒฌ ์‚ฌํ•ญ (Findings) ### N-2 (P2) โ€” `PyYAML` ์ด ์„ ์–ธ๋˜์ง€ ์•Š์€ ํ•˜๋“œ ํ…Œ์ŠคํŠธ ์˜์กด `requirements.txt` ๋Š” `pytest>=8.0` **๋‹จ ํ•œ ์ค„**์ธ๋ฐ `tests/test_sanity.py:3` ์€ ์ตœ์ƒ๋‹จ์—์„œ `import yaml` ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ venv ์—๋Š” ์„ค์น˜๋˜์–ด ์žˆ์–ด ๋“œ๋Ÿฌ๋‚˜์ง€ ์•Š์ง€๋งŒ, `requirements.txt` ๋งŒ์œผ๋กœ ์ƒˆ venv ๋ฅผ ๋งŒ๋“ค๋ฉด **์ˆ˜์ง‘ ๋‹จ๊ณ„์—์„œ ๋‹ค์ˆ˜ ํŒŒ์ผ์ด ์—๋Ÿฌ**๋‚ฉ๋‹ˆ๋‹ค. D-22 ~ D-28 ์ด `yaml` ์˜์กด์„ ๋” ๊นŠ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. **์ฒ˜๋ฐฉ**: `requirements.txt` ์— `PyYAML>=6.0` ์ถ”๊ฐ€. (`pytest.importorskip` ์€ ๋ถ€์ ์ ˆ โ€” ์Šคํ‚ต๋˜๋ฉด ๋ฐฐํฌ ๊ฐ€๋“œ๊ฐ€ ์กฐ์šฉํžˆ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.) ### N-3 (P2) โ€” D-16 ๊ฐ€๋“œ์˜ ๊ตฌ๋ฉ: `nats:2.12` ๊ฐ€ ํ†ต๊ณผํ•œ๋‹ค `tests/test_deploy_freshness.py:387` ์˜ `assert "-alpine" in tag or tag.startswith("2.")` ๋Š” `or` ๋•Œ๋ฌธ์— `nats:2.12`(non-alpine) ๋‚˜ `nats:2.12-scratch` ๋ฅผ ํ†ต๊ณผ์‹œํ‚ต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ healthcheck ๋Š” alpine ์—๋งŒ ์žˆ๋Š” `wget` ์„ ์”๋‹ˆ๋‹ค(M-3). ์ฆ‰ D-16 ์€ ์ž์‹ ์ด ๋ง‰์œผ๋ ค๋˜ ์‹คํŒจ ๋ชจ๋“œ์˜ **์ธ์ ‘ ๋ณ€์ข…์„ ๋†“์นฉ๋‹ˆ๋‹ค**. **์ฒ˜๋ฐฉ**: `assert "alpine" in tag` ๋กœ ๋‹จ์ˆœํ™”. ### N-4 (P3) โ€” `implementation_plan.md` ์ฒดํฌ๋ฆฌ์ŠคํŠธ๊ฐ€ ์‹ค์ œ๋ณด๋‹ค ๋’ค์ฒ˜์ง ยง8 M2b ์˜ `- [ ] ์‹ ๊ทœ ๊ฐ€๋“œ G-D5 ~ G-D9, G-R1, G-R2 โ€ฆ(290 -> 297)` ์ด ๋ฏธ์ฒดํฌ์ด๋‚˜ ์‹ค์ธก ์ˆ˜์ง‘ ์ˆ˜๋Š” **297** ์ด๊ณ  D-15 ~ D-21 ์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. T-4 ์—์„œ ์ •์ •ํ•ฉ๋‹ˆ๋‹ค. ### N-5 (P2) โ€” NATS ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์„ **์„ค์ • ๋ ‰์„œ๋กœ ์žฌํŒŒ์‹ฑ**ํ•œ๋‹ค `conf/parse.go` ์˜ `lookupVariable` ์€ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ `parseEnv(fmt.Sprintf("%s=%s", pkey, vStr), p)` ๋กœ **๋‹ค์‹œ ํŒŒ์‹ฑ**ํ•ฉ๋‹ˆ๋‹ค(M-7). ๋น„์ธ์šฉ ๋ฌธ์ž์—ด ์ข…๊ฒฐ์ž๋Š” NLยทEOFยท`;`ยท`,`ยท`]`ยท`}`ยท๊ณต๋ฐฑ(M-8)์ด๋ฉฐ `#`ยท`$`ยท๋”ฐ์˜ดํ‘œ๋„ ์œ„ํ—˜ํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ๋Š” `openssl rand -base64 32` ๋ฅผ ์“ฐ๋ฉด์„œ **์™œ ๊ทธ๋ž˜์•ผ ํ•˜๋Š”์ง€**๋ฅผ ์ ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ยง3.1/ยง3.3 ์ฃผ์„๊ณผ README ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์— ๋ช…๋ฌธํ™”ํ•ฉ๋‹ˆ๋‹ค. ### N-6 (P3) โ€” ํฌ๊ธฐ ์ ‘๋ฏธ์‚ฌ๋Š” ๋Œ€๋ฌธ์ž ์ „์šฉ `opts.go:2507` `suffixMap` ์€ `{"K","M","G","T"}` ๋ฟ. `max_file: 10g` ๋Š” ๊ธฐ๋™ ์‹คํŒจ. D-26 ์ด `^\d+[KMGT]$` ๋กœ ๋ด‰์ธํ•ฉ๋‹ˆ๋‹ค. ### N-7 (P1, ์‹ ๊ทœ) โ€” 8080 ์€ `/mqtt` ๊ฒฝ๋กœ๋กœ MQTT-over-WebSocket ์„ ์„œ๋น™ํ•œ๋‹ค ยงA-5 ์ฐธ์กฐ. `mqttWSPath = "/mqtt"`(`mqtt.go:193`)์ด๋ฉฐ `websocket.go:1334-1335` ๊ฐ€ ๋„ค์ดํ‹ฐ๋ธŒ 1883 ๋ฆฌ์Šค๋„ˆ์™€ **๋™์ผํ•œ `createMQTTClient`** ๋กœ ๋ถ„๊ธฐํ•ฉ๋‹ˆ๋‹ค. ๊ฒŒ์ดํŠธ๋Š” `opts.MQTT.Port != 0` ๋ฟ์ด๋ผ ์šฐ๋ฆฌ ์„ค์ •์—์„œ **์ด๋ฏธ ํ™œ์„ฑ**์ž…๋‹ˆ๋‹ค. ์„ธ ๊ฐ€์ง€ ๊ท€๊ฒฐ: 1. ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ MQTT.js ๋กœ `/mqtt` ์— ๋ถ™์œผ๋ฉด **retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค** โ†’ Rev.2 ์ด๋ž˜์˜ ์—ด๋ฆฐ ์งˆ๋ฌธ ํ•ด์†Œ, JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ ๋ถˆํ•„์š”. 2. ๊ฐ™์€ ํฌํŠธ์— ๊ฒฝ๋กœ๋งŒ ๋‹ค๋ฅด๊ฒŒ ๋ถ™์œผ๋ฉด(NATS ๋„ค์ดํ‹ฐ๋ธŒ) N-1 ์ด ๊ทธ๋Œ€๋กœ ์ ์šฉ๋˜์–ด ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ชป ๋ฐ›์Šต๋‹ˆ๋‹ค. **๊ฐ™์€ ํฌํŠธ, ๋‹ค๋ฅธ ๊ฒฝ๋กœ, ๋‹ค๋ฅธ ๊ฒฐ๊ณผ** โ€” ํ•จ์ •์ž…๋‹ˆ๋‹ค. 3. ๋…ธ์ถœ ๋ชจ๋ธ ์„œ์ˆ  ์ •์ • ํ•„์š”: 8080 ์€ 'Plane B ์ „์šฉ'์ด ์•„๋‹ˆ๋ผ **MQTT ํ”„๋กœํ† ์ฝœ ํ‘œ๋ฉด์„ ํ•จ๊ป˜ ๋…ธ์ถœ**ํ•ฉ๋‹ˆ๋‹ค(์ธ์ฆ์€ ๋™์ผ ๊ณ„์ • ๊ทœ์น™์œผ๋กœ ๊ฐ•์ œ). ### N-1 ์žฌํ™•์ธ (๊ธฐ์กด) โ€” retained ๋Š” MQTT ์ „์šฉ `mqttSendRetainedMsgsToNewSubs` ๊ฐ€ `mqttPacketSub` ํ•ธ๋“ค๋Ÿฌ์—์„œ๋งŒ ํ˜ธ์ถœ๋˜๊ณ  MQTT ์ „์šฉ ํ•„๋“œ `sub.mqtt.prm` ๋ฅผ ์ˆœํšŒํ•œ๋‹ค๋Š” Rev.2 ์‹ค์ธก์€ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ N-7 ๋กœ **๊ฒฝ๊ณ„๊ฐ€ ํ”„๋กœํ† ์ฝœ์ด์ง€ ํฌํŠธ๊ฐ€ ์•„๋‹˜**์ด ๋ถ„๋ช…ํ•ด์กŒ์Šต๋‹ˆ๋‹ค โ€” MQTT-over-WS ๋„ MQTT ์ด๋ฏ€๋กœ retained ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. --- ## 8. ์—ด๋ฆฐ ์งˆ๋ฌธ (๋น„์ฐจ๋‹จ) ๊ณ„ํš์€ ์•„๋ž˜ ๋‹ต ์—†์ด ๊ทธ๋Œ€๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์„ ๋ช…์‹œํ–ˆ์œผ๋ฏ€๋กœ **์‘๋‹ต์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ง„ํ–‰**ํ•ฉ๋‹ˆ๋‹ค. | # | ์งˆ๋ฌธ | ๊ธฐ๋ณธ๊ฐ’(๋ฌด์‘๋‹ต ์‹œ) | |---|---|---| | **Q-1** | ์ด๋ฏธ์ง€ ํ•€์„ `nats:2.12-alpine`(๋งˆ์ด๋„ˆ ์ถ”์ข…) ๋กœ ๋‘˜ ๊ฒƒ์ธ๊ฐ€, `2.12.15-alpine`(ํŒจ์น˜ ๊ณ ์ •) ์œผ๋กœ ์กฐ์ผ ๊ฒƒ์ธ๊ฐ€? | `2.12-alpine` ์œ ์ง€ โ€” ๋ฌธ์„œ ยง9.2 ์™€ ์ผ์น˜, ๋ณด์•ˆ ํŒจ์น˜ ์ž๋™ ์ˆ˜๋ น | | **Q-2** | ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋น„๋ฃจํŠธ(`user: "1000:1000"`) ๋กœ ๋Œ๋ฆด ๊ฒƒ์ธ๊ฐ€? | ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ โ€” `nats-data` ๋ณผ๋ฅจ ์†Œ์œ ๊ถŒ ์ดˆ๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•ด ๋ฌด์ฆ์ƒ ์‹คํŒจ ์œ„ํ—˜. ๋ณ„๋„ ํ•˜๋“œ๋‹ ์žก์œผ๋กœ ๋ถ„๋ฆฌ | | **Q-3** | `docker/` ๋ฅผ `deploy/install.sh` ๋กœ ํ•˜์œ„ ์›Œํฌ์ŠคํŽ˜์ด์Šค์— ๋ฐฐํฌํ•  ๊ฒƒ์ธ๊ฐ€? | ๋ฐฐํฌํ•˜์ง€ ์•Š์Œ (D-8) | | **Q-4** | `.mam.env` ์˜ `MQTT_PASSWORD` โ†” ์„œ๋ฒ„ `docker/.env` ์˜ `MAM_BROKER_PASS` ๋™๊ธฐํ™”๋ฅผ ์Šคํฌ๋ฆฝํŠธํ™”ํ•  ๊ฒƒ์ธ๊ฐ€? | ์ˆ˜๋™ โ€” ๋‘ ํŒŒ์ผ์ด ๋‹ค๋ฅธ ํ˜ธ์ŠคํŠธ์— ์žˆ์–ด ์ž๋™ํ™”๋Š” ์‹œํฌ๋ฆฟ ์ „์†ก ๊ฒฝ๋กœ๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“œ๋Š” ์ผ. README 6์ ˆ์— ์ ˆ์ฐจ๋งŒ ๊ธฐ์ˆ  | | **Q-5** ๐Ÿ†• | ๋Œ€์‹œ๋ณด๋“œ ํ”„๋กœํ† ์ฝœ: MQTT.js(`/mqtt`) ๋กœ ํ†ต์ผํ•  ๊ฒƒ์ธ๊ฐ€, `nats.ws` ๋„ ํ—ˆ์šฉํ•  ๊ฒƒ์ธ๊ฐ€? | **MQTT.js ๋‹จ์ผ ๊ถŒ์žฅ.** retained ๋ฅผ ๋ฌด์ƒ์œผ๋กœ ์–ป๊ณ  N-1 ํ•จ์ •์ด ์‚ฌ๋ผ์ง. `nats.ws` ๋Š” KV/JetStream ๊ฐ™์€ NATS ๊ณ ์œ  ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•  ๋•Œ๋งŒ | > Rev.2(`b11d499d`) ์ด๋ž˜ ์ด์›”๋˜๋˜ *"๋Œ€์‹œ๋ณด๋“œ๋ฅผ MQTT ๋กœ ๋ถ™์ผ ๊ฒƒ์ธ๊ฐ€, JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์˜ ๋””์Šคํฌ ๊ด€๋ฆฌ ๋ถ€๋‹ด์„ ์ˆ˜์šฉํ•  ๊ฒƒ์ธ๊ฐ€"* ๋Š” **N-7 ๋กœ ํ•ด์†Œ**๋˜์–ด ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ MQTT ๋ฅผ ์“ธ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. --- ## 9. ๋ถ€๋ก โ€” Creator ์ฐฉ์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - [ ] `tests/test_deploy_freshness.py` ์— ยง4.1 ํ—ฌํผ(`_load_compose`, `_active_conf_lines`) + D-22 ~ D-30 ์ถ”๊ฐ€ โ†’ `pytest -k d22` ๊ฐ€ **FAIL** ํ•˜๋Š”์ง€ ๋จผ์ € ํ™•์ธ - [ ] `requirements.txt` ์— `PyYAML>=6.0` ์ถ”๊ฐ€ (N-2) - [ ] `docker/nats.conf` ์ž‘์„ฑ โ€” **websocket ์ฃผ์„ ๋ธ”๋ก ํฌํ•จ** (ยง3.1) - [ ] `docker/docker-compose.yaml` ์ž‘์„ฑ โ€” ๊ธฐ์กด 0๋ฐ”์ดํŠธ ํŒŒ์ผ ๋ฎ์–ด์“ฐ๊ธฐ (ยง3.2) - [ ] `docker/.env.example` ์ž‘์„ฑ โ€” ์‹œํฌ๋ฆฟ 4์ข… **๋นˆ ๊ฐ’** ํ™•์ธ (ยง3.3) - [ ] `docker/README.md` ์ž‘์„ฑ โ€” ๋ธŒ๋ผ์šฐ์ € ๋ ˆ์‹œํ”ผ + ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 12ํ–‰ (ยง3.4) - [ ] `PRIVATE_SERVER.md` T-1 ~ T-3, **T-5** (ยง5) - [ ] `implementation_plan.md` T-4 (ยง5) - [ ] D-16 ๋‹จ์–ธ ์ •์ • (N-3) - [ ] `pytest tests/ -q` โ†’ **306 passed** - [ ] ยง4.3 ๋ฎคํ…Œ์ด์…˜ **12๊ฑด** ๊ฐ๊ฐ FAIL ํ™•์ธ ํ›„ ์›๋ณต, ๋กœ๊ทธ ์ฒจ๋ถ€ - [ ] `git status` ์— `docker/.env` ๋ถ€์žฌ ํ™•์ธ - [ ] โš ๏ธ **`allowed_origins: ["*"]` ๋ฅผ ํ™œ์„ฑ ์„ค์ •์œผ๋กœ ๋„ฃ์ง€ ๋ง ๊ฒƒ** โ€” ๋ธŒ๋กœ์ปค๊ฐ€ ๊ธฐ๋™ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค (ยงA-3, M-22)