DynamoDB のレコード一括削除処理を AWS SDK for Python (Boto3) を用いて行う

 先日、DynamoDB のレコードのパージ(一括削除)処理を書きました。将来の自分へのメモとしてソースコードを残したいと思います。また、そもそもこの書き方がベストな気が全くしないので、コードを晒すことでいろいろコメントをもらえないかなーという魂胆です。コメント頂けるとありがたいです。

DynamoDB レコード削除の要件について

 まず今回私が叶えたかった要件についてです。

  • 対象はHashキーとRangeキーを持つテーブル
  • Hashキーは全レコード共通(現時点では)
  • Rangeキーにはレコードの作成時刻がYYYYMMDDHHMI形式で格納されている
  • レコードは毎分作られるので、何もしないとそれなりの数のレコードが溜まっていく。そこで、一週間以上前までに作られたレコードを全て消したい

が実現したいことです。

参考文献

 下記の記事で全てのレコードを消すケース(RDBでいうTRUNCATE)が紹介されていたので、参考にさせていただきました。ありがとうございます。

qiita.com

一括削除プログラム

私が作成したコードは下記になります。

# -*- coding: utf-8 -*-
import os
import boto3
import json
import logging
import decimal
import datetime
from boto3.dynamodb.conditions import Key, Attr

logger = logging.getLogger()
logger.setLevel(logging.INFO)

TARGET_ID = "1234567890"
dynamo_table = boto3.resource('dynamodb').Table("*** TARGET_TBL_NAME ***")

def lambda_handler(event, context):
    try:
        logger.info("*** delete_record() is started. ***")

        delete_targets = dynamo_table.query(
            KeyConditionExpression=Key('id').eq(TARGET_ID)
            & Key('updated_at').between("201701010000", _get_week_ago_as_YYYYMMDD2359())
        )['Items']

        key_names = [ x["AttributeName"] for x in dynamo_table.key_schema ]
        delete_keys = [ { k:v for k,v in x.items() if k in key_names } for x in delete_targets ]

        with dynamo_table.batch_writer() as batch:
            for key in delete_keys:
                batch.delete_item(Key = key)
                logger.info("Delete: " + str(key))

        response = {
            'statusCode': '200',
            'body': '',
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            }
        }
        return response
    except Exception as e:
        logging.error("type: % s", type(e))
        logging.error(e)

def _get_week_ago_as_YYYYMMDD2359():
    week_ago_jst = datetime.datetime.now() + datetime.timedelta(hours=9) + datetime.timedelta(weeks=-1)
    return week_ago_jst.strftime('%Y%m%d') + "2359"

レスポンス部分はじめ、やや適当なところもありますがお許しください。

実行環境

 実行環境は下記の通りです。

  • Lambda 上で実行
  • Runtime: Python 3.6
  • 設定メモリ: 128 MB
  • cron(0 15 * * ? *) として Daily で実行

課題感

 幾つか課題感があるので共有します。

between(to, from) の From の埋め方

 今回実現したかった要件は「YYYYMMDD2359より前の(小さい)レコードを全て削除」でした。そのため between(from, to) の to のみ設定し、Fromは無しできないかなと考えていました。ただ軽く試してみたところ、from に空文字やNoneは受けつられなかったので、アプリ仕様を鑑みて下記のような形にしました。

delete_targets = dynamo_table.query(
    KeyConditionExpression=Key('id').eq(TARGET_ID)
    & Key('updated_at').between("201701010000", _get_week_ago_as_YYYYMMDD2359())
)['Items']

「201701010000 より後(大きい) かつ YYYYMMDD2359より前の(小さい)レコードを全て削除」となりました。今回はいいんですが、将来的にこれじゃダメな要件が出てくるかもしれないので(まぁどうにかなりそうですが)、どこかで調べてみたいところです。

Hash キーが固定ではなくなったときにどうするか

 再掲となりますが、削除対象のレコードを

delete_targets = dynamo_table.query(
    KeyConditionExpression=Key('id').eq(TARGET_ID)
    & Key('updated_at').between("201701010000", _get_week_ago_as_YYYYMMDD2359())
)['Items']

やろうと思えばできるのですが、全てのHashキーを取得して、その数だけこの処理をする形はイマイチなんじゃないかと悩み中です。

削除速度について

 あまり効率のいい削除方式ではないなーと思ったので、1分間でどれくらいのレコードを削除できるか実験してみたところ、1分間で約350レコードという結果でした。Write Capacity Unit を 1 にしていたのでその影響もあると思い、Write Capacity Unit を 5にして実験してみると約20秒で約600件のレコードを削除できました。

 現在、1日に1,440レコード(60分 * 24時間)貯まるので、Write Capacity Unit が 1 だと Lambda の制限時間5分ギリギリかなーという状況です。将来的にレコードが増えたら Write Capacity Unit を上げる必要があるかもしれません。ただ、このパージ処理のために Write Capacity Unit をあげるのもどうかなーと考え中です。

まとめ

 以前、DynamoDB に関する記事を書きました。

www.ketancho.net

少しずつ DynamoDB の知見が溜まってきた気がするのですが、「将来の拡張性を(ある程度)担保できる設計」についてはまだまだナレッジが不足している気がします。こればっかりは実際にシステムを運用してみて良い思いや辛い思いをしていくしかないと思いますので、今後も継続して使っていければと思います。