TL;DR
Jenkinsで最新ビルドが欲しいのに古いジョブがキューイングされてしまっていてビルドできない!なんてことはありませんか?
実は最新ビルド以外を自動でキャンセルさせるいい方法があるんです!
それはJenkinsで最新のビルドだけ実行したい場合はdisableConcurrentBuilds(abortPrevious: true)
を使うことです!
出典 : https://www.jenkins.io/doc/book/pipeline/syntax/#:~:text=options%20%7B%20checkoutToSubdirectory(%27foo%27)%20%7D-,disableConcurrentBuilds,-Disallow%20concurrent%20executions
・・・なんて思っていた時期もありました。
これには欠点があり、実行してみればわかるのですが${JOB_NAME}@N
という末尾に数字の付いたworkspaceが新規で作られてしまうのです!
つまりはジョブがほぼ同時に多数実行されてしまうと無制限(とは流石にいかないでしょうが)にディレクトリが増えていくことに・・・!
しかもそのジョブがキャンセルされなければcheckoutされてしまうディレクトリも増えるということで・・・つまり無制限にストレージ容量も食ってしまう!!!
ということで試行錯誤した結果milestone
とlock
とstages
のネストを駆使すればなんとかいけるというところまで漕ぎつけたのでまとめてみます。
未調査項目
JenkinsPipelineを使わない方法については未調査です。
また、master-slave構成ではない場合についても未調査です。
悪しからず。
要素説明
調べたり試したりした順番で記載します。
milestone
milestoneコマンドを呼び出すと、それ以前に同じ数字で呼び出した前のJobをキャンセルしてくれるらしい。
と思って試してみたものの、うまくいかず・・・。例にもれず癖のある挙動をするようです。
どうも後続のJobがより大きな数字で呼び出した=処理を追い越したと判断する模様?
おとなしくstackoverflowに載っていた↓のコードを使いましょう。
def buildNumber = env.BUILD_NUMBER as int
if (buildNumber > 1) milestone(buildNumber - 1)
milestone(buildNumber)
出典 : https://stackoverflow.com/a/55818301
agent none
milestoneでキャンセルされたよやったね!と喜んだのもつかの間、また@N
ディレクトリが使用されてしまうことが発覚。
pipelineの開始時点で既にworkspaceは決定されてしまった後であるらしく、abortPrevious
と同じ問題を抱えることに。
じゃあworkspaceが決定される前にmilestone処理を実行すればいいのでは?ということで、stage内でagentを指定することに。
pipeline {
agent none
stages {
stage('milestone') {
agent { label 'master' } // 管理してるだけのmasterマシン
steps {
script {
def buildNumber = env.BUILD_NUMBER as int
if (buildNumber > 1) milestone(buildNumber - 1)
milestone(buildNumber)
}
}
}
stage('build') {
agent { label 'slave' } // 実際にビルドが走るslaveマシン
steps {
...
}
}
}
agent none
についてはこちら : https://www.jenkins.io/doc/book/pipeline/syntax/#agent:~:text=example%3A%20agent%20any-,none,-When%20applied%20at
が、失敗。変わらず@N
が付いてしまう結果に…。
TIPS
ちなみにpostがある場合、以下のように書かないとエラーになります。post { success { node(label: 'slave') { ... } } failure { node(label: 'slave') { ... } } }
まとめて管理するためにenvironmentで管理すべきかもしれませんね。
以下のように書くことが可能です。environment { BUILD_MACHINE = "slave" } ... stage('build') { agent { label env.BUILD_MACHINE } // 実際にビルドが走るslaveマシン steps { ... } } ... post { success { node(label: env.BUILD_MACHINE) { ... } } failure { node(label: env.BUILD_MACHINE) { ... } } }
environment内で
sh
を呼び出している場合などにおいても同じような問題があります。
こちらについてはどうしようもなさそうなのでgroovyの構文で解決できないか検討すべきでしょう。
未確認ですが、agent
を指定済みのstage内にenvironment
を置くことで回避できるかもしれません。
lock
問題はmilestoneを呼び出してキャンセルしてからstage(もしくはagentかsteps)に到達するまでに前のジョブが終わっていないことだと推測。
(本当にここについては私の推測にすぎません。根拠はないです。)
何かJob同士の協調ができるようなものはないかと探した結果・・・ありましたlock
が!
ちなみにJenkinsのメニュー内にある
Lockable Resources
でリソースを管理するのかと思いきや、
If the resource does not exist in Global Settings it will be automatically created on build execution. Either a resource or a label need to be specified.
ということらしく、${JOB_BASE_NAME}
あたりを突っ込んでおけば良さそうでした。
ただ、githubを見ても使えそうで使えないという感じなんですよね・・・。
なんせ複数のstageに跨ってlockをかけられない・・・!
色々なところに挿入してみたものの構文エラーが多発し断念・・・。
stagesのネスト
方向性は合っているはずとあきらめきれずググってみたところ↓のようなブログを発見。
https://www.prkz.de/blog/20-jenkins-using-lockable-resource-across-multiple-stages
stages
は入れ子にできるのか・・・!!!
ということで↓のような形に。
pipeline {
agent none
stages {
stage('milestone') {
agent { label 'master' }
steps {
script {
def buildNumber = env.BUILD_NUMBER as int
if (buildNumber > 1) milestone(buildNumber - 1)
milestone(buildNumber)
}
}
}
stage ('lock') {
options { lock("${JOB_BASE_NAME}") }
stages {
stage ('build') {
agent { label 'slave' }
steps {
...
}
}
}
}
}
}
ちなみにoptions
と同じレイヤにagent
を指定すると@N
が付くことがあったためstagesの中に入れています。
これで@N
が付かなくなった!workspaceは1つでよくなったぞ!目的達成!
・・・の前にもう1課題。
stagesの入れ子2
やりたいことは達成できたものの、stage
すべてにagent
を指定しないといけない状態でした。
と、ここで思い出します。stages
は入れ子にできた・・・ということは?
pipeline {
agent none
stages {
stage('milestone') {
agent { label 'master' }
steps {
script {
def buildNumber = env.BUILD_NUMBER as int
if (buildNumber > 1) milestone(buildNumber - 1)
milestone(buildNumber)
}
}
}
stage ('lock') {
options { lock("${JOB_BASE_NAME}") }
stages {
stage ('agent') {
agent { label 'slave' }
stages
{ // インデント調整用
stage('build-step1') {
steps {
...
}
}
stage('build-step2') {
steps {
...
}
}
}}}}
}
}
これは酷い。
酷いんだけど必要要件はこれで揃いました。
ここからもう少し楽にできないか見てみたのですがうまくいかず・・・。
私はこの辺りで妥協しました。
求)更にリファクタしたコード
完成
というわけで(妥協の結果とはいえ)古いジョブはキャンセルしつつ、最新のジョブのみを実行することができました。
が、まだ問題点も残っているようで・・・(Jenkins側の問題ではないかと疑っている)
TIPS
post
はstages
に対応する
つまり、以下のように書くことでlockを外す前にpost処理を行うことができます。
pipeline {
agent none
stages {
stage('milestone') { ... }
stage ('lock') {
options { lock("${JOB_BASE_NAME}") }
stages {
stage ('agent') {
agent { label 'slave' }
stages { // インデント調整
...
}
post
{
success { ... }
failure { ... }
}
}}}}
}
}
問題点
問題点1 : キャンセルされないジョブがある
タイミングによっては古いジョブがキャンセルしきれず終わらないことがありました。
その場合は単純に待つか急ぎであればコンソールログ上に表示されるClick here to forcibly terminate running steps
というリンクを踏めば良さそうです。
問題点2 : @N
が付く状態が固定化されることがある
今回の作業中試行錯誤をしたせいなのか、短い時間で連打したせいなのか@N
が固定化されてしまい、単独でトリガーしても@N
が付いたままになってしまうことがありました。
こちらについては以下のページにある通り再起動すると直ったのでJenkinsのバグだと思います。
https://stackoverflow.com/a/71828212
最後に
いかがでしたでしょうか?
最新ビルドだけを実行することができればビルドマシンを効率よく利用できますね!
かなり若干見づらいコードとなってしまったのは残念ですが、もう少し効率のいい書き方を発見できたらこの記事をアップデートしようと思います!
ではまた別の記事でお会いしましょう!