AWS
docker

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

はじめに

pythonのスクレイピングフレームワークである、scrapyを使って作成したクローラをEC2にdocker-composeをインストールしてcronで定期実行していた。
しかし、デプロイするときや、新しくspiderを作成したときに、いちいちsshしてサーバに入って設定するのが面倒だったので、コンテナ管理ができるECSに移行してみた。
その時のECSの設定とか、ecs-cliについて実際に触ってみたときのメモをまとめる。

ECSってなんなの?

AWSのコンテナ管理サービスでDockerに対応している。
ECSがやっていることは、dockerが入っているAMIからEC2インスタンスを作成して、その上でdockerを動かしている。
詳しい概念や、仕組みについては別記事で結構まとめられたたので参考までに。

Amazon EC2 Container Service(ECS)の概念整理

移行手順

ECSに移行する前の構成

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

その時のdocker-compose.ymlがこんな感じ(collecというのはクローラのmodule名)。

version: '3.1'
services:
  splash:
    restart: always
    image: scrapinghub/splash:2.3.3
    container_name: collec_splash
    ports:
      - "5023:5023"
      - "8050:8050"
      - "8051:8051"
  mongo:
    restart: always
    image: mongo:3.4.5
    container_name: collec_mongo
    volumes:
      - ./data/mongo:/data/db
  redis:
    restart: always
    image: redis:3.2.9
    container_name: collec_redis
    volumes:
      - ./data/redis:/data
    command: redis-server --appendonly yes
  scrapy:
    build: .
    container_name: collec_scrapy
    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://collec_redis

Pythonクローリング&スクレイピング ―データ収集・解析のための実践開発ガイド―を参考にクローリングとスクレイピングをrqを使って分離している。

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

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

ECSに移行するための準備

ecs-compose.ymlの作成

ECSを使用するためには、タスク定義を作成する必要がある。
タスク定義はAWSコンソール上から手動で作成することもできるが、結構めんどくさいので、docker-compose.ymlを使ってタスク定義を作成したかった。
調べてみると、ecs-cliというものが用意されており、これを使えばdocker-compose.ymlを元にタスク定義を作成する事ができる。
しかし、docker-compose.ymlの2系にしか対応していなかったのと、log driverのような専用の指定が必要だったため、ecs-cli用にecs-compose.ymlを作成した。

作成したecs-compose.ymlがこちら(collecはモジュール名)

version: '2'
services:
  splash:
    image: scrapinghub/splash:2.3.3
    ports:
      - "5023:5023"
      - "8050:8050"
      - "8051:8051"
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: splash
  mongo:
    image: mongo:3.4.5
    ports:
      - "27017:27017"
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        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: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: redis
  scraper:
    image: {aws-id}.dkr.ecr.ap-northeast-1.amazonaws.com/collec/scraper
    environment:
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    links:
      - mongo:collec_mongo
      - redis:collec_redis
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: scraper
    command: rq worker crawler scraper -u http://collec_redis
  crawler:
    image: {aws-cli}.dkr.ecr.ap-northeast-1.amazonaws.com/collec/crawler
    environment:
      ENV: production
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: crawler
    links:
      - mongo:collec_mongo
      - redis:collec_redis
      - splash:collec_splash
      - scraper:collec_scraper

crawlerとscraperのタスクには自作のdocker imageを使用している。
次に書くが、自作のimageはどこかに上げておかないと使用できない。
上げたimageのURIをecs-compose.ymlのimageに書くことを忘れないこと。

Docker imageの作成

ECSでタスク定義する際には、docker imageをpullしてくるため、どこかにimageをpushしておく必要がある。
1GBまでは無料で使えるので、今回はECRを使用することにした。

AWSコンソール上でECRリポジトリを作成し、pushするだけなので手順は割愛。

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

  1. ecs-cliのインストール
    まず、ここの手順に従ってecs-cliをインストールする。

  2. ecs-cliの設定
    次に、ここに従って、ecs-cliの設定を行う。ただ、aws-cliの設定がされているのであれば必要ないので飛ばしても良い。

  3. クラスターの作成
    以下のコマンドを実行すると、ECS上にクラスターが作成される。AWSコンソールでも作成できるが今回はecs-cliで作成する。AWSコンソールでクラスターを作成した場合には、ecs-cli configure --clusterでecs-cliとクラスタを紐付ける必要があるので注意すること。

    $ ecs-cli up --capability-iam --size 1 --instance-type t2.micro
    
  4. タスク定義の作成
    以下のコマンドを実行するとタスク定義が作成される。

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

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

image.png

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

タスクの実行については割愛する。
AWSコンソールで実際に色々触って試して欲しい。

タスクのスケジューリングは非常に簡単だった。
先程作成したクラスターをAWSコンソール上で選択する。
詳細画面が表示されるので、その中の「タスクのスケジュール」タブを選択し、作成ボタンを押す。

スケジュール作成画面では、実行頻度と、実行したいタスクを入力する必要がある。
先程作成したタスクを選択し、実行頻度は日時にする。cron式で書くと、実行時間も指定できるので、細かく指定したい場合はそちらを選択すると良い。

ちなみに、このスケジュールは内部でCloudWatch Eventsを使用している。
先程のスケジュール作成後、CloudWatch Eventsを見ると、ルールが追加されていることが確認できる。

image.png

所感

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

最近kubernetesが流行っているので、そちらも触ってみたい。
一応、ECSとkubernetesの連携もできるっぽい。

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

詰まったところ

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

ecs-compose.ymlから作成したタスク定義からタスクを実行させることができなかった。
原因は、1コンテナのデフォルトメモリ割り当てが512MBだったため、t2.microではメモリが足りず、エラーになってしまうためであった。
インスタンスタイプを上げるか、タスク定義で使用できるメモリの上限を988MBに設定することで、解決することができた(今回は後者を手動でやっている)。

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

AWSコンソールから要らなくなったタスク定義を削除しようとしてもできなかった。
どうやら、Inactiveにしておくことはできるが削除はできないようだった。
この仕様は変わるかもしれないので一旦放置(ここの注記を参照)。

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

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

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

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