こんにちは。
メディアサービス開発部Webアプリケーション開発課でフロントエンドエンジニアをしているnerikeshiです。主にTypeScriptでReactアプリケーションの開発をしています。
本記事では、コンテナで稼働させていたNext.jsアプリケーションのVercelへの移行と、それに付随して行ったISR導入時に起きたハマりどころについてお話しします。
本記事は同じWebアプリケーション開発課でバックエンドを担当しているフサギコ(髙﨑)との共同執筆記事です。
一迅プラスとは
一迅プラスは株式会社一迅社さまが運営する公式漫画連載サービスです。
冒頭試し読みから連載まで、2022/10/28現在で200を超える作品が掲載されています。
この一迅プラスにおいてブックウォーカーは開発保守運用を担当しています。
以前の一迅プラス
一迅プラスは当初、Next.jsのDockerコンテナをAWSのECS Fargateを使って起動する形1で運用していました。
これは、Googleなどの検索エンジンに対するSEOやTwitterなどで表示されるOGPカードのために、DOM構造の大半をSSRによってレンダリングするためでした。
サイトの公開からしばらくの間はこれで問題なく稼働していましたが、2022年5月下旬に新しく公開された作品がTwitter等のSNSにおいて大きく話題となり、大量のアクセスが殺到する状態になりました。
ECSのオートスケールは設定していたものの、負荷が急激なものだったため間に合わず、そもそも前段のELBの段階でELBが503を返してしまう状態にまで陥りました。 ELBはAWSへ申請すればウォームアップすることもできますが、これほどの反響が予想される作品が公開されることをエンジニアは知らなかったため、ウォームアップ申請も行っていませんでした。
ELBのスケールアウトとECSのオートスケールによって正味7分ほどで障害は自動復旧した2とはいえ、今回のような予測の難しい突発的な負荷が今後も起こりうるため、フロントエンドの構成を根本的に見直すことにしました。
フロントエンドに関するインフラ構成の見直し
構成を見直すにあたって、特に負荷を受けるページについてはSSRではなくISR 3を使って構築することを考えました。ISRにすればアクセスの大半をキャッシュから返せるため、かなり大きな突発負荷であっても耐えられることが期待できます。
しかし、一迅プラスは前述の通りコンテナベースで構築されていたため、そのままではISRを導入できませんでした4。 そのため、Next.jsをECS FargateからVercelへ移行することにしました。
Vercelへ移設するにあたって遭遇した迷いどころ、ハマりどころ
この節では、Vercelへ移設するにあたって遭遇した、いくつかの不明点とハマった点についてお話します。
DNSレコードの設定
一迅プラスはichijin-plus.com
というドメインでサービスを提供しています。
Vercelのサービスコンソールの設定手順ではCNAMEレコードの設定を案内されますが、 ichijin-plus.com に対してCNAMEレコードを設定してしまうとichijin-plus.com
はゾーンAPEX5であるため、SPFレコードなどの設定で不都合が生じてしまいます。
これについてVercelのサポートへ問い合わせたところ、特定のIPアドレスをAレコードとして設定すればいいよ
との回答が得られました。
現時点ではそのような回答でしたが、将来的には状況が変わる可能性もありますので、実際に設定される際はサポートに改めてご自分で問い合わせされることをおすすめします。
SSL証明書
事前にステージング環境をVercelにデプロイしてみて調べたところ、VercelはHTTPS通信時に使うSSL証明書の発行元としてLet’s Encryptを使用していました。
Let’s EncryptでSSL証明書を発行するには、ACMEチャレンジというものでそのドメインの所有を証明しなければなりません。
Vercelが対応しているのはHTTP-01チャレンジのみのようで、これは当該ドメインの所定のpathに対してHTTPでリクエストし、チャレンジ用に発行した文字列がレスポンスとして返却されるかを検証するものです。 言い換えれば、HTTP-01チャレンジでSSL証明書を発行するにはDNSレコードを完全に切り替えてしまわなければなりません。
このことから、ACMEチャレンジによるSSL証明書の発行作業中はHTTPS接続ができません。 一迅プラスはHTTPSのみでサービスを提供していますので、事実上のサービス断となります。
そのため、一迅プラスではVercel移設をサービス断の可能性があるメンテナンスとして事前告知のうえ、下記の手順で実施しました。
- 事前にprod環境をデプロイしておく
- Route53上のAWS ELBへのALIASレコードを重み付きレコードに変更し、VercelへのAレコードを重み0で追加する
- メンテナンス開始予定日時になったら、AWS ELBへのALIASレコードを重み0、VercelへのAレコードを重み100にする
- SSL証明書発行を待つ
- 一通り動作確認する
- 移設完了
実際には1分ほどでSSL証明書が発行され、アクセスできない時間はほとんど発生しませんでした。
ISRページが404になったときのrevalidateの挙動について
ISRする場合、getStaticPropsの中にサーバー内でのレンダリング処理を書きます。この際、 { notFound: true }
を返す場合のrevalidateを記述し忘れるとある問題が発生します。
// データ取得に失敗した場合は404ページを表示するような実装(実際のコードとは異なります) export const getStaticProps = async () => { try { const data = await fetch(...); return { props: { ... }, revalidate: 10, // ここにrevalidateを設定したが…… }; } catch (err) { return { notFound: true, // ここにrevalidateを設定し忘れていた }; } };
Next.jsのISRは、revalidateが省略されたときrevalidateなしと解釈します。 つまり、上記のような状態でデプロイした場合、一度でも { notFound: true }
が返ると以後そのURLではずっと404が返り続けるという恐ろしい状態になります。一迅プラスでは実際にこの間違いをおかし、あるエピソードが公開中にもかかわらず404になるという不具合を起こしてしまいました。その際、まず応急処置としてVercelの設定画面上でredeployをかけて誤ったキャッシュを飛ばし、その後速やかにrevalidateを設定して再デプロイするという対応を行いました。
export const getStaticProps = async () => { try { … } catch (err) { return { notFound: true, revalidate: 10, // notFoundを返す際もきちんとrevalidateを設定する }; } };
ISRの利用を検討されている方はnotFound時のrevalidate設定漏れにご注意ください。
バックエンドがフロントエンドからのアクセスをIPアドレスで制限できない
これまでの構造ではフロントエンドがSSRでページをレンダリングする際、バックエンドのpublicなELBに対してリクエストしていました。
つまり社員以外のアクセスが想定されていないステージング環境においては、バックエンドのセキュリティグループに、VPCのNATゲートウェイと会社で使用しているVPNのIPアドレス帯のみを許可することで、社員以外のアクセスを拒否する設定になっていました。
しかし、Vercelに移行したことで、アクセス元のIPアドレスでは制限できなくなってしまいました。
最終的にはフロントエンドとバックエンドが事前に共有した、環境ごとに異なる値を特定の名前のHTTPヘッダに入れてリクエストして認証することにしました。
このHTTPヘッダは、バックエンドチームがTerraformで管理しているELBのリスナールールとして検証する仕組みになっています。
一迅プラスの現在
Vercel移行によって、一迅プラスのインフラ構造は下記のように変わっています。
これによって、下記のようなメリットを享受できるようになりました。
突発的なアクセス急増でも落ちなくなった
突発的なアクセス急増も問題なく捌けるようになりました。 今後同様の突発負荷が発生しても、フロントエンドに起因する障害はほぼ起きないだろうと期待されます。
デプロイ待ち時間の短縮
Vercel移行以前は下記のような手順でデプロイしていました。
- GitHub ActionsがDockerコンテナをbuild
- GitHub ActionsがECRへプッシュ
- GitHub Actionsがecspressoを使ってデプロイ
この手順では、GitHub Actionsによって全て自動化されているとはいえ、10分から20分ほどの長時間かかってしまっていました。 これもVercelへ移行したことで、2分から3分程度で終わるようになりました。
Vercelのpreview deploy機能
VercelをGitHubリポジトリと連携させたうえでプルリクエストを作成すると、プレビューデプロイ が実行され、プレビューデプロイごとに自動生成のURLが発行されます。 そのURLにアクセスすれば、そのプルリクエストがデプロイされたときのアプリケーションの挙動を実際に確認できます。
フロントエンドとバックエンドは別のドメインで動いているので、ステージング環境のバックエンドのCORSにおいてその自動生成URLの形式を正規表現で許可してもらう6ことで、プルリクエストのレビューがとてもやりやすくなりました。
Vercelのpassword protection機能
追加課金が必要ですが、Vercelにはpreviewビルドにパスワード保護をかける機能があります。前節のバックエンドがフロントエンドからのアクセスをIPアドレスで制限できない
の項でお話した通り、元々はIPアドレスで開発環境へのアクセスを制限していました。
エンジニアは基本的に100%自宅勤務、企画や運用チームも必要がなければ自宅で勤務できるため、VPNに接続しないとステージング環境を見られないのは不便でした。 Vercelへ移行しpassword protectionを有効化したことでVPNに繋がずとも見られるようになり、便利になりました。
今後の展望
アクセス負荷対策において重要と考えられるページのISR化は終わりましたが、まだSSRのまま提供されているページも多数残されています。今後はそれらのページでもISR化を進めていきたいです。それだけでなく、Vercel化の副次的なメリットとしてNext.jsの最新機能を活用しやすくなったため、引き続きNext.jsの進化を注視し、必要に応じて新たな機能を取り入れていければと考えています。
まとめ
本記事では、Next.jsで構築したWebサイトをプロセスとして起動するインフラ構造からVercelへ移設する際の迷いどころとハマりどころについて、一迅プラスの事例を通してお話しました。getStaticPropsの設定漏れは起こりやすく、本番で不意に発生してはじめて発覚するパターンが多そうなのでお気をつけください。本記事がNext.jsプロダクトをVercel移行する際の参考になれば幸いです。
ブックウォーカーでは物理・電子・Web連載問わず漫画や本が好き、あるいはインフラ構造の変更も視野に入れたフロントエンド領域の課題解決に興味があるWebアプリケーションエンジニアを募集しています。もし興味がありましたら、ぜひブックウォーカーの採用情報ページからご応募ください。
-
詳しくはフサギコ(髙﨑)の一迅プラスのインフラ構成についてをご覧ください↩
-
自動復旧したのでAWSの基礎的な活用はできていたと言えるものの…機会損失であることにかわりはありません↩
-
ISR Incremental Static RegenerationはNext.jsが提供するレンダリング方式の一つです。SSRはユーザからのリクエストに応じて毎回HTMLをレンダリングしますが、ISRはリクエストによってレンダリングしたHTMLをキャッシュに保存し、キャッシュの有効時間を過ぎたページへアクセスが来た場合にはそのリクエストに対しては古いキャッシュを返しつつ、その裏でページのHTMLをレンダリングしなおしてキャッシュを新鮮な内容に更新します。これによって、アクセス頻度が高いページをキャッシュしながら、内容の新鮮さも有効時間の設定で制御できます。↩
-
正確に言えばコンテナでの起動でもISR導入はできます。しかし、複数のコンテナが独立してキャッシュを持つため、同じページを連続して更新すると違う内容が表示される可能性があるなど、ISRの恩恵を受けづらくなってしまいます。↩
-
DNSゾーン名と同じドメインであること、いわゆる裸ドメインなどとも Zone Apex(ゾーンAPEX / ネイキッドドメイン)とは - IT用語辞典 e-Words↩
-
バックエンドではCORSヘッダの生成をrack-corsによって行っているため、許可するドメインの指定に正規表現を使えます↩