この記事は新歓ブログリレー2023の21日目、SysAdTechBlogの第11回目の記事です。
こんにちは。19B(23M)の@mazreanです。普段はSysAd班でサーバーサイドの開発・運用を行なっています。
@BOT_pika_test、かわいそう…どこの悪魔がこんなことを…そう、私です!
この記事では、私がtraPのSysAd班で開発・運用を行っているtraP Collectionというゲームランチャーで行った、REST APIの移行について話していきます。
この記事は新歓ブログリレーの記事ですが、新入生が内容を理解できることを想定している記事ではないです。理解できたあなた、是非SysAd班に入ってください。良い経験ができると思います。
SysAd班では毎年Webエンジニアになろう講習会というプログラミング初心者がWebエンジニアとして必要な知識と能力を身に着けるための講習会を行っています。今はこんな記事を書いている自分も大学に入ったときはプログラミング経験がなく、Webエンジニアになろう講習会で学生エンジニアとしての一歩を踏み出した一人です。
新入生にはなんとなく「SysAd班に入るとプログラミング初心者でもこんな経験ができるようになるのか」ということが伝わってくれればうれしいです。
ゲームランチャー traP Collection
traPはゲーム開発を行うサークルとして生まれたこともあり、毎年多くのゲームが製作されています(作品紹介から見れます)。
traP Collectionはこれらのゲームをサークルの外の方により手軽に遊んでもらい、部員の創作活動へのモチベーションをさらに高めるためのサービスとして開発されました。
traP Collectionができる前は、traPで開発されたゲームを遊ぶためにはそれぞれのゲームのブログ記事で公開されたリンクからゲームのインストーラーを実行し、インストールする必要がありました。traP Collectionにより、ゲームランチャー1つをインストールするだけで、traPで開発されたゲームをダウンロード、起動することができるようになりました。
このランチャーをコミックマーケット(以下コミケ)で販売したり、工大祭での展示に利用することで、traPで開発されたゲームの発信に利用しています。
構成
traP Collection全体の構成の概要です。このうちの管理用Web UI、ランチャー、APIサーバーがtraP Collectionのコンポーネントです。
ランチャーはAPIサーバーからゲームの一覧などの情報を取得し、Object Storageからゲームの実行ファイルなどをダウンロードすることで、ゲームの管理を行います。このランチャーからは一切APIへの書き込みのリクエストは発生しないのが、移行の際のポイントとなります。
部員によるゲームの追加はWeb UIからAPIサーバーを通して行えるようになっています。この際の部員の認証は、部内SNS traQでのOAuthを利用することで行っています。
APIサーバーでのゲームに関する情報などはデータベース(MariaDB)に保存しています。
APIサーバーの詳細
今回の記事の主役となるAPIサーバーについて、もう少し詳しく解説していきます。
APIサーバーは使えるリソースの制限やSysAd班内での使用技術の統一性などを考慮しGo言語で実装しています。
また、構成図を見てわかるようにAPIサーバーはObject Storageや認可サーバーなどの多くのコンポーネントと接続しています。また、現在SysAd班内ではObject StorageではOpenStack Swift互換のストレージからS3互換のストレージへの移行や、認可サーバーの役割をtraQから内製の部員管理サービスportalへ移すことなどが検討されており、各コンポーネントを柔軟に入れ替えられる必要がありました。これらの事情を鑑みて、APIサーバーではドメイン知識と外部コンポーネントとの接続部分を分離し、外部コンポーネントの変更に強いクリーンアーキテクチャをベースとしたアーキテクチャを採用しています。
APIの移行の経緯
旧APIは自分がサーバーサイドの開発を始めたばかりのころに作ったこともあり、整合性や運用の継続の観点で問題がありました。具体的には、ランチャーで必要となるゲームの紹介画像などがない状態になりうる、同一のゲームの紹介画像が複数回アップロードされストレージを無駄に使用する、などです。
このような問題をエラーを返すなどで逐一対応していくには限界があり、開発速度や安定性の低下を招くおそれがあることから、APIの設計を変更することで対応することになりました。
新APIの設計・実装
移行をするためには新APIを設計・実装する必要があります。
設計は、旧APIで発生した整合性の問題を踏まえ、ゲーム情報などがランチャーから見えてはいけない状態とならないのを最優先にした設計としています。また、旧APIで追加されたデータの扱いについては細心の注意を払い、API設計のレビュー時には過去のデータが関わる変更全てに旧APIで追加されたデータの扱いを併記しています。以下の新API設計時のPull Requestを見ると、注意の払い具合はわかりやすいと思います。
実装は、クリーンアーキテクチャベースのアーキテクチャとなっていることを利用して、最初に役割ごとに旧APIの実装を転用しても問題ない箇所、新規の実装が必要な個所を切り分けていきました。具体的には、認証部分やObject Storageとの接続部分については旧APIと果たすべき役割が変わらずほとんど変更を行わずに使えましたが、整合性周りへの対応でドメインのEntity間の関係が変わったことでデータベースとの接続部分や変更が入ったEntityに関するUseCase層は書き直しが必要になりました。また、当然APIの設計が変わっているためAPIの接続部分は完全に書き直しとなりました。体感としては、全体の6割程度が書き直しになった印象です。
また、後述の旧APIから新APIへのダウンタイムほぼゼロでの移行を実現するために、環境変数による簡易的なFeature Flagを導入し、環境変数を変えるだけで新API、旧APIの有効・無効の切り替えができるようにしています。特に旧APIについては書き込みのみの無効化と全面無効化の2段階を用意しています。Feature Flagの実装は、Feature Flagの管理用のpackageで環境変数を読み込み、その結果をAPIで読みこむことで有効・無効の切り替えを行うというシンプルなものです。言葉で聞いてもイメージがわきづらい場合は以下で実装を見ることができます。
そのほかに、データベースのスキーマにも変更が必要となりました。データベースのレコード数がそこまで多くなく、テーブル全てのレコードを再度INSERTしなおしてもそこまで時間がかからないことから、ダウンタイムの削減のために旧APIと新APIが同時に動作できるよう、スキーマの変更ではカラムの削除などの旧APIが動作しなくなる変更が必要となる全てのテーブルを再度作り直すことにしました。
API移行の流れ
コミケではランチャーを販売しており、ダウンタイムが発生しランチャーが正常に動作しない時間が発生するとゲームを遊ぶ機会の損失やランチャーへの信頼性の低下などが発生することが考えられます。また、SysAd班内では数少ないサークル外の人がユーザーのサービスであるため、「外部のユーザーを考慮した移行」を自分がやってみたかったというのもあり、外部ユーザーから見たダウンタイムが極力発生しないことを目指して移行を行いました。ぶっちゃけそこまでユーザー数は多くなくダウンタイムが発生するデメリットは極めて小さいサービスなので、後者の「やってみたかったから」というのが最大の理由となります。
手順としては、以下のようになります。
- 旧APIからの書き込みを停止
- データベースのマイグレーション
- 新API有効化
- 新APIに対応したランチャーのリリース
- しばらく様子を見て、メトリクスで旧APIへのリクエストが完全になくなったのが確認出来た時点で旧API停止
この手順であれば、マイグレーションに漏れが発生することもなく、ランチャーがAPIサーバーを利用できなくなるのはAPIの有効化・無効化に必要となるサーバープロセスの再起動の間のみとなります。APIサーバーでは起動時に行う特別な処理などはないため、ほぼダウンタイムなしでAPIの移行を実現できました。
まとめ
「外部ユーザーから見える部分で書き込みが発生しない」という特徴を利用して、ダウンタイムほぼなしでAPI移行を実現した事例を解説しました。
今回の移行では「書き込みがない」という特性の有用さを実感しました。移行の楽さ以外にも書き込みがないことにはキャッシュ導入が楽になるなどの多くの利点があるとおもいます。不要な書き込みが発生しない設計を心掛けたいですね。
traPのSysAd班では今回のようにバイトなど以上の自由度のある環境で、バイトなど以上に本格的な開発・運用経験を積むことができます。
また、最初にも述べましたがSysAd班では初心者がWebエンジニアとして必要な知識を身に着けて、本格的に開発・運用の経験を積める環境が整っています。少しでも興味がある方はぜひtraPに入部してみてください。
明日は@dogwood_floの記事です。お楽しみに!