CSVファイルをAWS DynamoDBに自動更新する

#はじめに
私はよく、スクレイピングで収集したデータをCSVファイルで出力しています。
しかし、データを効率よく使うには、CSVファイル内のデータをデータベースなどにストアしなくてはいけません。
せっかくデータ収集を省力化しても、データベースの運用に時間を取られるのは苦痛ですよね？
その悩み、クラウドのデータベースサービスで解決しましょう！

本投稿では、AWS の DynamoDB にCSVファイル内のデータを自動で反映する方法をご紹介します。
これで煩わしいデータベース更新業務とはオサラバです！

#目次

- [AWS CLI の設定](#aws-cli-の設定)
- [DynamoDBのテーブル更新用コード （Python）](#dynamodbのテーブル更新用コード-python)
- [方法1：仮想マシン(VM)からcronで定期実行](#方法1仮想マシンvmからcronで定期実行)
- [方法2：GitHub Actions](#方法2github-actions)
- [二つの方法の比較](#二つの方法の比較)

今回は、**既にAWSで自動更新したいDynamoDBが存在**し、スクレイピングなどで取得したCSVファイルでそのファイルを更新したい場合を想定します。

#AWS CLI の設定
AWSに接続するために、AWS CLI をインストールして初期設定を行います。
表示に従い、各種設定を登録すればOKです。
アクセスキーとシークレットアクセスキーは、AWSのセキュリティ認証ページから「アクセスキーの作成」を実行して用意してください。
参考：[AWS CLIのインストールから初期設定メモ](https://console.aws.amazon.com/iam/home?#/security_credentials)

```コマンド
$ sudo pip install awscli
$ aws configure

AWS Access Key ID [None]: {アクセスキー}
AWS Secret Access Key [None]: {シークレットアクセスキー}
Default region name [None]: {AWSのリージョン}
Default output format [None]: {json, yaml, text, table から出力形式を選ぶ}
```
設定の確認は`aws configure list`で行います。

#DynamoDBのテーブル更新用コード （Python）
例えば、スクレイピングなどで取得した下記のようなCSVファイルがあると仮定します。

```揚げもの.csv
メンチカツ,パン粉,ひき肉,母
エビフライ,パン粉,エビ,母
エビ天,天ぷら粉,エビ,母
イモ天,天ぷら粉,サツマイモ,母
大福,もち,あんこ,母
```

これを用いて、以下のDynamoDBを更新します

|料理（主キー）|衣|中身|料理人|
|---|---|---|---|
|メンチカツ|パン粉|ひき肉|母|

DynamoDBにCSVの内容を**追記**するには、下記の更新用コードを実行します。

```addDynamoDB.py
import csv
import boto3
from boto3.dynamodb.conditions import Key

def addDynamoDB():
    dynamodb = boto3.resource("dynamodb", region_name="データベースのリージョン")
    table = dynamodb.Table("DynamoDBテーブル名") # テーブル名を指定して変数に格納

    filepath = "揚げもの.csv"
    with open(filepath, "r", encoding="utf-8") as f:
        reader = csv.reader(f)
        # batch_writer()で、25項目ずつCSVの全てをputする
        with table.batch_writer() as batch:
            for row in reader:
                item = {
                    "料理": row[0],
                    "衣": row[1],
                    "中身": row[2],
                    "料理人": row[3],
                }
                batch.put_item(Item=item)

if __name__ == "__main__":
    addDynamoDB()
```

**addDynamoDB.py 実行後のデータベース**

|料理（主キー）|衣|中身|料理人|
|---|---|---|---|
|メンチカツ|パン粉|ひき肉|母|
|エビフライ|パン粉|エビ|母|
|エビ天|天ぷら粉|エビ|母|
|イモ天|天ぷら粉|サツマイモ|母|
|大福|もち|あんこ|母|

これでDynamoDBの更新ができるようになりました。

しかしここで問題があります。大福は揚げ物ではないですよね？
`table.batch_writer()`はDynamoDBの追記はできますが、項目の削除はできません。
例えば、揚げもの.csvを下記のように修正しても、addDynamoDB.pyではDynamoDBに変化はありません。

```揚げもの.csv
メンチカツ,パン粉,ひき肉,母
エビフライ,パン粉,エビ,母
エビ天,天ぷら粉,エビ,母
イモ天,天ぷら粉,サツマイモ,母
```

DynamoDBとCSVの内容を**同期**するには、下記の更新用コードを実行します。

```updateDynamoDB.py
import csv
import boto3
from boto3.dynamodb.conditions import Key

def updateDynamoDB():
    dynamodb = boto3.resource("dynamodb", region_name="データベースのリージョン")
    table = dynamodb.Table("DynamoDBテーブル名") # テーブル名を指定して変数に格納

    # DynamoDBに対してクエリを実行、データを取得する
    response = table.query(
        IndexName="DynamoDBインデックス名", # インデックス名の指定
        # その他オプションがあれば以下に記入する
        # 例: テーブルの「料理人」が「母」の要素のみを取得したい場合、以下を設定する
        KeyConditionExpression=Key("料理人").eq("母") # 例
    )
    dbItems = response['Items']
    # レスポンスが1MBを超えた場合、LastEvaluatedKeyが含まれなくなるまでループする
    while 'LastEvaluatedKey' in response:
        response = table.query(
            IndexName="DynamoDBインデックス名", # インデックス名の指定
            ExclusiveStartKey=response['LastEvaluatedKey'],
            # その他オプションがあればここに記入する
            # 例: テーブルの「料理人」が「母」の要素のみを取得したい場合、以下を設定する
            KeyConditionExpression=Key("料理人").eq("母") # 例
        )
        dbItems.extend(response['Items'])

    csvItems = []
    filepath = "揚げもの.csv"
    with open(filepath, "r", encoding="utf-8") as f:
        reader = csv.reader(f)
        # batch_writer()で、25項目ずつCSVの全てをputする
        with table.batch_writer() as batch:
            for row in reader:
                item = {
                    "料理(主キー)": row[0],
                    "衣": row[1],
                    "中身": row[2],
                    "料理人": row[3],
                }
                batch.put_item(Item=item)

                csvItems.append(row[0])

            # CSVファイルに存在しない型番は、batch.delete_item()でDBからdeleteする
            for dbItem in dbItems:
                if dbItem["料理(主キー)"] not in csvItems:
                    batch.delete_item(
                        Key={
                            "料理(主キー)": dbItem["料理(主キー)"],
                        }
                    )

if __name__ == "__main__":
    updateDynamoDB()
```
**updateDynamoDB.py 実行後のデータベース**

|料理（主キー）|衣|中身|料理人|
|---|---|---|---|
|メンチカツ|パン粉|ひき肉|母|
|エビフライ|パン粉|エビ|母|
|エビ天|天ぷら粉|エビ|母|
|イモ天|天ぷら粉|サツマイモ|母|

これで、コードを手動で実行すればDynamoDBが更新できるようになりました。
では、これを自動化するにはどうすれば良いでしょうか？
いくつか方法はありますが、今回はそのうちオススメの二つをご紹介させていただきます。


#方法1：仮想マシン(VM)からcronで定期実行
クラウド上で、AWS CLI など環境を整えたVMインスタンスを立ち上げるのが一つの方法です。
VMインスタンスは、過去の記事でも取り扱ったGoogle Cloud Engine の無料枠を使います。
過去の記事：[クラウドで手軽に始めるクローリング](https://qiita.com/Moh_no/items/e8902ec40df7ddf9448a)

VMインスタンス上に[上記のコード](#dynamodbのテーブル更新用コード-python)を上げて、cronで定期実行しましょう。
cronは`crontab -u < ユーザ名 > -e`コマンドで以下ように設定してください。

```
SHELL=/bin/bash
CRON_TZ="Japan"
00 00 01 * * python3 /< 任意の絶対パス >/updateDynamoDB.py
```
`00 00 01 * *`の部分は、DynamoDBの更新をしたい任意の時間と頻度に変更しましょう。

[前回の記事](https://qiita.com/Moh_no/items/e8902ec40df7ddf9448a)のようなクローラーがある場合、クローラーの実行が終了次第、DynamoDB への反映が実行されるように実装すると便利です。


#方法2：GitHub Actions
CSVファイルをGitHub上で管理することで、git push時に自動でDynamoDBが更新される実装も可能です。
具体的には、GitHub Actions のワークフローで実行します。

Git リポジトリに`.github/workflows/`ディレクトリを作成して、その中にYAML形式で定義してください。
YAMLファイルは例えば以下のように記述します。

```dynamoDB.yml
name: 任意のAction名

on:
  push:
    branches:
      # Run only on changes on master branches
      - 'master'
    paths:
      # Run only on changes on these files
      - '**/揚げもの.csv'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
      with:
        fetch-depth: 25
    - name: Python3.8の設定
      uses: actions/setup-python@v2
      with:
        python-version: 3.8
    - name: Pythonライブラリのインストール
      run: |
        python -m pip install --upgrade pip
        pip install boto3
    - name: AWS認証情報の設定
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: 任意のリージョン
    - name: DynamoDBの更新
      run: |
        python /< 任意の絶対パス >/updateDynamoDB.py
```
これでmasterブランチにpushされたことをトリガーに、`updateDynamoDB.py`が実行されます。

YAMLファイルの記法について、GitHub公式と併せて下記の記事が特に参考になりました。ありがとうございます。
参考：[Github Actionsの使い方メモ](https://qiita.com/HeRo/items/935d5e268208d411ab5a)
GitHubアクションの「AWS認証情報の設定」については、詳しいことは[GitHub公式ページ](https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actions)で確認できます。


#二つの方法の比較
||VMからの実行（GCE）|GitHub Actions|
|:-:|:-:|:-:|
|**ファイルのタイムスタンプ**|有り|無し|
|**環境構築**|GCEのVMインスタンスが必要|GitHubリポジトリとgit 設定が必要|
|**自由度の高い運用[^1]**|比較的簡単<br>（クラウドのサービスも利用できる）|難しい|
[^1]: 特殊な運用は煩雑になりがち。例えば、GitHubにpushしたくない中間ファイルを出力閲覧したい場合、中間ファイル用ストレージを別に用意したり、gitのコマンドを工夫する必要がある。
|**トリガー**|基本はcronのできる範囲のみ|リモートへのpushやpull requestの作成をトリガーにできる|
|**料金**|USリージョンのf1-microインスタンス1個だけなら無料。リージョンやインスタンスにより、**プランに応じて**料金が発生する(E2-mediumで、$0.01005/時間)|リポジトリが公開なら無料。非公開なら、Actionsの実行時間が2000分/月を超えると、**利用時間に応じて**料金が発生する(Linuxで$0.008/分)|
|**VMのスペック**|選択可能[^2]|固定[^3]|
[^2]: 公式ドキュメント：[マシンタイプ | Compute Engine ドキュメント | Google Cloud](https://cloud.google.com/compute/docs/machine-types?hl=ja)
[^3]: Standard_DS2_v2, CPUx2, メモリ 7Gb, ストレージ 14Gb, MAC Stadium

所感としては、
VMからの実行は**クラウドサービスを合わせて活用したいタスクや、実行時間が長く複雑なタスク**に、
GitHub Actions は**gitの更新をトリガーにしたいタスクや、実行時間が少なくシンプルなタスク**に向いているように感じました。

あなたの目的に合った方を活用してくださいね。


#参考
[GithubにSSH鍵登録](https://qiita.com/Toshimatu/items/9f7f2fd30fe456bd64b6)
[GithubのSSH通信設定](https://qiita.com/yysskk/items/974c9c55d66a26515651)
[AWS CLIのインストール](https://qiita.com/yuyj109/items/3163a84480da4c8f402c)
[AWS CLIのインストールから初期設定メモ](https://qiita.com/n0bisuke/items/1ea245318283fa118f4a)
[Github Actionsの使い方メモ](https://qiita.com/HeRo/items/935d5e268208d411ab5a)
[GitHubアクションの「AWS認証情報の設定」アクション](https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actions)
[Github Actionsの使い方メモ](https://qiita.com/HeRo/items/935d5e268208d411ab5a)
[Pythonで手軽に始めるWebスクレイピング](https://qiita.com/Moh_no/items/a835f77b6b4e3972fbbe)
[クラウドで手軽に始めるクローリング](https://qiita.com/Moh_no/items/e8902ec40df7ddf9448a)