Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

M1 MacのDockerでChromiumを使ったFeature Specを動かす

はじめに

こんにちは。広告技術部の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