IoT Workshop #3: ตั้ง Project & DevOps ให้พร้อมรบ

IoT Workshop #3: ตั้ง Project & DevOps ให้พร้อมรบ

ShowkhunWorkshop

IoT Workshop #3: ตั้ง Project & DevOps ให้พร้อมรบ

Branch: step-01-fiber-bootstrap (วาง infra + Makefile) — อ้างอิงจาก step-19-e2e Phase: Planning (3/3) Repo: kangana1024/showkhun-workshop


สวัสดีน้องๆ! พี่โชว์กลับมาแล้ว ╰(°▽°)╯

สองบทที่แล้วเราวางแผน Architecture และ Database Design กันไปแล้ว วันนี้ถึงเวลา ตั้ง project จริงๆ ซะที ก่อน code ได้ เราต้องจัดบ้านให้เรียบร้อยก่อน — Monorepo, Docker Compose, Makefile, env, Git Strategy ครบหมด พอตั้งเสร็จแล้ว workshop ทุก step ที่ตามมาจะ smooth มาก มาลุยกันเลย!


ทำไมต้องตั้ง Dev Environment ให้ดี? (WHY ก่อนเสมอ)

ลองนึกภาพว่าเรากำลังจะทำอาหารกับเพื่อน 10 คน แต่ครัวยังไม่ได้จัดเลย มีด กระทะ เขียง กระจายอยู่ทั่วบ้าน บางคนใช้แก๊ส บางคนใช้ไฟฟ้า แน่นอนว่า chaos ตั้งแต่ยังไม่ได้เริ่มทำ

Dev Environment คือครัวของ developer ถ้าไม่จัดก่อน:

  • น้องแต่ละคน setup นานคนละ 2-3 ชั่วโมง (เสียเวลา workshop ไปเลย)
  • “แต่มันรันได้บน machine เรานะ!” — classic 555
  • Service versions ต่างกัน → bug ที่หาไม่เจอ

เราจะแก้ปัญหานี้ด้วย Docker Compose (infra service เหมือนกัน 100%) + Makefile (command เดียวกันทุกคน) + Monorepo (code ทุกส่วนอยู่ที่เดียว) + version ที่ pin ไว้หมด


สิ่งที่น้องๆ จะได้เรียนรู้

  • Monorepo Structure — จัดบ้านให้ backend + 2 frontends + infra อยู่ด้วยกันอย่างเป็นระเบียบ
  • Docker Compose — spin up infra 4 services (MongoDB, Mosquitto, InfluxDB 2.7, Telegraf) ในคำสั่งเดียว
  • Makefile — ทำ CLI ของทีมให้ใช้โดยไม่ต้องจำ command ยาวๆ
  • Service Configs — ตั้งค่า Mosquitto + Telegraf (json_v2 → InfluxDB 2.x)
  • Environment Variables — แยก infra/.env กับ backend/.env อย่างถูกต้อง
  • Git Branching Strategy — branch step-NN-... ให้ workshop ไหลลื่น

ภาพรวม Dev Environment

ก่อนลงมือ มาดูภาพรวมว่า services เชื่อมกันยังไง — สังเกตว่า infra มี 4 ตัว และ backend รันแยก:

graph LR
    DEV[👩‍💻 Backend make run :3000] -->|27017| MO[(🍃 MongoDB)]
    DEV -->|8086 write+Flux| IN[(📈 InfluxDB 2.7)]
    DEV -->|1883 sub/pub| MQ[📡 Mosquitto]
    MQ -->|subscribe| TG[🔁 Telegraf]
    TG -->|8086 write| IN
    SUB[🌡️ Sensors] -->|1883| MQ

ดูแล้วเข้าใจเลยว่า Telegraf เป็น “พนักงานเก็บข้อมูล” ที่นั่งฟัง MQTT แล้วยก data ไปเก็บใน InfluxDB ส่วน backend เราจะ make run รันตรงๆ บนเครื่อง (dev loop เร็วกว่า run ใน Docker)

ไม่มี Chronograf / Kapacitor / api container! infra จริงมีแค่ 4 service ตาม infra/docker-compose.yml — alerting เราเขียนเป็น Go ในตัว backend และ monitoring dashboard ทำเองใน admin panel


Monorepo Structure — จัดบ้านก่อนเริ่มงาน

Monorepo คือการเอา project ทั้งหมด (backend, frontend-mobile, frontend-admin, infra, e2e) ใส่ไว้ใน repo เดียว แทนที่จะกระจายหลาย repo

เปรียบเหมือน คอนโดรวม vs บ้านแยก — ถ้าอยู่คอนโดรวม ใช้ lift ร่วม ไปมาหาสู่กันง่าย แต่ถ้าบ้านแยกหลายหลัง กว่าจะไปหากันแต่ละทีก็เหนื่อยแล้ว

# Clone workshop repository
git clone https://github.com/kangana1024/showkhun-workshop.git
cd showkhun-workshop

โครงสร้างจริงของรีโป:

showkhun-workshop/
├── backend/                  # Go Fiber API server
│   ├── cmd/server/           # main + graceful shutdown
│   ├── internal/             # config, database, model, repository,
│   │                         # service, handler, auth, mqtt, ws, ...
│   ├── Dockerfile            # multi-stage → distroless
│   ├── .env.example
│   ├── go.mod
│   └── go.sum
├── frontend-mobile/          # LynxJS mobile app (Rspeedy)
│   └── src/
├── frontend-admin/           # Vite + React + TypeScript admin panel
│   └── src/
├── infra/                    # Infrastructure (development)
│   ├── docker-compose.yml    # MongoDB, Mosquitto, InfluxDB, Telegraf
│   ├── mosquitto/mosquitto.conf
│   ├── telegraf/telegraf.conf
│   └── .env.example
├── e2e/                      # Playwright E2E (admin)
├── Makefile                  # <=== ตัวนี้สำคัญมาก!
├── .gitignore
└── README.md

สังเกตว่า config infra อยู่ใต้ infra/ (ไม่ใช่ deployments/) และ Makefile อยู่ที่ root ของรีโป


Makefile — CLI ของทีมเรา

Makefile เปรียบเหมือน รีโมทคอนโทรล ของ project แทนที่จะต้องจำ docker compose -f infra/docker-compose.yml up -d ทุกครั้ง แค่พิมพ์ make up ก็จบ! นี่คือ Makefile จริง (ย่อ):

# Showkhun IoT platform — developer workflow shortcuts.
BACKEND_DIR := backend
COMPOSE_FILE := infra/docker-compose.yml
COMPOSE := docker compose -f $(COMPOSE_FILE)

.DEFAULT_GOAL := help

up: ## Start the infrastructure stack (MongoDB, Mosquitto, InfluxDB, Telegraf)
	$(COMPOSE) up -d

down: ## Stop and remove the infrastructure stack
	$(COMPOSE) down

logs: ## Tail logs from the infrastructure stack
	$(COMPOSE) logs -f

ps: ## Show the status of the infrastructure containers
	$(COMPOSE) ps

config: ## Validate and render the docker compose configuration
	$(COMPOSE) config

run: ## Run the backend API server locally
	cd $(BACKEND_DIR) && go run ./cmd/server

build: ## Compile the backend
	cd $(BACKEND_DIR) && go build ./...

tidy: ## Tidy backend Go module dependencies
	cd $(BACKEND_DIR) && go mod tidy

test: ## Run backend tests
	cd $(BACKEND_DIR) && go test ./...

vet: ## Run go vet on the backend
	cd $(BACKEND_DIR) && go vet ./...

fmt: ## Format backend Go code
	cd $(BACKEND_DIR) && go fmt ./...

นอกจากนี้ยังมี make seed-admin สำหรับ bootstrap admin คนแรก:

seed-admin: ## Bootstrap the first admin user (ADMIN_EMAIL=.. ADMIN_PASSWORD=..)
	@if [ -z "$(ADMIN_EMAIL)" ] || [ -z "$(ADMIN_PASSWORD)" ]; then \
		echo "Usage: make seed-admin [email protected] ADMIN_PASSWORD='strong-pass'"; \
		exit 1; \
	fi
	cd $(BACKEND_DIR) && \
		APP_AUTH_SEED_ADMIN_EMAIL='$(ADMIN_EMAIL)' \
		APP_AUTH_SEED_ADMIN_PASSWORD='$(ADMIN_PASSWORD)' \
		go run ./cmd/server

ลองพิมพ์ make help แล้วจะเห็น menu สวยงาม:

  up         Start the infrastructure stack (MongoDB, Mosquitto, InfluxDB, Telegraf)
  down       Stop and remove the infrastructure stack
  logs       Tail logs from the infrastructure stack
  run        Run the backend API server locally
  test       Run backend tests
  seed-admin Bootstrap the first admin user (ADMIN_EMAIL=.. ADMIN_PASSWORD=..)
  ...

เปรียบเหมือนร้านอาหารที่มีเมนู ดีกว่าต้องไปถามพ่อครัวทุกครั้งว่า “วันนี้มีอะไรบ้าง” 555


Docker Compose — infra 4 services ที่ pin version

Docker Compose เปรียบเหมือน ผู้จัดการทีม ที่รู้ว่าต้องเรียก service ไหนก่อน-หลัง ใครต้องรอ healthcheck ของใคร นี่คือ infra/docker-compose.yml จริง (ย่อให้เห็นแก่นแต่ค่าตรงของจริง):

# infra/docker-compose.yml
name: showkhun-iot

services:
  mongodb:
    image: mongo:8.0.16            # 8.0 LTS — patch ล่าสุดที่ boot ได้บน kernel ใหม่
    container_name: showkhun-mongodb
    ports:
      - "${MONGO_PORT:-27017}:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USERNAME:-root}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:-rootpassword}
      MONGO_INITDB_DATABASE: ${MONGO_DATABASE:-iot_workshop}
    volumes:
      - mongo_data:/data/db
    healthcheck:
      test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

  mosquitto:
    image: eclipse-mosquitto:2.0.22
    container_name: showkhun-mosquitto
    ports:
      - "${MQTT_PORT:-1883}:1883"
      - "${MQTT_WS_PORT:-9001}:9001"
    volumes:
      - ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
      - mosquitto_data:/mosquitto/data
      - mosquitto_log:/mosquitto/log

  influxdb:
    image: influxdb:2.7.12
    container_name: showkhun-influxdb
    ports:
      - "${INFLUX_PORT:-8086}:8086"
    environment:
      DOCKER_INFLUXDB_INIT_MODE: setup           # provision org/bucket/token คำสั่งเดียว
      DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUX_USERNAME:-admin}
      DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD:-adminpassword}
      DOCKER_INFLUXDB_INIT_ORG: ${INFLUX_ORG:-showkhun}
      DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUX_BUCKET:-iot_workshop}
      DOCKER_INFLUXDB_INIT_RETENTION: ${INFLUX_RETENTION:-30d}
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN:-dev-influx-token-change-me}
    volumes:
      - influx_data:/var/lib/influxdb2
      - influx_config:/etc/influxdb2

  telegraf:
    image: telegraf:1.39.0
    container_name: showkhun-telegraf
    depends_on:
      influxdb:
        condition: service_healthy
      mosquitto:
        condition: service_healthy
    environment:
      INFLUX_TOKEN: ${INFLUX_TOKEN:-dev-influx-token-change-me}
      INFLUX_ORG: ${INFLUX_ORG:-showkhun}
      INFLUX_BUCKET: ${INFLUX_BUCKET:-iot_workshop}
      MQTT_BROKER_URL: tcp://mosquitto:1883
    volumes:
      - ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro

volumes:
  mongo_data:
  mosquitto_data:
  mosquitto_log:
  influx_data:
  influx_config:

จุดที่อยากให้สังเกต:

  • ทุก image pin version (mongo:8.0.16, eclipse-mosquitto:2.0.22, influxdb:2.7.12, telegraf:1.39.0) เพื่อให้ workshop ทำซ้ำได้เหมือนกันทุกเครื่อง
  • InfluxDB 2.7 ใช้ DOCKER_INFLUXDB_INIT_* provision org/bucket/token ได้ในคำสั่งเดียว — ไม่ใช่ INFLUXDB_DB แบบ 1.8 อีกแล้ว
  • Telegraf ใช้ depends_on + condition: service_healthy — เริ่มได้ก็ต่อเมื่อ InfluxDB กับ Mosquitto พร้อมจริง เหมือนบอกน้องใหม่ว่า “รอ senior พร้อมก่อนนะ ค่อยเริ่มทำงาน”

เรื่อง MongoDB กับ kernel ใหม่: Mongo 8.0+ มี startup guard ที่หยุดทำงานบน host ที่ใช้ Linux kernel ≥ 6.19 (รวม OrbStack/Lima/Colima รุ่นใหม่) เราเลย pin 8.0.16 ซึ่งเป็น patch ล่าสุดที่ยัง boot ได้


Service Configurations

Mosquitto MQTT Broker

MQTT Broker เปรียบเหมือน ไปรษณีย์กลาง — sensor ส่ง message มา broker รับ แล้ว forward ให้ subscriber ทุกคนที่สนใจ topic นั้น นี่คือ infra/mosquitto/mosquitto.conf จริง:

# infra/mosquitto/mosquitto.conf (dev only)
persistence true
persistence_location /mosquitto/data/

log_dest stdout
log_type error
log_type warning
log_type notice
log_type information

listener 1883
protocol mqtt

listener 9001
protocol websockets

allow_anonymous true   # dev only! production ต้องปิดและตั้ง auth/TLS

Port 1883 ใช้ native MQTT ส่วน 9001 ใช้ WebSocket เพราะ browser connect MQTT ตรงๆ ไม่ได้ ต้องผ่าน WebSocket

Telegraf — สายพานข้อมูล (json_v2 → InfluxDB 2.x)

Telegraf คือ พนักงานสายพาน ที่หยิบ message จาก MQTT แล้วเขียนลง InfluxDB จุดสำคัญคือใช้ data_format = "json_v2" (ไม่ใช่ json เก่า) และ output เป็น influxdb_v2:

# infra/telegraf/telegraf.conf (ย่อ)
[agent]
  interval = "10s"
  flush_interval = "10s"
  precision = "1s"

[[inputs.mqtt_consumer]]
  servers = ["${MQTT_BROKER_URL}"]
  topics = ["devices/+/telemetry"]
  qos = 1
  client_id = "telegraf-ingest"
  topic_tag = ""                       # ทิ้ง raw topic หลังดึง device_id ออก
  data_format = "json_v2"

  # ดึง device_id จาก topic: devices/<id>/telemetry → tag
  [[inputs.mqtt_consumer.topic_parsing]]
    topic = "devices/+/telemetry"
    tags = "_/device_id/_"

  [[inputs.mqtt_consumer.json_v2]]
    measurement_name = "raw_telemetry"
    [[inputs.mqtt_consumer.json_v2.object]]
      path = "fields"                  # ทุกค่าตัวเลขใน "fields" → InfluxDB fields
    [[inputs.mqtt_consumer.json_v2.tag]]
      path = "tags.location"
      rename = "location"
      optional = true
    # ... tags.zone / tags.unit / tags.source (optional ทุกตัว)

# rename raw_telemetry → telegraf_sensor_data (กัน double-write กับ backend)
[[processors.rename]]
  namepass = ["raw_telemetry"]
  [[processors.rename.replace]]
    measurement = "raw_telemetry"
    dest = "telegraf_sensor_data"

# coerce ทุก field เป็น float กัน type ขัดกัน
[[processors.converter]]
  namepass = ["telegraf_sensor_data"]
  [processors.converter.fields]
    float = ["*"]

[[outputs.influxdb_v2]]
  namepass = ["telegraf_sensor_data"]
  urls = ["http://influxdb:8086"]
  token = "${INFLUX_TOKEN}"
  organization = "${INFLUX_ORG}"
  bucket = "${INFLUX_BUCKET}"
  content_encoding = "gzip"

topic_parsing เจ๋งมาก — ดึง device_id จาก topic path devices/{device_id}/telemetry มาเป็น tag อัตโนมัติ ส่วนการ rename เป็น telegraf_sensor_data คือกุญแจกัน double-write กับ measurement sensor_data ที่ backend เขียน (อ่านเหตุผลเต็มใน #2 Database Design)

สังเกตว่า ไม่มี Kapacitor และไม่มี Chronograf ในไฟล์ compose — alerting เราเขียนเป็น Go (ดู #12 Go Alerting Engine)


Environment Variables — แยก infra กับ backend

ของจริงแยก env เป็น 2 ไฟล์: infra/.env (สำหรับ docker compose) กับ backend/.env (สำหรับ Go server) ทุกตัวแปรของ backend ขึ้นต้นด้วย prefix APP_:

# 1) infra/.env (docker compose โหลดอัตโนมัติ)
MONGO_PORT=27017
MONGO_ROOT_USERNAME=root
MONGO_ROOT_PASSWORD=rootpassword
MONGO_DATABASE=iot_workshop
MQTT_PORT=1883
MQTT_WS_PORT=9001
INFLUX_PORT=8086
INFLUX_ORG=showkhun
INFLUX_BUCKET=iot_workshop
INFLUX_RETENTION=30d
INFLUX_TOKEN=dev-influx-token-change-me
# 2) backend/.env (Go server) — ตัวอย่างค่าสำคัญ
APP_PORT=3000
APP_MONGO_URI=mongodb://root:rootpassword@localhost:27017/?authSource=admin
APP_MONGO_DATABASE=iot_workshop
APP_INFLUX_URL=http://localhost:8086
APP_INFLUX_TOKEN=dev-influx-token-change-me
APP_INFLUX_ORG=showkhun
APP_INFLUX_BUCKET=iot_workshop
APP_INFLUX_MEASUREMENT=sensor_data
APP_MQTT_ENABLED=true
APP_MQTT_BROKER_URL=tcp://localhost:1883
APP_ALERT_ENABLED=true
APP_AUTH_ENABLED=true
APP_AUTH_JWT_SECRET=        # ต้องตั้ง! >=32 bytes มิฉะนั้น backend fail closed

copy ทั้งสองไฟล์จาก template ก่อน run:

cp infra/.env.example infra/.env
cp backend/.env.example backend/.env

สำคัญ: APP_AUTH_JWT_SECRET ไม่มี default — ถ้าไม่ตั้ง (หรือสั้นกว่า 32 bytes) backend จะ ไม่ยอม start เพื่อไม่ให้เซ็น token ด้วย key ที่คาดเดาได้ สร้างด้วย openssl rand -base64 48


เริ่มใช้งานจริง (Getting Started)

พอ env พร้อม ก็แค่ 2 คำสั่ง:

# 1) รัน infrastructure (MongoDB, Mosquitto, InfluxDB, Telegraf)
make up

# 2) รัน Go API server ที่ http://localhost:3000
make run

ทดสอบว่า server ทำงาน:

curl http://localhost:3000/healthz
# {"status":"ok","service":"showkhun-iot-platform","dependencies":{"mongodb":"ok", ...}}

curl http://localhost:3000/api/v1/ping
# {"message":"pong", ...}

/healthz ตรวจ dependency (MongoDB + InfluxDB) ด้วย — ถ้าต่อไม่ได้จะตอบ HTTP 503 "status":"degraded" ให้ orchestrator แยก “process ขึ้น” กับ “พร้อมรับ traffic” ได้ ปิด stack เมื่อเสร็จด้วย make down


Git Branching Strategy — ถนนของ workshop

พี่โชว์ออกแบบ branch ให้ workshop ไหลเป็นเส้นตรง น้องๆ checkout ทีละ step แล้วดู diff ระหว่าง step ได้เลย เหมือน ถนนที่มี milestone ชัดเจน — และ branch จริงใช้รูปแบบ step-NN-...:

main
├── step-01-fiber-bootstrap     # Backend: Go Fiber Setup
├── step-02-mongodb-models      # Backend: MongoDB Models
├── step-03-device-api          # Backend: Device API
├── step-04-sensor-ingestion    # Backend: Sensor Ingestion
├── step-05-mqtt-broker         # Backend: MQTT Integration
├── step-06-websocket           # Backend: WebSocket
├── step-07-influx-setup        # Data: InfluxDB 2.7 + Telegraf Setup
├── step-08-telegraf-pipeline   # Data: Telegraf Pipeline + Flux
├── step-09-alerting            # Data: Go Alerting Engine
├── step-10-lynxjs-setup        # Mobile: LynxJS Setup
├── step-11-lynxjs-dashboard    # Mobile: Dashboard
├── step-12-lynxjs-control      # Mobile: Device Control
├── step-13-lynxjs-charts       # Mobile: Charts
├── step-14-lynxjs-alerts       # Mobile: Notifications
├── step-15-vite-setup          # Admin: Vite Setup
├── step-16-admin-crud          # Admin: CRUD
├── step-17-admin-monitoring    # Admin: Monitoring
├── step-18-admin-auth          # Admin: Auth & RBAC
└── step-19-e2e                 # Integration: ทุกอย่าง + Playwright E2E  <-- ตัวรวม

Workshop Flow สำหรับน้องๆ

# เริ่มต้น
git clone https://github.com/kangana1024/showkhun-workshop.git
cd showkhun-workshop

# ดู branch ทั้งหมด
git branch -a

# เริ่ม Workshop Step แรก
git checkout step-01-fiber-bootstrap

# ดู diff ระหว่าง steps — เจ๋งมาก ดูได้เลยว่า step นี้เพิ่มอะไรบ้าง
git diff step-01-fiber-bootstrap..step-02-mongodb-models

อยากเห็นทั้งระบบครบในที่เดียว? checkout step-19-e2e ได้เลย — มีครบทุก service พร้อม E2E test


สรุปสิ่งที่เราทำในบทนี้

น้องๆ เก่งมากที่อ่านมาถึงตรงนี้! (ノ◕ヮ◕)ノ*:・゚✧

สิ่งที่ทำ ประโยชน์
Monorepo Structure code + infra อยู่ที่เดียว ง่ายต่อการ cross-reference
Docker Compose 4 services MongoDB/Mosquitto/InfluxDB 2.7/Telegraf เหมือนกันทุกเครื่อง (pin version)
Makefile command กลาง (make up / make run / make seed-admin)
Service Configs Mosquitto + Telegraf (json_v2 → influxdb_v2) คุยกันได้ตั้งแต่แรก
Env แยก 2 ไฟล์ infra/.env สำหรับ compose, backend/.env (prefix APP_) สำหรับ Go
Git Branching step-NN workshop flow ชัดเจน เรียนทีละ step + step-19-e2e รวมครบ

Next Step — เริ่ม Phase 2: Development!

Phase 1: Planning เสร็จแล้ว! เราวางแผนครบทั้ง Architecture, Database Design และ Project Setup

ตอนนี้ถึงเวลาลงมือ code จริงๆ แล้ว! บทต่อไปเราจะ bootstrap Go Fiber API ตั้งแต่ศูนย์ มี middleware, Viper config, health check, graceful shutdown ครบถ้วน มาลุยกัน!

Navigation: