一覧に戻る
    CIで作ったDockerイメージが別のサーバーで動かなかった話:落とし穴はビルドツールのデフォルト設定にあった
    開発ラボ
    PRこの記事には広告が含まれています

    CIで作ったDockerイメージが別のサーバーで動かなかった話:落とし穴はビルドツールのデフォルト設定にあった

    11 分で読める

    何が起きたか

    CI のジョブが、こんなエラーで落ちていました。

    does not seem to be running inside a container
    $ docker run -t -d ... your-registry/your-image:latest cat
    ERROR: The container started but didn't run the expected command.

    「コンテナが起動したけど、期待したコマンドが実行されなかった」というエラーです。

    ENTRYPOINT の設定ミスかな、とか、ユーザー権限の問題かな、と最初は思いました。でも原因は全然別のところにありました。

    原因:CPUアーキテクチャの不一致

    Docker イメージは CPU のアーキテクチャに依存します。大きく分けると「arm64(Apple Silicon や AWS Graviton など)」と「x86_64(Intel/AMD の一般的なサーバー)」があり、片方向けにビルドされたイメージは、もう片方では動きません

    Windows のソフトが Mac で動かないのと、感覚的には近いです。

    今回の構成はこうなっていました。

    • ビルドジョブ: arm64 のノードで docker build → push → arm64 専用イメージができる
    • 検証ジョブ: 同じ arm64 ノードで動く → 当然 OK
    • 本番ジョブ: x86_64 のノードで動く → arm64 イメージを pull してきても実行できない → 即死

    「検証は通るのに本番が落ちる」という構造になっていたわけです。ログのエラーも起動失敗を直接示してくれないので、最初は何が起きているのかわかりませんでした。

    解決の方針:1つのイメージで両方に対応する

    対処方法はいくつか考えられました。

    1. 本番ジョブのサーバーを arm64 に変える(最小変更)
    2. arm64 用と x86_64 用の 2 つのイメージを別タグで管理する
    3. 1 つのイメージで両方のアーキテクチャに対応する(マルチアーキ対応)

    1 は手っ取り早いですが、「arm64 のサーバーにしか使えないイメージ」という制約が残ります。2 はタグ管理が煩雑になる。

    3 を選びました。docker buildx というツールを使うと、1 つのイメージタグに「arm64 向け」と「x86_64 向け」の両方を詰め込めます。サーバーが自分のアーキテクチャに合う方を自動で選んで pull してくれるので、利用側の Jenkinsfile は一切変更不要です。

    buildx + QEMU の仕組み

    「arm64 のサーバー上で、どうやって x86_64 向けのイメージをビルドするのか」という問題があります。ここで使うのが QEMU です。

    QEMU は CPU エミュレーターで、「あるアーキテクチャの命令を、別のアーキテクチャ上で翻訳しながら実行する」ソフトです。arm64 のサーバー上で x86_64 向けのビルドが走るとき、裏で QEMU が命令を 1 つずつ翻訳してくれます。

    # これでarm64ノード上でもx86_64のバイナリが実行できるようになる
    docker run --privileged --rm tonistiigi/binfmt:qemu-v8.1.5 --install all

    デメリットはビルドが遅くなることです。ネイティブ実行の 3〜10 倍かかります。今回は 7 分前後だったビルドが 22 分になりました。「遅い」けど「動く」を選んだわけです。

    ビルドコマンド自体はこうなります。

    docker buildx build \
        --platform linux/amd64,linux/arm64 \
        --push \
        -t your-registry/your-image:latest \
        .

    --platform で両方を指定して、--push でそのままレジストリに送ります。これで ECR に「arm64 用」と「x86_64 用」が 1 つのタグの下にまとまった状態で push されます。

    「直った!」と思ったら、また動かなかった

    ここが今回のいちばんの落とし穴でした。

    ECR への push は成功。マニフェストを確認すると、arm64 と x86_64 のエントリが両方ある。でも本番ジョブは相変わらず同じエラーで落ちる

    「なぜ?」と調べてみると、ECR に上がっていたマニフェストが想定と違いました。

    {
      "manifests": [
        { "platform": { "architecture": "amd64", "os": "linux" } },
        { "platform": { "architecture": "arm64", "os": "linux" } },
        { "platform": { "architecture": "unknown", "os": "unknown" } },
        { "platform": { "architecture": "unknown", "os": "unknown" } }
      ]
    }

    architecture: unknown の正体は、buildx がデフォルトで自動付与するメタデータ(SLSA provenance attestation)です。「このイメージがどこでどうビルドされたか」を証明するための情報で、セキュリティ上は有用なものです。

    ただ、これが Jenkins の Docker Plugin と相性が悪いunknown/unknown のエントリを見て Jenkins 側が混乱し、イメージを正しく扱えなくなっていました。これは buildx と Jenkins の既知の互換性問題です。

    解決はシンプルでした。

    docker buildx build \
        --platform linux/amd64,linux/arm64 \
        --provenance=false \
        --sbom=false \
        --push \
        -t your-registry/your-image:latest \
        .

    この 2 フラグを追加するだけ。マニフェストが「amd64」と「arm64」の 2 エントリだけのシンプルな構成になり、Jenkins も問題なく処理できるようになりました。

    その他の判断ログ

    ノードのサイズを上げた

    ビルドが遅くなった原因は QEMU だけではありませんでした。元々使っていた軽量ノード(t 系 micro 相当)はメモリが 1GB しかありません。

    npm install や TypeScript のコンパイル、QEMU 自体もメモリを食います。メモリ不足でスワップが起きると、体感より大幅に遅くなります。

    ノードのクラスを比べると、vCPU 数はどれも同じで、差はメモリ量だけでした(1GB / 2GB / 4GB)。medium クラス(4GB)に変更したところ 22 分で完了。これは QEMU の影響込みでの数字なので、妥当な範囲だと思っています。

    ECR ビルドは 1 日 1 回しか動かないので、ノードサイズを上げてもコストへの影響は無視できる範囲です。

    cronを週次に変更した

    元は毎日深夜に自動実行していました。目的はベースイメージのセキュリティパッチを自動取り込みすることです。

    ただ、ソース変更がない日にも毎日ビルドが走るのは純粋に無駄です。急ぎのときは手動で実行できるので、週次(日曜深夜)に変更しました。最大で 1 週間分の遅れは出ますが、実用上は許容範囲と判断しました。

    まとめ

    今回やったことを整理すると、こうなります。

    問題: arm64でビルド → x86_64で動かない
    解決: docker buildx --platform linux/amd64,linux/arm64 --push
    
    落とし穴: それでもJenkinsで動かない
    原因: buildxのデフォルト動作でattestationメタデータが付与される
    解決: --provenance=false --sbom=false を追加

    docker buildx 自体の使い方はドキュメントに豊富にあります。でも「これを Jenkins で使うと attestation 問題が出る」という情報は、探してもなかなか出てきません。正直、ここは予想外の障壁でした。

    buildx を使ったマルチアーキビルドを CI 環境に組み込もうとしている方は、マニフェストの中身を docker manifest inspect で確認する手順を習慣にしておくといいと思います。「pull できたのになぜ起動しないか」を切り分けるのに直接効きます。

    参考

    Docker の使い方を体系的に押さえたい方には、以下の書籍が参考になります。

    ECR や ECS など AWS でコンテナを動かす設計を学びたい方にはこちら。

    Jenkins 側の構成や Docker Plugin との付き合い方をもう少し深く知りたい場合には、こちらもおすすめです。

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

    Coffee cup

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

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

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

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

    対話の時間について