引言#

當專案規模超越單人、時間跨越數日、或產出不只一個執行檔時,僅靠 IDE 來建置與測試已不足夠。建置與部署腳本(build and deployment scripts)是持續交付的基石,必須像對待產品程式碼一樣認真維護——納入版本控制、定期測試、持續重構。

自動化部署遠比「把檔案丟到伺服器」複雜:需要設定應用程式、初始化資料、配置作業系統與中介軟體、模擬外部系統等。這些步驟一旦沒有自動化,就會隨專案複雜度成長而愈發脆弱且容易出錯。

部署方式的決策必須由開發人員與維運人員共同參與,因為雙方都需要熟悉所使用的技術。


建置工具概覽#

所有建置工具的核心都是相依性網路(dependency network):你指定一個目標,工具會計算出正確的任務執行順序,且每個前置任務只執行一次。

Figure 6.1: A simple build dependency network

任務導向 vs. 產品導向#

建置工具的關鍵差異在於它們是 task-oriented 還是 product-oriented

類型代表工具狀態管理方式特點
Task-orientedAnt, NAnt, MSBuild不保留跨次建置的狀態,靠內部標記避免重複執行適合有內建增量編譯的語言(如 C#)
Product-orientedMake, SCons以檔案時間戳記(或 MD5)判斷是否需要重新執行支援增量建置(incremental build),對 C/C++ 尤為重要

Rake 則兼具兩種模式,可視需求靈活使用。

主要建置工具比較#

Make

  • 歷史悠久的產品導向工具,擅長追蹤相依性、只編譯變動部分
  • 缺點:Makefile 複雜度隨專案增長而爆炸、空白字元敏感(tab vs. space)、依賴 shell 導致跨平台困難、宣告式模型對多數開發者不直覺
  • 現代替代方案:SCons(以 Python 編寫,跨平台且支援平行建置)

Ant

  • Java 生態系的任務導向工具,以 XML 定義腳本,跨平台且高度可擴展
  • 缺點:XML 冗長、領域模型貧乏(anaemic domain model)、宣告式與命令式混用易造成混亂、Ant 檔案往往長達數千行且難以重構

NAnt / MSBuild

  • .NET 生態系的 Ant 變體,MSBuild 與 Visual Studio 緊密整合
  • 繼承了 Ant 的多數限制

Maven

  • 採用**慣例優於配置(convention over configuration)**原則,提供標準專案結構與自動相依管理
  • 三大問題:
    1. 專案結構不符 Maven 預設假設時,極難客製化
    2. 擴展需撰寫 plugin(Mojo),門檻較高
    3. 預設自動更新,可能導致建置不可重現(non-reproducible builds);snapshot 依賴加劇此問題

若團隊已綁定 Ant 但需要相依管理,可搭配 Ivy 取得 Maven 的部分優勢,而不必遷移整個建置系統。

Rake

  • Ruby 的內部 DSL 建置工具,繼承 Make 的功能但用真正的程式語言編寫
  • 優勢:可用 Ruby 完整的除錯、重構、模組化能力來維護建置腳本
  • 不限於 Ruby 專案(如 Albacore 專案提供 .NET 的 Rake 任務)

Buildr / Gradle

  • 新一代建置工具,以真正的程式語言(Ruby / Groovy)撰寫內部 DSL
  • Buildr 建立在 Rake 之上,可直接替換 Maven(使用相同慣例與 repository),且支援增量建置
  • Gradle 適合偏好 Groovy 的團隊

Psake

  • Windows 平台的內部 DSL 建置工具,以 PowerShell 撰寫

建置與部署腳本的原則與實踐#

1. 為部署管線的每個階段建立腳本#

  • 專案初期可用單一腳本涵蓋所有操作,尚未自動化的步驟用 dummy target 佔位
  • 腳本成長後,依管線階段拆分:
    • Commit 腳本:編譯、打包、執行提交測試套件、靜態分析
    • 驗收測試腳本:部署到適當環境、準備資料、執行驗收測試
    • 非功能測試腳本:壓力測試、安全測試等

所有腳本必須納入版本控制,最好與原始碼放在同一個 repository,以便開發與維運人員協作。

2. 使用適當的技術來部署應用程式#

  • 不要用通用建置工具處理部署(除非部署流程極其簡單)
  • 每種中介軟體通常都有專屬的配置與部署工具(如 WebSphere 的 Wsadmin)
  • 部署方式的決策需在專案早期就讓開發、測試、維運三方共同參與

書中案例「Conan the Deployer」:某電信專案的開發團隊用 Ant 部署到本機,但維運團隊拒絕使用 Ant。最終組建了跨職能的 build team,用 Bash 腳本創建統一部署流程。維運團隊之所以信任這套系統,一是因為他們參與了建立過程,二是因為他們看到同一套腳本在管線中的每個測試環境都被使用。

3. 用相同的腳本部署到所有環境#

  • 同一份腳本部署到開發、測試、預備、生產等所有環境
  • 環境差異(URI、IP 位址等)以配置資訊分離管理
  • 建置與部署腳本必須在開發者本機也能運作
  • 若應用程式的部署架構複雜,可能需要投入額外工作(如以 in-memory database 取代 Oracle cluster),但回報是值得的

避免出現「只有開發者使用的平行建置系統」——這會讓正式建置腳本缺乏持續測試與維護的驅動力。

4. 使用作業系統的打包工具#

  • 部署一堆散落在檔案系統中的檔案既低效又難以維護(升級、回滾、卸載)
  • 建議使用 OS 原生打包系統:Debian/Ubuntu 的 .deb、RedHat 的 .rpm、Windows 的 MSI
  • 打包後可搭配環境管理工具(Puppet、CfEngine 等)自動部署
  • 對於需要特殊工具的商業中介軟體,採用混合方式:用原生套件處理一般檔案,用專用工具處理特殊部署

優先使用作業系統級打包系統而非平台特定工具(如 RubyGems、Python Eggs),因為系統管理員通常偏好統一的套件管理體系。

5. 確保部署流程的冪等性(Idempotent)#

部署流程無論目標環境處於何種狀態,都應該將其帶到相同的正確狀態。實現方式:

  • 最佳做法:從已知良好的基線環境開始(透過自動化佈建或虛擬化)
  • 次佳做法:在部署前驗證環境的前置假設,若不符則中止部署
  • 整體部署:若應用程式作為單一整體建置與整合,就應該作為單一整體部署——每次從單一版本控制修訂版重新部署所有層級
  • 例外情況:叢集系統的金絲雀發布(canary releasing)、元件化架構中已驗證的單一元件更新
  • 冪等工具:Rsync(只傳輸差異)、Puppet(宣告式分析並只做必要變更)

6. 逐步演進部署系統#

  • 不要試圖一次建立完美的自動化部署系統
  • 從最基本的開始:先寫腳本讓本地開發環境可部署,並分享給團隊
  • 逐步擴展:測試環境 → 驗收測試環境 → 預備環境 → 生產環境
  • 每一步都已帶來價值

JVM 專案結構#

Maven 標準目錄佈局#

即使不使用 Maven,遵循其標準目錄結構也能大幅降低團隊溝通成本:

/[project-name]
  README.txt
  LICENSE.txt
  /src
    /main
      /java          Java 原始碼
      /resources     專案資源
      /config        配置檔
      /webapp        Web 應用程式資源
    /test
      /java          測試原始碼
      /resources     測試資源
  /lib
    /runtime         執行期函式庫
    /test            測試用函式庫
    /build           建置用函式庫
  /target
    /classes         編譯後的類別
    /test-classes    編譯後的測試類別
    /surefire-reports 測試報告

關鍵管理原則#

類別管理方式
原始碼遵循 Java 命名慣例,一個檔案一個類別,用 CheckStyle/FindBugs 強制規範
測試單元測試放在與被測類別相同的 package 層級結構中;驗收測試、整合測試可用獨立 package 區分
產出物所有建置產出放在 target 目錄,此目錄不納入版本控制;最終產出 JAR/WAR/EAR 存入 artifact repository
函式庫可委託 Maven/Ivy 管理,或直接 check in 到 lib 目錄下依 runtime/test/build 分類;大型組織建議建立內部 repository

部署腳本#

三種部署模式#

模式說明
單機本地腳本最簡單,直接在目標機器上執行所有步驟
CI 代理模式撰寫本地腳本,由 CI server 的 agent 在遠端機器上執行。優勢是 CI server 提供任務管理、失敗重試、狀態儀表板等基礎設施
基礎設施管理工具(推薦)使用 Puppet、CfEngine、ControlTier 等工具,搭配 OS 打包系統,宣告式且冪等地將正確版本部署到所有機器

若以上工具都不可用,可用 Scp/Rsync + Ssh(Unix)或 PsExec/PowerShell(Windows)自行腳本化,也有 Fabric、Capistrano 等高階工具可用。

自行腳本化的部署無法妥善處理部分完成的部署、或新節點加入叢集需要佈建的情境,因此仍建議使用專業部署工具。

分層部署與測試#

部署應建立在已知良好的基礎上,採用分層策略

Figure 6.2: Deploying software in layers

部署層級由下而上:

  1. 作業系統
  2. 中介軟體及應用程式依賴的其他軟體
  3. 中介軟體與 OS 配置
  4. 應用程式二進位檔、服務與配置

環境配置的煙霧測試#

每一層部署完成後都應執行簡單的煙霧測試(smoke test),驗證該層是否正常運作,以便在問題發生時快速定位。

Figure 6.3: Deployment testing layers

煙霧測試範例:

  • 確認能從資料庫取得記錄
  • 確認能連上網站
  • 驗證 message broker 已註冊正確的訊息集
  • 透過防火牆發送 ping 以驗證流量通行與負載均衡

實用技巧#

始終使用相對路徑#

  • 絕對路徑會將建置綁定到特定機器配置,使得同一專案無法在同機器上多次 checkout
  • 預設使用相對路徑,將所有路徑設為相對於少數定義明確的根路徑(部署根、配置根等)
  • 不可避免時,將絕對路徑隔離在 properties 檔案中

消除手動步驟#

  • 第二次手動執行某操作時就該思考自動化;第三次時必須使用自動化流程
  • 手動部署文件永遠是過時或錯誤的,每次部署都需要排演,且無法重用前次經驗

從二進位檔建立到版本控制的追溯性#

  • 任何二進位檔都必須能追溯到產生它的版本控制修訂版
  • 方法:在 .NET assembly 嵌入版本元資料、在 JAR manifest 寫入修訂版標識
  • 若技術不支援嵌入元資料,可對每個二進位檔取 MD5 雜湊值,連同名稱和修訂版存入資料庫

不要將建置產出物 check in 到版本控制#

  • 建置產出物應存放在共用檔案系統或 artifact repository(如 Nexus)
  • 若需要重建,從原始碼重新建置是最佳做法
  • 將產出物 check in 會導致修訂版標識混淆

測試目標不應讓建置立即失敗#

  • 當某個測試 target 失敗時,應記錄失敗但繼續執行後續測試
  • 在建置流程結束後統一檢查並報告所有失敗
  • 這樣能在一次建置中獲得最完整的測試回饋

用整合煙霧測試約束應用程式#

  • 讓部署腳本在部署前驗證是否在正確的機器上執行
  • 對於批次處理程式(月結、季結、年結),安裝時就驗證配置,而非等到排程執行時才發現問題

總結#

建置與部署腳本是系統的一等公民(first-class citizen),應伴隨系統的整個生命週期。核心要點:

  1. 逐步演進:不要追求一步到位,從最痛苦的手動步驟開始自動化
  2. 統一機制:開發、測試、生產使用相同的部署腳本
  3. 跨職能協作:開發與維運共同參與腳本的設計與維護
  4. 品質對待:腳本需要版本控制、測試、重構,絕非實習生練手的工作
  5. 以管線為組織原則:建置腳本的結構應反映部署管線的階段