- はじめに
- 元々の構成
- Google Chromeが異常終了する
- 別コンテナでChromiumを起動する
- Chromiumを動かすDockerイメージにseleniarmを使う
- CapybaraでリモートドライバとしてChromiumを指定
- まとめ
- 参考記事
はじめに
こんにちは。広告技術部のjohnmanjiroです。普段は広告配信のAPIや管理画面を作っています。ピーナッツくんのライブに現地参戦したのがここ最近で一番楽しかったです。
Gunosyでは、社員が使っているPCが古くなってきたタイミングで新しいものへ置き換えるPCリプレースを行っています。今回私もリプレースの対象になり、MacBook ProがIntelからM1 Proになりました。
新しいPCで管理画面の環境構築をしていた際、Google Chromeを使ったFeature Specが動かなくなっていることに気づきました。どうにか動くようになったものの、なかなか苦労したので、この記事にまとめようと思います。同じ問題で悩んでいる方の助けになれれば幸いです。
ちなみに、System SpecでなくFeature Spec表記にしているのは、管理画面で使っているのがFeature Specだからです。ですが基本的にはSystem Specを使っていても同様の方法で解決できると思います。
元々の構成
本題に入る前に、元々どういった構成でFeature Specを動かしていたのかを紹介します。
アプリケーションの実行にはDockerを採用し、ローカル開発時にはdocker composeを使って動かしています。 Railsのコンテナは自前でDockerfileを用意しており、元々の構成ではRailsの構築に加えてGoogle Chromeのインストールなども同一のDockerfileで行なっていました。 RailsとGoogle Chromeが同じコンテナ内に同居している状態です。
RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' \ && sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' \ && apt-get update \ && apt-get install -y \ google-chrome-stable \ ...(中略) && CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` \ && wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ \ && unzip ~/chromedriver_linux64.zip -d ~/ \ && mv ~/chromedriver /usr/bin/chromedriver \ ...
この構成でM1 Mac上で動かしたとき、Feature Specを実行するとGoogle Chromeが異常終了するという問題が発生しました。
Google Chromeが異常終了する
これはM1 Mac上で linux/amd64
のイメージを使った際に発生します。
QEMUでSegmentation Fault
Docker Desktop for Macは、ホストマシンのアーキテクチャに合わせてDockerイメージを取得します。したがって、ホストマシンであるMacがIntelチップであれば linux/amd64
のイメージを取得しますし、M1であれば linux/arm64/v8
のイメージを取得します。
しかし、arm64向けのDockerイメージがないものも存在します*1。その場合は、linux/amd64
を指定する*2ことで別アーキテクチャのイメージを取得することができます。そのイメージはQEMUでエミュレーションされたamd64の上で実行されます。
ここで問題があり、QEMUのエミュレーションは完璧ではありません。一部の命令がサポートされておらず、それを使用するイメージは実行時にSegmentation Faultで終了します*3。
Google Chromeもこれに該当し、M1 Macのlinux/amd64
上で動かした場合には動作しません。
arm64向けのGoogle Chromeはない
ではarm64向けのGoogle Chromeを使えばいいじゃない、ということになりますが、現時点でarm64向けのGoogle Chromeは提供されていません。
したがって、M1 MacのDockerではGoogle Chromeは動作しないということになります。
そのためGoogle Chromeを使うことは諦めて、Google ChromeのコードベースであるChromiumを使おうということになりました。
別コンテナでChromiumを起動する
Chromiumを使うことに決めたとはいえ、既存の構成のようにRailsと同一のコンテナに導入するのは、自前でインストールの処理を記述する必要がありなかなか骨が折れます。
そこで、同一コンテナに置くことをやめ、Chromiumを提供してくれるDockerイメージを別コンテナとして起動し、コンテナ間で通信してFeature Specを実行する構成に切り替えることにしました。
Chromiumを動かすDockerイメージにseleniarmを使う
Chromiumを提供してくれるものとして、docker-seleniumがよく知られています。最初はdocker-seleniumの selenium/standalone-chrome
を使おうと思ったのですが、残念ながらこれもM1 Mac上では動かないようでした。
ほかのイメージを探していたところ、同じコミュニティが提供している docker-seleniarm を発見しました。READMEに書いてあるとおり、linux/arm64
を含む複数のアーキテクチャに対応したイメージを提供するリポジトリです。seleniarm/standalone-chromium
を起動してみたところ問題なく動作したため、このイメージを使用することになりました*4。
また、seleniarmは linux/amd64
でも動作するので、IntelチップとM1チップ両方でこのコンテナを使うことにしました。
CapybaraでリモートドライバとしてChromiumを指定
ここまででM1 MacのDocker上でChromiumを動かすことができました。 あとはCapybaraで別のコンテナで動作しているChromiumを利用できるように設定すればFeature Specが動作するはずです。
ここでは実際に行った設定を紹介します。
Capybaraのリモートドライバ設定
CapybaraはリモートドライバとしてURLを指定することができます。今回はリモートドライバにChromiumを設定します。
また、ローカルでのテストとは別にCIにはCircleCIを使っており、CircleCIでは同一コンテナにGoogle Chromeが入っているイメージを使っているので、CircleCIとそれ以外で設定を分けています。
ドライバの設定
Capybara.register_driver :selenium do |app| if ENV['CIRCLECI'] options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless') options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') options.add_argument('--window-size=1400,1400') Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) else capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( chromeOptions: { prefs: { 'download.default_directory' => DownloadHelper::PATH.to_s }, args: [ '--headless', '--window-size=1400,1400', '--no-sandbox', '--disable-dev-shm-usage', '--lang=ja-JP', ], } ) Capybara::Selenium::Driver.new( app, browser: :remote, url: 'http://chromium:4444/wd/hub', desired_capabilities: capabilities, ) end end
ここで、DownloadHelperはダウンロードのテストを行うために追加しているものです。本記事では割愛します*5*6。
ホストとポートの固定
ただドライバを設定しただけだと、Capybaraのホストやポートが実行ごとに変わってしまい、Chromium側で net::ERR_CONNECTION_REFUSED
が発生するので、ホストとポートを固定します。
config.before(:each, type: :feature, js: true, driver: :selenium) do Capybara.server_host = IPSocket.getaddress(Socket.gethostname) Capybara.server_port = 4444 Capybara.app_host = "http://#{Capybara.server_host}:#{Capybara.server_port}" if ENV['CIRCLECI'] page.driver.browser.download_path = DownloadHelper::PATH.to_s end end
ファイルダウンロードテストのためにvolumeを共有
ファイルをダウンロードするテストを行う場合、なにも設定がない状態だとファイルがChromiumのコンテナにダウンロードされてしまうため、Rails側でダウンロードが成功したかどうか判定ができません。
そこで、docker composeでRailsとChromiumでダウンロード先のディレクトリをVolumeとして共有することでファイルをRails側でも見られるようにしました*7。
# Railsのコンテナ app: volumes: - .:/usr/src/app ... # Chromiumのコンテナ chromium: image: seleniarm/standalone-chromium:4.1.4-20220429 volumes: - ./tmp/downloads:/usr/src/app/tmp/downloads ports: - 4444:4444
下記の図が最終的な構成です。 以上の実装により、無事M1 MacのDocker上でChromiumを使ってFeature Specを動かすことができました。
まとめ
この記事では、M1 MacのDocker上でChromiumを使いFeature Specを動かすまでの対応内容を紹介しました。
- M1 MacのDockerではGoogle Chromeが異常終了する
- arm64対応のGoogle Chromeは存在しないので、arm64対応のChromium(docker-seleniarm)を使う
- 別コンテナとしてChromiumを起動するので、Capybaraでリモートドライバとして設定する
ある程度環境が整ってきたとはいえ、M1未対応なものもまだある程度はありそうです。少しでもお役に立てたなら幸いです。
参考記事
M1 mac上のDockerコンテナ内でChromiumを動かそうとしてやったこと&やろうとしてること - savanna blog
Docker Desktop for Apple siliconでseleniumを使う - Qiita
*1:MySQLなど
*2:--platform linux/amd64 で指定することができます。https://docs.docker.com/desktop/mac/apple-silicon/#known-issues
*3:https://github.com/docker/for-mac/issues/6204
*4:Laravelのドキュメントでもseleniarmの使用が勧められています。https://laravel.com/docs/9.x/sail#selenium-on-apple-silicon
*5:https://tech.medpeer.co.jp/entry/2018/07/04/090000
*6:https://tech.medpeer.co.jp/entry/2019/09/25/090000
*7:https://qiita.com/github0013@github/items/e6783d3434682d2fd73c