Hori Blog

フリーランスでバックエンドエンジニアとして活動している Ryota Hori のブログです。
最近はテック系記事より雑記ブログ気味。

「しいたけ占い公式サイト」を支える技術

ご縁がありまして「しいたけ占い公式サイト」の開発に携わりました。

比較的シンプルなページ構成ですが、高トラフィックを快適に提供する Web サイトとして色々と工夫したので記録に残しておきます。似たような構成の Web サイトを構築する際の参考になれば幸いです。

(技術的開示および実績公開に関してはしいたけ.さんより許諾を頂いております。ありがとうございます。)

しいたけ占い公式サイト」とは

占い師であるしいたけ.さんが占いコンテンツを連載する公式サイトです。

しいたけ占いの公式サイト。占い師しいたけ.が 12 星座別の運勢をオーラカラーと共に診断。毎週月曜日更新の週刊占いと、上半期、下半期占いをお楽しみいただけます。

しいたけ占い公式サイト より

元々は他のメディアで連載されていたのですが、メディア運営終了に伴い新しい場所として制作することになりました。

公開をお知らせした投稿の反響からもわかるように待ちわびている方々がとても多く、公開時にはかなりの閲覧数を想定しておく必要がありました。

私の担当

主にバックエンドの担当としてお声がけいただきました。デザイン及び UI 周り以外の全てを主動する役回りです。

開発チームは私を含め 3 名の小さなチームです。

  • ディレクション及びデザイン:@shiratoriyurie
  • フロントエンド:(匿名希望のフロントエンドエンジニア)
  • バックエンド:私

という座組です。

公開時の @shiratoriyurie の投稿

しいたけ占い公式サイト」の特性と方針

前述の通り、ページ構成はシンプルですが非常に高いトラフィックが予想されました。

また、周知のタイミングでコンテンツが公開されるため、公開時には閲覧数の急騰が見込まれます。

更に、ニュースメディアへの掲載や各社ポータル系へのピックアップによる掲載、テレビ番組での紹介なども多く実績があり、予測できないタイミングでの閲覧数の急騰にも備えが必要です。

一方で閲覧者ごとの固有のコンテンツは基本的になく、入稿およびリリース時に提供する内容を確定することができます。

加えて、入稿の快適さは大事としつつ楽しみにしている方々に快適に閲覧していただけることを最重要視すると優先順位をつけました。

よって「しいたけ占い公式サイト」では Static Site Generation(SSG)を行い、安定した閲覧体験の提供を方針の軸としています。

「下書きプレビュー」という課題

コンテンツを SSG にて提供すると決めたものの、SSG で構築する際に生じる課題が「下書きプレビュー」です。「下書きプレビュー」はコンテンツ公開前に入稿データがどう見えるのかを実際の画面で確認する機能を指しています。

SSG ではコンテンツを反映するのに生成処理を介するため、確認できるようになるまでどうしても数十秒〜数分の待ち時間が発生します。一方で入稿体験としては(主観ですが)遅くとも 10 秒ほどで反映されて欲しいと考えます。

閲覧体験を最重要視するとはいえ、入稿体験が損なわれるとミスも生じやすくなり、最終的には閲覧体験を損なうことに繋がりかねません。

よって色々なデメリットはありますが、「下書きプレビュー」環境は Server Side Rendering(SSR) で並列して提供することにしました。SSR であればリアルタイムにコンテンツを反映することができ、快適に編集作業を行えます。

デメリットについては後述のアーキテクチャ紹介で詳しく触れていきます。

補足:SSG or SSR

負荷対策の観点では CDN を活用することで Server Side Rendering(SSR)でも十分に快適な閲覧体験を提供できる想定ではありました。下書きプレビューを SSR で提供するなら全て SSR という選択肢もあります。

一方で、長期的に安定して快適な閲覧体験を提供したいという観点で SSG を選択しました。編集体験(or 編集環境提供の難易度)とのトレードオフですが、可用性、インフラ環境の移植の容易さ、事前確認のしやすさなどを考慮し今回は SSG を選定しました。

負荷対策とインフラ費用

閲覧数の見積もり

先述のとおり、継続的な大量のアクセスと突発的なアクセス増加への対応が必要でした。

詳細な数値や計算方法はそれなりにぼかしますが、「しいたけ占い」は元々連載していたメディア「VOGUE GIRL」の主力級コンテンツであり、「VOGUE GIRL」は月間 6,300 万 PV を記録することもあったようなので、月間数千万 PV までは対応しておきたいと考えました。

現在『VOGUE GIRL』の月間 PV は約 6300 万。そして、月間 UU は約 460 万人で、

『VOGUE GIRL+』が切開く、スーパーアプリ LINE の新境地 : 最短&低リスクのサブスク開発とは | DIGIDAY[日本版]

アクセス分布は均等ではなく各コンテンツの公開時や外部メディアへの掲載時に急騰すると予測されるため、諸々を鑑みて数万 PV/分(数百 PV/秒)くらいのオーダー感は安心して提供できるように対策します。

負荷対策

単体の Web サイトとしてはあまり類を見ない分間アクセス数の想定となります。バックエンドエンジニアとしては腕の見せどころ、と言いたいところですが、SSG を選定した時点で大手の CDN を用いれば問題なくさばける見込みです。

基本的にほぼ全てを CDN 経由で提供することを目指し、負荷対策は終了です。良い時代ですね…!

高トラフィックと転送料金

SSG により静的なファイルの供給となるためアプリケーションやデータベースのサーバコストは生じない一方、数千万 PV/月を想定すると気になってくるのが転送料金です。

収益性の高い大規模なサービスであれば閲覧数に比例して発生するコストは気にならなくなりやすいですが、小規模体制で構築するシンプルな Web サイトにおいて大きな料金発生はエンジニアとしてはできれば避けたいところです。

技術選定段階では各ページのデザインも決まっておらず PV ごとの転送量は全く未定でしたが、仮に少し多めに 5 MB 前後とした場合 1,000 万 PV では約 50 TB になります。

例えば 2023 年 7 月時点での Amazon CloudFront の日本向け料金は約 0.1 USD/GB であるため、1,000 万 PV ごとに約 5,000 USD かかる計算になります。(加えてリクエスト数での課金も発生しますが支配的ではないので無視します。)

この金額をどう感じるかは前提条件によりますが、今回は SSG した静的コンテンツを提供するシンプルな Web サイトとしては積極的に削減したいコストと捉えました。

Cloudflare の選定

CDN の課金モデルには従量課金モデルや帯域での課金モデルなど、基本的にはトラフィックに応じた料金体系となっているものが多いです。

本件では転送料金の高騰を避けるために CDN として Cloudflare を選定しました。

Cloudflare はトラフィックに応じた課金ではなく利用したい機能によって課金するモデルとなっており、「しいたけ占い公式サイト」のような転送量が多大になるサービスを提供する場合はコストパフォーマンスが非常に高くなります。

特に本件では SSG で生成したコンテンツを提供するだけなので、画像などの最適化も事前に行う難易度が低いです。よって極端な話をすれば CDN だけを無料プランで利用する「タダノリ」状態で利用することで転送料問題が解消できそうでした。

実際には無料プランではなく有料契約をし、カスタマーサポートへの相談や画像最適化などを利用しています。それでも非常に安いので何か見落としていないか不安になりがちでした…!

導入に当たってサポートの方に「想定しているアクセス分布が対応できるのか、料金的にも問題ないのか」と相談させていただきましたが、心強い返答をいただきました。Cloudflare で提供されている既存サービスなどをみても十分に広く使われており、信頼性も問題ないと判断しております。(広く使われている大手のサービスか否かの説明として、「もし大規模障害が生じたらニュースになるレベル」をよく使っています。)

補足:Cloudflare vs. Amazon CloudFront

Amazon CloudFront など他の CDN を下げる話にならないように念のため補足です。

本件ではコストパフォーマンスの観点で Cloudflare が強すぎて選定動機となっておりますが、転送料が支配的になる特性のあるサービスでなければ料金ではなく用途に合う機能を提供する CDN を利用することの方が多いと思っています。

特にアプリケーションのリソースと同じプラットフォームに CDN を設置する管理上のメリットは大きく、個人的にも AWS でアプリケーションを構築するときは CloudFront を好んで使っております。

システムの全体像

アプリケーションのフレームワーク

SSG 及び SSR が可能なフレームワークとして Astro を選定しました。

Astro

選定理由は公式ドキュメントの Why Astro? で挙げられている特徴が要件に合うと判断したことに拠ります。

比較的新しいフレームワークですが開発者の開発環境(VSCode や Neovim)で問題なく各種ツールが使え、画像の最適化なども整備されていたので導入に踏み切りました。

フレームワークについては強くこだわるスタンスではなく、例えば Gatsby などでも十分に品質担保できたと思っています。とはいえ個人的には Astro の Zero JS, by default の方針は魅力に感じていたので、 Astro で構築できたのは嬉しいです。

プロビジョニングツール

Cloudflare は Terraform の Provider があるので安定の Terraform で管理します。

本番環境の全体像

本番環境の SSG 用ビルドとデプロイは以下のようなフローで行われます。

CMS は Contents Management System の略で、コンテンツ管理を担う外部サービスを利用しています。

production build flow

コンテンツ及び実装の更新時に build 処理を行い、Cloudflare Pages にデプロイしています。

Cloudflare Pages を用いることで後述の開発環境を提供でき、緊急時のロールバックも行えます。

一方で Cloudflare Pages の build 機能は使わず、GitHub Actions を用いています。これは環境反映のルールを柔軟にしたり、前段に check 処理を入れたり、秘匿情報を GitHub の secrets に寄せたり、といった細やかな調整をしやすい状態にしたかったのが主な理由です。

結果、デプロイ以外の処理ともリソースを共有しやすく、各種の自動処理整備が捗りました。開発者が GitHub Actions に習熟していることもありこの構成を取りましたが、単純にブランチを環境に反映するだけなら Cloudflare Pages の build 機能は便利です。

本番環境では Cloudflare Pages を origin として独自ドメインでアクセスを受ける CDN を前段に設定します。Cloudflare で Zone 設定(ドメイン設定)を行い Cloudflare Pages の独自ドメイン設定を行えばこの構成になるので、あまり変わったことはしていません。

開発環境の全体像

Cloudflare Pages が本番用ブランチ以外のブランチへの deploy で preview 環境を作成してくれるので、開発用の環境は素直にそれを使います。

一方、開発環境は非公開とするため前段に Cloudflare Zero Trust の Application を設定します。

(実際のデータの流れを描いているわけではなく、概念図として見ていただけると幸いです)

development build flow

これにより開発環境に認証機構を導入でき、認可された人だけがアクセスできる開発環境を実現できます。

各種 IdP も対応しているので GitHub などでログインできるのはエンジニアとしては嬉しいです。

下書きプレビュー環境

「下書きプレビュー」をリアルタイムに行うため、Cloudflare 用に SSR でビルドしたものを Cloudflare Pages にデプロイします。

(Editors と Previewers は実際には同じ人です。作図の都合で分けています。)

preview build flow

Astro の integration に Cloudflare Pages Functions 用のものがあるので、こちらを使えば Cloudflare Pages 上で動く SSR ビルドが行なえます。

@astrojs/cloudflare 🚀 Astro ドキュメント

一方で、Cloudflare Pages Functions (Cloudflare Workers) は CDN の Edge で動くことを想定したいわゆる「エッジコンピューティング」であり、Node.js の API が(基本的には)使えません。

そのため例えば CMS へのリクエストをキャッシュするために fs パッケージを使っていたり、画像の最適化のために @astrojs/image を利用しているとエラーになります。

実行時エラーになるなら回避しやすいのですが、import 時点でエラーが生じビルドに失敗したため対応を要しました。具体的には Astro の設定にて vite.resolve.alias を設定することでモジュール単位で差し替えを行うなどの工夫をしています。

想定はしておりましたが、下書きプレビュー環境の構築は SSG での前提が覆ることがあり、本番環境の構築よりもむしろ時間がかかったかもしれません。とはいえ想定通りではあり、トレードオフの塩梅としては良い落とし所になったと思っています。

余談ですが生じた問題は概ね既知の課題として認識されているものであり、そのうち解消されるかもしれません。

例:@astrojs/image の対応

@astrojs/image SSR support for non-Node runtimes · Issue #4109 · withastro/astro

例:Cloudflare Workers の Node.js API 対応

Node.js compatibility for Cloudflare Workers – starting with Async Context Tracking, EventEmitter, Buffer, assert, and util

更に余談というかメモですが、Astro の Debug コンポーネントという便利なものを使用したところ、ハイライト周りで色々使っているようで下書きプレビュー環境構築でエラーが多発しました。気づきづらかったのでメモしておきます。

その他

アクセシビリティ

見え方については個人的な感想以上のことはデザインの専門家に一任していますが、代替テキストなどは(主にフロントエンド担当が)一通り対応している認識です。

また、スクリーンリーダー(音声読み上げ)での操作感も一通り確認したつもりではいますが、なにかお気づきの点があれば連絡いただけますと助かります。

(大変恐縮ではありますが、公開の場でやり取りするとしいたけ占い公式サイトとしての公式なスタンス表明と受け取られてしまう可能性があるため、個人的に連絡いただけると大変助かります。)

コンテンツの自動バリデーション

利用している CMS の機能だけではコンテンツの正しさが判定できないパターンが色々とありました。早期に気づき対応できるよう、スクリプト化したバリデーターを GitHub Actions でコンテンツの更新時や定期的に実行するようにしています。

また、CMS からの取得データも Zod でスキーマ定義し、型アサーションではなく正しく想定しているデータ状態かを保証できる環境を整えました。

後には textlint などの導入も検討し、フローのさらなる改善を図っていきたいです。

ダミーコンテンツの補填

CMS のスキーマ定義と開発と入稿が並列して進行するため、開発者目線では未入力の項目があっても開発画面ではそれっぽく表示されて欲しいと考えます。

取得データに不足があった場合の挙動と補填内容を環境変数などで切り替えられるように整備し、それぞれの環境での欲求に答えられるように整備しました。

Visual Regression Test

しいたけ占い公式サイト ではページパターン数が少ないこともあり、デザイン作成においても実装反映においても認識合わせはパーツ単位ではなくページ単位で進めていくことが多くなりました。

よって Storybook のようなツールの活用タイミングは少なく、実際の成果物を見て互いにフィードバックを行って進めるフローがチームに合いました。

一方で、開発終期ではデザイン確認済みの実装に対し最適化や設定調整などの横断的な変更を行います。全体の変更確認を都度行うのは現実的でないため、Visual Regression Test を導入しました。

基本的な方針として、CI での自動チェックは目指さずシンプルに手元で差分確認を行えることを目指します。

詳細は省略しますが、基本的には Playwright で全ページのスクリーンショットを比較したい 2 環境でそれぞれ撮影し、差分の有無を検証します。

全ページを確認するために spec ファイルは自動生成するスクリプトを整備しました。SSG なので local で build した結果から index.html ファイルを探索すると楽です。

(補足:認識合わせがページ単位というだけで、デザインや実装ではコンポーネントを意識した設計になっています。そのくらいの勘所は前提にしていい信頼が互いにあったので、ページ単位のフィードバックでフローが回せました。)

以上

以上、「しいたけ占い公式サイト」の開発についてのご紹介でした。

サイト公開後は想定通り大きな反響で多大なアクセスが発生しておりますが、システム停止もなく快適に閲覧いただけているようでとても嬉しいです。

しいたけ占い公式サイト」に限らずではありますが、大切に愛されているものをお届けする手助けができることを名誉に思いつつ、期待に答え続けられるよう気を引き締めてまいります。