Jenkinsの公式Dockerイメージ使ってみた

以前のエントリ( http://knjname.hateblo.jp/entry/2014/05/03/190842 )で自分でJenkinsのDockerイメージを作成したりしてみましたが、 Jenkins公式でDockerイメージを配布するようになったので、それを使用したほうがいいと思います。

普通に使うだけなら、下記のようにすればいいだけですが、

docker run -p 8080:8080 jenkins

これだと何も細かいことを設定できていないので、いくつか補足。

Dockerイメージ内のJenkinsのバージョンについて

基本的にJenkinsのサイトのトップ( http://jenkins-ci.org/ )に載っているようなマジもんの最新版は不安定です。

ただ、DockerイメージのlatestはLTS版(安定版)の最新がデフォルトで指定されているようです。 ちゃんと運用するのであれば明示的なバージョン指定をオススメしますが、特に気にしなくても問題なさそうです。

# 今(2015-02-10)のところ下記3つは同じ
docker run -p 8080:8080 jenkins
docker run -p 8080:8080 jenkins:latest
docker run -p 8080:8080 jenkins:1.580.2

# (実はLTS版は1.580.3がリリースされていたりするがDockerHubにはない模様…)

今のところ、使用されるJavaはOpenJDK7のようです。気に入らなければ自分でイメージを作って別のJDKを使わせるようにしたほうがいいでしょう。

永続化ディレクトリ(JENKINS_HOME)について

永続化が必要なデータはコンテナ内の /var/jenkins_home に作成されるので、ホストディレクトリをその位置にマウントしましょう。

docker run -p 8080:8080 -v /hostdir/jenkins_home:/var/jenkins_home jenkins

ただし、root権限でそのまんま作成したディレクトリ(普通にdocker runが勝手に作ったものがこれに該当)はコンテナ側のJenkinsユーザ(jenkins(UID=1000) )で書くことができません。 以下のいずれかで対処しましょう。

  • オススメ: chown 1000 /hostdir/jenkins_home する。(※UIDはDockerfile内で明示的に指定されているので変わる可能性は低いでしょう)
  • chmod 777 /hostdir/jenkins_home する。
  • 独自のJenkinsイメージを作成してJenkinsの実行ユーザをrootにする。
  • 一度マウントしないで起動させたコンテナ内から/var/jenkins_homeをコピーしたものをあらためてマウントする。

JenkinsのJVMのオプションについて

JavaのメモリetcのためのJVMオプションはJAVA_OPTSという名前で環境変数に設定すればいいです。

docker run -p 8080:8080 -e JAVA_OPTS="-Xmx1g" jenkins

Jenkinsのオプションについて

たとえば、 http://hostname/jenkins のようにURLプレフィックスを設定するには、docker run jenkinsの後ろに引数追加すればいいです。あるいはJENKINS_OPTSに定義すればいいです。

docker run -p 8080:8080 jenkins --prefix=/jenkins
docker run -p 8080:8080 -e JENKINS_OPTS="--prefix=/jenkins" jenkins

Jenkins用の引数一覧は https://wiki.jenkins-ci.org/display/JENKINS/Starting+and+Accessing+Jenkins で一部リスト化されています。

ログファイルの出力先などもオプションでの指定できるはずなのですが、記載がないですね。https://github.com/jenkinsci/jenkins/blob/master/rpm/SOURCES/jenkins.init.in とかを参考に追加してみるといいかもしれません。

docker run -p 8080:8080 jenkins --logfile=/var/jenkins_home/jenkins.log

Jenkins初期化Groovyスクリプト

ほか、設定ファイルに永続化されておりパラメータでは初期指定できない情報や、 初期プラグインを勝手にインストールさせるなどを行わせるGroovyスクリプトをロードさせることができます。

(Groovyでの自動化の詳細は https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Script+Console を見て下さい。)

ためしに、下記のスクリプト(my-jenkins-init.groovy)が入ったディレクトリを /var/jenkins_home/init.groovy.d/ にマウントしてみましょう。

// my-jenkins-init.groovy
import hudson.model.*;
import jenkins.model.*;

def inst = Jenkins.instance

def uCenter = inst.updateCenter

// Set the proxy for retrieving plugins. (Comment out and modify it if you need.)
// inst.proxy = new hudson.ProxyConfiguration("192.168.10.10", 3128, "proxyUser", "proxyPassword", "localhost, 192.*")

// Ensure the Update Center is up-to-date.
uCenter.updateAllSites()

def checkIfPluginInstalled = { pluginid ->
  for(p in inst.pluginManager.plugins) {
    if(p.shortName == pluginid){
      return true
    }
  }
  return false
}

// Installs the following plugins if not installed.
["checkstyle", "copyartifact", "build-timeout", "emotional-jenkins-plugin"].findAll({! checkIfPluginInstalled(it)}).each {
  uCenter.getPlugin(it).deploy()
}

// Set the number of executors (in master-node) to 0.
inst.setNumExecutors(0)

inst.reload()

自動でいくつかプラグインがインストールされており、マスターノードのエグゼキュータ数が0の設定が行われているはずです。

初期のイメージでは、スレーブ用のJNLPポートの自動設定スクリプトが入っているようです。

// tcp-slave-angent-port.groovy
import jenkins.model.*;

Thread.start {
      sleep 10000
      println "--> setting agent port for jnlp"
      Jenkins.instance.setSlaveAgentPort(50000)
}
Feb 09, 2015 5:08:41 PM jenkins.util.groovy.GroovyHookScript execute
INFO: Executing /var/jenkins_home/init.groovy.d/tcp-slave-angent-port.groovy
Feb 09, 2015 5:08:43 PM org.jenkinsci.main.modules.sshd.SSHD start

(2015/09/09) 今のJenkinsのイメージだとコンテナ起動時に指定された環境変数 JENKINS_SLAVE_AGENT_PORT の番号にJNLPのポートを変更するようになっています。良い改善だと思います。

ということで、

だいたい上記でやりたいことはできます。

yumapt-getで入れたパッケージ版のJenkinsも結局は、 上記で説明しているようなパラメータの設定を設定ファイルにバラしているだけなので、 パッケージ版に機能で遅れを取るといったことは一切心配不要です。

Jenkinsのマスターはマスター!

以前のエントリ( http://knjname.hateblo.jp/entry/2014/05/03/190842 )でも書いたとおり、Jenkinsのマスターノードにビルドを直接させる必要は一切ありません。

大量にいろんな種類のジョブをこなすようになると、スレーブが増えていき、 そんな中でマスターがビルドしているのが不自然かつ、マスター依存のジョブがあった日にはスケールが面倒になってきます。

特に貧弱な環境じゃない限り私はマスターノードにビルドをさせていません。マスターのジョブキューの太さは0にしてます。

今回のエントリでふれているのは、Jenkinsマスターノードのセットアップであり、スレーブノードの話ではありません。 Jenkinsのマスターノードは、WebUIを提供して、設定を保存して、スレーブノードとつながって、 スレーブが収めてくるジョブの履歴などをディレクトリに貯めていればそれでいいんです。(というのが私の考え。)

Jenkinsが活躍している開発現場でマスターノードが死ぬと開発者への影響が大ですが、 スレーブに関していえばホストOSごとおっ死んでも大して影響はないでしょうし、代替のスレーブがいればスレーブの1台2台ダウンしようが 平気なところも多いはずです。

実際ビルドを行わさせるスレーブについては、Jenkinsのビルド用のDockerコンテナを作成してSSHでマスタから繋くとかの平易な構成でいいと思います。 (もちろんDockerコンテナ使い捨てとか頑張るのもアリ)

ちなみにスレーブ内にビルド用のJDKやAntを含めてもいいですが、実際には含めず、Jenkinsの設定画面で JDKやAntのtar.gzやzipをビルド実行時にネットからインストール or どっかのURLから解凍できるように構成したほうが楽です。 (とはいえ、スレーブプロセスはJVMで動くので、スレーブ自身が動くためのJREは必要です。)

現状の公式Dockerイメージへの個人的な不満点

  • OpenJDK7を使っている。OpenJDK8じゃ駄目なのかな。個人的にはヒープはともかく、いろいろやると広がるパーマネント領域を意識したくない。 → 最新のイメージはOpenJDK8使っているようです (2015/08/05)
  • /var/jenkins_homeにroot権限で作成した外部ディレクトリをマウントさせると起動に失敗する件。…まあこれはDocker自体の問題に近いですが。 → docker run 時に -u root オプション付ければうまくいくことも多いでしょう(2015/09/09)
  • Jenkinsのログファイルの出力場所やログレベルの指定など細かい起動オプションが分かりづらい。

VBAで長いパスが扱えないと思ったら

Windowsでは長いパス(260文字前後)を使うと呪いに遭います

Windowsでは長いパスを使うと呪いに遭います。

(個人的には英語の長ったらしいディレクトリ名とファイルパスをついたものをJenkinsでこれまた長ったらしいワークスペースフォルダにチェックアウトした時に遭遇している)

こんなディレクトリとファイルがあったとしましょう。

C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\
    01234567.xlsx
    a.xlsx
    b.xlsx

上記中の 01234567.xlsxWindowsで許されている最長のファイルパスを持つエントリです。

ただ、ファイルパスが長過ぎるためか、、、

  • ExcelVBAのWorkbooks.Open()で開くことができません。(アプリケーション側から開くのも不可能です。)
  • FSOが妙な挙動を示すようになります。

このFSOの妙な挙動というのは、詳しくいうと、下記のようにファイルの列挙に失敗します。

Sub listFiles()

  Dim fso As New FileSystemObject

  Dim i&

  ' 正しいファイル数 3 が表示される
  Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count

  ' 長いファイルパスのFileオブジェクトの作成にFor Each文で失敗し、2が表示される
  For Each f In fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files
    i& = i& + 1
  Next
  Debug.Print i

End Sub

ロングパス対策

では、どうやってこれを克服すればいいかというと、解決方法は簡単で、長いパス部分についてショートパスを使ってあげればOKです。

Sub listFiles2()

  Dim fso As New FileSystemObject
  Dim i&

  Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count
  For Each f In fso.GetFolder("C:\LONGPA~1\012345~2").Files
    Debug.Print f
    i& = i& + 1
  Next
  Debug.Print i

End Sub

同様に、長過ぎるパスのブックをExcelは開けないので、かわりにショートパスを与えてあげれば問題なく開くことができます。

Dim wb As Workbook

' 失敗する
Set wb = Workbooks.Open("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\01234567.xlsx")

' これならOK
Set wb = Workbooks.Open("C:\LONGPA~1\012345~2\012345~1.XLS")

ちなみに上記のようなショートパスでファイルを開いた場合、ショートパスのファイルとしてVBA内では扱われます。

Debug.Print wb.Name ' 01234567.xlsx
Debug.Print wb.Path ' C:\LONGPA~1\012345~2
Debug.Print wb.FullName ' C:\LONGPA~1\012345~2\01234567.xlsx

ショートパス⇔ロングパス変換

たとえばファイルパスの一覧を出すといったマクロの場合、長過ぎるパスを処理する場合に、ショートパスが欲しかったり、逆にショートパスで処理しているが故にレポート出力時ではちゃんとロングパスで出力したい場合があると思います。

ロングパス → ショートパス変換はFSOを使って行うこともできますが、

Dim fso As New FileSystemObject

Dim shortPath As String
shortPath = fso.GetFile("えらい長いパス").ShortPath

そもそも長すぎてGetFileする時点で失敗するかもしれないし、相互の変換については、専用のWindows APIがあるので、それを使いましょう。

以下がWindows API(の宣言とその応用のためのVBAプロシージャ)です。64bit Officeでしか試していませんし素人なので、なんか間違ってるかも。(いつか調べます)

' LongPath -> ShortPath
#If VBA7 Then
Private Declare PtrSafe Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
  (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As LongLong) As LongLong
#Else
Private Declare Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
  (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long
#End If

Function GetShortPathName(ByVal longPath As String) As String
  Const PATH_LENGTH = 260

  Dim pathBuffer As String
  pathBuffer = String$(PATH_LENGTH + 1, vbNull)

  #If VBA7 Then
    Dim pathLength As LongLong
  #Else
    Dim pathLength As Long
  #End If
  pathLength = Win32APIGetShortPathName(longPath, pathBuffer, PATH_LENGTH)

  GetShortPathName = Left(pathBuffer, CLng(pathLength))
End Function


' ShortPath -> LongPath
#If VBA7 Then
Private Declare PtrSafe Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _
  (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As LongLong) As LongLong
#Else
Private Declare Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _
  (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As Long) As Long
#End If

Function GetLongPathName(ByVal shortPath As String) As String
  Const PATH_LENGTH = 260

  Dim pathBuffer As String
  pathBuffer = String$(PATH_LENGTH + 1, vbNull)

  #If VBA7 Then
    Dim pathLength As LongLong
  #Else
    Dim pathLength As Long
  #End If
  pathLength = Win32APIGetLongPathName(shortPath, pathBuffer, PATH_LENGTH)

  GetLongPathName = Left(pathBuffer, CLng(pathLength))

End Function

上記のソースで使っているWinAPIはこちら。

https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx https://msdn.microsoft.com/en-us/library/windows/desktop/aa364980(v=vs.85).aspx

このVBAソースはGistにもあげてあります。

https://gist.github.com/knjname/ba3b7d433655d5930a08

PowerShellのダイナミックスコープについて

PowerShellのスコープが好きになれないし、罠にわりとハマっているので、個人的に動きを確認してみた。

ある程度以上の規模のスクリプトを書くにあたって、PowerShellは特に、スコープの意識が絶対必要です。

基本はダイナミックスコープ

PowerShellのスコープはダイナミックスコープ(スタック上、最後の変数宣言を見る)。

普通の言語みたいにレキシカルスコープ(上に辿って一番近くの変数宣言を見る)ではない。

$foo = "FOO"
"foo is $foo" # foo is FOO

Function Update-foo(){
  $foo = "BAR"
  "foo is $foo" # foo is BAR
}
Update-foo

"foo is $foo" # foo is FOO

まあこれが基本。

ただ、ダイナミックスコープって一言でいうけど、どういうシチュエーションでどう変わるか、確信持てますか?

ダイナミックスコープ七変化

とりあえずどういうスコープで変数が束縛されているのか知るためにいろいろコードのパターン並べてみました。

ちゃんと脳内動作と一致しているか確認してみよう!たぶん一致してないと思うよ!

(僕もいくつかの動作はよく理解できませんでした。いつか学習したい…)

思ったこと

PowerShellがシンプルに見える人は病院いったほうがいいと思う。

パクり元のPerlみたいにmy $lexicalVar; local $dynamicVar;させてほしい…。

Jenkinsのジョブの進化

Jenkinsのジョブの進化をみていくよ〜。

はじまりはシンプルだった

プロジェクトの成果物をチェックするために、チェッカを動かすジョブ check-project-artifacts を作る。

check_aというチェッカを動かす必要があり、そのチェッカを動かした後のワークスペースの成果物はこんな感じ:

check-project-artifacts:
  ./target/check_a-result

一般の開発者は check-project-artifacts のJenkins成果物をブラウザで開いて利用する。

チェッカが増えた

チェッカの種類が増え、もう1つのチェック check_b を実施することになった。

check-project-artifacts のジョブの最後にチェッカの追加をそのまま追加することにした。

check-project-artifacts:
  ./target/check_a-result
  ./target/check_b-result

さらにチェッカが増えた

更にチェッカの種類が増えた。(check_c)

このままチェッカを一つのジョブに追加しつづけると埒があかず、しかもチェッカを直列に実行していると遅いので、チェッカごとにジョブを分割することにした。

  • check-project-artifacts (もともとあったやつ)
    • check-project-artifacts_a (分割後)
    • check-project-artifacts_b (分割後)
    • check-project-artifacts_c (分割後)

それぞれ子供側のチェッカのジョブが終了したら、自分のチェック結果を成果物として保存し、 check-project-artifacts を後続ジョブとして呼ぶ。

check-project-artifacts ではそれぞれ分割したジョブから成果物をコピーし、そのまま自分のジョブ成果物として保存する。

check-project-artifacts:
  ./target/check_a-result
  ./target/check_b-result
  ./target/check_c-result

check-project-artifacts_a:
  ./target/check_a-result

check-project-artifacts_b:
  ./target/check_b-result

check-project-artifacts_c:
  ./target/check_c-result

一部のチェッカについて、実行時間が長いので並列化を検討する

check_b が長時間かかっているようなので、チェック対象となるプロジェクト成果物をある程度の単位(ここでは dirA,dirB,dicC)に切り分け、ジョブをさらに細かく分割する。

  • check-project-artifacts
    • check-project-artifacts_check_a
    • check-project-artifacts_check_b
      • check-project-artifacts_check_b_dirA (分割後)
      • check-project-artifacts_check_b_dirB (分割後)
      • check-project-artifacts_check_b_dirC (分割後)
    • check-project-artifacts_check_c

check-project-artifacts_check_bcheck-project-artifacts を分割した時と同様、分割したジョブが完了したら分割前の親ジョブを呼び、親ジョブで子ジョブの成果物を回収する。

ただし、分割された子ジョブの成果物はそのままでは分割されていて見づらいため、親ジョブで分割された成果物を結合する処理を実施する。

check-project-artifacts_check_b_dirA:
  ./target/dirA_check_b-result

check-project-artifacts_check_b_dirB:
  ./target/dirB_check_b-result

check-project-artifacts_check_b_dirC:
  ./target/dirC_check_b-result

check-project-artifacts_check_b:
  ./target/dirA_check_b-result (結合用)
  ./target/dirB_check_b-result (結合用)
  ./target/dirC_check_b-result (結合用)
  ./target/check_b-result (上記3ファイルを結合したもの)

(ほかのプロジェクトは省略)

で、何が言いたいの?

Jenkinsのジョブがこうやって成長と複雑化を遂げることを言いたかった。

また、上記のようなジョブの分割といった場合は前のエントリで紹介しているように、Job DSL Pluginを使うと簡単に好きなだけコントロール可能な方法で分割できます。

もし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はまったく詳しくないので、厳密にどうなっているのか知りませんが。)

WindowsでEmacsのClojure開発環境を立ち上げる

Windowsで最低限のEmacs+Clojureの開発環境を立ち上げる手順を書いてみました。

対象読者は、あやふやです。なんとなくEmacsClojureもやったことがある、ぐらいな感じ。

ちなみに全般的にWindowsでこの手のUnix世界に塗れたものを開発するのは地獄なので、LinuxOS Xという選択肢を選べる人は素直にそちらを選んだほうがいいでしょう。

まずはChocolateyを入れる

今や常識、Windowsのパッケージ管理ツール、Chocolatey(https://chocolatey.org/)を入れましょう。

そこまでこだわりのないパッケージであればChocolatey経由でインストールしたものでまかなえます。

基本的なツールを入れる

curlwget を入れておきましょう。

choco install curl wget

MsysGit(https://msysgit.github.io/)もChocolatey経由じゃなくてもいいですが、入れておきましょう。Chocolateyでは以下。

choco install git

Clojureの実行環境を作る

http://leiningen-win-installer.djpowell.net/ というのもあるみたいですが、使いません。

まずはJDK8をインストール。

choco install jdk8

次にLeiningen(Clojureのビルド兼実行ツール)をインストール。leiningenのインストール時、お呼びでないJDK7も入れようとするので -ignoreDependencies を指定しておきます。

choco install lein -ignoreDependencies

このままだと多分Leiningenが古いので、仕上げに下記コマンドを打っておきましょう。

lein upgrade
lein self-install

JDKもLeiningen自分で入れるんじゃーい!と思った方は、OracleからJDKダウンロードしてインストールして、Leiningenも公式サイト(http://leiningen.org/)からlein.batをダウンロードしてパスを通せばOKです。

最後に下のコマンドでREPLに入れればOKです。

lein repl

Emacsを入れる

EmacsはEmacs24以上を使います。色々Windows用にビルドはあるのですが、それぞれ長所短所があり、決定打というものがありません。

今回はemacs-w64(Windows x64用のEmacs25)を使いたいと思います。おそらく、日本語入力は辛いと思うので日本語は使わない想定で… (改行コードの扱いもあやしい気がするが…)

普通に解凍して、 bin フォルダにパスを通します。起動時は runemacs.exe を使うといいでしょう。

EmacsClojureを開発できる環境を作る(Caskを入れる)

Emacsといえばその拡張性の高さが有名ですが、拡張性が高いだけで最近までいい拡張の管理方法がありませんでした。

しかし今どきはCask(https://github.com/cask/cask)というツールがあり、これにパッケージ依存性を書いたファイルを食べさせると自動でEmacsにパッケージをインストールしてくれます。

基本的にCaskはUnix上じゃないとどうも怪しい動きをしているのですが、Windowsで使えないことはありません。インストールしてみましょう。

まず、Caskの動作のためにChocolateyでPython2を入れます。残念ながらPython3は不可です。

choco install python2

(Python3と同居させたい人には、pyenvというものがWindows以外ならありますが、 https://github.com/yyuu/pyenv/issues/62Windowsだとどうすればいいんでしょうね…)

次に公式サイトに載っているコマンド(http://cask.readthedocs.org/en/latest/guide/installation.html#manual-installation)でCaskをインストールします。

curl -fsSL https://raw.githubusercontent.com/cask/cask/master/go | python

上記コマンドで %userprofile%\.cask\bincask および cask.bat が用意されます。( %HOME% 変数を定義している人は %userprofile% のかわりに、そこを探しましょう) この bin フォルダにパスを通しておきましょう。

その状態で、 %userprofile%\.emacs.d フォルダ(なければ作成)上で下記コマンドを実行し、Caskの初期ファイルを作成します。

cask init

上記コマンドで出来た Cask ファイルに下記内容を追記しましょう。Emacsでcider(Clojureの開発用)とparedit(Lispのカッコ編集用)という拡張に依存するという内容です。

(depends-on "cider")
(depends-on "paredit")

そのまま同じフォルダでCaskに依存している拡張を .emacs.d フォルダ内にインストールさせます。

cask install

なんかこのコマンド、Windows上だと一瞬で終了しちゃいますが、非同期で結果がバカバカ吐かれるので、ちょっと待ちましょう。

(この際、Emacs24から使えるGnuTLSのサポートが無いEmacs(公式配布とか)を使っていると、Caskに文句言われます。)

上記インストールが終わったら、今度は %userprofile%\.emacs.d\init.el (Emacsが起動時に読むユーザーカスタマイズ用のファイル) に下記内容を追記します。

(add-to-list 'load-path "~/.cask")

(require 'cask)
(cask-initialize)

(show-paren-mode 1)

(require 'paredit)
(add-hook 'emacs-lisp-mode-hook #'enable-paredit-mode)

(require 'cider)
(add-hook 'clojure-mode-hook #'enable-paredit-mode)
(add-hook 'cider-repl-mode-hook #'enable-paredit-mode)

追記が終わったら、 runemacs.exe を実行してみましょう。エラーなく起動すればOKです。

試しに、Clojureのプロジェクトを作ってみる

簡単なClojureのプロジェクトを作成してREPLを開いてみましょう。

どっか作業用のフォルダ上で、下記コマンドを打ち、Clojure開発用の空のプロジェクト(my-test)を作成します。

lein new my-test

できた my-test フォルダ内の project.clj ファイルを開いて、 C-c M-jキーを押せばプロジェクトに対応したClojureのREPLを立ち上げることができます。

src フォルダの中にひな形のソースファイルが入っているので C-x C-e(カーソルの前の式を評価) などを使ってREPLに投げて遊んだりしましょう。

EmacsのCIDERとLeiningenのCIDERのバージョンが合わないエラー

EmacsのCaskで入れたEmacs側のCIDERのバージョンと、LeiningenがダウンロードしたJava側のCIDER(cider-nrepl)のJARのバージョンがあってねえよと文句言われることが多々あると思います。

CIDER's version (0.8.2-snapshot) does not match cider-nrepl's version (0.8.1)

ちゃんとLeiningenが持ってくるJava側のcider-nreplのバージョンをEmacsのCIDERにあわせてあげましょう。

LeiningenのユーザごとのプロファイルにCIDERのバージョンを指定してあげます。下記のいずれかのファイルを編集します。

  • %LEIN_HOME% を定義済みならば %LEIN_HOME%\profiles.clj
  • それ以外の場合は、 %userprofile%\.lein\profiles.clj ( 多分%HOME%は見てないかも )

ファイルに下記内容を書きます。

{:user
 {:plugins [[cider/cider-nrepl "0.8.2-SNAPSHOT"]]}} ; ←ここをEmacs側のバージョンにあうように変更しましょう!

ちょっと管理上よろしくないですが、上記のプラグインの設定を各プロジェクトの project.clj:plugins に追記してもいいです。

試しに Clojure+ClojureScript (+Facebook ReactのラッパーのOm) のプロジェクトを作ってみる

ちょっとだけ背伸びをしてみましょう。プロジェクトテンプレートとして、plexus/chestnut(https://github.com/plexus/chestnut)を使います。

どっか作業用のフォルダ上で、下記コマンドを打ち、ClojureScriptを開発する雛形のプロジェクト(my-test)を作成します。

lein new chestnut my-test

その中の project.clj ファイルをEmacsで開いて C-c M-j キーを押してプロジェクトに対応したClojureのREPLを立ち上げます。

REPLはデフォルトの名前空間( my-test.server )で起動していると思いますが、そこで (run) 関数を起動してWebサーバを起動します。

; CIDER 0.8.2snapshot (package: 20141130.803) (Java 1.8.0_25, Clojure 1.6.0, nREPL 0.2.6)
my-test.server> (run)
Starting figwheel.
Starting web server on port 10555 .
#<Server org.eclipse.jetty.server.Server@e7a73f8>
my-test.server> Compiling ClojureScript.
Figwheel: Starting server at http://localhost:3449
Figwheel: Serving files from '(dev-resources|resources)/public'
Compiling "resources/public/js/app.js" from ("src/cljs" "env/dev/cljs")...
WARNING: Use of undeclared Var cljs.core.async/do-alts at line 62 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async/impl/ioc_helpers.cljs
WARNING: Bad method signature in protocol implementation, impl/Handler does not declare method called lock-id at line 214 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async.cljs
WARNING: Use of undeclared Var cljs.core.async.impl.protocols/lock-id at line 217 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async.cljs
WARNING: Bad method signature in protocol implementation, impl/Handler does not declare method called lock-id at line 214 resources\public\js\out\cljs\core\async.cljs
WARNING: Use of undeclared Var cljs.core.async.impl.protocols/lock-id at line 217 resources\public\js\out\cljs\core\async.cljs
Successfully compiled "resources/public/js/app.js" in 13.856 seconds.
notifying browser that file changed:  /js/app.js
notifying browser that file changed:  /js/out/goog/deps.js
notifying browser that file changed:  /js/out/my_test/dev.js
notifying browser that file changed:  /js/out/my_test/core.js

上記出力が終わったら http://localhost:10555 を見ればClojureで立ち上がったWebサーバ上が見えると思います。この時のサーバのページの表示はClojureScriptで行われています。

今度はClojureScriptをいじってみましょう。REPL上で

(browser-repl)

でClojureScriptのREPLを起動してみましょう。これにより cljs.user 名前空間に切り替わるので、REPL上で

(js/alert "Hello!")

とか打ってみましょう。ちゃんとブラウザ上でalertが出たのが見えるでしょうか。

つづいて、Om(React)らしく、JS上のモデルを書き換えてページのDOMが書き換わるのを観察してみましょう。

src/cljs/my_test/core.cljs を開いて参考にしつつ、ファイルに下記式を書いて評価( C-x C-e)してみましょう。

(reset! app-state {:text "Goodbye Chestnut!"})

シームレスにブラウザ上のDOMがリレンダされたのがわかるでしょうか。

このまま Omのチュートリアル(https://github.com/swannodette/om/wiki/Basic-Tutorial)にすすんでみましょう。

公式チュートリアルはLightTable前提ですが、Emacsでもやっていけるはずです。

Jenkinsのビルドエグゼキュータ(ビルド状況)をREST APIで取得したい

個人的なメモ。

Jenkinsのビルドエグゼキュータは、たとえば https://ci.jenkins-ci.org/ajaxExecutors のように /ajaxExecutors 経由でJenkinsのウェブサイトに定期的に描画されているものですが、これはHTMLのスニペットを定期的に配信していることで実現していて、たとえばTwitter BootstrapのカッコイイUIでこのビルドエグゼキュータを描画したいといった場合、HTMLのスニペットの定期取得では難しいこともあるでしょう。

(以下はそのスニペット

<div id="executors" class="container-fluid pane-frame"><div class="row"><div class="col-xs-24 pane-header"><a title="collapse" class="collapse" href="/toggleCollapse?paneId=executors"><img style="width: 16px; height: 16px; " alt="collapse" class="icon-collapse icon-sm" src="/static/8e43c810/images/16x16/collapse.png" /></a><a href='/computer/'>ビルド実行状態</a></div></div><div class="row pane-content"><table class="pane "><colgroup><col width="30" /><col width="200*" />

<!-- 以下略 -->

ということで、Jenkinsに備え付けられているREST APIから、ビルドエグゼキュータの情報を取ろうと考えつくわけですが、どこを取ればいいかわからない。

まあ正解からいうと、 https://ci.jenkins-ci.org/ (公式Jenkins)を例にとったとして、 https://ci.jenkins-ci.org/computer/api/json?pretty=true を取ればいいんですが、エグゼキュータ描画に必要な情報が入っていない。( ?pretty=true は実装時には要らないです)

なので、より深い情報を取るために https://ci.jenkins-ci.org/computer/api/json?pretty=true&depth=2 などと depth=2 を指定すりゃあいいんですが、

  • 余計な情報つきすぎ
  • ノードをON/OFFしていると例外をBeanからシリアライズできないとかで500出る(バグ?)

といった苦難にあうので、きちんと取るべき情報を指定してあげましょう。

https://ci.jenkins-ci.org/api/ に書いてあるんですが、RESTで取る際のデータはある程度選べます。

今回のビルドエグゼキュータを自分で描画したいといった場合、JSON上の下記の構造の部分だけデータが取れれば、まあだいたい僕の欲望は満たせました。

  • computer (複数) スレーブごとに存在
    • displayName スレーブの名前
    • executors (複数) スレーブのエグゼキュータ(キュー)ごとに存在
      • progress ジョブの進捗%
      • idle 空いているか?
      • currentExecutable 実行中ビルドの詳細
        • estimatedDuration ジョブの完了見積もりミリ秒
        • fullDisplayName ビルドのフル名
        • url ビルドのURL

上記のデータだけを取る場合は tree パラメータを駆使して、 https://ci.jenkins-ci.org/computer/api/json?pretty=true&tree=computer[displayName,executors[progress,idle,currentExecutable[estimatedDuration,fullDisplayName,url]]] とすればよいです。前までつけていた depth は不要になります。

下記に(Lisp並みに)わかりやすく分解したURLを置いておきます。

https://ci.jenkins-ci.org/computer/api/json?pretty=true&tree=
    computer[
        displayName,
        executors[
            progress,
            idle,
            currentExecutable[
                estimatedDuration,
                fullDisplayName,
                url]]]

上記のURLで下記のようなJSONがとれるので、後は定期的に再取得しなおしたり、再取得するまでの間は estimatedDurationprogress からビルドの予定時間を算出してスムーズにプログレスバーでビルド進捗率を描画するなりすればいいと思います。

{
    "computer" : [
    {
      "displayName" : "master",
      "executors" : [
        {
          "currentExecutable" : {
            "estimatedDuration" : 8807897,
            "fullDisplayName" : "infra_update_center_v3 #57",
            "url" : "http://ci.jenkins-ci.org/job/infra_update_center_v3/57/"
          },
          "idle" : false,
          "progress" : 81
        },
        {
          "currentExecutable" : null,
          "idle" : true,
          "progress" : -1
        }
      ]
    },
    {
      "displayName" : "celery",
      "executors" : [
        {
          "currentExecutable" : null,
          "idle" : true,
          "progress" : -1
        },
        {
          "currentExecutable" : null,
          "idle" : true,
          "progress" : -1
        }
      ]
// 以下略

ちなみになんでこんなことを僕が調べているかというと、開発サーバ上で動くJenkins数台などを巻き込んだSPAをAngularJSとか使って作っているからです。