はじめに
前回の記事では、SonarQube環境をDockerで安定稼働させるまでを解説しました。
今回は、その環境を使って400以上のJavaプロジェクトを解析するというミッションに挑みます。全プロジェクトの解析には数日を要するため、途中でサーバーが再起動したり、ジョブが止まったりしても、「最初からやり直さずに続きから再開する」仕組みが必須でした。
Jenkins Pipelineが持つ再起動にも負けない仕組みと、チェックリスト方式によるレジューム術を紹介します。
1. シェルスクリプトによるループの限界
当初は単純なシェルスクリプトでループを回していましたが、以下の課題に直面しました。
- 進捗が見えない: 今何件目なのか、あと何時間かかるのかが不透明。
- 中断に弱い: プロセスが死ぬと、どこまで進捗したかを調べる手間が発生。
- エラー管理: 1件失敗した時に、ループ全体を止めるのか飛ばすのかの制御が複雑。
- リストの保守: プロジェクトが増減するたびに、対象リスト(テキストファイル)を手動で更新するのが苦行。
これらを解決するために、Jenkins Pipeline (Groovy) への移行を決断しました。
2. 環境のシャットダウンにも耐える Jenkins
Jenkinsでジョブを回している最中、サーバーの再起動が発生しました。平日のみ11:00-20:00で環境を稼働させていたため、週末を跨いで月曜にJenkinsの画面を開くとこんな表示がありました。
Build Duration: 2 days 20 hr
なんと、ジョブは死ぬことなく、サーバー復活と共に中断したその行から自動で再開していたのです。
なぜ再開できたのか?
Jenkins Pipeline(特に Jenkinsfile 形式)には、Durable Pipeline(耐久性パイプライン) という強力な仕組みが備わっているからです。
- CPS (Continuation Passing Style) 変換: Jenkinsはスクリプトを1行実行するたびに、「今どの変数がどんな値で、どこまで進んだか」の状態をディスクにシリアル化して保存しています。
- 冬眠と目覚め: サーバーが落ちるとジョブは「一時停止」状態になり、再起動時にディスクから状態を読み戻して、中断したステップから動き出します。
詳細はjenkinsのページに記載がありました。和訳が以下です。
コマンドラインから実行される通常の Groovy プログラムとは異なり、パイプラインビルドのプログラムの完全な状態は、非同期操作が実行されるたびにディスクに保存されます。これには、ほとんどのパイプラインステップが含まれます。ビルドの実行中に Jenkins を再起動しても、中断した箇所からプログラムの実行が再開されます。これは効率性を意図したものではないため、Jenkins の機能に直接関連する高レベルの「グルー」コードに限定すべきです。プロジェクト独自のビルドロジックは、ビルドノード上の外部プログラムから、sh または bat ステップで実行する必要があります。
3. 「動的リスト取得」と「二重チェックリスト」によるレジューム術
プラットフォーム側の耐久性に加え、アプリケーション層でも強力なレジューム機能を実装しました。
実装のポイント
-
動的なプロジェクトスキャン:
findコマンドでディレクトリを直接スキャンし、targetや@tmpを除外した「最新のリスト」を都度生成。 -
完全一致での判定:
String.contains()ではなく、リスト化した上でのcontains()を使い、部分一致による誤スキップを防止。 - 失敗も「処理済み」として扱う: 成功リスト(status)と失敗リスト(failed)を分け、どちらかに記載があればスキップすることで、壊れたプロジェクトに捕まってループが止まるのを防ぎます。
実装コード(抜粋)
stage('Loop Analysis') {
steps {
script {
def allProjectsFile = "resources/all_projectsList.txt"
def statusFile = "resources/status_${env.TARGET_TAG}.txt"
def failedFile = "resources/failed_${env.TARGET_TAG}.txt"
sh "touch ${statusFile} ${failedFile}"
// 1. ディレクトリから動的にプロジェクト一覧を取得
def findCmd = """
find ${env.PRJ_ROOT}/app -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | \
grep -vE '^(EXCLUDED1|EXCLUDED2)\$|@tmp\$'
""".stripIndent()
def allProjects = sh(script: findCmd, returnStdout: true).trim().split('\n').collect { it.trim() }
writeFile file: allProjectsFile, text: allProjects.join('\n') // 最新リストを保存
for (project in allProjects) {
// 2. 成功・失敗リストを読み込み、完全一致でチェック
def finishedList = readFile(statusFile).split('\n').collect { it.trim() }
def failedList = readFile(failedFile).split('\n').collect { it.trim() }
if (finishedList.contains(project) || failedList.contains(project)) {
echo "SKIP: ${project} (処理済み)"
continue
}
// 3. 解析実行
try {
dir("app/${project}") {
sh "mvn sonar:sonar ..."
}
sh "echo '${project}' >> ${statusFile}" // 成功記録
} catch (Exception e) {
sh "echo '${project}' >> ${failedFile}" // 失敗記録
echo "ERROR: ${project} を失敗リストに記録しました。"
}
}
// 4. 全件(成功数 + 失敗数)が完了したら後続ジョブへ
def processedCount = readFile(statusFile).split('\n').size() + readFile(failedFile).split('\n').size()
if (processedCount >= allProjects.size()) {
build job: 'Report_Generator', ...
}
}
}
}
4. この構成にして良かったこと
-
「放置できる」という信頼感: 400件中1件がMavenエラーになっても、それを
failed.txtに放り込んで次の解析に進んでくれるため、朝起きたらジョブが止まっているという絶望がなくなりました。 - メンテナンスフリー: プロジェクト用ディレクトリを増減させるだけで、リスト更新から解析、集計までが自動で追従します。
- 正確なスキップ判定: リスト形式での判定に切り替えたことで、名称が似たプロジェクトの未解析問題を完全に解消しました。
まとめ
「解析に数日かかる」という物理的な制約を、Jenkinsの耐久性とチェックリストロジックの合わせ技で克服しました。
これまでSSH接続をして手動でシェルを蹴っていた手間が完全に無くなり、CI/CDツールとしてのJenkinsの底力を改めて実感しています。