会社でサーバーを構築するのは僕が想像していたほど簡単ではなかった
動機
今回のプロジェクト開発において、従来の純粋な静的ランディングページ(Landing Page)に対して、企画側からAPIを叩いてデータを取得し動的に更新したいという要望が出た。さらにページのインタラクションもますます増えてきたため、従来のpug+webpack+jQueryによる純粋な静的ページでは対応しきれなくなり、僕たちは新バージョンの開発でnext.jsを導入した。
まずは従来のLanding Pageのアーキテクチャについて話そう。CIを実行する際に静的HTMLページをビルドし、CSS、JavaScript、画像をCDNへアップロードする。HTMLは直接サーバー上に配置され、その前段にリバースプロキシとしてnginxを配置している。
あれ?せっかくCDNにアップロードしているのに、なぜHTMLはサーバー上に置くのか?
これには歴史的な経緯が関係している。まず会社で採用しているのが自社構築のCDNであり、サポートできるカスタムドメインに制限があった。そのため、もしHTMLまでCDNにアップロードしてしまうとドメイン名が変わってしまい、企画側にとっては好ましくない事態になる。
もう一つの理由は、フロントエンドエンジニアが制作するLanding Page以外に、企画側のキャンペーンなどでテンプレート型のLanding Pageジェネレーターを使ってページを生成することがあり、このドメインも固定されていて変更できなかったからだ。そのため、両者(テンプレートLPと開発チームが制作するLP)を両立させるために、nginxをプロキシとして挟むことが比較的バランスの取れた落としどころだった。
そこで問題が生じた。next.jsは従来の開発アーキテクチャと互換性がなく、すべてのLanding PageをReactで書き直すのはあまりに非現実的だ。そこで僕たちは、next.jsで開発するLanding Page専用の新しいサーバーを1台追加し、nginxによるルーティングで同一ドメインを保証することにした。
問題点
本題に入る前に、従来の静的サーバーにはどのような問題があり、サーバーを追加することでどう改善されるのかについて話しておこう。
1. SEO
最大の懸念点はSEOだ。従来の静的サーバーでもSEO自体は達成できたが、冒頭で触れたように、APIを叩いてデータを取得したり、ブログ記事のような要件を実現したりする場合、データはすべてビルド時に生成される。そのためAPIを叩くとなると、必然的にフロントエンドのJavaScript内でAjaxやfetchを使ってデータを取得することになり、そうしたデータは効果的にSEOを行うことができない。もう一つの懸念点はCORS問題で、APIサーバーとLanding Pageのドメインが異なるため、どうしてもCORSの問題が発生してしまう。
2. パフォーマンス
Landing Pageで扱うデータ量は決して多くはないが、やはりSSRが実現できればパフォーマンス的にも有利になる。
3. サーバーを導入するメリット
Node.jsをバックエンドサーバーとして持つことで、将来的に企画側の要望にも柔軟に対応できるようになる。next.js自体も複数のビルド方式をサポートしており、例えばgetStaticPropsなら事前に静的ファイルとしてビルドされ、getServerSidePropsを使って初めてSSRが行われる。バックエンドにNode.jsがあれば、キャッシュやデータベースへのアクセスといった要件の実装も容易になる。
以上の理由から、僕たちは新しいサーバーを構築することを決定した。しかし、ここからが本当の面倒の始まりだとは思いもしなかった。
奮闘の記録
SREとの調整
サーバーを構築するには、僕たちのプロジェクトではまずSREと調整し、課題を説明する必要があった。おそらくSRE特有の慎重で堅実な気質もあってか、彼らの懸念事項は多岐にわたり、他のプロジェクトも担当しているためコミュニケーションにも多くの時間を費やした。しかし動機は明確であり、事前に僕の方でアーキテクチャ全体を整理していたため、比較的すぐに許可を得ることができた。
僕たちのクラウド環境はプライベートクラウドで、alpha環境であれば各開発者が自分でマシンを構築できるため、alphaの設定はすぐに完了した。しかしbetaや本番環境となると比較的複雑な手続きが必要で、ACLの申請を行い、SREを通じて構築してもらう必要がある。マシンを立ち上げるのは簡単だが、各種パッケージのインストールが面倒だった。監視ツール、Node.js、nginxなど、この部分は幸いにもSREの協力があって無事にインストールできた。
歴史の痕跡が色濃く残るnginx設定ファイル
サーバーの構築自体はシンプルだが、僕を本当に悩ませたのは歴史の長いnginxの設定ファイルだった。中には様々なソースからのリダイレクト設定や、メンテナンスモード発動時の処理、特定のパスにおける特殊な処理などが記述されていた。
Ansible Playbookによるデプロイ
SREは環境構築の問題を手伝ってくれたが、次はデプロイだ。この部分は自分で解き明かさなければならなかった。会社では主にAnsible Playbook + awxを使ってデプロイを行っている(AWXはWeb画面上やAPI経由でPlaybookを実行できるGUIツールだ)。幸い他のプロジェクトで同僚と一緒に作業したことがあり、Playbookの経験が少しあったからよかったものの、そうでなければあの大量のYAMLの塊には本当に目が眩んでいたところだ。
Docker
利便性のために、最近のデプロイ手法はマシンを1台立ち上げてDockerをインストールし、その中でsystemctlのサービスを作ってDockerイメージを動かすというものだ。
ビルド自体は簡単だと思っていたが、実際にやってみると様々な問題に直面した。これもまた僕たちのプロジェクトの歴史的経緯に起因する。元々はLerna + モノレポ(monorepo)構成を採用しており、各サブディレクトリからルートファイルへの参照があったため、特定のサブディレクトリだけを切り出してビルドするのが非常に難しかった。
Lernaには主に2つの特徴がある:
- パッケージインストール時に、他のプロジェクトで使われているパッケージがルートディレクトリの
node_modulesにホイスティング(hoist)される - サブプロジェクト内で
import a from 'sub-project'のように他のサブプロジェクトの関数を参照できる(裏の仕組みとしてはnode_modules内にシンボリックリンクが作成される)
一括でまるごとビルドした結果、イメージのサイズは1GB以上にも膨れ上がり、改めてnode_modulesの恐ろしさを痛感した。さらにsymbolic linkの問題で長時間のデバッグを強いられることになった。シンボリックリンクの処理を行わないと、npmがネットワーク上からsub-projectというパッケージを探しに行こうとしてしまい、当然見つかるわけがないからだ。
Jenkinsとの統合
会社では統合にJenkinsを使用しているため、DockerイメージもJenkins上でビルドされ、社内のDocker Hubにアップロードされる。後になって、Jenkinsでビルドする際に時々原因不明のエラーが発生することが判明したが、何度か再実行すると正常に戻るという状態だった。
リスクアセスメント(Risk Assessment)
新しいサーバーを構築したため、手続き上、リスクアセスメント(Risk Assessment)を実施する必要があった。
セキュリティチェック(Security Check)
大きなプロジェクトがあるたびにセキュリティチェックは行われるが、新規サーバーの構築においてもセキュリティチームによるセキュリティ問題のチェックと、必要な修正対応が求められる。
スケジュールの見誤り
next.jsの導入は僕たちのチームが発端ではなく、他拠点の同僚たちが別のプロジェクトで導入していたものだった。そのため、サーバーはすでに構築済みで、僕たちがLanding Pageを開発する際はそれを流用するだけで済むと思い込んでいた。しかし実際には、next.jsのSSG機能を使って生成された静的ファイルを従来のLanding Page用サーバーに配置していただけだった。
当時は少し呆然としたが、QAテストまではまだ1ヶ月以上あり、next.jsのサーバー機能を適用するだけなら余裕があるはずだと考えていた。しかし、その時点ですでに一部の機能はQAテストに入っており、Landing Page自体の開発もまだ始まっていなかった。ページ制作自体は1週間で終わったものの、QA対応、他部署との調整、アーキテクチャの設計、そして前述の一連のプロセスに対応しているうちに、あっという間に1ヶ月が過ぎ去ってしまった。さらに新規プロジェクトの検討フェーズも重なり、リリース日は目前に迫り、多忙を極める中で誰にも助けを求められない状況に陥った。
この期間の僕のプレッシャーは相当重く、休日出勤までして限界寸前だった。
考察
このような想定外の要件には、常にもっと余裕を持った準備期間を確保しておく必要がある。実際のところ、SEO改善のような目的は、正直言ってあまりパッとしないし……評価にも繋がりにくい。はぁ、年齢も重ねてきたことだし、自分の働き方についても見直さなければならないな。
1. 僕がボトルネックになってしまった
今回のサーバー構築では、フロントエンド以外の知識に関わる部分があまりにも多く、さらに社内ツールに深く精通していなければ対応できなかった。多くのことを僕自身がドキュメントを読みながら手探りで進めたため、フロントエンドとの関連性が低く、チームメンバーに手伝ってもらうことが難しかった。もっと良いやり方はなかったのかとも考えたが、こうした仕事は往々にして、自分が諦めて手を引いてしまえば、提案は放置され、誰も泥をかぶろうとしなくなるものだ。とはいえ、これが決して好ましい状態ではないのは確かだ。
2. サーバー構築の複雑さを見落としていた
サーバー構築そのものが難しいというよりは、部署をまたぐコミュニケーションやその後の準備作業によって、開発サイクル全体が必然的に大幅に長引いてしまったと言える。正直なところ、一度通して経験してみないと分からないことばかりだった。次回に向けてできることは、今回の経験をできる限りドキュメントにまとめ、今後の道のりを少しでも楽にすることだ。
3. 他部署の多くが日本人主体だったこと
僕たちのチームを除けば、他の部署はほとんどが日本人主体だったため、日本語力の不足はコミュニケーションの齟齬を招きやすい。そのため、僕がコミュニケーションの架け橋になったり、多くの場面に僕自身が首を突っ込まざるを得なくなったりした。これも実際にはあまり良いことではない。今後どう改善できるか考えているところだ。
4. 事前準備の不足
当初サーバーが存在していないことに気づけなかったのは、僕自身の事前調査が不十分だったからであり、反省すべき点だ。今後、同様のサーバー構築の要望がある場合は、想定外の事態に備えてスケジュールを長めに確保しておくべきだろう。
5. 複数のタスク・スケジュールが重複していた
サーバー構築以外にも、当時は既存のQA対応、Landing Pageの開発、コードレビュー、SREとの調整、要件変更の検討など、複数のスケジュールが重なっていた。頻繁なコンテキストスイッチが発生したことで、サーバー構築に集中できる時間が削られてしまった。
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- Cloudflare Images を画像ストレージ・変換ソリューションとして使う ウェブページに画像を1枚置くのはフロントエンドにとって最も簡単なことだが、リサイズや各種フォーマットの生成、さらにはトラフィックの負荷に耐えることまで完璧にやろうとすると、実際には一つの包括的なソリューションが必要になる。僕はその後、すべて Cloudflare Images に任せるようになり、オリジナル画像1枚だけを渡すようにしている。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。