サーバーレスでサーバーサイドレンダリング(SSR)の後編です。前編はこちら。
なお、同内容をこちらのイベントでも話す予定ですので興味あるかたはぜひこちらも。
serverless-newworld.connpass.com
はじめに
前回、SSRとはって話を簡単にしました。今回はSSRをAWSのサーバーレス、つまりAWS Lambdaでやってみたいと思います。 今回はVue.jsのフレームワークであるNuxt.jsで作ったサンプルアプリのSSRをLambdaで試してみます。
前回のブログでNuxt.jsでの例という説明をしましたが、今回はそこを実際にやっていく感じです。 なお、Nuxt.jsをLambdaで動かす場合の話って実はググってもあまり出てきません。いくつかの記事が出てくるだけです。また、Nuxt.jsが結構頻繁なアップデートを繰り返していることもあり書き方が少し変わってきたりしています。
というわけで今回の環境は以下を前提とします。
- Lambdaのランタイム: Node.js 12.x
- Nuxt.js 2.14.0
では早速やっていきます。
サンプルアプリ
デプロイするためのサンプルアプリを用意しています。ソースコード一式はこちらにあります。
サンプルアプリはNuxt.jsを使った簡単なブログになっています。といってもコンテンツ自体は自分がはてなブログでやっている別のブログをWordPressにインポートしてWordPressのAPIを叩いています。
WordPress自体は新たにWordPress.com上に用意しました。なお、本題ではないので詳細は割愛しますがWordPress.comのAPIは通常のWordPressのAPIとは若干異なっています。
また、Nuxt.jsを使ったアプリそのものに関してはそんなに大したものでもないので詳細や説明は割愛しますが、構成としては投稿をリストするトップページとそのページからリンククリックで遷移する個別の投稿ページを用意しています。個別の投稿ページは動的ルーティングを使ってラクしてます。
さて、前回の話の通り、LambdaでNuxtのSSRをやるには大きく2つやることがあります。
- Nuxtで作ったアプリをExpressで動かす
- Expressで作ったアプリをLambdaで動かす
1についてはNuxt公式のページでも解説されていますし、実際にそんなに難しい話ではないです。公式サイトではNuxt.jsをプログラムで使う方法について解説があり、基本そのままです。
2に関してはaws-serverless-express
というライブラリを利用します。
これはAWSが公開しているライブラリで、既存のExpressアプリケーションをLambdaで動かすための薄いラッパーのようなものとなっています。 実際にはモノリシックな単一のLambdaファンクションをプロキシとして用意してすべてのリクエストがそのファンクションを経由してExpressアプリケーションへとルーティングされるといった動きをします。
AWSが公開しているものですが、awslabsとして公開されているのでご利用は自己責任です。
よくあるパターンはExpress用の設定を行うapp.js
なりを用意した上でモジュールとしてmodule.exports
し、Lambdaファンクションから読み込んで使うってパターンですが今回はまとめてしまいます。
というわけでこんな感じです。これをLambdaファンクションとして登録します。今回はlambda.js
という名前で用意しています。
"use strict"; const path = require("path"); const { loadNuxt } = require("nuxt"); const express = require("express"); const app = express(); const awsServerlessExpress = require("aws-serverless-express"); const awsServerlessExpressMiddleware = require("aws-serverless-express/middleware"); app.use(awsServerlessExpressMiddleware.eventContext()); app.use( "/_nuxt", express.static(path.join(__dirname, ".nuxt", "dist", "client")) ); async function start() { const nuxt = await loadNuxt("start"); app.use(nuxt.render); return app; } exports.handler = (event, context) => { start().then((app) => { const server = awsServerlessExpress.createServer(app); awsServerlessExpress.proxy(server, event, context); }); };
少しずつ見ていきましょう。
const awsServerlessExpress = require("aws-serverless-express"); const awsServerlessExpressMiddleware = require("aws-serverless-express/middleware"); app.use(awsServerlessExpressMiddleware.eventContext());
まず、API Gatewayに届いたHTTPリクエストをLambdaが処理可能なイベントの形式に変換するためのミドルウェアを登録します。
async function start() { const nuxt = await loadNuxt("start"); app.use(nuxt.render); return app; }
Nuxtのインスタンスを取得してnuxt.render
を呼び出します。Expressに届いたリクエストはNuxt側でルーティングされます。
この箇所、以前はnew Nuxt(config)
したりその後でnuxt.ready()
を読んだりする必要がありましたが今はloadNuxt()
だけで大丈夫です。
exports.handler = (event, context) => { start().then((app) => { const server = awsServerlessExpress.createServer(app); awsServerlessExpress.proxy(server, event, context); }); };
Lambdaファンクションとしてのエントリポイントとなるハンドラ関数を用意して、createServer
します。このとき先ほど作ったExpressのインスタンスを渡します。なお、この構成ではUnixドメインソケットを使ってやりとりされるそうなので、Express側でのListenは不要です。
最後にハンドラに渡ってきたイベントとコンテキストを受け取るproxyを作成して終了です。
ちなみにコールドスタート対策でserver
を毎回作成するのではなくなければ作成みたいにしてもいいかもしれません。こんな感じで。これはちゃんと試してないのでどの程度効果あるかわかりませんが。
let server = undefined; exports.handler = (event, context) => { start().then((app) => { if (server === undefined) { server = awsServerlessExpress.createServer(app); } awsServerlessExpress.proxy(server, event, context); }); };
serverless.yaml
今回はServerless Frameworkを使っています。こんな感じでyamlファイルを作ってデプロイしています。
service: nuxt-ssr plugins: - serverless-offline frameworkVersion: '2' provider: name: aws runtime: nodejs12.x region: ap-northeast-1 functions: nuxt-ssr: handler: lambda.handler timeout: 30 events: - http: ANY / - http: 'ANY {proxy+}'
大して解説することもないですが、ポイントはAPI Gatewayの設定としてプロキシインテグレーションを使ってることくらいでしょうか。これですべてのURLのパスを受け止めています。そしてそのまままるっとLambdaに渡しています。
最後に
どうでしょう。意外と簡単なことがわかりましたか。
しかし、本番向けにはもう少し考えることがあります。例えば今はserverless deploy
でまるっとディレクトリ全体をパッケージングしてデプロイしています。でもこれだとソースコードが含まれていたり、無駄なものが多く容量も大きいです。
Lambdaの基本としてデプロイパッケージはなるだけ小さくしてコールドスタートを早くする、というのに反しています。したがってこのあたりは検討が必要になるでしょう。きっとCI/CDのパイプラインが用意されると思うのでそのあたりで必要なものののみデプロイするようにするのがいいと思います。
また、ChromeのDeveloper Toolとかで見てもらうとわかりますが、1ページのリクエストでCSSとかJSとかいろんなものを呼んでいるのがわかるかと思います。そしてこれらのリクエストもすべてLambdaファンクションに届いてしまっています。つまり静的なファイルへのリクエストもLambdaで処理してしまっていることになるのでこれはコストの無駄といえるでしょう。ここはAWSであればCloudFrontのようなCDNを使うのがいいと思います。
そして、今回はNuxt.jsをサンプルにしましたが実はサーバーレスでSSRやるのに相性が良いのはNext.jsなのではないかと思っています。というのもNext.jsはtarget
のmode
としてserverless
をサポートしているのです。これ自体はLambdaファンクションのコードを出力するというものではないのですが、これに何らかのトランスパイラやServerless Next Componentを組み合わせるととてもいい体験が得られるような気がしています。こちらもまたどこかで試して紹介したいと思います。