たった1人でEC2&RDSのバックエンドをLambda&DynamoDBに移行した話

先日お仕事でスマホアプリのバックエンドを移行する機会をいただきました。
システムのフルリニューアルは初めての経験だったので色々と学びが多くありました。
せっかくなので知識を共有したいと思います。

# サマリー
## 概要
こんな感じのプロジェクトでした。

- 開発期間 9ヶ月
- 体制 1人開発
- API数 約100
- バッチ数 約10 
- その他の移行対象(DBデータ,管理コンソールetc)

## 移行の目的
バックエンドの移行の主な目的は以下でした。

- 内製化によって開発速度を上げる
- 内製化するために社内のスキルセットに合う言語・アーキテクチャにする
- AWSコストの削減

## 移行前後の違い
|| 移行前 | 移行後 |
|---|---|---|
|実行環境|EC2|Lambda & Api Gateway|
|フレームワーク|Rails|なし|
|メインDB|RDS(PostgreSQL)|DynamoDB|
|バッチの仕組み|Rails(SideKiq)|AWS Batch/Lambda|

# 実作業前の準備
## 現状把握
私は移行対象の元のシステムを開発したわけではないので、現状把握から全体のタスクを開始しました。
Railsも未経験のためチュートリアルを一通りこなし、大変お世話になっているJetBrains社製のIDE RubyMineでコードを読み始めました。
RailsにはAPI一覧を出力するためにコマンドラインツールがあったので(もう忘れたけど)API一覧を作るのが簡単で助かりました。
またBatchも1つのディレクトリにまとまっていたのでやりやすかったです。

### やったこと
- 移行対象のAPIの洗い出し
- 移行対象のバッチジョブの洗い出し
- 管理コンソールの機能調査

## 方針
今回はバックエンド移行が主目的でした。
そのためクライアントアプリが正しく動作するようにバックエンドを移行するという方針にしました。
バックエンドに合わせてクライアントアプリ側の改善をすることも可能でしたが、以下の懸念がありやらないことにしました。
要件と時間次第ではクライアントアプリを修正するのも、アプリ全体にとっては有益だと思います。

- 正とする挙動がわからなくなる
- ↑のため、都度仕様検討となり時間がかかり過ぎそうだと思った

## 設計
事前にざっくり以下のような設計をしておきました。

- AWSリソースの構築はCDKで行う
- dev/prod環境を作る
- メインデータストアはDynamoDBを使用する
- API/バッチ開発言語はTypescriptを使用する
- 管理コンソールのフロントはAngularを使用する
- API Gatewayを使いカスタム認証用のLambdaを利用する(既存の認証ロジックとの互換性のため)
- データ移行にはDMSを使用する

# 実作業
プロジェクトは3つのフェーズに分けて行いました。

- Phase1 管理コンソール移行
- Phase2 API・バッチ移行
- Phase3 クライアントアプリ微調整・テスト

## Phase1 管理コンソール移行
まず難易度の低い管理コンソールに必要なAPI・データの移行のみ行い、全体の移行計画がうまくいきそうか技術検証も兼ねて行いました。

## Phase2 API・バッチ移行
このプロジェクトのメイン開発です。ひたすらRailsのコードを読み、TypeScriptで再実装していきました。

## Phase3
クライアントアプリは基本的にAPIのエンドポイントの修正のみ行いました。
DynamoDBではオフセット指定のページングが技術的に難しいので、ページングの仕組みもDynamoDBにマッチするように修正しました。


# ノウハウ
実際に開発・移行を進めていく中で様々な問題がありました。
以下のような解決方法で問題を解決・改善し、プロジェクトを進めていきました。

## 開発
## CDK
CDKはすごく便利でした。
今まではAWSの管理コンソールぽちぽちしたり、CloudFormationの長々した定義を作ったりして構築していました。
CDKはTypeScriptでAWSリソースの定義をすることもでき、変数の利用も簡単なので、開発/ステージング/本番などのような環境の複製もあっという間にでき、しかも間違いがありません。

辛いところは、CDK自体のバージョンアップで既存のCDKのコードが動かなくなることです。
リリースノート追って、Braking Changeを見て必要な修正をすれば問題なく動くようになります。

たまにデプロイが失敗しロールバックも出来ない状態になって、リソースを削除するしかなくなったこともありました。

## DynamoDB
### 値にnullは使わないほうがよい
理由はあとからGSIを追加した時に、既存のデータをそのまま更新することができないためです。
また値がないのなら素直に属性なし(undefined)にしておくのが素直だと思いました。

### リトライを意識する
オンデマンドでもプロビジョンドでも多数のリクエストを行うバッチ処理などではスループットエラーなどが一時的に出ることがあります。
その場合エラーをしっかりキャッチしリトライする機構を入れておく必要があります。
特に大量データの場合このあたりの仕組が重要です。

### BatchWriteはすべて成功するわけでは無い
複数のアイテムを一括書き込みするバッチライトですが、コマンドが正常に終了してもすべての書き込みができているわけではありません。
ドキュメントにはベストエフォートと表現されており、書き込みできなかったアイテムがレスポンスに含まれるので、リトライで再度書き込みを試みる必要があります。

### 息をするように並列化する
RDBMSなら1クエリで済むような処理も、データ構成によってはDynamoDBでは何度かリクエストする必要があります。
すべて直列だとAPIの処理時間が長くなってしまうので、Promise.allを当たり前のように使っていくのがおすすめです。

## テスト
デプロイに結構時間がかかるのでトライ・アンド・エラーすると時間がいくらあっても足りません。
DynamoDB Localを導入し、できるだけローカルの単体テスト(DB書き込み含む)で機能を実装できるように工夫しました。

## DMS
DMSはAWSが提供する各種DB間のデータ移行のためのサービスです。
今回の移行はRDSからDynamoDBにデータ移行するのにDMSを利用しました。
データ量によってはかなり時間がかかってしまうのですが、DMSのタスクを並列化することでかなり短縮することが可能なようです。
(今回はそこまでしませんでした)

### 構築
cdkでの構築に苦戦しました。
そのため移行時の一時的な利用ということもあり、DMSに関しては手動で構築しました。

### 日付データ
PostgreSQLのDate型がDynamoDBでは文字列で移行されてしまいます。
そのため、今回はDMSが文字列として移行したデータをさらに、タイムスタンプに変換するようなバッチ処理をDMS移行後に実行しました。
DMS時にカスタム関数などで値を変換できると便利なのになと思いました。

## データ移行
### S3Sync
S3Syncは差分更新してくれるので移行が簡単でした。
ローカルからの実行では時間がかかりすぎるので、AWS Batchで実行することでレイテンシーを低くし、より高速な移行ができました。

# 失敗
大規模な移行だったのでそれなりに失敗と言えるものもありました。

# 既存の仕様が把握しきれない
- 既存システムへの理解不足からくるAPIの仕様考慮漏れ
- ↑からくるテスト項目が作れないという問題

このあたりは発注者さんに協力していただき、テスト仕様書をつくり動作確認を協力していただきました。

# データが全件移行できていない(移行リハーサルで判明)
先に書いたBatchWriteの書き込み失敗を把握しておらず、ハマりました。

# CDK停止したら本番RDSのディスク容量が増加し続けた
CDKのタスクだけ停止したらRDSのレプリケーション設定が消えておらず、(MySQLでいう)クエリログ的なものが増え続け、RDSの容量が増加し続けてしまいました。
一度増加した容量は下げられないので、移行完了しRDSを破棄するまで、増えた容量を確保し続けることになってしまいました。


# まとめ
長いことシステムを運用していると多かれ少なかれ多少の移行が必要なメンテナンスがあると思います。
今回移行の全プロセスをすべて経験したことで、何に気をつけるべきか、どのくらい予定通りにいかないか等肌感覚で理解できました。
こういった移行のときにはやはりTypescriptのような型のある言語が何かと安心でした。(型違いの実行時エラーなど減らせるので)

# お手伝い可能
今回のような移行を検討している方の相談にのることができます。
https://twitter.com/sekitaka_1214 にDMいただければと思います。