JenkinsとDockerを使い、動作環境の異なるアプリをCIする

私が属する組織では受託開発がメインでして、サーバーやミドルウェアなどの要件を我々が自由に決定できないケースもしばしば。
基本はPHPなのですが、案件によってバージョンが異なったり、近頃ではNodeの案件なども出てきました。

長いこと「CIサーバを導入したい」と思いつつもアプリ毎にCIサーバを立ち上げるのもメンドイコスト等の関係で現実的ではないので、頭を悩ませていました。
そんな中、Dockerを使って解決している先人がいましたので、早速、試してみました。

目次

  • 前提環境
  • 先人の知恵まとめ
  • 実際の手順
    1. VagrantでJenkins + Dockerのサーバーを用意する
    2. デモアプリ(PHP, Node)を用意する
    3. Dockerファイルを作成する
    4. テスト実行用のスクリプトを作成する
    5. Jenkinsにジョブを作成し、ビルドする
  • 今後の課題など

長いです。。。

前提環境

OS
Windows 8
Virtual Box
4.3.20
Vagrant
1.7.2

先人の知恵まとめ

いろいろと調べていたところ、以下のようなポイントがあることがわかりました。

JenkinsユーザーをDockerグループに所属させる

→JenkinsのジョブでDockerコンテナを操作できるようにするため

JenkinsのworkspaceをDockerにマウントする

→JenkinsとDockerとでシームレスにファイルを利用できるようにするため

Dockerfileはアプリに同梱しておく

→だって開発中にインフラ要件変わるかもでしょ?

アプリ毎に異なる初期設定やテストの手順はスクリプトにまとめてアプリに同梱しておく

→Jenkinsのビルド手順を共通化するため

実際の手順

1. VagrantでJenkins + Dockerのサーバーを用意する

JenkinsとDockerを導入したCentOS6.5をVagrantで構築します。

$ vagrant box add centos65-x86_64-20140116 https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box
$ vagrant init centos65-x86_64-20140116

作成されたVagrantfileを編集します。

  • ホストオンリーネットワークを有効にする
  config.vm.network "private_network", ip: "192.168.33.10"
  • 初期設定およびJenkinsとDockerを導入するプロビジョニングコードを記述する
  config.vm.provision "shell", inline: <<-SHELL
    sudo cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
    sudo sh -c 'echo ZONE=\"Asia/Tokyo\" > /etc/sysconfig/clock'
    sudo sh -c 'echo UTC=\"false\" >> /etc/sysconfig/clock'
    sudo sh -c "source /etc/sysconfig/clock"

    sudo yum update -y
    sudo yum install -y wget

    sudo rpm -ivh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/6/i386/epel-release-6-8.noarch.rpm
    sudo yum install -y docker-io
    sudo chkconfig docker on

    sudo yum install -y java-1.7.0-openjdk
    sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
    sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
    sudo yum install -y jenkins
    sudo chkconfig jenkins on
    sudo sed -i -e 's@JENKINS_ARGS=""@JENKINS_ARGS="--prefix=/jenkins"@' /etc/sysconfig/jenkins
    sudo sed -i -e 's@JENKINS_JAVA_OPTIONS="-Djava.awt.headless=true"@JENKINS_JAVA_OPTIONS="-Djava.awt.headless=true -Dhudson.util.ProcessTree.disable=true"@' /etc/sysconfig/jenkins
    sudo usermod -G docker jenkins

    sudo yum clean all
  SHELL

VMを作成します。

$ vagrant up

成功したらVMを再起動し、ブラウザで、http://192.168.33.10:8080/jenkinsにアクセス、jenkinsが正常に動作していることを確認します。
jenkinsが正常に動作していたら、以下を行っておきます。

  • Git Pluginのインストール
  • JenkinsがGitリポジトリにアクセスするためのSSHキーの設定

2. デモアプリ(PHP, Node)を用意する

ごくごく簡単なFizzBuzzをテストするアプリをPHP, Nodeのそれぞれで作成しました。

PHP

piccagliani/php-fizzbuzz · GitHub

テストはCodeceptionで記述しました。

Node

piccagliani/node-fizzbuzz · GitHub

テストはmochaで記述しました。

3. Dockerファイルを作成する

$APPROOT/.docker/Dockerfileをそれぞれに作成しました。

PHP

CentOS + PHP + Composerとします。

FROM centos:6

MAINTAINER @piccagliani

RUN cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN echo ZONE="Asia/Tokyo" > /etc/sysconfig/clock
RUN echo UTC="false" >> /etc/sysconfig/clock
RUN source /etc/sysconfig/clock

RUN yum update -y
RUN rpm -Uvh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/6/i386/epel-release-6-8.noarch.rpm
RUN rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
RUN yum install -y --enablerepo=remi --enablerepo=remi-php56 php php-mbstring php-mcrypt

RUN curl -sS https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer

CMD ["/bin/bash"]
Node

Ubuntu + NodeJS + npmとします。

FROM ubuntu:14.04

MAINTAINER @piccagliani

RUN cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

RUN apt-get update
RUN apt-get -y upgrade
RUN apt-get install -y nodejs npm
RUN update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10

CMD ["/bin/bash"]

4. テスト実行用のスクリプトを作成する

それぞれのアプリに$APPROOT/.docker/run-tests.shを作成し、テスト実行用のスクリプトを記述しました。

PHP
#!/bin/bash

cd /opt/php-fizzbuzz
composer install
vendor/bin/codecept run
Node
#!/bin/bash

cd /opt/node-fizzbuzz
npm install
npm test

/opt/php-fizzbuzz/opt/node-fizzbuzzには、Jenkinsのワークスペースがマウントされます。

5. Jenkinsにジョブを作成し、ビルドする

それぞれのアプリ用のジョブを以下のように作成しました。

ジョブ名

リポジトリの名前に合わせました。

ジョブの形式

「フリースタイル・プロジェクトのビルド」を選択。

ソースコード管理

Gitを選択し、リポジトリのURL、資格情報を設定。

ビルド手順

「シェルの実行」を選択し、以下を記述。

docker build -t $JOB_NAME $WORKSPACE/.docker/
docker run -v $WORKSPACE:/opt/$JOB_NAME $JOB_NAME sh /opt/$JOB_NAME/.docker/run-tests.sh

ジョブが作成できたら、それぞれビルドを実行してみます。
初回はコンテナの作成に時間がかかると思いますが、2度目からはキャッシュが使われるようになるため、ササッといくはずです。

あとは、GitのHookなり、ポーリングなりを利用すればCIできますね!

最後に. 今後の課題など

より複雑なテストはどうなる?

今回は単純なアプリのテストなので、Dockerfileもテスト手順もシンプルでした。
より複雑な手順が必要となる、Seleniumを利用したWEBアプリのE2Eテストなどもこの方式でできるのかどうか、引き続き実験してみます。

パーミッションまわり

今回はユーザー権限まわりを気にしていないので、すべてrootで実行されます。
このあたりも問題になる可能性がありますね。どうするのがBest Practiceなのでしょう?

テスト実行後にコンテナを破棄したい

何度もビルドしていると、以下のように古いコンテナが残ってしまいます。

$ sudo docker ps -a
CONTAINER ID        IMAGE                 
a858a6348358        php-fizzbuzz:latest   
876216e216ac        node-fizzbuzz:latest  
4f93cfe9961a        node-fizzbuzz:latest  
582b56657583        node-fizzbuzz:latest  
a1ef176f0497        php-fizzbuzz:latest   

これら、破棄した方がいいと思いますが、jenkinsでどうやるんでしょう?



以上となります。
Docker、「foregroundプロセスがないとダメ」とかいろいろ躓いたところはありましたが、使いこなせるようになると強力ですね。
これでようやくCIサーバーを導入できそうです。