GigaViewer の配信基盤を支えるマルチテナントアーキテクチャ

執筆: id:chaya2z

この記事は SRE 連載の 3 月号です。 2 月の記事は id:koudenpa さんの Amazon AuroraのCPU世代を上げるとどうなる? でした。

はじめに

GigaViewer は、はてなが提供するマンガビューワーです。 2017 年のリリースから 2026 年 3 月現在で 20 以上のサービスに導入されています。

各サービスには専属のデザイナーや独自のマーケティング戦略があり、配信基盤がサービス固有の要件を制約しないことが求められます。 加えて、作品の更新タイミングでトラフィックが急増し、そのスケジュールもサービスごとに異なります。 この 2 つの要件を満たしながら、インフラの運用コストをサービス数に比例させない仕組みが必要でした。 この記事では、マルチテナント SaaS の設計パターンを応用したアーキテクチャと、移行過程で得られた知見を紹介します。

技術的な課題

スケーラビリティ: 人気作品の更新時には、1 分間でトラフィックが平常時の 10 倍以上に急増します。 以前の構成ではオートスケールが未導入で、常にピークを想定したキャパシティを確保していました。

図1: サービスごとのトラフィック変動 (対数スケール)。夜間や日中など、サービスごとに異なる時間帯にスパイクが発生しているのがわかる。

コスト効率: インフラ環境をサービスごとに独立させると、サービスの追加がそのままコスト増に直結します。

運用負荷: デプロイは開発者のローカルマシンからの手動実行でした。 ミドルウェアの更新も SSH で仮想マシンに接続して行う運用で、サービス成長に伴いメンテナンスの工数が増大していました。

アーキテクチャ

前章の課題を解決するため、配信基盤をサイロモデルプールモデルを組み合わせたブリッジモデル1で設計しました。

図2: CDN をサービス専用、ロードバランサー・リバースプロキシをサービス共有とするブリッジモデルのアーキテクチャ

サイロモデル: CDN

CDN はサービスごとに独立したリソースを割り当てるサイロモデルを採用しています。

各サービスは独自のドメインと URL 構造を持っており、キャッシュ戦略やリダイレクトルールもサービスの要件に応じて異なります。 トレードオフとしてサービス追加のたびにリソースの構築が必要になりますが、Terraform によるモジュール化でプロビジョニングを標準化しているため追加の工数は許容範囲に収まっています。

サービスごとに作成するリソース:

  • Amazon CloudFront
  • TLS 証明書 (AWS Certificate Manager がメイン)
  • ドメイン (Amazon Route 53 がメイン)

プールモデル: ロードバランサー・リバースプロキシ

ロードバランサーとリバースプロキシは全サービスでリソースを共有するプールモデルを採用しています。

共通のコンテナイメージを使用することで、セキュリティパッチの適用をサービス数に依存しない一定のコストで実現しています。

さらに、全サービスのトラフィックを合算することでスパイクが平滑化され、サービスごとにピークキャパシティを確保する場合と比較してリソースコストを削減できます。

図3: プール化によるトラフィックの平滑化効果。サービス全体 (赤実線) で見れば最大約2.7倍の変動に収まっている

全サービスで共有するリソース:

  • Application Load Balancer (ALB)
  • Amazon ECS で運用する nginx

サイロとプールの境界

ブリッジモデルでは、どのレイヤーをサイロにし、どこをプールにするかの境界が設計上の重要な判断になります。

キャパシティ管理の要否:

  • CloudFront はフルマネージドでキャパシティ管理が不要なため、サイロにしてもリソースコストがサービス数に比例しない
  • ALB や ECS で運用する nginx はキャパシティ管理が必要であり、サービスごとに分離するとキャパシティプランニングがサービス数に比例して増加する

障害復旧の速度:

  • CloudFront はエッジロケーションへの設定反映に時間がかかるため、障害の影響範囲を事前にサービス単位で限定する必要がある
  • ECS で運用する nginx は Blue/Green デプロイやカナリアリリースにより障害の影響範囲を制御でき、プールのリスクを運用で許容できる

また、境界をどのレイヤーに引くかだけでなく、分離したレイヤー間をどう接続するかも設計上の課題になります。

配信基盤の移行戦略

ここまで述べたアーキテクチャを実現するため、EC2 で運用していた nginx のコンテナ化と ECS on Fargate への移行、および CloudFront 未導入サービスへの CloudFront 導入を進めました。

この移行は全サービスに影響する変更であるため、カナリアリリースで段階的に進めました。 ただし、CloudFront 導入済のサービスと未導入のサービスが混在していたため、カナリアの方法を分ける必要がありました。 CloudFront 導入済のサービスでは CloudFront の継続的デプロイを、未導入のサービスでは Route 53 の加重ルーティングを利用し、 いずれもメトリクスを確認しながらトラフィック比率を引き上げました

このカナリアリリースが実際に効果を発揮する場面がありました。 EC2 上の nginx は /// に正規化する、末尾の / を補完するといった URL の処理を暗黙的に行っており、アプリケーションがこの挙動に依存していました。 CloudFront にはこれらと同等の機能がなく、移行後にアプリケーションが想定しない URL を受け取る形で障害が発生しましたが、 カナリアリリース中の発覚だったため、影響範囲はカナリアに振り分けられたトラフィックに限定できました。

一方で、カナリアリリースと外形監視の相性に課題がありました。 外形監視は AWS 外部から CloudFront 経由でリクエストしていたため、監視リクエストが新環境と旧環境のどちらにルーティングされるかを制御できませんでした。 監視リクエストが旧環境に振り分けられ続けた場合、新環境で障害が発生してもアラートが発火しません。 実際に、アラートではなくユーザー報告で初めて気づいた障害がありました。

サイロとプールの橋渡し

サービス固有のリクエストを共通のプロキシが処理できる形に整えるため、 エッジ関数を活用しています。

エッジ関数には CloudFront Functions と AWS Lambda@Edge の 2 つの選択肢があり、制約に応じて使い分けています。

比較項目 CloudFront Functions Lambda@Edge
規模 毎秒最大数百万件のリクエスト リージョンごとに最大 10,000 件のリクエスト/秒 (デフォルトクォータ)
実行時間制限 サブミリ秒 5 ~ 30 秒 (トリガーにより異なる)
ネットワークアクセス 不可
リクエストあたりのコスト 約 1/6 基準

参考: Differences between CloudFront Functions and Lambda@Edge

本番環境では CloudFront Functions を採用しています。 キャッシュキーの正規化やヘッダー操作など、ネットワークアクセス不要な処理に限定されるため、低コスト・高スループットの CloudFront Functions が適しています。

ステージング・開発環境では Lambda@Edge を採用しています。 これらの環境ではネットワークアクセスを伴うアクセス制御が必須であるためです。

しかし、この使い分けにはトレードオフがあります。 CloudFront のビューワーリクエストイベントには CloudFront Functions と Lambda@Edge のどちらか一方しかアタッチできないため、 非本番環境で CloudFront Functions の動作を確認できません。 そこで、CloudFront のマネジメントコンソールのテスト機能で動作確認と実行時間の検証を行っています。

オリジンの保護

エッジ関数による変換を前提とするため、すべてのリクエストが CloudFront を経由する経路制御が必要です。 VPC Origin を採用し、プライベートサブネット内の ALB を CloudFront のオリジンとして指定することでリクエストの迂回を防いでいます。

CloudFront から ALB への内部通信には HTTPS を採用しています。 VPC Origin ではオリジンへの通信が AWS のバックボーンネットワークを経由するため HTTP で十分とする考え方もありますが、 セキュリティのベストプラクティスにある多層防御の考え方に基づき、アプリケーション層でも暗号化する方針としました。

しかし、HTTPS を採用したことでサービスごとの TLS 証明書の管理が課題になります。 この課題を SNI と Host ヘッダーを分離する設計で解決しています。 CloudFront から ALB への TLS ハンドシェイク (SNI) には全サービス共通の内部ドメインを使用し、ALB 側の証明書は 1 枚で済むようにしています。 一方、HTTP Host ヘッダーにはサービス固有のドメインを保持したままプロキシへ届け、リクエストを正しくルーティングします。

ふりかえり

nginx の ECS on Fargate への移行により、サービス数に比例しないコスト構造を実現しました。 加えて、デプロイを自動化したことで属人化を排除し、チームの誰もが設定変更を速やかに反映できるようになりました。 デプロイ担当者から心理的ハードルが下がったというフィードバックをもらっています。

CDN にサイロモデルを採用したことで CloudFront のデフォルトクォータを超過する場面がありました。 テナント数の多い環境でサイロモデルを採用する場合は、リソースのクォータを事前に確認しておくべきという学びがありました。

カナリアリリースは障害の影響範囲を限定できる一方、既存の監視体制がカナリア環境をカバーできるとは限らないため事前の検証が必要です。 また、比較的リクエスト数の少ないサービスからリリースを進めましたが、サービスごとに利用する機能が異なるため段階的リリースだけでは機能カバレッジを保証できません。

これで終わりではなく、今後も信頼性を維持しつつ、各サービスの成長を加速させるプラットフォームへと進化させていきたいと考えています。