引言:CI 還不夠#

持續整合(Continuous Integration, CI)是生產力與品質的重大躍進——它確保團隊能夠在高信心的狀態下協作開發大型複雜系統。CI 的核心在於快速回饋:每次 commit 後驗證程式碼能否編譯、能否通過單元測試與驗收測試。

然而,CI 還不夠。CI 主要聚焦於開發團隊,其產出通常是手動測試流程的輸入,接著才進入後續的發布流程。軟體交付中的大量浪費來自於測試與維運階段,常見的問題包括:

  • 建置與維運團隊等待文件或修正
  • 測試人員等待「可用的」建置版本
  • 開發團隊在數週後才收到 bug 回報,此時早已轉向新功能
  • 在開發末期才發現架構無法滿足非功能性需求

這導致軟體因為太晚進入類生產環境而無法部署,也因為開發團隊與測試/維運團隊之間的回饋週期太長而充滿缺陷

解決方案是採用更全面的、端到端的軟體交付方式。透過自動化建置、部署、測試與發布流程,部署應用程式——甚至到生產環境——往往只需按一個按鈕。這創造了一個強大的回饋迴路:部署簡單 → 快速回饋 → 頻繁測試 → 降低風險。

最終形成的是一個 pull system(精實術語):

角色可執行的操作
測試團隊自助部署建置版本到測試環境
維運團隊一鍵部署到 staging 和 production
開發人員看到每個建置通過了哪些階段
管理者監控 cycle time、throughput、程式碼品質等關鍵指標

什麼是部署管線(Deployment Pipeline)?#

在抽象層次上,部署管線是將軟體從版本控制送到使用者手中的自動化流程。每個變更都經過一個複雜的過程:建置軟體,然後通過多個測試與部署階段。部署管線將這個過程模型化,讓你能看見並控制每個變更從版本控制到發布的進展。

價值流映射(Value Stream Map)#

從概念到現金(concept to cash)的整個過程可以用價值流映射來表示。

Figure 5.1: A simple value stream map for a product

價值流映射告訴我們一個故事:整個過程可能耗時約三個半月,其中約兩個半月是實際工作,其餘是各階段之間的等待時間。例如,開發完成到測試開始之間可能有五天的等待,這可能是因為部署到類生產環境所需的時間。

用紙筆追蹤一個客戶需求從進入組織到完成的整個流程,記錄每個步驟的增值時間與等待時間——這就是最基本的價值流映射方法。

變更如何流經管線#

部署管線所涵蓋的是從開發到發布這段價值流。建置版本會多次通過這段流程。可以將其視覺化為一個序列圖:

Figure 5.2: Changes moving through the deployment pipeline

關鍵觀察:

  • 管線的輸入是版本控制中的特定修訂版本
  • 每次 commit 都會啟動管線的新實例
  • 隨著建置通過每個測試階段,信心逐漸增加
  • 我們願意投入的資源也隨之增加,環境也越來越接近生產環境
  • 目標是儘早淘汰不合格的候選版本,並儘快將失敗根因回饋給團隊

Figure 5.3: Trade-offs in the deployment pipeline

管線的四個核心階段#

所有專案共通的最小階段子集:

階段名稱驗證目標
1Commit Stage(提交階段)驗證系統在技術層面能運作——編譯、通過單元測試、執行程式碼分析
2Automated Acceptance Test Stage(自動驗收測試階段)驗證系統在功能與非功能層面能滿足使用者需求與客戶規格
3Manual Test Stage(手動測試階段)驗證系統的可用性、偵測自動測試未捕獲的缺陷、確認對使用者的價值(包括探索性測試、整合環境、UAT)
4Release Stage(發布階段)將系統交付給使用者——部署到 production 或 staging 環境

部署管線也被稱為 continuous integration pipeline、build pipeline、deployment production line 或 living build。不管叫什麼名字,本質上都是一個自動化的軟體交付流程。這不代表完全沒有人的參與,而是確保容易出錯的複雜步驟是自動化、可靠且可重複的。

基本部署管線#

Figure 5.4: Basic deployment pipeline

典型的管線運作流程:

  1. 開發人員提交變更到版本控制系統
  2. Commit Stage:編譯、執行單元測試、程式碼分析、建立安裝包,產出的二進位檔存入 artifact repository
  3. Acceptance Test Stage:由 commit stage 成功完成後自動觸發,執行較長時間的自動驗收測試
  4. 管線分支:此後可獨立部署到各環境——UAT、capacity testing、production
  5. 後續階段通常不自動觸發,而是由測試人員或維運團隊自助式選擇建置版本並按鈕部署

Figure 5.5: Go showing which changes have passed which stages

快速回饋的關鍵在於可見性:能看到每個 check-in 經過了管線的哪些階段、通過或失敗。這使得問題可以立即追溯到導致失敗的特定變更。

部署管線實踐(Deployment Pipeline Practices)#

只建置一次二進位檔(Only Build Your Binaries Once)#

許多建置系統在不同階段反覆從原始碼編譯——commit 階段編譯一次、acceptance test 階段又編譯一次、capacity testing 再編譯一次。每次編譯都有引入差異的風險:編譯器版本不同、第三方函式庫版本不同、甚至編譯器設定不同都可能改變應用行為。

正確做法:

  • 只在 commit stage 建置一次二進位檔
  • 儲存在檔案系統中(不是版本控制,因為它們是基線的衍生物)
  • 後續所有階段都重用同一份二進位檔
  • 透過 hash 值驗證每個階段使用的二進位檔完全一致
  • 二進位檔不應該是環境特定的——必須將程式碼(各環境相同)與設定(各環境不同)分離

建立只能在特定環境運行的二進位檔是非常糟糕的實踐。這會讓建置系統快速變得極度複雜,產生大量的特殊情況處理。作者曾遇過一個專案需要五人全職維護建置系統。解決方案是將環境特定設定與環境無關的二進位檔分離。

每個環境使用相同的部署方式(Deploy the Same Way to Every Environment)#

必須使用相同的流程部署到每個環境——不論是開發人員工作站、測試環境還是 production。原因:

  • 部署到 production 的頻率最低,但風險最高
  • 只有在數百次部署到其他環境後,才能消除部署腳本作為錯誤來源
  • 使用相同的部署腳本能有效防止「在我機器上可以跑」的問題

每個環境的差異(IP 位址、作業系統設定、資料庫位置等)不應該用不同的部署腳本處理,而是將環境特定設定獨立出來

  • 每個環境一個 properties 檔案,存入版本控制
  • 透過主機名稱或環境變數選擇正確的設定檔
  • 或者使用目錄服務(LDAP、ActiveDirectory)、資料庫等集中管理

應該能從單一來源(版本控制、目錄服務或資料庫)查詢所有應用程式在所有環境中的設定。作者見過需要寄 email 給分布在不同大洲的不同團隊才能拼湊出完整設定資訊的組織——這在除錯時是極大的障礙。

若使用統一的部署流程,部署失敗可以縮小到三種原因:

  1. 應用程式的環境特定設定檔有誤
  2. 基礎設施或依賴服務有問題
  3. 環境設定有問題

煙霧測試你的部署(Smoke-Test Your Deployments)#

部署應用程式後,應有自動化腳本執行煙霧測試以確認其正常運作:

  • 可以簡單到啟動應用程式並檢查主畫面是否正確顯示
  • 也應檢查應用程式依賴的服務是否正常(資料庫、訊息匯流排、外部服務)
  • 煙霧測試可能是除了單元測試之外最重要的測試——它能確認你的應用程式確實能執行

部署到 Production 的副本(Deploy into a Copy of Production)#

為了對上線有高度信心,測試與持續整合應在儘可能接近 production 的環境上進行。需要確保:

  • 基礎設施(網路拓撲、防火牆設定)相同
  • 作業系統設定(包括 patches)相同
  • 應用程式堆疊相同
  • 應用程式資料處於已知的有效狀態

可利用磁碟映像、虛擬化、以及 Puppet 等工具配合版本控制來管理環境設定。

每個變更應即時通過管線(Each Change Should Propagate through the Pipeline Instantly)#

與傳統的定時排程不同(每小時建置、每晚跑驗收測試、週末跑容量測試),部署管線採取不同的方式:第一個階段由每次 check-in 觸發,每個階段成功完成後立即觸發下一個

Figure 5.6: Scheduling stages in a pipeline

當多個 check-in 快速發生時的智慧排程:

  • 若上一輪 acceptance test 還在執行,新的 commit stage 通過後不會觸發新的 acceptance test
  • 等 acceptance test 完成後,CI 系統會檢查是否有新變更,並針對最新版本執行
  • 若建置失敗,開發人員通常可以自行判斷是哪個 commit 導致的
  • 此智慧排程僅適用於全自動階段;手動測試環境的部署需要按需觸發

管線任何部分失敗就停止(If Any Part of the Pipeline Fails, Stop the Line)#

如果部署到某個環境失敗,整個團隊擁有這個失敗。團隊應停下一切,先修復問題。這適用於整個部署管線,不僅僅是 commit stage。

提交階段(The Commit Stage)#

每次 check-in 都會建立管線的新實例。提交階段的目標是儘快淘汰不適合 production 的建置版本。開發人員提交後應等待結果,理想上不超過 5 分鐘,最多不超過 10 分鐘

提交階段的典型步驟#

  1. 編譯程式碼(如有需要)
  2. 執行一組 commit tests(主要是單元測試,但也包含少量其他類型的測試以提高信心)
  3. 建立供後續階段使用的二進位檔
  4. 執行程式碼分析以檢查程式碼健康狀況
  5. 準備artifacts(如測試資料庫),供後續階段使用

程式碼分析指標#

當程式碼未達到預設的門檻值時,應和測試失敗一樣讓 commit stage 失敗。有用的指標包括:

指標說明
Test coverage(測試覆蓋率)如果只覆蓋 5% 的程式碼,幾乎沒有意義
Duplicated code(重複程式碼數量)
Cyclomatic complexity(循環複雜度)
Afferent and efferent coupling(傳入與傳出耦合)
Number of warnings(警告數量)
Code style(程式碼風格)

提交階段最佳實踐#

  • 開發人員應等待 commit stage 通過
  • 若失敗,應快速修復或回退變更
  • 通過 commit stage 是候選版本生命週期中的重要里程碑
  • 開發人員仍有責任監控後續階段的進展
  • 修復損壞的建置仍是最高優先級,即使斷裂發生在管線的後續階段

管線名稱的由來:「Pipeline」一詞並非來自液體流過管道的意象,而是借用處理器指令管線化(instruction pipelining)的概念。處理器透過「猜測」操作結果來平行執行指令流——如果猜對了,效率加倍;猜錯了,丟棄結果,沒有損失。部署管線的工作方式類似:我們設計 commit stage 以捕捉大多數問題並快速執行,然後「猜測」後續階段都會通過,於是繼續開發新功能。管線在背景樂觀地平行處理我們的候選版本。

自動驗收測試關卡(The Automated Acceptance Test Gate)#

為什麼單元測試不夠#

作者分享了一個擁有約 80 位開發人員的大型專案故事:即使單元測試覆蓋率平均達 90%,一個微小的 bug 導致系統三週無法正確啟動,卻沒有人發現——因為開發人員通常只執行測試而非應用程式本身。最終引入了簡單的自動煙霧測試作為 CI 流程的一部分才解決問題。

教訓:

  • 單元測試只測試開發人員對解決方案的觀點,無法充分證明應用程式從使用者角度確實能運作
  • 部署過程本身也是常見的失敗來源——手動密集的部署流程容易出錯
  • 需要額外的測試形式來彌補單元測試的不足

自動驗收測試的角色#

  • 驗證系統是否交付客戶期望的商業價值
  • 同時作為回歸測試套件
  • 由開發人員、測試人員和客戶跨職能協作建立和維護
  • 團隊必須即時回應驗收測試的失敗

自動驗收測試關卡是候選版本生命週期中的第二個重要里程碑。管線只允許通過此關卡的建置版本進入後續階段。管線使做正確的事比做錯誤的事更容易。

自動驗收測試最佳實踐#

  • 在類 production 環境上執行;若目標環境多樣,使用 build grid 平行執行
  • 整個團隊擁有驗收測試,而非獨立的測試團隊——否則開發人員不會感覺擁有這些測試,導致長期損壞
  • 開發人員必須能在自己的環境中執行驗收測試
  • 驗收測試應使用業務語言(「下單」而非「點擊訂單按鈕」)
  • 驗收測試也是回歸測試——不要盲目自動化每一個驗收標準
  • 維護成本過高時,改變管理方式通常比停止自動測試更有效

後續測試階段(Subsequent Test Stages)#

通過 acceptance test stage 後,候選版本進入更廣泛的使用領域。

Figure 5.7: Example deployment page

關鍵改進——取代「nightly build」概念:

  • 測試人員可以自行選擇要部署哪個建置版本,而非被迫接受某個任意版本
  • 可以看到哪些建置通過了自動測試、做了哪些變更
  • 若當前版本不適合,可以重新部署其他任何版本

手動測試(Manual Testing)#

在自動驗收測試之後進行,包括:

  • 探索性測試(Exploratory testing)
  • 可用性測試(Usability testing)
  • 展示會(Showcases)

測試人員的角色不是做回歸測試,而是:

  1. 確認驗收測試確實驗證了系統行為
  2. 進行人類擅長但自動測試不擅長的測試——探索性測試、平台外觀檢查、極端情況測試

自動驗收測試釋放了測試人員的時間,讓他們能專注於高價值活動,而非成為「人肉測試腳本執行機」。

非功能性測試(Nonfunctional Testing)#

  • 每個系統都有容量、安全性、SLA 等非功能性需求
  • 應建立自動化測試來衡量系統對這些需求的符合程度
  • 容量測試結果可以作為自動門檻(高效能應用)或提供給人類判斷的參考資訊

準備發布(Preparing to Release)#

每次 production 系統的發布都有商業風險。部署管線透過以下方式緩解這些風險:

  • 發布計畫由所有相關人員共同建立和維護(開發、測試、維運、基礎設施、支援)
  • 自動化最容易出錯的步驟以減少人為失誤
  • 在類 production 環境中反覆演練流程
  • 具備回退能力
  • 資料遷移策略(升級和回退)

自動化部署與發布#

  • Production 環境應完全鎖定——變更只能透過自動化流程進行
  • 管理 production 環境的流程也應用於其他測試環境
  • 自動化部署讓流程民主化:測試人員、開發人員、維運人員都不再依賴工單系統和 email 往返

部署自動化帶來了微妙但深遠的影響——人們開始放鬆,因為專案確實變得不那麼有風險了。當你已經成功部署複雜系統五十或一百次而毫無問題時,部署就不再是件大事了。

回退變更(Backing Out Changes)#

發布日令人恐懼的兩個原因:

  1. 害怕引入問題——手動步驟可能出錯,或指令本身有錯誤
  2. 害怕無法回退——一旦發布失敗就被困住

緩解策略:

方案說明
最佳方案保留舊版本在運作中(如使用 symlink 指向當前版本),新版本有問題時立即切回
次佳方案從頭重新部署上一個已知良好的版本
絕對不要使用與正向部署不同的回退流程,也不要做增量部署或增量回退——這些流程很少被測試因此不可靠

建立在成功之上(Building on Success)#

當候選版本可以部署到 production 時,我們已確知:

  • 程式碼可以編譯
  • 通過了單元測試(開發人員認為的正確行為)
  • 通過了驗收測試(分析師/使用者認為的正確行為)
  • 基礎設施和環境設定管理得當(已在類 production 環境中測試)
  • 部署系統可以運作(至少在開發環境、acceptance test、測試環境各部署過一次)
  • 版本控制包含部署所需的一切(已多次部署而無需手動介入)

實施部署管線(Implementing a Deployment Pipeline)#

不論是新專案還是現有系統,都應漸進式地實施部署管線。一般步驟:

  1. 建模價值流並建立 walking skeleton
  2. 自動化建置與部署流程
  3. 自動化單元測試與程式碼分析
  4. 自動化驗收測試
  5. 自動化發布
flowchart TD
    A[步驟 1\n建模價值流\n建立 Walking Skeleton] --> B[步驟 2\n自動化建置\n與部署流程]
    B --> C[步驟 3\n自動化單元測試\n與程式碼分析]
    C --> D[步驟 4\n自動化\n驗收測試]
    D --> E[步驟 5\n自動化\n發布流程]

步驟一:建模價值流與 Walking Skeleton#

  • 對於現有專案:花半小時用紙筆與所有相關人員確認流程,記錄每步的經過時間與增值時間
  • 對於新專案:參考類似專案,或從最小管線開始(commit stage → acceptance test stage → 部署到類 production 環境展示)
  • 在 CI/Release Management 工具中建模流程,一開始各階段可以是空的佔位符
  • Walking skeleton:做最少量的工作讓所有關鍵元素就位(Hello World 程式 + 一個 assert true 的測試 + 自動部署 + 一個簡單的驗收測試)
  • 新專案應在開發正式開始前(iteration zero)完成這些設定

步驟二:自動化建置與部署#

  • 建置流程:原始碼 → 二進位檔(可在新機器上只需正確環境設定即可啟動,不依賴開發工具鏈)
  • CI server 監視版本控制,每次變更時自動建置
  • 自動化部署:取得機器 → 編寫部署腳本 → 部署測試(煙霧測試)
  • 實現一鍵部署到 UAT 環境
  • 發布流程應與部署到測試環境的流程相同,只有環境設定不同

步驟三:自動化單元測試與程式碼分析#

  • 每次 check-in 執行單元測試、程式碼分析
  • 單元測試不碰檔案系統或資料庫(那是 component tests),因此執行速度快
  • Commit stage 超過五分鐘時,拆分為平行執行的套件

步驟四:自動化驗收測試#

  • 重用部署腳本,煙霧測試後啟動驗收測試框架
  • 儲存應用程式日誌,可使用 Vnc2swf 等工具錄製畫面以輔助除錯
  • 功能性和非功能性驗收測試一開始可以在同一階段背靠背執行,之後再分離
  • 每種類型的測試至少早期就準備一兩個並整合到管線中

演進你的管線#

管線會隨著專案變複雜而演進。兩個常見的擴展方向:

  • 元件化(Components):大型應用拆為元件,每個元件有自己的小管線,再有一個組裝管線
  • 分支(Branches):詳見進階版本控制

實施管線時的三個重要提醒:

  1. 不需一次到位——漸進實施,手動流程先用佔位符,記錄開始和結束時間以識別瓶頸
  2. 管線是豐富的資料來源——記錄每個流程的開始與結束時間、具體變更內容,以計算 cycle time 並找出瓶頸
  3. 管線是活的系統——持續改善和重構,就像對待你的應用程式一樣

指標(Metrics)#

回饋是軟體交付流程的核心。改善回饋的最佳方式是縮短回饋週期並讓結果可見(information radiators)。

最重要的全域指標:Cycle Time#

Cycle time 是從決定實作某個功能到該功能發布給使用者的時間。Mary Poppendieck 的提問:「你的組織需要多久才能部署一個只涉及一行程式碼的變更?你能以可重複、可靠的方式做到嗎?」

  • Cycle time 難以測量(涵蓋分析、開發到發布),但比任何其他指標更能反映你的流程
  • 許多專案錯誤地選擇其他指標(如缺陷數量)作為主要指標——但如果修復一個缺陷需要六個月才能發布,知道缺陷存在也沒太大用處
  • 降低 cycle time 會自然促進提升品質的實踐

約束理論(Theory of Constraints)#

利用約束理論來持續改善:

步驟行動說明
1識別系統中的限制性約束(瓶頸)
2利用最大化瓶頸的吞吐量(例如確保手動測試永遠有待測項目的緩衝區)
3從屬其他流程從屬於約束——其他資源不必 100% 利用(例如開發人員不要以全速開發 stories,而是保持待測積壓穩定,將剩餘時間用於撰寫自動化測試)
4提升若仍不夠快,增加資源(如聘請更多測試人員)
5重複找到下一個約束,回到步驟一
flowchart LR
    A[1. 識別瓶頸] --> B[2. 最大化\n瓶頸吞吐量]
    B --> C[3. 其他流程\n從屬於約束]
    C --> D[4. 提升\n約束能力]
    D --> E[5. 找到下一個\n約束]
    E --> A

其他診斷指標#

  • 自動測試覆蓋率
  • 程式碼庫屬性(重複程式碼、循環複雜度、耦合度、風格問題等)
  • 缺陷數量
  • Velocity(團隊交付可工作、已測試、可使用程式碼的速率)
  • 每日 commit 數、每日建置數、每日建置失敗數
  • 建置持續時間(包含自動測試)

Figure 5.8: A tree map generated by Panopticode showing cyclomatic complexity

指標的呈現也很重要——程式經理可能只需要紅/黃/綠的健康度指標,技術負責人需要更多細節但也不想翻閱大量報告。Panopticode 專案能產生豐富、密集的視覺化圖表,讓人一眼就能看出程式碼庫的問題所在。每次 check-in 都應產生這些報告,存入 artifact repository,並匯整到內部網站供所有專案監控。

總結#

部署管線的目的是讓所有參與軟體交付的人都能看見建置從 check-in 到 release 的進展。核心能力包括:

  • 看到哪些變更破壞了應用程式、哪些產生了適合測試或發布的候選版本
  • 一鍵部署到手動測試環境
  • 在完全了解候選版本已通過整個管線的情況下進行一鍵發布

實施部署管線後,流程中的低效率會變得顯而易見,cycle time、手動測試階段耗時、各階段發現的缺陷數量等資訊都能用來持續優化流程。

部署管線依賴幾個基礎:良好的組態管理自動化的建置與部署腳本自動化測試。它也需要紀律——確保只有通過自動化建置、測試與部署系統的變更才能被發布。