整合 Jenkins 和 Docker
這篇將會記述一些我自己整合 Jenkins CI 和 Docker 的思路、想法、要點以及備忘。不會有 step by step 的教學,若有此類需求請參考最後附錄。
Why Docker?
Jenkins 跑的好好的,為什麼要摻 Docker 呢?原本我們 Rails Rspec 跑的其實也不錯,但受限於 database 以及 elasticsearch, redis 等 services,無法同時跑多個 worker, 再加上未來若有平行化測試以及多個專案 / 不同 db 版本等等的需求,引入 docker 可以完美解決這些問題。
Concept
使用 Docker 的好處就是原本的 shell script 幾乎都不用改即可繼續使用,引入的門檻降到極低。
基本概念是建立一個可以跑 Rails app 起來的環境,然後把整個 CI 的 workspace 丟進去跑測試,其他的步驟都一模一樣。
在建立環境這邊基本上有兩個選擇,一種是全部包成一個 image, 就用這個 container 來跑測試。另一種是每個需要的 service 都是一個各自的 container, 彼此之間透過 Docker Container Linking 來通訊,例如 postgresql 自己一個、elasticsearch 自己一個、rails 自己一個這樣。
不過由於跑測試都是用過即丟,這次我直接採用最簡單的包一大包的策略來進行,減少複雜度。
我會選擇自己 Build docker 來跑測試主要是還想運用在其他地方,包括 trigger 不同的瀏覽器跑 feature tests 而不需重新 Build docker image 等等,如果沒有特殊需求的話也可以參考看看 Jenkins 的 Docker Plugin 基本概念是直接把 Jenkins slave 用 docker 跑起來。可以評估看看自己是否合用。
Base Image
我的設計是先建立一個 base image 例如給他 tag 叫 project/base
裡面先預裝好了所有環境包括 pg, elasticsearch, redis, rvm, ruby 等等。
舉例來說可能長這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
|
Test Image
接著用 project/base
build 一個 project/test
,這個 image 會安裝一些「只有測試會用到」的套件,順便把 Gemfile 複製進去安裝一下 Gem, 這樣到時在跑測試的時候就可以省略 bundle install
的時間了。因為我的 base image 還有打算拿來做其他用途,所以這邊是這樣設計。
每天凌晨三點左右用 crontab 重新 Build 一次這個 project/test
的 image 以更新 gems. 當然這邊牽涉到一些如何同步你專案中的 Gemfile 不過這都是簡單的 script 可以解決的問題,這邊不贅述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
注意這邊最後一段用到了上一篇文章 Docker Basics 中所講到的 ONBUILD, 使用這個功能我們就可以很輕鬆的 build 出真正用來測試的 Image.
這幾行實際做的動作是複製兩個 scripts 分別名叫 start_services.sh
和 run_tests.sh
並且 CMD
預設執行這兩個檔案。
但是這兩個檔案現在實際不存在,我會透過 jenkins 的 build scripts 來寫這兩個檔案,其實也就是原本在 build scripts 的內容移到這兩個檔案中了。之所以不把這兩個檔案存在某處再複製過來,就是想保留原本在 Jenkins configure 可以調整 Build Script 的機制,多留一點彈性。
為什麼要這麼麻煩使用 ONBUILD + CMD ,而不是直接 RUN 然後最後直接看 Image 有沒有建置成功就好?除了上述想多留一點彈性的原因外,還有用 Build 這樣 Build 的過程勢必會跑這兩支 script, 而我有可能 Build 完以後不跑這兩支 script, 而是做一些其他的動作例如 /bin/bash
進去 debug 等等,當然可以透過改寫這兩支 script 的內容來使 Build 過程不跑測試,但增添了複雜度。使用這個機制我覺得是最有彈性的。
Jenkins Build Script
Jenkins build script 這邊改動的幅度不大,原本的流程大概是:
改好相關 application.yml, database.yml 等等 local 設定檔並塞進去。
跑 db:reset 等等重置環境
跑測試
基本步驟還是一樣,第一步可以完全不用變,後面就得修改一下,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
一開始用 echo 寫入兩個檔案,內容大致就是開啟 service 並且開始跑測試,值得注意的是我們在 Jenkins workspace 裡寫了一個新的 Dockerfile, 裡面只有一行內容 FROM project/test
配合之前的 ONBUILD
就可以建置出這個 image. 之所以不直接用 <
的方式把內容丟到 docker build
指令,是因為 ADD
需要 context, 也就是 Jenkins workspace, 所以必須要寫實體的檔案出來。
Image tag 直接取用 Jenkins 的環境變數 $BUILD_NUMBER
因此像第 300 個 build 他的 image 就會叫 project/300
清楚明瞭。
Build 和 Run 都使用 --rm
來確保跑完以後就刪除,節省系統空間。當然如果有保留的需求,例如這個跑完以後自動 trigger 一個專門測 IE 的 selenium test target 的話這邊是可以不用刪除的,看個人需求。
Build 完以後也可以同時跑好幾個 containers,利用一個 image 可以跑很多 containers 的特性,例如把 spec 目錄分成幾區,同時開始跑測試,這樣平行處理可以節省時間。
這邊有一個問題就是 docker run
理論上要回傳 command 的 exit code 不過這部分常常出問題,時好時壞 所以這邊我決定自己來處理。
想法很簡單,直接把 docker run
的 output 拿來檢查,有偵測到爆炸的話就寫一個檔案,最後來檢查檔案,如果沒過就手動爆炸。等這個 bug 修復穩定之後,就可以不要使用這個 workaround 了。
1 2 3 4 5 6 7 8 9 10 |
|
如果你沒有遇到這個問題,或者你是使用 rspec_junit_formatter 之類的套件產生 JUnit 檔案的話並加掛 Post-build action 的話,這個動作會讀 JUnit 檔案的內容來改變 Build result 因此也不需要這個 workaround.
其他整合
Jenkins 有一個 plugin Github Pull Request Builder 可以讓 Jenkins 像 travis-ci 那類 service 在 Github 有人發 PR 時自動抓回來 Build。
Hipchat plugin 可以整合到公司通訊軟體。
rspec_junit_formatter 可以把 rspec 的結果產生成 JUnit 的 xml 給 Jenkins 讀取。
Test Coverage 的部分我們則是使用 SimpleCov 可以搭配 SimpleCov Rcov Formatter 產生 Jenkins 可讀的報表。
要使用以上這兩個套件,必須在測試跑完以後使用 docker cp <file> .
指令把報表複製回 workspace 讓 Jenkins 讀取。
參考連結
這邊列出一些不錯的連結:
- Docker quicktip #3 – ONBUILD
- Integrating Docker with Jenkins for continuous deployment of a Ruby on Rails application
- Using Docker To Run Ruby Rspec CI In Jenkins
- Your Dockerfile for Rails
- Using Docker to Parallelize Rails Tests
- How to Set Up TravisCI-like Continuous Integration with Docker and Jenkins