整合 Jenkins 和 Docker

http://www.tuicool.com/articles/6FBfamA

整合 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 自己一個這樣。

不過由於跑測試都試用過即丟,這次我直接採用最簡單的包一大包的策略來進行,減少複雜度。

Base Image

我的設計是先建立一個 base image 例如給他 tag 叫 project/base 裡面先預裝好了所有環境包括 pg, elasticsearch, redis, rvm, ruby 等等。

舉例來說可能長這樣:

 
FROM ubuntu:12.04
MAINTAINER hSATAC

# We use bash
RUN rm /bin/sh && ln -s /bin/bash /bin/sh

# We don't like apt-get warnings.
ENV DEBIAN_FRONTEND noninteractive

# Add the PostgreSQL PGP key to verify their Debian packages.
# It should be the same key as https://www.postgresql.org/media/keys/ACCC4CF8.asc
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8

# Add PostgreSQL's repository. It contains the most recent stable release
#     of PostgreSQL, ``9.3``.
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" > /etc/apt/sources.list.d/pgdg.list

# Update the Ubuntu and PostgreSQL repository indexes
RUN apt-get update

# === Locale ===
RUN locale-gen  en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
ENV LC_ALL en_US.UTF-8

# === Requirements ===
RUN apt-get -y -q install nodejs libpq-dev wget git curl imagemagick vim postfix

RUN cd /tmp &&\
  wget http://downloads.sourceforge.net/project/wkhtmltopdf/0.12.0/wkhtmltox-linux-amd64_0.12.0-03c001d.tar.xz &&\
  tar Jxvf wkhtmltox-linux-amd64_0.12.0-03c001d.tar.xz &&\
  cd wkhtmltox &&\
  install bin/wkhtmltoimage /usr/bin/wkhtmltoimage

# === Redis ===
RUN apt-get -y -q install redis-server

# === Elasticsearch ===
RUN apt-get install openjdk-7-jre-headless -y -q
RUN wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.1.0.deb
RUN dpkg -i elasticsearch-1.1.0.deb

# === RVM ===
RUN curl -L https://get.rvm.io | bash -s stable
ENV PATH /usr/local/rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN /bin/bash -l -c rvm requirements
RUN source /usr/local/rvm/scripts/rvm && rvm install ruby-2.1.2
RUN rvm all do gem install bundler

# === Postgresql ===
RUN apt-get -y -q install python-software-properties software-properties-common
RUN apt-get -y -q install postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3

# Update template1 to enable UTF-8 and hstore
USER postgres
RUN    /etc/init.d/postgresql start &&\
    psql -c "update pg_database set datistemplate=false where datname='template1';" &&\
    psql -c 'drop database Template1;' &&\
    psql -c "create database template1 with owner=postgres encoding='UTF-8' lc_collate='en_US.utf8' lc_ctype='en_US.utf8' template template0;" &&\
    psql -c 'CREATE EXTENSION hstore;' -d template1

# Create a PostgreSQL role and db
RUN    /etc/init.d/postgresql start &&\
    psql --command "CREATE ROLE jenkins LOGIN PASSWORD 'jenkins' SUPERUSER INHERIT CREATEDB NOCREATEROLE NOREPLICATION;" &&\
    createdb -O jenkins jenkins_test &&\
    createdb -O jenkins jenkins_production


# Adjust PostgreSQL configuration
RUN echo "local all  all  md5" > /etc/postgresql/9.3/main/pg_hba.conf

# And add ``listen_addresses`` to ``/etc/postgresql/9.3/main/postgresql.conf``
RUN echo "listen_addresses='*'" >> /etc/postgresql/9.3/main/postgresql.conf

Test Image

接著用 project/base build 一個 project/test ,這個 image 會安裝一些「只有測試會用到」的套件,順便把 Gemfile 複製進去安裝一下 Gem, 這樣到時在跑測試的時候就可以省略 bundle install 的時間了。因為我的 base image 還有打算拿來做其他用途,所以這邊是這樣設計。

每天凌晨三點左右用 crontab 重新 Build 一次這個 project/test 的 image 以更新 gems. 當然這邊牽涉到一些如何同步你專案中的 Gemfile 不過這都是簡單的 script 可以解決的問題,這邊不贅述。

 
FROM project/base

# Switch back to root
USER root

# Set ENV
ENV RAILS_ENV test

# Preinstall gem
ADD gem /opt/project_gem # 這個 gem 目錄裡面有 Gemfile 和 Gemfile.lock
WORKDIR /opt/project_gem
RUN rvm all do bundle install

# Install Firefox & Xvfb
RUN apt-get -y install firefox xvfb # 跑 selenium test 用的

# Onbuild witch back to root
ONBUILD USER root

# Mound Rails directory
ONBUILD ADD . /opt/project
ONBUILD WORKDIR /opt/project

# Bundle install
ONBUILD RUN rvm all do bundle install

# Go
ONBUILD ADD start_services.sh /opt/project/start_services.sh
ONBUILD RUN chmod +x /opt/project/start_services.sh
ONBUILD ADD run_tests.sh /opt/project/run_tests.sh
ONBUILD RUN chmod +x /opt/project/run_tests.sh
ONBUILD CMD /opt/project/start_services.sh && /opt/project/run_tests.sh

注意這邊最後一段用到了上一篇文章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 這邊改動的幅度不大,原本的流程大概是:

  1. 改好相關 application.yml, database.yml 等等 local 設定檔並塞進去。

  2. 跑 db:reset 等等重置環境

  3. 跑測試

基本步驟還是一樣,第一步可以完全不用變,後面就得修改一下,例如:

 
# Start services in docker
echo "
Xvfb :99 -screen 0 1366x768x24 -ac 2>/dev/null >/dev/null &
/etc/init.d/postgresql start &&\
/etc/init.d/redis-server start &&\
/etc/init.d/elasticsearch start
" > start_services.sh

# Run tests
echo "
rvm all do bundle exec rake db:migrate &&\
DISPLAY=:99 rvm all do bundle exec rspec spec --format=documentation
" > run_tests.sh

echo "FROM project/test" > Dockerfile
docker build --rm -t project/$BUILD_NUMBER .
docker run --rm project/$BUILD_NUMBER

一開始用 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 了。

 
docker run --rm project/$BUILD_NUMBER | perl -pe '/Failed examples:/ && `echo "fail" > docker-tests-failed`'
docker rmi project/$BUILD_NUMBER

if [ ! -f docker-tests-failed ]; then
  echo -e "No docker-tests-failed file. Apparently tests passed."
else
  echo -e "docker-tests-failed file found, so build failed."
  rm docker-tests-failed
  exit 1
fi

其他整合

Jenkins 有一個 plugin Github Pull Request Builder 可以讓 Jenkins 像 travis-ci 那類 service 在 Github 有人發 PR 時自動抓回來 Build。

另外也有 hipchat plugin 可以整合到公司通訊軟體,這部分和主題相關薄弱就不多談了。


你可能感兴趣的:(Continuous,integration)