MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

搬一個服務,不是把 Docker Compose 複製過去就好

最近在盤點一個內部自動化服務未來如果要換主機,會遇到哪些麻煩。表面上看,這種事很像只是把 repository 拉下來、把 Docker Compose 跑起來、把幾個 volume 掛好,最多再補幾個環境變數。

但只要真的開始列清單,就會發現:服務搬遷最難搬的,往往不是服務本身,而是它周圍那些「平常看起來不像系統的一部分」的東西。

這件事我覺得很值得記下來,因為很多團隊在做內部工具、AI agent、自動化排程或小型平台時,都會低估這個問題。我們很容易把一個服務想成程式碼加資料庫,但真正在 production 裡活著的服務,通常是一團狀態。

程式碼是最容易搬的部分

對工程師來說,程式碼反而最單純。版本控制裡有原始碼,容器設定裡有啟動方式,套件管理器裡有依賴。只要平常有整理,換一台機器重新 build,不太會是最可怕的地方。

真正麻煩的是程式碼之外的東西。

例如:某些設定檔放在使用者家目錄,某些 token 放在另一個 credentials 目錄,某些排程是用本機 cron 跑,某些 webhook 指到舊機器的網址,某些 runtime 檔案被同步工具寫進去,某些 dashboard 又依賴本機資料庫。平常它們安靜地運作,沒有人特別注意;一旦要搬家,這些東西就全部浮出水面。

這也是我越來越不喜歡把遷移計畫寫成「部署新主機」的原因。那樣太容易讓人以為重點在安裝。比較準確的說法應該是:我們在遷移一個有狀態的作業系統。

狀態會藏在意想不到的地方

所謂狀態,不只是資料庫。

資料庫當然重要,該 dump、該 restore、該確認 schema、該驗證資料一致性,這些都是基本功。但很多狀態其實藏得更散:

  • agent 的 profile、技能、記憶與工作目錄;
  • 排程任務目前由誰負責觸發;
  • 外部平台的 webhook、callback URL 與 allowlist;
  • 本機檔案系統裡的 runtime volume;
  • 憑證、token、service account 與權限範圍;
  • dashboard、API、MCP 或內部入口的連線設定;
  • 哪些任務可以重跑,哪些重跑會造成重複通知或重複寫入。

這些東西沒有整理好,搬遷就會變成一場猜謎遊戲。新機器上服務看起來啟動了,但某個排程沒有跑;排程跑了,但寫到舊路徑;路徑修好了,但 credential 指到不存在的位置;credential 補上了,但 webhook 還打到舊主機;新舊主機同時跑,又開始重複送訊息。

這種 bug 最煩人的地方,是它不一定會在啟動時爆炸。它會等到半夜排程觸發、某個外部事件進來、某個少用功能被點開時,才慢慢露出來。

Cutover 比部署更重要

所以我覺得搬這類服務,不能只問「新環境能不能跑」,而要問「什麼時候正式切過去」。

部署只是把新環境準備好;cutover 是把責任從舊環境交給新環境。兩者差很多。

如果是有排程、有 webhook、有外部通知、有自動寫入的服務,cutover 沒設計好,最常見的不是停機,而是雙機並行。兩邊都以為自己是正本,兩邊都在處理事件,結果不是重複發送,就是資料被寫兩份,甚至新舊狀態互相覆蓋。

因此遷移計畫裡,我會特別要求幾件事:

第一,列出所有入口。包含人會打的網址、機器會打的 webhook、排程會啟動的腳本、外部系統會呼叫的 API。沒有入口清單,就不知道切換點在哪裡。

第二,列出所有寫入點。只讀服務搬起來比較安全,會寫資料、送通知、建立任務、更新狀態的服務就不能亂開。每一個寫入點都要知道它寫到哪裡、是否具備冪等性、重跑會不會出事。

第三,決定凍結時間。切換前是否要暫停排程?是否要停 webhook?是否要讓舊機器進入 read-only?這些事情要明確,不要靠「應該沒有人會用」這種祈禱式工程。

第四,準備 rollback。遷移不是證明自己一次成功,而是承認真實世界一定有漏網之魚。能回得去,團隊才敢切。

Secret inventory 是成熟度測驗

另一個我很在意的地方是憑證盤點。

內部系統常常長大到某個階段後,secret 會散得到處都是。有些在 .env,有些在 shell profile,有些在某個 credentials 資料夾,有些被 CI/CD 保存,有些甚至是當年某個人手動放上去的檔案。平常只要服務沒壞,就沒人想碰。

但搬遷會逼你面對現實:如果一個系統的 secret 來源說不清楚,它其實就還沒有被好好管理。

我不一定主張每個小團隊一開始就上很重的 secret manager,但至少要有一份 inventory:每個 credential 做什麼、由誰擁有、放在哪裡、如何輪替、搬遷時要不要更新、外洩時怎麼停用。這份清單不性感,可是它直接決定系統能不能被複製、被恢復、被交接。

路徑也是架構的一部分

還有一個很容易被低估的細節:路徑。

從 macOS 搬到 Linux,從個人工作站搬到伺服器,或從舊目錄結構搬到新目錄結構,很多原本「反正可用」的假設都會失效。家目錄不同、權限不同、大小寫敏感度不同、背景服務使用者不同、同步資料夾不存在、某些 CLI 不在 PATH 裡。

這些聽起來很瑣碎,但架構往往就是被這些瑣碎的假設綁住。當一個腳本寫死某個個人路徑,它就不再只是腳本問題,而是部署模型問題。當一個排程依賴某台機器上某個使用者的環境,它就不再只是排程問題,而是營運風險。

所以我現在看到內部自動化服務,會更早要求把路徑、credential、runtime data、config 分清楚。不是為了潔癖,而是為了未來真的要搬、要備援、要交接時,不會被自己過去的方便綁架。

遷移清單不是文件,是風險控制

最後我覺得最務實的做法,是在還沒有真的搬之前,就先寫一份遷移 checklist。

這份文件不用一開始就很完美,但至少要涵蓋:服務啟動方式、volume 清單、資料庫備份還原、secret inventory、排程切換、webhook 更新、對外入口、安全設定、煙霧測試、監控告警、rollback 步驟。

很多人覺得 checklist 是行政工作,我倒覺得它是工程判斷的外化。你寫得出清單,代表你知道系統由哪些東西組成;你寫不出來,代表你對系統的理解其實還停留在「它現在有跑」。

而「它現在有跑」不是架構,只是運氣。

服務搬遷最有價值的地方,不只是把東西搬到新主機,而是趁機把那些平常沒被命名的依賴命名出來。哪些是資料,哪些是設定,哪些是憑證,哪些是入口,哪些是排程,哪些是不可重跑的副作用。

一旦這些東西被看見,系統就不只是比較容易搬,也比較容易維護、備援與交接。

這大概也是技術管理裡很常見的一種殘酷真相:真正成熟的工程能力,不是把服務啟動起來,而是知道它為什麼能啟動、靠什麼活著、壞掉時怎麼收拾。

能回答這些問題,才算真的擁有那個系統。