前言

前陣子入手一台 MacBook Pro,CPU 處理器是 Apple 自家的 M2 晶片,開發上意外發現使用 docker build 的 image 會無法運行在 AWS EC2 上的 Ubuntu 作業系統上,明顯違反了 docker 的口號: Build once, run everywhere,深入研究後才理解,原來這是跟 CPU 處理器的架構有關,稍微紀錄一下來龍去脈和問題解法,以供未來的自己回來參考

以 Python FastAPI 簡例還原問題

建置本地端的 docker image

首先,為了還原遇到的問題,用 python:3.9-slim 的 image 來 host 一個 container,並在裡頭運行一個簡單的 FastAPI server

Python 環境建置、套件安裝

python3 -m venv venv
source ./venv/bin/activate

pip3 install fastapi uvicorn
pip3 freeze > requirements.txt

並準備 FastAPI 主程式

from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/")
def info():
    return {
        "message": "Successfully running API!"
    }

if __name__=="__main__":
    uvicorn.run(app=app, host="0.0.0.0", port=8000)

值得注意的是,uvicorn 運行的 host 必須是 0.0.0.0 才能允許外部連線,否則 127.0.0.1 是 loopback address,只容許本機 localhost 連線

完成 FastAPI 程式撰寫之後,再來是寫 dockerfile

FROM python:3.9-slim

RUN apt-get update && \
    apt-get install -y python3-pip python3-dev && \
    cd /usr/local/bin && \
    pip3 install --upgrade pip

COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip3 install -r requirements.txt

COPY . /app

EXPOSE 8000
CMD [ "python", "main.py"]

如果成功運行的話,FastAPI 運行的 server 將會監聽在 8000 port,並且回傳 Successfully running API! 的訊息

建置 docker image,並貼上一個標籤叫 mac-build

docker build . -t mac-build
docker images | grep mac-build   # 驗證打包好image

寫一個 run.sh 檔來封裝啟動 container 的指令,$1 是 image id,等等把這個 shell script 複製到遠端 AWS EC2 的機器上就能輕鬆執行相同指令

docker run -itd --rm --name mac-build -p 8000:8000 $1

本地端啟動 docker container 來確認 API 是否正常運作

docker images | grep mac-build  # 找image id
sh run.sh <image id>

打開瀏覽器,連到 localhost:8000 確實看到 Successfully running API! 這個回傳結果

遠端 docker image 測試

接下來,我不使用 DockerHub 上傳 repo 的方式來傳遞檔案,而是使用以下指令將 docker image 封裝成 tar 檔

# docker save -o <tar檔路徑> <image id或tag>
docker save -o mac-build.tar mac-build:latest

將壓縮檔mac-build.tarrun.sh 透過scp指令複製到遠端預先開啟的 AWS EC2 上 (作業系統是選用 Ubuntu 22.04)

scp -i ~/pem/<pem file> ./mac-build.tar ubuntu@<remote ip>:/home/ubuntu
scp -i ~/pem/<pem file> ./run.sh ubuntu@<remote ip>:/home/ubuntu

接著,ssh 連線到遠端 AWS EC2 內,使用以下指令將剛剛複製過去的 tar 檔解壓縮,拿到剛剛在 mac 建置的 image

# docker load -i <tar檔路徑>
sudo docker load -i ./mac-build.tar
sudo docker images | grep mac-build  # 確實拿到剛剛包好的image(image id一樣)

接下來就是問題重現的關鍵,運行 sudo sh run.sh <image id>,就會看到以下錯誤!

WARNING: The requested image’s platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested

總結問題: 在 Mac 建置的 image 無法在 AWS EC2 上的 Ubuntu 運行

CPU Core 差異:AMD64 vs ARM64

從剛剛的錯誤訊息中,可以看出幾個關鍵字: image's platform, linux/arm64/v8, linux/amd64

循線追查之後,才知道這個問題跟 Mac 新版的 M1, M2 晶片有關,因為其底層的 CPU 處理器與舊版 Mac 使用的 Intel 晶片不同,才會導致這個問題!

首先,CPU Instruction set(中央處理器指令集)是 CPU 溝通的指令,2020 年以前的 Mac 是使用 Intel 晶片,而 2020 年以後的 Mac 則使用 Apple 自家出產的 M1, M2 晶片,因為底層 CPU 處理器不同,導致 docker 這類仰賴 Host OS 的機制也連帶受影響.

  • Intel 晶片的 CPU 是使用 AMD64 架構,他是一個基於 x86 架構的 64-bit 擴展
  • 新版 Mac 晶片的 CPU 是使用 ARM64 架構,又稱為 ARMv8-A,是基於智慧手機或互聯網的 ARM(Advance Risc Machine)之上的 64-bit 擴充

解決方法

為了解決上述遇到的問題,Docker 有提供一個指令參數 --platform 來決定以何種架構來建置 image,可以設置成很多選項,例如: linux/amd64, linux/arm64, darwin/amd64, … 詳細內容可以查閱 官方文件

所以我將剛剛本地端建置的舊 image 刪除,重新加上 --platform指令來建置新的 image

docker rmi <舊image id>

# 建置新的image,加上建置參數platform為符合Ubuntu作業系統的linux/amd64
docker build . -t mac-build --platform=linux/amd64

# 跟剛剛一樣的步驟,複製到遠端EC2上
docker save -o ./mac-build.tar mac-build:latest
scp -i ~/pem/<pem file> ./mac-build.tar ubuntu@<remote ip>:/home/ubuntu

遠端 EC2

sudo docker rmi <舊image id>
sudo docker load -i ./mac-build.tar
sudo sh run.sh <新image id>

運行後沒有剛剛的建置錯誤,所以看看 localhost:8000 是不是有回傳結果

curl localhost:8000   # {"message":"Successfully running API!"}

成功解決!!

總結

本篇文章以 Python FastAPI 的簡單例子來探討 MacBook Pro(M2 晶片)在建置 image 時遇到的問題,學到 AMD64 和 ARM64 這兩個 CPU 處理器架構的不同(雖然兩個實在長很像),也透過 EC2 的 Ubuntu 作業環境來驗證這個小實驗,確保未來再遇到這個問題時可以不再困擾.仔細想想後,負責打包 image 的環境應該會是一個專門建置與測試的環境,而這個環境往往也不會選用 MacOS 的作業系統,而是 Ubuntu 的作業系統,所以實務上不太會遇到這個問題,比較像是個人電腦開發時才會碰到的小問題.