Heroku でも Selenium を使いたい!

Webサイトのクローリングや、開発しているWebアプリのUATとして、たとえばSeleniumを使ってブラウザテストを行いたいケースはよくあるでしょう。では、これらをHeroku上で実施するには...？

Heroku CI では、[Heroku CI: Browser and User Acceptance Testing (UAT)](https://devcenter.heroku.com/articles/heroku-ci-browser-and-user-acceptance-testing-uat)という`ベータ機能`を提供しています。ここで、headless Chromeを利用することができるようになっており、この機能を活用することで、UATやクローリングを実装することができます。

# Headless Chrome を Heroku 上で利用するには

なんてことはありません `https://github.com/heroku/heroku-buildpack-google-chrome` buildpack を入れるだけです。Headless Chromeを操作するために、別途言語のランタイムも必要になるでしょうから、複数の buildpackを指定する `multi buildpack`構成になります。偉そうに名前をつけていっていますが、ただ、複数のbuildpackを入れるだけです。

buildpack の入れ方は、Heroku Dashboardからでも、Heroku CLIからでも、カンタンに導入可能です。

## Heroku Dashboard からの場合

利用したい Herokuアプリのダッシュボードで、`Settings` タブへ移動します。`Buildpacks` 項目で「Add Buildpack」というボタンがありますので、これをクリックし、追加で利用したいbuildpackのURLまたはプログラミング言語名を入力したあと `Save changes` するだけです。

![Screen Shot 2019-05-28 at 16.57.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37263/39c99f7f-23a1-d3ca-90e0-3802ecc72c37.png)

## Heroku CLI の場合

`heroku buildpacks:add` コマンドを利用します。通常は `heroku buildpacks:set` コマンドで、該当のプログラミング言語を指定します。multi buildpacks 構成にするには、`buildpacks:add`で追加を行うことになります。くわしくは、[Using Multiple Buildpacks for an App](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app)をご覧ください。

```bash
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-google-chrome -a <APP_NAME>
```

> `<APP_NAME>` には対象の Herokuアプリ名を指定

# Selenium も使いたい!!

Selenium を使う場合には、更に[heroku-buildpack-chromedriver](https://github.com/heroku/heroku-buildpack-chromedriver) buildpack が追加で必要になります。追加の方法は Chrome Driver と同じで、更に buildpackを追加します。

追加する Chrome Driver のURLは `https://github.com/heroku/heroku-buildpack-chromedriver` となります。

# Selenium on Heroku の利用方法

さて、buildpackを設定して、これらを実際に使う場合にはどのようにすればよいでしょうか。次の2点に気をつければ問題ありません。

1. Headless Chromeを利用するには `--headless` および `--disable-gpu` オプションが必要です
2. chrome の binary PATH は、環境変数 `$GOOGLE_CHROME_SHIM` を指定します

## 設定サンプル

例えば `Ruby`の場合、次のように指定します。

```ruby
options = Selenium::WebDriver::Chrome::Options.new
options.binary = ENV.fetch(“GOOGLE_CHROME_SHIM”)
options.add_argument('headless')
options.add_argument('disable-gpu')
driver = Selenium::WebDriver.for :chrome, options: options
```

あとは、普段どおりにSeleniumをしばき倒せば問題ありません。

## Heroku CI上で利用する場合には

Heroku CIで、UATとして利用するには、`app.json`でbuildpackを利用するよう定義しておく必要があります。

例えば `test` 環境で定義するには、`environments`内に次のように`buildpacks`を定義しておきます。

```json
{
  "environments": {
    "test": {
      "buildpacks": [
        { "url": "heroku/ruby" },
        { "url": "https://github.com/heroku/heroku-buildpack-google-chrome" },
        { "url": "https://github.com/heroku/heroku-buildpack-chromedriver" }
      ]
    }
  }
}
```

# 注意事項

- この２つのbuildpackは、公式として提供されているわけではないので、現時点では未サポートです

# おまけ - Selenium on Heroku でダウンロードする場合には

ファイルをダウンロードしたいんだと言うときには、次の注意点に留意して実行します。

1. `setDownloadBehavior`を`allow`設定にする
2. 指定したディレクトリに自動ダウンロードするように`preference`を設定する

## Ruby で設定するには

`setDownloadBehavior`を`allow`させるには、次のようなメソッドを定義し、Selenium の `driver`と、ダウンロード先`directory`を明示して、指定したダウンロード先へのダウンロードを許可します。
将来、Google Chrome の内部構造の変更により、この書き方が変更になったり、不要になる場合が想定されます。

```ruby
def enable_chrome_headless_downloads(driver, directory)
  bridge = driver.send(:bridge)
  path = '/session/:session_id/chromium/send_command'
  path[':session_id'] = bridge.session_id
  bridge.http.call(:post, path, {
  "cmd" => "Page.setDownloadBehavior",
  "params" => {
    "behavior" => "allow",
    "downloadPath" => directory,
  }
})
end
```

例えば、`prefs`というオブジェクト内に、ダウンロードするディレクトリと、自動的にダウンロードするオプションを指定して、`options.add_preference` で追加設定を行うように指定します。

```ruby
prefs = {
  prompt_for_download: false, 
  directory_upgrade: true,
  default_directory: download_directory
}
options.add_preference(:download, prefs)
```

# Worker Dyno として利用する場合の注意事項 (2019/5/31追記)

実際に運用されている開発者の方から、Worker Dyno として起動させ続けるようなケースにおいての注意事項をいただきましたので、追記しておきます。対処法など分かれば、また追記します。

1. Dyno Restart の際、`SIGTERM`が送られてくると、その時点で Chromeのプロセスが先に停止してしまう様子。すると、Selenium側からの実行で制御不能になるとのこと
2. One off dyno の場合、24時間以上動作するようなケース以外では、強制停止のみなので、この事象には当たらないものと想定される