一覧に戻る
    Dockerエージェントを ECR イメージ化したら改善は出た。でも同じ日に3つのバグと1つの誤診断が出てきた話
    開発ラボ
    PRこの記事には広告が含まれています

    Dockerエージェントを ECR イメージ化したら改善は出た。でも同じ日に3つのバグと1つの誤診断が出てきた話

    18 分で読める

    前回の記事(CIで作ったDockerイメージが別のサーバーで動かなかった話)で、docker buildx を使ったマルチアーキテクチャ対応と、--provenance=false --sbom=false が必要だという話を書きました。

    今回はその後日談です。「ちゃんと動いているか確認しよう」と思って監視ログを眺めていたら、別の問題が3つ出てきました。そしてそのうち1つは、最初の診断が間違っていました。

    前回からの続き

    前回の記事で対応したのは、「arm64 でビルドした Docker イメージが x86_64 のノードで動かない」という問題でした。docker buildx でマルチアーキテクチャ対応し、buildx のデフォルト挙動(attestation メタデータの付与)が Jenkins と相性が悪いことを発見して、--provenance=false --sbom=false を追加することで解決しました。

    その対応に加えて、今回はもう一つの改善も進めていました。Jenkins の Docker エージェント指定を dockerfile から ECR イメージの直接参照に変えるという変更です。毎回 Dockerfile からビルドするのをやめて、事前にビルドしたイメージを直接 pull して使う方式です。

    ECR イメージ化の効果を数字で確認した

    エージェント指定の変更前後でメトリクスを比較してみました。

    メトリクス変更前変更後変化
    CPU 使用率ピーク31.8%16.1%約半減
    ネットワーク受信ベースライン−26%減少
    パケット受信ベースライン−85%大幅減少
    CPU クレジット使用量ベースライン−45%減少
    CPU クレジット残高ベースライン+98%約倍に回復

    特に「パケット受信 −85%」の数字が象徴的です。変更前は、ジョブが起動するたびに apt や npm の依存パッケージを丸ごとダウンロードしていました。それがまるごと消えた効果が数字に出ています。

    CPU クレジット残高が倍近くに回復しているのも見どころで、軽量インスタンスでバーストクレジットが枯渇するリスクが実質的になくなった、という裏付けです。

    「ちゃんと動いているか」を確認したら、動いていなかった

    喜んでいたのも束の間。動作確認のためにパイプライン全体を流したら、失敗しました。

    「さっきまで動いていたのに」という感じではなく、たまたまこのタイミングで問題が重なっていたようです。調べていくと、3つの独立したバグが出てきました。

    バグ1(最初の診断):Docker イメージに Claude Code CLI が入っていない?

    一番驚いたのがこれです。

    Claude Agent(Claude Code をバックエンドに使うエージェント)が、全フェーズで起動直後に exit code 1 で落ちていました。ログを見ると、起動から2秒以内に即死しています。エラーメッセージは何も出ない。

    「OAuth トークンが切れたか?」と思い、トークンを再発行して実行してみました。でも、同じ挙動。何度やっても再現するので「コード側の問題だ」と腹をくくって調べました。

    最初の診断はこうでした。

    使っている SDK(@anthropic-ai/claude-agent-sdk)は、内部で Claude Code 本体の CLI(cli.js)を spawn します。ところが package.json を見ると、SDK 本体は入っているのに CLI 本体(@anthropic-ai/claude-code)が依存関係に含まれていない。SDK が CLI を見つけられず、spawn 直後に即 exit 1 という動作になっているのでは、と。

    修正方針として、Dockerfile に以下を追加することを考えました。

    RUN npm install -g @anthropic-ai/claude-code@latest

    ただし、これは後で誤診断だったと判明します。そちらは後述します。

    バグ2:認証失敗の誤検知(コードがコード自身を誤認識する)

    こちらはもう少し込み入った問題です。

    Codex Agent(OpenAI Codex を使うエージェント)は、Claude Agent と違って正常に動作していました。ログを見ると type=turn.completed まで到達していて、成果ファイルの生成にも成功しています。

    なのに、直後に「Codex Agent authentication failed」と判定されてフォールバックが発動する。

    「どういうことだ?」と思ってコードを掘ってみると、認証失敗の検出ロジックがこうなっていました。

    // 認証失敗検出
    const authFailed = messages.some((line) => {
      const normalized = line.toLowerCase();
      return (
        normalized.includes('invalid bearer token') ||
        normalized.includes('authentication_error') ||
        normalized.includes('please run /login')
      );
    });

    messages は、エージェントが出力したすべての内容です。ここが問題でした。

    test_implementation フェーズは、テストファイルの読み書きが主な仕事です。Codex Agent がテストファイルを catgrep で参照したとき、ファイルの内容が messages に乗ります

    そして、このリポジトリのソースコード自体に authentication_error というリテラルが含まれています。認証失敗検出ロジックの実装コードや、そのテストコード、トラブルシューティングドキュメントに。

    Codex がテストファイルを読んだ瞬間に authentication_error という文字列が messages に流れ込み、正常動作しているのに「認証失敗」と誤判定される。

    なんというか、コードがコード自身のキーワードを読んで誤判定する、というフィードバックループです。test_implementation フェーズでだけ再現するのは、このフェーズだけが問題のファイルを参照するからでした。

    バグ3:認証検証ジョブが JSON パースに失敗していた

    これは一番地味ですが、デバッグの邪魔をし続けていた問題です。

    「Claude Agent が動かない」を調べようとして、認証情報を検証するジョブを実行しました。ところがこのジョブも失敗していました。エラーは Invalid JSON String

    net.sf.json.JSONException: Invalid JSON String

    CLI が出力したはずの JSON を readJSON ステップで読もうとして失敗している。

    おそらく原因は、CLI が stdout にログを混入させていることです。--output json を指定した場合、JSON は stdout に吐き、ログは stderr に吐く、というのが期待する動作のはずです。でも、ロガーの設定によっては [INFO] ... のようなログ行が stdout に混ざってしまいます。JSON の手前にログ行が入ればパースは失敗します。

    このバグが「認証情報の検証ができない」という状態を作っていたため、「Claude Agent の認証が原因なのか、別の何かが原因なのか」を検証する手段が一つ潰れていた、という皮肉な状況でした。

    ここで立ち止まって、差分を調べた

    3つのバグを整理しながら、ふと気になったことがありました。

    「main ブランチの all-phases は Dockerfile を直接使う実装で、こちらは Claude Code が正常に使えていた。差分はどこにあるのか?」

    改めてブランチの差分を調べてみると、Dockerfile の変更点は2つだけでした。

    • AWS CLI のマルチアーキ対応(arm64 / x86_64 の分岐追加)
    • ENTRYPOINT [] の追加(Jenkins withDockerContainer 互換のため)

    @anthropic-ai/claude-code の有無は変わっていない。package.json も無変更。

    ではなぜ、main ブランチでは動いていたのに、このブランチでは動かないのか。

    さらに調べると、Jenkinsfile の差分に目が向きました。

     agent {
    -    dockerfile {
    +    docker {
    +        image 'your-registry/ai-workflow-agent:latest'
             label 'ec2-fleet-small'
    -        dir '.'
    -        filename 'Dockerfile'
    -        args "-v ${WORKSPACE}:/workspace -w /workspace -e CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1"
    +        args "-u root:root -e CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1"
         }
     }

    -u root:root が追加されていました。

    誤診断の訂正:真犯人は -u root:root だった

    Claude Code CLI は、root ユーザーで実行したときに特殊な挙動をすることで知られています。

    Dockerfile の中で node ユーザーに sudo 権限を与えており、本来の想定は「node ユーザーで動かし、必要であれば sudo を使う」設計です。-u root:root で root 直接実行するのは想定外のパスでした。

    -u root:root を外して再実行しました。

    -args "-u root:root -e CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1"
    +args "-e CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1"

    develop から新しいブランチを切り、この1行だけを修正してジョブを実行。

    結果は明確でした。

    [INFO ] [AGENT RESULT] status=success, turns=1, duration_ms=13040
    [INFO ] Difficulty analysis complete: level=moderate, confidence=0.85

    起動直後に即死していた箇所が、13秒で正常完了。-u root:root が Claude Code CLI の起動失敗の根本原因で確定でした。

    バグ1の「Dockerfile に @anthropic-ai/claude-code を追加する」という修正方針は、不要でした。

    ジョブの完走を確認した

    -u root:root を外したジョブをウォッチし続けたところ、SUCCESS で完走しました。

    同時に、バグ2(認証失敗の誤検知)も予想通り再現しました。Codex が agent-executor.test.ts を読み込んだ直後に authentication_error リテラルを検知して、turn.completed の直後に fallback が発動しています。ただし、fallback 先の Claude Agent が正常動作するようになっていたため、ジョブ自体は最終的に完走できました。

    問題の整理と残課題

    今日の作業を整理するとこうなります。

    ✅ 確認できたこと
      ECRイメージ化でCPU/ネットワーク負荷が大幅に改善(パケット数−85%)
    
    ❌ → ✅ 解決した問題
      Claude Agent が全フェーズで exit code 1(即死)
      → 原因: Jenkinsfile に -u root:root を追加していたため
      → 修正: -u root:root を削除(1行の変更)
    
    ⚠️ 別 issue で対応
      バグ2: 認証失敗検出ロジックの誤検知
        → ソースコード内のキーワードが messages に流入して false positive
        → 実害は「Codex の成果を捨てて Claude でやり直す倍コスト」
        → ワークフローは完走するが、コスト効率が悪い
    
      バグ3: 認証検証ジョブの JSON パース失敗
        → stdout へのログ混入が原因(別途対応)
    
      その他: setupNodeEnvironment() の npm install + npm run build が ECR イメージ化後に冗長
        → 毎ジョブで依存インストールが走っており、ECR イメージ化の性能改善効果を相殺している

    最後に issue の整理もしました。

    • Issue #830(最初の診断で起票):原因が「CLI 未インストール」ではなく「root 実行」だったため、クローズ
    • Issue #832(新規):認証失敗誤検知ロジックの厳密化
    • Issue #833(新規):setupNodeEnvironment() の冗長処理の解消

    雑感

    「動作確認のつもりで回したら、別の問題が3つ出てきた」というのはよくある展開なのですが、今回はそれに加えて「最初の診断が間違っていた」という経験も入ってきました。

    バグ1の診断で「CLI が入っていないのでは」と見立てて調べ始めたとき、それなりに筋が通っていると思っていました。でも、main との差分を丁寧に追いかけていくと、Dockerfile の問題ではなく Jenkinsfile の -u root:root だった。

    「exit 1 だけど何も出ない」というエラーは、候補が多くて絞りにくいです。今回の教訓としては、「環境自体が正しいか」を確認する前に「main との差分は何か」を先に見ておけば、もう少し早く真因に辿り着けていたと思います。

    バグ2(誤検知)は構造として面白かったです。コードがコード自身のキーワードを読んで誤判定する、というのは、単純なキーワードマッチの脆さが出た事例です。エラー検出ロジックは「何を検査対象にするか」を設計レベルで決めておく必要があることを改めて実感しました。

    参考

    Jenkins のパイプライン設計や Docker エージェントまわりの挙動、そして Claude Code の使い方について体系的に学びたい方には、以下の書籍が参考になります。

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

    Coffee cup

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

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

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

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

    対話の時間について