栗原(ksss)です。
新緑の季節になってきましたね。最近よく使う絵文字は🍀です。
READYFORでは貸与PCにて日々の開発を進めています。Apple社のMacBook Proを選択するエンジニアも多く、
「Apple M1チップの乗ったPCで開発できるのか、社の資産として購入・貸与していいのか?」といった問題がありました。
そして2022年4月中頃、長い戦いの末ついに対応完了となりました。
本記事ではREADYFORで行った"M1対応"の歴史を紹介したいと思います。
初期
2020年11月10日 M1発表
Appleが発表したCPU"Apple M1"*1は、後に"M1対応"という言葉も生み出すこととなりました。 ARMアーキテクチャーという、これまでRaspberry Piぐらいでしか使われていなかったCPUアーキテクチャーを採用しているため、多くのソフトウェアが未サポートだったARMアーキテクチャーに対応することを余儀なくされました。
2021年2月8日 10:36
Slack上に「M1 mac困っていることスレ」スレッドが発足。 「取りあえず手元で動く」を目標に、M1 mac所有者間での模索が始まりました。
2021年2月10日 14:12
Slack上に「#dev-m1-mac-challenge」チャンネルが発足 スレッドからチャンネルに移り、活発な情報交換が行われました。
混乱期
2021年10月11日 19:53
フロント開発で使用しているローカルのNode.jsをv14系にupdate完了。(Docker imageは後ほど) Node.jsはv14.17.0からM1対応されているようで、これでフロントエンド開発としてはM1対応完了となりました。
2021年10月19日
Apple「Apple M1 Pro」「Apple M1 Max」を発表。 これに伴い、Intel CPUを搭載したMacBook Proは販売終了に。 社内でいよいよM1対応をしなければという雰囲気が出てきました。
2021年11月10日 16:28
M1 mac所有者もじわじわ増えてきたためか、 GitHub Issues上に「開発で M1 Mac を使えるようにする」というissueが建てられました。
2022年1月1日 (筆者入社)
エンジニアの努力により「取りあえず手元で動く」の目標は達成されましたが、各自で対応が違っていたり、ベストな対応が決めきれなかったりで、"手元にcommitしてはいけない差分がある"状態で開発を続けていました。
私もこの状況下での入社となり、前職でM1 mac使用経験もあったことからM1 macが支給されました。
この時の私のSlack上の独り言チャンネルにはこう記されています。
Yuki Kurihara (ksss) 3月10日 16:29:23 m1対応branch戦略 - m1対応diffが大きいので消えると困る。commitを作って、適宜つけ外しする戦略。 - 'm1-base' というbranchを作ってm1対応を積む - 'm1-base' から変更を積んでいき開発する。これを '***-m1' branchとする - 変更をpushするときは '***' というbranchにrenameしてm1対応commitをrebase -iで消す。このとき '***-m1' branchは必ず残しておく - push内容を変更するときは '***-m1' branchで変更を追加して、 '***' branchを消し、また '***-m1' branchから分岐させる - m1対応に変更がある場合は、'***-m1' branch全てに入れる必要がある。'm1-base' branchを修正して '***-m1' branchに載っているm1対応commitを削除して 'm1-base' をmergeする。 - むり😇
複数の作業を並行していて本当に大変でした。。。
対応期
このままでは自分が辛いので、M1対応に本格的に乗り出しました。
これまでの有志たちの手によって様々な問題についての複数の解決方法が提案されてきており、カードは集まっていました。
しかしながら問題毎にスコープが膨らみがちで、足踏みしている状態でした。
私はSF小説「火星の人」のマークよろしく、問題を一つずつ対応することにしました。
Dockerfile問題
ローカルでbuildしているDockerにはnodeやtblsといったツールを導入していました。 これらはbuild済みバイナリをダウンロードして使用していましたが、ダウンロードURLがamd64(x64)アーキテクチャー向けにbuildしたものを指していました。よってこれらのツールが動かない状態でした。
この問題はunameを見てダウンロード先を変更する解決方法もありますが、Dockerfileが複雑になっていまう懸念がありました。
今回はDockerfileをシンプルに保ちつつ、build時間もそこまで長くならないようにという狙いでMulti Stage Build*2を使うことにしました。
nodeについてはbuild済みのimageをCOPYし、tblsについてはgolang imageからgo installしてからCOPYするように修正。
ここまでをスコープとして1 PR上で議論し、修正完了となりました。
tblsは後にGitHub Actionsでの自動更新対応が行われたので、もうDockerfileには不要かもしれませんね。
config.file_watcher問題
「エラーログが出続けるのでconfig.file_watcher = ActiveSupport::EventedFileUpdateChecker
をconfig.file_watcher = ActiveSupport::FileUpdateChecker
に修正しましょう」
という記事は良く見られるかと思いますが、現在この問題は修正されているので変更の必要はありません。
rb-fsevent gemのv0.11.0以上を使用することでActiveSupport::EventedFileUpdateChecker
を使用してもエラーは発生しないようです。
https://github.com/guard/rb-fsevent/pull/88 で修正されたようですね。コミュニティに感謝。
MySQL問題
MySQLはv5.6のdocker imageを使用していました。このimageはM1上では動きません。
MySQL問題についてもインターネット上に様々な情報が上がっています。
MySQLのバージョンアップはやりたい課題の一つではありますが、これを待っているとM1対応がなかなか終わりません。
今回は以下の判断基準に基づき、platform: linux/amd64
をdocker-compose.ymlのMySQLコンテナの設定に追加しました。
O=良い △=微妙 X=そもそも動かない
方法 | amd64/パフォーマンス | amd64/互換性 | M1/パフォーマンス | M1/互換性 | 備考 |
---|---|---|---|---|---|
このまま | O | O | X | X | |
mariadb | O | △ | O | △ | tbls等で差分が出る |
platform指定 | O | O | △ | O | エミューレートしている分倍ぐらい遅い |
mysql v8 | O | △ | O | △ | 将来的にはこれか |
ElasticSearch問題
ElasticSearchは elasticsearch:7.1.1
docker imageを使用していました。このimageもM1上では動きません。
こちらもMySQLと同じくversionを上げればarm64向けにbuildされたimageもあるのですが、
今回はパフォーマンスより開発のしやすさを重視して、platform: linux/amd64
をdocker-compose.ymlのelasticsearchコンテナの設定に追加しました。
単純にplatform: linux/amd64をつけるだけだと、seccomp(linuxのシステムコールをフィルターする機能)関連のエラーが出てしまい起動しません。
原因は完全には追いきれてはいないのですが、開発環境なのでセキュリティー機能はoffにして問題ないだろうと判断し、環境変数を追加しています。(bootstrap.system_call_filter=false
)*3
mini_racer問題
M1問題でこれが一番厄介でした。 mini_racerはRuby上でJSのコードを実行する事ができるgemです。 弊社ではreact_on_railsのSSRのために使用しています。
このmini_racerをupdateすると、M1ではbuildできるのにIntel環境(特にCI)ではbuildできないという問題が現れました。 この問題のために、長い間Gemfile.lockに差分があるのにcommitできないという辛い時期が続きました。
mini_racerを削除する?
react_on_railsが依存するexecjsでは複数のバックエンドを利用することができ、mini_racerを使わない選択肢も模索されました。
しかしながらreact_on_railsでは
Be sure to use mini_racer. *4
と紹介されており、mini_racerを外すためにはパフォーマンスの劣化による影響が懸念されました。
今回は外さずにupdateで対応する方向に舵を切りました。
調査の結果、下記issueが見つかりました。
https://github.com/rubyjs/mini_racer/issues/220
私はこのissueを最初に読んだとき「これはrubygemsで対応中なのかなあ?」とissueを閉じてしまっていましたが、
このissueにヒントがありました。bundle lock --add-platform
です。
Gemfileにplatformを指定することでbuild済みのgemを使用することができます。fat gemというやつです。
bundle lock --add-platform
することで「このplatformを使っているのでバイナリくださいー」と指定することができます。
fat gemは現在あまり推奨されていません。 しかしながら今回のようにbuildに問題がある場合があるためか、今も使われているようです。
でも失敗
「とにかく全部指定すれば良いのか?」と考えた私はx86_64-linux
(Intel用)、x86_64-linux-musl
(CIで見る)、aarch64-linux
(M1用)を追加しました。
これでいけるか?と思ったらなぜかSassCがSyntaxErrorを出しました。
イチかバチか
もうだめかと諦めかけたとき、以下のbuildできないときのログから、
/home/circleci/app/vendor/bundle/ruby/2.7.0/gems/libv8-node-16.10.0.0-x86_64-linux-musl/vendor/v8/x86_64-linux/libv8/obj/libv8_monolith.a: No such file or directory
「もしかしてx86_64-linux-musl
とx86_64-linux
を見分けるのが難しいのかも?」と考え
イチかバチかx86_64-linux
の指定だけにしてみたところ、見事M1機でもIntel機(CI)でもmini_racer(libv8-node)のinstallに成功しました。
また、社のエンジニアにも協力を仰ぎ、「installできた」との報告をいただきました。
動作確認をし、リリースまでもっていけました。
成功したけどギブアップ
正直なぜbuildできなかったのか、なぜこれでinstallできるのかよくわかっていません……。 しかしながら「手元にcommitできない変更を抱えながら作業する」という負担から開放される誘惑に負けて調査は諦めました。
まとめ
これにてREADYFORでのM1対応は終了しました。 大きい問題は、小さい問題を1つずつ対応すればいつか解決できます。
このことを、これでもかと教えてくれる「火星の人(ハヤカワ文庫SF)」を是非読みましょう!
(※ 画像はNASA https://www.nasa.gov/mission_pages/msl/overview/index.html より)
*1:https://ja.wikipedia.org/wiki/Apple_M1
*2:https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/
*3:https://www.elastic.co/guide/en/elasticsearch/reference/master/_system_call_filter_check.html
*4:https://github.com/shakacode/react_on_rails/blob/52ea14bbc2de7de175d421b5491492c6a195ab4e/docs/javascript/server-rendering-tips.md