読者です 読者をやめる 読者になる 読者になる

あらしおブログ

技術ネタ中心の粗削りブログ

Jenkins パイプラインの stash/unstash を使ってノード間でファイルを転送する

Jenkins のパイプラインで stash/unstash を使って異なるノード間でファイルを転送する方法を紹介します。 英語の stash には、「しまう」という意味があるようです。一時的にファイルをどこかにしまって(stash)、 必要になったら取り出す(unstash) という操作です。

使いどころとしては、

  • マスターでソースコードをチェックアウトし、複数のスレーブに転送して並列テストを実行する*1
  • スレーブノードで得られたビルド後の成果物をマスターに転送する

などが考えられます。 便利なのですが、「しまう」という表現のためか、ちょっと抽象化されすぎていて stash/unstash の中で実際に何をしているのか不安だったので、そこらへんも調べてみました。

stash/unstash を使った例

以下のような簡単なシナリオで stash/unstash の動作を確認しましょう。

  1. マスターで git からソースコードをチェックアウト後、stash する。
  2. スレーブでソースコードunstash 後、テストを実行する

パイプラインスクリプトは以下のようになります。(スレーブノードの環境設定とノード追加は実施済みとします)

node('master') {
    deleteDir()
    git url: 'https://github.com/arasio/simple-gradle-project-with-tests.git'
    stash name: 'project'
    sh 'ls -l'
}

node('slave1') {
    deleteDir()
    unstash 'project'
    sh 'ls -l'
    sh 'gradle test'
}
まずはワークスペースの掃除

各ノードでまず deleteDir() をしているのは、前回のビルドでワークスペースに残っているゴミを掃除するためです。 上のジョブを1回実行すると、マスターでは git からチェックアウトしたソースコードがそのまま残りますし、 スレーブでも unstash したソースコードがそのまま残ってしまいます。ワークスペースはまっさらにしておいたほうが精神衛生上良いですね。

マスターでソースコードを stash する

git ステップでソースコードをチェックアウトしたあと、 stash name: 'project' で現在のディレクトリにあるファイルやディレクトリを stash します。 後で unstash できるように stash するときは名前をつけます。 ここで ls -l を実行してワークスペースの状態を確認してみると、下のようになっています。 これらのファイルが stash されたことになります。

+ ls -l
total 28
-rw-r--r-- 1 jenkins jenkins 1063 Oct 18 12:47 LICENSE 
-rw-r--r-- 1 jenkins jenkins  253 Oct 18 12:47 build.gradle
drwxr-xr-x 3 jenkins jenkins 4096 Oct 18 12:47 gradle
-rwxr-xr-x 1 jenkins jenkins 5242 Oct 18 12:47 gradlew
-rw-r--r-- 1 jenkins jenkins 2260 Oct 18 12:47 gradlew.bat
drwxr-xr-x 4 jenkins jenkins 4096 Oct 18 12:47 src
スレーブでソースコードを unstash する

スレーブ側でもワークスペースを掃除した後、unstash 'project' を実行すると、さきほど stash したソースコードをスレーブノード上で取り出すことができます。 unstash 後に ls -l を実行して確認すると、上のマスターのワークスペースと全く同じ状態になっていることがわかります。 ちなみに、ディレクトリは再帰的に処理されますので、src ディレクトリ以下もすべて転送されています。

そのあとはテストを実行するなり、実際にやりたい処理を書くことになるでしょう。

stash/unstash の中では何が起きている?

stash/unstash が実際にどのような処理になっているか知っておくと安心して使えます。 基本的には

  • stash: 対象ノードのカレントディレクトリを圧縮してマスターの所定の場所に退避
  • unstash: マスターに退避した圧縮ファイルを対象ノードのカレントディレクトリにコピーして展開

という処理になっています。

stash を実行すると対象ノードのカレントディレクトリを tar.gz で圧縮して、ジョブのビルドディレクトリに保存します。 上の例では、stash された圧縮ファイルは以下にできていました。stash するときに 'project' という名前をつけたので project.tar.gz になっています。

${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_NUMBER}/stashes/project.tar.gz

このファイルは一時ファイルなので、ビルドが終了すると消えます。掃除は考える必要がありません。 スレーブで stash を実行してもマスターのビルドディレクトリに圧縮ファイルが生成されます。

unstash は stash で退避した圧縮ファイルを対象ノードにコピーして展開します。 注意したいのが展開したファイルの所有者はすべて jenkins ユーザになることです。 圧縮が jenkins ユーザで実行されるので展開すると元の所有者によらず jenkins ユーザになります。

より詳細に知りたい方は Jenkins のソースコードを直接確認すると良いでしょう。私は以下を参考にしました。

オプション

stash は基本的にはカレントディレクトリを圧縮するのですが、 いくつかのオプションが用意されています。

stash name: 'test', excludes: '**/*.log'

ワイルドカードの表記は ant スタイルに従います。

stash name: 'test', includes: '**/*.java'
  • デフォルトで除外されるファイルも stash する (useDefaultExcludes)

デフォルトでは特定の隠しファイルなどは stash されません。上の例では、.git/ や .gitignore などは実は stash されていません。 これらのファイルも含めて stash/unstash したい場合は useDefaultExcludesfalse に設定します。

stash name: 'test', useDefaultExcludes: false

なお、デフォルトで除外されるファイル一覧は以下のようになっています。

**/*~
**/#*#
**/.#*
**/%*%
**/._*
**/CVS
**/CVS/**
**/.cvsignore
**/SCCS
**/SCCS/**
**/vssver.scc
**/.svn
**/.svn/**
**/.DS_Store
**/.git
**/.git/**
**/.gitattributes
**/.gitignore
**/.gitmodules
**/.hg
**/.hg/**
**/.hgignore
**/.hgsub
**/.hgsubstate
**/.hgtags
**/.bzr
**/.bzr/**
**/.bzrignore

出典: https://ant.apache.org/manual/dirtasks.html#defaultexcludes

*1:下の記事で実際のサンプルを紹介していますので興味ある方はご覧くださいarasio.hatenablog.com

Jenkins を使って Gradle プロジェクトのテストを複数ノードで並列実行する方法

Parallel Test Executor Plugin は テストを分割し複数のノードで並列実行することができる Jenkins のプラグインです。 公式のチュートリアル には Maven プロジェクトでの使用例がありますが、Gradle での使い方が見つからなかったので私が試した方法を紹介したいと思います。

テスト並列実行のしくみ

Parallel Test Executor を使った並列テストのしくみについて簡単に。 並列テストの実行には以下の機能が関わっています。

  • Jenkins 組み込みの並列処理機能 (Jenkins 2.x のパイプライン)
  • Parallel Test Executor によるテスト振り分け
  • Maven や Gradle などビルドツールによる特定のテスト除外機能 (exclude 指定)

そして、テストの並列実行は以下のように実現されます。

  1. 一つ前の成功ビルドのテスト結果 (各テストクラスの所要時間) を参照する
  2. テスト時間が各ノードで均等になるようにテストを振り分ける
  3. 振り分けたテストを各ノードで並列実行する

このうち、1 と 2 は Parallel Test Executor が良きに計らってくれる部分です。 3 については、ビルドツールで割り振られたテスト以外は実行しない (exclude) ように自分で設定する必要があります。

実績をベースにテストの振り分けを自動化しているのがうれしいですね。

Gradle プロジェクトでの使用例

Gradle で管理される Java プロジェクトの JUnit テストを3つのノードで並列実行する例を紹介します。

1. Gradle のサンプルプロジェクト

並列テスト対象の Gradle のサンプルプロジェクトを GitHub に上げました。
https://github.com/arasio/simple-gradle-project-with-tests

プロジェクトには以下のようなテストが Sample1Test.java ~ Sample9Test.java まで9つ入っています。

public class Sample1Test {
    @Test
    public void test() throws InterruptedException {
        assertEquals(3, 1 + 2);
        Thread.sleep(10000);
    }
}

Sample1Test は sleep の値が 10秒, Sample2Test は 20秒, ... , Sample9Test は 90秒 というように設定しています。 そのままシーケンシャルにテストを実行すると、7分30秒+α かかる計算になります。

2. Jenkins のノード準備

予め並列実行する3つのノードを Jenkins に登録しておきます。
詳しい手順は省略しますが、サーバをセットアップして

  • JDK のインストール
  • jenkins ユーザの作成
  • Gradle のインストールとパスを通す

などを済ませます。 そして、Jenkins の管理 > ノードの管理 > 新規ノードの作成 からノードを追加します。 ここで、3つのノードにはすべて slaves というラベルを付けておくことにします。

f:id:arasio:20161013010553p:plain

3. Jenkins のパイプラインジョブ設定

Parallel Test Executor プラグインをインストールすると、 パイプラインスクリプト (jenkinsfile) で splitTests というメソッドが使えるようになります。 引数の size に分割数(ノード数) を設定すると、返り値として各ノードで除外すべきテストクラスのリスト(の配列)が得られます。 各ノードでこれを exclusions.txt に保存 → exclusions.txt にないテストのみ実行(4参照) → テスト結果をアーカイブして集約 という流れになります。

下は slaves ラベルがついた 3 つのノードでテストを並列実行する Jenkinsfile のサンプルです。

project = 'simple-gradle-project-with-tests'
repositoryUrl = "https://github.com/arasio/${project}.git"

node('master') {
    stage('checkout') {
        deleteDir()
        git url: repositoryUrl
        stash name: project
    }
}

stage('parallel test') {
    def splits = splitTests parallelism: count(size: 3)
    def branches = [:]
    
    for(int i = 0; i < splits.size(); i++) {
        def exclusions = splits.get(i);
        branches["split${i}"] = {
            node('slaves') {
                deleteDir()
                unstash project
                writeFile file: 'exclusions.txt', text: exclusions.join('\n')
                sh "gradle build"
                step([$class: 'JUnitResultArchiver', testResults: '**/build/test-results/*.xml'])
            }
        }
    }
    parallel branches
}

4. build.gradle の設定

ここで Gradle プロジェクトに話を戻します。 Gradle では test タスクの exclude を使ってテスト対象外にしたいテストクラスを指定できます。 いろいろ探してみたのですが、exclusions.txt のような、除外クラスをリストアップしたファイルを直接指定するメソッドは Gradle にはないようです。 そこで以下のようにファイルを読み込んで一行一行、exclude に追加する処理を仕込んでおきます。

test {
  def exclusionList = new File('exclusions.txt')
  if(exclusionList.exists()) {
    exclusionList.eachLine { line ->
      exclude line
    }
  }
}

build.gradle: https://github.com/arasio/simple-gradle-project-with-tests/blob/master/build.gradle

5. ジョブの実行

並列テストを実行する準備が整ったのでジョブを実行してみます。

初回のビルドは過去の実績(テスト時間)がないので、すべてのテストがどれか1つのノードで実行されます。 2回目以降はテストが分割されて並列実行されるようになります。

* テスト実行結果

テスト時間は以下のように 7分47秒 (初回,シーケンシャル) → 3分12秒 (2回目,並列) になりました。1/3 とまではいきませんが、半分以下までテスト時間を短縮することができました。

f:id:arasio:20161013215633p:plain

* テスト分割の内訳を確認してみる

分割の内訳は以下のようになっていました。理想ではありませんが、ほぼ均等に振り分けられているのがわかります。

ノード 実行したテスト テスト時間
#1 Sample1Test, Sample4Test, Sample9Test 140s
#2 Sample3Test, Sample6Test, Sample7Test 160s
#3 Sample2Test, Sample5Test, Sample8Test 150s
* テストレポートを確認してみる

テスト結果を確認してみましょう。 ちゃんと9つのテスト結果が集約されたレポートになっています。また、ここで表示されるテスト時間は、各ノードで要した時間が合算されたものになるようです。

f:id:arasio:20161013214903p:plain

* その後、テストが増えたら振り分けはどうなる?

新しく追加されたテストはすぐには振り分け対象になりません。 過去の実績をもとに、除外するテストをリストアップする方法をとっているので、 新しいテストはどのノードでも除外されない、つまりすべてのノードで実行されることになります。 一度ビルドが成功すればその後は振り分け対象になります。

おまけ: Gradle の maxParallelForks もあるよ

Jenkins とは関係ないですが、Gradle の Java プラグインにはテストを並列実行するための機能 maxParallelForks があります。 ただし、こちらは同一マシン上でプロセスを複数立ち上げて並列実行するものです。 test.maxParallelForks = 4 のように並列実行数を指定するだけなので、 Jenkins のプラグインを使うよりはるかに簡単です。 もし、以下のような条件がそろっている場合は maxParallelForks を使ったほうがよいですね。

  • テストがデータベースなどに依存しておらず、完全に独立している
  • マシンのリソース的にも同一マシン上の複数プロセスでテストを並列実行するだけの余裕がある

Jenkinsfile を書く前に知っておくべきこと (機能制約編)

前回は Jenkinsfile を書く上で知っておくべきセキュリティ上の制約について紹介しました。

arasio.hatenablog.com

今回は、機能上の制約を書きたいと思います。 ここで言う機能上の制約とは、Jenkins がジョブの一時停止/再開機能を実現するために、代償としてパイプラインでは使えなくなっている Groovy の制約のことです。

一時停止/再開機能をどう実現しているかを説明してから、それに起因する制約と回避策を紹介します。

ジョブの一時停止/再開

パイプラインジョブは実行中に一時停止し、そこから再開することができます。 これを実現するために、パイプラインスクリプトはそのままコンパイルされるのではなく、 Jenkins 内部で持っている Groovy インタープリタで処理されます。 そして、一メソッドずつ命令を実行し、スタックなども含めたプログラムの状態を逐一永続化しながらスクリプトを処理しています。 スクリプトを再開するときは、外部ファイルからプログラムの状態を読み込んで復元しています。

CPS 変換

プログラムの状態を保存できるようにするため、Jenkins は内部でパイプライスクリプトを中間スクリプトに変換してから実行しています。 この変換を CPS 変換といいます。CPS 変換について詳しく知りたい方は groovy-cps/cps-basics.md at master · cloudbees/groovy-cps · GitHub などを参考にしてください。

CPS 変換でやりたいことは、同期メソッド呼び出しを非同期に変換することです。 同期メソッドだとパイプラインスクリプト内でそのまま処理が流れてしまうので、どこまで処理が進んだのか Jenkins が管理することができません。 そこで、JavaScript のコールバックのように非同期処理に変換し、メソッド呼び出しごとに処理が終わったら Jenkins が結果をもらう、そして状態を保存する、という仕組みになっています。

例えば、以下のようなパイプラインスクリプトを考えます。

Object foo(int x, int y) {
    return x+y;
}

これをビルドすると、Jenkins は CPS 変換により下のような中間スクリプトを生成するようです *1。 コールバックは例外のしくみで実現しているみたいですね。

Object foo(int x, int y) {
    throw new CpsCallableInvocation(___cps___N, this, new Object[] {x, y});
}

private static CpsFunction ___cps___N = ___cps___N();

private static final CpsFunction ___cps___N() {
    Builder b = new Builder(...);
    return new CpsFunction(['x','y'], b.plus(b.localVariable("x"), b.localVariable("y"))
}

私もあまり理解できていないのですが、ここでは変換処理の詳細よりも、パイプラインスクリプトはそのまま実行されるのではなく、 中間処理として非同期メソッドへの変換が入るということを覚えておけばよいと思います。

Serializable

プログラムの状態を保存するというのは、つまりインスタンスなどの状態を保存することです。 インスタンスは通常、さまざまなポインタを含んでいるのでそのままでは状態を保存することができませんが、 Serializable にすると外部にインスタンスの状態を出力することができるようになります。 Serializable ついて詳しく知りたい方は Java直列化メモ(Hishidama's Java Serializable Memo) などを参考にしてください。 Jenkins でもこの仕組みを使ってプログラムの状態を保存しています。

制約

CPS 変換が可能であることと Serializable がパイプラインスクリプトの要件だとわかりました。 逆に言うと制約としては

  • 制約1: CPS 変換に対応していないメソッドは使えない
  • 制約2: Serializable でないクラスは使えない

ということになります。それぞれ例を使って説明したいと思います。

制約1: CPS 変換に対応していないメソッドは使えない

これは現状 Jenkins が対応していないだけで、今後対策されると思われますが、 非常に一般的なクロージャを使ったループ表現が使えません (Jenkins 2.7.4) 。

例えば以下のようなスクリプトです。

// UnsupportedOperationException になる例
def list = ['1', '2', '3']
list.each { it ->
    echo it
}

これを実行すると下のように UnsupportedOperatinException が発生します。 エラーメッセージにあるとおり、each は Jenkins の CPS 変換でまだサポートされていないので使えないとのこと。

java.lang.UnsupportedOperationException: Calling public static java.util.List org.codehaus.groovy.runtime.DefaultGroovyMethods.each(java.util.List,groovy.lang.Closure) on a CPS-transformed closure is not yet supported (JENKINS-26481); encapsulate in a @NonCPS method, or use Java-style loops
    at org.jenkinsci.plugins.workflow.cps.GroovyClassLoaderWhitelist.checkJenkins26481(GroovyClassLoaderWhitelist.java:90)
    at org.jenkinsci.plugins.workflow.cps.GroovyClassLoaderWhitelist.permitsStaticMethod(GroovyClassLoaderWhitelist.java:60)
    at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onMethodCall(SandboxInterceptor.java:92)
    at org.kohsuke.groovy.sandbox.impl.Checker$1.call(Checker.java:149)
    at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:146)
    at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:16)
    at WorkflowScript.run(WorkflowScript:3)
    at ___cps.transform___(Native Method)

回避策は2つあります。
まず、each を使わない for ループにすることでです。

def list = ['1', '2', '3']
for(def elem : list) {
    echo elem
}

もう一つは each の処理を1つのメソッドにしてまとめ、@NonCPS をつけることです。 @NonCPS メソッドは、CPS 変換の対象外となり、そのままバイトコードに変換されて実行されます。

def list = ['1', '2', '3']
printList(list)

@NonCPS
def printList(list) {
    list.each { it ->
        echo it
    }
}

制約2: Serializable でないクラスは使えない

2つ目の制約として、Serializable でないクラスをそのまま使うことはできません。

例1

例えば以下のようなスクリプトです。

// NotSerializableException になる例
node {
    def user1 = new User('user1')
    sh "echo ${user1.name}"
}

class User {
    def name
    User(def name) {
        this.name = name
    }
}

これを実行すると下のように例外が発生します。 ファイルにプログラムの状態を書きだそうとしたところで、User クラスが Serializable でないのでエラーになりました。

java.io.NotSerializableException: User
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:860)
    at org.jboss.marshalling.river.BlockMarshaller.doWriteObject(BlockMarshaller.java:65)
    at org.jboss.marshalling.river.BlockMarshaller.writeObject(BlockMarshaller.java:56)

独自クラスの場合は純粋に Serializable を実装することで回避できます(フィールドも Seriazalibale にする必要があります)。 上の例では User クラスが Serializable インタフェースを実装すれば使うことができるようになります。

class User implements Serializable {
    def name
    User(def name) {
        this.name = name
    }
}
例2

では、独自のクラスではなくライブラリで提供されるクラスが Serializable でない場合はどうでしょうか。 例えば、下のようなスクリプトを考えます。

// NotSerializableException になる例
def map = ['a': 10,'b': 20,'c': 30]
def sum = 0
for(elem in map) {
    sum += elem.value
}
echo "sum: $sum"

一見問題ないようですが、これを実行すると以下のような例外が発生します。HashMap のエントリクラスが Serializable ではないようです。

java.io.NotSerializableException: java.util.LinkedHashMap$Entry

このようなクラスを使う必要がある場合は、制約1と同様に、それに関連する処理を一つのメソッドとしてまとめ、メソッドに @NonCPS をつける回避策があります。

def map = ['a': 10,'b': 20,'c': 30]
def sum = sumMap(map)
echo "sum: $sum"

@NonCPS
def sumMap(def map) {
    def sum = 0
    for(elem in map) {
        sum += elem.value
    }
    return sum
}

注意してほしいのは @NonCPS メソッド内で、通常のCPSメソッドを呼ぶべきではないということです。 呼び出してもエラーにはなりませんが、配下のメソッドがすべて NonCPS で扱われますので注意してください。

まとめ

Jenkinsfile を書くときに注意したい機能上の制約をまとめると以下のようになります。

  • ジョブの一時停止/再開を実現するためにパイプラインスクリプトCPS 変換される
  • CPS 変換に対応していない groovy の文法や、Serializable でないインスタンスは使えない
  • 問題の文法を避ける、または @NonCPS を使って回避する手段がある

参考

Jenkinsfile を書く前に知っておくべきこと (セキュリティ制約編)

Jenkins のパイプラインスクリプト(Jenkinsfile)は groovy ベースですが、 セキュリティや機能上の理由から様々な制約があります。 特別複雑な処理に制約があるというわけでもなく、groovy なら誰もが使いたくなるリストやマップのクロージャ処理が使えなかったり、 Serializable でないオブジェクトを扱えなかったりと、思った以上に使いにくいところがあるようです。

今回はセキュリティ上の制約についてわかったことを書きたいと思います。

対象読者は、Jenkins 公式の Overview を一通り読んでこれから自分でスクリプトを書こうと思っている方、 書いてみたけど RejectedAccessException 例外が発生して先に進めない方などです。

セキュリティ上の制約

誰でも任意のコードをパイプラインスクリプトで実行できるのはさすがにセキュリティ上の問題がありそうです。 そこでパイプラインにはセキュリティを担保する、以下の仕組みがあります。

スクリプト承認

スクリプト承認とは、管理者権限があるユーザが承認しない限り、パイプラインスクリプトそのものが実行できないというものです。 その代わり承認されれば中の処理に関わらず何でも実行することができます。シンプルですね。 なお、管理者自身がスクリプトを設定した場合は自動的に承認されます。

非管理者ユーザが未承認のスクリプトを実行すると、 以下のように UnapprovedUsageException エラーになり、スクリプトは承認待ちの状態になります。

Started by user user1
org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException: script not yet approved for use
    at org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.using(ScriptApproval.java:459)
    at org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition.create(CpsFlowDefinition.java:106)
    at org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition.create(CpsFlowDefinition.java:59)
    at org.jenkinsci.plugins.workflow.job.WorkflowRun.run(WorkflowRun.java:214)
    at hudson.model.ResourceController.execute(ResourceController.java:98)
    at hudson.model.Executor.run(Executor.java:410)
Finished: FAILURE

承認待ちのスクリプトは管理者がブラウザから [Jenkins の管理] > [In-process Script Approval] で確認することができます。 ここで「Approve」をクリックするとスクリプトが実行できるようになります。(画像の赤枠部分)

f:id:arasio:20161007004746p:plain

Groovy サンドボックス

Groovy サンドボックスとは、使えるメソッドが制限されたサンドボックスの中でパイプラインを実行するというものです。 許可されていないメソッド呼び出しがあると、RejectedAccessException が発生し、ビルドが失敗します。

例えば、Thread.sleep(1000) という一行だけのパイプラインスクリプトでも、Thread.sleep() が未承認のためエラーになります。 エラーメッセージを見る限り、すべての静的メソッドは承認が必要なようです。

Started by user user1
[Pipeline] End of Pipeline
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod java.lang.Thread sleep long
    at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectStaticMethod(StaticWhitelist.java:190)
    at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onStaticCall(SandboxInterceptor.java:142)
    at org.kohsuke.groovy.sandbox.impl.Checker$2.call(Checker.java:180)
    at org.kohsuke.groovy.sandbox.impl.Checker.checkedStaticCall(Checker.java:177)
    at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:91)
    at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:16)
    at WorkflowScript.run(WorkflowScript:1)
    at ___cps.transform___(Native Method)

実行しようとした未承認のメソッドも、スクリプト同様に UI から確認できるようになっています。 上の画像で青枠の部分になります。この例では Thread.sleep() が未承認となっています。 メソッドを確認し問題なければ「Approve」をクリックし、承認リストに追加することで、次回以降パイプラインスクリプトの中で実行できるようになります。

しかし、これは非常に面倒な作業です。なにしろ、実行してみないと何が使えないのかわかりません。 いくつかのメソッドはデフォルトでホワイトリストに入っているようですが、 私の感覚では、スクリプト内でちょっとデータ処理をしようと思うとほぼ確実に RejectedAccessException に出くわします。 一つ進んだと思ったら次のメソッドでまたエラーというのもよくあることです。 その度にジョブが失敗するので、はっきり言って結構萎える作業です。

設定方法

実際にパイプラインを作成するとき、どちらを使うか設定する方法を紹介します *1。 ここで、パイプラインスクリプトを Jenkins の UI 上に直接記述するか、バージョン管理システムからチェックアウトするかが重要になってきます。

1. パイプラインスクリプトを UI 上で直接記述する (Pipeline Script)

簡単な動作確認などをしたい場合、下のように Jenkins の UI 上で直接編集する場合があります。

f:id:arasio:20161007003144p:plain

ここで "Use Groovy Sandbox" にチェックを入れるとサンドボックスで実行されるようになります。 チェックをはずすと、スクリプト承認になります(ジョブを設定したのが管理者であれば自動承認)。 特に難しいところはありません。

2. パイプラインスクリプトを SCM で管理する (Pipeline Script from SCM)

パイプラインスクリプトは普通 git などでバージョン管理していると思います。 注意してほしいのは「Pipeline script from SCM」を選択すると、Groovy サンドボックスが強制的にオンになります。 チェックボックスがなく、選択の余地はありません。

f:id:arasio:20161007002959p:plain

管理者がビルドを実行するときも例外ではなく、未承認のメソッドが含まれる場合はビルドが失敗します。そして通すには管理者としてメソッド承認する必要があります。 おそらく SCM 管理だと誰が編集しているのかわからないので、常に Groovy サンドボックスで実行するようになっているのでしょう。

さきほど愚痴ったように、サンドボックスの仕組みは正直なところ実運用上とても使いにくいと感じています(特に小規模のチームでは)。 そこで、以下のようなハックも考えられています。

  1. パイプラインスクリプト自体は SCM で管理する
  2. パイプラインジョブではスクリプトを直接書く形式を選択し、Groovy サンドボックスをオフにする
  3. ジョブのスクリプト内で、パイプラインスクリプトを SCM からチェックアウトし実行するスクリプトを記述する

こうすればパイプラインスクリプト本体を SCM 管理にしたまま、Groovy サンドボックスをオフにした状態でジョブを実行することができます。 詳しくはこちらを参考にしてください。 セキュリティ上のリスクはもちろん負うことになります。

まとめ

長々と書きましたが、まとめると以下のようになります。

  • Jenkinsfile で記述できる groovy スクリプトにはいろいろな制約がある
  • セキュリティ上の制約として、スクリプト認証と Groovy サンドボックスの2つの仕組みがある
  • スクリプト認証
    • シンプルで運用が簡単
    • パイプラインスクリプトを UI 上で直接書く場合に使える
    • パイプラインスクリプトを SCM で管理する場合は基本的に使えない (回避策は一応あり)
  • Groovy サンドボックス
    • 運用が複雑というか面倒な代わりに、セキュリティ管理がしっかりできる
    • メソッド単位でサンドボックスで実行できるものを承認
    • 規模の小さいチームでは非効率でオーバースペック


機能上の制約についても書きましたのでご参考まで (2016/10/08 追記)

arasio.hatenablog.com

参考

*1:どちらも使わないという選択肢は残念ながらないようです

寄生獣が好きな人のための Kindle Unlimited で読めるおすすめ漫画

根拠は何もないですが寄生獣が好きな方におすすめしたい、Kindle Unlimited で読める漫画を紹介します。
なんというか私の好みを晒しているだけですねw

国民クイズ (上・下)

国民クイズ  上

国民クイズ 上

近未来の日本は民主主義を捨て「国民クイズ」と呼ばれる国家主催のクイズ番組が国権の最高機関となっている、というぶっ飛んだ設定がどう考えても面白いです。 クイズに合格すると違法だろうがなんだろうが望みを国が叶えてくれます。その代わり、不合格の場合は反社会勢力として捕まります。 望みのレベルが高いほど合格するのが難しく、また不合格だった場合の罰も厳しくなります。 あなたは何を望むでしょうか?

日本国憲法第12章 国民クイズ国民クイズの地位)

第104条「国民クイズは国権の最高機関であり、その決定は国権の最高意思、最高法規として、行政・立法・司法、その他ありとあらゆるものに絶対・無制限に優先する。本憲法もその例外ではない。」

バイバル (全10巻+1)

サバイバル 1巻

サバイバル 1巻

中学生の主人公が友達と洞窟に遊びに行ったところ、急に強い地震に見舞われ、洞窟は崩れてしまいます。 主人公はなんとか洞窟から抜け出すのですが、そこはなぜか海に囲まれた孤島になっていました。 そこで主人公は一人、サバイバル生活を強いられます。中学生なんですが、生命力が強すぎてまぶしいです。 法の秩序が崩壊した世界で人間がどういう行動にでるのか、考えてみるのもおもしろいですね。 地球温暖化や気候変動などを取り扱った社会派な面もある漫画です。

荒野のグルメ (1, 2巻)

孤独のグルメで有名な久住昌之さんのシリーズです。 仕事で疲れたサラリーマンが行きつけの小料理屋で心と体を癒す漫画ですw ありふれた日常をここまで魅せてくれるのがすごいです。 女将さんの描写が素晴らしく、 いつも顔が見えそうで見えない角度で何か料理などをしながら話をきいてくれます。 女将さんとの距離感も絶妙です。

Jenkins のパイプラインでマスターで実行するタスクを node ブロックで囲む意味

Jenkins のパイプラインスクリプトはマスターで処理されます。 また、各ノードで実行するタスクは node ブロックで囲んで表現します。

パイプラインスクリプトがそもそもマスターで処理されるのであれば マスターで実行したいタスクがあるとき、わざわざ node ブロックで囲む意味は何でしょうか? 例えば下の2つのパイプラインスクリプトを見てください。 どちらもマスターで echo が実行されます。この2つのスクリプトの違いについて書きたいと思います。

node('master') {
    echo 'Hello world'
}
echo 'Hello world'

エグゼキュータータイプ

この違いにはエグゼキューターのタイプが関係しています。

エグゼキューターとは実際にタスクを実行する計算リソースのことですが、 エグゼキューターには以下の2種類があります。

  • 軽量エグゼキューター (flyweight executor)
  • 重量エグゼキューター (heavyweight executor)

後者はジョブを実行するときに必要なふつうのエグゼキューターです。 マスターやスレーブの各ノードで並列実行が許可されている場合は、その数だけエグゼキューターが存在します。 ここでは軽量エグゼキュータと比較するために「重量」と表現していますが、一般的なただのエグゼキューターです。

それに対して軽量エグゼキューターはエグゼキューターのスロットにカウントされない一時的なものです。 軽量エグゼキューターはスロットにカウントされないので、たとえ実行中であっても Jenkins のビューから見えるエグゼキューターのスロットに表示されません。 スロットに空きがなくても実行できるのです。

node で囲む意味

話をもとに戻して、node で囲む意味ですが、

  • node で囲われていない処理は、軽量エグゼキューターによりマスターで実行される
  • node で囲われた処理は、重量エグゼキューターにより対応するノードで実行される

という決まりがあります。 最初の例では実際のところ、ほとんど処理がないのでどちらでもあまり関係ないのですが、 本来はビルドなどリソースを多く消費する処理が想定されます。 node で囲わずに軽量エグゼキュータでこのような処理を実行するとマスターサーバが重くなり Jenkins サービス自体のパフォーマンス低下をまねく恐れがあります。 スロット数で制御することもできないので、裏でいつの間にか重い処理がいくつも動いていたということにもなりかねません。 基本的にはタスクを node 内で実行するようにしましょう。

ただし、node で囲わずに軽量エグゼキューターで実行したほうがよいものもあります。 例えば、ジョブをユーザの承認待ちにする、input を使うときです。 下の例は stage1 が終わった後、user1 というユーザの承認を待ってから stage2 を実行するパイプラインスクリプトです。

stage('stage1') {
    node {
        echo 'stage1: 実際は重い処理'
    }
}

timeout(time:5, unit: 'DAYS') {
    input message: '承認しますか?', submitter: 'user1'
}

stage('stage2') {
    node {
        echo 'stage2: 実際は重い処理'
    }
}

この例では、stage1 が成功した後、ユーザが認証しない限り、stage2 が実行されません。 ユーザの認証はいつ実行されるかわからないブロッキング処理です。 したがって、input を node 内で実行してしまうと、承認されるまで無意味なブロッキングタスクがエグゼキューターを1つ占有し続けてしまうことになります。 このようなタスクは軽量エグゼキューターで実行するのがよいでしょう。

ちなみに、input に対するユーザアクションは、ブラウザでジョブのコンソールログなどから続行するかアボートするかを選択することができます。

参考: Getting Started with Pipeline

Jenkins のパイプラインでは従来のプラグインは使えない

Jenkins 2 のパイプラインを使い始めました。 いろいろ試してみて私が一番驚いたのは、これまでのプラグインはどうやら基本的に使えないということです。

この記事は下のような方を対象にしています。

  • Jenkins 1.x で簡単なジョブ設計の経験がある
  • パイプラインは未経験もしくはチュートリアルを終えたくらい

なお、私自身パイプラインに触れて間もないので間違いなどがあればご指摘いただけるとうれしいです。

パイプラインジョブ

パイプラインを使った経験が全くない方のために少し説明すると、 パイプラインジョブは、新規にジョブを作成するときに「パイプライン」という専用のジョブを選択して作成します。 それ以外の例えばフリースタイルジョブなどを選択すると、パイプラインを使うことはできません。 従来のジョブとは明確な違いがあります。

Pipeline as Code

パイプラインでは、Jenkinsfile と呼ばれるスクリプトに、 ジョブの設定も含めて全てコードでフローを記述するような設計思想 (Pipeline as Code) があります。 パイプライン用のプラグインというのは、Jenkinsfile から実行できる API を提供するかたちになります。 というわけで、これまでジョブ設定のページ(GUI)でポチポチ挙動を設定していた従来のプラグインAPI を提供していない限り使うことができません。

パイプライン用のプラグイン

パイプライン用のプラグインには、 〇〇 Pipeline Plugin とか 〇〇 Workflow Plugin などの名前がついていることが多いです。 メジャーなプラグインはすでにパイプライン用のプラグインも公開されている場合が多いでしょう。 数は少ないですが、有名なプラグインのパイプライン対応状況は以下で紹介されています。

pipeline-plugin/COMPATIBILITY.md at master · jenkinsci/pipeline-plugin · GitHub

その他のプラグインについてはプラグインの公式 wiki を参照するなどして、対応状況を確認するしかありません。 ややこしいことに、プラグインの名前からは元のプラグインのパイプライン版なのか、別物なのか判断できません。 例えば Docker PluginDocker Pipeline Plugin というプラグインがあります。 一見、前者のパイプライン版が後者のように見えますが、この 2 つは機能が違う別物です *1

プラグインの公式ページでパイプラインに対応しているか、必要な機能を実現するプラグインなのか、注意して確認しましょう。

パイプラインへの移植は慎重に

1.x の非パイプラインジョブをパイプラインに書き直すときは、プラグインの対応状況などを調査して そもそも実現可能か慎重に検討する必要があります。 Jenkins 2.x は 1.x と後方互換があるとされています *2 ので、無理して書き換える必要もないですが、 Pipeline as Code の波にのりたい方は心得ておいたほうがよさそうです。

それからパイプラインはまだまだ発展途上の技術といった印象で、 後方互換が失われるようなパイプラインスクリプトの仕様修正も見受けられます。 本格的な導入はもう少し枯れてからでも遅くないと思いました。

*1:詳細はまた別に機会に書きたいと思いますが、簡単にいうと、

Docker Plugin: Docker コンテナ自体をスレーブノードとして動的に立ち上げるプラグイン
Docker Pipeline Plugin: すでに登録済みのノード上で Docker を操作するプラグイン

*2:こちらに書いたように一部のプラグインが使えなくなる場合もあるようです・・・