2024年6月30日に今の会社を退職し、翌7月1日から別の会社に入社することになった。
現職の在籍期間は大体3年弱ほどで、アーキテクチャを中心とした技術的な意思決定も色々してきた。新規構築から運用までやってきた中で感じたことや経験豊富なエンジニアからいただいたアドバイスなど、それらを含めて当時の意思決定の反省を自戒を込めてここで書いておく。
Table of Contents
留意事項
- 当時の意思決定の良し悪しではなく、次に同じ状況になったらどうするか?を軸に書いていこうと思う。ので、特定の何かを攻撃する意図は全く無い
- 記憶だけを頼りに書いているので認識相違、自身の勝手な勘違いも含まれると思う
- 技術戦略や組織戦略が関わる部分まで書くと収拾がつかなくなるのであくまでも技術的な意思決定に絞った内容を書く
やったこと
やったことはざっくり一言で言うと「共通機能のマイクロサービス化」である。もともとPHP+Fuelで書かれたモノリスが持っていた認証系機能をマイクロサービス化した。また、認可(OIDC)基盤も新規構築した。
チーム外での活動ではOpenTelemetryとDatadog APMを用いた分散トレーシング導入の推進もしたが、そちらは今回は割愛する。
想定効果
当時組織的にもマイクロサービス化が推進されていたのもあるが、共通機能をモノリスから引き剥がし独立したコードベース及びサービスとして構築する狙いとして下記があったと思う。
基盤チーム側の効果
アジリティの向上
- 認証に関わる機能開発が他機能に引きずられない
- 開発サイクルを自チームで独立して回せる
共通基盤特有の非機能及び機能要件の充足
- 高い可用性や認証方式など共通基盤として求められる要件の充足に集中できる
サービス開発(基盤のクライアント)側の効果
認証系機能の利用しやすさ、横展しやすさ向上
- 認証に関わる実装を意識する必要がなく、必要なパラメーターをセットしてリダイレクトするだけで良い
- ドメインを跨いでもOK、外部サービスでもOK(OIDCを利用)
意思決定
構築にあたり様々な技術的意思決定が行われたが、今回の記事に関連するものは下記の通り。
技術スタック
- BE:Golang
- FE:Vue+Typescript
- インフラ管理:Terraform
コード管理
- コンポーネント毎(インフラ, API, frontend, web backendなど)にリポジトリを作成して管理
- 共通コンポーネントはライブラリ化して各コンポーネントで利用
インフラ
- GCP(Spanner, GKEなど)がメイン、一部AWSを利用
- GKEやSpannerなどマルチテナントで利用できるものはする
- DBは専用のSpannerインスタンスを立てる
その他
- OIDCのスクラッチによる実装
所感と教訓
以上を実践し3年弱運用した結果を踏まえて所感と教訓を書いてみる。
コンポーネントとリポジトリの粒度は別
内部通信用のgRPCサーバーやWebバックエンド用のHTTPサーバー、OIDCやユーザーAPIなどコンポーネント毎にリポジトリを分割した結果、あっという間に自チーム管理のリポジトリ数が10~20個ほどに膨れ上がってしまった。新規構築時はあまり気にならなかったが特に問題になるのは運用開始後で、dependabot対応やローカル開発環境構築、新メンバーのオンボーディング負荷など色々なところで辛さが出てきた。
例えばGolangであればcmd/
にエントリーポイントを置いて、pkg/
配下などでモジュールを分割すればそれほど苦労せずにモノレポ構成を実現することができる。CICDについても、Github Actionsをはじめとして特定ディレクトリのworkflow定義を作成する機能が良いされている。
運用コストを考慮するとリポジトリを分割する意思決定はかなり大きな意思決定であり、
- リポジトリのオーナーシップが異なる、混ざってしまう
など、リポジトリを共有することによるデメリットが構築時点で自明な場合以外は、モジュールを分割した上で一つから始めるのが良いと考えている。
複雑性を犠牲にする決断の重さ(マルチクラウド、マイクロサービス etc.)
GCPとAWSを併用する形にしてしまったのは今になってはかなり渋い選択であった(初期構築時の意思決定に自分は絡んでいなかったがそれに乗っかって色々作ってきたのはあるので)。
「マルチクラウド」「マイクロサービス」「DDD」など色々と流行りの手法はあるが、自分としてはその選択により複雑性がどれだけ持ち込まれるかを特に注意した方が良いと思っている(KISSの法則など昔から言われているところではあるが)。
通信経路が一本増えたり、管理するリソース(DBのような大きなものから割当IP一つ程度のものまで色々)が一つ増えるだけで認知負荷、運用コストが爆上がりする意識を持つべきだった。
例えばAWSが採用されている中GCPを利用することを決定した背景にGCPのSpannerはメンテナンスフリーだから、GKEはメンテ容易だから ということがあった。しかし果たしてそれがトレードオフ的に本当に最優先すべき観点だったんだろうか?先に今運用しているDBをスキーマ新規作成で間借りして後で分割することでメンテコストをそこまで上げずに済む方法もあったかもしれないし、コンテナオーケストレーションもECSを使用するで事足りたかもしれない。
共通基盤を初めから独立したサービスとしてデプロイしないのもあり
様々なサービスから呼び出されるサービスをファーストリリース時点で独立したサービスとしてデプロイする前に、下記を検討してみるのもありだと思う。
- 初めはライブラリとして提供する
- コンテナを用意してサイドカーとして提供する
特に同期通信を伴うサービスはできるだけクライアントに近い方がオーバーヘッドが少ないので依存コンポーネントが少ない場合は検討する価値はそれなりにあると思う。
サブシステムとしての共通基盤にどこまで粗結合を求めるべきか考える
たしかにサービスのほとんどの機能を利用するためには認証を通る必要がある。しかし逆に、認証サービスそれだけではユーザーとしては何も便益を享受できない(ユーザーはそのサービスに認証しに来ているわけではない)。
こうした関係を共通基盤はサービスの中のサブシステムであるとみなすと、先述した「共通基盤特有の非機能及び機能要件の充足」はそこまで重要ではなかったのではないかと思ってしまう。自身の所属会社が扱っていたサービス構成はメインサービス一本だけと言って差し支えなく、「認証が落ちたらメインサービスが利用できなくなる」し、「メインサービスが落ちたら認証にもアクセスが来なくなる」ものである。
第二の柱となるサービスが出てくる可能性も考慮し今後の成長性を鑑みて初めから独立したサービスを立てるというジャッジも正当性があるように思えるが、結果だけ見るとオーバーエンジニアリングと見ることもできる。
結論:後から分割でも遅くない
新規サービス開発、とりわけリアーキテクティングやリライトプロジェクトでは初期構築時点でDRYや粗結合など「俗にキレイと言われる作法」を必要以上に重視してしまう傾向があるように思える(一般論ではないかも。自戒9割で捉えてください)。
結局そうしたプラクティスにもトレードオフがあり、どの程度適用すべきかどうかはプロジェクトや構成による。変更に強く安定したシステムを作るのはとても難しく、そうしたアーキテクチャ設計をどこかの書籍で「必要十分な設計」と見かけてまさにそうだなと思ったことがある。
詳細を詰めすぎてもいけない、先延ばしにすべきでない決定まで先延ばしにしてもいけない。設計上のスイートスポットをまだ私は見つけることができていないが、今回の経験で「システムに複雑性を持ち込むこと」に対する意識が大きく変わった。
モジュールが分かれていれば後からリポジトリを分けることもできるし、DBも論理的に分かれていればインスタンスを後で分離することもできる。クラウドを跨いで利用するメリットではなく跨いで利用「せざるを得ない」要件は?本当にその要件は必要か?これから技術的な意思決定を行う際は自問していきたいと思う。
さいごに
現職は同僚にも恵まれ、エンジニアとして色々な経験を積むことができた。現在は反省を踏まえて改善を勧めているフェーズなので、このまま在籍し続けていたとしてもそれはそれで成長できたと確信している。
とはいえ最終的にはエンジニアとしてのキャリアの20代終盤を悔いなく過ごしたいという思いでアグレッシブな選択することになった。これからも引き続き色々手を出しつつ失敗しつつ学んでいこうと思う。