もしJenkinsでちょっとずつ違う100のジョブを管理しなくてはいけなくなったら

(2015/05/14 追記) このエントリで言及しているSubversionリポジトリ複数指定できない問題ですが、今は解決されています。

個人用メモも兼ねて。

ちょっとこんがらがったプロジェクトではありがちですが、ある特定の環境へのリリースのためにいくつかのジョブで構成されたビルドパイプラインがあり、それを環境ごとに微妙な差分をつけて、複数種類のビルドパイプラインを構成する必要があったりします。

環境の数やアプリの数によっては、これらの差分を含めてJenkins上にジョブを構成しようとすると、下手をすると100個近いジョブに到達することもあります。

(いうまでもないですが、こういうジョブ過多な状態を通常は目指すべきではありません。)

こういう場合に手作業でJenkinsのジョブを構成しようとすると恐ろしいクリックゲームになるので、Jenkins公式が用意しているJob DSL Plugin を使ってプログラミングで大量のジョブを効率よく構成しましょう。

Job DSL Pluginの概要

https://github.com/jenkinsci/job-dsl-plugin/wiki

  • 普通にプラグイン設定画面からインストール可能です。入れてない人はぜひ入れましょう。
  • GroovyのDSLで、Jenkinsのジョブやビューをサクッと構成可能。
    • 繰り返しや分岐やマップなどJava(Groovy)にある道具を使ってジョブの生成を柔軟に行える。
    • 基本的にDSLでジョブ・ビューごとの設定ファイル(XML)を出すようなイメージ。
    • 作ったジョブは、作成後、作成用のジョブのトップページに表示されます。
      • すでにあるジョブを作成した場合は、設定を上書き。今まであったジョブの履歴などはきちんと保存されます
      • 以前に作成したけれども、次回以降作成対象にならないジョブ(要するに定義が変更されて消えるジョブ)は、放置するか、無効にするか、削除するかといったことも指定可能です。
  • 意外なほど多種のプラグインに対応している。
    • このプラグインDSLが定義されているJenkinsプラグインはメジャーなプラグインと思ってもいいかもしれません。
    • Job DSL Pluginが対応外のものでも設定ファイル(XML)ほぼそのままに記述することにより対応可能。手作業ジョブ設定作業の完全代替にほぼなるはず。
  • (ちなみにSCM Sync Pluginとこれを一緒に使うと、内部のジョブ定義1つごとにSCMにコミットされます。ジョブ定義をするたびにコミットログがウザいことになりますが、気にしないことにしましょう:-p )

さっそくJob DSLを書いてみる

ローカル開発環境のインストール (Windows可)

JenkinsのJob DSL PluginのGroovyソースのテキストエリアにDSLを貼り付けてもジョブの生成はできるのですが、

  • Groovyの文法が正しいのか、
  • セマンティックなエラーは起きないのか、
  • どういうジョブ定義が出力されるのか、

などなどJenkins上だけで実行ボタンをおしたりしてチマチマ確認するのはとっても面倒なので、ローカルでDSLの実行結果を確認できる環境を作ると非常に捗ります。

詳しい手順は https://github.com/jenkinsci/job-dsl-plugin/wiki/User-Power-Moves に書いてあるのですが、とりあえずここでも紹介しておきます。

まずは、Groovyを扱うため、JDKを入れて使えるようにしておきましょう。

あとはJob DSL Pluginをgit cloneしたりして

$> git clone https://github.com/jenkinsci/job-dsl-plugin.git
$> cd job-dsl-plugin

ちょっと適当なJob DSLを所定のフォルダにおいてあげましょう。下記のJob DSLjob-dsl-core/sample.groovyとして保存してください。

def jobNames = ['Alpha', 'Bravo', 'Charlie']

for (jobName in jobNames) {
  job {
    name jobName
  }
}

ここまでやったら下記コマンドを実行すれば、

./gradlew run -Pargs=sample.groovy

job-dsl-core/ 以下に下記3つのそれぞれジョブの設定が入ったファイルができているはずです。

  -rw-r--r--   1 knjname  staff   588 Jan  6 01:29 Alpha.xml
  -rw-r--r--   1 knjname  staff   588 Jan  6 01:29 Bravo.xml
  -rw-r--r--   1 knjname  staff   588 Jan  6 01:29 Charlie.xml
(Alpha.xmlの中身)
<project>
    <actions></actions>
    <description></description>
    <keepDependencies>false</keepDependencies>
    <properties></properties>
    <scm class='hudson.scm.NullSCM'></scm>
    <canRoam>true</canRoam>
    <disabled>false</disabled>
    <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
    <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
    <triggers class='vector'></triggers>
    <concurrentBuild>false</concurrentBuild>
    <builders></builders>
    <publishers></publishers>
    <buildWrappers></buildWrappers>
</project>

基本的にはDSLを編集→gradlew run ...を実行→ローカルに出力されたXMLの中身を確認→DSLを編集、というループで開発していき、納得できる形になってから最後にJenkins上でDSLを実行という形になると思います。

Jenkins上でJob DSLを動かしてジョブを定義する

特に難しいことはなく、Tutorialにあるように、ジョブ生成用の種ジョブを作成するだけですね。

種ジョブは通常のジョブとほとんどかわらないので、たとえばJob DSLや特定の定義ファイルが更新されたら起動するように種ジョブのトリガをしかけておいて、状況に応じてジョブが勝手に増えるようにすることもできます。

ほか

プラグインに対応したビュー、ジョブ、トリガー、前処理、後処理などなどの指定方法はWikiを見ましょう。リファレンスが充実しています。意外に対応範囲が広いことに驚くと思います。

Wiki見てピンとこなければGithub上でソースで該当の関数を検索するのがてっとり早いです。

自分の使っているプラグインに該当するDSLがなければ configure ブロックを駆使して設定ファイルほぼそのまんまで記述しましょう。

ちょっとしたTipsなど

Gradleでの*.groovyファイルの指定方法いくつか

入力となるDSLのgroovyファイルを絶対パスで特定の場所で指定したい人は-Pargs=/root/hoge.groovy (Windowsなら -Pargs=\C:\Users\root\hoge.groovyみたいに指定)、複数指定したい人は"-Pargs=hoge.groovy fuga.groovy" とすれば大丈夫です。

ちなみにXMLの出力場所の指定方法は知りません!

Gradleの起動が遅くてイラつく

gradlewの起動が遅くてイライラする人はgradlewのユーザごとプロファイル設定ファイル(~/.gradle/gradle.properties)に下記の値を指定してGradle用のJVMをデーモン起動させるといいらしいよ!

org.gradle.daemon=true
DSL上で使えるコンテキスト依存の関数の作り方(+Subversionの場合の複数箇所チェックアウト)

たとえば、Subversionを使っている場合、普通に1ジョブ内で複数箇所からチェックアウトすると思うのですが、DSLではこの手のケースは想定されていません!

だから公式ではこうやれってよ!

scm {
  svn('http://hoge/svn/trunk', 'hoge') {
    it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' {
      remote 'http://fuga/svn/trunk'
      local 'fuga'
    }
    it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' {
      remote 'http://baz/svn/trunk'
      local 'baz'
    }
  }
}

ロケーションの1個目だけを特別扱いして書いて、あとは無理やり設定XMLまんま書けとかイケてないですね。

せっかくプログラミング言語を使っているので、関数に出してすっきりさせてあげましょう。

def svnRepositories = { repositories ->
  svn(repositories[0].remote, repositories[0].local) {
    if(repositories.size == 1) return
    for(repos in repositories[1..-1]) {
      it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' {
        remote repos.remote
        local repos.local
      }
    }
  }
}

// ...

scm {
  svnRepositories([
    [remote: 'http://hoge/svn/trunk', local: 'hoge'],
    [remote: 'http://fuga/svn/trunk', local: 'fuga'],
    [remote: 'http://baz/svn/trunk', local: 'baz'],
  ])
}

Groovy的にいいコードなのかどうかわかりませんが、適当に上記のようにしてみました。

しかし、これを実行すると下記のように例外が発生してしまいます。

Exception in thread "main" groovy.lang.MissingMethodException: No signature of method: sample.svn() is applicable for argument types: (java.lang.String, java.lang.String, sample$_run_closure1_closure3) values: [http://hoge/svn/trunk, hoge, sample$_run_closure1_closure3@279fedbd]
Possible solutions: run(), run(), any(), find(), wait(), dump()
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:55)

まあ当然といえば当然なのですが、GroovyのDSLと呼ばれているものは、DSLを書く場所に実行時、あらかじめコンテキストオブジェクトがはりついており、コンテキストオブジェクト上のメソッド―上記だとsvn(...)を呼ぶことによって実装しているわけですね。

ところが外部関数にいくと関数呼び出し元のコンテキストもへったくれもない。見えないというわけです。さあて困った。

なので、下記の苦しい呪文をsvnRepositories([...])の前に挟みます。

scm {
  svnRepositories.delegate = delegate // 苦しい呪文
  svnRepositories([
    [remote: 'http://hoge/svn/trunk', local: 'hoge'],
    [remote: 'http://fuga/svn/trunk', local: 'fuga'],
    [remote: 'http://baz/svn/trunk', local: 'baz'],
  ])
}

この苦しい呪文より、うまくいくようになります。同様にDSL内で関数くくりだしをした場合は上記呪文を挟めばいけます。

delegateでコンテキストオブジェクトを参照し、さらに関数の同名のプロパティにあらかじめ設定することで、その関数内のコンテキストオブジェクトを書き換えることができるので、関数呼び出し元のコンテキストオブジェクトを伝播させてやっているわけですね。正直Groovyはまったく詳しくないので、厳密にどうなっているのか知りませんが。)