はじめに
pythonのスクレイピングフレームワークであるscrapyを使って作成したクローラをEC2にdocker-composeをインストールしてcronで定期実行していました。
しかし、デプロイする時や、新しくspiderを作成したときにいちいちsshしてサーバに入って設定するのが面倒になってきたので、コンテナ管理ができるECSに移行してみたというお話です。
今回はECSのデプロイツールとしてecs-cliを採用したので、その時のECSの設定や、ecs-cliについて実際に触ってみたときのメモについてもまとめようと思います!
Let's hack!
ECSってなんなの?
AWSのコンテナ管理サービスのことです。
詳しい概念や、仕組みについては別記事で結構まとめられていましたので、こちらを見ていただくのが良いかと思います!
Amazon EC2 Container Service(ECS)の概念整理
移行手順
ECSに移行する前の構成
EC2上にdocker-composeをインストールしてcronで動かしていたので、開発用と本番用のdocker-compose.ymlを作成していました。
その時のdocker-compose.ymlが以下のような感じです。
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クローリング&スクレイピング ―データ収集・解析のための実践開発ガイド―を参考にクローリングとスクレイピングをrqを使って分離しています。
本来ならば、クローリングとスクレイピングでコンテナを分けるべきですが、移行前の時点では分けていませんでした。
また、Dockerfileも一つだけで、volumesでソースを読み取り、 docker-compose exec
コマンドで実行させるようにしていました。
アンチパターンだらけでしたが、一旦動くことを優先して放置していたところです。
しかし、ECSに移行するにあたり、ちゃんとしたイメージを作る必要があったためリファクタすることにしました
ECSに移行するための準備
ecs-compose.ymlの作成
ECSを使用するためには、タスク定義を作成する必要があります。
タスク定義とは、コンテナをどのようなDocker Imageを使い、どのようなコマンドで実行するのか、ポートは開けるのか、環境変数はどうするのかといったコンテナタスクを実行するために必要な情報を定義する概念になります。
タスク定義はAWSコンソール上から手動で作成することもできるのですが、結構めんどくさいです...
また、docker-compose.ymlがほぼタスク定義の内容と同じというもありdocker-compose.ymlを再利用したい気持ちに駆られました。
調べてみると、ecs-cliというものがAWS公式で用意されており、これを使えばdocker-compose.ymlを元にタスク定義を作成する事ができそうです。
ecs-cliように作成したdocker-compose.ymlがこちらです。
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するだけなので手順は割愛させて頂きます
ecs-cliを使ったタスク定義の作成
ようやく本題です。
ecs-cliを使ったタスク定義の作業手順が以下になります。
1. ecs-cliのインストール
ドキュメントの手順に沿って設定するだけです。
2. クラスターの作成
以下のコマンドを実行すると、ecs-cliがECSクラスターを作成してくれます。
ECSクラスターで利用するEC2インスタンスも同時に立ててくれるのでかなり楽です
AWSコンソールでも作成できるのですが今回はecs-cliで作成します。
もし、AWSコンソールでクラスターを作成した場合には、ecs-cli configure --cluster
でecs-cliとクラスタを紐付ける必要があります。
$ ecs-cli up --capability-iam --size 1 --instance-type t2.micro
4. タスク定義の作成
以下のコマンドを実行すると、ecs-cliがecs-compose.ymlの内容を読み込んでタスク定義を作成してくれます。
$ ecs-cli compose -f ecs-compose.yml up
AWSコンソールで確認すると、ecs-compose.ymlで定義したコンテナが作成されていることが確認できます。
タスクのスケジューリング
タスクの実行については割愛します。
2018年時点ではecs-cliでスケジューリングを設定することはできなかったため、AWSコンソールで設定しました。
以下はタスクのスケジューリングのAWSコンソールでの設定手順です。
- 先程作成したクラスターをAWSコンソール上で選択
- 詳細画面が表示されるので、その中の「タスクのスケジュール」タブを選択し、作成ボタンを押下
- スケジュール作成画面で、実行頻度と、実行したいタスクを入力
- 先程作成したタスクを選択
- 実行頻度はお好みで。今回は日時にしました (cron式書けます)
ちなみに、ECSのスケジュールはCloudWatch Eventsと連携してタスクの計画的実行を行っているようです。
スケジュール作成後にCloudWatch Eventsを見ると、ルールが追加されていることが確認できます。
所感
EC2上にdocker-composeインストールして、cronで実行していた時に比べると、sshでサーバに入ってデプロイする必要もなくなり、新しいタスクの定義もとても簡単にできるようになりました!
最近kubernetesが流行っているので、そちらも触ってみたいですね〜。
一応、ECSとkubernetesの連携もできるっぽいのですがECSもkubernetesも知識が足りず理解することには至りませんでした...
詰まったところ
今回のECS移行をする時に詰まった内容を備忘録的にまとめておきます。
メモリが足りなくてタスクが実行できない
ecs-compose.ymlから作成したタスク定義からタスクを実行させることができませんでした。
原因は、1コンテナのデフォルトメモリ割り当てが512MBだったため、t2.microではメモリが足りなかったためです。
対処法としては、インスタンスタイプを上げるか、タスク定義で使用できるメモリを調整することです。
今回は後者を採用することで解決することができました
タスク定義が削除できない
AWSコンソールから要らなくなったタスク定義を削除しようとしてもできませんでした。
どうやら、タスク定義はInactiveにしておくことはできるが削除はできないようです。
この仕様は変わるかもしれない(ここの注記を参照)ので一旦放置します。
スケジュールした時間にタスクが実行されない?
毎日午前3時にタスクを実行する設定をしていたのですが、ログを見る限りずれた時間に実行されていました。
原因は簡単で、ECSから設定するタスクのスケジュールはUTC時間になるため9時間ずれてしまうことでした。
なので、cron式で書いていた時間を-9時間して解決です。
東京リージョンで使ってるんだから、いい感じに日本のタイムゾーンになると思っていたのが甘かったようです...
コンテナが強制終了される?
これはまだ解決していないのですが、スクレイピング用のコンテナが強制終了される事象が発生していましたた(終了コードも127だし)。
これは予想ですが、クローリングとスクレイピングを2つのコンテナで分けて実行しているためので、クローリングの実行が先に終わると、スクレイピングの実行を待たずにコンテナを落としてしまっているのではないかと思います。
ちょっと調査する必要がありそうです。
=== 追記 ===
一つのタスク定義でクローリングとスクレイピングを実行しており、クローリングのコンテナをEssentialに指定していたため、クローリングの実行が終わったタイミングでタスク自体の実行が終わったと判定されてスクレイピングのコンテナが強制終了していたようです。
そもそも非同期でスクレイピングの実行をする目的で分離していたので、タスク定義自体をクローラーとスクレイピンで分離するのが正しい設計のようです。
ということで、クローリングとスクレイピングのタスクを分離し、ついでにQueueとして利用していたRedisも分離することできれいな設計になりました
タスク間の通信だけは若干難しさを感じたので、それはどこかで記事としてまとめようと思います。