EventBridgeScheduler+ECS+Docker+SeleniumでWebサイトのスクリーンショットを定期的に撮影する

EventBridgeScheduler, ECS, Docker, Seleniumを使い1時間ごとに阿部寛のHPのスクリーンショットを撮影しS3に保存する方法についてまとめました。

## スクレイピング用のDockerイメージを作成
以下の記事を参考にしながらスクレイピング用のDockerイメージを作成します。
環境変数からスクレイピング先のURLと保存先のバケットを読み取ります。

参考: [Selenium×dockerでテスト自動化してみた](https://zenn.dev/carenet/articles/4ca98b5e35bb24)

```
.
├── Dockerfile
└── app
    └── main.py
```

```dockerfile:Dockerfile
FROM --platform=linux/x86_64 python:3.12.1-alpine3.19

ENV PYTHONIOENCODING utf-8
WORKDIR /app

RUN apk add --update \
        wget \
    # Add chromium and dependences
        udev \
        ttf-freefont \
        chromium \
        chromium-chromedriver \
    # Add Japanese font
    && mkdir noto \
    && wget -P /app/noto https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip \
    && unzip /app/noto/NotoSansCJKjp-hinted.zip -d /app/noto \
    && mkdir -p /usr/share/fonts/noto \
    && cp /app/noto/*.otf /usr/share/fonts/noto \
    && chmod 644 -R /usr/share/fonts/noto/ \
    && fc-cache -fv \
    && rm -rf /app/noto \
    # Add selenium
    && pip install selenium \
    && pip install boto3

COPY ./app .

CMD [ "python", "main.py" ]
```

```python:main.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
import os
import shutil
import configparser
import datetime
import boto3


def save_screenshot(target_url: str, bucket_name: str):
    folder = '/opt/app'

    service = Service(executable_path=r'/usr/bin/chromedriver')
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--disable-gpu')
    options.add_argument('--window-size=1200x1800')
    prefs = {"download.default_directory" : folder}
    options.add_experimental_option("prefs",prefs)

    driver = webdriver.Chrome(service=service, options=options)

    driver.get(target_url)

    file_name = str(datetime.datetime.now()) + '.png'
    driver.save_screenshot(file_name)
    driver.quit()

    client = boto3.client('s3',region_name='ap-northeast-1')
    client.upload_file(file_name, bucket_name, file_name)


if __name__ == '__main__':
    target_url = os.environ['TARGET_URL']
    bucket_name = os.environ['BUCKET_NAME']
    save_screenshot(target_url, bucket_name)
```


## ECRの作成、イメージのプッシュ
スクレイピング用のDockerイメージ保存するためのECRのプライベートリポジトリを作成します。

![ECRの作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/7b290304-8fd6-cb01-0baa-be1ddf15ebbb.png)

Dockerイメージをビルドし、ECRリポジトリにプッシュします。

```
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {YOUR_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t web-crawler .
docker tag web-crawler:latest 397693451628.dkr.ecr.ap-northeast-1.amazonaws.com/web-crawler:latest
docker push {YOUR_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/web-crawler:latest
```


## ECS,IAMロールの作成
スクレイピング用のDockerイメージを動かすためのECSクラスター、タスク定義、IAMロールを作成します。

### ECSクラスターの作成

ECSクラスターを作成します。

![クラスターの作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/9dfbe863-db10-86c9-2734-a974385a5e1c.png)

### タスクIAMロールの作成
スクレイピングするタスクを動かすためのIAMロール（タスクロール）を作成します。
スクリーンショットをS3に保存する必要があるのでS3へのファイルアップロードを許可する権限を付与します。（画像中ではFullAccess権限を与えていますが、サンドボックス環境以外では適切な権限を設定してください）
作成した `web-crawer-task-role` をタスク定義設定時に使用します。

![タスクIAMロールの作成1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/b89c36fb-c88c-8932-ca92-8b518e758929.png)

![タスクIAMロールの作成2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/4bdbb923-e1c5-de73-ff7b-ebd2c9fa227c.png)

![タスクIAMロールの作成3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/c01ad972-b922-eb13-ea5c-a1ea5e9c684c.png)


### タスク定義の作成
スクレイピング用のタスク定義を作成します。
タスクロールの項に先ほど作成した `web-crawer-task-role` を設定し、コンテナの詳細欄にはECRのURIを設定します。
料金節約のためCPUとメモリサイズは調整しましたが他はデフォルトの設定を使用します。

![タスク定義の作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/3b0b7002-18a6-b809-e695-20b62f275757.png)


## S3バケットの作成
スクリーンショットを保存するための S3バケットを作成します。
`takoikatakotako-web-crawer-bucket` という名前で作成しました。


## EventBridgeSchedulerの作成
定期実行のためのEventBridgeSchedulerを作成します。
EventBridgeSchedulerを用いて定期的なタスクを実行し、オーバーライドオプションで環境変数の上書きを行います。

![EventBridgeSchedulerの作成1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/6b8addb0-3b9e-674f-6352-35dea18b44f6.png)

![EventBridgeSchedulerの作成2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/4f95f383-7ca8-9b3b-f5dd-2eb93d7a9237.png)

```
{
  "containerOverrides":[
    {
      "name":"ojichat",
      "environment":[
        {
          "name":"TARGET_URL",
          "value":"http://abehiroshi.la.coocan.jp/"
        },
        {
          "name":"BUCKET_NAME",
          "value":"takoikatakotako-web-crawer-bucket"
        }
      ]
    }
  ]
}
```

![EventBridgeSchedulerの作成3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/ab0869d4-dd2f-6c1d-3c5d-656c07cd18d2.png)

![EventBridgeSchedulerの作成4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/2b50b46f-9e0e-d7bc-0f9a-3622110c1bdf.png)

阿部寛のHPのスクリーンショットが保存されることを確認します。


## 詰まったところ
### ECSタスクを変更する場合、新しいロールを作成する必要がある
EventBridgeSchedulerで生成されるIAMロールのポリシーにECSタスクのARNが含まれるため、コンソールからECSタスクを更新すると新しいロールを作成する必要があります。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecs:RunTask"
            ],
            "Resource": [
                "arn:aws:ecs:ap-northeast-1:397693451628:task-definition/web-crawer-task-definition:*",
                "arn:aws:ecs:ap-northeast-1:397693451628:task-definition/web-crawer-task-definition"
            ],
            "Condition": {
                "ArnLike": {
                    "ecs:cluster": "arn:aws:ecs:ap-northeast-1:397693451628:cluster/web-crawler-cluster"
                }
            }
        }
    ]
    ...
}
```

### デバッグにCloudTrailが必要な場合がある
上手く動かない場合、CloudTrailを使用してデバッグする必要があります。
CloudTrail イベント履歴から RunTask などを確認する必要があります。

![CloudTrail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/89cd0bb1-9c13-f305-14b2-0d1366757c0c.png)