EC2で直接動かしていたクローラをECSに移行した話


# はじめに

pythonのスクレイピングフレームワークである[scrapy](https://scrapy.org/)を使って作成したクローラをEC2にdocker-composeをインストールしてcronで定期実行していました。
しかし、デプロイする時や、新しくspiderを作成したときにいちいちsshしてサーバに入って設定するのが面倒になってきたので、コンテナ管理ができるECSに移行してみたというお話です。

今回はECSのデプロイツールとしてecs-cliを採用したので、その時のECSの設定や、ecs-cliについて実際に触ってみたときのメモについてもまとめようと思います！:smile:

Let's hack!

# ECSってなんなの？

AWSのコンテナ管理サービスのことです。
詳しい概念や、仕組みについては別記事で結構まとめられていましたので、こちらを見ていただくのが良いかと思います！

[Amazon EC2 Container Service(ECS)の概念整理](https://qiita.com/NewGyu/items/9597ed2eda763bd504d7)

# 移行手順

## ECSに移行する前の構成

EC2上にdocker-composeをインストールしてcronで動かしていたので、開発用と本番用のdocker-compose.ymlを作成していました。

その時のdocker-compose.ymlが以下のような感じです。

```yaml
version: '3.1'
services:
  splash:
    restart: always
    image: scrapinghub/splash:2.3.3
    ports:
      - "5023:5023"
      - "8050:8050"
      - "8051:8051"
  mongo:
    restart: always
    image: mongo:3.4.5
    volumes:
      - ./data/mongo:/data/db
  redis:
    restart: always
    image: redis:3.2.9
    volumes:
      - ./data/redis:/data
    command: redis-server --appendonly yes
  scrapy:
    build: .
    environment:
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    volumes:
      - .:/root/dev
    depends_on:
      - splash
      - mongo
      - redis
    tty: true
    command: rq worker crawler scraper -u http://redis
```

[Pythonクローリング＆スクレイピング ―データ収集・解析のための実践開発ガイド―](https://amzn.to/2CHtccf)を参考にクローリングとスクレイピングをrqを使って分離しています。

本来ならば、クローリングとスクレイピングでコンテナを分けるべきですが、移行前の時点では分けていませんでした。
また、Dockerfileも一つだけで、volumesでソースを読み取り、 `docker-compose exec` コマンドで実行させるようにしていました。

アンチパターンだらけでしたが、一旦動くことを優先して放置していたところです。
しかし、ECSに移行するにあたり、ちゃんとしたイメージを作る必要があったためリファクタすることにしました:+1:

## ECSに移行するための準備

### ecs-compose.ymlの作成

ECSを使用するためには、タスク定義を作成する必要があります。
タスク定義とは、コンテナをどのようなDocker Imageを使い、どのようなコマンドで実行するのか、ポートは開けるのか、環境変数はどうするのかといったコンテナタスクを実行するために必要な情報を定義する概念になります。
タスク定義はAWSコンソール上から手動で作成することもできるのですが、結構めんどくさいです...:disappointed: 
また、docker-compose.ymlがほぼタスク定義の内容と同じというもありdocker-compose.ymlを再利用したい気持ちに駆られました。

調べてみると、[ecs-cli](https://github.com/aws/amazon-ecs-cli)というものがAWS公式で用意されており、これを使えばdocker-compose.ymlを元にタスク定義を作成する事ができそうです。

ecs-cliように作成したdocker-compose.ymlがこちらです。

```yaml
version: '2'
services:
  splash:
    image: scrapinghub/splash:2.3.3
    ports:
      - "5023:5023"
      - "8050:8050"
      - "8051:8051"
    logging:
      driver: awslogs
      options:
        awslogs-group: crawler
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: splash
  mongo:
    image: mongo:3.4.5
    ports:
      - "27017:27017"
    logging:
      driver: awslogs
      options:
        awslogs-group: crawler
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: mongo
  redis:
    image: redis:3.2.11-alpine
    command: redis-server --appendonly yes
    ports:
      - "6379:6379"
    logging:
      driver: awslogs
      options:
        awslogs-group: crawler
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: redis
  scraper:
    image: {aws-id}.dkr.ecr.ap-northeast-1.amazonaws.com/scraper
    environment:
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    links:
      - mongo:collec_mongo
      - redis:collec_redis
    logging:
      driver: awslogs
      options:
        awslogs-group: crawler
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: scraper
    command: rq worker crawler scraper -u http://redis
  crawler:
    image: {aws-id}.dkr.ecr.ap-northeast-1.amazonaws.com/crawler
    environment:
      ENV: production
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    logging:
      driver: awslogs
      options:
        awslogs-group: crawler
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: crawler
    links:
      - mongo:_mongo
      - redis:redis
      - splash:splash
      - scraper:scraper
```

2018年時点ではdocker-compose.ymlの2系にしか対応していなかったのと、log driverのような専用の指定が必要であったため、ecs-compose.ymlというecs-cli用のdocker-composeファイルを作成しました。

また、crawlerとscraperのタスクには自作のdocker imageを使用しています。
この自作のimageはどこかにアップロードしておかないと使用できません。
(自作のdocker imageについては次のセクションで触れたいと思います。)

大事なことはアップロードしたdocker imageのURIをecs-compose.ymlのimageに書くことを忘れないことです (若干ハマりましたw)。

### Docker imageの作成

ECSでタスク定義する際には、どのdocker imageを利用するかを明示する必要があります。
今回はAWSさんが提供しているECRを利用することにしました。

採用理由としては以下の感じです。

- 1GBまでは無料で使える
- AWSサービスなのでECSと連携しやすい気がした

AWSコンソール上でECRリポジトリを作成し、pushするだけなので手順は割愛させて頂きます:bow:

### ecs-cliを使ったタスク定義の作成

ようやく本題です。
ecs-cliを使ったタスク定義の作業手順が以下になります。

#### 1. ecs-cliのインストール

ドキュメントの手順に沿って設定するだけです。

1. [ここ](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI_installation.html)の手順に従ってecs-cliをインストール
2. [ここ](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI_Configuration.html)に従って、ecs-cliを設定

#### 2. クラスターの作成

以下のコマンドを実行すると、ecs-cliがECSクラスターを作成してくれます。
ECSクラスターで利用するEC2インスタンスも同時に立ててくれるのでかなり楽です:smile:
AWSコンソールでも作成できるのですが今回はecs-cliで作成します。
もし、AWSコンソールでクラスターを作成した場合には、`ecs-cli configure --cluster`でecs-cliとクラスタを紐付ける必要があります。

```bash
$ ecs-cli up --capability-iam --size 1 --instance-type t2.micro
```

#### 4. タスク定義の作成
以下のコマンドを実行すると、ecs-cliがecs-compose.ymlの内容を読み込んでタスク定義を作成してくれます。

```bash
$ ecs-cli compose -f ecs-compose.yml up
```

AWSコンソールで確認すると、ecs-compose.ymlで定義したコンテナが作成されていることが確認できます。

![img1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120358/ff00d037-6b5e-cdef-84ce-bcc1edce0a70.png)

### タスクのスケジューリング

タスクの実行については割愛します。
2018年時点ではecs-cliでスケジューリングを設定することはできなかったため、AWSコンソールで設定しました。

以下はタスクのスケジューリングのAWSコンソールでの設定手順です。

1. 先程作成したクラスターをAWSコンソール上で選択
  - 詳細画面が表示されるので、その中の「タスクのスケジュール」タブを選択し、作成ボタンを押下
2. スケジュール作成画面で、実行頻度と、実行したいタスクを入力
  - 先程作成したタスクを選択
  - 実行頻度はお好みで。今回は日時にしました (cron式書けます)

ちなみに、ECSのスケジュールはCloudWatch Eventsと連携してタスクの計画的実行を行っているようです。
スケジュール作成後にCloudWatch Eventsを見ると、ルールが追加されていることが確認できます。

![img2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120358/5e55e0ec-aab7-280b-fe0d-20cb8b6d073c.png)

## 所感

EC2上にdocker-composeインストールして、cronで実行していた時に比べると、sshでサーバに入ってデプロイする必要もなくなり、新しいタスクの定義もとても簡単にできるようになりました！:smile:

最近kubernetesが流行っているので、そちらも触ってみたいですね〜。
一応、ECSとkubernetesの連携もできるっぽいのですがECSもkubernetesも知識が足りず理解することには至りませんでした... :disappointed:

https://aws.amazon.com/jp/blogs/news/amazon-elastic-container-service-for-kubernetes/

## 詰まったところ

今回のECS移行をする時に詰まった内容を備忘録的にまとめておきます。

### メモリが足りなくてタスクが実行できない

ecs-compose.ymlから作成したタスク定義からタスクを実行させることができませんでした。
原因は、1コンテナのデフォルトメモリ割り当てが512MBだったため、t2.microではメモリが足りなかったためです。
対処法としては、インスタンスタイプを上げるか、タスク定義で使用できるメモリを調整することです。
今回は後者を採用することで解決することができました:smile:

### タスク定義が削除できない

AWSコンソールから要らなくなったタスク定義を削除しようとしてもできませんでした。
どうやら、タスク定義はInactiveにしておくことはできるが削除はできないようです。
この仕様は変わるかもしれない([ここの注記を参照](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/deregister-task-definition.html))ので一旦放置します。

### スケジュールした時間にタスクが実行されない？

毎日午前3時にタスクを実行する設定をしていたのですが、ログを見る限りずれた時間に実行されていました。
原因は簡単で、ECSから設定するタスクのスケジュールはUTC時間になるため9時間ずれてしまうことでした。
なので、cron式で書いていた時間を-9時間して解決です。
東京リージョンで使ってるんだから、いい感じに日本のタイムゾーンになると思っていたのが甘かったようです... :disappointed:

### コンテナが強制終了される？

これはまだ解決していないのですが、スクレイピング用のコンテナが強制終了される事象が発生していましたた(終了コードも127だし)。
これは予想ですが、クローリングとスクレイピングを2つのコンテナで分けて実行しているためので、クローリングの実行が先に終わると、スクレイピングの実行を待たずにコンテナを落としてしまっているのではないかと思います。
ちょっと調査する必要がありそうです。

=== 追記 ===

一つのタスク定義でクローリングとスクレイピングを実行しており、クローリングのコンテナをEssentialに指定していたため、クローリングの実行が終わったタイミングでタスク自体の実行が終わったと判定されてスクレイピングのコンテナが強制終了していたようです。
そもそも非同期でスクレイピングの実行をする目的で分離していたので、タスク定義自体をクローラーとスクレイピンで分離するのが正しい設計のようです。
ということで、クローリングとスクレイピングのタスクを分離し、ついでにQueueとして利用していたRedisも分離することできれいな設計になりました :smile:
タスク間の通信だけは若干難しさを感じたので、それはどこかで記事としてまとめようと思います。