JenkinsとDockerでTravisっぽいCIサーバを育ててみている
最近プライベートなプロジェクトのCIにはcircleciとかが人気なんでしょうかね。
GitHub 時代のデプロイ戦略 - naoyaのはてなダイアリー
近頃のCIサーバはアプリケーションのテストだけじゃなく、インフラのテストやデプロイ、ChatOpsなどgitやチャットツールなど他のシステムと連携した自動化のための必須プラットフォームといった感じになってきてる。とはいえ、趣味で開発しているプロダクトに余計なコストはかけたくない。ああ、でもやっぱCIはしたい。
そんなわけで以前から契約だけしていて放置気味だった、さくらのVPSの1GにjenkinsをたててオレオレCIを育てているのでその辺のことを書いてみる。CIの実行環境はDockerを使って仮想化し、ジョブの内容はTravisやcircleciのようにリポジトリ側のyamlファイルに記述できるようにしてみた。構成をざっくりと図解すると以下のようになる。

また参考にさせていただいたのは以下の記事。
Docker + Jenkins + travis.yml parser 作って Travis っぽいものを作った話 - from scratch
Use Docker + Jenkins to run GitHub tests
jenkinsでのunitテストは、dockerでクリーン環境を作って行う!!shinofara's Blog (*´ω`*) | shinofara's Blog (*´ω`*)
ベースとなっている環境
CentOS6.5の上に構築している。さくらのVPSのデフォルトがそうなのと、個人的に使い慣れているというのが最大の理由。ただCentOSのDockerはカーネル周りのバグでディスク領域が開放されないことがあるらしいので、今後移行するかもしれない。
Docker on CentOS 6.5 で詰んだのでメモ - sonots:blog
CentOS6系のカーネルに上記のバグフィックスがバックポートされるのも期待している。。。まあ今のところ、仕事でつかっているわけではなく、ディスク領域にも余裕があるのと環境構築はchefでなるだけ自動化しながら作っているので、ディスクが詰まったら最悪、環境を再インストールすればいいかなという判断でやっている。
Dockerのインストール
Dockerのインストール自体について特記することはあまりない。epelリポジトリを使えるようにしておいて以下の様なchefのレシピを書いた。実際にやってることはyum install docker-ioと/etc/init.d/docker startと同じだ。
docker/recipes/default.rb
package "docker-io" do action :install end service "docker" do action [:enable, :start] end
jenkinsのインストール
CentOS上のjenkinsのインストールについては以前に記事を書いた。今回はそれをちょこちょこ修正した(yumリポジトリからインストールするなど)。詳細は以下にペーストしたレシピの内容を見てもらえばいいが、jenkinsからdockerを実行するために
jenkinsユーザのuidを明示的(uid:45678)に指定したりdockerグループの追加したりしている。Dockerコンテナ内でジョブを実行するユーザとjenkinsの実行ユーザのIDは同じにしておかないといろいろパーミッション周りでハマる。
jenkins/recipes/default.rb
group "jenkins" do gid 45678 action [:create, :manage] end user 'jenkins' do comment 'Jenkins Continuous Build server' uid 45678 group 'jenkins' home '/var/lib/jenkins' shell '/bin/false' password nil action [:create, :manage] end script "install_jenkins_yum_repo" do interpreter "bash" user "root" cwd "/tmp" code <<-EOH wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key EOH not_if "test -e /etc/yum.repos.d/jenkins.repo" end package "jenkins" do action :install end service "jenkins" do action [:enable, :start] end group "docker" do action [:modify] members ["jenkins"] append true end
jenkins cliとプラグインのインストール
jenkinsのプラグインもchefでインストールさせるため以下のようなレシピを書いた。jenkinsは起動までに時間がかかるのでscript[install-jenkins-cli]でjenkins-cli.jarをダウンロードする際に10秒待ち、HTTPレスポンス503はリトライする仕組みにしている。
jenkins/attributes/default.rb
default['jenkins']['cli_url'] = "http://127.0.0.1:8080/jnlpJars/jenkins-cli.jar" default['jenkins']['jenkins_url'] = "http://127.0.0.1:8080/" default['jenkins']['wait_for_boot'] = "10" default['jenkins']['plugins'] = [ "git", "cloverphp", "simple-theme-plugin", "jquery", "gravatar", "disk-usage", "envinject", "extra-columns", "categorized-view", "ci-skip", "timestamper", "monitoring", "view-job-filters", "locale", "sidebar-link", "pegdown-formatter", "ansicolor" ]
jenkins/templates/default/jenkins.rb
#!/usr/bin/env bash java -jar /usr/lib/jenkins/jenkins-cli.jar -s <%=node['jenkins']['jenkins_url']%> "$@"
jenkins/recipes/default.rb
template "/usr/local/bin/jenkins" do
source "jenkins.erb"
owner "root"
group "root"
mode "0755"
end
cli_url = node['jenkins']['cli_url']
wait_for_boot = node['jenkins']['wait_for_boot']
script "install-jenkins-cli" do
interpreter "bash"
user "root"
cwd "/tmp"
code <<-EOH
sleep #{wait_for_boot}
http_response_code=503
while [ $http_response_code -eq 503 ]
do
http_response_code=`curl -LI #{cli_url} -o /dev/null -w '%{http_code}' -s`
sleep 5
done
wget -t 5 --waitretry 5 -O /usr/lib/jenkins/jenkins-cli.jar #{cli_url}
EOH
not_if "test -e /usr/lib/jenkins/jenkins-cli.jar"
end
directory "/var/lib/jenkins/updates" do
owner "jenkins"
group "jenkins"
mode "0755"
action :create
end
# https://issues.jenkins-ci.org/browse/JENKINS-10061
# https://gist.github.com/rowan-m/1026918
script "update-jenkins-updatecenter" do
interpreter "bash"
user "jenkins"
cwd "/tmp"
code <<-EOH
curl -L http://updates.jenkins-ci.org/update-center.json | sed '1d;$d' > /var/lib/jenkins/updates/default.json
EOH
not_if "test -e /var/lib/jenkins/updates/default.json"
end
node['jenkins']['plugins'].each do |plugin_name|
execute "install-jenkins-plugin-" + plugin_name do
user "root"
command "/usr/local/bin/jenkins install-plugin " + plugin_name
action :run
not_if "/usr/local/bin/jenkins list-plugins | awk '{print $1}' | grep ^#{plugin_name}$"
notifies :run, "execute[jenkins-safe-restart]"
end
end
execute "jenkins-safe-restart" do
command "/usr/local/bin/jenkins safe-restart"
action :nothing
end
yamlパーサとコンテナの起動スクリプト
CIのジョブはTravisのようにリポジトリ側のyamlで制御、設定できるようにした。yamlファイルはこんな感じ。
.jenkins.yml
container:
image: jenkins-ci-base
before_script:
- composer install --dev --no-interaction
script:
- php vendor/bin/phpunit -c phpunit-ci.xml.dist
これをパースしてimageで指定されたDockerイメージをrunする。この辺の処理は使い慣れたPHPで実装した。ソースは公開していないが、具体的には以下のようなことをやっている。
- リポジトリのルートにある
.jenkins.ymlをパース。 before_scriptやscriptで指定された内容からそれぞれbefore_script.shやscript.shのようなスクリプトファイルを生成してJenkinsのワークスペースに出力する。- 出力したスクリプトファイルを順に起動する
start.shをJenkinsのワークスペースに出力する。 imageで指定されたdockerイメージにワークスペースをマウントしてコンテナを起動。具体的にはdocker run -v $WORKSPACE:/home/worker/workspace -w /home/worker/workspace -u worker $IMAGE /bin/bash -l start.shのようなコマンドを実行する。- dockerが処理を終えたあと
docker rmを実行してコンテナを削除する。
この一連の処理を行うPHPスクリプトをjenkinsのジョブ設定の「ビルド」->「シェルの実行」で起動するように設定しておく。
Dockerイメージ
Dockerイメージはあらかじめ作っておくのだが、jenkinsから起動するために以下の決められた仕様で構成している
- uid
45678のworkerユーザがいる(ホスト側のjenkinsユーザと同じuid) /home/worker/workspaceディレクトリがある(ホスト側のjenkinsワークスペースがマウントされる)workerはパスワードなしでsudoできる
この仕様を満たす基本的なDockerイメージを作成するDockerfileは以下のようになる
FROM centos:centos6 # basic settings RUN rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm RUN rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm RUN yum -y groupinstall "Base" "Development tools" RUN yum -y install --enablerepo=remi,epel \ sudo \ readline \ readline-devel \ compat-readline5 \ libxml2-devel \ libxslt-devel \ libyaml-devel \ git \ make \ autoconf \ automake \ bison \ libtool \ sysstat \ gettext \ traceroute \ openssl \ openssl-devel \ curl \ wget # add worker user RUN useradd -u 45678 -d /home/worker -m -s /bin/bash worker && \ mkdir /home/worker/workspace && \ chown worker:worker /home/worker/workspace && \ echo "worker ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/worker && \ sed -i 's/.*requiretty$/#Defaults requiretty/' /etc/sudoers # Enable to run sudo in the script RUN sed -i 's/.*requiretty$/#Defaults requiretty/' /etc/sudoers # timezone RUN echo 'ZONE="Asia/Tokyo"' > /etc/sysconfig/clock && \ rm -f /etc/localtime && \ ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime ################################# # default behavior is to login by worker user ################################# CMD ["su", "-", "worker"]
これで、dockerコンテナ内でworkerというユーザが処理を実行する。あとは必要な環境ごとにカスタマイズしたDockerイメージを用意しておけば、いろんな環境でCIができる。ちょうどTravisがPHPやRubyといった言語ごとのテスト環境を用意してくれるように、例えばPHPをインストールしたjenkins-ci-phpやRubyをインストールしたjenkins-ci-rubyというDockerイメージを作っておいて、
container:
image: jenkins-ci-php
や
container:
image: jenkins-ci-ruby
などと.jenkins.ymlで指定すればいい。
まとめ
ここ数日運用してみての感想だが、これはかなりいい感じ。現在の自分の用途の範囲では、ほぼTravisでやれることがプライベートでも実現できている。 CIの実行環境はDockerによって独立し常に使い捨てにされるので、サーバ設定を丸ごと書き換えるようなプロビジョニングのテストなどにも使えると思う。
あとjenkinsのテーマをアトラシアン風にする djonsson/jenkins-atlassian-theme · GitHub があるので、これをちょっとカスタマイズして、UIの見た目も変えてみた。

やっぱし、コンソール表示は黒背景がいい!