一覧に戻る
    Jenkinsfile 3連修正の記録:プラグイン依存の罠・エージェントサイズ・ビルドステータス設計
    開発ラボ
    PRこの記事には広告が含まれています

    Jenkinsfile 3連修正の記録:プラグイン依存の罠・エージェントサイズ・ビルドステータス設計

    15 分で読める

    CI/CD の改善作業をしていたある日、Jenkinsfile のちょっとした修正が 3 つ重なりました。

    個別に見ると「あ、そういうことか」で終わる話ですが、並べてみると共通のテーマが浮かんできます。

    「Jenkins は暗黙の前提が多い」

    ということで、今日はその 3 連修正の記録を残しておきます。

    背景

    Seed Job Orchestrator の並列化(CI 実行時間の短縮を目的とした PR)のレビュー・修正作業をしていたところ、Jenkinsfile がエラーで落ちました。そこから芋づる式に、別のジョブの問題も 2 つ見つかったので、合わせて直した形です。

    Fix 1:lock ステップが使えない環境での対処

    何が起きたか

    Jenkinsfile の中で、Seed Job の排他実行を保証するために lock ステップを使っていました。

    lock(resource: "seed-job-${jobName}") {
        def result = build(
            job: "Seed_Jobs/${jobName}",
            wait: true,
            propagate: true
        )
    }

    これを実行したところ、こんなエラーが。

    java.lang.NoSuchMethodError: No such DSL method 'lock' found among steps [...]

    エラーの中に lock が利用可能ステップ一覧にないことが書かれていました。

    lock ステップは Lockable Resources Plugin が必要で、この環境には未インストールだったということです。

    選択肢の整理

    対応案は 3 つ考えました。

    1. lock を削除 + disableConcurrentBuilds() を追加(最小修正)
    2. throttle に置換(環境にある Throttle Concurrent Builds Plugin を使う。ただしリソース定義が事前に必要)
    3. Lockable Resources Plugin をインストール(現行コードのまま動かす。Jenkins 管理権限が必要)

    正直、最初は「プラグインを入れれば済む話では?」と思いました。

    ただ、管理権限の問題もあり、今の環境で動くものを作る方が現実的でした。

    そもそも「Seed Job Orchestrator を同時に複数起動する運用はしない」という前提があるので、disableConcurrentBuilds() で二重起動を防止すれば、lock が担っていた役割はほぼ代替できます。

    pipeline {
        agent {
            label 'built-in'
        }
    
        options {
            disableConcurrentBuilds()  // ← これを追加
        }
    
        // ...
    }

    合わせて、lock ラッパーを 2 箇所(直列実行ステージ・並列実行ステージ)から削除しました。

    // 変更前
    lock(resource: "seed-job-${jobName}") {
        def result = build(
            job: "Seed_Jobs/${jobName}",
            wait: true,
            propagate: true
        )
        echo "✓ ${jobName} completed: build #${result.number} (${result.result})"
    }
    
    // 変更後(lock ラッパーを削除)
    def result = build(
        job: "Seed_Jobs/${jobName}",
        wait: true,
        propagate: true
    )
    echo "✓ ${jobName} completed: build #${result.number} (${result.result})"

    学んだこと

    lock ステップは Lockable Resources Plugin の機能です。

    プラグインが「あって当然」と思って書いたコードが、別の環境では動かないことがある。これは Jenkins あるあるですが、今回あらためて実感しました。

    disableConcurrentBuilds() は組み込みオプションなのでプラグイン不要。

    「同一パイプラインの二重起動を防ぐ」という目的なら、こちらの方がシンプルです。

    用途によって使い分けが必要です。今回は明らかに「パイプライン自体の二重起動防止」だったので、disableConcurrentBuilds() で十分でした。

    Fix 2:エージェントラベルの変更(nano → small)

    前提:エージェントラベルをインスタンスサイズごとに分けている

    まずこの修正が「1 行変更で済んだ」理由を書いておきます。

    エージェントのラベルをインスタンスサイズごとに分けて定義しているという設計があったからです。

    ec2-fleet-t-nano   → t系 nanoサイズのエージェントプール
    ec2-fleet-t-small  → t系 smallサイズのエージェントプール
    ec2-fleet-t-medium → t系 mediumサイズのエージェントプール

    この設計があるから、Jenkinsfile 側でラベルを変えるだけでインスタンスサイズを切り替えられます。

    もしラベルがサイズごとに分かれていない環境では、同じアプローチは使えません。その場合はエージェント設定自体を変更するか、別の方法を取る必要があります。

    何が起きたか

    別のジョブ(GitHub Webhooks の設定を行うパイプライン)が ec2-fleet-t-nano エージェントでうまく動かなくなっていました。

    // 変更前
    agent {
        label 'ec2-fleet-t-nano'
    }
    
    // 変更後
    agent {
        label 'ec2-fleet-t-small'
    }

    考えたこと

    EC2 Fleet でエージェントを動的に起動するとき、インスタンスサイズの選択は意外と後回しにされがちです。

    「とりあえず一番安い nano で」という判断は最初の段階では合理的ですが、ジョブが増えて処理量が増えてくると、じわじわ問題が出てきます。

    今回のケースでは、ジョブが複数の GitHub リポジトリに Webhook を設定する処理をしており、実行時間も長め。nano ではリソース不足になっていた可能性があります。

    「ラベルをサイズ別に分けて設計しておく」ことで、こういうときの対処がシンプルになります。

    最初からこの設計にしていたのは正解でした。ただ、それを知らずにラベル名だけ真似ても動かないので、前提として書き残しておきます。

    Fix 3:SUCCESS と UNSTABLE の使い分け

    何が起きたか

    複数のリポジトリに GitHub Webhook をバッチ設定するジョブを確認していたところ、ログにこんな出力がありました。

    Total Repositories: 226
    Already Configured: 212
    Success: 0
    Failed: 0
    Timeout: 2
    Error: 12
    Finished: SUCCESS

    226 件中 14 件(タイムアウト 2 件・エラー 12 件)が失敗しているのに、ビルドは SUCCESS で完了していました。

    これは問題です。CI の結果を見た人は「成功した」と判断してしまいます。

    実際には一部のリポジトリに Webhook が設定できていない状態のまま、気づかれない可能性がある。

    どう修正したか

    失敗・タイムアウト・エラーがある場合に、ビルドを UNSTABLE に変更する処理を追加しました。

    if (summaryJson.failed > 0 || summaryJson.timeout > 0 || summaryJson.error > 0) {
        // 問題があったリポジトリの一覧をログに出力
        // ...
    
        // Mark build as UNSTABLE when any repository failed/timed out/errored
        echo "⚠️  ${summaryJson.failed} failed, ${summaryJson.timeout} timeout, ${summaryJson.error} error — marking build as UNSTABLE"
        currentBuild.result = 'UNSTABLE'
    }

    合わせて、post ブロックにも unstable の処理を追加しました。

    post {
        success {
            echo '✅ Batch webhook configuration completed successfully!'
        }
        unstable {
            echo '⚠️  Batch webhook configuration completed with failures/timeouts/errors. See summary above.'
        }
        failure {
            echo '❌ Failed to execute batch webhook configuration'
        }
    }

    これで unstable 時には success のメッセージが出なくなります。

    Jenkins のビルドステータスの使い分け

    Jenkins のビルドステータスは 3 種類あります。

    • SUCCESS:全て正常に完了した
    • UNSTABLE:ビルド自体は完了したが、一部に問題がある(テスト失敗、部分的な処理失敗など)
    • FAILURE:ビルド自体が失敗した

    今回のケースは明らかに UNSTABLE が適切でした。「処理は走り切ったけど、一部がダメだった」という状態を表現するためのステータスです。

    currentBuild.result = 'UNSTABLE' で明示的にセットできます。

    グリーンとレッドの間にある「黄色」を使いこなすと、CI の信頼性が上がります。

    今日の記録まとめ

    修正問題対処
    Seed Job Orchestratorlock ステップが使えない(Lockable Resources Plugin 未インストール)disableConcurrentBuilds() に置き換え
    GitHub Webhooks Settingエージェント nano でのリソース不足small に変更
    Batch Webhook Setting部分失敗なのに SUCCESS になる失敗時は UNSTABLE に変更

    個別に見ると小さな修正ばかりですが、共通して言えるのは「暗黙の前提が裏切られる」というパターンです。

    • プラグインが入っている前提で書いたコード
    • エージェントが処理に耐えられる前提(+ラベル設計があるから 1 行で直せた、という文脈も含む)
    • ビルドが成功なら全部うまくいっている前提

    どれも「まあ大丈夫だろう」という判断が積み重なった結果、じわじわ問題になる類のものです。

    特に 3 つ目のビルドステータスの話は、CI を「監視ツール」として使うなら重要だと思っています。

    UNSTABLE を使いこなすことで、「完全に壊れているわけじゃないけど、見ておくべき問題がある」という状態を正確に表現できます。

    引き続き検証を続けます。

    参考

    Jenkins のパイプライン記法やプラグインを体系的に学びたい方には、以下の書籍が参考になります。

    エージェントサイズや CI 実行基盤まわりの土台を押さえたい方には、こちらもおすすめです。

    この記事は役に立ちましたか?

    Coffee cup

    この記事が、何かの整理につながったら

    コーヒー1杯分の応援をもらえると嬉しいです。

    ※ これは応援とは別の話ですが、

    同じようなテーマを自分の文脈で整理したい場合は、 (文章だけだと詰まりやすい人向けに) 思考整理の壁打ちという形で対話の時間も取っています。

    対話の時間について