AWS FargateでSeleniumを使い定期的にチェックする環境を作ってみた

## TL;DR (5行)
- 頻繁に手動で更新するようなWebページ（今回は例としてZOZOTOWNを使います）を自動チェックしたい
- Fargateを使ってSeleniumの実行環境を作ります
- ローカルのコンテナをECRにpushしてFargateにデプロイします
- Seleniumによるブラウザ操作をスケジュール設定し，バッチ処理として動作させます
- バッチ処理の結果は，CloudWatchにてチェック

### 注意
本記事はFargate+Selenium導入紹介を目的としています．
筆者は内定者で許可もとっているので，題材として自社サービスのZOZOTOWNを使ってます！
もし，記事内容を流用する場合はマナーや規約に違反しない範囲でお願いします！

## 使用するサービスの説明
### Fargateとは
通常コンテナをEC2で運用する場合，インスタンス管理する必要がありますが，Fargateの場合，インスタンス管理はAmazon側に任せ，コンテナを登録するだけでサーバレスにコンテナを動作させることができるサービスです．

サーバレスで有名なサービスとしてLambdaがありますが，こちらはコンテナを使えなかったり，タイムアウトなどの制約から柔軟性に欠ける部分があります．

一方で，Fargateはローカルで実行しているコンテナをそのまま登録して使えるため，多様なサービスの提供が可能です．

### Seleniumとは
Webアプリのテストを自動化するためのブラウザ駆動型テストツールです．

PythonやRuby，Javaなど多様な言語に対応しており，簡単にテストスクリプトを作成することができます．

## アーキテクチャ
今回は，AWS上に以下のようなアーキテクチャを構築します．

<img width="575" alt="スクリーンショット 2020-01-03 11.00.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/5048d6c6-bd68-5799-a688-53acbfe2e423.png">


## Dockerfileとメインコードの作成
Selenium + Pythonでテストスクリプトを作成します．

### Dockerfileの作成

```dockerfile:./Dockerfile
FROM joyzoursky/python-chromedriver:3.8-alpine3.10-selenium

WORKDIR /usr/src
ADD main.py /usr/src

CMD ["python", "main.py"]
```
今回，Selenium+Headless Chromeを使うにあたって，
ベースイメージはこちらを使っています．

joyzoursky/python-chromedriver:3.7-alpine3.8-selenium
https://hub.docker.com/r/joyzoursky/python-chromedriver/

### メインコードの作成

```python:./main.py
# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.common.exceptions import TimeoutException, ElementClickInterceptedException, NoSuchElementException

def check_coupon(driver, my_favorite_brand):
    # ZOZOのクーポンページに遷移
    driver.get("https://zozo.jp/coupon/")
    i = 1
    while True:
      try:
          coupon_brand = driver.find_element_by_xpath(f'//*[@id="body"]/div[3]/ul/li[{i}]/a/figure/div[2]').text
          if coupon_brand == my_favorite_brand:
              return True
          i += 1
      except NoSuchElementException:
          return False

if __name__ == '__main__':
    try:
        # Headless Chromeの設定
        options = webdriver.ChromeOptions()
        options.add_argument('--no-sandbox')
        options.add_argument("--disable-setuid-sandbox")
        options.add_argument('--window-size=1420,1080')
        options.add_argument('--headless')
        options.add_argument('--disable-gpu')
        # Headless Chromeブラウザに接続
        driver = webdriver.Chrome(options=options)
        # seleniumの動作タイムアウトを15秒間に設定
        driver.implicitly_wait(15)

        # 好きなブランド
        my_favorite_brand = "Carlie e felice"
        # クーポンのチェック
        if check_coupon(driver, my_favorite_brand):
            print("見つけたよ！ ", my_favorite_brand)
        else:
            print("今日は見つけられなかった・・・")

    # 例外処理
    except ElementClickInterceptedException as ecie:
        print(f"exception!\n{ecie}")
    except TimeoutException as te:
        print(f"timeout!\n{te}")
    finally:
        # 終了
        driver.close()
        driver.quit()
```

クーポンページにブランド：**Carlie e felice**があるかどうかチェックします．

Requests + Beautiful Soup 4でスクレピングしても良かったのですが，今回はSeleniumを使った環境を構築したかったので，まさかり投げないでください；；

### ローカル環境でコンテナの実行

```bash
# コンテナの構築
$ docker build -t zozo_check_coupons .

# コンテナの実行
$ docker run -it --rm zozo_check_coupons
見つけたよ！  Carlie e felice
```

無事，ローカル環境で実行できたことを確認したら，次はこのコンテナをAmazon ECRにプッシュします．

ECRはAWS上のプライベートなDocker Hubみたいなイメージです．

## AWSの必要環境の構築
### ECRの環境を構築
#### ECRのリポジトリ作成
ECRに今回管理したいコンテナ専用のリポジトリを作成します．

- サービスから，**ECR**を選択し，**リポジトリの作成**を選択
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/a9913d19-36ba-6764-33ea-28899f5f7890.png)

- リポジトリ名「zozo_check_coupons」を入力し，リポジトリを作成
<img width="986" alt="スクリーンショット 2020-01-02 21.21.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/a4cdf4fe-2c67-3f2d-a9c3-2e397d7ca399.png">

- 無事，リポジトリを作成することができました
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/c1505465-7768-e34d-3d34-177692cf8747.png)

このときの，リポジトリのURIはコンテナをプッシュするときに，使うのでメモしておきましょう．

#### ECRにログイン

```bash
$ aws ecr get-login --region ap-northeast-1 --no-include-email
docker login -u AWS -p ...
.
.
. .dkr.ecr.ap-northeast-1.amazonaws.com

# 返ってきたdocker login ~をコピペして打ち込む
$ docker login -u AWS -p ...
Login Succeeded
```
Login Succeededと表示されたらOKです

#### ECRにプッシュ
先程，メモしたリポジトリのURLをコピペし，作成したリポジトリにプッシュします

```bash
# リポジトリのURIでタグを付ける
$ docker build -t xxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/zozo_check_coupons .
# タグをつけたコンテナをECRにプッシュする
$ docker push xxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/zozo_check_coupons
```

コンテナを無事リポジトリにプッシュできました
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/d4c26741-8bfb-95c2-2bb6-495279e40fe1.png)

イメージのURIは，タスク定義で使うので，メモしておきましょう

### ECSでクラスター作成
コンテナを動作させる環境であるクラスターを作ります

- サービスから**ECS**を選択し，**クラスターの作成**を選択します．
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/f0bc5b7b-8e18-faf2-c67e-a0fcd96abf52.png)

- クラスターテンプレート「ネットワーキングのみ」を選択
<img width="830" alt="スクリーンショット 2020-01-02 22.31.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/3e5dd538-87ec-826e-6ea8-2bd46a0ea19c.png">

- クラスター名を入力し，VPCの作成にチェックします．
<img width="849" alt="スクリーンショット 2020-01-02 22.33.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/50d10740-57a3-8c4e-a0d5-8550e93a78d7.png">

- 最後に，作成ボタンを押せばクラスターが作成できます
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/fbe839e4-4309-49c1-b1bd-1bd1126c5b09.png)

### ECSでタスクの定義
次に，タスクの定義をします．

- **新しいタスクの定義の作成**を選択します
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/9506e40f-ceec-d366-ee7e-b3345284bc00.png)

- 起動タイプの互換性の選択で**Fargate**を選択
<img width="909" alt="スクリーンショット 2020-01-02 22.56.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/e34c147a-f887-e3a4-e383-1564ae1916f3.png">

- 以下のようにタスクを定義します
<img width="938" alt="スクリーンショット 2020-01-02 22.57.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/6e8d1c8f-c002-4c93-72cb-d0c36aab62de.png">

タスク実行ロールが何もない場合は，下の余談を参照し，作成してください

- コンテナ追加を選択し，コンテナ名と先程プッシュしたコンテナイメージのURIをここでコピペします
<img width="941" alt="スクリーンショット 2020-01-02 22.59.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/250675d4-6a54-a530-2702-e92a398f3906.png">

- メモリとCPUを設定
<img width="672" alt="スクリーンショット 2020-01-03 01.07.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/ee46b7b7-056b-d92f-e173-ffba8df1872c.png">


- 最後に，**作成**を選択してタスク定義完了です
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/4f8a79e7-af78-f2dc-70fd-82cc00d7c3e6.png)


### 余談 : CLIでタスク定義
- タスク実行に必要なロールを定義

```bash:./task-execution-assume-role.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

- 定義ファイルを用いて，ロールの作成

```bash
$ aws iam --region ap-northeast-1 create-role --role-name ecsTaskExecutionRole --assume-role-policy-document file://task-execution-assume-role.json
```

- タスクの定義ファイルを作成

```./task-config.json
{
  "family": "zozo-check-coupons-task",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "zozo-check-coupons-task",
      "image": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/zozo_check_coupons:latest",
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "ap-northeast-1",
          "awslogs-group": "/ecs/zozo_check_coupons-task",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::xxxxxxxx:role/ecsTaskExecutionRole"
}
```

- 定義ファイルをもとにタスクを作成

```bash
$ aws ecs register-task-definition --cli-input-json file://task-config.json
```

これで，打ち間違い，漏れなくタスクを定義することができます

詳しくは，こちらを参照ください
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html

### スケジュール実行の作成
次に，いよいよ定義したタスクをスケジュールで実行していきます．

- 作成したクラスターを選択し，**タスクのスケジューリング**を選択，そして**作成**を押します

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/560a97ed-9581-6780-0a30-81a42c16add1.png)

- 以下のように設定しました．固定時間は，クーポン更新が24時間だったので**24**に設定しました
<img width="1134" alt="スクリーンショット 2020-01-02 23.05.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/f0db8c19-b2e2-a875-6d50-ae44b9b2f394.png">

- クラスター作成時に，作成したVPCを選択します
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/d15828aa-272f-5ec5-1f67-97de45597c33.png)

- 最後に，作成を選択すると，タスクが実行されます
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/768cc6fe-2f6c-27b9-3b4a-107869aa6a5a.png)

## CloudWatchでログをチェック
タスクが完了すると，CloudWatchの方にログが送られます

確認すると，以下のようなログがありました！

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/c418b8f2-692a-0d0f-9b64-7c448d09c606.png)

今日は無いみたいですね．．．

## おわりに
Fargate + Seleniumの環境を作りました！
Fargateはコンテナを使って動かしたコンテナをそのまま登録できるので，かなり柔軟性はいいです．

ですが，クローリングをする場合，CPUやメモリにページの読み込みが遅くなり，プログラムによるブラウザ操作がうまく行かなかったり，タイムアウトが発生してしまう場合があるので，sleepを入れる等の対策は十分にしたほうが良さそうです．

最後に，本記事は導入目的で紹介しているので，流用する場合はマナーや規約をよく読んでからお願いします！

## 参考文献
https://yomon.hatenablog.com/entry/2019/08/fargateselenium