Next.jsをサーバーレスでやっていくためのServerless Next.js Component

今回は最近その存在感がますます上がっているNext.jsとサーバーレスの話です。

はじめに

この投稿は2020年11月27の21時から開催予定のイベント(ライブストリーミング)で話す内容です。

serverless-newworld.connpass.com

もし間に合えば、かつ時間があればぜひライブ配信のほうにも参加ください。

さて、今回は11/9に登壇させていただいたFront-End Studyでの話でも少し紹介したServerless Next.js Componentについて取り上げます。

僕は昨今のフロントエンドWeb周りの技術では最近は一番Next.jsが好きなんですが、そのNext.jsで作ったアプリケーションをAWSのサーバーレスで動かしていくって話です。Static Site Generation(SSG)したものをS3などで配信していくというのはよくある話ですが、もちろんServer Side Rendering(SSR)も可能です。

さて、SSRとサーバーレスと言えば過去に一度やっていて、そのときはNuxt.jsを使って説明しました。そのあたりはこちらにまとまっています。

www.keisuke69.net

前回のNuxt.jsの際にも言及したように、aws-serverless-expressを使ってサーバーレスでSSRやるのは簡単ではあるものの、一方で無駄も多いです。そこで、本来であればその無駄を最適化するために自分で手を動かしていく必要があったりするのですが、Next.jsを使っている場合はServerless FrameworkのプラグインであるServerless Next.js Componentを使うとこの辺をうまいことやってくれます。しかもちょっとやるだけなら超絶簡単

というわけでやっていきます。今回の環境は以下を前提とします。

Lambdaのランタイム: Node.js 12.x
Next.js 10.0.3

では早速やっていきます。

(2020.11.27 Update)

上記ライブ配信アーカイブが公開されています。3:25あたりから始まります。

https://www.youtube.com/watch?v=r0OdiKoMmig


サーバーレスアンチパターン今昔物語 第七夜

チャンネル登録してくれると更新通知も飛ぶのでどうぞ。

サンプルアプリ

今回もデプロイするためのサンプルアプリを用意しています。ソースコード一式はこちらにおいておきました。

Nuxt.jsのときと同様に簡単なブログです。コンテンツについても前回同様はてなブログでやっている別のブログWordPressにインポートしてWordPressAPIを叩く感じになっています。

使うWordPress自体は今回は新たに作成せずに前回作ったものがまだWordPress.com上で動いていますのでそれをそのまま使っています。

Next.jsで作ったアプリそのものについてもNuxt.jsのときと同様に大したものではないので詳細は割愛します。構成としては投稿をリストするトップページとそのページからリンククリックで遷移する個別のページです。URLとしては以下のような感じです。

  • / - トップページ兼一覧ページです。個別の記事へのリンクを一覧表示します。(SSR
  • /posts/[id] - 個別の投稿ページです(SSG)
  • /about - SSGとSSRの確認テスト用

今回は全部SSGでもいいようなアプリケーションなのですが、それでは話が微妙なのでトップページの/SSRで処理しています。あと、実際のところSSGとSSRのどっちで動いてんのかわからん!とか挙動を確認してみたい!って目的のために/aboutというページを用意しています。残りは全てSSGです。

さて、以前にSSRをLambdaでやったときには大きく以下のような手順を踏むという話をしました。

  1. Nuxt.js / Next.jsで作ったアプリをExpressで動かす
  2. Expressで作ったアプリをLambdaで動かす

今回はこういった手順を踏まずにServerless Next.js Componentでやっていきます。

ちなみにこのアプリケーションをyarn buildするとこんな感じになっています。 f:id:Keisuke69:20201127082318p:plain

これを見ておわかりのように//api/helloSSRされ、/posts/以下はSSGで各IDごとにPre-Renderingされています。

Serverless Next.js Component

AWSを始めとするサーバーレス環境のデプロイやプロビジョニングツールであるServerless Frameworkの関連機能であるServerless Componentの一つです。 Serverless Componentっていうのは超絶簡単に説明するとServerless Frameworkという仕組みを通じてユースケースを意識したServerless Applicationをデプロイできるというものですが、正直に言うとあまり詳しく知らないです。 今回利用するServerless Next.js ComponentはNext.jsをサーバーレス環境にいい感じでデプロイしてくれるServerless Componentです。

github.com

このServerless Next.js Componentの特徴としては、ざっくり言うとNext.jsのSSRをLambda@Edgeで実現してくれるというものです。Nuxt.jsでやったときのようにaws-serverless-expressを使ってAmazon API GatewayAWS Lambdaを使って実現するのではなく、AWSCDNであるCloudFrontのエッジサーバ上で稼働するLambda@Edgeを利用します。

また、aws-serverless-expressでやる場合、そのままだとリソースの観点で無駄が多いという話をしましたが、Serverless Next.js Componentでデプロイすると静的なアセットや静的ファイルとして生成されたものはS3でホスティングするようにデプロイした上でCloudFrontを使って配信されるようになります。

また、Lambda@Edgeで静的ページへのリクエストをハンドリングしてSSRしないリクエストはS3へとフォワードしてくれます。またいわゆるルーティングもやってくれるんですが、Dynamic Routingもサポートされています。

こういったことを、Next.jsが標榜するのと同じZero Configでできます。まじでめっちゃ簡単です。しかも今回はCloudFormationベースでのdeployではないこともあってデプロイ自体が速いですね。

さて、Lambda@Edgeを使うってこともさることながら、静的アセットや静的ファイルのみS3とCloudFrontを使ってサービスするというのは口で言うのは簡単ですが想像以上に面倒だと思います。プロダクション用にビルドしてできあがったもののうち、静的なもののみS3にデプロイするってのはCI/CDのようなパイプラインで考慮する必要があり多少作り込む必要もあります。ビルドしたものを何も考えずにごそっとデプロイするってわけにはいかないんですね。

また、Lambda@EdgeでルーティングやSSG、SSRのハンドリングといったことを自前で実装するのは結構たいへんです。

このあたりをまるっとやってくれるServerless Next.js ComponentはNext.jsを使ってる人にはとてもいいものだと思います。というかNext.jsをサーバーレスでSSRやるにはこれ一択と言ってもいいんじゃないかなと思ってます、個人的には。

デプロイ

実際のところ前回やったNuxt.jsのときのようにサーバーレスで動かすために実装しなければいけないこと、設定とかを特に必要としません。今回のサンプルアプリも実際には普通のサーバーとかでも動きます。

Serverless Frameworkベースなのでserverless.yamlを用意します。クレデンシャルなどのServerless Framework使う上で必要な設定はやっておいてください。serverless.yamlの中身は以下のように。これはデフォルトでカスタムドメインはCloudFront周りの設定などもう少し細かく設定することも可能ですが、これだけでも十分問題なく動きます。まさにZero Config。

myNextApplication:
  component: "@sls-next/serverless-component@1.18.0"

myNextApplicationの部分は好きに定義して大丈夫です。一点注意があるとするとバージョン番号(今回であれば1.18.0)を指定すること、そして必ず最新版を入れたほうがいいです。使い始めてからもなるべく最新版を使い続けるようにしたほうがいいです。自分の経験では少し古いバージョンでも動かないことがありました。

あとは同じServerless FrameworkといってもこちらはComponentということもあってかオリジナルとは微妙にフォーマットが違うそうです。

ファイルを用意したらシンプルに、

$ serverless

と実行してください。通常のServerless Frameworkを使うときのようにserverless deployやsls deployではなくserverless`とだけ実行してください。実行完了までにかかる時間は恐らく生成されるページの数などによって左右されると思います。また初回実行時はCloudFrontのディストリビューション作成を伴うため、有効になるまで少し時間がかかります。

実行が完了するとこんな出力がされます。

serverless

  myNextApplication: 
    appUrl:         https://<abcdefghijklm>.cloudfront.net
    bucketName:     abcdfeg-abcdefg
    distributionId: XXXXXXXXXXXXX

  ******************************************************************************************************************************************************************
  Warning: You are using the beta version of Serverless Components. Please migrate to the GA version for enhanced features: https://github.com/serverless/components
  ******************************************************************************************************************************************************************


  162s › myNextApplication › done

このときは初回だったので162秒かかったようです。ここにあるappUrlというのがCloudFrontで配信されているエンドポイントですね。なのでこちらにブラウザでアクセスしていきます。なお、カスタムドメインを利用することももちろん可能です。

今回はブログということで全部SSGにしてしまっていたのですが、無理やりindexのページだけSSRをするようにしています。

なのでアクセスしてみるとわかるのですが各ページへのアクセスは速いです。一方でトップページへのアクセスはSSRが実行されているので他と比べると多少遅いはずです。

なお、ドキュメントではgetInitialPropsを使っている場合にSSRするとなっていますが、9.3以降で推奨のgetServerSidePropsにも対応しています。もちろんgetStaticPropsgetStaticPathsにも対応。

また、serverlessを実行すると以下の内容のnext.config.jsが一時的に生成されます。

module.exports = { target: 'serverless' };

注目はターゲットがserverlessであるということです。これは何かというとNext.js8からサポートされたもので、ページごとに出力します。このファイルは実行完了時に自動的に削除されます。

作成されるリソース

実際にデプロイすると以下のようなリソースが作成されています。御覧のようにサーバーレスでWebっていうと出てくることが多いAmazon API Gatewayの姿がありません。

CloudFrontのディストリビューション

オリジンとして一緒に作成されたS3のバケットが指定されています。バケットというかS3でStatic Website Hostingした際のURL。そしてOAIが設定されているのでCloudFront経由のリクエストしか届かないようにも設定されています。 特徴的なのがBehaviorの設定ですね。こんな感じになってます。

f:id:Keisuke69:20201127015136p:plain

これってつまりNext.jsをビルドしたときに生成されるディレクトリやそこへのパスを元にリクエストの振り分け設定がされています。具体的には静的なファイルなど基本的にすべてS3を使ったオリジンが指定されていますね。

Lambdaファンクション

Lambdaのほうはたった2つのファンクションがデプロイされているだけです。

defaultとapi用でどちらも512MBメモリです。

これらのファンクションがLambda@Edgeとして実行されるように設定されています。

S3バケット

S3はこんな感じで、静的Webサイトとして配信するための設定がされています。 f:id:Keisuke69:20201127020233p:plain

大まかな挙動

Serverless Next.js Componentを使ってデプロイした際の挙動について簡単に解説します。基本的にはビルド結果の生成物をS3に種別ごとにフォルダをわけて格納し、リクエストのパスを元に振り分けるという感じになります。その振り分けを行うのにCloudFront上でのBehaviorとLambda@Edgeが使われます。

まず、Behaviorについて見ていきます。作成されるのはDefalutとそれ以外の4種類で計5種類です。Behaviorというのは指定したリクエストパスのパターンでオリジンを変更したり異なる設定をしたりするもので、それが5パターンあると思っていただくといいです。それが以下の5種類です。

  • _next/static/*
  • static/*
  • api/*
  • _next/data/*
  • Default(*)

_next/static/*static/*は来たリクエストをそのままS3へと転送します。

api/*Origin Requestイベントに対してAPI用のLambda関数が設定されています。Origin RequestイベントってのはCloudFrontがリクエストをOriginに転送したときにのみ関数が実行され、CloudFrontのキャッシュから返される場合は実行されないというものです。ここでAPIへのリクエストをハンドリングします。ここでいうAPIってのはNext.jsで作成可能なAPIのことですね。

Next.jsではサーバーサイドで実行するAPIのエンドポイントも作成可能になっています。これはpages/api配下にファンクションを作ることで実現するのですが、これをいわゆるサーバーレス環境のファンクションとしてデプロイ可能となっています。ただし、あくまでも汎用的なファンクションシグネチャとなっていてLambdaそのもののフォーマットとは少し異なります。

こんな感じのものを用意します。

export default (req, res) => {
  // ...
}

これをyarn buildすると.next/serverless/pages/api/の下にビルドされたものができあがるのですが、Serverless Next.js ComponentはこれをLambdaファンクションのデプロイパッケージに含める形でデプロイされます。なお、LambdaファンクションとしてはServerless Next.js Componentが生成するindex.jsが登録されていて、APIへのリクエストに応じて呼び出す形になります。実際に登録されるindex.jsは御自身の開発環境のプロジェクトフォルダ内の.serverless_nextjs/api-lambda/index.jsです。

_next/data/*もLambda関数が紐付けられています。こちらはOrigin RequestOrigin Responseの両方でどちらも同じLambda関数で処理されています。Origin ResponseはOriginからのレスポンスを受け取ってキャッシュする前に実行されるというものです。Originからエラーが返ってきても実行されますが、Origin Requestで実行された関数からレスポンスが生成された場合は呼び出されません。

ここでは3種類のリクエストをハンドリングします。ここでいう3種類とは、

  • サーバーサイドレンダリング
    • getInitialPropsやgetServerSidePropsが使われているページはこの時点でレンダーされ、すぐにレスポンスされます
  • 最適化された静的ページ
    • Next.jsによってプリコンパイルされたHTMLページへのリクエストはS3に転送されます
  • パブリックリソース
    • いわゆるルートレベルの静的なアセットたちですね。faviconとかの。これらもS3へと転送されます。

静的ページとパブリックリソースもLambdaファンクション通るのはなんで?って思ったかもしれませんがこれはそのルートがパスのパターンマッチングで特定できないからだそうですがちょっと深堀りできていないです。

最後のDefaultも_next/data/*と同様のようです。同じ関数、同じイベントが設定されています。

つまり、上から順にリクエストされたURLのパスを評価していって、マッチしたパターンがあればオリジンにそのまま転送したりしていくのです。

なお、デプロイごとにCacheのInvalidateも行われます。これは/*を対象に行われます。

できないこと

さて、こんな感じで割と簡単に利用できるServerless Next.js Componentなんですが、実は最近のNext.jsにおける最注目機能といっても過言ではないIncremental Static Regeneration(ISR)には対応していません。ISRとはすごく雑に説明すると『基本的にはキャッシュから返しつつ、有効期限を過ぎたら非同期でキャッシュの更新をSSRで行う』っていうものです。Cache Controlにおけるstale-while-revalidateと同じような考え方が適用されたものと言ってもいいかもです。Next.js 9.4から追加された機能です。

これの嬉しさについては別のサイトに譲るとして、SSGとSSRそれぞれ辛みがあるわけですがその辛みの一旦を解消してくれそうな革命的な機能です。つまり、これまでパフォーマンス的に、スケーラビリティ的に、運用的にはSSGのほうが楽ではあるものの、SSRでないと動的な処理などは実現できないことがあったわけです。一方でSSGはその性質上ページの数やコンテンツの数が多くなるとビルドが思くなるという課題もありました。それをISRでは言葉どおり段階的にしつつ、CDNのキャッシュを有効活用しつつ、動的に近いこともできるようになった感じですね(実際には動的ではないですが)。

とういわけでこれに対応されるととてもいいのですが、残念ながら現状ではまだ対応されていません。一応、Issueは起こされているので近い将来対応することが期待されます。。。

まとめ

今回はNext.jsのアプリケーションをAWSのサーバーレス環境でServer Side Renderingしていく上で非常に簡単に使えるServerless Next.js Componentの紹介でした。とても使い勝手はいいのですが、破壊的なアップデートもガンガン入ってたりするのでそのあたり気をつけつつ使ってもらえるといいんじゃないでしょうか。

きっとNuxt.js向けの似たようなものもそのうち出てくると期待。

©Keisuke Nishitani, 2023   プライバシーポリシー