IoT Workshop #3: ตั้ง Project & DevOps ให้พร้อมรบ
IoT Workshop #3: ตั้ง Project & DevOps ให้พร้อมรบ
Branch:
step-01-fiber-bootstrap(วาง infra + Makefile) — อ้างอิงจากstep-19-e2ePhase: 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 pathdevices/{device_id}/telemetryมาเป็น tag อัตโนมัติ ส่วนการ rename เป็นtelegraf_sensor_dataคือกุญแจกัน double-write กับ measurementsensor_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:
- Prev: #2 Database Design
- Next: #4 Go Fiber Bootstrap — Phase 2 เริ่มแล้ว!