Jenkins + dockerでテストの並列化

追記: 2016/01/31

2016/01/18ごろにWorkflowプラグインは、Pipeline プラグインに改名されたようです。
https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Plugin

---

自動テストでデータベースなどのリソースの問題で並列実行ができないときに、Dockerを使ってリソースを用意して並列化する方法をまとめました。

なるべく実践的な環境を想定して以下のことができることを目標とします。

  * GitLabと連携してマージリクエストに対してテストを実行する
  * 手動でブランチ名を指定してジョブを実行できるようにする
  * １つのテストジョブを分割して並列処理できるようにする


構成
======

構築した環境の構成は以下の通りです。

  * OSX
  * Docker v1.9.1
  * Jenkins v1.625.3
    * dockerイメージを使用 https://hub.docker.com/_/jenkins/
  * Jenkins Git Plugin v2.4.1
    * https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin
  * Jenkins GitLab Plugin v1.1.28
    * https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Plugin
  * Jekins Parallel Test Executor Plugin v1.7
    * https://wiki.jenkins-ci.org/display/JENKINS/Parallel+Test+Executor+Plugin
  * Jenkins Parameterized Trigger plugin v2.30
    * https://wiki.jenkins-ci.org/display/JENKINS/Parameterized+Trigger+Plugin
  * Jenkins Workflow Plugin v1.12, Workflow Multibranch
    * https://wiki.jenkins-ci.org/display/JENKINS/Workflow+Plugin
  * Jenkins CloudBee Docker Workflow Plugin v1.2
    * https://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Docker+Workflow+Plugin


環境構築
=====

環境の構築手順は以下の通りです。
OSXへのdockerのインストールは公式の手順通りに完了し、Dockerサーバーが"default"という名称で動いているところからスタートしています。

Jenkins環境構築
-----

まずは、JenkinsをDockerコンテナで構築します。

```bash
# 色々使うのでDockerサーバーのIPを取得
DOCKER_IP=$(docker-machine ip default)

# Docker Remote API 用環境変数設定
eval $(docker env default)

# Jenkins用コンテナからdockerソケットに読み書きできるように権限をつける
docker-machine ssh default sudo chmod o+rw /var/run/docker.sock

# docker でJenkinsを起動
docker pull jenkins
# dockerサーバー内の/var/jenkins_homeをマウントして起動
docker-machine ssh default \
  'docker run --detach --name jenkins 
    -v /var/run/docker.sock:/var/run/docker.sock 
    -v /var/jenkins_home:/var/jenkins_home 
    -p 8080:8080 -p 50000:50000 jenkins'
# jenkins-cliとの通信用ポートの50000を変える場合は以下の引数を指定します
# --env JENKINS_SLAVE_AGENT_PORT=50001

# 起動確認
curl http://${DOCKER_IP}:8080/api/json
# => JSONで設定状況を表示


# JenkinsのURLを環境変数に設定
export JENKINS_URL=http://${DOCKER_IP}:8080

# プラグインインストールのためJenkins-cli.jarを取得
wget ${JENKINS_URL}/jnlpJars/jenkins-cli.jar

# プラグインをインストールする
java -jar jenkins-cli.jar install-plugin \
  git \                     # Git Plugin
  gitlab-plugin \           # GitLab Plugin
  parallel-test-executor \  # Parallel Test Executor Plugin
  parameterized-trigger \   # Parameterized Trigger plugin
  workflow-aggregator \     # Workflow: Aggregator
  workflow-multibranch \    # Workflow: Multibranch
  docker-workflow           # CloudBees Docker Workflow

# プラグインをDLするのを待つ。
# 以下の画面で確認できる
#   '${JENKINS_URL}/updateCenter/'

# プラグインを反映するため再起動
java -jar jenkins-cli.jar safe-restart


# Jenkinsコンテナ内にDockerコマンドをインストール
docker exec -u root jenkins sh -c \
  'apt-get update &&
    apt-get install -y docker.io &&
    rm -rf /var/lib/apt/lists/*'
```

テストに使用するDockerイメージを用意します。
今回は、mavenのイメージをそのまま使うので、pullだけしておきます。

```bash
docker pull maven
```

Jenkinsのシステム設定を変更します。

  1. [Jenkinsの管理]-[システムの設定]を開く
  2. [Gitlab]の項目に、GitlabのURLと連携用API Tokenを設定

![Jenkins設定-Gitlab設定.png](https://qiita-image-store.s3.amazonaws.com/0/47562/e491cef3-d673-0781-0d23-24b1db54e83b.png)

ジョブ設定
----

次にジョブを作成します。
ジョブは２つ１セットで作成します。１つはGitlabのマージリクエストのトリガを受け取るジョブで、もう１つが先のジョブから起動される実際にテストを行うジョブです。

１つ目：MergeRequestのトリガを受け取るジョブ

  * ジョブ名: main
  * ジョブの種類: フリースタイル・プロジェクトのビルド

![Jenkins-job-main.png](https://qiita-image-store.s3.amazonaws.com/0/47562/0c4c6671-b378-2335-bfd4-0041177c063b.png)

２つ目:テストを実行するジョブ

  * ジョブ名: sub
  * ジョブの種類: Workflow

![Jenkins-job-sub.png](https://qiita-image-store.s3.amazonaws.com/0/47562/8159992b-6a10-4d67-bcbd-c412d938100a.png)


GitlabのプロジェクトのHookにmainプロジェクトを追加します。
プロジェクトの[Settings]-[Web Hook]でHookを追加します。

  * URL: http://192.168.99.100:8081/project/main
  * Trigger:
    * Push events
    * Merge Request events

![Gitlab-Hook設定.png](https://qiita-image-store.s3.amazonaws.com/0/47562/2efc74eb-4dcc-bb84-1e5c-131b9f409d68.png)


テスト実行スクリプトを実装
----

テスト対象のプロジェクトのルートフォルダに、Workflowプラグインで実行するスクリプトを記述するJenkinsファイルを追加します。

```groovy:Jenkins
#!groovy
# ↑の行はエディタなどのシンタックスハイライトの判定用

DOCKER_IMAGE = 'maven'

stage 'UnitTest'
node {
  // 下記の後部にWorkflow Multibranchが必要
  checkout scm
  // mavenの依存ライブラリの取得を先に行っておく
  sh "${tool 'maven3'}/bin/mvn --batch-mode dependency:go-offline -Dmaven.repo.local=./m2repo"
  stash includes: '**/*', name: 'files'
}

// https://github.com/jenkinsci/workflow-plugin/blob/master/TUTORIAL.md#creating-multiple-threads
def splits = splitTests([$class: 'CountDrivenParallelism', size: 3])
def branches = [:]
for (int i = 0; i < splits.size(); i++) {
  def exclusions = splits.get(i);
  branches["split${i}"] = {
    node {
      docker.image(DOCKER_IMAGE).inside {
        sh 'rm -rf *'
        unstash 'files'
        writeFile file: 'exclusions.txt', text: exclusions.join("\n")
        // テスト実行
        sh """
        mvn --batch-mode -Dmaven.test.failure.ignore -Dmaven.repo.local=./m2repo test
        """
        step([$class: 'JUnitResultArchiver', testResults: 'target/surefire-reports/*.xml'])
      }
    }
  }
}
parallel branches
```

これでテスト並列実行のための設定は完了です。


解説
==========

ここからは、なぜこういう設定にしているかという意図や、上記の方法にたどり着くまでに試行錯誤したことを紹介します。


Dockerへの接続方法
----

dockerコマンドを使う時は、ソケットかTCPかの２つの通信方法を選べます。
ソケットを使うにはDockerサーバー上の`/var/run/docker.socket`にアクセスできる必要があるので通常はTCPを使うことになると思います。TCPの通信は、ツールによってはHTTPのURL形式で指定します。


TCP方式ではなぜかdocker execが途中でとぎれてしまう
----

前述の構築方式ではソケット通信を使っていますが、最初はTCPの接続方式を利用するつもりでした。

ですが、私が試した環境ではTCPを利用すると、Jenkinsのジョブ中で実行する`docker exec`コマンドがなぜか処理途中で中断してしまう現象がおきました。
例えば、Dockerサーバー上で以下のようにコマンドを実行すると、１つ目はsleepでの待ち時間た経過してから処理が完了します。ところが、２つ目の方はsleepで時間が経過する前にコマンドが終了してしまいます。

```bash
$ docker exec 9a2d5f18ed69 sh -c "echo begin; sleep 10 ;echo end"
begin
end
$ DOCKER_HOST=tcp://192.168.99.100:2375 docker exec 9a2d5f18ed69 sh -c "echo begin; sleep 10 ;echo end"
begin
```

色々試すとなんとなくttyの有無による影響があるようですが、よくわかりませんでした。
結局は、Jenkinsを実行するDockerコンテナの実行時にdocker.sockもVOLUMEとしてマウントし、それを使ったソケット通信でdockerコマンドを使うようにしました。


httpスキーマのURLを使う方法
-----

今回の方法ではソケットを使いましたが、TCPを使おうとしたときにも詰まったことがあったので紹介しておきます。

TCPのAPIと書きましたが、実際にはDockerをインストールするとデフォルトではTLSでのセキュアな通信(HTTPS)となります。これを利用するには、dockerコマンドを実行する環境にクレデンシャル情報が必要になります。

このHTTPSでの通信はまだ実装されてから間がないらしく、HTTPでなければ利用できないツールが多いようです。JenkinsのDocker Pluginもその一つで、httpsのURLは利用できません。設定画面にクレデンシャルを設定する入力欄もありますが、それは機能しません。
そこで、このDockerのAPIをHTTPにする方法を調べました。

まず、dockerサーバーを構築・管理する`docker-machine`コマンドで切り替えられないかを調べたのですが、どうやら方法は無いようでした。
`docker-machine create`でDockerサーバーを新規作成するときのオプションも効果がありません。

調べたところ、Dockerデーモンを起動する時の設定を変えるとTLSをOFFにできるようでした。Dockerサーバーの `/var/lib/boot2docker/profile` ファイルの `DOCKER_TLS`変数の値を"auto"から"no"に変えて、dockerデーモンを再起動すると変更できます。

```diff:/var/lib/boot2docker/profile
-DOCKER_TLS=auto
+DOCKER_TLS=no
```

ところが、これでHTTP化自体はできるのですが色々と不都合もあります。
dockerコマンドは、自前でDOCKER_HOST変数などを正しく設定すれば使えるようにはなりますが、docker-machineコマンドの`docker-machine env`がエラーになり使えなくなります。OSXについてくる管理用GUIツールのKitematicも同様に使えなくなります。

```bash
% docker-machine env default
Error checking TLS connection: Error checking and/or regenerating the certs: There was an error validating certificates for host "192.168.99.100:2376": tls: oversized record received with length 20527
You can attempt to regenerate them using 'docker-machine regenerate-certs [name]'.
Be advised that this will trigger a Docker daemon restart which will stop running containers.
```

`~/.docker/machine/machines/default/config.json`に設定ファイルがあるのですが、私が触った限りではどうやっても対処することができませんでした。
また、`docker-machine restart`でdockerサーバーを再起動したら、上記の設定が元に戻ってしまいます。
ということで、デーモンをHTTP設定で起動するのはちょっと良くなさそうです。


別の方法として、HTTP用のProxyサーバーを立てるのが良さそうでした。
下記のページでソケット通信をTCP通信にしてくれるProxyサーバーを稼働させるコンテナが紹介されています。

  * Boot2docker TLS workaround - SequenceIQ Blog
    * http://blog.sequenceiq.com/blog/2014/10/17/boot2docker-tls-workaround/

このコンテナは、以下のようにDockerサーバー上で実行します。

```bash
# dockerサーバー上でDocker Remote API をHTTPアクセス可能にするProxyを起動
docker-machine ssh default eval '$(docker run sequenceiq/socat)'
# Proxy起動確認
curl http://${DOCKER_IP}:2375/_ping
# => OK
```

上記では`docker run`時に「-v」オプションがついていませんが、このコンテナ起動の結果としてdocker.sockをマウントして`docker run`を実行するコマンドが出力されるので、それをevalで実行しています。

これにより、HTTPSでもHTTPでもRemote APIを実行することができるようになります。


JenkinsサーバーへのDockerインストール
---------

前述の方法ではJenkinsサーバー上にDockerコマンドをapt-getでインストールしています。
ちなみに、JenkinsにDocker関連のpluginをインストールすると、システム設定の画面にDockerをツールとして設定する項目が追加されます。そこには、Dockerについて[自動インストール]のチェックがありますが、これにチェックと入れても実際にはインストールされません。

![Jenkins設定-Docker.png](https://qiita-image-store.s3.amazonaws.com/0/47562/9ad948e8-9b40-649b-4110-08472f08cd71.png)


Jenkinsのjenkins_homeディレクトリのマウント
-----

今回の方法では、jenkinsサーバーを起動するときにわざわざDockerサーバーの中で`docker run`を実行し、/var/jenkins_homeをVolumeとして設定しています。これは、dockerコンテナ内で`docker run`したときに「-v」でのマウント場所がおかしくなる問題への対処になります。
dockerコンテナの中で`docker run -v/host/path:/cont/path`といったようにVOLUME指定をすると、なぜかホスト側のパス(/host/path)が実行したコンテナ内のパスではなく、Dockerサーバー上のパスでマウントされてしまいます。
そのため、Jenkinsのジョブ内でテスト用のコンテナを実行する時に、作業用のworkspaceを正常にマウントして使うことができませんでした。
そこで、Jenkinsコンテナ上の/var/jenkins_homeと、Dockerサーバー上の/var/jenkins_homeを同一のものとしてマウントすることで、jenkins_home下にあるworkspaceがDockerサーバー上のパスとして扱われても同じ結果になるようにしています。

また、このマウントにはもう一つ注意することがあります。
workspaceとしてマウントされるディレクトリの所有者(UID)がマウント先のコンテナ上のメイン動作用のユーザのUIDと一致するようにしておく必要があります。
そうでなければ、テスト用のコンテナからworkspace内に書き込みができません。

さらに、細かい条件がわからないのですが、書き込み権限を0777などと緩和してもUIDが異なる場合にはworkflowスクリプトが途中で詰まる現象が発生します。
現象自体は、以下にISSUEとしてすでに登録されていました。

  * [JENKINS-28821] Docker Workflow demos: sh step on Docker image hangs if workspace is not writable - Jenkins JIRA
    * https://issues.jenkins-ci.org/browse/jenkins-28821

具体的な現象としては、workflow内で `image.inside { sh "mvn test" }`といったようにコンテナ内でシェルコマンドを実行しようとするとその処理で止まってしまいます。

```log:ジョブの実行ログ
Running: Shell Script
[workspace] Running shell script  ＜＝ここで止まる
```

```log:FINEレベルのJenkinsログ
Send Response[retVal=hudson.remoting.UserResponse@212ec6fc,exception=null]
Jan 02, 2016 11:47:28 AM FINE hudson.remoting.Channel
Received UserRequest:org.jenkinsci.plugins.durabletask.BourneShellScript$DarwinCheck@87761c4
Jan 02, 2016 11:47:28 AM FINE hudson.remoting.Channel
Send Response[retVal=hudson.remoting.UserResponse@7d49d292,exception=null]
Jan 02, 2016 11:47:28 AM FINE hudson.remoting.Channel
Received UserRequest:hudson.Launcher$RemoteLaunchCallable@701b314c
Jan 02, 2016 11:47:28 AM FINE hudson.Proc
Running: docker exec 94d7388a388bf746b16afbbbf436ce49a048e10457080a30ef7527e6ea8be770 env BUILD_DISPLAY_NAME=#54 BUILD_ID=54 BUILD_NUMBER=54 BUILD_TAG=jenkins-workflow-54 BUILD_URL=http://192.168.99.100:8080/job/workflow/54/ CLASSPATH= DOCKER_HOST=tcp://192.168.99.100:2376 HUDSON_HOME=/var/jenkins_home HUDSON_SERVER_COOKIE=027abd10c6603ceb HUDSON_URL=http://192.168.99.100:8080/ JENKINS_HOME=/var/jenkins_home JENKINS_SERVER_COOKIE=please-do-not-kill-me JENKINS_URL=http://192.168.99.100:8080/ JOB_NAME=workflow JOB_URL=http://192.168.99.100:8080/job/workflow/ NODE_NAME=192.168.99.1 sh -c echo $$ > '/Users/dev/jenkins/slave/workspace/workflow/.jenkins-11263679/pid'; jsc=durable-751167607317369943dd70151bd14f2d; JENKINS_SERVER_COOKIE=$jsc '/Users/dev/jenkins/slave/workspace/workflow/.jenkins-11263679/script.sh' > '/Users/dev/jenkins/slave/workspace/workflow/.jenkins-11263679/jenkins-log.txt' 2>&1; echo $? > '/Users/dev/jenkins/slave/workspace/workflow/.jenkins-11263679/jenkins-result.txt'
  ↑このログ出力後にとまっている様子
Jan 02, 2016 11:47:28 AM FINE hudson.remoting.Channel
Send Response[retVal=hudson.remoting.UserResponse@43a401e1,exception=null]
Jan 02, 2016 11:47:28 AM FINE hudson.remoting.Channel
Received UserRequest:RPCRequest(39,getIOtriplet)
Jan 02, 2016 11:47:28 AM FINE hudson.remoting.Channel
Send Response[retVal=hudson.remoting.UserResponse@22a82a0,exception=null]
```

この現象が発生して処理が詰まっている時は、起動しようとした`docker exec 〜〜`のプロセスが存在していませんでした。また、JenkinsのFINEレベルのログをみると実行直前に出力されるログ(コマンドの内容)があるので、以下のプロセスを起動する部分でとまっているのではないかと思います。

  * https://github.com/jenkinsci/jenkins/blob/jenkins-1.625.3/core/src/main/java/hudson/Proc.java#L244


Gitlabのマージリクエストに対するジョブの起動方法
-----

今回はGitlabプラグインを使いましたが、Gitlabとの連携をするためのプラグインはもう一つあります。

  * Gitlab Merge Request Builder Plugin - Jenkins - Jenkins Wiki
    * https://wiki.jenkins-ci.org/display/JENKINS/Gitlab+Merge+Request+Builder+Plugin

こちらの方は非公式のもののようですが、先発なので現在の利用者数はこちらの方が多いのではないかと思います。
今回採用しているGitlabプラグインは、このGitlab Merge Request Builder(以降 Gitlab MRB)を参考にして作成された公式のプラグインのようです。
Gitlab MRBを参考にしているということで、大まかな設定方法(gitlab固有の変数とか)は同じなのでプラグインを切り替えても、そのまま動作できそうな雰囲気です。（ただし未検証です）

主な違いは以下のようなところです。

  * Gitlabプラグインは、
    * ci-skipプラグインの機能も含んでいる
      * https://wiki.jenkins-ci.org/display/JENKINS/Ci+Skip+Plugin
    * トリガの起点がGitlabのHook機能によって行われる
      * Gitlab MRBプラグインは定期ポーリングでのトリガ起動ができる
    * Gitlab 8.x系の機能との連携がある
      * Commit履歴画面で各コミットごとにCI通過結果のマークが表示される など
    * Merge Requsetに関係なく、個々のコミットに対してテストを起動できる

Gitlab8.xの機能を気にしなければ、機能的にはどちらを使っても十分だと思います。
ちなみに、8.xでの連携を行うとGitlabで以下の様な表示ができるようになります。

  * コミット履歴において、各コミットごとにテスト結果が表示される（チェックマークと、×マーク）
    ![Gitlab-history.png](https://qiita-image-store.s3.amazonaws.com/0/47562/76177ddc-a75f-a2d8-a6dd-34ece6332046.png)
  * Merge Request画面にテスト結果が表示される
    ![Gitlab-mergerequest.png](https://qiita-image-store.s3.amazonaws.com/0/47562/3726df7c-4d81-283d-3a2c-c111cff1cf60.png)


起動用ジョブとテスト処理ジョブの２つを作成する理由
-----

今回の方法でGitlabと連携するジョブとテストを実際二行うWorkflowジョブが分かれているのは、Workflow形式のジョブでは上記のGitlab用プラグインが機能しなかったためです。
Workflow形式のジョブでは、Gitlab MRBプラグインはそもそもジョブ設定画面に設定欄自体が表示されません。一方のGitlabプラグインは設定欄は表示されるものの設定してもトリガが反応しません。そのため、起動用ジョブとメイン処理ジョブを別々に作成しています。

ちなみに、どうもWorkflowジョブは環境変数の展開処理が行われないようで、ブランチ名などに変数を指定しても展開されません。ジョブのビルドパラメータの指定も効きませんし、環境変数を細かく指定できるEnvInjectプラグインも効果がありませんでした。


ビルドパラメータの設定
----

起動用ジョブにはGitlabプラグインで指定される変数をビルドパラメータに設定しています。これによって、手動で対象ブランチを指定してビルドを実行できるようにしています。
このとき、Gitリポジトリの設定欄で[Branches to build]に指定するブランチ設定は１つだけにしておく必要があります。これは、Gitプラグインで特定のブランチをビルド対象にするには、ブランチ名が一意に決まる状況にしておく必要があるためです。

詳細は、こちらの記事を参照ください。

  * JenkinsのGit Pluginの設定の意味がやっとわかった
    * http://qiita.com/namutaka/items/b41a4661cf6cec7f48c5


Gitlabプラグインの設定内容について
-------

Gitlabプラグインを使ったJenkinsとGitlabの連携用設定は、プラグインのGithub上の説明を読んでもらえば詳細が書いてあります。
今回は設定していませんが、テスト対象ブランチをメインブランチにマージした状態でテストするようにすることもできます。

  * jenkinsci/gitlab-plugin
    * https://github.com/jenkinsci/gitlab-plugin

なお、Gitlab本体のドキュメントに以下のページがあります。

  * GitLab Documentation
    * http://doc.gitlab.com/ee/integration/jenkins.html

ここでは「Services」というところで「Jenkins CI」という項目を設定するとの記載がありますが、これはGitlab EE固有のものでCE版にはこの項目がありません。代わりにHook機能を使えばよいようです。


Jenkinsジョブ側の設定で、デフォルト状態から変えている部分の理由は以下のとおりです。

  * [Build on Push Events]はオフ
    * これがオンだと、Gitリポジトリ上のすべてのコミットに対してジョブを実行してしまうようだったので切っています
    * またMerge Request上のブランチへのコミットだと、Push EventとMR Eventの両方で同じコミットに対するジョブが２つ起動されてしまいます
  * [Rebuild open Merge Requests]を「On push to source branch」にする
    * 初期はこれが「never」ですが、それだとマージリクエストが作成された時しかジョブが実行されません。
    * 作成済みのMerge Requestに追加でコミットが行われたときにもビルドを実行するために、この設定にしています。

Push Eventをオフにすると、メインとするブランチの定期的なビルドができませんが、それは別のトリガを設定することでカバーすれば良いと思います。


Workflowスクリプトについて
------

Workflowプラグインは、ジョブで行いたい処理をgroovyスクリプトで記述することができるプラグインです。ユーザの入力を行ったり、処理をStageに分けて記録したりすることができます。また、Jenkins Enterpiseであれば、workflowの途中で中断・再開をする機能も使えるようです。

Workflowのスクリプトの記述は、以下のページを参照してください。

  * https://github.com/jenkinsci/workflow-plugin/blob/master/TUTORIAL.md

このページはTUTORIALとのことらしいのですが、これ以外にリファレンス的なものはみつけられませんでした。


Workflow スクリプトでの `checkout scm`
------

今回のスクリプトではコードのチェックアウトを`checkout scm` という記述で行っています。
この`scm`変数を使うには「Workflow: Multibranch」プラグインが必要です。scmは、ジョブで指定したリポジトリ設定を保持しています。
起動用ジョブの[Trigger/call builds on other project]タスクで[Pass-through Git Commit that was built]を指定したことで、起動用ジョブで対象としているブランチをチェックアウトできるようになっています。
ちなみに、このscmを使わない場合は直接リポジトリのURLやブランチ名を明記することしかできないようでした。


Git checkoutの後のclean
------

JenkinsでGitを使用するのであれば、基本的にはジョブ設定のリポジトリ設定欄で「Additional Behaviours:」に[Clean after checkout]を追加しておくのが良いと思います。
Jenkinsではworkspaceのディレクトリを使い回すので、以前のジョブ終了時のファイルが全て残っています。その状態でブランチをcheckoutしてもgit管理外のファイルがあるため、想定外の動作をしてしまう場合があります。
このclean設定を有効にすると、ブランチのチェックアウト後にgit管理外のファイルを全て削除した状態にしてくれます。


docker workflow の使い方
------

Workflowスクリプトの中でdockerの操作を行うために、docker workflowプラグインを使用しています。
このdocker関連のスクリプトの詳細は以下を参照してください。

  * http://documentation.cloudbees.com/docs/cje-user-guide/docker-workflow.html

このプラグインは内部的にdockerコマンドを使っているので、ジョブが実行されるJenkinsのノードにdockerコマンドをインストールしておく必要があります。また、dockerコマンドがDockerサーバーと接続できる状態にしておくことも必要です。
プラグインの機能の```withServer()```メソッドでTCP(HTTPS非対応)形式のAPIを使うこともできるのですが、前述の通りソケット方式でなければ正常に動作しなったので今回は使っていません。


Parallel Test Executorプラグイン
----

これはMavenやAntでのJUnit形式のテストを並列実行できるようにするプラグインです。今回はworkflowの中で`splitTests()`メソッドとして利用しましたが、通常のジョブでも利用することができます。
このプラグインでは、テストの並列実行は以下の様な方式で行われています。

  1. ジョブの実行履歴の中で、正常実行(テストの成否は問わない)した最近の記録を取得する
  2. 取得した記録からテストクラスの一覧を収集し、指定した並列数に応じて分割する
  3. 並列実行する各１ジョブごとに、*実行しない分*の一覧をexclusions.txtファイルに書き出して割りあてる
  4. mavenやantのテスト実行時に、このexclusions.txtファイルのリストを除外してテストを実行するようにしておく

このように、実行しない分のリストを指定する方式なので、初回実行時は空のリストで１ジョブで実行されます。また、新規にテストを追加した場合は並列実行する全てのジョブで実行されることになります。


Docker Pluginを使う方法
-----

今回は最終的にWorkflowを使いましたが、当初はParallel Test Executor Plugin と Docker Pluginの組み合わせで行おうとしていました。
せっかくなのでその方法も記載しておきます。

  * Jenkins Docker Plugin v0.16.0
    * https://wiki.jenkins-ci.org/display/JENKINS/Docker+Plugin

先の手順のプラグインのインストールを以下の様に変更します。

```bash
# プラグインをインストールする
java -jar jenkins-cli.jar install-plugin \
  git \                     # Git Plugin
  gitlab-plugin \           # GitLab Plugin
  parallel-test-executor \  # Parallel Test Executor Plugin
  docker-plugin             # Docker Plugin
```

Jenkinsのシステム設定を以下の様に設定します

  1. [Jenkinsの管理]-[システムの設定]を開く
  2. [クラウド]の項目で、[追加]-[Docker]を選択してDocker設定を追加する
  3. [Name]を適当につけて、[Docker URL]にRemote APIのHTTP(HTTPS不可)のURLを記載する
  4. [Images]で[Add Docker Template]メニューから新規イメージテンプレートを追加する
    このイメージテンプレートがテスト実行時に起動されることになる
  5. [Docker Image]に使用するDockerイメージ名を指定
    * 今回は、「maven」を使用
  6. [Instance Capacity]にこのイメージを同時起動する最大数を指定
  7. [Labels]、[用途]は通常のスレーブノードと同じように設定する
    * 今回は、「slave」というラベルを指定
  8. [Launch method]で「Docker SSH computer launcher」を選択し、認証情報にSSH接続のための情報を追加する
  9. [Pull strategy]は用途に応じて設定
    * 個人的には「Never Pull」にして適宜自前で用意するようにするのがいいのではと思います

![Jenkins-DockerPlugin.png](https://qiita-image-store.s3.amazonaws.com/0/47562/71a43ec4-741b-b9c5-05e1-04d8d2b5f1e9.png)


最後にParallel Test Executorの説明通りに２つのジョブを以下の様に作成します。

1つ目: MergeRequestのトリガを受け取って起動し、subテストを並列実行するジョブ

  ジョブ名: main
  ジョブの種類: フリースタイル・プロジェクトのビルド

![Jenkins-docker-plugin-job-main.png](https://qiita-image-store.s3.amazonaws.com/0/47562/c6e321e1-f6ab-fa7e-5d33-e9e58e6d3343.png)


２つ目:テストを実行するジョブ

  * ジョブ名: main
  * ジョブの種類: フリースタイル・プロジェクトのビルド

![Jenkins-dokcer-plugin-job-sub.png](https://qiita-image-store.s3.amazonaws.com/0/47562/49a396ac-532f-2db0-9dc5-7ef4ab06ff08.png)

Docker Pluginを使うと、Docker Image Templateで指定したイメージがJenkinsの通常のスレーブノードの様に扱われます。
ジョブが開始されて担当するノードを決定するときに、このイメージテンプレートが選択されるとDockerコンテナを起動し、ジョブに割り当てます。
コンテナが起動してからは、通常のスレーブノードと同様にジョブの処理が行われ、終了した時点でDockerコンテナが破棄されます。
この方法ではジョブの設定で実行ノードの制限を指定する以外は特別なことはありません。Parallel Test Executorプラグインで起動される多数のsubジョブでもDockerコンテナが１つずつ起動されます。

この方法はDockerでテストを実行させるのなら便利なのですが、実際にやってみると問題ありました。
複数のsubジョブが一度にジョブキューに登録されるのですが、１つ目のDockerコンテナが起動したら、２つ目以降はノードの空きを待つ状態で止まってしまいます。期待した動作としては、それぞれに対してDockerコンテナを起動して欲しいところです。しかし、どうも「実行するノードを制限」でラベルを指定すると、現在のノードリスト中にそのラベル条件に合致するノードが存在する場合は、そのノードの終了を待つ状態になってしまうようでした。実際にラベル指定を行わずに試してみると、複数のDockerコンテナが起動されました。

結局はテストの並列実行ができなかったので、この方法は採用しませんでした。


[追記] Workflowスクリプトで実行可能なメソッド
----

（書き忘れていたことがあったので追記です）

WorkflowのGroovyスクリプトは制限されたSandbox環境で実行されます。そのため、実行できるJavaのクラスやメソッドが制限されていて、利用不可のメソッドを使うと以下のようなエラーが発生します。

```
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use method groovy.lang.GroovyObject invokeMethod java.lang.String java.lang.Object (org.jenkinsci.plugins.workflow.cps.CpsClosure2 sleep java.lang.Integer)
	at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectMethod(StaticWhitelist.java:155)
	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onMethodCall(SandboxInterceptor.java:77)	
```

これを解除するには、Jenkins設定の[In-process Script Approval]画面にて承認する必要があります。上記のエラーが出たあとで、この設定画面を開くと下記のように許可するか、拒否するかを選択できる状態になっています。[Approve]ボタンをクリックして許可すると、次回以降はエラーがでなくなります。

![Jenkins-approval.png](https://qiita-image-store.s3.amazonaws.com/0/47562/8cc68240-cd1c-0b69-ecfc-16e975905c6c.png)

この設定は事前に許可リストを投入するようなことはできないようなので、先にジョブを実行させなければならないようです。また、許可済みの項目を個別に除外するようなこともできなさそうです。


まとめ
=====

かなり長くなりましたが、色々とはまりながらなんとかそれらしい形にまとめることができました。ですが、まだ実践では使っていないので、これからも試行錯誤することがありそうな気がします。