📌 這是 WeaMind 系列 的第 5 篇。
本系列以真實世界專案為背景,記錄重要技術實作與經驗分享。

WeaMind 是一個查詢台灣天氣的 LINE Bot——點擊選單或輸入地名就能查天氣,是我花了數個月共 150+ 小時催生的 Side Project。

以下會從兩個層面介紹這個專案:

  • 第一部分是應用層(你直接看到、用到的那一面),聊聊這個 Bot 本身的技術設計和工程品質追求。
  • 第二部分是基礎設施層(支撐 Bot 在雲端運行的底層環境),談談為什麼我要把它部署到 K8s 叢集上。

從本文開始,這個寫了近 5 年的 Python 後端部落格,寫作重心將逐漸從後端開發轉向雲端與 DevOps

這是一個全新的開始。


第一部分:一個天氣 Bot 的誕生

最好的 side project 往往來自於自己的痛點

我個人是中央氣象局「生活氣象 App 」的重度使用者,但每次要在不同行政區之間切換查天氣,操作都很麻煩。

這個痛點存在很久了,一直希望有更好的手機查詢體驗。考慮到氣象資料有公開資料與 API,我就想說:「乾脆自己做一個 LINE Bot 吧!」

除此之外,它必須是一個能展示我真實 Infra 能力的作品——不只是把 Bot 寫好,而是從程式碼到雲端部署,都有真實工程水準的那種。

多年前,從法律公務員轉職軟體工程師,我正是花了兩個月做了這個「給嗜讀者的 Python Flask 網頁服務」才順利找到工作。

三個第一次

這是我第一次做 LINE Bot——要學習令人痛苦的 LINE Bot SDK - Python😂——強烈懷疑它就是 Java 版本直接「翻譯」過來,程式碼非常不 Pythonic!

第一次用 FastAPI(工作上都是 Django)、第一次大量使用 AI(GitHub Copilot)輔助開發。

三個「第一次」疊在一起,學習曲線並不低。但正因為有 Copilot 全程參與,一個人也能在合理時間內把這些東西做出來——還做得有模有樣

老實說,如果沒有 AI 輔助,我大概不會想一口氣挑戰「三個第一次」。

這大概就是 AI 時代的開發樣貌。


功能介紹

WeaMind 的核心功能很直覺:直接輸入行政區名稱就能查天氣,像是「大安區」、「中壢」;也可以設定住家和公司地址,之後一鍵查詢——後者才是重點,因為它真正解決了我切換行政區的難題。

還有地圖查詢功能,在 LINE 地圖上選任意位置就能查。系統也會記錄最近查過的 5 個地點,方便快速重複查詢。

WeaMind UIWeaMind UI

我相信這就是一個好的 Bot 該做的事:簡單易用、直指痛點


應用層架構設計

做過 LINE Bot 的人都知道,它有幾個特別的「坑」

  • webhook 必須在時限內回應(具體要多久之內是動態的,官方不會告訴你),否則 LINE Platform 會重送請求——用戶此時會收到重複的回覆,影響體驗。
  • 無論有心還是無意,少部分用戶可能會瘋狂連點按鈕XD。而 LINE Platform 並未提供類似 rate limit 的機制,後端對每個請求都要處理——你的 db 正在燃燒🔥
  • webhook handler 邏輯不同於一般 API 開發,天然容易變成一團大泥球——解析、查詢、回覆全擠在一起,功能一多就很難看懂與維護。

WeaMind 對這三個問題都有明確的解法:

  • Fast ACK Webhook:花數十毫秒先回應 LINE Platform,業務邏輯交給 FastAPI 的BackgroundTasks非同步運行,用戶感受到的回應在 2 秒以內——實際只需要幾百毫秒,但 Hetzner 主機對台灣的延遲很高😂
  • Redis 分散式鎖:按鈕操作有 2 秒鎖定以防止連點,文字查詢則不受影響。
  • DDD 模組劃分coreuserlineweather四個領域,並以 router → service → model 三層分離,邊界清晰不互相污染。

更多細節可參考 WeaMind 專案 README 的「開發者技術亮點」。


工程品質的「刻意」追求

自己的專案,可以用自己的標準

我親愛的偏執狂

100% 的 type hints 覆蓋——使用 Pyright、94% 的單元測試覆蓋率、pre-commit 強制檢查、完整的 CI pipeline。

相關文章:

這些都不是偶然,而是刻意選擇的結果。在自己的專案裡,終於沒有人會說「這個可以之後再補」。

你可能會好奇「為什麼測試覆蓋率不是 100%?現在不是有 AI 了嗎?讓它一口氣寫完不就好了?」

我的看法是,覆蓋率畢竟只是測試品質的其中一個面向而已。為了讓覆蓋率達到百分之百,而寫一大堆不好讀、不好維護的機械性測試邏輯,這不符合我的開發價值觀。

哪怕是在 AI 時代,凡事也宜適可而止

偏執狂的工具鏈與 CI

工具鏈基本上是,想的到的一口氣全上了——說好的適可而止呢XD

uv 管理 Python 虛擬環境;Ruff 一個工具取代了 Flake8、Black、isort 三個;Pyright 做型別檢查。

相關文章:

安全掃描建了三層,角色不同:Bandit(靜態分析)、pip-audit(CVE 檢查)、detect-secrets(敏感資料防護)。這是我以前比較生疏的部分,這次特別加強。

每次 push 都會跑完 Ruff → Pyright → Bandit → pip-audit → pytest + Codecov 的完整流程。

相關文章:用 Side Project 學 CI:WeaMind 的 CI 實作策略

第一階段 CI 通過後,最後會自動推送新版 image 到 GHCR


第二部分:從單機到叢集

講完應用層,你可能會想:

這個 LINE Bot 跑在單機 VM 上就能正常運作了,幹嘛還要搞 K8s?

原因很簡單:因為這個專案從一開始就「不只是」為了做一個 LINE Bot——它是我為了 DevOps 轉職所做的刻意練習

K3s on Hetzner

我在 Hetzner 上另外開了三台 VM 組成 K3s 叢集,加上一台付費的 Load Balancer——都是錢啊啊啊!🤯

原本那台跑 Docker Compose 的 VM,在叢集建成後角色順勢轉變為堡壘機兼資料層,負責跑 PostgreSQL 和 Redis,同時作為 SSH 跳板與管理叢集——雖然我大部分時候都是在本機透過ProxyJump連到 Control Plane 上的 API Server。

持續演進的平台

對我來說,這套 Kubernetes 部署不是做完就收工的作品。接下來,我還想在上面實作可觀測性Helm 等 DevOps 必修課題。

任何我想學的 DevOps 技能,都可以在這上面繼續疊加。它顯然比任何線上課附的 lab 都有趣且真實多了——想想就有點小興奮🤩


Infra 關鍵架構決策

叢集是 K3s 三節點(1 控制平面 + 2 工作節點),跑在 Hetzner 私有網路內;資料層(PostgreSQL + Redis)留在堡壘機,透過內網連接。

流量路徑:LINE Platform 的 webhook 請求會先到 Hetzner Load Balancer(TCP 443 passthrough),再由 Traefik Ingress Controller 完成 TLS 終止,最後轉送到 line-bot Service 與 line-bot Pod。

為什麼選 K3s?

K3s 是由 Rancher(現為 SUSE)推出的輕量級 Kubernetes 發行版,專為資源受限的環境設計。

K3s 這個名稱意味「比 K8s 少 5 個字元的複雜度」——本質上就是一個更精簡、更易部署的 Kubernetes。

採單一 binary 檔、內建 Traefik Ingress Controller,並通過 CNCF 認證

對於單人維運的小型叢集,它可能是最接近真實環境(相比於 MinikubeKind)而且最務實的選擇。

為什麼選 Hetzner?

相關文章:Hetzner VPS 實測:比 DigitalOcean 更劃算的選擇?

當然是因為便宜!雖然它在今年 4 月就要漲價了🥲

表格是整個架構的所有成本(每月/歐元,漲價前、稅前):

節點 規格 月費
原 VM(堡壘機兼資料層) 4C/8GB €6.5
K8s 控制平面 2C/4GB €3.5
K8s 工作節點 × 2 2C/4GB €7
Hetzner Load Balancer €5
合計 €22/月

總體開銷,差不多等於訂閱一個 ChatGPT Plus 的月費。

但同樣的機器若要部署在 DigitalOcean 或者 GCP 上,所需的價格大概是 4 到 6 倍。(即使 Hetzner 漲價後也要 3 到 5 倍)

總之,我愛 Hetzner,德國人萬歲!

資料層為什麼留在 VM?

簡潔與穩定性優先。

PostgreSQL 和 Redis 現階段不需要水平擴展——以後大概也不需要XD,放進 K8s 反而要處理 StatefulSet 和 PV 的管理複雜度。

而且用內網連接,延遲幾乎可以忽略不計。

LINE Webhook URL 切換

這是一個意外好用的設計!

我原本的想法是:LINE webhook 固定打 api.kyomind.tw(你直接點擊網址,會回應{"message":"Welcome to WeaMind API"})上的 webhook endpoint,平常 DNS 的 A record 指向 Hetzner Load Balancer。

若叢集出問題需要 rollback,就手動把 A record 改回舊 VM 的 IP 地址,這樣就能快速切換回單機環境。

但這樣的問題是 DNS 傳播有延遲,少則幾分鐘、多則數小時,rollback 並不即時。

後來發現一個更簡單的做法:保留 api.kyomind.tw 指向舊 VM,另外新建一個 k8s.kyomind.tw 指向 Hetzner Load Balancer——兩個環境各自有固定網址。

切換時,只要在 LINE Platform 後台把 webhook URL 從一個換成另一個,秒級生效,不需要動 DNS。

這讓 K8s 環境和原本的單機環境可以並行運行,測試和 rollback 都很方便。


結語

WeaMind 對我來說不只是一個天氣 Bot。

應用層是五年後端經驗的集大成之作——終於有機會按自己的標準,從頭到尾做一次。基礎設施則是跨入 DevOps 的第一步,用真金白銀搭出來的練兵場🐥

如同我在自己的 Landing Page 說的:

AI 與 Infra 是開發者的未來。

而 WeaMind,則是未來的開端。


想看程式碼的讀者,歡迎逛逛 GitHub,並給我一個 ⭐️ 唷!

想試用 WeaMind,可掃描下方 QR Code 或搜尋 LINE ID @370ndhmf 加入好友
你的支持是我持續分享的動力