# 讓開發自動化: 部署自動化模式,第 2 部分
_更多一鍵式部署模式_
Java?部署常常很混亂,容易出現錯誤,需要許多手工操作,這會延誤向用戶交付軟件的時間。本文是分兩部分的讓開發自動化系列文章的第 2 部分。在本文中,自動化專家 Paul Duvall 進一步補充用于開發可靠、可重復且一致的部署流程的一些關鍵模式,幫助讀者為 Java 應用程序生成簡便的部署。
## 關于本系列
作為開發人員,我們致力于為用戶自動化流程;但許多開發人員疏忽了自動化我們自己的開發流程的機會。為此,我們編寫了 [_讓開發自動化_](http://www.ibm.com/developerworks/cn/java/j-ap/)系列文章,專門探討軟件開發流程自動化的實踐應用,為您介紹 _何時_以及 _如何_成功應用自動化。
部署是軟件創建過程中又一個適合實現自動化的方面。通過自動化部署,可獲得一個可靠、可重復的流程,其中好處頗多:更高的準確性、更快的速度和更好的控制。在這個分兩部分的系列文章的 [第 1 部分](http://www.ibm.com/developerworks/cn/java/j-ap01139/)中,我描述了 8 種部署自動化模式。在本期,我進一步擴大討論范圍,闡述另外 7 種同樣有益的部署方法:
* **Binary Integrity**,確保全部目標環境使用相同的工件。
* **Disposable Container**,使目標環境處于已知狀態,以減少部署錯誤。
* **Remote Deployment**,確保部署可以從一個集中化的機器或集群與多臺機器交互。
* **Database Upgrades**,提供一個集中管理的、腳本化流程,以便將增量更改應用到數據庫。
* **Deployment Test**,根據最近的部署,使用部署前和部署后檢查,確認應用程序按預期運行。
* **Environment Rollback**,如果部署失敗,回滾應用程序和數據庫更改。
* **Protected Files**,控制對構建系統使用的某些文件的訪問。
圖 1 解釋了本文闡述的部署模式之間的關系(未使用陰影的那些模式在 [第 1 部分](http://www.ibm.com/developerworks/cn/java/j-ap01139/)中介紹過):
##### 圖 1\. 部署自動化模式

這 7 個附加的部署自動化模式以之前 8 個模式為基礎,它們有助于創建一鍵式(one-click)部署。
## 一次編譯,部署到多個環境
**名稱**:Binary Integrity
**模式**:對于每個標記過的部署,每個目標環境中使用相同的歸檔文件(WAR 或 EAR)。
**反模式**:對于同一標記,為每個目標環境單獨進行編譯。
就這個話題與同事經過多次討論后,我最終站在了 _一次編譯,部署到多個目標環境_一邊,而不是 _在每個目標環境中編譯和打包_。例如,一次 Java 開發產生的部署工件是 Web 歸檔(WAR)或企業歸檔(EAR)文件。這個歸檔應該注冊到版本控制儲存庫中,并一次性貼上標記 —就像在 DEV 環境中那樣。
圖 2 解釋了 _一次編譯,部署到多個環境_這一概念:構建機器上生成的同一個 brewery.war 被部署到每個目標環境:
##### 圖 2\. 同一個 Web 歸檔被部署到不同的目標環境

Ant 提供一個 `checksum`任務 —使用 Message-Digest algorithm 5(MD5)hash 算法 —以確保構建機器上編譯和打包的文件就是部署到每個目標環境的文件。
有人會爭辯說,雖然工件可能相同,但每個目標環境的部署配置是不同的。也就是說,當使用 Single-Command、Scripted Deployment 時,無論它是否為相同的歸檔,很多自動化流程可以改變應用程序的輸出。確實如此;但是,您還需要花不必要的時間解決問題,因為在 STAGE 環境中使用與 QA 環境不同的 JDK 版本編譯和打包軟件。并且,當 DEV 中使用的來自一個集中式依賴管理儲存庫(例如 Ivy 或 Maven)的 JAR 與準備(staging)環境中的那些 JAR 不同時,失敗的機率就會增加。這些風險使我確信,為了確保二進制代碼的完整性,必須一次性編譯和打包,以便部署到多個環境。
* * *
## 用一次性容器降低部署成本
**名稱**:Disposable Container
**模式**:通過將安裝和配置解耦,使 Web 和數據庫容器的安裝和配置自動化。
**反模式**:手動將容器安裝到每個目標環境并進行配置。
在較早一期的 [_讓開發自動化_](http://www.ibm.com/developerworks/cn/java/j-ap/)文章 “[持續集成反模式,第 2 部分](http://www.ibm.com/developerworks/cn/java/j-ap03048/)” 中,談到了為什么清理一個 “受污染的” 環境有助于防止出現誤判或漏判的構建。Disposable Container 可以減少在使用持久性容器時可能出現的很多問題。Disposable Container 模式基于兩個原則:_完全移除所有容器組件_,以及 _將容器的安裝與配置分離_。對于有些人,尤其是系統工程師來說,這似乎是一個極端的概念,因為不再要求由一個單獨的團隊管理和模糊化容器,不讓開發人員或其他人接觸到它們。然而,考慮到部署期間經常出現的、代價不菲的問題,它可以讓所有團隊成員的利益最大化。
## 一鍵式部署
經常有一些團隊和我說:“是的,我們已經實現了自動化部署。”當我問一些簡單的問題時 —例如 “輸入一條簡單的命令(例如 `ant`)就可以生成一個有效的軟件應用程序嗎?” —回答通常是這樣的:“是的,一旦安裝和配置好 Web 容器……”,或者 “是的,一旦設置好數據庫”。我對于一個真正自動化的部署的定義是,應該能夠從一臺干凈的機器開始,安裝 Java 平臺和 Ant(有一些方法可以免除這個步驟),然后輸入一條 _簡單命令_,即可得到一個可以正常工作的軟件應用程序。如果做不到,就不算是 “一鍵式” 部署,并且在部署過程中將出現代價不菲的人員方面的瓶頸問題。
如圖 3 所示,Disposable Container 模式基于這樣一個原則:_一切_都應該在 _系統_中 —(使用 [第 1 部分](http://www.ibm.com/developerworks/cn/java/j-ap01139/)中提到的 Repository 模式)—而不是在某個人的頭腦中。
##### 圖 3\. 部署期間移除和安裝容器

清單 1 中的 Ant 腳本從 Internet 下載 Tomcat ZIP,移除之前的部署殘留的任何容器,然后解壓、安裝和啟動 Tomcat:
##### 清單 1\. 用 Ant 腳本編寫的部署,該腳本移除、重新安裝、啟動和配置容器
```
<!-- Check to see if Tomcat is running prior to this -->
...
<exec executable="sh" osfamily="unix" dir="${tomcat.home}/bin" spawn="true">
<env key="NOPAUSE" value="true" />
<arg line="shutdown.sh" />
</exec>
<delete dir="${tomcat.home}" />
<get src="${tomcat.binary.uri}/${tomcat.binary.file}"
dest="${download.dir}/${tomcat.binary.file}" usetimestamp="true"/>
<unzip dest="${target.dir}" src="${download.dir}/${tomcat.binary.file}" />
<exec osfamily="unix" executable="chmod" spawn="true">
<arg value="+x" />
<arg file="${tomcat.home}/bin/startup.sh" />
<arg file="${tomcat.home}/bin/shutdown.sh" />
</exec>
<xmltask source="${appserver.server-xml.file}"
dest="${appserver.server-xml.file}">
<attr path="/Server/Service[@name='${s.name}']/Connector[${port='${c.port}']"
attr="proxyPort"
value="${appserver.external.port}"/>
<attr path="/Server/Service[${name='${s.name}']/Connector[${port='${c.port}']"
attr="proxyName"
value="${appserver.external.host}"/>
</xmltask>
<!-- Perform other container configuration -->
...
<echo message="Starting tomcat instance at ${tomcat.home} with startup.sh" />
<exec executable="sh" osfamily="unix" dir="${tomcat.home}/bin" spawn="true">
<env key="NOPAUSE" value="true" />
<arg line="startup.sh" />
</exec>
```
通過使環境處于已知狀態,并以一種受控制的方式部署容器,可以減少很多常見的、引發大部分部署難題的部署錯誤。
* * *
## 在多個外部環境中運行命令
**名稱**:Remote Deployment
**模式**:使用一個集中式機器或集群將軟件部署到多個目標環境。
**反模式**:在每個目標環境中通過手動方式在本地應用部署。
一旦安裝了數據庫和 Web 容器,讓部署在開發人員的 _工作站_上運行是件非常簡單的事情。然而,開發與生產之間有著巨大的差異。如果組織有多個項目和不同的目標環境(例如測試或準備環境),那么常常需要從一個單獨的環境集中地管理部署:一臺機器或一個集群。團隊常使用一臺構建服務器來管理每個目標環境的部署。在 [第 1 部分](http://www.ibm.com/developerworks/cn/java/j-ap01139/)中,我介紹了 Headless Execution 模式,該模式采用公共和私有密鑰,所以不必手動登錄到每臺機器。如圖 4 所示,Remote Deployment 依賴于 Headless Execution、Single Command 和 Scripted Deployment 模式,因此可以方便地部署到遠程機器:
##### 圖 4\. 用于多個環境的構建管理服務器

要從集中構建服務器遠程部署軟件,需要使用一些機制安全地進行遠程復制和運行命令。我將討論的兩個機制使用 Secure Copy(SCP)和 Secure Shell(SSH)。如清單 2 所示,在 Scripted Deployment 模式下,集中構建機器上生成的 Web 歸檔被遠程地復制到一個目標環境:
##### 清單 2\. 將 war 文件安全地從一臺機器復制到另一臺機器
```
<target name="copy-tomcat-dist">
<scp file="${basedir}/target/brewery.war"
trust="true"
keyfile="${basedir}/config/id_dsa"
username="bobama"
passphrase=""
todir="pduvall:G0theD!stance@myhostname:/usr/local/jakarta-tomcat-5.5.20/webapps" />
</target>
```
當 WAR 文件被安全地復制到遠程目標環境時,我就可以在 Java Secure Channel 中使用 `SSHExec`之類的任務運行任何 SSH 命令,這一切都是從集中構建機器中遠程執行的。另一種方法是 `ssh`到遠程環境,并在本地運行命令。這樣可以減少來回的遠程傳輸,并縮短部署時間。
* * *
## 使數據庫和數據處于已知狀態
**名稱**:Database Upgrade
**模式**:使用腳本和數據庫在每個目標環境中應用增量更改。
**反模式**:在每個目標環境中手動應用數據庫和數據更改。
在圖 5 中,可以看到一個在 Scripted Deployment 中使用自動化腳本更新數據庫的例子:
##### 圖 5\. 自動應用增量數據庫更新

在較早一期的 _讓開發自動化_文章 “[實現自動化數據庫遷移](http://www.ibm.com/developerworks/cn/java/j-ap08058/)” 中,我談到了以自動化的方式應用增量數據庫更改的必要性。和 Scripted Deployment 中的其他部分一樣,數據庫更新腳本被簽入到儲存庫中。
LiquiBase(參見 [參考資料](#resources))是用于將增量更改應用到數據庫的工具,可使同樣的更改作為 Scripted Deployment 的一部分應用到每一個目標環境。在清單 3 中,一個 SQL 腳本被作為 LiquiBase changelog 的一部分調用。然后,Scripted Deployment(使用一種構建腳本工具實現 —例如 Ant)調用這個 changelog(使用 XML 定義)。
##### 清單 3\. 從 LiquiBase 更改集運行定制的 SQL 文件
```
<changeSet id="1" author="jbiden">
<sqlFile path="insert-distributor-data.sql"/>
</changeSet>
```
要學習和應用自動化數據庫更新,還有相當多的事情要做,但其主旨是在 Scripted Deployment 中執行更新,使所有數據庫更改都在 _系統_中,而不是一個寫好的程序或存在某個人的頭腦中。
* * *
## 冒煙測試部署
**名稱**:Deployment Test
**模式**:將自測試功能編寫到 Scripted Deployments 中。
**反模式**:通過運行手動功能測試來驗證部署,沒有關注特定于部署的方面。
圖 6 解釋了在部署前后運行部署測試的一個例子:
##### 圖 6\. 對應用程序運行功能部署測試

在清單 4 中,我使用 Ant 執行部署前測試,以確認正在使用的版本是正確的工具版本。在 Scripted Deployment 中,腳本可以檢查正在使用的端口(這可能導致 Web 容器部署失敗),檢查與數據庫的連接,檢查容器是否已被啟動,以及很多其他內部部署測試。
##### 清單 4\. 運行部署前檢查,確保部署有效
```
<condition property="ant.version.success">
<antversion atleast="${ant.check.version}" />
</condition>
<antunit:assertPropertyEquals name="ant.version.success" value="true" />
<echo message="Ant version is correct." />
<echo message="Validating Java version..."/>
<condition property="java.major.version.correct">
<equals arg1="${ant.java.version}" arg2="${java.check.version.major}" />
</condition>
<antunit:assertTrue message="Your Java SDK version must be 1.5+. \
You must install correct version.">
<isset property="java.major.version.correct"/>
</antunit:assertTrue>
```
更全面的部署測試可以確保應用程序的 _功能性_是正確的。通過使用用于 Web 應用程序的 Selenium 或用于客戶機應用程序的 Abbot 之類的工具編寫 _特定于部署_的自動化功能測試,可以驗證是否已正確應用了部署更改。可以將這些測試看作 _冒煙測試(smoke tests)_:只需測試受部署影響的功能。例如,表 1 展示了使用 Selenium 和其他用于 Web 應用程序的工具的一些方式:
##### 表 1\. 部署測試
| 部署測試 | 描述 |
| --- | --- |
| 數據庫 | 編寫一個自動化功能測試,該測試將數據插入到數據庫。驗證數據是否被輸入到數據庫中。 |
| 簡單郵件傳輸協議(Simple Mail Transfer Protocol,SMTP) | 編寫一個自動化功能測試,該測試從應用程序發送一個電子郵件消息。 |
| Web 服務 | 使用 SoapAPI 之類的工具提交一個 Web 服務,并驗證輸出。 |
| Web 容器 | 驗證所有容器服務是否正確運行。 |
| 輕量級目錄訪問協議(Lightweight Directory Access Protocol,LDAP) | 使用應用程序,通過 LDAP 進行驗證。 |
| 日志記錄 | 編寫一個測試,該測試使用應用程序的日志記錄機制編寫日志。 |
自動化測試不僅僅用于測試用戶功能。通過創建側重于部署測試的套件,可以檢驗部署的有效性,減少下游錯誤和開發成本。
* * *
## 回滾所有部署更改
**名稱**:Environment Rollback
**模式**:當部署失敗后,提供自動的 Single Command 更改回滾。
**反模式**:手動回滾應用程序和數據庫更改。
圖 7 解釋了回滾數據庫更改 —使用 Database Upgrade —以及回滾 Web 部署的自動化過程:
##### 圖 7\. 回滾部署更改

不管是否執行自動化部署,當部署失敗時,最好有一種方式可以回滾更改。在某些情況下,錯誤的更改可能導致系統中斷,使組織損失數百萬美元。要執行 Environment Rollback,需要讓目標環境回到部署前的狀態。為此,實際上每個更改都需要一個回滾腳本。Web 部署常常需要回滾更多的更改。Environment Rollback 的一個例子是在部署前復制歸檔(例如一個 WAR 文件),并為每個更改提供回滾數據庫腳本。另外還需要重新應用已應用于 Web 容器的配置更改。
清單 6 演示了一個示例,該示例使用 LiquiBase 為每個前滾語句提供一個回滾語句。我將添加一個名為 `brewery`的新表,同時提供一個相應的 `dropTable`回滾語句。
##### 清單 6\. 當應用增量數據更新時提供回滾過程
```
<changeSet id="rollback-database-changes" author="bobama">
<createTable tableName="brewery">
<column name="id" type="int"/>
</createTable>
<rollback>
<dropTable tableName="brewery"/>
</rollback>
</changeSet>
```
這個簡單的例子僅用于說明問題,并不意味著回滾就是這么簡單。恢復到前一個部署常常是一個復雜的、費時的過程(需要實現自動化)。用于編寫回滾腳本的時間應該與部署失敗付出的代價成比例。
* * *
## 保護信息不被窺探
**名稱**:Protected Files
**模式**:使用儲存庫,只允許經過授權的團隊成員共享文件。
**Antipattern**:在團隊成員的機器上管理文件,或者將文件存儲在可由已授權團隊成員訪問的共享驅動器上。
圖 8 展示了一個受保護的版本控制儲存庫,它用于存放只有已授權人員或系統可以訪問的文件:
##### 圖 8\. 使用受保護的版本控制儲存庫存放敏感文件

在某些情況下,并不是所有團隊成員都應該訪問特定于環境的數據。但是,將該信息與部署腳本分離又可能使腳本無法執行。當討論 Headless Execution 模式時,我描述了使用 SSH 密鑰和 Java Secure Channel 工具復制文件,并安全地運行遠程命令,而不需要人為輸入命令。使用 Externalized Configuration 處理的屬性很可能包含不應該讓所有團隊成員看到的數據。為了確保實現 Headless Execution,同時防止 .properties 文件中的數據被窺探,我使用的技巧是將這些文件簽入到一個受保護的儲存庫中。
在清單 7 中,我配置了一個由 Apache 托管的 Subversion 儲存庫,先拒絕所有用戶訪問某個目錄,然后顯式地添加某些用戶:
##### 清單 7\. 在 Apache 上使用 Subversion 保護一個 Subversion 儲存庫
```
<DirectoryMatch "^/.*/(\.svn)/">
Order deny,allow
Deny from all
Allow bobama,jbiden,hclinton
</DirectoryMatch>
```
通過保護對 Subversion 儲存庫的訪問,可以將一個 Scripted Deployment 設為被允許的用戶,使其不必輸入密碼就可以訪問屬性,從而通過 SSH 密鑰定義的方式實現 Headless Execution。
* * *
## 一鍵式部署
除了這份分兩部分的系列文章中描述的 15 個部署自動化模式外,我還歸納了更多的模式,但是這 15 個模式大概可以應對我遇到的 80% 的部署情況。每個模式都是為了幫助在每個目標環境中實現真正的一鍵式 / 單命令部署。希望您一切順利!
### 結束語
這是我的 [_讓開發自動化_](http://www.ibm.com/developerworks/cn/java/j-ap/)系列的最后一篇文章。在兩年多的時間里與您一起分享我的經驗,令我感覺像在經歷一場有趣的探險。編寫這個系列的目的是展示如何以及為何自動化大量軟件開發過程,使開發人員可以將更多的時間花在感興趣的問題上,而不是將時間浪費在重復的、容易出錯的活動上。在這個系列中,我演示了如何自動化代碼檢查以便適當地重構、增量式地升級應用程序數據庫、應用 Continuous Integration 實踐和工具、每次更改時運行自動測試、生成 GUI 安裝程序、創建一鍵式部署、使開發人員自動生成文檔、執行依賴管理、利用版本控制儲存庫,以及有效地使用各種構建腳本和工具。希望您在閱讀這個系列時能夠充滿樂趣。
- 讓開發自動化
- 讓開發自動化: 部署自動化模式,第 2 部分
- 讓開發自動化: 部署自動化模式,第 1 部分
- 讓開發自動化: 使用基于向導的安裝程序
- 讓開發自動化: 針對廣大開發人員的并行開發
- 讓開發自動化: 實現自動化數據庫遷移
- 讓開發自動化: 持續重構
- 讓開發自動化: 文檔化一鍵通
- 讓開發自動化: 利用 Ivy 管理依賴項
- 讓開發自動化: 自動負載測試
- 讓開發自動化: 使用自動化加速部署
- 讓開發自動化: 持續集成反模式
- 讓開發自動化: 斷言架構可靠性
- 讓開發自動化: 持續測試
- 讓開發自動化: 用 Eclipse 插件提高代碼質量
- 讓開發自動化: 除掉構建腳本中的氣味
- 讓開發自動化: 選擇持續集成服務器
- 讓開發自動化: 持續檢查