用 Docker, maven, jenkins 完成 CI
隨著特征分支的演進以及git使用的增加,對于分支的持續集成成了基礎架構的夢魘。Docker 可以用來消除到同一遠程服務器部署生產和構建集成測試的需要。規模控制可通過 jenkins 的 slaves 同時運行一個或者多個任務來實現。
問題
Git 將主干分支合并變得簡單化。一個對待開發的正常方法流程是根據每個需求特點各建立一個特征分支,當分支需求開發測試完成,即將特征分支合并到主干。對于單元測試,現在已經有了現成可行的工具可幫助你在合并分支到主干前進行自動化測試特征分支。Travis-ci 已經提供了這樣的工具到 Github 工程,推薦前去嘗試!集成測試仍然存在問題。當你需要一個運行環境來進行復雜的集成測試,管理配置這些各不相同的分支和環境會變得有點困難。你可以選擇為不同的特征分支分別建立虛擬機進行來回的切換測試或者嘗試讓這些分支共享一個環境配置。或者最糟糕的選擇:集成到同一分支后再進行集成測試。這個選擇的問題在于它違背了“無損害”的原則:當你知道它不必去拆分內容時你會去完成分支的合并,但現在卻有了一個合并的分支卻注定需要拆分?對我來說這是很不好的設計。
部署集成的舊方式
部署一個測試環境通常就意味著在服務器上部署一個新版本的應用。其他內容都需要準備就緒。我以一個以java為基礎的 CMS作為例子:Hippo. Hippo 包括兩個 WAR 文件:cms.war 和 site.war. 這兩個 WARs 通過共享一些公有的 jars 包部署到 tomcat 實例。如果你是基于標準 maven 原型構建的工程,構建過程將會產生一個壓縮包,你可以順利的將它解壓到 tomcat 的運行目錄。以下我運行于 jenkins 的 deploy.sh 腳本的偽代碼:- 將工程壓縮包通過 SCP 上傳到服務器
- ssh 登錄到服務器
- 停止 tomcat 進程
- 刪除 "work", "webapps", "shared", 和"common" 目錄
- 將上傳的壓縮包解壓到 tomcat 目錄
- 啟動 tomcat </ul>
操作完,我們將等待 tomcat 的啟動部署 webapps. 這樣的過程包含了將一個腳本復制到服務器上去運行檢查在 catalina.out 文件中是否存在 "server startup in xxx" 字符串這樣一個步驟。一個流程下來有些步驟需要 root 權限。雖然這些都是能夠實現的并已經有很大一部分的實踐先例,但增加了復雜程度,而且對于一個簡單的集成化測試目標,顯得也有些呆板。
Docker
Docker 能夠實現在一個機器上將多個進程分別運行于各自獨立的容器,而無需大量的虛擬機。它可以讓進程隔離,對于進程來說就像各自運行在自己獨立的環境中。 Docker 基于 Go 語言開發,是 LXC 的接口,在 linux 內核 3.8 版本發布時作為一個新的功能點。Docker 在 ubuntu 系統中運行順利,紅帽也準備好兼容 Docker.(實際已經實現)
Docker 可以讓你用 Dockerfile 文件去啟動特定的容器,這些特點可以用來與適用于虛擬機的 vagrantfile 進行比較。這些 Docker 文件可以由其他的 Docker 文件組成,建立一種繼承/復合的容器。
基于 Docker 構建集成服務器
我在 Github 上創建一個示例工程,演示了如何借助 Docker 實現 Hippo 工程的集成測試。如果你使用 vagrant, 你可以發現在工程的根目錄下的 vagrant 啟動 jenkins 服務器。借助于 Dockerfile 的命令,我建立了一個 jdk 7 的鏡像,可以作為 tomcat 的基礎鏡像。我又用這個 tomcat 鏡像作為集成的環境基礎。這個用于構建集成鏡像的 Dockerfile 放置在工程的根目錄下,內容如下:FROM wouterd/tomcat MAINTAINER Wouter Danes "https://github.com/wouterd" ADD myhippoproject.tar.gz /tmp/app-distribution/ RUN for i in $(ls /tmp/app-distribution/) ; do mkdir -p /var/lib/tomcat6/${i} && cp -f /tmp/app-distribution/${i}/* /var/lib/tomcat6/${i}/ ; done
wouterd/tomcat 鏡像構建如下:
FROM wouterd/oracle-jre7 VOLUME ["/var/log/tomcat6"] MAINTAINER Wouter Danes "https://github.com/wouterd" RUN apt-get install -y tomcat6 CMD JAVA_HOME=/usr/lib/jvm/java-7-oracle CATALINA_BASE=/var/lib/tomcat6 CATALINA_HOME=/usr/share/tomcat6 /usr/share/tomcat6/bin/catalina.sh run EXPOSE 8080
wouterd/oracle-jre7 是在純凈的 ubuntu 系統上安裝 jdk 7 的鏡像。這些代碼都在 Github 工程 Docker-images 目錄中。wouterd/tomcat 鏡像完成了以下這些內容:
- FROM wouterd/oracle-jre7 表示以 wouterd/oracle-jre7 為基礎鏡像(包括了 ubuntu + oracle java 7)
- VOLUME ["/var/log/tomcat6"] 告訴容器暴露這個文件路徑給外界。Docker 實際上 "物理地" 將這個路徑放置在這個容器外部從而使其他容器可以共享到這個文件。等會兒我會展示這將是一個很好的設計。
- RUN apt-get install -y tomcat6 通過 ubuntu 維護的庫安裝 tomcat6
- CMD [some bash] 設置容器運行時執行的命令,在這個例子中設置了兩個環境變量,并運行 catalina.sh 腳本
- EXPOSE 8080 暴露容器 8080 端口到宿主機 </ul>
- 得到 wouterd/tomcat 鏡像
- ADD [file] [destination] 將 maven 工程的壓縮包復制到臨時的目錄,并進行解壓
- RUN 命令是一個比較曲折的方法去復制壓縮包中的內容到 TOMCAT_HOME 目錄,由于簡單的添加壓縮包到 TOMCAT_HOME 可能不能正確的解壓。如果 TOMCAT_HOME 目錄是空的,這個命令將會執行。
- 將 wouterd/tomcat 鏡像的 CMD 命令繼承下來,所以當你使用 Docker run 命令啟動集成的容器,它將會啟動 tomcat 并開始部署。 </ul>
Docker 鏡像在集成測試完成兩件事的過程中得到創建(不知道怎么翻譯合適):
用容器實現持續集成
既然我們已經準備好建立集成服務器需要的一切,我們可以開始將他們匯總在一起進行構建。我將使用 jenkins 作為構建服務器,但完全可以用你自己的構建服務器進行替換,像 Go, Hudson 或者 Bamboo。集成測試也可以使用 mvn 運行,如果你是使用不低于 3.8 版本內核的 linux 系統或者 boot2Docker-cli 的 MacOS X 系統。參考以下的工程要求部分,按照要求的步驟和軟件在你自己的機器上運行 mvn。用 boot2Docker 測試
為了能夠用 boot2Docker 完成集成測試,你需要在命令行設置 -Dboot2Docker=[IP-of-boot2Docker-vm],如:mvn verify -Dboot2Docker=192.168.59.103. 接下來你將看到怎么為 boot2Docker 虛擬機設置 IP, 你需要設置的是 eth1 的 IP:
Wouters-MacBook-Pro-2:hippo-Docker wouter$ boot2Docker-cli ssh Warning: Permanently added '[localhost]:2022' (RSA) to the list of known hosts. Docker@localhost's password: ## . ## ## ## == ## ## ## ## === /""""""""""""""""_/ ===~ {~~~~~~ / ===- ~~~ ____ o / \ \ / ______/ _ _ | | | ||__ \ | | _ | | __ __ | ' \ / \ / | | ) / ` |/ \ / | |/ / _ \ '| | |) | () | () | | / / (| | () | (| < / | |_./ _/ _/ _|___,|_/ _||\__|| boot2Docker: 0.8.0 Docker@boot2Docker:~$ ifconfig Docker0 Link encap:Ethernet HWaddr 56:84:7A:FE:97:99 inet addr:172.17.42.1 Bcast:0.0.0.0 Mask:255.255.0.0 inet6 addr: fe80::5484:7aff:fefe:9799/64 Scope:Link UP BROADCAST MULTICAST MTU:1500 Metric:1 RX packets:71349 errors:0 dropped:0 overruns:0 frame:0 TX packets:119482 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:3000501 (2.8 MiB) TX bytes:169514559 (161.6 MiB)eth0 Link encap:Ethernet HWaddr 08:00:27:F6:4F:CB inet addr:10.0.2.15 Bcast:10.0.2.255 Mask:255.255.255.0 inet6 addr: fe80::a00:27ff:fef6:4fcb/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:415492 errors:0 dropped:0 overruns:0 frame:0 TX packets:125189 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:603256397 (575.3 MiB) TX bytes:7233415 (6.8 MiB)
eth1 Link encap:Ethernet HWaddr 08:00:27:35:F0:76 inet addr:192.168.59.103 Bcast:192.168.59.255 Mask:255.255.255.0 inet6 addr: fe80::a00:27ff:fe35:f076/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:591 errors:0 dropped:0 overruns:0 frame:0 TX packets:83 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:94986 (92.7 KiB) TX bytes:118562 (115.7 KiB)
lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope:Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:40 errors:0 dropped:0 overruns:0 frame:0 TX packets:40 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:8930 (8.7 KiB) TX bytes:8930 (8.7 KiB)</pre>
你可以通過在工程根目錄下運行 vangrant up 創建 jenkins 服務器。在初始化和啟動之后,可通過 http://localhost:8080 訪問 jenkins.
在構建過程中,maven 工程完成了以下這些內容:
- compile 編譯工程
- test 執行單元測試
- package 建立壓縮包
- pre-integration-test 創建 Docker 鏡像并啟動新容器,等待 tomcat 完成啟動
- integration-test 用 junit, webdriver, phantomjs 在容器中完成集成測試
- post-integration-test 停止并刪除容器以及刪除創建的鏡像文件 </ul>
所有有趣的部分在 "myhippoproject" 工程中的 "integrationtests" 模塊:
pre-integration-test 和 post-integration-test 是通過 exec-maven-plugin 插件運行 shell 腳本完成的。其實已經有了很好的 Docker api java 客戶端和 Docker 的 maven 插件可以使用,但在我編寫這些腳本時還沒發現。以下是啟動腳本。這個腳本可用不超過10行的 java 代碼實現。
#!/bin/bashif [[ ${BOOT_2_Docker_HOST_IP} ]] ; then echo "Boot2Docker specified, this will work if you use the new boot2Docker-cli VM.." boot2Docker='yes' Docker_run_args='-p 8080' else boot2Docker='' Docker_run_args='' fi
set -eu
work_dir="${WORK_DIR}" Docker_file="${Docker_FILE_LOCATION}" distribution_file="${DISTRIBUTION_FILE_LOCATION}" Docker_build_dir="${work_dir}/Docker-build"
mkdir -p ${work_dir}
mkdir -p ${Docker_build_dir}
cp ${Docker_file} ${distribution_file} ${Docker_build_dir}/
image_id=$(Docker build --rm -q=false ${Docker_build_dir} | grep "Successfully built" | cut -d " " -f 3) echo ${image_id} > ${work_dir}/Docker_image.id
rm -rf ${Docker_build_dir}
catalina_out="/var/log/tomcat6/catalina.$(date +%Y-%m-%d).log"
container_id=$(Docker run ${Docker_run_args} -d ${image_id}) echo ${container_id} > ${work_dir}/Docker_container.id
container_ip=$(Docker inspect --format '{{.NetworkSettings.IPAddress}}' ${container_id})
echo -n "Waiting for tomcat to finish startup..."
Give Tomcat some time to wake up...
while ! Docker run --rm --volumes-from ${container_id} busybox grep -i -q 'INFO: Server startup in' ${catalina_out} ; do sleep 5 echo -n "." doneecho -n "done"
if [[ ${boot2Docker} ]] ; then
This Go template will break if we end up exposing more than one port, but by then this should be ported to Java
code already (famous last words...)
tomcat_port=$(Docker inspect --format '{{ range .NetworkSettings.Ports }}{{ range . }}{{ .HostPort }}{{end}}{{end}}' ${container_id}) tomcat_host_port="${BOOT_2_Docker_HOST_IP}:${tomcat_port}" else tomcat_host_port="${container_ip}:8080" fi
echo ${tomcat_host_port} > ${work_dir}/Docker_container.ip</pre>
針對遠端運行Docker這個腳本通過一定的技巧解決了端口問題(這里應該有問題),只不過是運行一些 Docker 命令去建立鏡像和啟動容器。我使用 Docker 自帶功能去管理 tomcat 容器中的日志文件。通過 tomcat 容器中的 VOLUME 命令,我指定 /var/log/tomcat6 目錄為數據卷,我可以通過 --volumes-from 啟動一個新的 tomcat 容器來得到這個目錄。以下這個命令在容器中執行 grep 來確定服務是否已經啟動:Docker run --rm --volumes-from ${container_id} busybox grep -i -q 'INFO: Server startup in' ${catalina_out} --rm 參數的作用是在啟動容器后立即停止并刪除容器,這個對于我們來說是好的,我們在服務啟動之前將會執行很多次這樣的操作。
這個腳本片段如下:<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <executions> <execution> <id>build-and-start-integration-server</id> <goals> <goal>exec</goal> </goals> <phase>pre-integration-test</phase> <configuration> <environmentVariables> <WORK_DIR>${project.build.directory}/Docker-work</WORK_DIR> <Docker_FILE_LOCATION>${project.basedir}/src/main/assembly/Dockerfile</Docker_FILE_LOCATION> <DISTRIBUTION_FILE_LOCATION>${project.build.directory}/myhippoproject.tar.gz</DISTRIBUTION_FILE_LOCATION> </environmentVariables> <executable>${project.basedir}/src/test/script/start-integration-server.sh</executable> </configuration> </execution> </executions> </plugin>
package 是使用 maven-assembly-plugin 插件去創建壓縮包,所創建的壓縮包將需要解壓到 tomcat 容器中的 CATALINA_HOME 目錄。
這個 integration-test 階段是通過 webdriver 和 phantomis driver 實現的。但你可以使用你自己喜歡的測試工具用于集成測試。一個例子是使用集成的 apache CXF REST 代理去驗證你創建的 rest 接口。
工程要求
- 安裝 Docker
- 安裝 PhantomJS,設置好路徑
- 不要求 git,但需要能夠得到這個工程
- Maven 3.x
- Java 7+ (Oracle JDK preferred)
- 你需要運行 ./build-Docker-images.sh 來創建 jdk 7 和 tomcat 鏡像,才能進行集成測試 </ul>
原文鏈接:Continuous Integration Using Docker, Maven and Jenkins
來自:http://dockone.io/article/501