ヘッドレスブラウザSplashをHerokuにDockerデプロイする

# はじめに
この記事はSplashをHerokuにデプロイする手順の解説記事です。

[Splash](https://splash.readthedocs.io/en/stable/install.html)はJavaScriptレンダリングが可能な、スクレイピングに特化したPython製のヘッドレスブラウザです。
ローカル環境では公式のDockerイメージで起動することができます。```docker run -it -p 8050:8050 --rm scrapinghub/splash```

このSplashをHeroku上で動かそうと試したときの手順をまとめます。



#### Splashのイメージ
```mermaid
sequenceDiagram
    クライアント ->> Splashサーバ: リクエスト
    Splashサーバ->>Webサイト: リクエスト
    Webサイト->> Splashサーバ:レスポンス
    Splashサーバ->> クライアント: JS適用後のHTMLやスクリーンショット

```




# 前提
- Herokuアカウント作成済み
- Heroku CLI インストール済み
- Docker インストール済み


# 手順

Heroku Container Registry を使用するとDockerイメージをHerokuにデプロイできます。
ただし、いくつか制約があり元のSplashイメージをそのまま使うことはできないので、修正したイメージをリリースします。

## 1. Dockerイメージを作成する

### Dockerfileを作成
```Dockerfile:Dockerfile
# 公式のイメージをベースにする
FROM scrapinghub/splash

# ベースのENTRYPOINTを上書きする
ENTRYPOINT [""]

# 環境変数のポート番号で起動する
CMD python3 /app/bin/splash --port $PORT
```

### Dockerイメージをビルド
ここではmy-splashというイメージ名を付けてビルドします。
```sh
$ docker build -t my-splash . 
```

docker inspectコマンドで```Entrypoint```と```Cmd```を確認できます。
```sh
$ docker inspect my-splash
〜〜略~~~
            "Cmd": [
                "/bin/sh",
                "-c",
                "python3 /app/bin/splash --port $PORT"
            ],
〜〜略~~~
            "Entrypoint": [
                ""
            ],
〜〜略~~~
```

## 2. Herokuアプリを作成する
Herokuにログインし、新しいアプリを作成します。

```sh
$ heroku login
```

```sh
$ heroku create               
Creating app... done, ⬢ <アプリ名>
```

## 3. Dockerイメージにプッシュ用の名前を付ける
命名規則に従って先ほど作成したイメージに別名をつけます。
```<アプリ名>```の箇所は```heroku create```時に作成されたアプリ名です。
```sh
$ docker tag my-splash registry.heroku.com/<アプリ名>/web
```

## 4. Herokuにイメージをプッシュする

Herokuのコンテナレジストリにログインします。
```sh
$ heroku container:login
```

DockerイメージをHerokuにプッシュします。
```sh
$ docker push registry.heroku.com/<アプリ名>/web
```

## 5. リリースする
```sh
$ heroku container:release web -a <アプリ名>
```

# 動作確認

ページを開くと、無事Splashの画面が表示されました。
```sh
$ heroku open -a <アプリ名>
```
![splash-img.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695992/2dadbdf3-ee23-e025-cff6-b6abd96d3eda.jpeg)


### render.htmlエンドポイント
```sh
curl "https://<アプリ名>.herokuapp.com/render.html?url=http://google.com&wait=0.5" -o test.html
```
- test.html
```html:test.html
<!DOCTYPE html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en"><head>
〜略〜
```

### render.pngエンドポイント
```sh
$ curl "https://<アプリ名>.herokuapp.com/render.png?url=http://google.com&wait=0.5" -o test.png
```
- test.png
![render-png.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695992/f145a657-37e6-5880-74bf-55acfe958c1a.jpeg)


# 説明

### 公式イメージのDockerfile
Splashの[公式イメージDockerfile](https://github.com/scrapinghub/splash/blob/master/Dockerfile)の最後あたりは以下のようになっています。
```Dockerfile
〜〜略〜〜

VOLUME [ \
    "/etc/splash/proxy-profiles", \
    "/etc/splash/js-profiles", \
    "/etc/splash/filters", \
    "/etc/splash/lua_modules" \
]

EXPOSE 8050

ENTRYPOINT [ \
    "python3", \
    "/app/bin/splash", \
    "--proxy-profiles-path", "/etc/splash/proxy-profiles", \
    "--js-profiles-path", "/etc/splash/js-profiles", \
    "--filters-path", "/etc/splash/filters", \
    "--lua-package-path", "/etc/splash/lua_modules/?.lua" \
]
```

このうち Heroku Container Registryでは```VOLUME```と```EXPOSE```コマンドをサポートしていません。
また、ポートはHerokuで指定される```$PORT```環境変数の番号でリッスンする必要があります。

[未サポートの Dockerfile コマンド](https://devcenter.heroku.com/ja/articles/container-registry-and-runtime#unsupported-dockerfile-commands)
> VOLUME​ - ボリュームマウンティングはサポートされていません。 dyno のファイルシステムは一時的です​。
> EXPOSE​ - EXPOSE​ はローカルテストに使用できますが、Heroku のコンテナランタイムではサポートされていません。 代わりに、> > Web プロセス/コードで $PORT 環境変数を取得する必要があります。

ですので、コンテナ起動時のコマンドを変更することでHerokuで動くように対応させます。

- Splashのデフォルトのポート番号は8050ですが```--port```オプションで変更できます。
- プロファイルのディレクトリを指定している```--proxy-profiles-path```などは```VOLUME```が使えないので除外することにします。

修正は公式GitHubのソースを変更してビルドし直す方法も可能ですが、
公式イメージをベースにDockerfileを作るほうが簡単ですのでその手順で行いました。



# 参考
https://github.com/scrapinghub/splash/issues/744

https://splash.readthedocs.io/en/stable/install.html

https://devcenter.heroku.com/ja/articles/container-registry-and-runtime