# ๐ŸŒ ์›๊ฒฉ ์„œ๋ฒ„ `nats-server` Docker ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๊ณ„ํš์„œ **Rev.2** (Job `b11d499d`) - **์—ญํ• **: Planner (`MULTI_AGENT_RULES.md` ยง1 โ€” ์ €์žฅ์†Œ ์ฝ”๋“œ/๋ฌธ์„œ ๋ฏธ๋ณ€๊ฒฝ, ๊ณ„ํš์„œ๋งŒ ์‚ฐ์ถœ) - **์„ ํ–‰ ๋ฆฌ๋น„์ „**: Rev.1 = Job `27236ab6` - **๋ฐ˜์˜ ์ฑŒ๋ฆฐ์ง€**: Job `9a5cb88f` (`agy`) โ€” `[VERDICT: PASS WITH CHALLENGE]` - **๊ธฐ์ค€ ์ปค๋ฐ‹**: `c6b6c77` (Track 0 ์™„๋ฃŒ, **290 tests collected** ์‹ค์ธก) - **๊ฒ€์ฆ ์›์น™**: ์ถ”๋ก ์ด ์•„๋‹Œ **์‹ค์ธก**. ์ฑŒ๋ฆฐ์ง€๋Š” ์ง€์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ **๊ฐ€์„ค**๋กœ ์ทจ๊ธ‰ํ•˜์—ฌ ์žฌํ˜„ยท๋ฐ˜์ฆํ–ˆ์Šต๋‹ˆ๋‹ค. --- ## 0. Rev.2 ํŒ์ • ์š”์•ฝ (Adjudication) | ์ฑŒ๋ฆฐ์ง€ | ํŒ์ • | ์š”์ง€ | |---|---|---| | **C1** ๊ณ„์ • ๊ฒฉ๋ฆฌ๊ฐ€ ๊ต์ฐจ ๊ด€์ธก์„ ์ฐจ๋‹จ | ๐ŸŸก **๋ถ€๋ถ„ ์ธ์šฉ โ€” ์ง„๋‹จ ์œ ํšจ, ๊ท€์† ๋ถ€์ •ํ™•, ๋‘ ์˜ต์…˜ ๋ชจ๋‘ ๊ฒฐ์ •์  ํ•œ๊ณ„ ๋ˆ„๋ฝ** | ๊ณ„์ • ๊ฒฉ๋ฆฌ ์‚ฌ์‹ค์€ ๋งž์Œ. ๋‹ค๋งŒ "๊ณ„ํš์ด `HOME` ๊ณ„์ •์—์„œ MAM ์ด๋ฒคํŠธ ๊ด€์ธก์„ ์ฃผ์žฅํ•œ๋‹ค"๋Š” ๊ท€์†์€ ๋ถ€์ •ํ™• โ€” `PRIVATE_SERVER.md:230`์ด **์ •๋ฐ˜๋Œ€๋ฅผ ์ด๋ฏธ ์ฒ˜๋ฐฉ**. ๋ฐ˜๋ฉด ๋‚ด Rev.1 config์— ๊ด€์ธก์ž ์‚ฌ์šฉ์ž๊ฐ€ **์•„์˜ˆ ์—†์—ˆ๋˜ ๊ฒƒ**์€ ์‹ค์ œ ๊ฒฐํ•จ์ด๋ฏ€๋กœ ์ˆ˜์šฉ. **์‹ ๊ทœ ์‹ค์ธก**: retained ์ด๋ฒคํŠธ๋Š” **MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ** ์ „๋‹ฌ๋˜๋ฏ€๋กœ Option A/B ์–ด๋А ์ชฝ๋„ "์‚ฌํ›„ ์ ‘์† ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณธ๋‹ค"๋ฅผ ๋งŒ๋“ค์ง€ ๋ชปํ•จ | | **C2** `_load_dotenv` ์šฐ์„ ์ˆœ์œ„ | ๐ŸŸข **๋ฐฉํ–ฅ ์ˆ˜์šฉ + ๊ทผ๋ณธ ๊ฒฐํ•จ ์žฌ์ •์˜ + ์‹ ๊ทœ ๊ฒฐํ•จ 1๊ฑด ๋ฐœ๊ฒฌ** | ์ง„์งœ ๊ฒฐํ•จ์€ ํ›„๋ณด ๋ชฉ๋ก์ด ์•„๋‹ˆ๋ผ **๋‹จ์ผ ํ›„๋ณด ํ•ด์„**. ๊ทธ๋ฆฌ๊ณ  `MAM_ENV_FILE`์ด ์—†๋Š” ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚ค๋ฉด **๋‹ค๋ฅธ ํ›„๋ณด๋ฅผ ํ•˜๋‚˜๋„ ์‹œ๋„ํ•˜์ง€ ์•Š๊ณ  ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ํด๋ฐฑ**(์‹ค์ธก) โ€” ์ œ์•ˆ๋œ ์ˆœ์„œ๋กœ๋Š” ๊ณ ์ณ์ง€์ง€ ์•Š์Œ | | **C3** `G-D5` ์Šค์ฝ”ํ•‘ | ๐ŸŸก **์ด๋ฏธ Rev.1์— ์กด์žฌ. ๋‹จ ์ž”์—ฌ ์ง€์ ์ด D-1์„ ๊ฐ•ํ™”** | ํŽœ์Šค ํ•œ์ •ยท์˜คํƒ ๋ถ€์žฌ ๋‹จ์–ธ์€ Rev.1 ยง6์— ์ด๋ฏธ ๋ช…์‹œ. **์‹ ๊ทœ ์‹ค์ธก**: NATS ๋ ‰์„œ๋Š” **์ธ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’์—์„œ๋งŒ** `$VAR`๋ฅผ ํ•ด์„ โ†’ `store_dir: "$HOME/..."`๋Š” ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉฐ ์ธ์šฉ heredoc๊ณผ ๊ฒฐํ•ฉ ์‹œ **D-1๊ณผ ๋™์ผํ•˜๊ฒŒ ํŒŒ์†**. G-D5๋ฅผ 2ํ•ญ ๊ฒ€์‚ฌ๋กœ ๊ฐ•ํ™” | **Rev.2 ์‹ค์งˆ ๋ณ€๊ฒฝ 6๊ฑด** 1. ยงA-1์— **`mam_observer` ์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž**๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€(Option B ์ฑ„ํƒ โ€” ์ €์žฅ์†Œ ยง5.5 ์ฒ˜๋ฐฉ๊ณผ ์ผ์น˜). 2. **retained๋Š” MQTT ์ „์šฉ**์ด๋ผ๋Š” ์‹ ๊ทœ ์‹ค์ธก์„ ยง1.9๋กœ ์‹ ์„คํ•˜๊ณ  ยง5.2 ๋ธŒ๋ฆฌ์ง• ์ฃผ์žฅ์˜ ๊ฒฝ๊ณ„๋กœ ๋ช…๋ฌธํ™”. 3. Option A(export/import)๋ฅผ **์˜ˆ์™ธ ๊ฒฝ๋กœ**๋กœ ๋ฌธ๋ฒ• ๊ฒ€์ฆ๊นŒ์ง€ ๋งˆ์ณ ๋ถ€๋ก์— ๋ฐฐ์น˜(๋ฌด์กฐ๊ฑด ์ฑ„ํƒํ•˜์ง€ ์•Š๋Š” ๊ทผ๊ฑฐ 3๊ฑด ์ฒจ๋ถ€). 4. `B-17` ์ฒ˜๋ฐฉ์„ **first-hit-wins ํ›„๋ณด ๋ชฉ๋ก**์œผ๋กœ ์žฌ์ •์˜ํ•˜๊ณ  `MAM_ENV_FILE` ์กฐ๊ธฐ ํƒˆ์ถœ ๊ฒฐํ•จ์„ ์ถ”๊ฐ€. 5. **G-D5๋ฅผ 2ํ•ญ ๊ฒ€์‚ฌ๋กœ ๊ฐ•ํ™”**(๊ฐ’ + heredoc ๊ตฌ๋ถ„์ž), `$VAR` ์ธ์šฉ ๊ทœ์น™์„ ยง1.1 ๊ฐ์ฃผ์— ์ •๋ฐ€ํ™”. 6. **G-D9 ์‹ ์„ค** โ€” ๋ฌธ์„œ config ์˜ˆ์ œ์˜ subject ๋ฆฌํ„ฐ๋Ÿด๊ณผ `DEFAULT_TOPIC_ROOT` ์ผ์น˜ ๊ฐ•์ œ(M3 ํ† ํ”ฝ ์ „ํ™˜ ์‹œ ์กฐ์šฉํ•œ ํŒŒ์† ์ฐจ๋‹จ). ํ…Œ์ŠคํŠธ ์ „๋ง 290 โ†’ **297** โ†’ 298. --- ## 1. ์‚ฌ์ „ ์‹ค์ธก ๊ฒฐ๊ณผ (Pre-Flight Measurements) > Rev.1์˜ D-1 ~ D-5, H-1 ~ H-3์€ ์ฑŒ๋ฆฐ์ €๊ฐ€ "Verified 100% accurate"๋กœ ์Šน์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ์š”์ง€ ์œ ์ง€ + **Rev.2 ์‹ ๊ทœ ์‹ค์ธก 2๊ฑด(ยง1.9, ยง1.10)** ๋ฐ ยง1.1 ๊ฐ์ฃผ ์ •๋ฐ€ํ™”์ž…๋‹ˆ๋‹ค. ### 1.1 D-1 โ€” `store_dir`์˜ `~`๋Š” ํ™•์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค (P1) `PRIVATE_SERVER.md:73`, `:146`์ด `store_dir: "~/.local/share/nats/data"`๋ฅผ ์ง€์‹œํ•ฉ๋‹ˆ๋‹ค. **์ฆ๊ฑฐ 1 โ€” NATS ์„ค์ • ํŒŒ์„œ์— ํ‹ธ๋“œ ํ™•์žฅ ์—†์Œ** (`server/opts.go`): ```go case "store", "store_dir", "storedir": opts.StoreDir = mv.(string) // ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋Œ€์ž…. os.UserHomeDir ํ˜ธ์ถœ ์—†์Œ ``` **์ฆ๊ฑฐ 2 โ€” ์ธ์šฉ heredoc์ด ์…ธ ํ™•์žฅ๊นŒ์ง€ ์ฐจ๋‹จ** (์‹ค์ธก): `<<'EOF'` โ†’ `store_dir: "~/.local/share/nats/data"` ๋ฆฌํ„ฐ๋Ÿด ์œ ์ง€ / `< [!IMPORTANT] > **๊ฐ์ฃผ ์ •๋ฐ€ํ™” (Rev.2, C3 ํŒŒ์ƒ)**: NATS ์„ค์ •์˜ `$VAR` ์ฐธ์กฐ๋Š” **์ธ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’์—์„œ๋งŒ** ํ•ด์„๋ฉ๋‹ˆ๋‹ค. ๋ ‰์„œ ์›๋ฌธ โ€” *"Check if the **unquoted** string is a variable reference, starting with `$`."* ์ด๋ฉฐ `lexQuotedString`์€ *"It will not interpret any internal contents."* ์ž…๋‹ˆ๋‹ค. > ๋”ฐ๋ผ์„œ **`store_dir: "$HOME/..."`๋Š” ๋ฆฌํ„ฐ๋Ÿด ๋ฌธ์ž์—ด**์ด๋ฉฐ, ์ธ์šฉ heredoc๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด `./$HOME/.local/share/nats/data`๊ฐ€ ๋งŒ๋“ค์–ด์ ธ D-1๊ณผ **๋™์ผํ•˜๊ฒŒ ํŒŒ์†**๋ฉ๋‹ˆ๋‹ค. > ๊ทœ์น™: `$HOME`์€ **์…ธ์ด ์ „๊ฐœํ•  ๋•Œ๋งŒ**(= ๋น„์ธ์šฉ heredoc ์•ˆ์—์„œ๋งŒ) ํ—ˆ์šฉ. NATS๊ฐ€ ํ•ด์„ํ•ด์•ผ ํ•˜๋Š” ๋ณ€์ˆ˜(`password: $MAM_BROKER_PASS`)๋Š” **๋”ฐ์˜ดํ‘œ๋ฅผ ์”Œ์šฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ### 1.2 D-2 โ€” `nats:latest`๋Š” scratch ๋ณ€ํ˜•์ด๋ผ healthcheck๋ฅผ ๋„ฃ์„ ์ˆ˜ ์—†๋‹ค (P1) `docker-library/official-images`์˜ `library/nats`: `SharedTags: 2.14.5, 2.14, 2, latest` @ `Directory: 2.14.x/scratch`. ํ•ด๋‹น Dockerfile์€ `FROM scratch` + `ENTRYPOINT ["/nats-server"]`. โ†’ ์…ธยทwgetยทcurl ๋ถ€์žฌ๋กœ **healthcheck ๊ตฌํ˜„ ๋ถˆ๊ฐ€**, ๊ฒŒ๋‹ค๊ฐ€ ๋ฉ”์ด์ € ๊ฒฝ๊ณ„๋ฅผ ๋„˜๋‚˜๋“œ๋Š” ๋ถ€๋™ ํƒœ๊ทธ. **๊ต์ •**: `image: nats:2.12-alpine`. alpine ์—”ํŠธ๋ฆฌํฌ์ธํŠธ๊ฐ€ ์ฒซ ์ธ์ž `-` ๊ฐ์ง€ ์‹œ `nats-server`๋ฅผ ์ž๋™ prependํ•˜๋ฏ€๋กœ `command: ["-c", ...]` ๋ผ์ธ์€ **์–‘์ชฝ ๋ณ€ํ˜•์—์„œ ๋™์ผ ๋™์ž‘**(์‹ค์ธก): ```sh if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then set -- nats-server "$@"; fi ``` ### 1.3 D-3 โ€” ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง ํฌํŠธ๋ฅผ ์ „ ์ธํ„ฐํŽ˜์ด์Šค์— ๊ฒŒ์‹œ (P1) ํ˜„ํ–‰ `PRIVATE_SERVER.md:120-124`๊ฐ€ `"8222:8222"`, `"8080:8080"`์„ 0.0.0.0์— ๊ฒŒ์‹œํ•ฉ๋‹ˆ๋‹ค. NATS ๊ณต์‹ ๋ฌธ์„œ: *"The monitoring port is unauthenticated by default."* โ†’ `/varz`ยท`/connz`ยท`/jsz`ยท`/routez` ๊ณต๊ฐœ. 8080์€ `no_tls: true` ํ‰๋ฌธ. ### 1.4 D-4 โ€” TLS ์‚ฌ์šฉ ์‹œ ํ˜ธ์ŠคํŠธ๋ช… ๊ฒ€์ฆ์ด ๊ฐ•์ œ๋œ๋‹ค (P1) `make_client()`๋Š” `tls_set(...)`๋งŒ ํ˜ธ์ถœํ•˜๊ณ  `tls_insecure_set()`์„ ๋ถ€๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ธก(paho 2.1.0): `check_hostname=True`, `verify_mode=CERT_REQUIRED`, `_tls_insecure=False`, ์šฐํšŒ env **์—†์Œ**. | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ฒฐ๊ณผ | |---|---| | **A)** IP ํ˜ธ์ŠคํŠธ + ์ •ํ™•ํ•œ CA ๋ฒˆ๋“ค ํ•€ | `IP address mismatch, certificate is not valid for '127.0.0.1'` | | **B)** ์‚ฌ์„ค CA + `MQTT_CA_CERTS` ๋ฏธ์„ค์ • | `self signed certificate` | | **C)** `MQTT_TLS=0`์œผ๋กœ TLS ํฌํŠธ ์ ‘์† | **`CONNECTED (handshake ok)`** โ† ์†Œ์ผ“๋งŒ ์—ด๋ฆผ | โ†’ (1) TLS ์‹œ `MQTT_BROKER`๋Š” **์ธ์ฆ์„œ SAN์˜ DNS ์ด๋ฆ„** ํ•„์ˆ˜(IP ๊ธˆ์ง€). (2) ์‚ฌ์„ค CA๋ฉด `MQTT_CA_CERTS` ํ•„์ˆ˜, Let's Encrypt๋ฉด **๋น„์›Œ ๋‘˜ ๊ฒƒ**. (3) **์†Œ์ผ“ ์—ฐ๊ฒฐ ์„ฑ๊ณต์€ ๋ธŒ๋กœ์ปค ์ •์ƒ์˜ ์ฆ๊ฑฐ๊ฐ€ ์•„๋‹˜** โ€” ๊ฒ€์ฆ์€ CONNACK ๋˜๋Š” `/healthz`๊นŒ์ง€ ๋„๋‹ฌํ•ด์•ผ ํ•จ. ### 1.5 D-5 โ€” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ ์–‘๋ฐฉํ–ฅ ๋“œ๋ฆฌํ”„ํŠธ (P2) `deploy/install.sh:521-522`๊ฐ€ `MQTT_RETRY_INTERVAL=2`, `MQTT_MAX_RETRIES=5`๋ฅผ `.mam.env`์— **ํ™œ์„ฑ ๊ธฐ๋ณธ๊ฐ’**์œผ๋กœ ๊ธฐ๋กํ•˜์ง€๋งŒ **์ฝ๋Š” ์ฝ”๋“œ 0๊ฑด**. ์‹ค์ œ ์žฌ์‹œ๋„๋Š” `--attempts`(๊ธฐ๋ณธ 3) + `with_retry(base_delay=0.5, factor=2.0, max_delay=8.0)`. ์—ญ์œผ๋กœ ์ฝ”๋“œ๊ฐ€ ์ฝ๋Š” `MQTT_KEEPALIVE`(๊ธฐ๋ณธ 60)๋Š” `.mam.env.example`์— **0๊ฑด**. ### 1.6 H-1 โ€” freeze ๊ฒฝ๋กœ์˜ ์กฐ์šฉํ•œ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ํšŒ๊ท€ (P1) `_load_dotenv()`๋Š” `__file__`์—์„œ ์œ„๋กœ ์˜ฌ๋ผ๊ฐ€๋‹ค `.agents` ๋˜๋Š” `.git`์—์„œ ๋ฉˆ์ถฅ๋‹ˆ๋‹ค. freeze ์Šค๋ƒ…์ƒท ๋ฃจํŠธ๋Š” `.agents`๋งŒ ๋‹ด๊ณ  `.mam.env`๋Š” ์—†์Šต๋‹ˆ๋‹ค(์‹ค์ธก: `ls` ๊ฒฐ๊ณผ `.agents` ๋‹จ ํ•˜๋‚˜). | # | ์กฐ๊ฑด | ํ•ด์„๋œ ๋ธŒ๋กœ์ปค | |---|---|---| | 1 | ์ €์žฅ์†Œ ๊ฒฝ๋กœ ์Šคํฌ๋ฆฝํŠธ, `MAM_ENV_FILE` ์—†์Œ | `nats.example.internal:8883 tls=True` โœ… | | 2 | **freeze ๊ฒฝ๋กœ, `MAM_ENV_FILE` ์—†์Œ** | **`broker.hivemq.com:1883 tls=False`** โŒ | | 3 | freeze ๊ฒฝ๋กœ + `MAM_ENV_FILE` | `nats.example.internal:8883 tls=True` โœ… | ์ •์ƒ ๋ฃจํ”„๋Š” `run_loop.sh:106`์˜ `export MAM_ENV_FILE=...`์œผ๋กœ ๋ณดํ˜ธ๋˜๋‚˜, **์œ„์ž„ ๋ธŒ๋ฆฌํ”„๊ฐ€ ๋ฐฐํฌํ•˜๋Š” ๋ช…๋ น์ค„์€ freeze ๊ฒฝ๋กœ๋ฅผ ์ง์ ‘ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค.** ### 1.7 H-2 โ€” ์žก ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ธŒ๋กœ์ปค๋ฅผ ํ•€ ๊ณ ์ • (P1) `registry.py:73-74`๊ฐ€ ๋“ฑ๋ก ์‹œ์  ๋ธŒ๋กœ์ปค ๋ธ”๋ก์„ ์Šค๋ƒ…์ƒทํ•˜๊ณ  `broker_config_from_job()`์ด env๋ณด๋‹ค **์šฐ์„ ** ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. โ†’ `.mam.env` ๊ต์ฒด๋งŒ์œผ๋กœ๋Š” ๊ธฐ์กด pending/running ์žก์ด ์ „ํ™˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ### 1.8 H-3 โ€” ๋น„๋ฐ€๋ฒˆํ˜ธ ํ‰๋ฌธ ๋ณด๊ด€ / ์ž๋™ ํ† ํฐ ๋ฐœ๊ธ‰ ์ด๋“ (P2) ๋ ˆ์ฝ”๋“œ์— `"password": "SUPERSECRET123"` ํ‰๋ฌธ ํ™•์ธ(๋ชจ๋“œ 0600, `.gitignore:14`์— `.mam/`). ๋™์‹œ์— **`tls` ๋˜๋Š” `username` ๊ฐ์ง€ ์‹œ `auth_token` ์ž๋™ ๋ฐœ๊ธ‰** ํ™•์ธ โ†’ ์›๊ฒฉ ์ธ์ฆ ์ „ํ™˜์ด ๊ณง HMAC ์ž๋™ ํ™œ์„ฑํ™”์ด๋ฉฐ `B-16`/`G-11` ์œ„ํ—˜์„ ๋Œ€๋ถ€๋ถ„ ๋ถ€์ˆ˜ ํ•ด์†Œ. ### 1.9 ๐Ÿ†• **N-1 โ€” retained ๋ฉ”์‹œ์ง€๋Š” MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ ์ „๋‹ฌ๋œ๋‹ค** (P1, C1 ํŒŒ์ƒ ์‹ ๊ทœ ์‹ค์ธก) ์ฑŒ๋ฆฐ์ €์˜ C1์€ ๊ณ„์ • ๊ฒฝ๊ณ„๋งŒ ๋‹ค๋ค˜์œผ๋‚˜, **๊ณ„์ • ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ’€๋“  ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๋” ๊ทผ๋ณธ์ ์ธ ๊ฒฝ๊ณ„**๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. `server/mqtt.go` ์‹ค์ธก โ€” retained ์ „๋‹ฌ์€ **MQTT SUBSCRIBE ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ์—์„œ๋งŒ** ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค: ```go case mqttPacketSub: // โ† MQTT SUBSCRIBE ํŒจํ‚ท ์ฒ˜๋ฆฌ ... c.mqttEnqueueSubAck(pi, filters) c.mqttSendRetainedMsgsToNewSubs(subs) // โ† ์—ฌ๊ธฐ์„œ๋งŒ ํ˜ธ์ถœ func (c *client) mqttSendRetainedMsgsToNewSubs(subs []*subscription) { for _, sub := range subs { if sub.mqtt != nil && sub.mqtt.prm != nil { ... } // โ† MQTT ๊ตฌ๋…์—๋งŒ ์กด์žฌํ•˜๋Š” ํ•„๋“œ } } ``` **๊ฒฐ๋ก **: NATS ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ๋…์ž์™€ WebSocket(NATS) ๊ตฌ๋…์ž๋Š” **retained ๋ฉ”์‹œ์ง€๋ฅผ ์ ˆ๋Œ€ ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.** ๊ณ„์ •์„ ํ•ฉ์น˜๋“ (Option B), export/import๋ฅผ ๊ฑธ๋“ (Option A) ์ด ์‚ฌ์‹ค์€ ๋ณ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. **MAM์— ์ฃผ๋Š” ๊ตฌ์ฒด์  ์˜๋ฏธ**: - `publish_event.py`๋Š” `retain = args.retained or args.event in TERMINAL_EVENTS` โ€” ์ฆ‰ **์ข…๋ฃŒ ์ด๋ฒคํŠธ๊ฐ€ ์ •ํ™•ํžˆ retained ๋Œ€์ƒ**์ž…๋‹ˆ๋‹ค. - ์žก์ด ๋๋‚œ **๋’ค์—** ์ ‘์†ํ•œ NATS/WebSocket ๋Œ€์‹œ๋ณด๋“œ๋Š” **๊ทธ ์žก์˜ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.** ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. - `PRIVATE_SERVER.md` ยง5.2์˜ *"์ฆ‰์‹œ ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ "* ์ฃผ์žฅ์€ **๋ผ์ด๋ธŒ ๊ตฌ๊ฐ„์— ํ•œ์ •**ํ•ด์•ผ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. **๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์‚ฌํ›„ ์ƒํƒœ๊นŒ์ง€ ์•Œ์•„์•ผ ํ•œ๋‹ค๋ฉด ์„ ํƒ์ง€๋Š” 2๊ฐœ๋ฟ**: 1. ๋Œ€์‹œ๋ณด๋“œ๋ฅผ **MQTT๋กœ** ๋ถ™์ธ๋‹ค(๊ฐ™์€ `nats-server`์˜ 1883 ๋ฆฌ์Šค๋„ˆ ์‚ฌ์šฉ, retained ๊ทธ๋Œ€๋กœ ์ˆ˜์‹ ). 2. ยง5.3์˜ **JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ์˜ตํŠธ์ธ**ํ•œ๋‹ค(`python.mqtt.jobs.>` ๊ตฌ๋… ์ŠคํŠธ๋ฆผ + `max_age`/`max_bytes` ์ƒํ•œ ํ•„์ˆ˜). ์ด ๋‘ ๊ฐˆ๋ž˜๋ฅผ ยง5.2 ๊ฐœ์ •์•ˆ๊ณผ ยงA-1 ์ฃผ์„์— ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. ### 1.10 ๐Ÿ†• **H-4 โ€” `MAM_ENV_FILE`์ด ์—†๋Š” ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚ค๋ฉด ๋ชจ๋“  ํด๋ฐฑ์ด ๋ฌด๋ ฅํ™”๋œ๋‹ค** (P1, C2 ํŒŒ์ƒ ์‹ ๊ทœ ์‹ค์ธก) `_load_dotenv()` ๋„์ž…๋ถ€: ```python explicit_file = os.environ.get("MAM_ENV_FILE") if explicit_file: if os.path.isfile(explicit_file): _parse_env_file(explicit_file) return # โ† ํŒŒ์ผ์ด ์—†์–ด๋„ ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒ. ๋‹ค๋ฅธ ํ›„๋ณด๋ฅผ ์‹œ๋„ํ•˜์ง€ ์•Š์Œ ``` ์‹ค์ธก: | ์กฐ๊ฑด | ํ•ด์„๋œ ๋ธŒ๋กœ์ปค | |---|---| | `MAM_ENV_FILE=<์˜คํƒ€/์ด๋™๋œ ๊ฒฝ๋กœ>`, ์ €์žฅ์†Œ ๊ฒฝ๋กœ ์Šคํฌ๋ฆฝํŠธ | **`broker.hivemq.com tls=False`** โŒ | | `MAM_ENV_FILE=<์ •์ƒ ๊ฒฝ๋กœ>` (๋Œ€์กฐ๊ตฐ) | `nats.private.internal tls=True` โœ… | | freeze ๊ฒฝ๋กœ ์Šคํฌ๋ฆฝํŠธ, cwd = ์‹ค์ œ ์ €์žฅ์†Œ, env ์—†์Œ | **`broker.hivemq.com tls=False`** โŒ | **์ค‘์š”**: ์ฑŒ๋ฆฐ์ €๊ฐ€ ์ œ์•ˆํ•œ ์šฐ์„ ์ˆœ์œ„ ์žฌ๋ฐฐ์—ด(`MAM_ENV_FILE โ†’ MAM_REAL_ROOT โ†’ โ€ฆ`)์€ **์ด ๊ฒฝ๋กœ๋ฅผ ๊ณ ์น˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.** 1์ˆœ์œ„์—์„œ ์ด๋ฏธ `return`์œผ๋กœ ํƒˆ์ถœํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทผ๋ณธ ๊ฒฐํ•จ์€ ์ˆœ์„œ๊ฐ€ ์•„๋‹ˆ๋ผ **๋‹จ์ผ ํ›„๋ณด ํ•ด์„**์ž…๋‹ˆ๋‹ค(ยง8 `B-17` ์žฌ์ •์˜). ์„ธ ๋ฒˆ์งธ ํ–‰์€ ์ฑŒ๋ฆฐ์ €์˜ `os.getcwd()` ๋„์ž… ๊ทผ๊ฑฐ๊ฐ€ **์‹ค์ธก์œผ๋กœ ํƒ€๋‹นํ•จ**์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. --- ## 2. ยงA โ€” `PRIVATE_SERVER.md` ๊ฐœ์ •์•ˆ: ยง9 ใ€Œ์›๊ฒฉ ์„œ๋ฒ„ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌใ€ ์‹ ์„ค ### A-1. ํ”„๋กœ๋•์…˜ `nats.conf` (**Rev.2 โ€” ๊ด€์ธก์ž ์‚ฌ์šฉ์ž ์ถ”๊ฐ€**) ```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 ํ•œ์ • (ยงA-3) mqtt { port: 1883 # ๊ณต๊ฐœ TLS ๋ชจ๋ธ์—์„œ๋Š” 8883 + tls ๋ธ”๋ก (ยงB-3) ack_wait: 60s # WAN RTT ํก์ˆ˜ (๊ธฐ๋ณธ 30s) max_ack_pending: 1024 # ๊ธฐ๋ณธ 100 โ†’ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ๋™์‹œ ๋ฐœํ–‰ ์—ฌ์œ  } websocket { port: 8080 no_tls: true # ์‚ฌ์„ค๋ง/tailnet ํ•œ์ • } # โ”€โ”€ ์ธ์ฆ ๋ฐ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ (Rev.2: C1 ๋ฐ˜์˜) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 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..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๊ฑด**์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด๋Š” `PRIVATE_SERVER.md:230` ยง5.5๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฐฉํ•œ ๋ฐฐ์น˜("MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜")์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋กœ ๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ์ด๋ผ ๊ณ„์ •์„ ๋ฐ˜๋“œ์‹œ ๊ฐˆ๋ผ์•ผ ํ•˜๋Š” ์˜ˆ์™ธ ์ƒํ™ฉ์€ **๋ถ€๋ก X(Option A)** ๋ฅผ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค. > [!WARNING] > **๊ด€์ธก์ž๊ฐ€ ๋ฐ›๋Š” ๊ฒƒ๊ณผ ๋ฐ›์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ (ยง1.9 N-1)** > - โœ… ์žก ์‹คํ–‰ **์ค‘** ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์ด๋ฒคํŠธ (๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ) > - โŒ **์ด๋ฏธ ๋๋‚œ ์žก์˜ retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ** โ€” retained ๋Š” **MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ** ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. NATS/WebSocket ๋Œ€์‹œ๋ณด๋“œ๋Š” ์ ‘์† ์ด์ „ ์ƒํƒœ๋ฅผ ์žฌ๊ตฌ์„ฑํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. > - ์‚ฌํ›„ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๋Œ€์‹œ๋ณด๋“œ๋ฅผ **MQTT(1883)๋กœ** ๋ถ™์ด๊ฑฐ๋‚˜ ยง5.3์˜ **JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ์˜ตํŠธ์ธ**ํ•˜์‹ญ์‹œ์˜ค. > [!NOTE] > **๊ณ„์ •๋ณ„ JetStream ํ™œ์„ฑํ™”๋Š” ํ•„์ˆ˜**์ž…๋‹ˆ๋‹ค. MQTT๋Š” ์ ‘์† ๊ณ„์ • ์•ˆ์— `$MQTT_sess`ยท`$MQTT_rmsgs`ยท`$MQTT_out` ์ŠคํŠธ๋ฆผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๊ณ„์ •์— JetStream์ด ์—†์œผ๋ฉด `JetStream not enabled for account`(ErrCode 10039, HTTP 503)๋กœ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์ „์—ญ ์š”๊ฑด๋„ ๋ณ„๋„๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค โ€” `mqtt requires JetStream to be enabled if running in standalone mode` (`mqtt.go:234`). > *๋‹ค๊ณ„์ • ๊ตฌ์„ฑ์—์„œ์˜ ๊ณ„์ •๋ณ„ ์š”๊ตฌ๋Š” ์ŠคํŠธ๋ฆผ ์ƒ์„ฑ ๊ฒฝ๋กœ๋กœ๋ถ€ํ„ฐ์˜ ์ถ”๋ก ์ด๋ฉฐ, ์œ„ ์ฒ˜๋ฐฉ์€ fail-safe์ž…๋‹ˆ๋‹ค. **M2b ์ŠคํŒŒ์ดํฌ R-4์—์„œ ํ™•์ธ ํ•ญ๋ชฉ์œผ๋กœ ์ง€์ •**ํ•ฉ๋‹ˆ๋‹ค.* ### A-2. ํ”„๋กœ๋•์…˜ `docker-compose.yml` ```yaml 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" - "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 ํฌํŠธ ์ž์ฒด์— ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๋ฅผ ๋ช…์‹œ**ํ•˜๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. ### A-3. ํฌํŠธ ๋…ธ์ถœ ๋งคํŠธ๋ฆญ์Šค | ํฌํŠธ | ์šฉ๋„ | Tailscale ๋ชจ๋ธ (๊ถŒ์žฅ) | ๊ณต๊ฐœ TLS ๋ชจ๋ธ | ์ ˆ๋Œ€ ๊ธˆ์ง€ | |---|---|---|---|---| | 1883 | MQTT ํ‰๋ฌธ | tailnet IP ๋ฐ”์ธ๋“œ | โœ– ๋ฏธ๊ฒŒ์‹œ | 0.0.0.0 ๊ฒŒ์‹œ | | 8883 | MQTT TLS | (๋ถˆํ•„์š”) | `0.0.0.0` + LE ์ธ์ฆ์„œ | ์ธ์ฆ ์—†์ด ๊ฒŒ์‹œ | | 4222 | NATS ๋„ค์ดํ‹ฐ๋ธŒ | tailnet IP ๋ฐ”์ธ๋“œ | ๋ฏธ๊ฒŒ์‹œ(๋˜๋Š” TLS+์ธ์ฆ) | 0.0.0.0 ํ‰๋ฌธ | | 8222 | HTTP ๋ชจ๋‹ˆํ„ฐ๋ง | **`127.0.0.1` ํ•œ์ •** | **`127.0.0.1` ํ•œ์ •** | ์–ด๋–ค ๊ฒฝ์šฐ์—๋„ ๊ณต๊ฐœ | | 8080 | WebSocket(`no_tls`) | tailnet IP ๋ฐ”์ธ๋“œ | ๋ฏธ๊ฒŒ์‹œ | ๊ณต๊ฐœ ์ธํ„ฐํŽ˜์ด์Šค ๊ฒŒ์‹œ | ### A-4. ๐Ÿ†• `PRIVATE_SERVER.md` ยง5.2 ๊ฐœ์ • (N-1 ๋ฐ˜์˜) ๊ธฐ์กด ยง5.2 ๋ฌธ์žฅ *"์›น ๋ธŒ๋ผ์šฐ์ €๋‚˜ ํƒ€ ํ”„๋กœ์ ํŠธ์˜ NATS ๊ตฌ๋…์ž๋Š” โ€ฆ ์ฆ‰์‹œ ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค"* ์— ๋‹ค์Œ ๊ฒฝ๊ณ„๋ฅผ ๋ณ‘๊ธฐํ•ฉ๋‹ˆ๋‹ค. ```markdown - **๊ฒฝ๊ณ„ (ํ•„์ˆ˜ ์ธ์ง€)**: ๊ต์ฐจ ํ”„๋กœํ† ์ฝœ ๋ธŒ๋ฆฌ์ง•์€ **๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ์— ํ•œ์ •**๋ฉ๋‹ˆ๋‹ค. MQTT์˜ retained ๋ฉ”์‹œ์ง€๋Š” MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ ์ „๋‹ฌ๋˜๋ฏ€๋กœ(`mqttSendRetainedMsgsToNewSubs`๊ฐ€ MQTT SUBSCRIBE ๊ฒฝ๋กœ ์ „์šฉ), ์žก์ด ๋๋‚œ ๋’ค ์ ‘์†ํ•œ NATS/WebSocket ๋Œ€์‹œ๋ณด๋“œ๋Š” ๊ทธ ์žก์˜ **์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค**. ์‚ฌํ›„ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด (a) ๋Œ€์‹œ๋ณด๋“œ๋ฅผ MQTT(1883)๋กœ ์—ฐ๊ฒฐํ•˜๊ฑฐ๋‚˜ (b) ยง5.3 JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ์˜ตํŠธ์ธํ•˜์‹ญ์‹œ์˜ค. - **๊ณ„์ • ๋ฐฐ์น˜**: ๊ด€์ธก ํด๋ผ์ด์–ธํŠธ๋Š” ยง5.5 ์ฒ˜๋ฐฉ๋Œ€๋กœ `MAM` ๊ณ„์ • ์•ˆ์˜ ์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž (`mam_observer`)๋กœ ์ ‘์†ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณ„์ •์—์„œ๋Š” subject ๊ฐ€ ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ``` --- ## 3. ยงB โ€” ์›๊ฒฉ ๋„คํŠธ์›Œํ‚น & ๋ณด์•ˆ ๊ฐ€์ด๋“œ ### B-1. ๋…ธ์ถœ ๋ชจ๋ธ 3์•ˆ ๋น„๊ต ๋ฐ ๊ถŒ๊ณ  | ํ•ญ๋ชฉ | ๐Ÿ† **๋ชจ๋ธ T: Tailscale/WireGuard ์˜ค๋ฒ„๋ ˆ์ด** | ๋ชจ๋ธ P: ๊ณต๊ฐœ TLS (Let's Encrypt) | ๋ชจ๋ธ S: SSH ํ„ฐ๋„ | |---|---|---|---| | ์ธํ„ฐ๋„ท ๋…ธ์ถœ ๋ฉด์  | **0** | 8883 1๊ฐœ | 0 | | ์ธ์ฆ์„œ ํ•„์š” | ๋ถˆํ•„์š” (`MQTT_TLS=0`) | ํ•„์ˆ˜ + 90์ผ ๊ฐฑ์‹  | ๋ถˆํ•„์š” | | **D-4 ํ˜ธ์ŠคํŠธ๋ช… ์ œ์•ฝ** | **ํ•ด๋‹น ์—†์Œ** | ๋„๋ฉ”์ธ ํ•„์ˆ˜, IP ๋ถˆ๊ฐ€ | ํ•ด๋‹น ์—†์Œ | | ๋„๋ฉ”์ธ ํ•„์š” | ๋ถˆํ•„์š” | **ํ•„์ˆ˜** | ๋ถˆํ•„์š” | | ์ด๋™์„ฑ | ์ž๋™ | ์ž๋™ | ํ„ฐ๋„ ์ˆ˜๋™ ๊ด€๋ฆฌ | | ์žฅ์•  ์ง€์  | tailnet ์ฝ”๋””๋„ค์ดํ„ฐ | certbot ๊ฐฑ์‹  ์‹คํŒจ | SSH ์„ธ์…˜ | **๊ถŒ๊ณ : ๋ชจ๋ธ T.** ๊ทผ๊ฑฐ โ€” (1) D-4์˜ ๋„๋ฉ”์ธยทSAN ์ œ์•ฝ์„ ์†Œ๊ฑฐ, (2) D-3์˜ ๋ชจ๋‹ˆํ„ฐ๋ง/WS ๋…ธ์ถœ์„ ๊ตฌ์กฐ์ ์œผ๋กœ ์ œ๊ฑฐ, (3) ๋กค๋ฐฑ์ด `.mam.env` ํ•œ ์ค„, (4) MAM์€ **๊ด€์ธก ์‚ฌ์ด๋“œ์นด**์ด๋ฏ€๋กœ(์ œ์–ด ํ‰๋ฉด์€ `wait_for_job` ํŒŒ์ผ ํด๋ง) ์˜ค๋ฒ„๋ ˆ์ด ์ง€์—ฐ์ด ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ์ •ํ™•์„ฑ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ. ### B-2. ๋ฐฉํ™”๋ฒฝ (UFW) โ€” 2์ฐจ ๋ฐฉ์–ด์„  ```bash sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp # ๋ชจ๋ธ T: 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 # ๋ชจ๋ธ P: 8883๋งŒ ๊ณต๊ฐœ # sudo ufw allow 8883/tcp # sudo ufw allow 80/tcp # certbot HTTP-01 ์ฑŒ๋ฆฐ์ง€ ๊ธฐ๊ฐ„ ํ•œ์ • sudo ufw enable && sudo ufw status verbose ``` **Docker ์šฐํšŒ ๋Œ€์‘(ํ•„์ˆ˜)** โ€” compose ์˜† `.env`์— ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๋ฅผ ์ฃผ์ž…ํ•˜๊ณ  ์‹ค์ œ ๋ฐ”์ธ๋”ฉ์„ ๋‹จ์–ธํ•ฉ๋‹ˆ๋‹ค: ```bash MQTT_BIND=100.x.y.z # tailscale ip -4 WS_BIND=100.x.y.z ``` ```bash sudo ss -lntp | grep -E ':(1883|4222|8222|8080)\b' # ๊ธฐ๋Œ€: 8222 ๋Š” 127.0.0.1 ์—๋งŒ, 1883/8080 ์€ tailnet IP ์—๋งŒ ``` ### B-3. ๋ชจ๋ธ P ์ „์šฉ โ€” TLS / Certbot ```bash sudo certbot certonly --standalone -d mam-broker.example.com # nats.conf ์˜ mqtt ๋ธ”๋ก: # mqtt { # port: 8883 # tls { # cert_file: "/etc/letsencrypt/live/mam-broker.example.com/fullchain.pem" # key_file: "/etc/letsencrypt/live/mam-broker.example.com/privkey.pem" # } # } # compose ๋ณผ๋ฅจ: live/ ๋Š” archive/ ๋กœ์˜ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ โ†’ /etc/letsencrypt ์ „์ฒด๋ฅผ ๋งˆ์šดํŠธ # - /etc/letsencrypt:/etc/letsencrypt:ro sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nats.sh >/dev/null <<'SH' #!/bin/sh docker compose -f /srv/mam-nats/docker-compose.yml kill -s HUP nats SH sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nats.sh sudo certbot renew --dry-run ``` **ํด๋ผ์ด์–ธํŠธ ๊ทœ์น™ (D-4)**: `MQTT_BROKER`๋Š” **๋„๋ฉ”์ธ**(IP ๊ธˆ์ง€). Let's Encrypt ์‚ฌ์šฉ ์‹œ `MQTT_CA_CERTS`๋Š” **์„ค์ •ํ•˜์ง€ ์•Š์Œ**. ์‚ฌ์„ค CA์ผ ๋•Œ๋งŒ ์ง€์ •ํ•˜๊ณ  SAN์— ์ ‘์†๋ช…์„ ๋ฐ˜๋“œ์‹œ ํฌํ•จ. ### B-4. ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ์‹œํฌ๋ฆฟ ์ฃผ์ž… ```bash openssl rand -base64 32 # ๊ณ„์ •/์‚ฌ์šฉ์ž๋ณ„๋กœ ๊ฐ๊ฐ ์ƒ์„ฑ (mam_agent, mam_observer, home, sys) chmod 600 .env ``` NATS ํŒŒ์„œ๋Š” ๋ฏธํ•ด๊ฒฐ `$VAR`๋ฅผ **์—๋Ÿฌ**๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์‹œํฌ๋ฆฟ์ด ๋น„๋ฉด ์„œ๋ฒ„๊ฐ€ ์กฐ์šฉํžˆ ์ต๋ช…์œผ๋กœ ๋œจ์ง€ ์•Š๊ณ  **๊ธฐ๋™์— ์‹คํŒจ**ํ•ฉ๋‹ˆ๋‹ค(fail-closed, ์˜๋„์  ์ฑ„ํƒ). **H-3 ์™„ํ™” ๊ทœ์น™**: (1) `MQTT_PASSWORD`๋Š” ์‚ฌ๋žŒ์ด ์žฌ์‚ฌ์šฉํ•˜๋Š” ์•”ํ˜ธ๊ฐ€ ์•„๋‹ˆ๋ผ **๊ธฐ๊ณ„ ์ƒ์„ฑ ํ† ํฐ**๋งŒ ์‚ฌ์šฉ(๋ ˆ์ฝ”๋“œ์— ํ‰๋ฌธ์œผ๋กœ ๋‚จ์Œ). (2) ํšŒ์ „ ์‹œ ์„œ๋ฒ„ `.env` โ†’ `docker compose up -d` โ†’ ํด๋ผ์ด์–ธํŠธ `.mam.env` โ†’ **์ž”์—ฌ ์žก ๋ ˆ์ฝ”๋“œ ์ •๋ฆฌ**(ยงC-3๊ณผ ๋™์ผ ์ ˆ์ฐจ). (3) ์žฅ๊ธฐ ๊ณผ์ œ๋Š” `B-18`. **๊ถŒํ•œ ๊ฒฉ๋ฆฌ**: `mam_agent`๋Š” ๋ฐœํ–‰/๊ตฌ๋…, `mam_observer`๋Š” `publish: { deny: [">"] }`๋กœ **๋ฐœํ–‰ ์ „๋ฉด ๊ธˆ์ง€**. ์ด๋Š” A-2(์™ธ๋ถ€ ์•…์˜์  ์ด๋ฒคํŠธ ์ฃผ์ž…) ๋Œ€์‘๊ณผ ๊ฐ™์€ ๋ฐฉํ–ฅ์ด๋ฉฐ, HMAC(`auth_token`)๊ณผ ์ด์ค‘ ๋ฐฉ์–ด๋ฅผ ์ด๋ฃน๋‹ˆ๋‹ค. --- ## 4. ยงC โ€” ํด๋ผ์ด์–ธํŠธ ์„ค์ • ๋ฐ ์›๊ฒฉ ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ ### C-1. `.mam.env` (๋ชจ๋ธ๋ณ„) ```bash # โ”€โ”€ ๋ชจ๋ธ 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=<๊ธฐ๊ณ„ ์ƒ์„ฑ ํ† ํฐ> MQTT_KEEPALIVE=60 # D-5: ์ฝ”๋“œ๊ฐ€ ์‹ค์ œ๋กœ ์ฝ๋Š” ๊ฐ’. ํ…œํ”Œ๋ฆฟ์— ์ถ”๊ฐ€ ํ•„์š” # โ”€โ”€ ๋ชจ๋ธ P (๊ณต๊ฐœ TLS) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # MQTT_BROKER="mam-broker.example.com" # D-4: ์ธ์ฆ์„œ SAN ์˜ DNS ์ด๋ฆ„. IP ๊ธˆ์ง€ # MQTT_PORT=8883 # MQTT_TLS=1 # MQTT_CA_CERTS ๋Š” Let's Encrypt ์‚ฌ์šฉ ์‹œ '์„ค์ •ํ•˜์ง€ ์•Š์Œ' ``` > **D-5 ๊ต์ •**: `MQTT_RETRY_INTERVAL` / `MQTT_MAX_RETRIES`๋Š” ์–ด๋–ค ์ฝ”๋“œ๋„ ์ฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ…œํ”Œ๋ฆฟ์—์„œ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ "๋ฏธ์‚ฌ์šฉ(historical)"๋กœ ๊ฐ•๋“ฑํ•˜๊ณ , ์žฌ์‹œ๋„ ์กฐ์ •์€ `publish_event.py --attempts`์ž„์„ ๋ช…์‹œ. ์—ญ์œผ๋กœ `MQTT_KEEPALIVE`๋Š” ์ถ”๊ฐ€. ### C-2. ์›๊ฒฉ ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ (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` ์—†์ด ์‹คํ–‰ / ๊ทธ๋ฆฌ๊ณ  ์˜คํƒ€ ๊ฒฝ๋กœ๋กœ ์‹คํ–‰ | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ๋‚˜๊ฐ€์ง€ ์•Š์Œ โ€” **ํ˜„์žฌ๋Š” ์–‘์ชฝ ๋‹ค ์‹คํŒจ๊ฐ€ ๊ธฐ๋Œ€๊ฐ’**์ด๋ฉฐ `B-17`์˜ ๊ทผ๊ฑฐ | | **R-8** | ์ „์ฒด ํšŒ๊ท€ ์Šค์œ„ํŠธ | `.venv/bin/python -m pytest tests/ -q` | ํ˜„์žฌ ๊ธฐ์ค€ **290 passed** | | ๐Ÿ†• **R-9** | **๊ด€์ธก์ž ๊ณ„์ • ๊ฒฝ๊ณ„** | `mam_observer`๋กœ NATS ๊ตฌ๋… โ†’ ์ด๋ฒคํŠธ ์ˆ˜์‹  ํ™•์ธ. ์ด์–ด์„œ `home`(๊ณ„์ • HOME)์œผ๋กœ ๋™์ผ ๊ตฌ๋… | `mam_observer` **์ˆ˜์‹ **, `home` **0๊ฑด** (= ๊ฒฉ๋ฆฌ ์ •์ƒ) | | ๐Ÿ†• **R-10** | **N-1 retained ๊ฒฝ๊ณ„ ํ™•์ธ** | ์žก ์ข…๋ฃŒ **ํ›„** NATS ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ๋…์ž๋ฅผ ์ƒˆ๋กœ ๋ถ™์ž„ | **0๊ฑด ์ˆ˜์‹ **์ด ์ •์ƒ. ์ˆ˜์‹ ๋˜๋ฉด N-1 ์ „์ œ๊ฐ€ ํ‹€๋ฆฐ ๊ฒƒ์ด๋ฏ€๋กœ ยงA-4 ๋ฌธ๊ตฌ ์žฌ์ž‘์„ฑ | > R-9/R-10์€ ์ฑŒ๋ฆฐ์ง€ C1์ด ์ œ๊ธฐํ•œ ๊ณ„์ • ๋ฌธ์ œ์™€, ๊ทธ๋ณด๋‹ค ๊ทผ๋ณธ์ ์ธ retained ๊ฒฝ๊ณ„๋ฅผ **๊ฐ๊ฐ ์‹ค์ฆ**ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ R-10์€ **๋ฐ˜์ฆ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ**๋กœ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. **R-4 ๊ตฌ์ฒด ์ ˆ์ฐจ**: ```bash JID=$(.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \ --registry-dir .mam/jobs \ register --prompt "remote broker connectivity test" --agent-session "herdr:test") .venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py \ --registry-dir .mam/jobs --job "$JID" --event progress --detail "remote broker verified" -v .venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \ --registry-dir .mam/jobs status --job "$JID" --set completed # ์œ ๋ น ์žก ๋ฐฉ์ง€ ``` > `--registry-dir`๋Š” **๋ถ€๋ชจ ํŒŒ์„œ ์ธ์ž**์ด๋ฏ€๋กœ ์„œ๋ธŒ์ปค๋งจ๋“œ ์•ž์— ์˜ต๋‹ˆ๋‹ค. `register`์—๋Š” `--job-id`๊ฐ€ ์—†์–ด ID๋Š” stdout์—์„œ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค(์‹ค์ธก ํ™•์ธ). **์ง€์—ฐ ์ธก์ •**: ```bash .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 ``` **ํŒ์ •**: QoS1 RTT p50 > 200ms๋ฉด `mqtt.ack_wait` ์ƒํ–ฅ, > 1s๋ฉด ์˜ค๋ฒ„๋ ˆ์ด ๊ฒฝ๋กœ(๋ฆด๋ ˆ์ด ํด๋ฐฑ) ์ ๊ฒ€. `with_retry` ๋ฐฑ์˜คํ”„๊ฐ€ 0.5sโ†’1sโ†’2s์ด๋ฏ€๋กœ ์ˆ˜๋ฐฑ ms RTT์—์„œ๋Š” ์žฌ์‹œ๋„ ์—†์ด ํ†ต๊ณผํ•ด์•ผ ์ •์ƒ์ž…๋‹ˆ๋‹ค. ### C-3. ์ „ํ™˜(Cutover) ์ ˆ์ฐจ โ€” H-2 ๋Œ€์‘ ``` [1 ๋“œ๋ ˆ์ธ] โ”€โ”€> [2 ์ž”์—ฌ ์Šค์บ”] โ”€โ”€> [3 .mam.env ๊ต์ฒด] โ”€โ”€> [4 R-1~R-10] โ”€โ”€> [5 ๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ] ``` 1. **๋“œ๋ ˆ์ธ**: ์‹ ๊ทœ ์œ„์ž„ ์ค‘๋‹จ, ์ง„ํ–‰ ์ค‘ ์žก์ด ๋ชจ๋‘ terminal ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ. 2. **์ž”์—ฌ ์Šค์บ”** โ€” ์˜› ๋ธŒ๋กœ์ปค์— ํ•€ ๊ณ ์ •๋œ ๋ ˆ์ฝ”๋“œ ํ™•์ธ: ```bash .venv/bin/python - <<'PY' import json, glob for p in sorted(glob.glob('.mam/jobs/*.json')): d = json.load(open(p)) if d.get('status') not in ('completed', 'error', 'cancelled'): b = d.get('broker') or {} print(f"{d.get('job_id')} status={d.get('status'):<9} broker={b.get('host')}:{b.get('port')}") PY ``` ์ถœ๋ ฅ์ด ๋น„์–ด์•ผ 3๋‹จ๊ณ„ ์ง„์ž…. ๋‚จ์œผ๋ฉด ์ข…๊ฒฐ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ `broker` ๋ธ”๋ก์„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜. 3. `.mam.env` ๊ต์ฒด โ€” **์ฝ”๋“œ ๋ณ€๊ฒฝ 0์ค„.** 4. R-1 ~ R-10 ์ „๊ฑด ํ†ต๊ณผ. 5. ๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ: ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์ฃผ์†Œ๊ฐ€ ํ™œ์„ฑ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋‚จ์ง€ ์•Š๋„๋ก `.mam.env` / `deploy/install.sh` ์ ๊ฒ€. --- ## 5. ยงD โ€” `implementation_plan.md` ๊ฐœ์ •์•ˆ ### D-a. M2 ๋ถ„ํ•  | ๋งˆ์ผ์Šคํ†ค | ์ด๋ฆ„ | DoD | ๊ฒŒ์ดํŠธ | |---|---|---|---| | M0 | ๋ฌธ์„œ ์ •ํ•ฉ์„ฑ | (์™„๋ฃŒ) | (ํ†ต๊ณผ, 290 ์‹ค์ธก) | | M1 | Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ | (์™„๋ฃŒ โ€” `c6b6c77`) | (ํ†ต๊ณผ) | | **M2a** | ๋กœ์ปฌ ์ŠคํŒŒ์ดํฌ (Track 1) | ๊ฒฉ๋ฆฌ ํด๋ก ์—์„œ S-1 ~ S-9 ์™„์ˆ˜ | S-3 retained ํ†ต๊ณผ | | **M2b** | ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ์ „ํ™˜ (Track 1R) | D-1~D-5 ๊ต์ • + ยงA/ยงB ๋ฐฐํฌ + ยงC-3 ์ „ํ™˜ | **R-3 ยท R-5 ยท R-6 ยท R-9 ๋™์‹œ ํ†ต๊ณผ**, R-7ยทR-10 ๊ฒฐ๊ณผ ๊ธฐ๋ก | | M3 | Track 2 ๋ณด์•ˆ | A-2 ์ง€๋ฌธ ํ† ํ”ฝ, G-11 | ์ง€๋ฌธ ํ† ํ”ฝ ํ™•์ธ ํ›„ ๋ ˆ๊ฑฐ์‹œ ๊ตฌ๋… ์ œ๊ฑฐ **+ ๊ด€์ธก์ž ๊ถŒํ•œ/Export subject ๋™์‹œ ๊ฐฑ์‹ ** | | M4 | Track 3 ๋™๊ธฐํ™” | ๋ฌธ์„œ/๋ฐฐํฌ ์ •ํ•ฉ | ์ „์ฒด ์Šค์œ„ํŠธ Green | > **M2b ์ง„์ž… ์„ ํ–‰ ์กฐ๊ฑด**: D-1 ~ D-5 ๊ต์ •์ด `PRIVATE_SERVER.md`์— ๋ฐ˜์˜๋˜๊ณ  ์‹ ๊ทœ ๊ฐ€๋“œ๊ฐ€ ํ†ต๊ณผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ต์ • ์ „ ๋ฐฐํฌ๋Š” D-1(์Šคํ† ๋ฆฌ์ง€ ์œ ์‹ค)ยทD-2(healthcheck ๋ถˆ๊ฐ€)ยทD-3(๋ชจ๋‹ˆํ„ฐ๋ง ๊ณต๊ฐœ)ยทD-4(TLS ์ ‘์† ๋ถˆ๊ฐ€)๋กœ **๋ฐ˜๋“œ์‹œ ์‹คํŒจ**ํ•ฉ๋‹ˆ๋‹ค. > ๐Ÿ†• **M3์— ์ถ”๊ฐ€๋œ ๊ฒฐํ•ฉ ํ•ญ๋ชฉ**: ํ† ํ”ฝ ๋ฃจํŠธ๊ฐ€ `python/mqtt/jobs/โ€ฆ` โ†’ `mam//jobs/โ€ฆ`๋กœ ๋ฐ”๋€Œ๋ฉด ยงA-1์˜ `mam_observer.subscribe.allow`์™€ (Option A ์ฑ„ํƒ ์‹œ) export subject๊ฐ€ **์กฐ์šฉํžˆ ๋งค์นญ ์‹คํŒจ**ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋“œ `G-D9`๊ฐ€ ์ด๋ฅผ ๊ธฐ๊ณ„์ ์œผ๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. ### D-b. Track 1R (์‹ ์„ค) | ํŠธ๋ž™ | ๋Œ€์ƒ | ๋ชฉํ‘œ | ๋ณ€๊ฒฝ ์ง€์  | |---|---|---|---| | **Track 1R** | ์›๊ฒฉ ๋ฐฐํฌ | VPS/ํ™ˆ๋žฉ `nats-server` ์ƒ์‹œ ๊ฐ€๋™ ๋ฐ MAM ์ „ํ™˜ | `PRIVATE_SERVER.md` ยง9/ยง5.2, ์„œ๋ฒ„์ธก `nats.conf`ยท`docker-compose.yml`, `.mam.env` | ### D-c. M2b ๋‹จ๊ณ„๋ณ„ ๋กœ๋“œ๋งต ``` [P0 ๊ต์ •] D-1~D-5 ๋ฌธ์„œ ๊ต์ • + ยง5.2 ๊ฒฝ๊ณ„ ๋ช…๋ฌธํ™” + ์‹ ๊ทœ ๊ฐ€๋“œ 7์ข… โ–ผ [P1 ์„œ๋ฒ„ ์ค€๋น„] VPS/ํ™ˆ๋žฉ ํ”„๋กœ๋น„์ €๋‹, Docker, Tailscale ๊ฐ€์ž… โ–ผ [P2 ๋ฐฐํฌ] nats.conf + compose ๊ธฐ๋™, healthcheck healthy ํ™•์ธ โ–ผ [P3 ์ž ๊ธˆ] ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ํ•œ์ • + UFW + ss/nmap ๋กœ ๋…ธ์ถœ ๋ฉด์  0 ๋‹จ์–ธ (R-3) โ–ผ [P4 ์ „ํ™˜] ๋“œ๋ ˆ์ธ โ†’ ์ž”์—ฌ ์Šค์บ” โ†’ .mam.env ๊ต์ฒด (ยงC-3) โ–ผ [P5 ๊ฒ€์ฆ] R-1 ~ R-10. R-5(retained) / R-9(๊ณ„์ • ๊ฒฝ๊ณ„) ๋ฅผ ์ตœ์ข… ๊ด€๋ฌธ์œผ๋กœ โ–ผ [P6 ์ƒ์‹œํ™”] ๋กœ๊ทธ ๋กœํ…Œ์ด์…˜, JetStream ๋ณผ๋ฅจ ๋ฐฑ์—…, healthcheck ์•Œ๋ฆผ ``` **P6 ์ตœ์†Œ ์š”๊ฑด**: `nats-data` ๋ณผ๋ฅจ ์ฃผ๊ธฐ ์Šค๋ƒ…์ƒท(**retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊ฐ€ ์—ฌ๊ธฐ ์žˆ์Œ โ€” ๋ณผ๋ฅจ ์œ ์‹ค = ์ด๋ฒคํŠธ ์œ ์‹ค**), healthcheck ์ƒํƒœ ๊ฐ์‹œ, JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ ์‚ฌ์šฉ ์‹œ `max_age`/`max_bytes` ์ƒํ•œ ํ•„์ˆ˜. ### D-d. ์ฒดํฌ๋ฆฌ์ŠคํŠธ ```markdown ### M2b: ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ์ „ํ™˜ (Track 1R) - [ ] D-1 store_dir ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ต์ • + ๋น„์ธ์šฉ heredoc (PRIVATE_SERVER.md:73, :146) - [ ] D-2 ์ด๋ฏธ์ง€ ํ•€ nats:2.12-alpine + /healthz healthcheck - [ ] D-3 8222/8080 ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ํ•œ์ • - [ ] D-4 TLS ์ ˆ์˜ IP ์˜ˆ์‹œ ์ œ๊ฑฐ ๋ฐ DNS/SAN ์š”๊ฑด ๋ช…์‹œ - [ ] D-5 .mam.env.example ์ •ํ•ฉ (MQTT_KEEPALIVE ์ถ”๊ฐ€ / RETRYยทMAX_RETRIES ๊ฐ•๋“ฑ) - [ ] N-1 ยง5.2 ์— retained=MQTT ์ „์šฉ ๊ฒฝ๊ณ„ ๋ช…๋ฌธํ™” + ยงA-1 mam_observer ์ถ”๊ฐ€ - [ ] ์‹ ๊ทœ ๊ฐ€๋“œ G-D5(๊ฐ•ํ™”) ~ G-D9, G-R1, G-R2 ๊ตฌํ˜„ ๋ฐ mutation ํ™•์ธ (290 โ†’ 297) - [ ] ์„œ๋ฒ„ ๋ฐฐํฌ ๋ฐ R-3 ๋…ธ์ถœ ๋ฉด์  0 ๋‹จ์–ธ - [ ] ยงC-3 ๋“œ๋ ˆ์ธยท์ž”์—ฌ ์Šค์บ” ํ›„ .mam.env ์ „ํ™˜ - [ ] R-1 ~ R-10 ์ „๊ฑด ํ†ต๊ณผ (R-5 / R-9 ์ตœ์ข… ๊ด€๋ฌธ) ``` --- ## 6. ํšŒ๊ท€ ๊ฐ€๋“œ (7์ข…) โ€” `tests/test_deploy_freshness.py::test_d7` ๊ณ„๋ณด | ID | ๊ฐ€๋“œ ๋‚ด์šฉ | ๋ณ€์ด ๊ฒ€์ถœ ๊ธฐ์ค€ (Mutation) | |---|---|---| | **G-D5** ๐Ÿ”บ๊ฐ•ํ™” | **2ํ•ญ ๊ฒ€์‚ฌ**: (i) ํŽœ์Šค ๋ธ”๋ก ๋‚ด ๋ชจ๋“  `store_dir:` ๊ฐ’์ด `/`๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ ˆ๋Œ€๊ฒฝ๋กœ, (ii) ํ•ด๋‹น ๊ฐ’์„ ๊ธฐ๋กํ•˜๋Š” heredoc ๊ตฌ๋ถ„์ž๊ฐ€ **๋น„์ธ์šฉ**(`</jobs`๋กœ ๋ฐ”๊พธ๊ณ  ๋ฌธ์„œ๋ฅผ ๊ฐฑ์‹ ํ•˜์ง€ ์•Š์œผ๋ฉด FAIL | | **G-R1** | `run_loop.sh`์— `export MAM_ENV_FILE=` ๋ผ์ธ ์กด์žฌ ๋‹จ์–ธ + `.agents`๋งŒ ์žˆ๊ณ  `.mam.env`๊ฐ€ ์—†๋Š” ์ž„์‹œ ๋ฃจํŠธ์—์„œ์˜ ํšŒ๊ท€ ๋™์ž‘์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ณ ์ • | `run_loop.sh:106` export ์ œ๊ฑฐ ์‹œ FAIL | | **G-R2** | ์ฝ”๋“œ๊ฐ€ ์ฝ๋Š” ๋ชจ๋“  `MQTT_*` ์ด๋ฆ„์ด `.mam.env.example`์— ์กด์žฌ(ํŠนํžˆ `MQTT_KEEPALIVE`)ํ•˜๊ณ , ์ฝ”๋“œ๊ฐ€ ์ฝ์ง€ ์•Š๋Š” ์ด๋ฆ„์€ ํ™œ์„ฑ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๊ธฐ๋ก๋˜์ง€ ์•Š์Œ | `MQTT_KEEPALIVE` ์ œ๊ฑฐ ๋˜๋Š” `MQTT_RETRY_INTERVAL=2` ํ™œ์„ฑ ๋ณต์› ์‹œ FAIL | **๊ตฌํ˜„ ๊ทœ์น™(Rev.1 ์Šน๊ณ„ + C3 ๋ฐ˜์˜ ํ™•์ธ)**: - ๋ฌธ์„œ ๊ฐ€๋“œ๋Š” **ํŽœ์Šค ์ฝ”๋“œ ๋ธ”๋ก์— ํ•œ์ •**ํ•ด ์Šค์บ”ํ•˜๊ณ , ์‚ฐ๋ฌธ errata(์˜ˆ: *"๊ณผ๊ฑฐ์—๋Š” `nats:latest`๋ฅผ ๊ถŒ์žฅํ–ˆ์œผ๋‚˜โ€ฆ"*)๊ฐ€ ์˜คํƒ๋˜์ง€ ์•Š์Œ์„ **๋™๋ฐ˜ ๋‹จ์–ธ**ํ•ฉ๋‹ˆ๋‹ค. โ€” *์ด ๊ทœ์ •์€ Rev.1 ยง6์— ์ด๋ฏธ ์žˆ์—ˆ์œผ๋ฉฐ C3์˜ ์š”์ฒญ๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ์—†์ด ์žฌํ™•์ธํ•ฉ๋‹ˆ๋‹ค.* - G-D9์™€ G-R2๋Š” ์†Œ์Šค์—์„œ ๊ฐ’์„ **์ •์ ์œผ๋กœ ์ˆ˜์ง‘**ํ•ด ๋ฌธ์„œ์™€ ๋Œ€์กฐํ•ฉ๋‹ˆ๋‹ค(๋ธŒ๋กœ์ปค ์ ‘์† ์—†์Œ). **ํ…Œ์ŠคํŠธ ์ˆ˜ ์ „๋ง**: 290 (ํ˜„์žฌ ์‹ค์ธก) โ†’ **297** (G-D5 ๊ฐ•ํ™” + G-D6~G-D9, G-R1, G-R2) โ†’ **298** (M3์˜ G-11). --- ## 7. ๋กค๋ฐฑ ์ „๋žต | ์‹คํŒจ ์ง€์  | ๋กค๋ฐฑ | ๋น„์šฉ | |---|---|---| | R-5 retained ์‹คํŒจ | `.mam.env`์˜ `MQTT_BROKER`๋งŒ `eclipse-mosquitto`๋กœ ๊ต์ฒด | ์ฝ”๋“œ 0์ค„, ์ฆ‰์‹œ | | R-9 ๊ณ„์ • ๊ฒฝ๊ณ„ ์‹คํŒจ | `mam_observer` ๊ถŒํ•œ ๋ธ”๋ก๋งŒ ์ˆ˜์ •, MAM ๋ณธ์ฒด ๋ฌด์˜ํ–ฅ | ์„œ๋ฒ„ ์„ค์ • 1๊ณณ | | ์›๊ฒฉ ๋งํฌ ๋ถˆ์•ˆ์ • | `.mam.env`๋ฅผ ์ง์ „ ๊ฐ’์œผ๋กœ ์›๋ณต | ์ฝ”๋“œ 0์ค„ | | ์„œ๋ฒ„ ์ „์†Œ | Track 0 ๋•๋ถ„์— **๋ฃจํ”„๋Š” ๊ณ„์† ์™„์ฃผ**(๋””์Šคํฌ ํด๋ฐฑ). ๊ด€์ธก๋งŒ ์ผ์‹œ ์ƒ์‹ค | 0 | | JetStream ๋ณผ๋ฅจ ์œ ์‹ค | retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์œ ์‹ค โ†’ ๋ฐฑ์—… ์Šค๋ƒ…์ƒท ๋ณต๊ตฌ | P6 ๋ฐฑ์—… ํ•„์š” | Track 0(`B-14`/`B-15`)์€ ๋ธŒ๋กœ์ปค ์ œํ’ˆยท์œ„์น˜์™€ ๋ฌด๊ด€ํ•œ ์ˆœ์ด๋“์ด๋ฏ€๋กœ ๋กค๋ฐฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. **์›๊ฒฉ ์ „ํ™˜ ์ „์ฒด๊ฐ€ ๊ฐ€์—ญ์ ์ธ ์ด์œ ๊ฐ€ M1 ์™„๋ฃŒ ๋•๋ถ„์ž…๋‹ˆ๋‹ค.** --- ## 8. ํ›„์† ์ฝ”๋“œ ๊ณผ์ œ (Rev.2 ์žฌ์ •์˜) ### B-17 ๐Ÿ”บ์žฌ์ •์˜ โ€” `_load_dotenv` ๋‹จ์ผ ํ›„๋ณด ํ•ด์„ (P1) > **C2 ํŒ์ •**: ์ฑŒ๋ฆฐ์ €์˜ ์šฐ์„ ์ˆœ์œ„ ๋ชฉ๋ก์€ ๋ฐฉํ–ฅ์ด ์˜ณ์œผ๋‚˜, **๊ทผ๋ณธ ๊ฒฐํ•จ์€ ์ˆœ์„œ๊ฐ€ ์•„๋‹ˆ๋ผ "๋ฃจํŠธ๋ฅผ ํ•˜๋‚˜๋งŒ ์ •ํ•˜๊ณ  ๋๋‚ธ๋‹ค"๋Š” ๊ตฌ์กฐ**์ž…๋‹ˆ๋‹ค(ยง1.10 H-4). ์ˆœ์„œ๋งŒ ๋ฐ”๊พธ๋ฉด `MAM_ENV_FILE` ์˜คํƒ€ ๊ฒฝ๋กœ์—์„œ ์—ฌ์ „ํžˆ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค. **์ฒ˜๋ฐฉ โ€” ์ˆœ์„œ ์žˆ๋Š” ํ›„๋ณด ๋ชฉ๋ก + ์ฒซ ์ ์ค‘ ์šฐ์„ (first-hit-wins)**: ``` 1) $MAM_ENV_FILE (ํŒŒ์ผ์ด ์‹ค์ œ๋กœ ์กด์žฌํ•  ๋•Œ๋งŒ ์ฑ„ํƒ. ๋ถ€์žฌ ์‹œ return ํ•˜์ง€ ๋ง๊ณ  ๊ณ„์† ์ง„ํ–‰) โ† H-4 ๊ต์ • 2) $MAM_REAL_ROOT (run_loop.sh:104 ๊ฐ€ export) 3) $WORKSPACE_ROOT (run_loop.sh:105 ๊ฐ€ export) 4) walk_up(__file__) (์ €์žฅ์†Œ ์ง์ ‘ ์‹คํ–‰์—์„œ ์ด๋ฏธ ์ •์ƒ ๋™์ž‘ํ•จ์ด ์‹ค์ธก๋จ) 5) walk_up(os.getcwd()) (freeze ๊ฒฝ๋กœ๋ฅผ ์ˆ˜๋™ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๊ตฌ์ œ โ€” ์ฑŒ๋ฆฐ์ € ๊ทผ๊ฑฐ๊ฐ€ ์‹ค์ธก์œผ๋กœ ํƒ€๋‹น) โ†’ ๊ฐ ํ›„๋ณด์—์„œ .mam.env / .env ๋ฅผ ์ฐพ๊ณ , ์ฒซ ์ ์ค‘์„ ์ฑ„ํƒ โ†’ ์ „๋ถ€ ์‹คํŒจํ•˜๋ฉด ๋ฐ˜๋“œ์‹œ ๊ฒฝ๊ณ  ๋กœ๊ทธ: "no env file found; falling back to public default broker" ``` **Rev.1 ๋Œ€๋น„ / ์ฑŒ๋ฆฐ์ € ์ œ์•ˆ ๋Œ€๋น„ ์ฐจ์ด 3๊ฐ€์ง€**: 1. `MAM_ENV_FILE` ๋ถ€์žฌ ์‹œ **์กฐ๊ธฐ ํƒˆ์ถœ ์ œ๊ฑฐ**(H-4). ์ฑŒ๋ฆฐ์ € ์ œ์•ˆ์œผ๋กœ๋Š” ๊ณ ์ณ์ง€์ง€ ์•Š๋Š” ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค. 2. `walk_up(__file__)`์„ cwd๋ณด๋‹ค **์•ž**์— ๋‘ก๋‹ˆ๋‹ค. ์ €์žฅ์†Œ ์ง์ ‘ ์‹คํ–‰์€ ์ด๋ฏธ ์ •ํ™•ํžˆ ๋™์ž‘ํ•จ์ด ์‹ค์ธก๋˜์—ˆ๊ณ , cwd๋ฅผ ์•ž์„ธ์šฐ๋ฉด **๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์˜ `.mam.env`๋ฅผ ์ฝ๋Š” ๊ต์ฐจ ์˜ค์—ผ** ์œ„ํ—˜๋งŒ ์ปค์ง‘๋‹ˆ๋‹ค(MAM์€ ๋‹ค์ค‘ ํ”„๋กœ์ ํŠธ ์‚ฌ์šฉ์„ ์ „์ œ). 3. cwd๋Š” **`walk_up(os.getcwd())`** ๋กœ ๋‘ก๋‹ˆ๋‹ค. ์ €์žฅ์†Œ ํ•˜์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ์—์„œ ์‹คํ–‰ํ•ด๋„ ๋™์ž‘ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. 4. **์ตœ์ข… ํด๋ฐฑ ๊ฒฝ๊ณ ๋Š” ์ˆœ์„œ์™€ ๋ฌด๊ด€ํ•œ ์•ˆ์ „๋ง**์ด๋ฏ€๋กœ ํ•„์ˆ˜ ์š”๊ฑด์œผ๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์–ด๋–ค ์ˆœ์„œ๋“  ๋†“์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ### B-18 โ€” ์žก ๋ ˆ์ฝ”๋“œ ์ž๊ฒฉ์ฆ๋ช… ํ‰๋ฌธ ๋ณด๊ด€ (P2) `to_registry_block()`์—์„œ `password`๋ฅผ ๋งˆ์Šคํ‚นํ•˜๊ฑฐ๋‚˜ ๋ ˆ์ฝ”๋“œ ๋Œ€์‹  ์‹คํ–‰ ์‹œ์  env์—์„œ๋งŒ ํ•ด์„. `broker_config_from_job()`์˜ override ์šฐ์„ ์ˆœ์œ„ ๊ณ„์•ฝ(H-2์™€ ๋™์ผ ์ง€์ )๊ณผ ํ•จ๊ป˜ ์žฌ๊ฒ€ํ† . --- ## 9. ๋ถ€๋ก X โ€” Option A (๊ณ„์ • ๊ฐ„ Export / Import): **์˜ˆ์™ธ ๊ฒฝ๋กœ** ์ฑŒ๋ฆฐ์ €๊ฐ€ 1์ˆœ์œ„๋กœ ์ œ์•ˆํ•œ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. **๊ธฐ๋ณธ ์ฑ„ํƒํ•˜์ง€ ์•Š์œผ๋ฉฐ**, ๊ด€์ธก์ž๊ฐ€ ์‹ค์ œ๋กœ **๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ**์— ์†ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. **๋ฌธ๋ฒ• ๊ฒ€์ฆ ์™„๋ฃŒ** (์†Œ์Šค ๋Œ€์กฐ): ```conf accounts { MAM: { jetstream: enabled users: [ { user: mam_agent, password: $MAM_BROKER_PASS } ] exports: [ { stream: "python.mqtt.jobs.>", accounts: [HOME] } ] # accounts ๋กœ ์ˆ˜์ž…์ž ์ œํ•œ ๊ถŒ์žฅ } HOME: { users: [ { user: home, password: $HOME_BROKER_PASS } ] imports: [ { stream: { account: MAM, subject: "python.mqtt.jobs.>" } } ] } } ``` - `exports: [ { stream: "..." } ]` / `imports: [ { stream: { account: X, subject: "..." } } ]` ๋ฌธ๋ฒ•์€ ๊ณต์‹ ๋ฌธ์„œ์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. - **`>` ์™€์ผ๋“œ์นด๋“œ๋Š” ์œ ํšจ**ํ•ฉ๋‹ˆ๋‹ค โ€” export subject๋Š” `IsValidSubject`๋กœ ๊ฒ€์ฆ๋˜๋ฉฐ(`opts.go:3517`) ์ด ํ•จ์ˆ˜๋Š” ๋งˆ์ง€๋ง‰ ํ† ํฐ์˜ `>`๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค(์™€์ผ๋“œ์นด๋“œ ๊ธˆ์ง€์šฉ `IsValidLiteralSubject`๋Š” ๋ณ„๋„ ํ•จ์ˆ˜์ด๋ฉฐ export์— ์“ฐ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค). - ์ˆ˜์ž… ์ธก์€ `prefix:` / `to:`๊ฐ€ ์—†์œผ๋ฉด **๋™์ผ subject**๋กœ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค. **๊ธฐ๋ณธ ์ฑ„ํƒํ•˜์ง€ ์•Š๋Š” ๊ทผ๊ฑฐ 3๊ฐ€์ง€**: 1. **A-2 ๋ชฉ์ ๊ณผ ์ƒ์ถฉ** โ€” ๋ฒ”์šฉ ํ™ˆ๋žฉ ๊ณ„์ •์— **๋ชจ๋“  ์žก์˜ ํŽ˜์ด๋กœ๋“œ**(`detail` ๋ณธ๋ฌธ ํฌํ•จ)๋ฅผ ์—ด์–ด ์ค๋‹ˆ๋‹ค. ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฒฉ๋ฆฌ๋ฅผ ๊ฐ•ํ™”ํ•˜๋ ค๋Š” Track 2์™€ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์ž…๋‹ˆ๋‹ค. 2. **M3 ์กฐ์šฉํ•œ ํŒŒ์†** โ€” export subject๊ฐ€ ํ† ํ”ฝ ๋ฃจํŠธ๋ฅผ **๋‘ ๋ฒˆ์งธ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ**ํ•˜๋Š” ์ง€์ ์ด ๋ฉ๋‹ˆ๋‹ค. ์ง€๋ฌธ ํ† ํ”ฝ ์ „ํ™˜ ์‹œ ๋งค์นญ์ด ์กฐ์šฉํžˆ ๋Š๊น๋‹ˆ๋‹ค(โ†’ `G-D9`๊ฐ€ ๊ฐ•์ œ). 3. **N-1์„ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•จ** โ€” import๋Š” ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆผ์ด๋ฉฐ **retained๋ฅผ ์˜ฎ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ์ฆ‰ Option A๋ฅผ ์จ๋„ ์‚ฌํ›„ ์ ‘์† ๋Œ€์‹œ๋ณด๋“œ๋Š” ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. Option B์™€ ๋™์ผํ•œ ํ•œ๊ณ„์ž…๋‹ˆ๋‹ค. โ†’ ๊ฒฐ๋ก : ๋‹จ์ผ ์‚ฌ์šฉ์ž ํ™ˆ๋žฉ์—์„œ๋Š” **Option B(ยงA-1์˜ `mam_observer`)** ๊ฐ€ ์ €์žฅ์†Œ ยง5.5 ์ฒ˜๋ฐฉ๊ณผ ์ผ์น˜ํ•˜๊ณ  ๋…ธ์ถœ๋„ ์ตœ์†Œ์ž…๋‹ˆ๋‹ค. Option A๋Š” "๋‹ค๋ฅธ ์‚ฌ๋žŒ/๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ์ด ๊ด€์ธกํ•œ๋‹ค"๋Š” ์š”๊ตฌ๊ฐ€ ์‹ค์ œ๋กœ ์ƒ๊ฒผ์„ ๋•Œ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. --- ## 10. ๋ฏธ๊ฒฐ ์งˆ๋ฌธ (์‚ฌ์šฉ์ž/Creator ํŒ๋‹จ ์‚ฌํ•ญ) 1. **๋…ธ์ถœ ๋ชจ๋ธ**: ๋ชจ๋ธ T(Tailscale) ๊ถŒ๊ณ . ๋ณด์œ  ๋„๋ฉ”์ธ์ด ์žˆ๊ณ  ์™ธ๋ถ€ ํ˜‘์—…์ž๊ฐ€ ๋ถ™๋Š”๋‹ค๋ฉด ๋ชจ๋ธ P + ยงB-3. 2. **๊ด€์ธก ํด๋ผ์ด์–ธํŠธ์˜ ํ”„๋กœํ† ์ฝœ**: N-1 ๋•Œ๋ฌธ์— **์‚ฌํ›„ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด MQTT๋กœ ๋ถ™๋Š” ๊ฒƒ์ด ์ •๋‹ต**์ž…๋‹ˆ๋‹ค. WebSocket/NATS ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ณ ์ง‘ํ•œ๋‹ค๋ฉด ยง5.3 JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ ์˜ตํŠธ์ธ์ด ํ•„์š”ํ•˜๋ฉฐ, ์ด๋Š” ์ถ”๊ฐ€ ๋””์Šคํฌ ๊ด€๋ฆฌ ๋ถ€๋‹ด์„ ๋™๋ฐ˜ํ•ฉ๋‹ˆ๋‹ค. ์–ด๋А ์ชฝ์„ ํƒํ• ์ง€ ๊ฒฐ์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. 3. **์„œ๋ฒ„์ธก ์ž์‚ฐ์˜ ์œ„์น˜**: `nats.conf`/`docker-compose.yml`์„ `deploy/nats/`๋กœ ์ปค๋ฐ‹ํ• ์ง€, ์„œ๋ฒ„ ๋กœ์ปฌ์—๋งŒ ๋‘˜์ง€. ์ปค๋ฐ‹ํ•˜๋ฉด G-D5~G-D7ยทG-D9๋ฅผ **์‹ค์ œ ํŒŒ์ผ์— ์ง์ ‘** ๊ฑธ ์ˆ˜ ์žˆ์–ด ๋ฌธ์„œ ๊ฐ€๋“œ๋ณด๋‹ค ๊ฐ•ํ•ด์ง‘๋‹ˆ๋‹ค. ์‹œํฌ๋ฆฟ์€ ์–ด๋А ์ชฝ์ด๋“  `.env`๋กœ ๋ถ„๋ฆฌ. 4. **`B-17` ์šฐ์„ ์ˆœ์œ„**: H-1ยทH-4๋Š” ์›๊ฒฉ ์ „ํ™˜์˜ ๋ณด์•ˆ ๋ชฉ์ ์„ ์ง์ ‘ ํ›ผ์†ํ•ฉ๋‹ˆ๋‹ค. Planner ๊ถŒ๊ณ ๋Š” **M2b ์ง„์ž… ์ „ ์ฒ˜๋ฆฌ**์ž…๋‹ˆ๋‹ค. 5. **`implementation_plan.md` ํŒŒ์ผ๋ช…**: ์ €์žฅ์†Œ์˜ ๋Œ€๋ฌธ์ž ๊ด€๋ก€์™€ ์–ด๊ธ‹๋‚ฉ๋‹ˆ๋‹ค(์ด์ „ ๋ฆฌ๋น„์ „์—์„œ๋„ ์ œ๊ธฐ). --- ### ๋ถ€๋ก Y โ€” Rev.2์—์„œ ์ƒˆ๋กœ ์ˆ˜ํ–‰ํ•œ ์‹ค์ธก | ๋Œ€์ƒ | ํ™•์ธ ๊ฒฐ๊ณผ | |---|---| | retained ์ „๋‹ฌ ๊ฒฝ๋กœ | `mqttSendRetainedMsgsToNewSubs`๋Š” `mqttPacketSub` ์ฒ˜๋ฆฌ์—์„œ๋งŒ ํ˜ธ์ถœ, `sub.mqtt.prm` ์ˆœํšŒ โ†’ **MQTT ๊ตฌ๋…์ž ์ „์šฉ** | | NATS ๋ณ€์ˆ˜ ํ•ด์„ ๋ฒ”์œ„ | `isVariable()`์€ **๋น„์ธ์šฉ ๋ฌธ์ž์—ด**์—์„œ๋งŒ ๋„๋‹ฌ. `lexQuotedString`์€ *"will not interpret any internal contents"* | | export subject ์™€์ผ๋“œ์นด๋“œ | `IsValidSubject`๊ฐ€ ๋งˆ์ง€๋ง‰ ํ† ํฐ `>`๋ฅผ ํ—ˆ์šฉ(`isValidSubject`์˜ `fwc` ๋ถ„๊ธฐ). export๋Š” `IsValidLiteralSubject`๋ฅผ ์“ฐ์ง€ ์•Š์Œ | | export/import ๋ฌธ๋ฒ• | `exports: [{stream: "..."}]`, `imports: [{stream: {account: X, subject: "..."}}]`, `prefix`/`to` ์ง€์› | | ๊ณ„์ • ๊ฒฉ๋ฆฌ | *"Accounts create isolated tenant subject spaces"*, `jetstream: enabled`๋Š” ๊ณ„์ • ๋‹จ์œ„ | | `MAM_ENV_FILE` ์˜คํƒ€ ๊ฒฝ๋กœ | **`broker.hivemq.com tls=False`** (๋‹ค๋ฅธ ํ›„๋ณด ๋ฏธ์‹œ๋„) | | ๋Œ€์กฐ๊ตฐ(์ •์ƒ ๊ฒฝ๋กœ) | `nats.private.internal tls=True` | | freeze ์Šคํฌ๋ฆฝํŠธ + cwd=์ €์žฅ์†Œ | **`broker.hivemq.com tls=False`** (์ฑŒ๋ฆฐ์ €์˜ cwd ๋„์ž… ๊ทผ๊ฑฐ ์„ฑ๋ฆฝ) | | `PRIVATE_SERVER.md:230` | *"MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜"* โ€” Option B๋Š” ๊ธฐ์กด ์ฒ˜๋ฐฉ |