一迅プラスのインフラ構成について

こんにちは。

メディアサービス開発部バックエンド開発グループのフサギコ(髙﨑)です。

Ruby on Railsによるバックエンドの実装運用と、AWSによるサービスインフラの設計構築を中心とした、いわゆるテックリードのような立ち位置で働いています。

本記事では株式会社一迅社さまの公式漫画連載サイトであり、ブックウォーカーが開発保守運用を担当している一迅プラスのサービスインフラの概要についてご紹介したいと思います。

もしよければ、前記事のニコニコ漫画のインフラ構成についてならびに読書メーターのインフラ構成についてもご覧ください。

一迅プラスについて

一迅プラスは株式会社一迅社さまが運営する公式漫画連載サービスです。
冒頭試し読みから連載まで、2022/06/10現在で150を超える作品が掲載されています。

一迅プラスのトップページ

この一迅プラスにおいてブックウォーカーは開発保守運用を担当しています。

一迅プラスの構成

一迅プラスは大きく分けると下記の5つから構成されています。

  • ユーザ向けWebフロントエンド
    • ユーザ向けWebフロントエンド(Next.js)
    • ユーザ認証基盤(Firabase Authentication)
  • 運用者向けWebフロントエンド
    • 管理ツール(Next.js)
    • 簡易管理ツール(react-admin)
    • 運用者認証基盤(AWS Cognito)
  • バックエンド(Ruby on Rails)
  • 画像配信・生成系(AWS CloudFront & Lambda)
  • データ集計系(Google Analytics 4 & BigQuery)

これらの関係を概要図に表すと下記のようになっています。

一迅プラスの構成概要図

これらのサービスインフラを主にTerraformを用いて構成管理しています。

ユーザ向けWebフロントエンド

ここにおけるユーザとは、一迅プラスへ漫画を読みに訪れる読者、いわゆるエンドユーザを指します。 すなわちユーザ向けWebフロントエンドとは一迅プラスを配信提供しているサーバのことです。

ブックウォーカーのフロントエンドチームがNext.jsで実装し、GitHub ActionsとecspressoでCI/CD、ECS Fargateで実行しています。

細かいライブラリ構成などについてはフロントエンドチームのエンジニアが別途記事を書いてくれるかもしれませんが、 SWRやサーバサイドレンダリングなどを適時使用しているためモバイル、デスクトップともに比較的良好なCore Web Vitalsの点数を維持できていると思います。

一迅プラスではユーザが登録・ログインできる機能が実装されていますが、これに使う認証サービスはフロントエンドチームに使用経験があること、設定項目が少なめであることからFirebase Authenticationを採用しました。

運用者向けWebフロントエンド

運用者とは、一迅社さまの各編集部に所属する編集者の方々や、ブックウォーカーの担当者の総称です。

運用者は掲載する作品やエピソードを作成し、原稿をアップロードし、公開期間を指定するなどの多様な操作をシステムに対して行うため、ユーザとは異なる側面からサービスを利用する第二の利用者と言えます。

管理ツール

運用者が行う操作のうち、原稿のアップロードや公開期間の指定などの日常的に行う業務を対象にした、リッチな管理ツールです。

デザイナーとフロントエンドチームのエンジニアがFigmaなどを使って手順や導線を整理し、前述の日常的に行う業務を迷わず便利に行えるようになっています。

管理ツールの作品詳細画面とエピソード詳細画面の原稿画像タブ

これもユーザ向けWebフロントエンドと同様にNext.jsで実装されていますが、運用者向け画面はユーザ向け画面ほどの高速な表示は求められないため、クライアントサイドレンダリングのみで実装しています。

その代わりに状態管理ライブラリとしてRecoil.jsを採用してみたのですが、新鋭のライブラリであるため良い実装方針が社内に蓄積されておらず、少々扱いづらい実装になってしまっています。

具体的には、

  • atomが良くも悪くもどこからでも呼べてしまうためかなり注意深く実装しないとスパゲッティになる
  • 入力フォームのバリデーションが表現しづらい
    • react-final-formなどの定番の入力フォーム状態管理ライブラリを採用するべきだったのか
    • もしそうであるとするならばどこを責任分界点として棲み分けするのか
    • もしreact-final-formに移行するならばどのようにして表向きの動作を変えないままスムーズに移行するか
  • 入力フォーム状態管理だけでなくデータフェッチでもreact-swrとの棲み分けは?

などの課題があります。

GitHub ActionsとecspressoでCI/CD、ECS Fargateで実行しているのはユーザ向けと同様です。

簡易管理ツール

管理ツールは日常的な業務を迷わず行えるようになっている反面、デザイナーとフロントエンドチームの稼働が発生するため改修のリードタイムが長くなるデメリットもあります。

そこで、バックエンドチーム内で完結する程度の簡易な実装で、主に社内の担当者向けに機能提供までのリードタイム短縮を狙うのがこの簡易管理ツールです。 いったんはこの簡易管理ツール上で実装し、利用頻度が高いものから管理ツールへ実装していく流れを意図しています。

簡易管理ツール

具体的にはreact-adminというnpmパッケージを使用しています。 これが運用者のブラウザ上で動作し、後述するバックエンドの管理ツール系APIへjsonで直接リクエストしています。

react-adminはbuildすると単体のjsファイルになります。これをマウントするようなペライチのhtmlファイルとともにnginxのdockerイメージに載せてGitHub Actionsとecspressoでデプロイ、ECS Fargateで起動して配信しています。

運用者認証基盤

運用者の認証は棚卸しを不要にするため、会社業務で利用しているGoogle WorkspaceとのOpenID Connect連携でログインできるようになっています。

運用者の認証にあたっては、無関係な人物に管理ツールを利用させてはならないため、メールアドレスのドメイン部をホワイトリスト方式でチェックしなければなりません。 そのため、OpenID Connectを認可してから認証サービス上にアカウントが作成されるまでの間に割り込んで処理を行えるようなフックが必要です。 しかし、Firebase Authenticationにはそのようなフックが存在しなかったため、運用者の認証基盤としてはFirebase AuthenticationではなくAWS Cognitoを使用しています。

バックエンド

Ruby on Railsの最新7.0系で実装1し、CircleCIとecspressoでCI/CD、ECS Fargateで実行しているバックエンドです。

フロントにNext.jsが立っていることからバックエンドは純粋なJSON APIのみとなっており、実装上の特徴としては

  • レスポンスとなるJSONをレンダリングする際にjbuilderなどのテンプレートエンジンを使わず、to_hashメソッドを持ったピュアなClassのインスタンスをrenderメソッドにjsonキーワード引数で渡している
  • Firebase AuthenticationのJWT 2を検証する、外部のAPIを特定用途で呼ぶ、などの一定以上複雑かつ単一用途で明確に分離できる処理をcallメソッドのみがpublicであるようなサービスクラスとして切り出している

が挙げられます。

また、リソース操作に関する認可においてはユーザ、運用者ともにCanCanCanを使用しています。

CanCanCan含む認可系gem3種の概観比較とニコニコ漫画における認可のしくみについてお話した記事が過去にありますので、もしよろしければそちらもご覧ください。 developers.bookwalker.jp

データベースに目を向けると、一迅プラスではAWS AuroraのPostgreSQL互換を使用しています。 これは

  • MySQLのEXPLAINはお世辞にも見やすいとは言えない
  • CanCanCanが認可絡みでDB問い合わせをする際にサブクエリを駆使する3が、MySQLはサブクエリに弱い
  • 私がマストドンの運用でPostgreSQLを扱った経験がある

あたりが理由です。

JSON APIのスキーマ定義について

APIを作るうえで避けられないのがAPI仕様の定義です。 この節ではユーザ向けと運用者向けそれぞれについて、提供側であるバックエンドと利用側がどのようにして仕様を共有しているかについてお話します。

ユーザ向け

ユーザ向けについては別リポジトリにおいてOpenAPI v3のYAMLで記述し、openapi-generator-cliでjson形式に変換しています。 なお、変換前のYAMLも変換後のjsonもどちらも同時にcommitしています。

レスポンス構造の変更や新しいエンドポイントを追加する場合には、OpenAPIのリポジトリへPRを出します。 フロントエンドチームとバックエンドチームそれぞれがレビューして同意、マージすれば、それ以降はそれぞれが並列に実装できます。

フロントエンドはopenapi-typescriptを使ってjson形式からTypeScriptの型注釈へ変換したものをGitHub Packagesのprivate npm packageとしてreleaseして使用しています。

一方、バックエンドではOpenAPIのリポジトリをgitのsubmoduleとして取り込み、committee gemにjson形式の定義を読み込ませ、rspec上でレスポンスの構造が正しいかどうかをテストしています。

運用者向け

運用者向けである管理ツール、簡易管理ツールが利用するAPIは、ユーザ向けとは対照的に、あえてOpenAPIなどでのスキーマ定義をしていません。

これは主に簡易管理ツールを運用者の要望に応じて短時間で変更したいからで、スキーマ定義をしないかわり、 特定ユースケースのpathや階層を設けず完全にリソース種別ごとのフラットな構造にするという規約によって実装も利用も簡単にしています。

そういう意味ではユーザ向けと比べるとより厳密にRESTfulなAPIになっていると言えます。 また、レスポンスとなるJSONをレンダリングする際にjbuilderなどのテンプレートエンジンを使わず、to_hashメソッドを持ったViewModelクラスをrenderメソッドに渡す、という実装方針がここで地味に有利に働いている4と思います。

フロントエンドチームも関わる管理ツールにおいては、リクエストパラメータやレスポンスの型注釈、SWRでfetchするカスタムフック、axiosを使ったPOST, PATCH, DELETEリクエストを行う関数を私が直接書きました。

画像生成・配信系

作品の表紙、エピソードのサムネイル、ページ、作品別のOGP画像など、静的でない画像を生成・配信するためのCloudfront等です。

  • オリジナル画像をS3に持っておき、CloudfrontのOriginRequest時にLambdaがAPIGatewayを通じて動的生成 & 生成結果をCloudfrontでキャッシュ、配信
  • backendが必要に応じてSQSにメッセージをキューイングし、Lambdaがそのメッセージから起動して画像を静的生成してS3に格納 & CloudfrontはS3をOriginとして配信

という2通りの生成・配信方法を使い分ける形になっています。

Lambdaを中心としたいわゆるサーバレスな部分はアプリケーションコード的な側面とインフラ的な側面が混ざりあっているためどうやってコードとして管理するか悩ましい面もありますが、 一迅プラスの場合はCloudfrontやSQSのキュー、Lambdaに与えるIAMロールまでをTerraformで、LambdaならびにAPI Gateway部分をServerless frameworkで管理しています。

データ集計系

いわゆるPV数や、単位期間ごとのアクティブユニークユーザ数、エピソードごとの閲覧回数などの集計、推計に使うGoogle AnalyticsとBigQueryです。

一迅プラスは比較的最近に始まったサービスのため、サービス開始当初から最新のGoogle Analytics 4を使用しています。

Google Analytics 4ではBigQueryへイベントデータのエクスポートが簡単に設定できるため、それを元にイベントごとの中間テーブルや日次、週次、月次の集計をスケジュールクエリで行っています。 BigQueryのテーブル定義やスケジュールクエリについてはTerraformで管理しています。

Google Analytics 4からBigQueryへのイベントデータのエクスポートについては同僚の長田の記事がありますので、もしよろしければそちらもご覧ください。 developers.bookwalker.jp

まとめ

本記事では、一迅プラスを構成するコンポーネントとその関係について概観しました。

一迅プラスはメディアサービス開発部が担当するサービスの中ではかなり新しいサービスです。 そのためインフラ構成やWebフレームワークの選定といった設計面は比較的モダンな構成にできていると思います。 一方、それは裏を返せば後発ゆえに実現できていない機能がまだまだたくさんあるということでもあります。

ブックウォーカーでは物理・電子・Web連載問わず漫画や本が好き、あるいは将来の電子書籍や漫画連載をより良い設計ならびに実装で実現していくことに興味があるWebアプリケーションエンジニアを募集しています。

もし興味がありましたらぜひ、ブックウォーカーの採用情報ページからご応募ください。


  1. 初期は6.1でしたが、rspecでリクエストスペックを比較的手厚く書いたことで安心してアップデートできました

  2. JSON Web Token。jsonで表現した任意の情報に対して電子署名で発行者の正当性を証明できる、トークンの表現形式の規格

  3. オプションでLEFT JOINを使う方法も指定できるが、現行最新の3系はサブクエリを使う方法がデフォルト

  4. ViewModelのto_hashメソッドを見ればjsonにどのようなキーで何が入っているか一目でわかる