アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

M1 mac 上で Lima の Intel on ARM を試してみる

morishitaです。

前回は M1 Mac 上の Docker Desktop でオーソドックスな構成の Rails アプリケーションであるいこレポの docker-compose の開発環境を動かしてみました。
思ったより少ない変更で動くことが確認できました。

tech.actindi.net ※ ↑前回のエントリを前提にしているところもあるので先に読むことをおすすめします。

今回は同じ docker-compose を Lima の Intel on ARM な仮想マシンで動かしてみたいと思います。

マシンスペック etc は次の通りです。

  • MacBook Pro(14 インチ、2021)
  • チップ Apple M1 Max
  • メモリ 64GB
  • OS macOS Montrey
  • Lima 0.8.1

Lima とは

Lima は "macOS subsystem for Linux" を目指しているツールです。
わかりやすくざっくりいうと WSL の Mac OS 版ですね。

Linux OS の仮想マシンを起動し、その上でコンテナを動かす環境をコマンド1つで作れます。
リポジトリにあるデフォルトの設定ファイル default.yaml ではコンテナランタイムとして containerd を使いますが、 dockderd を利用できる仮想マシンも作れます。
そして、CPU アーキテクチャも選択できます。 Intel on ARM の仮想マシンつまり、M1 Mac 上に Intel CPU をエミュレートした仮想マシンを動かすことができるのです。
別に M1 Mac 用のツールではなく、Intel Mac でも動きます。ARM on Intel な仮想マシンだって作れます。

これを利用して、M1 Mac で Intel CPU のエミュレーションしながら docker-compose 環境を動かしてみました。

インストールから仮想マシンの作成

Lima のインストールから順に説明します。

まずはインストール

Homebrew を使うと次のコマンドで簡単にインストールできます。

$ brew install lima

すでに Docker Desktop がインストールされている環境では docker コマンド、 docker-compose コマンドは利用できるのでそれをそのまま利用しても構いません。
なければ次のコマンドでインストールしておきます。

$ brew install docker docker-compose

仮想マシンの作成

lima のリポジトリにはいくつかサンプルの仮想マシンの設定があります。
今回は lima/examples/docker.yaml を利用します。

次のコマンドでダウンロードします。
x86_64 アーキテクチャ用の設定とするので docker_x86_64.yaml という名前で保存します。

$ curl https://raw.githubusercontent.com/lima-vm/lima/master/examples/docker.yaml -o docker_x86_64.yaml

x86_64 アーキテクチャの仮想マシンを作りたいので次の設定をダウンロードしたファイルの最初に追加します。

arch: "x86_64"

設定ファイルが用意できたら次の様に limactl start コマンドで仮想マシーンを作成します。

❯ limactl start docker_x86_64.yaml
? Creating an instance "docker_x86_64" Proceed with the default configuration
INFO[0001] Attempting to download the image from "https://cloud-images.ubuntu.com/impish/current/impish-server-cloudimg-amd64.img"  digest=
INFO[0001] Using cache "${HOME}/Library/Caches/lima/download/by-url-sha256/ac74da77a6828e35de7edaa06fdbb33d12ef97cce2726550017e3c1066c88fb1/data" 
INFO[0001] [hostagent] Starting QEMU (hint: to watch the boot progress, see "${HOME}/.lima/docker_x86_64/serial.log") 
INFO[0001] SSH Local Port: 60204                        
INFO[0001] [hostagent] Waiting for the essential requirement 1 of 5: "ssh" 
INFO[0042] [hostagent] Waiting for the essential requirement 1 of 5: "ssh" 
INFO[0052] [hostagent] Waiting for the essential requirement 1 of 5: "ssh" 
INFO[0058] [hostagent] The essential requirement 1 of 5 is satisfied 
INFO[0058] [hostagent] Waiting for the essential requirement 2 of 5: "user session is ready for ssh" 
INFO[0069] [hostagent] Waiting for the essential requirement 2 of 5: "user session is ready for ssh" 
INFO[0075] [hostagent] The essential requirement 2 of 5 is satisfied 
INFO[0075] [hostagent] Waiting for the essential requirement 3 of 5: "sshfs binary to be installed" 
INFO[0115] [hostagent] Waiting for the essential requirement 3 of 5: "sshfs binary to be installed" 
INFO[0130] [hostagent] The essential requirement 3 of 5 is satisfied 
INFO[0130] [hostagent] Waiting for the essential requirement 4 of 5: "/etc/fuse.conf to contain \"user_allow_other\"" 
INFO[0143] [hostagent] The essential requirement 4 of 5 is satisfied 
INFO[0143] [hostagent] Waiting for the essential requirement 5 of 5: "the guest agent to be running" 
INFO[0143] [hostagent] The essential requirement 5 of 5 is satisfied 
INFO[0143] [hostagent] Mounting "${HOME}/Develop/actindi/ikorepo" 
INFO[0143] [hostagent] Mounting "/tmp/lima"             
INFO[0144] [hostagent] Waiting for the optional requirement 1 of 1: "user probe 1/1" 
INFO[0144] [hostagent] Forwarding "/run/user/501/docker.sock" (guest) to "${HOME}/.lima/docker_x86_64/sock/docker.sock" (host) 
INFO[0144] [hostagent] Forwarding "/run/lima-guestagent.sock" (guest) to "${HOME}/.lima/docker_x86_64/ga.sock" (host) 
INFO[0144] [hostagent] Not forwarding TCP 127.0.0.53:53 
INFO[0144] [hostagent] Not forwarding TCP 0.0.0.0:22    
INFO[0144] [hostagent] Not forwarding TCP [::]:22       
INFO[0184] [hostagent] Waiting for the optional requirement 1 of 1: "user probe 1/1" 
INFO[0224] [hostagent] Waiting for the optional requirement 1 of 1: "user probe 1/1" 
INFO[0265] [hostagent] Waiting for the optional requirement 1 of 1: "user probe 1/1" 
INFO[0305] [hostagent] Waiting for the optional requirement 1 of 1: "user probe 1/1" 
INFO[0318] [hostagent] The optional requirement 1 of 1 is satisfied 
INFO[0318] [hostagent] Waiting for the final requirement 1 of 1: "boot scripts must have finished" 
INFO[0325] [hostagent] The final requirement 1 of 1 is satisfied 
INFO[0325] READY. Run `limactl shell docker_x86_64` to open the shell. 
INFO[0325] To run `docker` on the host (assumes docker-cli is installed): 
INFO[0325] $ export DOCKER_HOST=unix://${HOME}/.lima/docker_x86_64/sock/docker.sock 
INFO[0325] $ docker ...  

(※ 実際には ${HOME} の部分は具体的なパスが出力されます。)

作成した仮想マシンを確認します。 仮想マシンの NAME は作成時の設定ファイル名で決まります。

❯ limactl ls
NAME                  STATUS     SSH                ARCH       CPUS    MEMORY    DISK      DIR
docker_x86_64         Running    127.0.0.1:60204    x86_64     6       16GiB     100GiB    ${HOME}/.lima/docker_x86_64

(※ 実際には ${HOME} の部分は具体的なパスが出力されます。)

x86_64 アーキテクチャの仮想マシンが起動しているようです。
limactl shell コマンドを使って実際に中に入って確認してみます。

❯ limactl shell docker_x86_64
${USER}@ubuntu:${HOME}/Develop/actindi/ikorepo$ uname -a
Linux ubuntu 5.13.0-20-generic #20-Ubuntu SMP Fri Oct 15 14:21:35 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

(※ 実際には ${USER}${HOME} の部分は具体的なユーザ名と具体的なパスがそれぞれ出力されます。)

docker コマンドと lima コマンドの向き先を変える

作業するときにはいちいち仮想マシンにログインして docker コマンドを実行するのは面倒です。
なので次の環境変数を設定します。

export DOCKER_HOST=unix://${HOME}/.lima/docker_x86_64/sock/docker.sock 
export LIMA_INSTANCE=docker_x86_64

DOCKER_HOSTdocker コマンドの接続先を指定する環境変数数です。
仮想マシンを起動したときに設定値が出力されるのでそれを指定します。
これでホストマシンで実行する dockerdocker-compose コマンドが仮想マシンで動いている dockerd に接続するようになります。
あたかもホストマシンで dockerd が動いているかのように操作できるようになります。

LIMA_INSTANCELima のインストール時に limactl とともにインストールされる lima コマンドの接続先の仮想マシンを指定します。
先程 limactl shell コマンドを使って仮想マシンに入りましたが、この環境変数を設定しておくと lima とコマンドするだけで仮想マシンに入れるようになります。

これらの環境変数は direnv を使って設定するようにしておけば便利でしょう。

いよいよ docker-compose を動かしてみる

docker-compose build でイメージをビルドします。

いこレポの開発環境を作る docker-compose はざっくりこんなコンテナで構成されています。

  • MySQL
    • mysql:5.6 をベースに開発用データを含むイメージをx86-64 環境で別途ビルドしておりそれを利用
  • Nginx
    • Docker 公式イメージの nginx:alpine をそのまま利用
  • Rails
    • Ruby の Docker 公式イメージをベースにビルド
    • マルチステージビルドで公式イメージ node:14 から Node.js をコピー
  • Webpack
    • Rails と同じイメージを利用して Webpack Dev Server を稼働

docker-compose build で問題なくビルドできました。
EntryKit もビルドされたものをダウンロードしてインストールする方法で問題なく動作します。
ちなみに libv8 も試してみましたが問題なくインストールできました。まあ、使わないですが。

そして docker-compose up でアプリケーションを起動します。
起動した各コンテナのプラットフォームを確認してみましょう。

❯ docker-compose exec mysql-master uname -a
Linux f2fc50ae80a7 5.13.0-20-generic #20-Ubuntu SMP Fri Oct 15 14:21:35 UTC 2021 x86_64 GNU/Linux

❯ docker-compose exec rails uname -a       
Linux 47862086e84a 5.13.0-20-generic #20-Ubuntu SMP Fri Oct 15 14:21:35 UTC 2021 x86_64 GNU/Linux

❯ docker-compose exec nginx uname -a
Linux c5a42f0a99c9 5.13.0-20-generic #20-Ubuntu SMP Fri Oct 15 14:21:35 UTC 2021 x86_64 Linux

❯ docker-compose exec webpack uname -a
Linux d37b009faef7 5.13.0-20-generic #20-Ubuntu SMP Fri Oct 15 14:21:35 UTC 2021 x86_64 GNU/Linux

全部 x86_64 ですね。

ブラウザでアクセスしてざっとサイトを一巡しましたが動作も問題ありません。

パフォーマンスは?

Docker Desktop で platform: arm64v8 として動かした場合と Lima で platform: linux/amd64(つまり x86_64)として動かした場合のいこレポのトップページをそれぞれ Chrome の Lighthouse で計測して見ました。

これが、Docker Desktop(platform: arm64v8)の結果です。

f:id:HeRo:20220116141500p:plain
いこレポトップ Docker Desktop(platform: arm64v8)での結果

で、Lima(platform: linux/amd64)の結果です。

f:id:HeRo:20220116141557p:plain
いこレポトップ Lima(platform: linux/amd64)での結果

FCP で 1 秒ほど差が出ていますが、体感では差は感じません。

続いて、東京の記事一覧(/prefectures/13)の計測結果です。

Docker Desktop(platform: arm64v8)は次の通り。

f:id:HeRo:20220116141636p:plain
記事一覧 Docker Desktop(platform: arm64v8)での結果

Lima(platform: linux/amd64)はこんな感じです。

f:id:HeRo:20220116141655p:plain
記事一覧 Lima(platform: linux/amd64)での結果

Lima の方が 2 秒ほど FCP が遅い結果となりました。 しかし、体感だとわずかに遅い気がしますが、そんなに差があるかなぁという感じです。

まあ、どちらも速くはないですが、おそすぎて開発できないということもないのかなぁと思います(私が遅い環境に慣れすぎ?)。

実は Lima v0.7.4 や v0.8.0 で試していたときには正直 Intel on ARM の仮想マシンは遅すぎて使えないなぁと思っていたのですが 0.8.1 で 'Improve x86_64 emulation on aarch64 platform' とあり改善されたようです。

ただし、ページの表示はそれほど大きな速度差はなくなったかなと思いますが、docker-compose up から実際にページにアクセスできるようになるまでは Docker Desktop の方が速いです。

ハマりどころ

Docker Desktop の時のように既存の Dockerfile や docker-compose.yml を変更する必要はなかったのですが、Lima の仮想マシン周りでいくつかハマったところがあります。

セキュリティソフトの設定

最初のうち、limactl start で仮想マシンを作成するときに途中で失敗する次のような事象が発生しました。

まず、次の処理を繰り返します。

INFO[0600] [hostagent] Waiting for the essential requirement 3 of 5: "sshfs binary to be installed" 

そして最終的に次のエラーが発生します。

FATA[0601] did not receive an event with the "running" status 

どうも仮想マシン自体はできているようですが、接続できないようです。

原因がわからずしばらく悩んだのですが、原因はセキュリティソフトでした。
lima で作成した仮想マシンにはホストマシンのディレクトリがマウントされています。
そのホストマシンとのディレクトリの共有には reverse sshfs が利用されています。
つまり、仮想マシン側から SSH でホストマシンに接続に来るのです。

一方で私のマシンには ESET CYBER SECURITY PRO をインストールしていました。
このセキュリティソフトはアンチウィルスの他にパーソナルファイアウォールの機能があります。
これが SSH 接続を遮断していため reverse sshfs によるファイル共有に失敗しエラーが発生しました。

ローカルループバックの接続は許可する次の設定をパーソナルファイアウォールに追加してエラーにならなくなりました。

  • アクション:内向き
  • プロトコル:TCP & UDP
  • 接続元アドレス: 127.0.0.1
  • ローカルポート:すべて
  • リモートポート:すべて
  • アプリケーションすべて

ファイルの書き込み権限

Lima で作った仮想マシンにはホストマシンのファイルシステムが共有されます。
デフォルトではユーザのホームディレクトリ以下が Read Only で共有されます。

その仮想マシンの中で docker-compose を使って Rails アプリの開発する場合には Read Only では困ります。
例えば Gmefile 変更後の bundle installGemfile.lock の更新ができません。

なのでアプリケーションのソースがあるディレクトリは少なくとも書き込み可能にする必要があります。
今回は次の設定を仮想マシンの設定ファイルに追加しました。~/Develop/ikorepo はいこレポのソースを置いているディレクトリです。
writable: true により書き込み可能となります。

mounts:
  - location: "~/Develop/ikorepo"
    writable: true

ただし、ここで懸念があります。
ホストマシンのハイバネーション時に書き込み可能ディレクトリのデータをロストするバグがあると Lima のリポジトリで警告されているからです1
私はまだ遭遇していませんが、easily lost と書かれているのでそのうち遭遇するのかもしれません。

ソースコードはリモートリポジトリで管理しているので一切を失うことはないと思いますが、最後の Push 以降の作業が吹っ飛ぶ可能性があるわけです。

単純にイメージをビルドするだけなら書き込まないので問題ないですが、Rails アプリケーションの開発環境としては書き込みディレクトリにせざるを得ないディレクトリもあるので心配です。
前述の例ではソースコードを置いているディレクトリ全体を書き込み可能にしています。しかし、gem のインストールディレクトリとか、node_modules とか結構あるので面倒ですが、書き込みが必要なディレクトリだけ個別に設定すればリスクは減らせると思います。

まとめ

さて、前回と今回のエントリで M1 Mac 上の Docker Desktop と Lima を比較してみました。

いこレポのようなオーソドックスな Rails アプリケーションではどちらでも開発環境を作れました。

ただし、Lima は次の点で劣ると思いました。

  • 仮想マシンの起動に数分の時間がかかり、手間
  • 書き込み可能ディレクトリのファイルロストバグがある
  • コンテナの動作が遅い

仮想マシンの起動の手間は同仕様もないですね。 一度立ち上げたら立ち上げっぱなしにしておけばいいのですが、ファイルロストバグは少々心配です。

ファイルロストバグは気をつければ被害は小さくできるとは思います。
きっと今後のアップデートで改善されてもいくでしょう。
が、本格的に開発環境として常用するのはちょっと時期尚早かもしれません。

最後のコンテナの動作が遅い件ですが、Rails アプリケーションへのリクエストを処理するのはさほど気になる差はないです。しかし、ネイティブ Gem のコンパイルなど少々重い処理でその差は大きくなるように思いました。
とはいえ、Lima の Intel on ARM な仮想マシンは Intel CPU でないと動かないイメージもまあ実用的な速度で動作すると思います。

Lima の使い所は x86_64 向けのイメージをどうしても M1 Mac で動かす必要があるときだと思います。

また、Docker Desktop は人数が多かったり売上が大きな組織では有料となります2。 そんな大企業で無料のコンテナ開発環境を小さな手間で作りたい場合にも Lima は役立つと思います。

何れにせよ Mac でコンテナを利用した開発をするならば Apple Silicon は避けられないです。
Lima などで一時しのぎしつつ、M1 mac でも動くように変更していくのが現実的かなと思います。
Mac である必然性がなければ Intel CPU の Windows や Linux に乗り換えるというのもありかもしれません。

おまけ: limactl の基本操作

以下は自分用のメモ代わりです。

仮想マシンの作成

次のコマンドで仮想マシンを作成し立ち上げる。
仮想マシンの名前は設定ファイル名から拡張子を除いたものとなる。

$ limactl start <仮想マシンの設定ファイルYAML>

仮想マシンの設定ファイルのサンプルはlima/examples/にある。

設定ファイルではアーキテクチャ(arch:)や CPU コア数(cpus:)、メモリ(memory:)、ストレージ容量(disk:)を設定できる。

設定例は次の通り。

arch: "x86_64"
cpus: 6
memory: "16GiB"
disk: "100GiB"

その他の設定方法は default.yamlのコメントに色々書かれているのが参考になる。

仮想マシンの一覧表示

$ limaclt ls

既存仮想マシンの起動、そして停止

停止している既存仮想マシンの起動は次の通り。

$ limactl start <仮想マシン名>

停止するには次のコマンドを実行する。

$ limactl stop <仮想マシン名>

仮想マシンへの接続

仮想マシンのシェルに入るなら仮想マシン名だけを指定する。

$ limactl shell <仮想マシン名>

次でも可能。

$ export LIMA_INSTANCE=<仮想マシン名>
$ lima

仮想マシンでコマンド実行したいなら仮想マシン名とコマンドを指定する。

$ limactl shall <仮想マシン名> <コマンド>

limactllima コマンドでなくどうしても ssh で仮想マシンに接続したい場合には ${HOME}/.lima/_config/user に接続用の秘密鍵があるので次の様に接続する。
ポート番号は limactl ls の出力に表示されている。

$ ssh 127.0.0.1  -i ~/.lima/_config/user -p <ポート番号>

仮想マシンの削除

先に仮想マシンを停止した上で次を実行する。

$ limactl delete <仮想マシン名>

最後に

アクトインディではエンジニアを募集しています。

actindi.net