Jenkinsのpipelineには2通りあります。
- declarative pipeline
- scripted pipeline
本記事は scripted pipeline の書き方です。
Jenkins2では、Groovy DSLを用いたpipelineの記述ができるようになったらしい。若干の時代遅れ感があるけど、最近仕事で使う機会があり、土日にわからないところを整理したのでメモる。
ジョブ定義を画面からぽちぽちするなんて時代遅れ!技術者として恥ずかしい!これこそがモダン!とか煽るつもりはまったくないけど、ジョブ定義の柔軟性が高いし、パイプラインに人の承認フローが組み込めたり、実行ノードを簡単に選べるし、何よりコード管理できるので、普通に良さそうだと思いました。情報が少ないことを除けばな!
Jenkins2のインストール
公式サイトからwarを落とすなりdockerで起動するなりして、Jenkinsをインストールする。(今回利用したのはver 2.32.1)

ジョブを作る。

通常はジョブをJenkinsfileに記述し、リポジトリのトップディレクトリに置いておく。
が、今回は勉強が目的なのでここにジョブを記述する。

とりあえず動かす
とりあえず動かしてみる。
node {
    print "Hello World"
}
実行結果は

コンソール出力で確かめる。

とりあえず動いた。
node
実行ノードを指定できる。Jenkinsが複数のslaveを持つときに、nodeの引数にslave名を指定する。
slaveをひとつ作って試してみる。
左側のビルド実行状態 > 新規ノード作成 から、以下の通り作成する。

slaveが作れた。

nodeの引数にslave名を指定する。
node('slave') {
    print "hello world"
}
実行してコンソール出力を確かめると、slaveで実行されていることがわかる。
Started by user root
[Pipeline] node
Running on slave in /tmp\jenkins/slave\workspace\pipeline-sample
[Pipeline] {
[Pipeline] echo
hello world
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
以下のように否定も書ける。
node('!master') {
    ....
}
環境変数の参照
job内では複数の環境変数を参照できる。
node {
    print "BUILD_NUMBER: ${env.BUILD_NUMBER}"
    print "BUILD_ID: ${env.BUILD_ID}"
    print "WORKSPACE: ${env.WORKSPACE}"
    print "JENKINS_URL: ${env.JENKINS_URL}"
    print "BUILD_URL: ${env.BUILD_URL}"
    print "JOB_URL: ${env.JOB_URL}"
}
実行してコンソール出力を確かめる。
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] echo
BUILD_NUMBER: 2
[Pipeline] echo
BUILD_ID: 2
[Pipeline] echo
WORKSPACE: /var/jenkins_home/workspace/pipeline-sample
[Pipeline] echo
JENKINS_URL: http://192.168.11.100:18080/
[Pipeline] echo
BUILD_URL: http://192.168.11.100:18080/job/pipeline-sample/2/
[Pipeline] echo
JOB_URL: http://192.168.11.100:18080/job/pipeline-sample/
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
参照できる環境変数の一覧は以下から確認できる。



パラメータ付きビルド
ビルドパラメータも定義できる。
node {
  properties([
    parameters([
      text(defaultValue: 'master', description: 'ブランチ名', name: 'branch')]),
      pipelineTriggers([])
    ]
  )
    
  print "branch: ${branch}"
}
一度ジョブを動かさないと反映されないので注意。
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] properties
[Pipeline] echo
branch: master
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
変数の定義
Groovyなので、変数定義や展開ができる。
def MESSAGE='sample'
node {
    print MESSAGE
    print "展開される ${MESSAGE}"
    print '展開されない ${MESSAGE}'
}
実行してコンソール出力を確かめる。ダブルクオートは中の変数が展開されるけど、シングルクオートは展開されないことがわかる。(Groovyの構文覚えないといけない)
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] echo
sample
[Pipeline] echo
展開される sample
[Pipeline] echo
展開されない ${MESSAGE}
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
制御文
Groovyなので、制御文も書ける。
node {
    for (i in 0..9) {
        println i
    }
}
ただセキュリティの観点から、Groovy DSLは色々と機能を絞ったサンドボックス上で実行されるので、デフォルトだとRejectedAccessExceptionになる。
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.ScriptBytecodeAdapter createRange java.lang.Object java.lang.Object boolean
	at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectStaticMethod(StaticWhitelist.java:192)
	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)
実行したければ Use Groovy Sandbox のチェックを外すか、

Jenkinsの管理からScriptを許可する。


ここらへんは詳しく書いている人がいたのでそっちを参照する。
Jenkinsfile を書く前に知っておくべきこと (セキュリティ制約編) - あらしおブログarasio.hatenablog.com
Groovyのメソッド
Groovyのメソッドも書ける。(やっぱりセキュリティにひっかかったりする)
node {
    println new Date().format("yyyyMMddHHmmssSSS")
}
実行してコンソール出力を確かめると、確かにメソッドが実行できる。
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] echo
20170128044540483
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
stage
テスト→ビルド→デプロイ、みたいにビルドのステージを定義できる。
node {
    stage('build') {
        print "build"
    }
    
    stage ('test') {
        print "test"
    }
    
    stage ('deploy') {
        print "deploy"
    }
}
実行するとステージごとにステータスが見れる。

あるステージで失敗した場合、ステージの表示が赤になる。
error という処理を途中で中断したいときに使うコマンドを使って失敗させてみる。
node {
    stage('build') {
        print "build"
    }
    
    stage ('test') {
        error "failed"
    }
    
    stage ('deploy') {
        print "deploy"
    }
}

コンソール出力からもERRORが起きてることがわかる。
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] stage
[Pipeline] { (build)
[Pipeline] echo
build
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (test)
[Pipeline] error
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: failed
Finished: FAILURE
throw new Exceptionでも同じことができる。
ただしコンソール出力にスタックトレースが出る点がerrorコマンドと異なる。
node {
    stage('build') {
        print "build"
    }
    
    stage ('test') {
        throw new Exception("failed")
    }
    
    stage ('deploy') {
        print "deploy"
    }
}
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] stage
[Pipeline] { (build)
[Pipeline] echo
build
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (test)
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use new java.lang.Exception java.lang.String
	at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectNew(StaticWhitelist.java:187)
	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onNewInstance(SandboxInterceptor.java:130)
	at org.kohsuke.groovy.sandbox.impl.Checker$3.call(Checker.java:191)
※ errorコマンドも内部ではAbortExceptionという例外をスローしてるっぽいけど、正直よくわかってない。
失敗しても処理を継続したい場合
何を失敗とするのかはstepによるけど、各stepは、失敗した場合にExceptionをスローするようになっている。
失敗しても処理を継続したい場合は、try-catchすればいい。
node {
    stage('stage1') {
        try{
            print "try"
            error "failed"
        } catch(Exception e) {
            print "catch"
        } finally {
            print "finally"
        }
    }
}
ただしこの場合は例外をにぎりつぶしてるので、ビルド結果がsuccessになってしまう。

Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] stage
[Pipeline] { (stage1)
[Pipeline] echo
try
[Pipeline] error
[Pipeline] echo
catch
[Pipeline] echo
finally
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
currentBuild.result
例外ハンドリングしつつ全体のステータスをfailにしたい場合は、currentBuild.resultを利用する。
node {
    stage('stage1') {
        try{
            print "try"
            error "failed"
        } catch(Exception e) {
            currentBuild.result = 'FAILURE'
        } 
    }
    stage('stage2') {
        print "stage2"
    }
}
こうすると、次のステージに進みつつ全体をFAILUREにできる。

ステージは進めるけどステージごとに成功、失敗を表示させる、というのはできないらしい。ただし、JenkinsのJIRAに上がってるのでそのうち対応される感はある。
https://issues.jenkins-ci.org/browse/JENKINS-26522
もしステージ内でエラーハンドリングして次のステージに進まずに失敗させたければ、例外を投げるかerrorを使うと良さそう。
sh
引数に与えられたシェルスクリプトを実行する。
node {
    sh "date"
}  
実行してコンソール出力を確かめる。
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] sh
[pipeline-sample] Running shell script
+ date
Wed Jan 18 22:57:12 UTC 2017
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
shは別シェルとして実行されるため、シェル変数は引き継がれない点に注意。
node {
    sh "HOGE=xxx"
    sh "$HOGE"
}
Started by user root
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] sh
[pipeline-sample] Running shell script
+ HOGE=xxx
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
groovy.lang.MissingPropertyException: No such property: HOGE for class: groovy.lang.Binding
	at groovy.lang.Binding.getVariable(Binding.java:63)
	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onGetProperty(SandboxInterceptor.java:224)
        at org.kohsuke.groovy.sandbox.impl.Checker$4.call(Checker.java:241)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedGetProperty(Checker.java:238)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedGetProperty(Checker.java:221)
	at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.getProperty(SandboxInvoker.java:28)
exit codeが0以外の場合、例外が投げられる。
node {
    stage('stage1') {
        sh "true"
    }
    
    stage('stage2') {
        sh "false"    
    }
}

コマンドを実行した結果が欲しい場合は以下のようにする。
node {
    OS = sh returnStdout: true, script: 'uname'
    print OS
}
[Pipeline] node
Running on master in /var/jenkins_home/workspace/pipeline-sample
[Pipeline] {
[Pipeline] sh
[pipeline-sample] Running shell script
+ uname
[Pipeline] echo
Linux
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
長めのシェルスクリプトと組み合わせる場合は見やすさのために以下のようにしてる。
(以下はJenkinsのREST APIにアクセスしてアクティブなノードを取ってくる例)
node {
    stage('sample'){
        command = $/
            curl -X GET 'http://localhost:8080/computer/api/json' \
            | jq '.computer[] | select(.offline==false) | .displayName' --raw-output
        /$
        activeNodes = sh returnStdout: true, script: command
    }
}
input
パイプラインに、人のチェックを組み込める。
node {
    stage('test') {
        print "this is test"
    }
    
    stage('permit'){
        input 'Ready to go?'
    }
    stage('deploy') {
        print "start"
    }
}
実行してみると、permitのstageで止まる。

permitのstageにカーソルを合わせると、人のチェックができる。

input ステップを実行する前に、mattermostやmailに通知しておけばチェック依頼も自動化できる。当然、mattermostやmailに通知するステップもある。
ただしnodeの中でinputを利用すると、実行ジョブを1つ消費してしまう。
そのため、input待ちの間にジョブが滞留する原因になる。

node外でinputを定義すると、実行ジョブを無駄使いしなくて済むらしい。
node {
    stage('test') {
        print "this is test"
    }
}
    
stage('permit'){
    input 'Ready to go?'
}
node {   
    stage('deploy') {
        print "start"
    }
}

その他のStep
便利なステップが山ほどある。
https://jenkins.io/doc/pipeline/steps/
が、もっとお手軽に、Pipeline Syntaxでstepを作成できる。

値を打ち込めばDSLを生成してくれる。

特にジョブの設定(properties)は、煩雑なのでDSLを生成したほうがラク。
その他のpluginを呼ぶには
調べてもよくわからなかったけど、現状、pipeline pluginに対応したものしか呼べないらしい。
Jenkins: pipeline script - call pluginstackoverflow.com
なので今時点(2017/1/30)では、emotional-jenkins-pluginを入れてJenkinsおじさんを怒らせることができないっぽい。
なんだって?jenkinsおじさんが怒ってくれないだと?使うわけないだろ!こんなもん!!!…とお怒りの皆さま、安心してください。
emotional-jenkins-pluginにpull requestが出されてるので、そのうち使えるんじゃないかと思います。
https://github.com/jenkinsci/emotional-jenkins-plugin/pull/2
またpipelineはJenkinsの標準機能なので、他のpluginもそのうち対応されていくと思いたい。
複数ブランチに対応する
git flow でブランチを運用するときに、複数のブランチが発生しては消えることになる。このときjenkinsでブランチごとにジョブを自動で生成したり削除するには、 新規ジョブ作成時に multibranch pipeline を選ぶ。
jenkinsfileを書くときの注意点
公式にまとめられている。
Top 10 Best Practices for Jenkins Pipeline Plugin | CloudBees Blog
また経験則的には、以下が見通しが良いと思う。
- ビルドの詳細はMavenやGradleといったビルドツールだったりシェルスクリプトに記述する
- Jenkinsfileはビルドタスクの呼び出し、ビルドフローの定義、ツール連携 に注力する
最後に
柔軟性が高いし、パイプライン中に人の承認フローが組み込めたり、実行ノードが簡単に選べるので良さげ。ただし、あまり柔軟性を持たせてもビルド職人を生むだけなので、意識的にDSLに閉じた操作にとどめたほうが無難だと思いました。
以下、わかってないのでそのうち調べる
- pluginが動く仕組みhttps://github.com/jenkinsci/pipeline-plugin/blob/master/DEVGUIDE.md
- サンドボックスの詳細https://github.com/jenkinsci/workflow-cps-plugin
Dockerを使う場合の記事も書いた
Jenkinsジョブ(ビルド~テスト)をDockerコンテナ上で実行する ~Docker Pipeline Pluginを使ってみる~ - SIerだけど技術やりたいブログkimulla.hatenablog.com