RendorでFlask × SeleniumのwebアプリをDockerでデプロイする

Seleniumによるブラウザの自動化をすることでスクレイピングが可能となります。今回は、スクレイピングした結果を使うようなwebサイトをFlaskで作成し、Rendorで公開するまでの流れを紹介します。

Rendorとは

Rendorとは、様々なwebアプリをデプロイするためのPaaSであり、Herokuのようなサービスを展開しています。 Rendor上では、Githubと連携することで特定のブランチにプッシュされたときのみアプリをデプロイするなど、自動デプロイが可能となります。

公式サイトが出している料金比較にもあるように、有料化したHerokuと比べても比較的安価に運用ができるため、さくっとサービスを公開したいときに適しているのではないかと思います。

ただし、Rendorには注意点もあり、docker composeをサポートしてはいません。その代わり、render.yamlという独自のYAMLファイルを書く必要があります。今回は、docker composeを使って開発したFlaskアプリを render.yaml に置き換えてデプロイするまでを書いていきます。

docs.render.com

Flask × Seleniumのアプリのローカル起動まで

今回は以下の記事をベースとした Flask x Seleniumのwebアプリを使います。

datascience-beginer.com

ローカルで実行する際には、Python用のコンテナとSelenium用のコンテナをdocker composeで同時にビルドし、起動するような構成となります。

ディレクトリ構造

以下のディレクトリ構造のプロジェクトで進めていきます。

.
├── Dockerfile
├── app
│   ├── __init__.py
│   ├── app.py
│   ├── driver.py
│   └── templates
│       ├── index.html
│       └── result.html
├── docker-compose.yml
└── requirements.txt

docker-compose.yml

docker composeは以下のようになっています。

version: '3'

services:
  selenium-worker:               
    image: selenium/standalone-chrome:latest 
    shm_size: 2gb         
    ports:
      - 4444:4444         
      - 7900:7900         

  python:
    build: .
    volumes:
      - ./app:/app
    tty: true
    depends_on:
      - selenium-worker
    ports:
      - 5000:5000
    environment:
      - SELENIUM_WORKER_HOST=selenium-worker

Dockerfile

Flask用のDockerfileは以下のようになっており、5000番ポートでリクエストを受ける形となります。

FROM python:latest
RUN apt-get update && \
    apt-get install -y \
    build-essential \
    cmake \
    git \
    sudo \
    wget \
    vim

RUN pip install --upgrade pip

COPY ./requirements.txt /requirements.txt

RUN pip install -r /requirements.txt

WORKDIR /app

COPY ./app .

ENV FLASK_APP=/app/app.py

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000",  "app:app"]

app/app.py

app.pyはFlaskアプリの実装しており、大まかな実装は以下となります。

from flask import Flask, render_template, request

from driver import init_driver, finish_driver

app = Flask(__name__)


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/scrape")
def scrape():

    ...

    driver = init_driver()

    # スクレイピング処理

    finish_driver(driver)

    return render_template("result.html", ...)


if __name__ == '__main__':
    app.run()

app/driver.py

driver.pyはChromeのリモートweb driverを初期化、終了するのに使われます。これらはapp.pyで呼び出されています。

import os

from selenium import webdriver


def init_driver():
    options = webdriver.ChromeOptions()

    ...

    driver = webdriver.Remote(
        command_executor='http://{}:4444/wd/hub'.format(os.environ['SELENIUM_WORKER_HOST']),
        options=options
    )
    driver.set_window_size(500, 500)
    driver.implicitly_wait(10)

    return driver


def finish_driver(driver):
    driver.quit()

ローカル起動

この構成のもと、docker-compose build から docker-compose up をすると、localhost:5000 からアプリにアクセスでき、適当なAPIを叩くことでスクレイピングした結果を返すことができます。ここまでが今回の記事で扱うwebアプリの実装となります。

Renderへのデプロイ

ここからは本題である、Renderへのデプロイに移っていきます。 最初にも書きましたが、Renderでは docker-compose.yml をサポートしていません。 その代わり、独自の render.yaml というものがあり、これを使ってデプロイすることになります。

docs.render.com

Dockerfile.seleniumrender.yaml の追加

render.yaml を使った形に変更するにあたって、先ほどのプロジェクトに新たに Dockerfile.seleniumrender.yaml を追加します。

render.yaml は先ほどから出てきているように、docker-compose.yml の代わりのファイルであり、Dockerfile.seleniumSelenium standaloneのイメージを利用するためのDockerfileとなります。 docker-compose.yml と違って、 render.yaml ではイメージを指定したりすることができません。そのため、サービスごとに Dockerfile を作って、それぞれに設定を追加していく必要があります。最終的なディレクトリ構造は以下となります。

.
├── Dockerfile
├── Dockerfile.selenium
├── app
│   ├── __init__.py
│   ├── app.py
│   ├── driver.py
│   └── templates
│       ├── index.html
│       └── result.html
├── docker-compose.yml
├── render.yaml
└── requirements.txt

Dockerfile.selenium

Dockerfile.seleniumの中身は以下となります。docker-compose.ymlの中身を書き換えただけなので、非常にシンプルになります。

FROM selenium/standalone-chrome:latest

EXPOSE 4444
EXPOSE 7900

render.yaml

次に、render.yamlは以下となります。

services:
  - type: pserv
    name: selenium-worker
    runtime: docker
    dockerfilePath: ./Dockerfile.selenium
    rootDir: .
    envVars:
      - key: PORT
        value: 4444

  - type: web
    name: flask
    runtime: docker
    dockerfilePath: ./Dockerfile
    rootDir: .
    envVars:
      - key: FLASK_APP
        value: /app/app.py
      - key: SELENIUM_WORKER_HOST
        value: ***

render.yamlでは、各サービスごとにtypeを設定することができ、selenium-workerでは pserv、flaskでは web となっています。 公式サイトによると、それぞれ以下のようなサービスとなっています。

  • web:いわゆるwebサービス。外部からのアクセスができる。
  • pserv (private service):外部からのアクセスができないサービス。Renderにデプロイされたwebサービスからはアクセスが可能だが、ブラウザなどの外部からはアクセスできない。

今回のwebアプリではselenium-workerはflaskからのみアクセスされるため、pserv としています。

docs.render.com

docs.render.com

ファイル下部のSELENIUM_WORKER_HOSTという環境変数はこの後設定していくため、一旦適当な文字にしておいてください。

Render上での操作

次に、Renderのダッシュボードでの操作に移ります。

Step 1 Blueprintを選択

Step 2 レポジトリを選択

GitHubと連携すると、デプロイしたいレポジトリが選択できます。Connectを押して連携します。

Step 3 プロジェクト名、ブランチの選択

適宜好きな名前を入力しましょう。ここで、下の方に flask-mn4bselenium-worker-mn4b というサービス名が表示されていることに注目してください。これらが各サービスの名前となっていて、例えばwebサービスhttps://flask-mn4b.onrender.comというURLでアクセスすることができるようになります。

Step 4 render.yaml の再編集

ここまで来れば気づくかもしれませんが、先ほど省略した SELENIUM_WORKER_HOST という環境変数の値には selenium-worker-mn4b を入れることになります。 ローカルのときには、selenium-workerへは http://selenium-worker:4444/... にアクセスすればよかったのですが、Render上ではURLが変わり、http://selenium-worker-mn4b:4444/... にアクセスするということになります。

結果、render.yamlは以下のようになります。

各サービスの名前はランダムにつけられるようで、デプロイするまで分かりません。 そのため、一回失敗すること前提でRender上でデプロイし、生成された名前をコピペして環境変数に入れ、再度変更をプッシュすることでしかデプロイできないのかもしれません。

services:
  - type: pserv
    name: selenium-worker
    runtime: docker
    dockerfilePath: ./Dockerfile.selenium
    rootDir: .
    envVars:
      - key: PORT
        value: 4444

  - type: web
    name: flask
    runtime: docker
    dockerfilePath: ./Dockerfile
    rootDir: .
    envVars:
      - key: FLASK_APP
        value: /app/app.py
      - key: SELENIUM_WORKER_HOST
        value: selenium-worker-mn4b

Step 5 デプロイ結果

修正した render.yaml をプッシュした結果、デプロイが成功することが確認できました。

まとめ

今回はFlaskとSeleniumを使ったwebアプリをDockerを使ってRenderにデプロイする方法を紹介しました。 Renderは初めて使ってみたのですが、自動デプロイは高速で簡単なので、インフラ周りを触ることなくデプロイができ非常に便利だと思いました。

一方、docker composeには対応しておらず、render.yaml という独自のyamlファイルを書く必要があるため、不便さもありました。 docker composeを使えるようにしてくれという声は昔からたくさんあるようなのですが、いまだに対応していない様子を見ると、今後もdocker composeがサポートされることは期待できないです。

feedback.render.com

また、docker-compose.yml ではselenium-workerにshm_size: 2gb を設定することでメモリ不足になることを防いでいたのですが、render.yaml ではshm_sizeを設定することはできないため、デプロイはできてもメモリ不足に陥ってしまうことが多いと思います。 今後 shm_size をRenderがサポートするのを期待したいです。