真っ当な技術を使ったふつうのWebサービス開発

野崎翔太氏(以下、野崎):まず最初に自己紹介をさせて下さい。僕は今年の1月にログミーに入社した、社内で唯一のエンジニアです。そのため、サービスの技術的な所は分野に関わらず、すべて担当しています。技術的な部分以外ですと、サービスのWebデザインも担当しています。

Web上ではemonkakというハンドルで活動しています。直近ですとFeedponというLivedoor ReaderライクなRSSリーダーを開発して公開しています。あとは仕事でメインで使っていたのもあって、PHPのライブラリをいくつか公開していたりもしますが、PHPが好きという訳でないです。好きな言語はHaskellです。

続いて、ログミーのサービスについて簡単に説明させて下さい。ログミーは世界をログする書き起こしメディアということで、主にリアルで開催されたイベントの書き起こしを記事にして配信しています。

メディアの展開としましては、主としてIT、スタートアップ関係のイベントを書き起こすログミー本体の他に、エンジニア向け勉強会の書き起こしをするログミーTech、投資家向けに決算説明会の書き起こしをするログミーファイナンスがあります。

今回のセッションでお話しするのは、ログミー本体とログミーTechのリニューアルになります。ログミーファイナンスにつきまして、別システムでリニューアルをする予定で、現在開発中となっています。

リニューアル以前のシステムについて

その前に、まずはリニューアル以前のシステムについてどうだったのかお話します。

以前のシステムはWordPressで独自にテーマ、プラグインを開発して運用していました。この開発・運用、あとはデザインに関して社外のエンジニア・デザイナーの方にお願いしていました。

このWordPressのシステムに先程お話した、ログミー、ログミーTech、ログミーファイナンスの3つのメディアがすべて載っていた形になります。現在はログミーファインスについてのみ、このWordPressの旧システムで動作しています。

WordPressについてはプラグインやテーマが豊富で、手軽に扱えるCMSではありますが、みなさんご存知の通り、作りはレガシーで技術的には少々問題があります。そういったこともあって、ちょっとした機能追加も中々面倒で、注意しないとすぐにコードも負債化してしまい保守も難しくなります。

特に問題だったのが遅いという点で、ユーザーはページがなかなか表示されなくてストレスになりますし、編集部の業務効率も落ちます。実際にレスポンスを返すのに約2000msもの時間を要していた時期がありました。これはちょっとWebサービスとして有り得ないレベルですよね。

リニューアルを進めているとはいえ、それまでずっとこのままにしておくのは流石に許容できませんでした。これはキャッシュやテーマのチューニングで約400msまではなんとか改善させました。それでもまだ遅いんですけどね。

過去のリニューアルと今回の開発体制

そしてリニューアルにということになるのですが、実は僕の入社以前からシステムのリニューアルプロジェクトは進行していました。しかし、このプロジェクトは失敗に終わってしまいました。というより僕の方からプロジェクトの終了を提案しました。僕の入社後最初の1ヶ月の仕事はこれですね。

このプロジェクトは、現行のWordPressのシステムから必要な機能だけに絞って、PHPのポピュラーなWebフレームワークであるLaravelで再実装するというものです。まずはリニューアルにあたって技術的な課題を解決することを優先したということですね。開発者主導でビジネスサイドの要望は後回し、とりあえずシステムを新しくすると。

しかし、ビジネスサイドの要望をヒアリングしてみると、当然ながら現行のシステムの再発明がしたいということではなかった訳です。それはこのリニューアルをそのまま進めたとして、すぐに大幅に設計を改めなければならないということを意味しました。

そこで体制も新たにプロジェクトをリスタートさせることにしました。

開発は完全に内製化、さらにドメイン駆動設計を導入し、編集部の業務をヒアリングし、ビジネスサイドの要望を受けながら特定の「技術」ではなく「ドメイン」駆動で開発を進めていくことにしました。

そこでまずは、ドメインエキスパートと開発者の共通言語たるユビキタス言語を定義して、そこからドメインモデルを構築しました。

ドメインモデルの構築

ドメインモデルを構築するとは言っても、システム上で行うメディアの業務は記事を作成してそれを公開するといった単純なものです。実際に悩む所は少なかったのですが、ログミー独自の概念「シリーズ」というものがありまして、これの扱いに関しては苦労する所がありました。

「シリーズ」の言葉通り日本語で言う「連載」という続きもの記事の集合であれば話は単純だったのですが、それは正確な理解ではありませんでした。

なんと「シリーズ」は親子関係を持つことができて、この時点で単純に続きものの記事の集合とは言えない訳です。

しかし何らかの関連性は持っていると。これを理解する鍵がこの親子関係が最大3代までしかないことにありました。

実は存在するすべてのシリーズが3代ある訳ではないことが理解を妨げていたのですが、シリーズのそれぞれの代は役割が異なるものだったのです。別々の概念をシリーズという言葉でまとめていて、それを親子関係にしていたわけです。

当初、このことはドメインエキスパートたる編集部の人間でさえ体系的には理解していなくて、ディスカッションを重ねてドメイン知識を蒸留することで得ることのできた新しい知識です。

そして、これら三代のシリーズは、上の代からそれぞれ、イベントの主催者たる「コミュニティ」、そのコミュニティによって開催された「イベント」、イベントで行なわれたセッションを記録した記事の集合たる「ログ」というように、明確に定義することができました。

最後に余談ですが、seriesは単数形と複数形の区別がない単複同形の名詞なのでコード上で使うとちょっとわかりにくい単語ですね。

技術選定の基準

さて、ここまででドメインモデルの構築まで終わりました。ここから先は技術の話です。

まずサービスをリニューアルするということは、現状のシステムに何らかの課題があるというのと、そのサービスの売り上げが立っている訳ですよね。売れていないサービスをリニューアルすることは普通ないはずで、やるとしたらコストをなるべく掛けずに部分的にちょっとずつ直すという感じかと思います。また、売り上げが立っているということは、サービスの継続可能性が高い訳で、長期間の保守に耐えられる設計が必要です。

逆に売れるかわからない新規のサービスは、保守性よりもコストを掛けずにいかに早く作るのかがより重要になります。とはいえ、保守性を犠牲していいという訳ではありませんけどね。

このセッションのタイトルにもありますように、今回のリニューアルに関しては真っ当な技術を選択するという方針で技術を選択しました。「真っ当な」というのは、標準化・規格化された技術、あるいはデファクトスタンダードの技術を示す意味で使っています。

これらの技術は長期間のサポートが期待されますし、ある規格に準拠した複数の実装があったとして、その間の移行も容易です。今使っている実装が古くなって、新しい良い実装が出てきたら、それを捨てて乗り換えることができる訳です。

一方で枯れた技術という言葉もあります。言い換えるなら成熟して安定しているので、更新をほとんど必要としない技術でしょうか。このような技術もまた長期間の保守という観点からも望ましいです。

しかし、有用な枯れた技術というのはあまりないのではないかと考えています。というのも、一定の複雑さを持った技術というのは、使われ続ける限り何だかんだ更新が発生するからです。更新が発生し続ける限りなかなか技術が成熟しない。つまりは枯れないということです。

また、外的要員の変化によって使われなくなった技術というのもあります。しかし、これを枯れた技術だと有り難がっても良いことはないですよね。

バックエンドの構成について

まず、アプリの実装にはPHPを採用しています。フロントにはnginxを置いてFastCGIでPHPとやり取りをするという、いたって「ふつう」の構成です。

何故PHPかといいますと、最初のリニューアルプロジェクトがPHPでLaravelを使って進んでいたので、そのままそれを踏襲した形ですね。個人的にPHPは好みませんが、動的型付け言語でありながら型注釈を付けてある程度は型安全なコードが書けるので、そう悪くはないと思います。何だかんだ利用者も多くライブラリも充実していますしね。システム要件としてもいたって「ふつう」のWebサービスなので、PHPで困ることも考えにくいです。

あとはデータベースにMySQL、キャッシュとセッションの管理にmemcached、ジョブスケジューラにcrondと、これも「ふつう」の構成ですね。

バックエンドのシステム全体としては、現状はモノリシックな構成となっています。ただ今後は、アクセスを集計して、ランキング・関連記事の取得などをするレコメンド処理を、マイクロサービスとして切り分けることを予定しています。

PHP使っている方はご存知だと思いますが、PHP-FIGという組織がありまして、ここがPSRという形で様々な仕様の標準化を勧告しています。Javaで言うところのJSRのようなものですね。この勧告はPHPコミュニティでかなりの影響力があって、数多くのライブラリ、フレームワークがこの勧告で規定されるインターフェイスを実装しています。

そのPSRの中にHTTPサーバーのインターフェイスを定義したPSR-15という勧告があります。これはリクエストを受け取ってレスポンスを返すというサーバーの動作を抽象化した、いたってシンプルなものです。

PSR-15のインターフェイス

実際のインターフェイスはこのような定義です。

RequestHandlerInterface というのがMVCスタイルのWebフレームワークで言う所のコントローラーに相当するものですね。リクエストを受け取ってレスポンスを返すと。MiddlewareInterface についてはnode.jsのExpressだとか、最近のWebフレームワークに良くある仕組みで、渡ってきたリクエスト、あるいは生成されたレスポンスに対して何か途中で処理を挟むための仕組みですね。

あとはこのリクエストハンドラーとミドルウェアを実行する仕組みがあれば、リクエストを生成してレスポンスを返すという、HTTPサーバーの基本的な仕組みはできてしまう訳です。

今回のリニューアルではこのPSR-15を利用することで、特定のWebフレームワークを利用せずにアプリを実装することにしました。

ありもののWebフレームワーク、特にRailsなどのフルスタックのフレームワークは機能が豊富で便利ではありますが、更新の頻度が比較的多いです。そのため、長期の運用で最新バージョンに追従し続けるのもなかなか大変です。

プロトタイプを早く作るという場面では効果的ではありますが、今回は長期の運用がよりし易いシンプルな形にしたかったので、フレームワークレスという形を取りました。フレームワークレスと言っても、Serverlessが実際にはサーバーがあるように、フレームワークがない訳でないのですが、その存在をほぼ意識しないで開発することができるという意味ですね。

続いて、ルーティングに関してはトライ木を使った独自の仕組みをPSR-15のミドルウェアとして実装しています。これはパスをスラッシュ区切りでトライ木を構築してキャッシュ、そこから高速にルーティングできるというのが特徴となっています。

PSR-15の他には、DIコンテナのインターフェイスを定義しているPSR-11も利用しています。具体的にはPSR-15のリクエストハンドラーを実装したクラスにコンストラクターインジェクションする形ですね。DIコンテナの実装としては自前で開発したものを使っていまして、これはクラスの依存グラフを丸ごとキャッシュしてしまうことで、インスタンス化を高速に行えるのが特徴となっています。

永続化とORM

PSR-15でHTTPサーバーの実装ができた訳ですが、Webサービスにはもう1つ重要な技術要素として永続化があります。

永続化に関してはJavaでいうJPAのようなものは残念ながらPSRには存在しません。ただいずれにせよ、JPAのようなものはちょっと複雑すぎるのでもっとシンプルなものが欲しいです。

しかし、その要望を満たすようなライブラリが無かったので、これもまた自前で開発しました。いわゆるData-MapperスタイルのORMで、マッピングされる対象のオブジェクトは永続化に関する振舞いのないプレーンなオブジェクトとしてして定義できます。

一方で、アプリケーション層、あるいはドメイン層が独自のライブラリに依存するのはある主のリスクです。その点、このORMに依存するのはRepositoryの実装のみで、Entityはプレーンなオブジェクトとして定義できます。このことはDDDと相性も良いです。

このORMを使ったリポジトリの実装はおおよそこのようになっています。

SQL文法を表すGrammarInterfaceからクエリビルダーを作成して、クエリを構築。あとは getResult() でArticleクラスにマッピングされたオブジェクトの結果セットを取得すると。最後に結果セットから firstOrDefault() で最初の要素を取り出して、なかったらnullを返すということをしています。

結果セットはこのようにC#のLINQ相当の豊富な操作ができるようになっています。 あとは、with() メソッドでリレーションを与えることができて、一対一、一対多、多対多、ポリモーフィック関連などの関連を表現できます。

先程までの話はドメイン層のEntityの永続化に関するものでした。一方で、プレゼンテーションで標準するためにデータをどう表現するのかという問題もあります。ドメイン層のEntityとValue Objectをそのままプレゼンテーション用のモデルとして用いることができれば話は単純ですか、そうはいかない場合があります。ドメインモデルにプレゼンテーションの振る舞いが混ざってしまうのも良くありません。

そこでCQRSという更新と読み取りを分けて設計しましょうという考え方を利用します。 更新に対応するコマンドモデルはドメインモデルをそのまま使い、読取に対応するクエリモデルをプレゼンテーションモデルとして新たにを作ります。プレゼンテーションモデルの実装に関しては、Getterだけを持った「○○Data」というクラスを、リポジトリに対応するものは「○○QueryService」のような名称でそれぞれアプリケーション層に作成しています。コマンドモデルがEntityとRepositoryで、クエリモデルがDataとQueryServiceといった形ですね。

フロントエンドの構成について

次は、フロントエンドの構成についてお話します。

まず、JSのビルドツールについてはベーシックにWebpackのLoaderを通してBabelとTypeScriptを利用しています。今回はIE11対応も必要でしたのでPolyfillの注入だったり、ES2015文法のトランスパイルにBabelを利用しています。

フロントの作りとしてはSPAではなく各ページごとに独立しているMulti Page Applicationで、いわゆる伝統的なWebページですね。MPAなので当然ながらServer-Side Rendering等もないので要件としてはいたって「ふつう」です。

ただMPAの世界で意外と困るのが、JSのブートストラップをどこでやるのかという問題です。

例えばあるコンポーネントのhtmlがあったとして、このhtmlに対応するJSをどこで実行するかという話です。

jQueryの時代ですとこのようなコードがHTML内のscriptタグに直接書かれたり、ページごとにJSファイル作ってそれを読み込んでいましたね。しかし、この方法はコンポーネント単位での再利用性が難しくなりますし、JSのコードとhtmlの情報が離れてしまって管理も難しいです。

Reactなんかを使う場合でみMPAの場合は ReactDOM.render() をどこに書くべきかとう問題が生じます。MPAだとページ全体をReactのコンポーネントにはしないので、そのページ内に配置されるすべてのコンポーネントについて ReactDOM.render() をどこかで呼ばないといけません。

WebComponents + lit-html

そこで考えたのがWebComponentsを使うという方法です。

WebComponentsモダンブラウザではネイティブサポートされていますし、非対応のIE11でもPolyfill読み込めば問題なく動作します。ブラウザの機能なのでpolyfill以外のライブラリが必要ないのもメリットです。

WebComponentsでカスタム要素を定義すれば、あとはタグを書くだけでJSのブートストラップは自動的やってくれます。これで、ブートスラップコードをどこに書くべきかという問題はそもそも存在しなくなった訳です。

あとはWebComponentsの機能そのままだとDOMツリーをひたすら手で触らないといけないので、ReactライクにHTMLを簡単に描画してくれる仕組みが欲しくなります。これを StatefulComponent として独自に作成しました。Statefulというのは内部に状態を持っていて、Reactのコンポーネントのように setState() ができるということですね。

HTMLの描画はどうしてるかと言うと、lit-htmlというES2015のTemplate Literalを利用したHTMLテンプレートの実装を使っています。Reactだとかの仮想DOMの仕組みはMPAで使うにはちょっと大袈裟すぎるので、フットプリントの小さいlit-htmlを採用しました。

FluxのようなComponent間のメッセージングはブラウザのイベントの仕組みをそのまま使っていまして、イベントを受け取った各コンポーネントがそれぞれの内部の状態を適宜更新するということをしています。

最終的にこの StatefulComponent は全体として400行程度のコンパクトな実装になりました。スライドにソースのリンクを貼っておきましたので興味のある方は読んでもらえればと思います。

インフラの構成とリニューアルで変わったこと

次にインフラの構成について簡単にお話します。

クラウドは元々AWSを使っていまして、リニューアル後も特に変える理由もなかったので継続して利用しています。リニューアル後変わった所としましては、以前はEC2のインスタンス上でWordPressを稼動していた所を、ECSを使ってコンテナ上でアプリケーションを稼動させるようにしました。ECSのバックエンドとしましてはEC2を経由せず、直接Dockerコンテナを起動させることのできるFargateを使っています。

あとはデプロイについては、以前はサーバーに直接入ってgit pullを叩くという牧歌的な仕組みだった訳ですが、これをCodePipelineを使ってコンテナのビルドとデプロイを自動化する仕組みを作りました。フローとしてはGitHubのリポジトリのmasterブランチにソースをpushすると、CodeBuildによる各種コンテナのビルドが開始されます。

ビルドに成功するとECRにコンテナイメージが登録されて、そのことをトリガーにデプロイ用のLambda Fcuntionが起動します。CodePipelineにもECSにデプロイする仕組みはあるのですが、これはコンテナイメージのバージョンのみを更新するもので、コンテナに与える各種設定までは変えてくれません。FargateであればCPUとMemoryの指定も含まれます。これを解決するために自前でデプロイする仕組みをLambda Functionで作ったというわけです。

まとめ

最後にまとめです。

サービスのリニューアルは売り上げが立っているので、サービスの継続可能性は高いだろうと。だから長期間の保守に耐えられる設計が必要になりました。

具体的にはサーバーサイドではフルスタックのフレームワークの利用を避けて、PSRによって標準化されたライブラリを組み合わせて実装。フロントエンドでは、ブラウザ組込みの機能とHTMLテンプレートライブラリであるlit-htmlを使って、フットプリントの小さなコンポーネントを作成。インフラは、コンテナ技術をフル活用しつつ、デプロイも完全自動化して、運用コストを削減しました。

最後に今回はお話する時間はありませんでしたが、サービスのデザインについては長文が読み易いようにということを心掛けてUIを設計しました。

今回の発表は以上となります。ご清聴ありがとうございました。