引言#
當專案規模超越單人、時間跨越數日、或產出不只一個執行檔時,僅靠 IDE 來建置與測試已不足夠。建置與部署腳本(build and deployment scripts)是持續交付的基石,必須像對待產品程式碼一樣認真維護——納入版本控制、定期測試、持續重構。
自動化部署遠比「把檔案丟到伺服器」複雜:需要設定應用程式、初始化資料、配置作業系統與中介軟體、模擬外部系統等。這些步驟一旦沒有自動化,就會隨專案複雜度成長而愈發脆弱且容易出錯。
部署方式的決策必須由開發人員與維運人員共同參與,因為雙方都需要熟悉所使用的技術。
建置工具概覽#
所有建置工具的核心都是相依性網路(dependency network):你指定一個目標,工具會計算出正確的任務執行順序,且每個前置任務只執行一次。

Figure 6.1: A simple build dependency network
任務導向 vs. 產品導向#
建置工具的關鍵差異在於它們是 task-oriented 還是 product-oriented:
| 類型 | 代表工具 | 狀態管理方式 | 特點 |
|---|---|---|---|
| Task-oriented | Ant, NAnt, MSBuild | 不保留跨次建置的狀態,靠內部標記避免重複執行 | 適合有內建增量編譯的語言(如 C#) |
| Product-oriented | Make, 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)**原則,提供標準專案結構與自動相依管理
- 三大問題:
- 專案結構不符 Maven 預設假設時,極難客製化
- 擴展需撰寫 plugin(Mojo),門檻較高
- 預設自動更新,可能導致建置不可重現(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
部署層級由下而上:
- 作業系統
- 中介軟體及應用程式依賴的其他軟體
- 中介軟體與 OS 配置
- 應用程式二進位檔、服務與配置
環境配置的煙霧測試#
每一層部署完成後都應執行簡單的煙霧測試(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),應伴隨系統的整個生命週期。核心要點:
- 逐步演進:不要追求一步到位,從最痛苦的手動步驟開始自動化
- 統一機制:開發、測試、生產使用相同的部署腳本
- 跨職能協作:開發與維運共同參與腳本的設計與維護
- 品質對待:腳本需要版本控制、測試、重構,絕非實習生練手的工作
- 以管線為組織原則:建置腳本的結構應反映部署管線的階段