DynamoDBのロギング戦略について、あるいはDynamoDB PTIRのユースケースについて

# はじめに

複合的要因により、DynamoDBをメインDBに据え置くという前衛的な開発をやりはじめて（やらざるを得なくなって）そこそこ時が経ったような気がするのですが、いくら注意深くコードを書いてみても結局データベースの過去の状態を簡単に再現できないのはとても不安に感じます。
「DynamoDB ロギング」と検索し、ログをDynamoDBに集約する数多の記事をスルーしつつ、一抹の希望を胸にAWSの公式ドキュメントの『AWS CloudTrail を使用した DynamoDB オペレーションのログ記録』( http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/logging-using-cloudtrail.html )に目を通し、テーブル操作周りばっかじゃねえか！と嘆かれた方も多いことでしょう。
DynamoDBそのもののログ収集構成に関するノウハウというものは私の知る限りあまり蓄積されていないようです。私自身もベストプラクティスを見つけきれていません。

ですが、そうした逆境に屈すること無く、現実的に取りうる幾つかのロギング戦略についてここ最近の思いつきを覚え書いてみたいと思います。
もし「こうすれば簡単だよ！」みたいなのがありましたらぜひコメントをよろしくお願いします。

ちなみに私はロギングが必要なデータをDynamoDBに蓄積する事自体がアンチパターンだと思っています、よろしくお願いします。

# 何故ログが必要なのか

いざ運用開始して「なんかおかしいので過去の状態を調査してください！」だとか「間違って操作したので、過去の状態に戻したいんですけど！」とか「業務上の都合で、誰がいつ操作したのか知りたいんですけど！」とか、そういう問い合わせは往々にしてやってきます。そしてそれに対応するにはログやバックアップが不可欠です。「いや、なんか経緯は分からないですけど今はこうなってます」で納得できる人間はめったにいません。
また、自分たちで操作していても、ミスを犯したのでロールバックしたいだとか、あの時はどういう状態だったけ？とか、データベースの過去の状態を遡りたい、などのシチュエーションは頻繁に起こります、人間の記憶容量は無尽蔵ではありません。

そういった「あるかもしれない事態」に対し、未然に予防策を打つ為にロギングという戦略が必要となってきます。
そのため、データベースに対する、主に変更系の操作の経緯を、時系列順に遡行可能な状態にしておく必要があるのです。
……今更ですね！

# 主な戦略

DynamoDBのロギング戦略として、主に2つの方面からのアプローチが考えられます。

- データベースサイドでのロギング
- アプリケーションサイドでのロギング

これらにはそれぞれメリット/デメリットが存在します、一つ一つ考えてみましょう。

# データベースサイドでのロギング
## メリット

- アプリケーションサイドの都合を気にせずにログ収集が可能
    - アプリケーションサイドがロギングを頑張らなくていい
        - （基本的に）ロギングの抜け漏れを気にしなくていい
        - 安心度が高い
    - 強制的に統一的なログを蓄積出来る
        - 各アプリによってログの出力先がバラバラだとか
            - ログを収集してないだとか
        - 皆さんも思い当たる節があるでしょう、ロギングルールが存在しない現場の方々による思い思いのログ出力など…
- サーバが冗長化されていても、DB側でロギングしているのでログ集約をしなくて済む
    - Fluentあたりを使ってログを集約すればいいだけの話だが…

## デメリット
- 操作ユーザなどのメタ的なログを残しづらい
    - 操作の度にユーザデータを取得して、その取得ログを吐き出すとかすれば何とか……
- DynamoDBのロギングがしょっぱい
    - とにかくこれに限る
    - デフォルトだとレコードレベルの操作ログを残せない

なんだかつらい気持ちになってきましたね。

# アプリケーションサイドでのロギング

## メリット
- メタ的なログの取得が容易
    - 操作を行うユーザは誰なのかとか
    - どのIPから、どのタイミングで操作が行われたのかとか
- クエリ自体や結果のログも自在に蓄積出来る
    - その分ロギングルールについては厳密にならないといけません
    - RDBMSだったらこの辺は柔軟にDB側で対応できそうです
- データベース側の都合を無視出来る
    - DBが死んでた時に投げられたクエリも救える

## デメリット
- ロギングルールの統一が必要
    - DBは複数のアプリケーションから使用されることも多々あるので、それぞれでロギングルールの統制を取るのは大変
    - そもそもロギングルールについて気を払わない人間も時には存在する
    - アプリ側だけではなく、DynamoDBを取り巻くLambdaのルールも統一しようと思うと、もっと大変です。得てしてLambdaの数は肥大化しがちです
- アプリケーション側での抜け漏れなくロギングするのが大変
    - DBアクセス層を抽象化するなどして、半強制的にロガーを仕込むなどが考えられるが、時にそうした作法を無視する人間もいて、そうなると破綻する
    - 柔軟性がある分、人力でルールを遵守するのつらい…
- 不完全なロギングに陥る可能性がある
    - クエリのパラメータだけロギングされてもと調査が大変
    - パラメータとその結果を見比べたい時も多々ある
    - この辺はロギングルールで縛る他に無い…

うーん、これも中々つらい。

# 各種戦略

## DBサイド戦略

### Data Pipelineでバックアップ

cronっぽいスケジューリングで定時バックアップを取れば過去の状態を遡れる！
結果はS3に蓄積されるのでダウンロードして調査するだけで簡単！

#### 懸念点

- 定時内に複数回の変更が行われた場合、過去の変更を完全に追跡することが出来ない
    - データの性質によるが大抵の場合は致命的
- フルバックアップになるのでコストが増大する
    - スマートじゃない

### DynamoDB Streamsロギング

DynamoDB Streamsを使ってLambdaをinvokeして、変更系のログをS3なりログテーブルなりに蓄積する。
Lambdaをトリガした段階だと、変更前の状態も変更後の状態も併せて渡す事が可能なので、柔軟なロギングが可能。実行順序も保証されるので安心！
幾つかの些細な懸念点はあるが、DBサイドでのロギングではこれが一番真っ当な手段なのでは、と思う。


#### 懸念点
- 各テーブルにトリガを仕込むのが億劫
    - 諦めろ
    - むしろロギングが不要なテーブルにトリガを設定する必要が無いことに感謝しよう
- （ログをDynamoDBのログテーブルに蓄積する場合）テーブル設計がやや煩雑になる
    - ロギングしたいテーブルによって、ソートキーがあったりなかったりするはずなので、検索容易性を考慮したログテーブルの設計が難しい
        - S3に突っ込むのが早い気はする
- （データ変更の頻度によるが）Lambdaの呼び出し回数の増大
    - 月々100万回の呼び出しまで無料なのでよっぽどのことがない限りコスト面での問題になることは無いとは思うが

## アプリケーションサイド戦略

### パラメータロギング

DynamoDBに投げるパラメータを片っ端からロギングする。
DBアクセス周りを抽象化していれば比較的簡単に実装可能。

#### デメリット

- パラメータの値しかログが残らず、実行前の状態を遡行するのが億劫
    - 参照系のログを残すと多少は楽になるかもしれないが、それでも「このクエリを投げる前はどうなっていたんだ？」を追いかけるのはかなり困難
- 直接DBの値を操作した時のログは一切残らない
    - 直接操作しなくて済むようにアプリサイドを構築して頑張れば何とか……

### パラメータ + 実行前状態ロギング

DynamoDBで操作系のクエリを投げる時は `ReturnValue: "ALL_OLD"` で実行前の状態を取得することが出来る。
実行前の状態とクエリ内容を取得できれば、DBの状態調査は格段に楽になる。

#### デメリット

- 操作系のクエリを投げる度に `ReturnValue: "ALL_OLD"` をログ出力する必要があり、実装が煩雑になりがち
    - そもそもログ出力を忘れる可能性もある
    - ロギングルールの徹底ぶりが試される部分
- 実行結果を使ってアレコレしたいんだ！って時に使えない
    - これはわりとあり得るパターンだし、実際の実行結果を無視してアプリ側で頑張ろうとすると何かと破綻していくかも知れない
- 直接DBの値を操作した時のログは一切残らない
    - まあ、アプリサイドロギングですし…

# 結論

色々と考慮してくと、DynamoDB Streamsを用いた操作系のロギングが一番信頼性が高いように思われます。
実装コストはそこそこ高そうですが、何故かRDBMSを使わずにDynamoDBオンリーで戦っている現場でのロギングはこれが一番効果的なように思われます。

# 2017年12月1日追記
**DynamoDB On-Demand Backupが発表されました！！( https://aws.amazon.com/jp/blogs/news/new-for-amazon-dynamodb-global-tables-and-on-demand-backup/ )**
これさえあればDataPipelineを使った地道なバックアップを行わなくて済みますね！
軽く調べた所、バックアップを取得して別テーブルにリストア出来る、という機能のようなので、既存のテーブルのバックアップをそのまま同じテーブルにリストア出来るわけではなさそうなので、そのへんは要件との相談になりそうです。
~~まあ、現状だと東京リージョンでは使えないのですが:upside_down:~~

# 2018年3月27日追記
**DynamoDB Point-Time-In-Recovery(PTIR) がリリースされました！！( https://aws.amazon.com/jp/blogs/aws/new-amazon-dynamodb-continuous-backups-and-point-in-time-recovery-pitr/ )**
さっと調べたところ、これはDynamoDBのテーブルを毎秒単位でリストア出来る機能のようです、凄すぎる！！
この機能さえあれば、ログがなくとも、オペミスや誤操作によるデータの欠損も簡単にリカバリ出来ますね！！！！

……本当にそうでしょうか？

DynamoDB BackupまたはDynamoDB PTIRは、リストア時に新規テーブルを指定してからリストアする機能です。
つまり、**データの破損が起こったテーブルに対して直接過去の状態をリストア出来るということは不可能**です。
もしデータのリストアが必要になった時に、以下のどちらかの対応を行う必要があります。

- リストア後、全システムのテーブルの向き先をリストアテーブルに切り替える
- リストア後、既存テーブルを削除し、既存テーブルと同名のテーブルをリストア済みテーブルからリストアする

一つ一つ検討していきましょう。

## リストア後、全システムのテーブルの向き先をリストアテーブルに切り替える

ダウンタイムが最も短くなりうるのはこっちだと思います。

破損したデータベースをメインに向けつつリストアを実行し、リストアが完了したタイミングでDBの向き先を一斉に変更すれば、(破損したデータベースのままどれだけサービスを継続できるか、という問題はありますが)ダウンタイムはほぼ無い状態になるかと思われます。

予めリストアテーブル名を持たせておいて、フラグ一つで向き先を切り替える、あるいはスクリプト一発で全部のテーブルの向き先を変更する、Serverlessフレームワークでまとめてデプロイする……これらは最も理想的なやり方です。
もちろん、それだけの用意を十分に済ませておく必要があるので、かなり大きいコストを払うことになるでしょう。
しかし、本番のWebサーバから各種Lambdaまで同タイミングでテーブルの向き先を変更するのは極めて困難な仕事のように思えます。
また、一斉に切り替える仕組みに、刻一刻と変化していくくサーバレスアーキテクチャの実態に対応しきれているという保証を持たせるのは、これもまたとても困難な仕事だと思われます。仮に3日前に新しく作られたLambdaを網羅するのを忘れていたら？　それは大惨事です。

これは、最も理想的でありながら、最も実現可能性が低い(これを実現することが出来た組織があれば、それは本当に素晴らしい組織なのだと思います。心から敬意を払います)アプローチです。

## リストア後、既存テーブルを削除し、既存テーブルと同名のテーブルをリストア済みテーブルからリストアする

これは大幅なサービス停止時間が発生する代わりに、実装面での準備が不要なやり方です。

リストアが完了した時点でサービスを一旦停止し、既存テーブルを削除して、既存と同名のテーブルをリストア済みのテーブルから再リストアする。
各リストアに掛かる時間はデータ量に依存しますが、軽微な修正であれば直接DBを触ったほうが早いですし、ここでは明らかに大規模なデータ破損を想定していると思われます。
`復元が完了するまでに数時間かかることがあります` という表示の通り、このケースは全く想定していなかった大規模なデータの破損を想定していると思われます。
元テーブルドロップから再リストアまでの時間を考慮すると、やはり大規模メンテナンスレベルの停止時間が必要になってきそうです。

## 結論
- リカバリ策を用意しきれるか、あるいはサービスを一定時間止めることが許されるか、これによって取るべき方針は異なってくると思われます。
- DynamoDB PTIRは、もはやDBのロールバックでしか復旧できないような致命的な障害が発生した際に、いつでもリカバリ出来るという安心を与える機能です。
    - 日常的な些細なオペミスをリカバリするためにPTIRを用いるくらいなら、常日頃からロギングを怠らず、手動でDBを操作したほうが圧倒的に工数が少なくて済むように思います
- 些細なオペミスを速やかに修正できるように、仮にDynamoDB PTIRが存在したとしても、ロギングは怠らないようにしましょう！！