【AWS】DynamoDBのデータ更新時にLambdaを使ってバックアップを作成するプログラムをpythonで実装する

先日公開した記事『【個人開発】ごみのお知らせをしてくれるLINEBot「ごみのお知らせくん」を作りました。』を多くの方に見ていただいており、思った以上の反響に少し驚いています。Qiitaの通知が2桁だったことがなかったので、「何か悪いことをしてしまったのか...！？」とビビりました。LGTMやストックしていただき、ありがとうございます！

https://qiita.com/wallkickers/items/7bf00de66f802503f957

さて、本記事では**DynamoDBのデータ更新をトリガーにしてデータのバックアップを作成するというフローを、DynamoDBストリーム、Lambda、Pythonを使って実装する話について**書いています。LINEBot『ごみのお知らせくん』で地区情報を更新した際にデータのバックアップを取っておきたいな、と思ったのがきっかけです。それでは早速いきましょう！

## ■構成
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/5babb9fe-92a2-b69a-bb81-e6177cef18a5.png">
今回のシステム構成です。試行錯誤しながらだったので結構時間がかかったのですが、図にするとかなりシンプルですね。

## ■作業手順
下記が今回の実行手順です。
やりたい内容に沿ってカスタマイズしてください。

1.DynamoDBの「DynamoDB ストリーム」を有効にする
2.IAMロールとIAMポリシーを作成する
3.Lambda関数を作成する
4.バックアップ作成用のプログラムをアップロードする
5.IAMポリシーを少し修正
6.データを更新してバックアップが作成されることを確認

### 1.DynamoDBの「DynamoDB ストリーム」を有効にする
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/d36e4493-8162-3241-edb4-5c7eb785ebaa.png">

まず、**DynamoDBのデータ変更をトリガーにするためDynamoDBストリームを有効にします。**これを有効にすることでデータ変更を取得できるようになります。

「概要」タブにある項目「DynamoDBストリームの詳細」の「DynamoDBストリームを管理」をクリックします。クリックすると”どの情報を保存するか”という設定ポップアップが開くので、用途に合わせて選択し、有効化をクリックします。僕の場合は変更後の状態を保存しておきたかったので「新しいイメージ」を選択しました。

<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/6ff94127-b893-65f0-064d-948ce35c23cb.png">

有効にすると上画像のようにストリーム有効の値が「はい」の表示になります。この時表示されるARNについては後ほど、IAMポリシーの設定で使用します。この値は覚えておかなくても大丈夫です。

### 2.IAMロールとIAMポリシーを作成する
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/86413020-0cff-a99b-994c-0c2d9b458131.png">
Lambdaに設定するIAMロールとIAMポリシーを作成します。特に重要なDynamoDBに付与するポリシーについては太字で書いています。

作成するIAMポリシーは下記のようになります。これらのポリシーをIAMロールに付与し、IAMロールをLambdaに付与します。

```
■DynamoDB
　アクション：書き込み『CreateBackup』
　*リソース：DynamoDBのリソースを選択

　アクション：読み込み
　『GetRecords』『GetShardIterator』『ListStreams』『DescribeStream』
　*リソース：DynamoDBストリームのリソースを選択

■Lambda
　アクション：書き込み『InvokeFunction』
　*リソース：Lambdaの関数を選択

■CloudWatch Logs
　アクション：書き込み『すべて選択』
　リソース：すべてのリソース
```

ここで、「*」をつけたDynamoDBとLambdaのリソースについては後でまとめて設定するので、一旦は「すべてのリソース」で設定します。


### 3.Lambda関数を作成する
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/a299732c-0fc9-47fd-fdec-77a5db7f0f5b.png">

バックアップを作成するLambda関数を作成します。ここでは「createDynamoDBBackup」という名前で作成しています。

<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/a96ad426-4fe4-7d4e-ddeb-75f5ff460b99.png">

関数作成の際、**先ほど作成したIAMロールを実行ロールに設定します。**また、今回はpythonで実装するのでランタイムにはpythonを選択しています。

#### 4.バックアップ作成用のプログラムをアップロードする
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/ec62365d-9c91-9ccc-a5ba-af20be052354.png">
Lambdaのコードソースからソースをアップします。僕の場合はdockerで構築した開発環境でソースコードを用意しました。

下記が今回使用した、「pyファイル」「docker-compose.yml」「Dockerfile」です。Lambdaにアップロードするときにはoptディレクトリ配下をzipで圧縮します。dockerによる開発環境の構築については『補足』に記載しています。

```
■ ディレクトリ構成
docker-compose.yml
Dockerfile
opt
 L lambda_function.py
```

```lambda_function.py
import json
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

import boto3

DATABASE = os.environ['DATABASE']
TABLE_NAME = os.environ['TABLE_NAME']

def lambda_handler(event, context):
    logger.info("backup取得開始")
    dynamodb_client = boto3.client(DATABASE)
    res = dynamodb_client.create_backup(
        TableName = TABLE_NAME,          # テーブル名
        BackupName  = TABLE_NAME + '_bk' # バックアップファイル名
    )
    logger.info(res)

    return {
        'statusCode': 200,
        'body': json.dumps('backup取得完了')
    }
```

```docker-compose.yml
version: "3"
services:
  python3:
    build: .
    container_name: "python3_lambda"
    working_dir: "/root/opt"
    tty: true
    volumes:
      - ./opt:/root/opt
    ports:
      - "8000:80"
```

```Dockerfile
FROM python:3
USER root

RUN apt-get update

ADD . /code
WORKDIR /code
RUN apt-get install -y vim less
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
```

### 5.IAMポリシーを少し修正
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/4214868d-83c3-7470-7355-411e5d05096d.png">
Lambda関数が作成できたらDynamoDBをトリガーに追加します。

部品が揃ったので、IAMポリシーを少し修正していきます。

#### Lambdaのリソースを修正
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/7b7255e4-9b33-8415-740c-4708b53de488.png">
IAMポリシーのLambdaリソースを先ほど作成したLambda関数に修正します。入力するのはLambdaの「関数のARN」の値です。「Lambda_functionのARNの設定」に入力すれば下の「Region」「Account」「Function name」欄にも自動的に入力されます。

#### DynamoDBのリソースを修正
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/98f76887-eb39-161d-c155-569dcb0f0846.png">
▲DynamoDBストリームのARN

<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/2c8edea8-99f9-e5db-d69c-ec63e77cf07c.png">
▲DynamoDBのARN

**IAMポリシーのDynamoDBリソースに「DynamoDBストリームのARN」と「DynamoDBのARN」を入力します。**ARNはそれぞれ上画像の部分です。


### 6.データを更新してバックアップが作成されることを確認
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/0164a528-2d5d-363d-b71b-39258b07e81b.png">
ここまで出来れば後は確認です。
Lambda関数は簡単にテストができるので確認してみます。

<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/0b802807-586c-7233-af0c-e25f81b2729b.png">

テストした際に該当テーブルの「バックアップ」タブに「テーブル名_bk」というファイルができていれば成功です。次は、DynamoDBのデータを更新してバックアップが出来ているか確認します。

バックアップができていない場合はどこかでエラーが起きているので、Lambdaの「モニタリング」タブでログを確認します。

<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/d93fffd0-2b84-bb6c-f1fb-3a6eedc1c8c0.png">

バックアップファイルが確認できたら「バックアップの復元」をクリックして、更新したデータでテーブルができるかどうか確認します。問題なく出来ていれば完了です。

# 補足
### pythonファイル、docker-compose.yml、Dockerfileついて
githubにアップしました。

https://github.com/wallkickers/lambda_python_docker

コンテナ起動後、python3コンテナに入り、下記コマンドを実行してください。

```
・docker-compose.ymlがあるディレクトリで
docker-compose up -d

・ステータスがupになったら
docker-compose exec python3 bash

・boto3をインストール
pip install boto3 -t ./
```

boto3はpythonでAWSを操作するためのライブラリです。
Lambdaにzipでアップする必要があるので、現ディレクトリ(/opt内)にインストールするようにしています。

### pythonコード内の`os.environ['-']`について
<img width="800" alt="アプリ開発きっかけ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/0c12a42a-2f8b-40ce-5719-acf2b41fb693.png">

プログラム内にはDBの情報を直接記載せず、Lambdaの環境変数で設定しています。環境変数は、Lambdaの「設定」タブ→環境変数から設定ができます。

# 最後に
DynamoDBのバッグアップには「オンデマンドバックアップ」と「ポイントインタイムリカバリ」の2種類あります。オンデマンドバックアップはバックアップを取得した時点のテーブル状態を保存し、ポイントインタイムリカバリは有効にした時点からテーブルに対する更新処理が記録されます。

今回は「データが更新された際にバックアップを作成したいこと」「無料の範囲で実装したいこと」があったので、これらのバックアップ方法は使わず、DynamoDBストリームとLambdaを組み合わせました。

ポイントインタイムリカバリを有効にしようとすると「追加料金がかかります」というように表示がされるため、『この方法は料金がかかってしまう！これでは実現できない！』と考え、本記事の実装をしました。ですが、ポイントタイムリカバリの説明を改めて見てみると、もしかしたらポイントタイムリカバリでもできるんですかね...？この部分についてはまた改めて調べてみたいと思います。ご存知の方がいたら教えて欲しいです。

それでは！

# 参考サイト
>Amazon DynamoDB
>https://boto3.amazonaws.com/v1/documentation/api/latest/guide/dynamodb.html

>CreateBackup
>https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/APIReference/API_CreateBackup.html

>DynamoDB StreamをトリガーにしてLambdaを実行する
>https://qiita.com/Fujimon_fn/items/1f18360ee9ebf6832617

>チュートリアル: DynamoDB Streams と Lambda を使用して新しい項目を処理する
>https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial.html#Streams.Lambda.Tutorial.CreateTrigger

>DynamoDB テーブルのバックアップ
>https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Backup.Tutorial.html

>AWS SDK for Python (Boto3) の “Client API” と “Resource API” の違いについて調べてみた
>https://dev.classmethod.jp/articles/boto3-client-api-and-resource-api/

>ステップ 3: Python で項目を作成、読み込み、更新、削除する
>https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.Python.03.html

>AWS Lambda 実行ロール
>https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-intro-execution-role.html