DockerでJenkinsサーバ(master/slave)を構築してみる

概要

タイトルどおり、DockerでJenkinsをマスター・スレーブ構成で作成します。

f:id:knjname:20140503205510p:plain

読者はDockerとJenkinsをちょっとずつやったことがある人を想定しています。

このエントリで使っているソースは全てGithubリポジトリにて入手可能です

どうやってやるか

DockerでJenkinsのマスターのイメージとスレーブのイメージをそれぞれ作って、それぞれ単純に立ち上げる。

マスターのイメージについては、おそらく1インスタンスしか立ち上げないが、スレーブについてはNインスタンス立ち上げるイメージ。

Dockerでやるメリットとデメリット

メリット

  • すぐに使い捨てスレーブ作れるよ。Dockerがインストールされていればそこにスレーブ作れるよ。
  • Jenkinsマスターも量産しようと思えばできるよ。
  • ビルドにどんなツールが必要なのかDockerfile見りゃわかるよ。

デメリット

素でやるのに比べたらまじで色々とめんどい。

Jenkins構成概要

マスターとスレーブ(1つ以上)にわけて構成します。どんなに小規模であってもそうします。

マスターノード

マスターノードは基本的に何もビルドさせません。また、ビルドに必要なツールを持つ必要すらありません。

初期はJavaとJenkinsだけインストールしてある状態ですね。

下記の役割を受け持ちます。

  • Webインタフェースの提供
  • プラグイン・設定・ビルド履歴の保持(永続情報の保持)
  • スレーブの管理

スレーブノード

スレーブノードが基本的にビルドを行います。

よって、ビルドに必要なツール(Antとかgccとか)やパッケージはスレーブで持ちます。

こうすることにより下記のメリットがあります。

  • マスターノードに依存してしまう、ジョブの作成を防ぐ
  • スレーブノードを好きなマシンに作成することができる

今回はDockerで構成するのでスレーブの複製・使い捨ても簡単です。

SSHでスレーブを構成します。

Dockerfile構成概要

上記の構成にあわせ、Dockerfileはマスターノード用とスレーブノード用にわけます。

マスター用Dockerfile

マスター用Dockerfileは下記要件を満たしている必要があります。

  • JDKインストール済みであること
  • Jenkinsインストール済み+立ち上がっていること
  • 細かい設定が環境変数で指定可能であること (→ここらへんは後述のrun.shにて実現しています)
    • 例)メモリetc JVMオプションが指定可能であること。
      • 言うまでもなく必須条件ですね。
    • 例)JNLPポート
      • → Docker上のJenkinsにWindowsスレーブ追加する場合はJNLPポートが固定かつ外部から接続可能である必要になる場合が多いはずです。そしてこのポート番号は基本的には内外一致する必要があります。(トンネル接続オプションを使えばたぶん回避可能。)そしてそのポート番号はconfig.xmlに書いてあります。これはできれば環境変数で指定できるとクールです。
    • 例)Jenkinsのログの出力場所
    • 例)JenkinsのURLのprefix
      • http://your_host/prefix にJenkinsを起動させたい場合も多いかと思います。(フロントにnginxをかませる場合など)これも環境変数で指定できるとクールです。

ログフォルダやJENKINS_HOME(ビルド履歴、プラグインなどが入るディレクトリ)は全て外部ボリュームとしてdocker run時に指定し、永続化させます。

(外部ボリュームを指定したほうがIOパフォーマンスは出るようです。 参考:LXCベースのDockerゲストマシンとホストマシンのディスクI/O性能を比較検証(Bonnie++編) - Y-Ken Studio))

スレーブ用Dockerfile

スレーブ用Dockerfileは下記要件を満たしている必要があります。

  • JDKインストール済みであること
  • ビルドに必要なツールをインストール済みであること
  • Open SSHサーバインストール済み+立ち上がっていること。
    • マスターが使用する公開鍵のログインを許容すること

今のところLinux以外のスレーブは想定してませんが、Linuxで多彩なスレーブが必要であればスレーブ用Dockerfileのバリエーションを作っておくことにより、色々なビルド要件に合わせることが可能です。

スレーブについて、起動時、ワークスペースを外部ボリュームにするかは任意ですが、ワークスペースを再構成するためのコスト(チェックアウトしなおしetc)のパフォーマンスが気になる人は外部ボリュームにしておきましょう。

Dockerfile実装例

マスター用Dockerfile

ここではknjname/jenkins-masterとしてDockerイメージをビルドします

基本的には、JDKSSHクライアント(多分要らない)入れ、SSH鍵をローカルに持っておいて(Jenkinsの設定次第では不要)、Jenkinsをインストールして、run.shという起動用のスクリプトファイルをコピーしているだけです。

起動用のrun.shというスクリプトについて

Dockerコンテナ起動時の環境変数で色々とJenkinsの挙動を制御できるようにしたいので、Dockerから叩くためのbashシェルスクリプトファイルとしてrun.shを作成し、追加しています。

(今回はもともとJenkinsのrpmに入っているサービススクリプト用ファイルをベースに自分の都合のいいように書き換えました。)

こういう便利な起動スクリプトが出来てしまうのは、Docker界隈ではよくあることです。

下記オプションが環境変数で指定可能です。-eで実行時に指定して下さい。(今回はJNLPポートしか指定しません)

: ${JENKINS_WAR:="/jenkins/bin/jenkins.war"}
: ${JENKINS_HOME:="/jenkins/home"}
: ${JENKINS_LOG_DIR:="/jenkins/logs"}
: ${JENKINS_URL_PREFIX:="/"}
: ${JENKINS_JNLP_PORT:=""}
: ${JENKINS_JAVA_CMD:="java"}
: ${JENKINS_USER:="root"}
: ${JENKINS_JAVA_OPTIONS:="-Djava.awt.headless=true"}
: ${JENKINS_PORT:="8080"}
: ${JENKINS_LISTEN_ADDRESS:=""}
: ${JENKINS_HTTPS_PORT:=""}
: ${JENKINS_HTTPS_LISTEN_ADDRESS:=""}
: ${JENKINS_AJP_PORT:="-1"}
: ${JENKINS_AJP_LISTEN_ADDRESS:=""}
: ${JENKINS_DEBUG_LEVEL:="5"}
: ${JENKINS_ENABLE_ACCESS_LOG:="no"}
: ${JENKINS_HANDLER_MAX:="100"}
: ${JENKINS_HANDLER_IDLE:="20"}
: ${JENKINS_ARGS:=""}

スレーブ用Dockerfile

ここではknjname/jenkins-slaveとしてDockerイメージをビルドします

基本的には、JDKとOpenSSHサーバ、他ビルドに必要なツールをインストールして、マスターノードをそのままSSHログインさせるためのauthorized_keysとその他(多分いらない)を持ち、最後にSSHDサーバを起動しているだけですね。

動かしてみよう

マスターノード

下記のコマンドで動かします。

-p ~~:~~ でDockerコンテナ内外のポートを結びつけています。8080でJenkinsのウェブインタフェース、10080でJNLPポートにバインドする構成です。

-e JENKINIS_JNLP_PORT=やらの指定でJenkinsの細かいオプションを指定しています。今回はJenkinsのJNLPポートを指定してみました。

-v ~~:~~ のところで、永続化のためにログディレクトリやJENKINS_HOMEのための外部ディレクトリ(ボリューム)を指定しています。

立ち上がるとブラウザでアクセス可能なはずです。

f:id:knjname:20140503171338p:plain

これにスレーブの設定を追加したいところですね。

スレーブノード

下記のコマンドで動かします。

今回は特に外部ボリュームでスレーブのワークスペースは外部指定しませんでしたが、パフォーマンスを考えると-v どっか適当なディレクトリ:/jenkins/ws したほうがいいです。

マスターとスレーブを連動させる

今までの手順を踏むと docker ps は下記に近い状態になっているはずです。

root@ubuntuserver:~/jenkins-docker-example/master# docker ps
CONTAINER ID        IMAGE                           COMMAND                CREATED             STATUS              PORTS                                              NAMES
03cf6daec38f        knjname/jenkins-master:latest   /bin/sh -c 'bash /je   18 minutes ago      Up 18 minutes       0.0.0.0:8080->8080/tcp, 0.0.0.0:10080->10080/tcp   jenkins-master
c77efec916c7        knjname/jenkins-slave:latest    /bin/sh -c '/usr/sbi   About an hour ago   Up About an hour    0.0.0.0:10022->22/tcp                              jenkins-slave

またここでは、 192.168.11.6サーバに上記コンテナが立ち上がっており、http://192.168.11.6:8080 で上記Jenkinsサービスにアクセスできるものとします。

さてさて!ここからjenkins-masterからjenkins-slaveをSSH経由でノード追加してみましょう。

Jenkinsの管理 → ノードの管理 → 新規ノード作成 で下記のようなノードを追加します。(ポートの指定忘れないように!!)

f:id:knjname:20140503174210p:plain

今回は認証情報にてシステムローカルの~/.ssh(Dockerfileにて追加済み)からSSH認証を読むようにしています。本当はSSH秘密鍵はどっかに別だしのほうがいいですね。それかユーザ名、パスワード認証でもいいでしょう。

そしてマスターのビルド機能は不要になったので、特定ジョブ専用にします。

f:id:knjname:20140503174430p:plain

上記例だと同時実行数も0にしていますが、config.xmlをジョブ経由でバックアップしたい場合など、マスターにしかできないジョブが発生すると考える場合は、わざわざ同時実行数を0にしなくていいと思います。

とりあえずできあがりました。

f:id:knjname:20140503174701p:plain

なんか実行してみましょう。

f:id:knjname:20140503174947p:plain

ちゃんとスレーブ上で実行できましたね!

スレーブ量産してみた

せっかくのDockerなんで、スレーブをいっぱい作ってみました。

root@ubuntuserver:~/jenkins-docker-example/slave# docker ps
CONTAINER ID        IMAGE                           COMMAND                CREATED             STATUS              PORTS                                              NAMES
c985b3b13e37        knjname/jenkins-slave:latest    /bin/sh -c '/usr/sbi   2 seconds ago       Up 2 seconds        0.0.0.0:20004->22/tcp                              jenkins-slave-20004
3b051e7e66ae        knjname/jenkins-slave:latest    /bin/sh -c '/usr/sbi   5 seconds ago       Up 5 seconds        0.0.0.0:20003->22/tcp                              jenkins-slave-20003
7dd880653474        knjname/jenkins-slave:latest    /bin/sh -c '/usr/sbi   7 seconds ago       Up 7 seconds        0.0.0.0:20002->22/tcp                              jenkins-slave-20002
7bf196ba719f        knjname/jenkins-slave:latest    /bin/sh -c '/usr/sbi   11 seconds ago      Up 10 seconds       0.0.0.0:20001->22/tcp                              jenkins-slave-20001
05ebb98e8ecc        knjname/jenkins-slave:latest    /bin/sh -c '/usr/sbi   19 minutes ago      Up 19 minutes       0.0.0.0:10022->22/tcp                              jenkins-slave
03cf6daec38f        knjname/jenkins-master:latest   /bin/sh -c 'bash /je   49 minutes ago      Up 49 minutes       0.0.0.0:8080->8080/tcp, 0.0.0.0:10080->10080/tcp   jenkins-master

f:id:knjname:20140503175726p:plain

ただ、同じホストでたくさん同じスレーブを動しても全く旨味がないので、実際Dockerでスレーブ量産する時は、自分でスレーブのイメージをプライベートリポジトリとかに登録しておき、他マシンのDockerからそれをロードしてスレーブを起動させましょう。

Windowsと繋いでみた

JNLPポートの確認と設定をして…

f:id:knjname:20140503171721p:plain

Windowsスレーブもたててみましょう!

f:id:knjname:20140503190301p:plain

普通にいけますね。

やったほうがいいかもしれないこと

  • 今回はDockerでJenkinsを立ち上げるまでの話なので、この後、当然素のJenkinsでも必要になってくる、JENKINS_HOMEのバックアップとか、設定XMLのバックアップとか、追加プラグインのインストールとかが必要になってきます。
  • スレーブはDockerのオプションでCPU優先度やメモリの制限かけちゃったほうが運用が安定すると思います。
  • JNLPポートについて、トンネル接続オプション使えば別に内外のポートを合わせる必要もないので、内側のポートを特定番号に固定してもいいかもしれません。(今回やってて途中まで気付かなかった。。。)
  • SSHの鍵はもうちょっと上手に管理しましょう。
  • ってか、もっと楽なスレーブの作り方あるんじゃね。

まとめ

  • Jenkinsはマスターとスレーブ分けよう。マスターは実務させない。スレーブに実務させる。
  • Dockerは開発用サーバセットアップでも役に立つ。