この記事は CAMPHOR- Advent Calendar 2021 10日目の記事です。
こんにちは @km_conner です。
昨日の記事では Website リプレイスのモチベーションやフロントエンドの構成に関して紹介されていました。
それに引き続きこの記事ではバックエンドの技術などに関して解説します。
以下では、移行前のWebサイトを「旧Website」、移行後のWebサイトを「新Website」と表記します。
バックエンドが担う機能の変化
Website のリニューアルにおいては、アプリケーションの構造をシンプルにするためにフロントエンドとバックエンドでの役割分担を見直しました。
新旧共に、Websiteはフロントエンドに表示される情報の取得・更新やお問い合わせフォームに来たメッセージの処理を主な機能としています。
旧Website においては、フロントエンドに対してメンバー一覧、イベントの記事一覧、スポンサー一覧の情報を提供する機能やそれらの情報をアップデートする機能、お問い合わせフォームに問い合わせが来た際の処理を全てバックエンドが担っていました。
それに対して 新Website では取得、更新する情報をメンバー一覧のみとして、イベントやスポンサーの一覧はフロントエンドにあらかじめ情報を持たせておくことで対応しました。
これは、これらの情報を操作するのはその時の運営メンバーのみであり、運営メンバーにとってはWebサイトにログインして Webフォームを操作するよりも Git の Repository にある情報を更新して Pull Request を出す方が実装上も運用上も楽になると考えたためです。
メンバーの情報に関しては卒業後も自分のプロフィールなどを編集する場合があるため Web 上で操作する機能を残しています。
結果として、バックエンドはメンバー一覧を管理する機能とお問い合わせを処理する機能のみを残したシンプルなものになりました。
使用した技術
以下の技術・ソフトウェアを使用しています
- Go
- MySQL
- gRPC (gRPC-Web)
- Envoy
昨日の記事にあった通り旧Website では Python (Django) を使用していましたが、新Website のバックエンドでは Go を使用しています。
これは、現在の運営メンバーには Python よりも Go の方が書き慣れている人が多いことが理由として挙げられます。
フロントエンドとの通信
新Website においてはフロントエンドとバックエンドの間の通信に gRPC-Web を使用しています。
gRPC とは HTTP/2.0 上で動作し、Protocol Buffers と呼ばれるメッセージの形式で API を呼び出すことのできるプロトコルで、様々なソフトウェアにおいてサービス間通信に使われています。
gRPC を使う際には以下のような proto ファイルを記述することで API 仕様を定義し、そこからサーバー、クライアントの Stub を自動生成することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
service Website { // トップページに表示されるユーザー一覧取得 rpc GetCurrentUsers (google.protobuf.Empty) returns (GetCurrentUsersResponse) {} // メンバーページのユーザー一覧取得 rpc GetAllUsers (google.protobuf.Empty) returns (GetAllUsersResponse) {} ... } message GetCurrentUsersResponse { repeated User users = 1; // ユーザー一覧 } message GetAllUsersResponse { repeated User current = 1; // 現役 repeated User graduated = 2; // 卒業生 } message User { int32 id = 1; // ユーザー ID string name = 2; // ユーザー名 string first_name_ja = 3; // 名 (日本語) string last_name_ja = 4; // 姓 (日本語) ... } |
ブラウザ上では gRPC が使用する HTTP/2.0 が常に使えるとは限らないため、ブラウザ上で gRPC を使用する際には gRPC-Web と呼ばれるプロトコルを用いる必要があります。
gRPC-Web は gRPC とは異なり HTTP/1.1 でも通信することができるようになっています。
gRPC と gRPC-Web の間には直接の互換性はないため、間にそれらを相互に変換する Proxy の役割として Envoy を使っています。
ここまで説明した通信の流れが上の図です。 (図を単純化するため Reverse Proxy や SQL など直接関係ないものは省略しています。)
コードの品質担保
Website は比較的長期にわたって運用されるプログラムであるため、コードの品質を担保することは大変重要です。
Go はコンパイラを用いた静的型付け言語であるためビルドが通ることをもって最低限の型チェックを pass することが保証されますが、これだけでは不十分です。
自分自身が Go を使ってあまり真面目にコードを書いたことがなかったのでこの辺りの知識が少なく、かなり勉強になりました。
エンジニアあるあるとして、開発を急ぐあまりテストを十分に書かなかったり、Linter 等の設定をサボったりすることがあります。(あくまで個人の感想)
新Website の開発に当たってはこのようなことがないように、かなり初期の段階で golangci-lint を GitHub Actions 上で実行するようにしました。
また、テストに関してもできる限り実装した機能とそれをテストするためのコードを同じ PR に含めることでテストの書き忘れをなくしました。外部 API を使用する部分などのテストにはモックを使用しています。Go におけるモックには gomockhandler を使用して、常に最新のモックが作成されていることを CI 上で確認しています。
定期的に Dependabot が依存するパッケージを更新する PR を出してきますが、その際もテストがあるおかげで安心してバージョンを上げられます。
フロントエンド側との連携
新Website ではフロントエンドとバックエンドの Repository を分けたこともあり、始めはフロント-バックエンド間の通信が必要ない機能、 UI 等をそれぞれ別の人が開発していました。しかしながら、最終的にそれらの間の通信を行う必要があります。
先述の通り フロントエンドとバックエンドの間は gRPC-Web を用いて通信していますが、この技術は gRPC を使ったことがない人にはやや複雑に感じるようです。「gRPC と gRPC-Web って違うの?」「Envoy って何者??」など基本的な質問が色々と飛んできました。
そこで、僕がひとまず API のエンドポイントを叩くコードをフロントエンドのコードとして実装し、それをサンプルコードとして使い方を説明することにしました。 Next.js も React も使ったことはなかったのですが、まあ何とかなりました。
これによって具体的な実装方法に関してはかなり具体的な説明ができるようになり、その後もスムーズに連携できていたように思えます。
gRPC とは何か、などの基礎的なところから順に説明していくべきか、それとも実際の実装を見せた後に基礎的な説明をするべきかは意見が分かれる部分だと思いますが、今回のケースにおいては後者の方法でうまくいったと思っています。
サーバーサイドの開発期間
昨日の記事では移行に2年半掛かったと書いていましたが、バックエンドの開発はフロントエンドの開発がスタートしてからかなり後で開発がスタートしたため (最初のコミットが2020年の9月末) 多少の空白期間はあったもののある程度継続して開発することができていました。
これはバックエンドの Repository の Contribution のグラフです。グラフが大きく盛り上がっているところは昨日の記事にもあった集中開発日付近で、進捗を生む効果がかなり大きかったことが分かります。
まとめ
Website のリプレイスの話が出たのは2年半前です。当時その話を聞いたとき、自分にとってはこれがどの程度現実的に可能なのか半信半疑でした。数か月が経過して開発が滞ってきた頃には、このプロジェクトは永遠に完成しないのではないかと思うこともありました。紆余曲折あったものの、結果として時間が掛かったとはいってもWebsiteのリプレイスを終わらせることができたことは非常に感慨深く、達成感も大きいものです。
全てのソフトウェアエンジニアにとって、既存のシステムを改善しようとする試みは非常に価値のあることです。そして、それを試みに留めるのではなく実際にやりきることはそれ以上に価値のあることだと思います。
今のこの記事を読んでいる皆さんも是非この機会に作りかけのプログラムを完成させてみてはいかがでしょうか?
新しい Website は https://camph.net/ から覗いてみてください。
明日の担当は Panorama くんです。