FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

プロジェクトで直面したCPUアーキテクチャの互換性問題を深掘りする

こんにちは、株式会社FLINTERSの栗原です。この記事は10周年記念として133日間ブログを書き続けるチャレンジの67日目の記事になります。

はじめに

今回の記事では、業務中に遭遇したCPUアーキテクチャに起因するソフトウェアの不具合に焦点を当てます。この事例を通じて、CPUアーキテクチャとコンテナについての理解を深めます。記念ブログとしてはかなり内容が局所的ですが、MacBookを利用している会社では業務用PCがM1/M2に変わってきている頃ですし、どこかで需要が1つでもあれば...と思い執筆しています。

業務中に発生した互換性問題

私が所属するプロジェクトで、M1チップを搭載した環境でのみ発生するエラーに直面しました。このセクションでは、そのエラーを再現し、解決に至るまでのプロセスをステップごとに詳述します。

Step1: Dockerfileのビルド

以下のDockerfileを用いて、Amazon Linux 2ベースの環境にカスタムPython環境を構築しました。これをM1チップ搭載のマシンでビルド(docker build)しました。

FROM amazonlinux:2

# yumをアップデートしPython3をインストール
RUN yum update -y && \
    yum install -y python3 python3-pip && \
    yum clean all

# 以降のコマンドでpython3とpip3が使用されるようにエイリアスを設定
RUN echo "alias python=python3" >> ~/.bashrc && \
    echo "alias pip=pip3" >> ~/.bashrc

Step2: AWS CodeBuildの設定

Terraformを使用して、前のステップで作成したイメージをAWS ECRに格納し、その後、AWS CodeBuildで使用しました。

resource "aws_codebuild_project" "test_sample" {
    name = "sample_codebuild_proj"

    environment {
        image = var.build_env_image # Step1のイメージが格納されているリポジトリURLを指定する
        type = "LINUX_CONTAINER"
        compute_type = "BUILD_GENERAL1_SMALL"
        image_pull_credentials_type = "CODEBUILD"
    }

    source {
        type = "CODECOMMIT"
        buildspec = "sample_buildspec.yml"
        # その他項目の記述省略
    }

    # その他項目も記述省略...
}

Step3: AWS CodeBuildの実行

ビルドプロセスを実行したところ、以下のエラーが発生しました。

SINGLE_BUILD_CONTAINER_DEAD: Build container found dead before completing the build. Build container died because it was out of memory, or the Docker image is not supported

当初はメモリ不足が原因ではないかと疑いましたが、メモリの増設後も問題は解決しませんでした。IntelベースのMacでビルドしたイメージでは問題なく動作したことから、この問題はDockerfileをビルドする際のCPUアーキテクチャが原因の可能性が高いと考えました。

原因と解決策

原因

結論として、この問題は、AppleのM1チップ(ARMアーキテクチャ)とIntelチップ(x86_64アーキテクチャ)の間の互換性に関連していました。
M1チップを使用してビルドされたDockerイメージは、ARMアーキテクチャに最適化されており、そのままではx86_64アーキテクチャを使用しているAWS CodeBuild環境では動作しません。AWS CodeBuildは、BUILD_GENERAL1_SMALL/MEDIUMコンピュートタイプを使用すると、デフォルトではx86_64ベースの環境を提供しているためです。

解決策

Docker Buildxという拡張CLIプラグインを使用し、マルチアーキテクチャビルドを行うことでこの問題は解決できました。

その際の手順は以下になります。

  • ビルダーインスタンスの作成
docker buildx create --name mybuilder --use
  • ビルダーインスタンスの起動とインスペクト
docker buildx inspect --bootstrap
  • ビルドの実行
    • 以下のコマンドは、ARMとx86_64の両方のアーキテクチャに対応したイメージをビルドし、AWS ECRにプッシュする例です。
docker buildx build --platform linux/amd64,linux/arm64 -t aws_account_id.dkr.ecr.region.amazonaws.com/myimage:latest . --push

この手順を行なって作成されたコンテナイメージを、AWS Codebuildの実行イメージに指定したところ、ビルドに成功することを確認できました。

ケーススタディ:この問題から考えるCPUアーキテクチャとコンテナ

せっかくなので、今回発生した互換性問題を参考に、周辺技術について理解を深めていきたいと思います。この項目の執筆には、手元にあった詳解システム・パフォーマンスとChatGPT(&DALL-E3)に協力してもらっています。

学び1: Docker Buildxとは何者なのか?

Docker BuildxはDockerの公式CLIプラグインであり、Dockerイメージのマルチアーキテクチャビルドをサポートしています。Buildxを利用することで、開発者は単一のコマンドで複数のプラットフォーム向けにイメージを同時にビルドし、それらをレジストリにプッシュすることが可能になります。これにより、ARMアーキテクチャのサーバーであろうと、x86_64アーキテクチャのサーバーであろうと、同じイメージからコンテナを実行することができます。Buildxは、ビルドキャッシュの共有や、新しいフロントエンド構文を使った高度なビルド機能など、従来のDockerイメージビルドの機能を大幅に拡張しています。特に、今回のようなアーキテクチャ間での互換性問題に直面した場合、Buildxはそのソリューションとして非常に強力なツールとなります。

学び2: コンテナ(OS仮想化)とは?

現代のクラウドコンピューティング環境では、OS仮想化が中心的な役割を担っています。この技術は、物理的なハードウェアを最大限に活用し、複数の仮想インスタンスを効率的に動作させることを可能にします。特に、Linux環境においては「コンテナ」という概念が重要です。

OS仮想化は、別々のゲストサーバーのように動作し、ホストから独立して管理、リブートできるインスタンスにOSを分割するものである。Linuxは、このようなインスタンスをコンテナと呼んでいる。こうすると、クラウドの顧客には小さくて効率がよく高速にブートするインスタンス、クラウドの管理者には高密度なサーバーを提供できる。
※ 詳解システム・パフォーマンス11章:クラウドコンピューティングより

このコンセプトは、クラウドサービスの柔軟性と拡張性の基礎となり、さまざまなアプリケーションに適応するための土台を築いています。

学び3: なぜビルドするマシンのCPUアーキテクチャの違いによって、作成されるコンテナイメージに違いが発生するのか?

コンテナイメージは、基本的にはアプリケーションとその依存関係を一緒にパッケージしたものです。Dockerfileは実際には「レシピ」であり、このレシピに従ってコンテナイメージがビルドされます。

Generated by DALL-E3

ビルドプロセス中に、ホストマシンのアーキテクチャに依存するバイナリや依存関係がイメージに含まれるため、通常のビルドの場合、異なるアーキテクチャでは異なるイメージが生成されるのです。例えば、ARMアーキテクチャ用にビルドされたバイナリはx86アーキテクチャでは動作せず、その逆も同様です。そのため、マルチアーキテクチャビルドを利用すると、異なるプラットフォームで動作するイメージを同一のDockerfileから生成することが可能になります。

学び4: ARMとx86で具体的に何が違うのか?

ARM向けのコンテナイメージとx86向けのコンテナイメージの違いは、主に実行可能なバイナリファイルのアーキテクチャに基づいています。コンテナイメージには、OSの基本システム、アプリケーション、ライブラリ、依存関係などが含まれており、これらは特定のCPUアーキテクチャ向けにコンパイルされています。

例えば、Windows PC(x86アーキテクチャ)で動作するソフトウェアが、iPhone(ARMアーキテクチャ)では動かないのと同じです。ソフトウェアは特定のプラットフォーム向けに作られているため、違うアーキテクチャで動作させるには、そのプラットフォーム用に再コンパイルするか、エミュレーションが必要になります。

具体的な例としては以下のようなものがあります。

  • バイナリファイル
    • x86向けにコンパイルされた実行ファイルは、ARMアーキテクチャでは直接実行できません。同様に、ARM向けに最適化された実行ファイルはx86システムでは動作しないか、パフォーマンスが非常に悪い可能性があります。
  • システムライブラリ
    • 各アーキテクチャには固有のシステムコールとライブラリがあります。たとえば、ARMアーキテクチャ向けのLinuxディストリビューションに含まれるシステムライブラリは、x86システムでの実行には適していません。
  • 依存関係とパッケージ
    • 特定のアーキテクチャ用にビルドされたパッケージや依存関係は、他のアーキテクチャでは異なるバージョンやビルドが必要になる場合があります。例えば、PythonのパッケージであるNumPyは、ARMとx86の両方で利用できますが、それぞれのアーキテクチャ用にコンパイルされたバイナリが異なります。

まとめと感想

M1チップになってアーキテクチャとやらが変わったらしい、ということは聞きつつも、なんのことかさっぱり...という状態でしたが、業務で詰まったことを機会にある程度調査することで、少し理解が深まったように感じます。本来もっと高水準な理解をするためには、各アーキテクチャ固有のシステムコールやライブラリ等を勉強する必要があるらしいのですが、このあたりは深く知ろうとすると際限なく複雑になっていくので、個人的には上記の理解で納得しておこうと思いました。