こんにちは。SRE の @int128 です。
背景
スタディサプリではいろいろな役割の人が協力してプロダクトを開発していくための文化と仕組みがあります。 例えば、Pull Request を作成すると自動的にプレビュー環境がデプロイされる仕組みや本番相当のデータで動作確認するための仕組みなどがあります。 これらは、開発途中の画面をすぐに Designer や Product Manager と共有したり、本番相当のデータでデザインやユーザ体験を確認したりする、といったスムーズな開発体験を支えています。
スタディサプリでは、本番環境と同等*1のデータベースを開発環境で利用するための仕組みを「データベースリストア」と呼んでいます。 データベースリストアは2010年代の前半から動いており、スタディサプリの開発体験を支える重要な仕組みとなっています。
本稿では、スムーズな開発体験を支えるデータベースリストアの仕組みを説明します。 また、以前の仕組みにあったセキュリティや信頼性などの課題と、Step Functions + ECS Fargate Task への移行事例を紹介します。
データベースリストアの概要
スタディサプリでは主に以下のデータベースを利用しています。
- Amazon Aurora
- self-hosted MongoDB
- MongoDB Atlas
開発環境のデータベースは日次で本番環境からリストアされるようになっています。 ただし、QA テストに利用するデータベースは静止点を長く保ちたいため、隔週でリストアする運用にしています。 また、負荷試験などで長期間に渡って利用するデータベースは必要な時にリストアできるようにしています。
具体的には、以下のデータベースがあります。
- develop: 日常の開発で利用。日次で自動リストア
- edge: Customer Support などで利用。日次で自動リストア
- report: データ分析で利用。日次で自動リストア
- release: QA テストで利用。隔週で手動リストア
- その他、負荷試験などで一時的なデータベースが必要になった時に動的に作成
歴史的経緯により、データベースリストアの実装は以下の4種類になっていました。
- Amazon Aurora(各マイクロサービス用のデータベース)
- Amazon Aurora(モノリス用データベース)
- Point-in-Time リストア方式を採用
- Ruby で実装
- self-hosted MongoDB(モノリス用データベース)
- スナップショットリストア方式を採用
- Ruby で実装
- MongoDB Atlas(モノリス用データベース)
- スナップショットリストア方式を採用
- Go で実装
ジョブの構成
これらは Jenkins のジョブで実装されており、おおまかには以下の呼び出し関係になっていました。
- モノリス用データベースをリストアする親ジョブ
- Amazon Aurora のデータベースを作成するジョブ
- 個人情報マスク処理のジョブ
- self-hosted MongoDB のデータベースを作成するジョブ
- 個人情報マスク処理のジョブ
- MongoDB Atlas のデータベースを作成するジョブ
- Amazon Aurora のデータベースを作成するジョブ
- 各マイクロサービス用のデータベースを作成するジョブ
- 個人情報マスク処理のジョブ(個人情報を含むデータベースのみ)
ただし、開発環境によって Jenkins で後続ジョブを設定している場合と Jenkins Pipeline を利用していている場合があり、呼び出し関係の実装はバラバラになっていました。
解決したい課題
データベースリストアで問題が起きた場合は SRE が原因を調査し、迅速に対処しています。 過去に起きていた問題をまとめると、以下の課題がありました。
- セキュリティ
- 本番環境と開発環境で AWS アカウントを分離するにあたり、リストア方式を見直す必要がある
- 信頼性
- 保守性
- Amazon Aurora のリストア方式が複数あるため、問題が起きた場合の調査や改修が難しい
- Jenkins のジョブの呼び出し関係が複雑になっているため、問題が起きた場合の調査や改修が難しい
- 負荷試験などで一時的にデータベースが必要になった場合は、Jenkins ジョブやスクリプトを手動で作成する必要がある
これらの課題を一気に解決することは困難であるため、段階的に解いていくことにしました。
セキュリティ
スタディサプリの本番環境と開発環境は AWS アカウント内の VPC で分離されています。 プロダクトの成長とともに以下の課題が顕在化するようになりました。
- セキュリティルールで個人情報をより安全に囲い込む必要がある
- IAM やネットワークの構成が複雑になり、理解が難しくなってきている
このため、現在 SRE では本番環境と開発環境で AWS アカウントを分離する取り組みを進めています。
現行のデータベースリストアは同じ AWS アカウントにデータベースがある前提で設計されているため、リストア方式を見直す必要があります。 しかしながら、先ほど述べたように現行のジョブやスクリプトに大きな変更を加えるのは困難という課題がありました。
以下のように段階的に対応することにしました。
信頼性と保守性
先ほど挙げた課題はデータベースリストア以外のジョブにも当てはまります。 将来的にデータベースリストア以外への展開も見据えて、Jenkins ではないジョブ基盤を模索することにしました。
具体的には以下の要件を考えました。
- GUI からジョブを簡単に実行できる
- 定期実行と手動実行、どちらにも対応できる
- ジョブ管理ミドルウェアの監視やバージョンアップが不要、もしくは、マイクロサービスごとに影響範囲を抑えられる
- Terraform などでジョブの構成を管理し、Pull Request で変更できる
解決策
Step Functions の実現性検証
スタディサプリでは主に AWS を利用しているため、フルマネージドなワークフローエンジンである Step Functions に白羽の矢が立ちました。 Amazon Aurora のリストア処理は AWS API で完結するため、まずは Step Functions で Amazon Aurora のリストア処理を試みることにしました。
このような不確実性の高い課題にはチームで取り組むことが効果的なので、 @snowfield702 @44smkn @int128 の3人でプロトタイプの実装を始めました。 プロトタイプの実装を進めると、Step Functions だけでは以下の課題が見えてきました。
- Pros
- ジョブの手動実行や実行履歴の確認などの基本機能を備えている
- EventBridge と組み合わせることで定期実行が可能
- ECS Task や Kubernetes Job を呼び出せる
- Step Functions 自体の監視やバージョンアップが不要
- Terraform で構成管理できる
- Datadog で state machine の実行結果や所要時間をモニタリングできる
- Cons
- 複雑な条件判断は実装できない
- 複雑なワークフローを記述するにはある程度の慣れが必要なため、組織で継続的にメンテナンスしていくのが難しい
- ワークフローの JSON を Pull Request 上でレビューするのが大変
当初は AWS API を直列実行するシンプルなワークフローを想定していました。 しかし、AWS API は非同期で実行されるため、実際の処理が完了するまでポーリングする必要があります。 例えば、スナップショットから新しいクラスタを作成する場合は、クラスタの作成を要求した後、クラスタの状態を取得して事後条件を満たすまでリトライする必要があります。
最終的にプロトタイプとして実装したワークフローの一部をお見せします。
Step Functions と ECS Fargate Task の利用
そこで、Step Functions から ECS Task を呼び出すことで、それぞれの良い点を取り入れることにしました。 具体的には以下の AWS サービスを組み合わせて利用しています。
- ジョブを定期実行する場合は EventBridge から Step Functions を呼び出す
- ジョブを手動実行する場合は Step Functions の画面から行う
- ジョブの実行履歴は Step Functions の画面で確認する
- ジョブの処理は ECS Fargate で実行する
- ジョブの処理は任意の言語で記述して Docker イメージとして配置する
- ジョブの実行ログは CloudWatch Logs で確認する
これにより、以下の利点と欠点があることが分かりました。
- Pros
- Cons
- Step Functions の画面から CloudWatch Logs への導線がない
- Step Functions を停止しても ECS Task が裏で動き続けてしまうため、意図しない結果をもたらすことがある
Step Functions の画面から CloudWatch Logs への導線がないという課題に対しては、CloudWatch Logs を直接見なくても迅速に調査できる仕組みを整備しています。 具体的には、ジョブの処理が失敗した場合は Sentry のエラー通知やスタックトレースから原因が分かるようにしています。
また、Step Functions を停止しても ECS Task が裏で動き続けてしまう件は今のところ解決が難しいことが分かりました。 Step Functions と ECS Task の利用が広がると今後の課題になると考えています。
データベースリストアの移行
Step Functions と ECS Fargate Task を導入していく道筋が見えたため、以下のように移行を進めました。
Amazon Aurora のリストアの移行
これまで Ruby とシェルスクリプトの実装が混在していましたが、まずは実現性検証を急ぐため、シェルスクリプトをそのまま利用することにしました。 その上で、リトライ処理を追加して安定性の改善を図りました。
具体的には、以下の流れでリストアを実現しています。
試行錯誤を繰り返して、最終的にすべての開発環境でリストアを Step Functions と ECS Task に移行しました。
次の段階で、本番環境でマスク済みスナップショットを作成してから開発環境にリストアする方式に変更します。 型やテストの恩恵を受けられるように、何らかのプログラミング言語で書き直す予定です。
self-hosted MongoDB のリストアの移行
Amazon Aurora の移行を通して、データベースリストアの全体像を深く理解できたという学習効果がありました。 そのため、self-hosted MongoDB の移行ではマスク済みスナップショットを経由する方式に一気に書き直すことにしました。 どのプログラミング言語を選ぶか悩みましたが、最終的に SRE で経験者の多い Go を採用しています。
具体的には、以下の流れで本番環境でマスク済みスナップショットを作成しています。
- スナップショットからボリュームを作成し、マスク処理用インスタンスにアタッチする
- マスク処理用インスタンスを起動する
- 個人情報マスク処理を行う
- マスク処理用インスタンスを停止する
- スナップショットを作成する
以下の流れで開発環境にマスク済みスナップショットをリストアしています。
現行の実装では個人情報マスク処理に4時間程度を要していました。 マスク済みスナップショットの作成では1つのインスタンスを使い回すため、2種類のマスク済みスナップショットを作成するには計8時間を要することになります。 このままでは翌朝までにすべてのリストアが完了しない可能性が出てきました。 そのため、MongoDB のコレクション単位でマスク処理を並列実行することで、所要時間を2時間弱に短縮しています。
MongoDB Atlas のリストアの移行
MongoDB Atlas のデータベースには個人情報が含まれないため、現行の実装をそのまま Docker イメージに移行しました。 Atlas API key などの秘密情報は AWS Secrets Manager を利用しています。
MongoDB Atlas のリストアについては @44smkn が主担当で進めてくれました。
今後の課題
データベースリストアの移行を通して Step Functions の有用性を検証できたため、他のジョブについても移行を検討していきます。 現時点では、以下の課題が残っています。
- パラメータ指定でジョブを手動実行したい場合、Step Functions の画面で JSON を入力する必要があるため、使い勝手が悪い
- Kubernetes Job を呼び出す場合のベストプラクティスをまだ検証できていない
また、Jenkins にあるジョブには GitHub Actions が適しているものもあるため、ユースケースに応じて移行先を整理していく予定です。
まとめ
本稿では、スムーズな開発体験を支えるデータベースリストアの仕組みを説明しました。 また、セキュリティや信頼性を改善するために、リストア方式を見直して Step Functions + ECS Fargate Task に移行した事例を紹介しました。
SRE では最高のプロダクトを支える最高のプラットフォームを実現するメンバーを募集しています。 ぜひカジュアル面談でお話ししてみませんか。
Product Security Engineer という新しいポジションもオープンしていますのでこちらもぜひ。