DMMグループの一番深くておもしろいトコロ。
テクノロジー

DMMプレミアム特典ページにおけるdynamic renderingの実装

DMMグループの一番深くておもしろいトコロ。

この記事はDMMグループAdvent Calendar2022の22日目の記事です。

ITインフラ本部SRE部の小野輝也です。ついにDMMプレミアムがサービス開始しましたね。DMMプレミアムには目玉となるDMM TV以外にも、プレミアム会員限定の特典やクーポンがあるのをご存知でしょうか?今回はその中でも、プレミアム特典のページで利用されているdynamic renderingの実装について紹介します。

dynamic renderingとは?

検索クローラからのリクエストを受けた際にのみ、JavaScriptが生成するコンテンツをサーバ側でレンダリングして提供する技術のことです。ブラウザからの通常のリクエストに対してはレンダリングを実行しません。主にJavaScript生成コンテンツに対応していない検索クローラ向けに、完全なページを提供することでSEO対策に使われます。 dynamic renderingは回避策とされており、サーバサイドレンダリングやハイドレーションでの解決が推奨されてはいますが、今も多く使われるテクニックとされています。

利用されるツール

dynamic renderingを実現するためにはサーバ側でレンダリングするための技術が必要になります。レンダラにはheadless browserが使われることが一般的なようです。次のようなレンダラが知られています。

導入経緯

プレミアムの特典ページはシングルページアプリケーション(SPA)で構築されており、ビジネス上の理由でプロジェクトの途中からSEO対策の必要性が生じてきました。各種検索クローラの中にはJavaScriptを解釈しないものもあるため、サーバ側で何かしらのレンダリングの仕組みを用意する必要がありました。手法について選定した結果、重厚なサーバサイドレンダリングの仕組みを導入するほどでもなかったため、dynamic renderingの導入に至りました。

アーキテクチャ

dynamic renderingの導入が決定するまでは、フロントエンドをCloudFront + S3で配信していました。 そこからレンダラを追加する際に、次の2通りのアーキテクチャを考えました

1: Lambda@edgeの利用

CloudFrontのオリジンリクエストイベントにLambda@edgeを追加し、そこでレンダリングする方針です。 この方法は既存のアーキテクチャに対して最小限の変更で済むといったメリットがありますが、一方でLambda@edgeから開発環境のようなアクセス制限されたAPIなどへアクセスする方法については課題が残ります(後述)。

2: Nginx on ECSのサイドカーを利用

こちらはS3でのフロントエンド配信をやめて、CloudFront+ECSで配信する方式です。ECSタスクにはフロントエンド配信用のNginxと、レンダラのコンテナを同居させます。 この方針だと既存のアーキテクチャからはある程度変更を加える必要があります。一方でVPC内にレンダラが配置されるため、送信元IPアドレスでアクセス制御している環境に対しては、VPC内にあるNATのEIPを許可することでアクセスできます。

技術選定

アーキテクチャ

2の「Nginx on ECSのサイドカーを利用」という方式を採用しました。

レンダラはフロントを取得した後APIにアクセスし、ページをレンダリングする必要があります。このAPIに接続しているALBに対しては、セキュリティグループでアクセス元IPアドレスによる制限がかかっていました。しかしLambda@edgeでレンダラを動かした場合は、そのIPアドレスが不定になりセキュリティグループで送信元IPアドレスを許可することが難しくなります。この問題はIPアドレスを使ってアクセス制御をしていることに起因するため、トークンを用いた認証をすることで解決できますが、諸事情により従来のアクセス制御方式から変更しませんでした。

アクセス制限がかかっているのは開発環境に限った話で、dynamic renderingを有効にしたい本番環境とは異なります。しかし、The twelve-factor appのX.開発・本番一致のプラクティスに則り、環境間での差分をなるべく小さくした構成をとる方針になりました。特にdynamic renderingに関するフロントエンド周りの設定にミスがあると、Webサイト全体が見られなくなる可能性があるため、開発環境で入念にテストをする必要がありました。

このような背景から、環境間による差分が少ない形で実行できる2の「Nginx on ECSのサイドカーを利用」という方式を採用しました。

レンダラ

レンダラに関してはRendertronを選択しました*1。このツールはPuppeteerを内包しており、直接Puppeteerを使うよりも簡単にdynamic renderingの実装ができます。
*1:残念ながらRendertronはつい先日Public Archiveになってしまいましたが、現在でも有用なツールであるためここで紹介します

Rendertronの使い方はとてもシンプルです。https://example.com/pageをレンダリングしたい場合は、次のようなリクエストをRendertronに送信します。

GET /render/https://example.com/page

実装

SPAを使うためにCloudFront FunctionsでリクエストURLを書き換えるといった処理をしていたため、そこに必要な処理を追加します。 例えば検索クローラかブラウザかの判定、書き換え前URLのヘッダへの保存、キャッシュキーの正規化などです。 概ね以下のようなコードになります(一部抜粋・変更)。

function handler(event) {
  var request = event.request;
  var headers = request.headers;

  // 元々アクセスしようとしていたパス。rendertronで使う。
  request.headers['original-path'] = {value: uri};

  // 検索クローラー判定
  var bots = /(Googlebot|Yahoo! Slurp|bingbot|Baiduspider|duckduckbot|Gigabot|SeznamBot|Sogou web spider|360spider|Yisouspider|Bytespider|Yeti|Linespider|facebook|Twitterbot)/i;
  var ua_type = bots.test(headers['user-agent'].value) ? 'bot' : 'browser';
  request.headers['ua-type'] = ua_type;

  // キャッシュキーの正規化(cachekey-pathヘッダはcache policyに含めてある)
  // bot: レンダリング済みページをキャッシュ, それ以外: index.htmlをキャッシュ
  request.headers['cachekey-path'] = (ua_type === 'bot' ? {value: 'bot/' + uri} : {value: 'browser/index.html'});

  // 以下、uriの書き換えなど。SPAなので全てのパスをindex.htmlに向ける。特典ページのルートは/benefit/index.html.
  request.uri = '/benefit/index.html';
}

CloudFront Functionsで処理されたリクエストは、ECSタスクのNginxへと転送されます。Nginxの設定は次のようになります(抜粋)。 botの場合は127.0.0.1:3000でリッスンしているRendertronへと転送します。ECS Fargateのawsvpcネットワークモードでは、タスク上に乗る全てのコンテナがlocalhostインタフェース経由で通信できるので、サイドカーへの通信はこのように指定します。また、CloudFront Functionsで付与したoriginal-pathヘッダが$http_original_pathとして取得できるので、Rendertronのレンダリング対象パスとして渡します。

location /benefit {
  location /benefit/index.html {
    if ($http_ua_type = bot) {
      proxy_pass http://127.0.0.1:3000/render/https://$host$http_original_path;
    }
  }
  try_files $uri $uri/ index.html?$query_string;
}

Rendertronへとリクエストが渡されると、今度はそれ自体がクライアントとなり、リクエストが送信されてページがレンダリングされます。その後レンダリング結果が最初のリクエストに対するレスポンスとなり、検索クローラへと返される、といった流れとなります。

おわりに

本記事では、プレミアム会員特典ページにおけるdynamic renderingの仕組みについて解説し、

  • Lambda@edge方式とECS+Nginx方式の利点と欠点
  • ECS + Nginxを利用する方式の実装

について紹介しました。

宣伝

SRE部では仲間を募集しています!大規模なサービスや新事業の立ち上げなど規模の大小、ジャンル・フェーズが存在する環境でSREを実践しませんか?

 

それでは皆さん良いクリスマスをお過ごしください〜

関連する求人

関連する記事