IoT Workshop #1: ออกแบบ Architecture ก่อนลงมือจริง

IoT Workshop #1: ออกแบบ Architecture ก่อนลงมือจริง

ShowkhunWorkshop

IoT Workshop #1: ออกแบบ Architecture ก่อนลงมือจริง

Branch: step-01-fiber-bootstrap (จุดเริ่ม) — อ้างอิงภาพรวมจาก step-19-e2e Phase: Planning (1/3) Repo: kangana1024/showkhun-workshop


มีโปรเจกต์หลายอันที่ล้มเหลวไม่ใช่เพราะโค้ดแย่ แต่เพราะ ไม่เคยคิดก่อนสร้าง เลย (╥_╥)

เราเลยอยากให้ Workshop ซีรีส์นี้เริ่มต้นด้วยการ “คิดก่อน code” — ออกแบบ Architecture ให้แน่น ก่อนที่น้องๆ จะเปิด IDE แม้แต่บรรทัดเดียว มาลุยกันเลย!


สิ่งที่น้องๆ จะได้จากบทความนี้

  • เข้าใจภาพรวม System Architecture แบบ Event-Driven สำหรับ IoT
  • เห็น Data Flow ทั้ง 3 แบบ: รับข้อมูล (ingest), ส่งคำสั่ง (command), ดึง History (query)
  • รู้จัก Communication Patterns — REST, MQTT, WebSocket ใช้ตอนไหน
  • เข้าใจ ว่าทำไม ถึงเลือก Tech Stack แต่ละตัว (ไม่ใช่แค่ “มันดี”)
  • รู้ว่าทำไมเราเขียน alerting เป็น Go เอง แทนที่จะใช้ Kapacitor
  • เห็น Deployment ทั้ง Dev และ Production

ก่อนอื่น — ทำไมต้อง Architecture First?

ลองนึกภาพว่าน้องๆ กำลังจะสร้างบ้าน แล้วเริ่มสั่งอิฐก้อนแรกโดยยังไม่มีแบบแปลน…

นั่นแหละปัญหา เพราะพอผนังขึ้นไปแล้วค่อยรู้ว่าประตูอยู่ผิดที่ มันรื้อยากมาก IoT System ก็เหมือนกัน — ถ้าไม่คิด Data Flow ไว้ก่อน พอ Sensor ส่งข้อมูลมา 1,000 เครื่องพร้อมกัน ระบบ crash แน่นอน

Architecture คือ แบบแปลน ที่บอกว่าแต่ละส่วนอยู่ที่ไหน คุยกันยังไง และมีทางออกฉุกเฉินไหม


ภาพรวมของระบบ — Event-Driven Architecture

ระบบ IoT Platform ที่เราจะสร้างในซีรีส์นี้ทำงานแบบ Event-Driven — ทุกอย่างเริ่มจาก “event” ที่ชื่อว่า sensor reading แล้วระบบก็ตอบสนองต่อ event นั้น (เก็บ, broadcast, alert) มาดูภาพรวมกัน:

graph TD
    A[🌡️ Devices / Sensors] -->|MQTT 1883| B[📡 Mosquitto Broker]
    A -->|REST 3000| C[🖥️ Backend Go + Fiber]
    B -->|subscribe devices/+/telemetry| C
    B -->|subscribe devices/+/telemetry| T[🔁 Telegraf]
    C -->|CRUD| M[(🍃 MongoDB 8)]
    C -->|write sensor_data| I[(📈 InfluxDB 2.7)]
    T -->|write telegraf_sensor_data| I
    C -->|Flux query 8086| I
    C -->|alert engine| H[🔔 Webhook]
    C -->|WebSocket /api/v1/ws| F1[📱 LynxJS Mobile]
    C -->|WebSocket /api/v1/ws| F2[🖥️ Vite Admin]
    C -->|publish devices/+/command| B

ดูแล้วอาจรู้สึกว่าเยอะ แต่ไม่ต้องตกใจ — หัวใจมีแค่นี้: ทุก reading ที่เข้ามา ไม่ว่าจะมาทาง REST หรือ MQTT จะวิ่งผ่าน ingestion path เดียวกันใน backend แล้วแตกออกไป 3 ทาง (เขียน InfluxDB / fan-out WebSocket / run alert engine) ส่วน Telegraf เป็น pipeline เสริมที่วิ่งคู่ขนาน

สังเกตให้ดี: ใน architecture จริงของเรา ไม่มี Kapacitor และไม่ได้รัน Chronograf เป็น service — เราเขียน alerting engine เป็น Go เองในตัว backend (เดี๋ยวอธิบายว่าทำไม) infra ที่รันจริงมีแค่ 4 ตัว: MongoDB, Mosquitto, InfluxDB, Telegraf


Data Flow: จาก Sensor ถึง Screen

อยากให้น้องๆ เข้าใจว่าข้อมูลมันเดินทางยังไง ก่อนจะมาดู code ทีหลัง

เปรียบเทียบกับชีวิตจริง: ลองนึกถึงระบบน้ำในบ้าน — น้ำออกจากประปา (Sensor) ผ่านท่อใหญ่ (MQTT/REST) ไปถึงมิเตอร์ (Backend) แล้วค่อยกระจายไปห้องครัว ห้องน้ำ (Frontend) แต่ละจุดมีวาล์วควบคุม และมีถังเก็บน้ำสำรอง (Database) ไว้เผื่อต้องย้อนดูว่าเมื่อกี้ใช้น้ำไปเท่าไหร่

Flow 1: Sensor Data Ingestion (เส้นทางหลัก)

นี่คือเส้นทางที่ข้อมูล sensor วิ่งจาก device มาถึงหน้าจอ user จุดเด็ดคือ REST และ MQTT มาบรรจบที่ ingestion path เดียวกัน:

Sensor Device

    ├──(A) MQTT Publish  devices/{device_id}/telemetry
    │       payload: {"fields":{"temperature":28.5,"humidity":65},"tags":{"location":"room-a"}}
    │            │
    │            ├──▶ Mosquitto Broker
    │            │        ├──▶ Telegraf (subscribe devices/+/telemetry)
    │            │        │        └──▶ InfluxDB  measurement: telegraf_sensor_data
    │            │        └──▶ Go Backend (subscribe devices/+/telemetry)
    │            │
    └──(B) REST POST /api/v1/sensors/data ────▶ Go Backend

                              ┌──────────────────────┼──────────────────────┐
                              ▼                       ▼                       ▼
                    Validate vs registry      Write InfluxDB          Run alert engine
                    (MongoDB) + enrich        measurement:            (threshold/offline/
                    tags (device_type,        sensor_data             anomaly) → webhook
                    group_id)                       │

                                            Broadcast via WebSocket

                                    ├──▶ LynxJS Mobile (real-time dashboard)
                                    └──▶ Vite Admin (monitoring)

สังเกตว่า reading จาก ทั้งสองทาง ถูก validate กับ device registry, เติม tag ที่ได้จาก registry (device_type, group_id) แล้วถึงเขียนลง InfluxDB — เพราะใช้ logic เดียวกัน reading จาก REST กับ MQTT จึงได้ผลเหมือนกันเป๊ะ

Flow 2: Device Command (เส้นทางย้อนกลับ)

น้องๆ อยากสั่งให้อุปกรณ์ทำอะไร เช่น เปิด/ปิดไฟ ก็ใช้ Flow นี้

Admin/User Action (Mobile App / Admin Panel)

    ▼ REST API Call
    │ POST /api/v1/devices/{id}/commands
    │ Body: {"action":"reboot","payload":{"delay":5}}

    └──▶ Go Backend

             ├──▶ ตรวจ device มีอยู่จริง (MongoDB registry)
             └──▶ MQTT Publish  devices/{device_id}/command
                      │ Payload: {"action":"reboot","payload":{"delay":5}}

                      └──▶ Device รับคำสั่ง → ทำงาน (เช่น สั่ง relay)

Flow 3: Historical Data Query

ดูกราฟย้อนหลัง “เมื่อคืนอุณหภูมิสูงสุดเท่าไหร่” — ใช้ Flow นี้ ซึ่งอ่าน InfluxDB ผ่าน Flux (ไม่ใช่ InfluxQL):

User requests chart data

    ▼ REST API Call
    │ GET /api/v1/devices/{id}/readings?range=24h&window=1h&aggregate=mean

    └──▶ Go Backend

             ├──▶ validate device vs registry
             ├──▶ สร้าง Flux query แบบ bounded + parameterised (กัน injection)
             └──▶ Query InfluxDB 2.7

                      └──▶ คืน rows {time, field, value, tags} + meta (ค่าที่ถูก clamp)

                               └──▶ Render chart บน Mobile/Admin

Component Diagram: ข้างในแต่ละส่วนมีอะไร?

Backend Components (Go + Fiber)

Backend เราแบ่งเป็นชั้น เหมือนกับแผนก HR ในบริษัท — มี Receptionist รับงาน (Handler), มี Manager วางแผน (Service), และมี Worker ลงมือทำ (Repository / database):

┌──────────────────────────────────────────────────┐
│              Go Fiber Application                 │
│                                                  │
│  ┌──────────┐  ┌─────────────┐  ┌─────────────┐  │
│  │  Router   │  │ Middleware  │  │   Config    │  │
│  │ /api/v1  │  │ - JWT/RBAC  │  │ - Viper     │  │
│  │ /healthz │  │ - CORS      │  │ - .env      │  │
│  │ .../ws   │  │ - ReqLogger │  │             │  │
│  └────┬─────┘  └─────────────┘  └─────────────┘  │
│       │                                          │
│  ┌────▼─────────────────────────────────────┐    │
│  │              Handler Layer                │    │
│  │  device  sensor  reading  command         │    │
│  │  auth    alert   ws                       │    │
│  └────┬─────────────────────────────────────┘    │
│       │                                          │
│  ┌────▼─────────────────────────────────────┐    │
│  │              Service Layer                │    │
│  │  DeviceService  SensorService  AuthService│    │
│  │  AlertEngine (+ alert_eval, alert_rule)   │    │
│  └────┬─────────────────────────────────────┘    │
│       │                                          │
│  ┌────▼─────────────────────────────────────┐    │
│  │   Repository (Mongo)   |   Database (Influx)│   │
│  │   devices, users,      |   write points,    │   │
│  │   groups, alerts       |   Flux query (tsquery)│ │
│  └──────────────────────────────────────────┘    │
│                                                  │
│  Side modules: mqtt (Paho) · ws (hub) ·          │
│  ratelimit · notify (webhook) · auth (argon2id)  │
└──────────────────────────────────────────────────┘

Service Communication Matrix

แต่ละส่วนคุยกันยังไง ใช้ Port อะไร — นี่คือ “แผนที่ท่อสื่อสาร” ของระบบจริง:

Source Target Protocol Port Purpose
Devices Mosquitto MQTT 1883 Sensor telemetry
Devices Backend HTTP 3000 REST telemetry (ทางเลือก)
Mosquitto Telegraf MQTT 1883 Config-driven pipeline
Mosquitto Backend MQTT 1883 Telemetry subscribe + command publish
Telegraf InfluxDB HTTP 8086 Write telegraf_sensor_data
Backend MongoDB TCP 27017 Registry/users/alerts CRUD
Backend InfluxDB HTTP 8086 Write sensor_data + Flux query
Backend Webhook HTTP/HTTPS Alert notification (Slack-compatible)
Mobile App Backend HTTP / WS 3000 API + real-time
Admin Panel Backend HTTP / WS 3000 API + real-time

สังเกตว่า ไม่มีแถวของ Kapacitor หรือ Chronograf — alerting อยู่ในตัว backend แล้ว และ monitoring dashboard เราทำเองใน admin panel (ดู #20 Monitoring)


Communication Patterns: เลือกอะไรใช้ตอนไหน?

นี่คือส่วนที่หลายคนสับสนมากที่สุด — REST vs MQTT vs WebSocket ต่างกันยังไง?

เปรียบเทียบกับการสื่อสารในชีวิตจริง:

  • REST = โทรศัพท์ ถามตอบทันที รอ response ก่อนแล้วค่อยทำต่อ
  • MQTT = วิทยุสื่อสาร ส่งออกอากาศ ใครสนใจ subscribe ไว้ก็รับเอง
  • WebSocket = LINE Group ที่เปิดค้างไว้ แล้วใครก็ส่งข้อความเข้ามาได้ตลอด

1. Request-Response (REST API)

ใช้สำหรับ CRUD, queries, commands ที่ต้องการ response ทันที ทุก response ใช้ envelope เดียวกัน: สำเร็จ {"data": ...}, error {"error":{"code","message"}}

Client ──── GET /api/v1/devices ────────▶ Go API
Client ◀── 200 {"data":[{...},{...}]} ──── Go API

Endpoints หลัก (ของจริง):

  • POST/GET/PUT/PATCH/DELETE /api/v1/devices — device registry
  • POST /api/v1/sensors/data และ /batch — ingest
  • GET /api/v1/devices/:id/readings, /api/v1/sensors/query — Flux query
  • POST /api/v1/devices/:id/commands — สั่ง command
  • POST /api/v1/auth/login / /refresh / GET /api/v1/auth/me — auth
  • POST/GET/PUT/DELETE /api/v1/alert-rules — alert rule CRUD

2. Publish-Subscribe (MQTT)

ใช้สำหรับ device communication — lightweight, low bandwidth เหมาะกับ sensor ที่ส่งข้อมูลต่อเนื่อง

Device   ──── PUBLISH devices/sensor-01/telemetry ───▶ Mosquitto

Telegraf ◀── SUBSCRIBE devices/+/telemetry ───────────────┤
Go API   ◀── SUBSCRIBE devices/+/telemetry ───────────────┘

Go API   ──── PUBLISH devices/sensor-01/command ────▶ Mosquitto ──▶ Device

Topic Structure (ของจริง):

devices/{device_id}/telemetry    # Sensor data (device → cloud)
devices/{device_id}/command      # Commands (cloud → device)

backend ดึง device_id จาก topic เสมอ (segment ที่ 2) ไม่เชื่อค่าใน payload — กันการปลอม device id

3. WebSocket (Real-time Push)

ใช้ push real-time data ไป frontend โดย client ไม่ต้อง poll ซ้ำๆ client เปิด WS ไปที่ /api/v1/ws แล้ว subscribe เป็น “room” ต่อ device หรือ group

Client ──── WS connect /api/v1/ws ──────────▶ Go API
Client ──── {"action":"subscribe","room":"device:sensor-01"} ──▶
Client ◀──── {"type":"sensor_data","room":"device:sensor-01","payload":{...}} ──── (ต่อเนื่อง)

ข้อความที่ใช้จริง:

{ "action": "subscribe",   "room": "device:sensor-01" }
{ "action": "unsubscribe", "room": "group:<group_id>" }
{ "type": "sensor_data", "room": "device:sensor-01",
  "payload": { "device_id": "sensor-01", "fields": {"temperature": 27.7}, "timestamp": "2026-..." } }

room ถูกจำกัด prefix แค่ device: กับ group: เท่านั้น (กัน client subscribe room ภายในเอง) และ hub ใช้ goroutine เดียวเป็นเจ้าของ state ทั้งหมด client ที่ช้าจน buffer เต็มจะถูกตัดทิ้งเพื่อไม่ block คนอื่น


Tech Stack Justification: ทำไมถึงเลือกอันนี้?

เรารู้ว่าน้องๆ หลายคนอยากถาม “ใช้ Node.js ได้เหมือนกันมั้ย?” หรือ “MongoDB แทน PostgreSQL ได้มั้ย?” — ดังนั้นขอตอบด้วยเหตุผล ไม่ใช่ความรู้สึก

ทำไมเลือก Go + Fiber?

Criteria Go + Fiber Alternatives
Concurrency Goroutines เบามาก (stack เริ่มแค่ ~2KB) เหมาะกับ connection พันๆ จาก sensor thread-based กิน memory มากกว่า
MQTT Eclipse Paho Go client นิ่งและ mature มีหลายภาษา แต่ Go ecosystem ลงตัว
Binary single binary, ไม่ต้องลาก runtime Node.js/Python ต้องมี runtime
Fiber API คล้าย Express เขียนเร็ว Gin/Echo ก็ดี แต่ Fiber คุ้นมือสาย Express

IoT Backend ต้องรับ connection พร้อมกันนับพัน Goroutines จัดการเรื่องนี้ได้ดีมาก — เราเลย run REST handler, MQTT subscriber, WebSocket hub และ alert engine อยู่ใน process เดียวกันได้สบายๆ

ทำไมเลือก MongoDB (8.0)?

  • Flexible Schema: device metadata หลากหลาย ไม่ต้อง migrate ทุกครั้งที่เพิ่ม sensor แบบใหม่
  • Document Model: device config / location เป็น nested object พอดีกับ document
  • Index ครบ: unique index บน device_id, compound index สำหรับ filter และ TTL index สำหรับ alert_history (ลบ record เก่าทิ้งเอง)
  • Typed filter: เราเขียน filter เป็น typed (กัน NoSQL injection)

ทำไมเลือก InfluxDB 2.7 + Flux (ไม่ใช่ 1.8 / 3)?

  • time-series database ออกแบบมาเพื่อข้อมูลที่มี timestamp โดยเฉพาะ — query 30 วันย้อนหลังเร็วกว่าเก็บใน Mongo มาก
  • 2.7 มี Org/Bucket/Token/UI ในตัว provision ผ่าน env ได้คำสั่งเดียว และยังใช้ Flux ได้เต็มที่ — เราใช้ Flux อ่านข้อมูลออก REST แบบ bounded + parameterised
  • เราใช้ 2 bucket: raw (iot_workshop) และ downsampled (iot_workshop_downsampled) แยก retention กัน

ทำไม “เขียน alerting เป็น Go เอง” แทน Kapacitor?

นี่คือจุดที่แพลนแรกกับของจริง ต่างกันมากที่สุด — แพลนเดิมจะใช้ Kapacitor (ส่วน A ของ TICK) แต่พอลงมือทำจริง การเขียน engine เป็น Go เองให้ข้อดีกว่า:

  • ประเมิน reading ได้ทันที ตอน ingest ไม่ต้องรอ Kapacitor subscribe กลับจาก InfluxDB
  • reuse logic เดียวกันทั้ง REST และ MQTT
  • unit-test ได้ละเอียด (evaluator เป็น pure function)
  • จัดการ rule ผ่าน REST + MongoDB ได้เหมือน resource อื่นในระบบ

จึงเป็นเหตุผลที่ infra ของเราไม่มี Kapacitor และไม่ได้รัน Chronograf เป็น service (monitoring เราทำเองใน admin panel)

ทำไมเลือก LynxJS (Mobile)?

  • เขียน UI ด้วยสไตล์คล้าย React (component-based) แต่ออกไป native
  • เบา เหมาะกับการแสดง real-time data
  • จาก codebase เดียวออกได้หลาย platform

ทำไมเลือก Vite + React (Admin)?

  • Fast DX: Vite HMR เร็วมาก feedback loop สั้น
  • TypeScript: type safety สำหรับ admin UI ที่ซับซ้อน จับ bug ก่อน runtime
  • Ecosystem: React มี library ครบ (React Router, TanStack Query, React Hook Form + Zod, Zustand)

Deployment Architecture

Development (Docker Compose)

infra service รันใน container แยก แต่คุยกันผ่าน Docker network ส่วน backend เรา make run รันตรงๆ บนเครื่อง (dev loop เร็วกว่า) — เหมือนอพาร์ทเมนต์หลายห้องในตึกเดียว ต่างคนต่างห้อง แต่ใช้ทางเดินร่วมกัน

# infra/docker-compose.yml (4 services จริง)
services:
  mongodb:    # MongoDB 8.0.16 (port 27017)
  mosquitto:  # MQTT Broker (port 1883 + 9001 ws)
  influxdb:   # InfluxDB 2.7.12 (port 8086)
  telegraf:   # Telegraf 1.39 (MQTT → InfluxDB, ไม่ expose port)
# backend รันแยกด้วย `make run` ที่ http://localhost:3000

Production Considerations

พอ traffic เยอะขึ้น เราสามารถ scale backend ออกเป็นหลาย instance โดยมี Load Balancer คอยแบ่งงาน (backend เป็น stateless, WebSocket hub ต่อ instance — production จริงใช้ shared pub/sub เช่น Redis เสริม)

                    ┌─── Load Balancer (nginx/traefik) ───┐
                    │                                      │
              ┌─────▼──────┐                    ┌─────────▼───────┐
              │ Backend #1 │                    │   Backend #2     │
              └──────┬─────┘                    └────────┬────────┘
                     │                                   │
              ┌──────▼───────────────────────────────────▼──────┐
              │              Shared Infrastructure               │
              │  MongoDB Replica Set | InfluxDB | Mosquitto      │
              └──────────────────────────────────────────────────┘

⚠️ ค่า default ทั้งหมด (รหัสผ่าน, token, anonymous MQTT) เป็น dev-only — production ต้องเปลี่ยน credential, ปิด anonymous MQTT, เปิด TLS, ตั้ง APP_CORS_ALLOW_ORIGINS ให้เจาะจง และตั้ง APP_AUTH_JWT_SECRET ที่สุ่มยาวพอ


สรุป — เราได้อะไรจากบทความนี้?

ก่อนลงมือ code เราได้วางรากฐานไว้แล้ว:

  • System Architecture แบบ Event-Driven — รู้ว่าแต่ละส่วนอยู่ที่ไหน และ telemetry จาก REST/MQTT บรรจบที่ ingestion path เดียวกัน
  • Data Flow 3 แบบ: Ingestion (sensor → screen), Command (user → device), Query (Flux history)
  • Communication Patterns: REST, MQTT, WebSocket ใช้ตอนไหน
  • Tech Stack พร้อมเหตุผล รวมถึง “ทำไมเขียน alert engine เองแทน Kapacitor”
  • Deployment infra 4 service + backend แยก สำหรับทั้ง Dev และ Production

เรียนรู้ WHY ก่อน HOW เสมอ แล้วตอน HOW จะง่ายขึ้นมาก (ง่าวง่าว)


Next Step

บทความหน้าเราจะลงลึกเรื่อง Database & Data Model Design — ออกแบบ Schema ของทั้ง MongoDB collections และ InfluxDB 2.7 buckets/measurements ให้ตรงกับ code จริง

Navigation: