docker-composeでScrapydサーバーを立てる

これは[クローラー／Webスクレイピング Advent Calendar 2017](https://qiita.com/advent-calendar/2017/crawler)の10日目の記事です。

## はじめに

アドベントカレンダーに参加登録した時は、こんな感じでScrapydの紹介記事を書こうと思っていました。

![Screen Shot 2017-12-10 at 12.41.11.png](https://qiita-image-store.s3.amazonaws.com/0/92660/dfd3772f-795f-1e1f-db9c-6b4e577cf176.png)

そう。当日この[軽めのスクレイピングプラットフォームを求めてScrapydを試してみた](https://qiita.com/inTheRye/items/06ceb938574ff9030080)を見かけるまでは。。。
この記事の「2017年12月09日に更新」って昨日やん。。。ネタが。。。

ということで、同じこと書いてもつまらないので、`Scrapydを実際にこんな感じで使っている`というのを書きます。Scapyd自体の紹介は[軽めのスクレイピングプラットフォームを求めてScrapydを試してみた](https://qiita.com/inTheRye/items/06ceb938574ff9030080)をご覧くださいw

## docker-composeを使う

開発はローカル、本番はGCEインスタンスを立てていますが、どちらもdocker-composeを使っています。ディレクトリ構成はこんな感じです。

```shell
.
├── .env  # ローカル用
├── db_init_local
│   └── db_init_local.sql  # ローカルで使うmysqlコンテナ初期化用
├── docker-compose.yml  # 本番(GCE)用
├── docker-compose_local.yml  # ローカル用
├── nginx  # nginxイメージ用。
│   ├── Dockerfile
│   └── default.template
└── scrapyd  # scrapydイメージ用。
    ├── Dockerfile
    ├── requirements.txt  # scrapyプロジェクトで必要なものすべて
    ├── scrapyd.conf  # bind_address = 0.0.0.0のみ記載
    └── start.sh
```

要点を列挙するとこんな感じです。

* 変数は `.env` から読み込む。ローカルでは上に記載したディレクトリ一覧にある.envを、GCEインスタンスではAnsibleのtemplateを使って配置した.envファイルを利用しています。
* DBの接続情報を環境変数で渡す。.envからの読み込み。
* JSを使ったサイトもスクレイピングできるようにseleniumコンテナも利用
* ずっと起動しておいてもらいたいので `restart: unless-stopped` を指定
* `./scrapyd/Dockerfile` では`CMD ["./start.sh"]`し、その中で`scrapyd`コマンドを実行しています。本番環境では、CloudSQLを利用するためのプロキシをstart.shの中で起動しているためです。
* Nginxの設定ファイルは後述するようにenvsubstで書き換え。
* これを下記のめちゃめちゃ長いコマンドで実行していますw 
  * 長くなっているのはdocker-compose自体を[docker run](https://cloud.google.com/community/tutorials/docker-compose-on-container-optimized-os)で動かしているため。
  * これは[Container-Optimized OS](https://cloud.google.com/container-optimized-os/docs/)というコンテナ実行用に最適化されたOSを利用しているからです。
  
  ```
  docker run --rm -d --name compose -v /var/run/docker.sock:/var/run/docker.sock -v "{{ docker_home }}:/rootfs{{ docker_home }}" -w="/rootfs{{ docker_home }}" docker/compose:1.13.0 up --build --force-recreate
  ``` 


```docker-compose.yaml
version: '3'
services:
  scrapyd:
    environment:
      DB_HOST: ${DB_HOST}
      DB_NAME: ${DB_NAME}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
    build:
      context: ./scrapyd
    image: scrapyd
    volumes:
    - ${LOGS}:/usr/src/app/logs
    - ${EGGS}:/usr/src/app/eggs
    - ${DBS}:/usr/src/app/dbs
    links:
    - selenium
    restart: unless-stopped
  selenium:
    image: selenium/standalone-chrome  # 公式イメージ
    ports:
    - "4444:4444"
    restart: unless-stopped
  nginx:
    environment:
      NGINX_SERVER_NAME: ${NGINX_SERVER_NAME}
    build:
      context: ./nginx
    links:
    - scrapyd
    ports:
    - "80:80"
    restart: unless-stopped
```
* scrapyd/Dockerfile
  * 特に変わったことはしていないです。

```
FROM python:3.6
WORKDIR /usr/src/app
COPY ./scrapyd.conf ./requirements.txt ./start.sh ./
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 6800  # Scrapydのデフォルト値
CMD ["./start.sh"]
```

* nginx/Dockerfile
  * nginxの設定ファイルでは環境変数をそのままでは利用できません。そこで、[envsubst](https://stackoverflow.com/questions/14155596/how-to-substitute-shell-variables-in-complex-text-files)を使うことで、.envに記載したNGINX_SERVER_NAMEをnginxの設定で利用しています。

```
FROM nginx:latest
COPY default.template /etc/nginx/conf.d/default.template
CMD ["/bin/bash", "-c", "envsubst '$$NGINX_SERVER_NAME' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
```

### ローカル用

ローカルでは下記のようにmysqlコンテナも利用しています。
実行コマンドは `docker-compose -f docker-compose_local.yaml up` です。

* ローカルではCloudSQLを使わないので `command: scrapyd` で起動時の処理を上書き
* mysqlコンテナは`/docker-entrypoint-initdb.d`に配置したsqlファイルやスクリプトを元にDBの初期化をします。必要なテーブルはここに置いたsqlで作成しています。

```diff
  version: '3'
  services:
    scrapyd:
++    command: scrapyd
      environment:
        DB_HOST: ${DB_HOST}
        DB_NAME: ${DB_NAME}
        DB_USER: ${DB_USER}
        DB_PASSWORD: ${DB_PASSWORD}
      build:
        context: ./scrapyd
      image: scrapyd
      volumes:
      - ${LOGS}:/usr/src/app/logs
      - ${EGGS}:/usr/src/app/eggs
      - ${DBS}:/usr/src/app/dbs
      links:
      - selenium
++    - mysqldb
    selenium:
      image: selenium/standalone-chrome
      ports:
      - "4444:4444"
    nginx:
      environment:
        NGINX_SERVER_NAME: ${NGINX_SERVER_NAME}
      build:
        context: ./nginx
      links:
      - scrapyd
      ports:
      - "80:80"
++  mysqldb:
++    image: mysql
++    ports:
++    - "3306:3306"
++    environment:
++      MYSQL_ROOT_PASSWORD: "password"
++      MYSQL_USER: ${DB_USER}
++      MYSQL_PASSWORD: ${DB_PASSWORD}
++      MYSQL_DATABASE: ${DB_NAME}
++    volumes:
++      - ./db_init_local:/docker-entrypoint-initdb.d
```

### その他の運用について

* Scrapyプロジェクトはリポジトリを分け、masterへのpush時に自動でScrapydサーバーにデプロイするようにしています。
* クローリングの実行はJenkinsの定期実行で`curl`を投げています。
  * [こちらの記事](https://qiita.com/inTheRye/items/06ceb938574ff9030080#scrapyd%E3%81%B8%E8%87%AA%E4%BD%9Cspider%E3%81%AE%E7%99%BB%E9%8C%B2)にあるscrapyd-clientですが、現在のバージョンv1.1.0だとまだまだ機能が貧弱です。(こちらの記事でもmasterから持ってきてますね)
  * いまのmasterにあるものがデプロイされたらやっと使えるかなという状況です。
* ScrapydはAnsibleでGCE上にデプロイしています。クローラ自体は日々動かしていますが、前回このコマンドを打ったのがいつか忘れてるくらいには安定的に稼働してくれています。