AWS LambdaとNuxt.jsでServer Side Renderingする(2020年版)

サーバーレスでサーバーサイドレンダリングSSR)の後編です。前編はこちら

www.keisuke69.net

なお、同内容をこちらのイベントでも話す予定ですので興味あるかたはぜひこちらも。

serverless-newworld.connpass.com

はじめに

前回、SSRとはって話を簡単にしました。今回はSSRAWSのサーバーレス、つまり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にインポートしてWordPressAPIを叩いています。

WordPress自体は新たにWordPress.com上に用意しました。なお、本題ではないので詳細は割愛しますがWordPress.comのAPIは通常のWordPressAPIとは若干異なっています。

また、Nuxt.jsを使ったアプリそのものに関してはそんなに大したものでもないので詳細や説明は割愛しますが、構成としては投稿をリストするトップページとそのページからリンククリックで遷移する個別の投稿ページを用意しています。個別の投稿ページは動的ルーティングを使ってラクしてます。

さて、前回の話の通り、LambdaでNuxtのSSRをやるには大きく2つやることがあります。

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

1についてはNuxt公式のページでも解説されていますし、実際にそんなに難しい話ではないです。公式サイトではNuxt.jsをプログラムで使う方法について解説があり、基本そのままです。

2に関してはaws-serverless-expressというライブラリを利用します。

github.com

これは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はtargetmodeとしてserverlessをサポートしているのです。これ自体はLambdaファンクションのコードを出力するというものではないのですが、これに何らかのトランスパイラやServerless Next Componentを組み合わせるととてもいい体験が得られるような気がしています。こちらもまたどこかで試して紹介したいと思います。

MacとKrispとOBSとで動画見ながらLive配信する方法完全に理解した

はじめに

コロナの影響で、主催する勉強会やミートアップなどをオンラインで開催する機会が増えてきました。

これまではZoomを使ってYouTube Liveに配信していたのですがこれだと画質が低いこともあって最近OBSを使った配信に切り替えています。

1人での配信だとシンプルなので特に困ったこともなかったのですが、最近とあるオンライン開催のコミュニティイベントの配信を見ながら副音声を載せて配信するってことを(ちゃんと許可を取って)やろうとしたら、わからないことばかりでいろいろ困ったのでその最終形を紹介します。

ググるMacでOBSでってなるとSound FlowerとLadioCastを使って〜みたいな記事が多いですが、自分の環境ではどれもうまく行かなかったです。

ちなみに、あとからゲーム実況配信と変わらないってことに気づきました。

やりたいこと

こういう感じで、音の出る画面(今回は動画をブラウザで視聴)を見つつ、それに自分の音声を載せて配信するってものです。

f:id:Keisuke69:20200914113155p:plain

前提

前提として以下の環境です

  • Macbook Pro (2019 Mid, Mojave)
  • OBS
  • マイク、スピーカーともにヘッドセット(Jabra Evolve75もしくはAirPods Pro)
  • マイクはKrispでノイズリダクション

ポイントはKrispでノイズリダクションしたマイク入力を使いたいってことですね

Krisp

Krispってのは機械学習の力でスピーカーとマイクのノイズキャンセリングをしてくれるアプリケーションです。

krisp.ai

超強力です。JabraのEvolve75で試した場合、後ろで音楽をそれなりの音量で流していても消してくれます。打鍵音も同様です。このリモートミーティング時代で僕にとって手放せないソフトウェアです。ちなみに無料でも一部使えます。

なので、今回もこのKrispを通した音声を配信に載せたかったのです。

CPUを食うって話もありますが、僕の環境(i9 9コア、32GBメモリ)では気にならないレベルです。なので常にオンにしてます。Zoomでもなんでもこれ経由です。

必要なもの

Macではデスクトップ音声(つまりMac上で再生した何らかの音声)をそのままOBSで取り込むことはできないです。なのでブラウザなどで動画を再生してそれをウィンドウキャプチャなどで取り込んでも映像だけで音声は聞こえない状況になります。

そこで、Blackholeという仮想オーディオデバイスを使います。

github.com

Web上の解説などではSoundflowerというものを使うものが多いですが、今回はその後継とも言えるBlackholeというものを使っています。Soundflowerはオリジナル版の開発は既に停止されており、最近のOSへの対応などは有志によるfolkとなっているためです。加えて僕の環境ではインストールが失敗してうまくいきませんでした。

あと、あわせてLadioCastというものをインストールするように案内している記事も多いですが今回は使いません。

ここからはBlackholeがインストールされた前提です。ちゃんとインストールされているとAudio MIDI Setupを開くとこんな感じになっていると思います。 f:id:Keisuke69:20200914201126p:plain

なお、お金があるならLoopbackという有料ソフトを使うのが一番簡単です。これからやることをごちゃごちゃやらずともあっさりできます。Soundfrowerのオリジナルの開発元はこのLoopbackの会社に買収されたそうです。

rogueamoeba.com

OBSの音声設定

マイク1をKrispに設定してます。マイクの音はヘッドセットのマイクから直接ではなくKrispのノイズリダクション後の音を使いたいからこうしています。そしてKrispの入力デバイスとしてヘッドセットのマイクが指定されています。

f:id:Keisuke69:20200914115324p:plain

いろんな記事がいろんなこと書いてますが、僕の場合はこれでいきます。なお、デスクトップ音声1、2はともに何も選べません。無効以外選択できない。

加えて、モニタリングデバイスとして自分がそのとき使っているスピーカーを設定します。今回はJabraのヘッドセットを直接指定しています。これは文字通りモニタリング用です。これがないと流れてる音声を聞きながら喋るってのができないです。後でまた触れます。

OBSのシーン設定

f:id:Keisuke69:20200914195401p:plain

この例で追加しているソースは以下4つです。

  • 映像入力キャプチャ(Webカメラからの入力、画像上α6400となってるのがこれ)
  • 音声入力キャプチャ(再生中の画面の音声)
  • テキスト(文字をかぶせる場合)
  • ウィンドウキャプチャ(特定のウィンドウの映像取り込み。今回はブラウザでYouTube動画を再生し最大化)

ポイントは音声入力キャプチャ。キャプチャするデバイスとしてBlackholeを設定します。 f:id:Keisuke69:20200914195810p:plain

音声ミキサーの部分のマイクは上で設定したKrispのマイク、音声入力キャプチャは今設定した音声入力キャプチャ(Blackhole)です。

配信

ここまで設定した状態でMacの出力デバイスをBlackholeに変更します。入力デバイスはヘッドセットのマイクを使うので今回はJabra Link 370を指定しています。 f:id:Keisuke69:20200914200114p:plain

で、これだけだと画面で再生している音をモニタできません。なぜならばMacの出力デバイスとしてはBlackholeが設定されているからです。音声出力先がBlackholeになっているのですべての音がそこに出力されるんです。なのでヘッドセットで聞きながら〜ってのができません。というわけでヘッドセットにも音を出します。

先ほど、OBSの設定でモニタデバイスとしてJabra Link 370を設定しました。なのでOBSのモニタ出力はここに入ってきます。ただし、通常では音声入力のモニタ出力は設定されていません。というわけでそれを設定します。

今回は自分の喋っている声はモニタする必要ない、つまりマイク入力はモニタ不要ということで音声入力キャプチャのモニタ設定だけ変更します。

f:id:Keisuke69:20200914200841p:plain

これはモニタ出力と配信側への出力と両方をできるようにしています。こうすることで音声入力キャプチャで入ってきたヘッドセットなどで聞きながら喋ったりできるようになりました。

あとは普通にOBSからYouTube Liveへ配信するなり、録画するなりしちゃってください。

なお、この設定はOBSを落としたら音が聞こえなくなります。なのでその場合はMacの出力デバイスを任意のデバイスに戻してあげましょう。

まとめ

というわけで僕の環境における設定でした。実際にこれでYouTube Liveで配信もやった実績あるので少なくとも僕の環境では問題ないです。

今回、Krispを通したマイク入力にこだわったことで難しいこともあったのですが、OBS自体にもノイズリダクションとかがフィルタとして存在してます。なのでそれを使う場合はもう少しシンプルかもしれません。どのくらい有効なのかはわからないですが。

あと、冒頭でも書いたようにこれって実はゲーム実況配信と変わらないことに後から気づきました。

そしてTwitch Studioを使ってTwitchで配信するならここまでやってきた苦労を何もせずとも同じことが簡単にできます。ソフトインストールして基本セットアップしただけでミックスされた画面と音声が用意されてボタン一発でTwitchへのストリーミングが開始できるのすごい。録画もできるのでTwitchでもいいのであれば圧倒的にTwitch Studio使ったほうが楽だと思います。

サーバーレスでサーバーサイドレンダリング 前編

はじめに

サーバーレスでサーバーサイドレンダリングの話です。ReactとかVueを使ったシングルページアプリケーション(SPA)を開発している人がサーバーサイドレンダリングやりたいんだけどサーバーレスでどうやるのって話です。

今回も『サーバーレスアンチパターン今昔物語』というイベントのための記事となっています。

serverless-newworld.connpass.com

なお、今回は前編と称してそもそものところを簡単に説明しつつ、サーバーレスでやる場合の基本的な話を説明していきたいと思います。次回、後編で実際にサンプルアプリを用意して動くもので説明をしていきます。

なお、最初にサーバーサイドレンダリングやJamstackってなんぞやというおさらいを簡単にしておきますが、残念ながら僕はフロントエンドについては少し嗜んでいる程度であってその道のプロではないので間違ってるところがあるかもしれません。

あと、本題に関係ないところは大幅に割愛しています。また、Angularに関しては全く触れていないです、すまん。

サーバーレスアプリケーションにおけるフロントエンド

サーバーサイドレンダリングとかの話の前にフロントエンドとサーバーレスアプリケーションの話をおさらいします。

サーバーレスアプリケーションというのはAWSの場合はFunction as a ServiceであるAWS Lambdaなどを使って作ったアプリケーションのことを指しています。

一応ことわっておくと、サーバーレスなサービスってのは何も本当にサーバがないわけではなくて、あくまでもユーザが管理しなければいけないインフラ要素をなるだけ少なくしたサービスってことです。なのでユーザはビジネスロジックに近いところにフォーカスできる、自分たちでやる必要のある付加価値になりにくい作業を減らせるというのがメリットです。

そんなサーバーレスのサービスですが、世の中にはいろんなベンダーがいろんなサービスを提供しています。AWSであれば前述のAWS LambdaだけでなくAPIを公開するためのAmazon API Gatewayであったりステートマシン制御のためのAWS Step Functionsであったり、その他にもいくつかあります。

さて、そんなサーバーレスのサービスを組み合わせて開発したアプリケーションのことをサーバーレスアプリケーションと呼んだりするわけですが、これにはWebシステムも含まれます。ただし、従来からある3層構造のWebシステムではなく、AWS Lambdaでロジックを実装し、Amazon API GatewayでそれをいわゆるREST APIとして公開する場合が多いです。そしてこのとき、クライアントサイドはいわゆるシングルページアプリケーション(SPA)という形を取ることが多いです。

REST API以外だとAWS AppSyncというサービスを使ってGraphQLで実装するケースもあります。

SPAが何かってのは簡単に言うと単一のWebページで構成されるWebアプリケーション、という感じですか。一応メリットもデメリットもあったりするのですがそれは本題ではないのでここでは割愛します。そしてSPAを開発するにはJavaScriptを用います。よく使われるフレームワークとしてReactとかVue、Angularあたりがあります。

SPAの課題

そんなSPAなんですが少しだけ課題があります。よく言われるのはSEOの問題です。

何が問題かというとSPAってのは先ほど述べたとおり1枚のHTMLに置かれたJSのアプリケーションですべてが成立しています。HTMLがリクエストされてもJSがロードされて処理が実行されるまでは真っ白です。ここ大事。

さて、誤解をおそれずにざっくりと説明すると、検索エンジンというのは特定のページに対してクローラーbot)が実際にリクエストして内容を取得することでインデックス化されています。

それを踏まえてSPAだと何が起こるか。

検索エンジンクローラーが仮にJavaScriptを解釈しないものだと真っ白な一枚の何もないものを受け取るだけです。

現在のところGoogleクローラー(Googlebot)はJavaScriptの解釈と実行も可能です。Googlebotの場合、アクセスしたサイトがJavaScriptで作られたサイトだった場合にJavaScript実行後の実際のコンテンツを取得するためにJavaScriptの実行を試みます。

このときGooglebotはもちろんHeadless ChromiumレンダリングエンジンでJavaScriptを実行するんだけど、昨年まではChrome41相当の結構古いやつだったのでそもそも正しく動いてくれない、つまり正しいHTMLを認識してくれないという問題があった。

でも、これに関しては昨年のGoogle I/Oでアップデートが発表されて最新のChromeに常に対応していくことが発表されています。なのでこれに関しては以前のような問題はなくなったと言えます。

じゃ、サーバーサイドレンダリングいらないじゃん?って話になるかと思いきやそんなことはないです。SPAのもう一つの課題としてファーストビューが遅いという問題があるんですね。これは人間に対してもそうなんですがbotに対しもです。例えば非同期に他のAPIをコールしてデータを取得してコンテンツを生成する場合ってあると思うんですが、ローディング中の表示やスピナを表示してるケースってあると思いますがbotはそういうのを待ってくれないケースが多いです。

というわけでこういった点がSEO戦略としてはかなり致命的だったと言えます。

あと、そもそもGoogle以外のbotではJavaScriptを解釈して実行してくれないものもあるかと思います。

そういった場合にサーバーサイドレンダリングを行うことで、レンダリング済の正しいページをクローラーが読み取れるのでSEOが向上すると言われています。

サーバーサイドレンダリング

では、サーバーサイドレンダリングとは何か。

これは荒っぽく言うとリクエストがあったときにJSをダウンロードしてクライアント側でレンダリングするのではなく、初回のリクエストはサーバ側でレンダリングしてしまいレスポンスするということです。

こうすることでつまり、先ほど説明したようなクローラーによるアクセスであっても正しいコンテンツを返せるようになると、そういうわけです。

本当はいろいろともっと細かい話があるのだけど今回の話的にはこのくらいの理解でOKです。

このあたりに対応しているフレームワークがReactだとNext.js、VueだとNuxt.jsが有名です。

サーバーサイドレンダリングやるのに必要になるのは当然サーバーサイドでレンダリングするためのサーバです。

Jamstack

少し似たものとしてついでに紹介しておくと、最近注目を浴びているのがJamstackです。JamstackなのかJAMStackなのか、はたまたJAMstackなのか表記が揺れててわからないけど、ここではJamstack.orgの表記にあわせます。

JamstackとはJavaScriptAPI、Markupというスタックからなるアプリケーションのことですね。すごく乱暴に言うとアプリケーション的な処理はJavaScriptでクライアント側でやり、いわゆるサーバーサイドの処理やデータストアといったものはAPIとして利用、そして作成されたものは静的なファイルとして出力してしまう、というものです。

つまりJavaScriptAPIでアプリケーションを開発し、それを静的サイトジェネレータを使って静的ファイルとして出力します。

この場合、サービスとして提供するのに必要になるものは静的サイトをホスティングするサーバです。

というわけで静的サイトジェネレータ関連のフレームワークというとReactだとGatsbyが有名ですね。Vue.jsだとVuePressとかGridsomeとか。実はサーバーサイドレンダリングフレームワークとしてあげたNext.jsやNuxt.jsもこのあたりできます。

じゃ、なんでGatsbyとか使うかっていうとNext/Nuxtあたりと比べて静的サイトジェネレータに特化しているのでよりシンプルとかそんな感じかなーと個人的には思っております。

ちなみにGridsomeについては僕も試したときの記事を書いてます。興味ある方はどうぞ。

www.keisuke69.net

サーバーフル? サーバーレス?

まず、JAMStackの場合はとても簡単な話ですね。出力した静的ファイルをどうやって公開するかってだけです。普通にWebサーバ立てるのもありなんでしょうけど、Vercel、NetlifyとかS3のStatic site hosting機能なんかを使って公開することが多いと思います。こういうの使うってのはつまりサーバーレス。

ちなみにJamstackの場合はどこで公開するかってのもあるけど、それよりもGitでPushしてからの静的サイト生成してからのデプロイっていういわゆるデプロイパイプラインをどう作るか、どんな感じでサポートされているかってところのほうが重要だったりするかと個人的には思う。

この辺りは前述のGridsomeで作ったサイトをAmplify Consoleでデプロイしてるブログ書いてるので、こちらも興味ある方はどうぞ。

www.keisuke69.net

で、ようやく本題。

割と面倒なのがサーバーサイドレンダリングをやりたい場合ですね。ちなみにこちらの場合もHTMLやCSSといった静的なファイルはNetlifyとかS3とか使えばいいですね。

サーバーサイドレンダリングの場合は、先ほど述べたようにサーバーサイドでレンダリングするのでそのためのサーバが必要になります。Node.jsを入れたサーバとかですね。

最近だとよくあるのはNode.jsを入れたサーバをコンテナで用意するパターンでしょうか。コンテナでやるのもいいのですがそれだとランタイムだのなんだのって管理しなければいけなくなります。

また、そもそもコンテナを実行するための基盤も必要になりますよね。

あとはサーバーサイドレンダリングのインフラ的な課題としてこんなのがあります。

  • CPU負荷が高くなりがち
  • CPU負荷高いのでさばけるリクエスト量が少なくなりがち
  • キャパシティ不足になってしまうとレスポンスが返せなくなることがあり、そうなるとブラウザ上では真っ白が画面に…

他にもブラウザとNode.jsの両方で動くことを想定した実装が必要とかいろいろありますがそのあたりは今回は割愛。

言いたいことはサーバーサイドレンダリングはインフラ的な課題が発生するということですね。

というわけでサーバーレスです。

サーバーレスでサーバーサイドレンダリング

さて、サーバーレスといっても世の中にはいろいろありますが、ここでは基本的にAWSのサーバーレスを前提に話を進めていきたいと思います。つまりAWS Lambdaとかそのあたりですね。

サーバーサイドレンダリングやるのにLambdaを使うってのはさっき言ったコンテナとかインフラそのものの管理をしたくないとかっていうそういうところもあるんですが、CPU負荷が高いところとそれによるスケーラビリティ上の難点を解決してくれるのではってところがあります。

Lambdaの場合、1リクエスト(=イベント)に対して1インスタンス(=コンテナ)が対応します。同時にリクエストが来た場合もそれぞれ別のインスタンスが対応することになります。なのでリクエストが集中してサーバに処理が集中してCPU負荷が高まった結果クライアントにレスポンスが返せなくなるみたいなことが発生しないんですね。

というわけでLambdaでやるわけですが、LambdaでやるといってもそのLambdaを呼び出すために手前にAPI Gatewayを用意します。

あと、キャッシュは必須ですね。なのでCloudFrontも併用します。つまり、キャッシュヒットしないやつはAPI Gatewayを経由してLambdaでサーバーサイドレンダリングされる感じになります。

それ以外の画像系とかその手の静的ファイルはCloudFrontで振り分けた上でS3を使って配信します。

あれ、そうすると普通のSPAとあまり変わらないってことに気づいたかと思います。そうです、つまり気にするのはLambda上の実装だけになります。

Nuxt.jsでの例

さて、ここからはNuxt.jsでサーバーサイドレンダリングする場合を例として書いていきます。といっても実はとてもシンプルです。

まずクライアント側は普通に作ります。nuxt.config.jsをちょっといじってあげる必要はあります。これは次回に。

そしてサーバー側はExpressを使います。その上で必要になるのがaws-serverless-expressというライブラリです。これはExpressで書かれたアプリをLambdaで実行可能にするためのラッパーのようなものです。

github.com

これを使ってやることは単にモノリシックなLambdaファンクションとしてデプロイして、すべてのリクエストに対してそれを経由させてExpressで作られたアプリケーションを呼び出すってだけです。

というわけでExpressで動くNuxt.jsのアプリを用意した上で、LambdaファンクションをNode.jsで作って前述のaws-serverless-expressを利用してExpressをLambdaファンクション上で実行する、つまりNuxtによるサーバーサイドレンダリングをLambda上で実行する。と、そういうわけです。

なお、そのためにLambdaファンクションとしてのエントリポイントとなるhandler関数を用意して、そこからNuxtのコードを呼び出していく感じになります。

以下はサンプルそのままのhandler関数ですね。

'use strict'
const awsServerlessExpress = require('aws-serverless-express')
const app = require('./app')
const server = awsServerlessExpress.createServer(app)

exports.handler = (event, context) => { awsServerlessExpress.proxy(server, event, context) }

ここでcreateServerに渡しているapp.jsがありますが、ここにNuxtとExpressを使うもろもろを記述していきます。が、そのあたりは時間切れとなってしまったので次回に。

次回

というわけで後編に続きます。

今回は最後にちょろっと説明しただけで実装面は何も説明しませんでしたが、後編では実際にサーバーレスでの実装をソースコードつきで具体的にお見せしていきたいと思います。

ちなみにVue.jsとNuxt.jsでやりますが、ReactとNext.jsでやりたい場合はServerless FrameworkのServerless Next Componentというのを使えば良さそうです。これは試したことないんですがいつか試してみようと思います。

github.com

AWS Lambdaが提供するProvisioned Concurrencyという機能を簡単に説明してみる

f:id:Keisuke69:20200710093627j:plain

はじめに

実は本日開催のイベントでProvisioned Concurrencyという機能について話す予定です。

serverless-newworld.connpass.com

このイベントは省エネで特定のトピックについて簡単に話すというのをモットーとしていまして、普段の各種イベントでの講演のようにプレゼンテーション用のスライドを作ったりはしないつもりだったんです。と、言いつつも前回は過去に書いたブログ記事を使って話をしたんです。でも今回はそれすらない。

最初はそれでもいいかと思っていたんですが、口頭だけだとなかなか難しそうだなと思えてきたので4時間後のイベントのためにブログをしたためています。

大前提

AWS Lambdaにはコールドスタートというものがあるのは多くの人が知っていると思います。以下はこのあたりに関する前回の記事のコピペです。

リクエストが来たらそれを処理するLambdaファンクションのコンテナを作成し、処理を実行します。

このLambdaファンクションのコンテナは1つあたり同時に1リクエスト(1イベント)しか処理しません。従って同時に100リクエスト来た場合は同じLambdaファンクションのコンテナ100個で処理されます。ただし、必ずしも毎回100コンテナが1から作成されて起動されるわけではありません。

Lambdaファンクションのコンテナは起動コストの効率化のために再利用も行います。いわゆるウォームスタートというものですね。ウォームスタート可能なコンテナがあれば新しくコンテナを作ることはせずにそれを利用します。しかし、ウォームスタートできるコンテナがない場合やあってもその数以上のリクエストを同時に処理する必要が発生した場合は新しくコンテナが作成・起動されます。これがデフォルトの同時実行1000だと1,000コンテナが同時に実行される可能性があるわけです。もちろん10,000であれば10,000個です。

はい、なんとなく理解しましたか?

そんなコールドスタートをみんななるだけ避けたいと思ってあの手この手で対応をしようとしています。とはいえ、コールドスタートはゼロにはできません。なので、本来はコールドスタートをいかに短くするかっていうのが基本ではありますが、今回はその話はしません。

そんなコールドスタートを避けるために古来から空実行と言うか、本来のリクエストではないリクエストを事前に送っておくなどして来たるべきに備えて先にコールドスタートを発生させておくことで、本来必要なリクエストを処理する際には起動済のコンテナが再利用されることを狙うってことをやっている人たちもいました。

これ自体は実のところあまり意味があるものではありません。というのも結局のところ、事前にリクエストをしたところでそれでカバーできるのはそのリクエスト数分だけなのです。

つまり何分かに1回実行するだけでは維持されるコンテナは1つだけです。そこに2以上の同時アクセスが来たらあぶれた分はコールドスタートが発生します。Pre Warmingのためのリクエスト数を増やせばいいのですが、そうすると本番で必要となる同時実行数分をリクエストし続けておく必要があり、サーバーレスのメリットでもあるNever Pay for Idleという特性が失われます。

また、いくらこれをやってもLambdaファンクションのデプロイをしたり、設定変更をするとコンテナの作り直しになるため、全体的にコールドスタートが発生してしまうことは防ぎようがありません。

トラフィックバースト

さて、先ほどの説明で同時実行という言葉が出てきました。これはデフォルトでは1,000に制限されています。デフォルトでは、と言ったのはこれは必要に応じて緩和申請をすることができるからです。とはいえ無尽蔵に、無制限に好きな値で申請できるわけではもちろんないです。必要なサイズを申請することになり、それが許可されればそのアカウントについては同時実行数が増えることになります。

さて、とあるLambdaファンクションに対する実行リクエストが大量に発生したときにどうなるかなんですが、この許可された同時実行数までは特に制限なく実行されると思っている人も多いですが、これは勘違いです。

Lambdaには同時実行のバーストについても制限があり、東京リージョンの場合は1,000となっています。これは先ほどの同時実行とは異なり緩和不可な制限となっています。

ではこのバーストの制限とはどういうものか。

平たく言うと同時実行数が1,001以上に許可されていた状態であっても、大量のリクエストが同時に発生した場合一気にそこまで増えるのではなく、このバースト制限までしか最初の段階ではコンテナの数は増えないということです。

例えば許可された同時実行数が30,000だとして、全然リクエストが来ていない状態(つまり、起動された状態で維持されたコンテナが0の状態)とします。この状態で10,000リクエストが同時に来た場合、この10,000リクエストを処理する分のコンテナが一気にコールドスタートするわけではないです。あくまでも最初の段階では1,000リクエスト分までで、その後もリクエストが続いている場合は1分ごとに500コンテナずつ起動されていきます。

これは、すべてのリクエストを処理するコンテナが起動しきるまで続きます。その前に許可された同時実行数に達した場合はその時点でこの動きは止まります。入ってくるリクエストにこのスケールのスピードが追いつかない場合はスロットリングされます。もちろん同時実行数に達した場合も同様です。

スパイクが発生するときにこのあたりの仕様・特性を知らないと悩むことになります。また、知っていても悩むことになります。スパイク時にコールドスタートが起きるのを減らしたいはもちろんなんですが、そもそもコールドスタートが起きることもない状態になってしまう場合もあるからですね。

Provisioned Concurrency

簡単に言うとPre Warming as a Serviceです。要はコールドスタートが起きないように一定量を事前に温めて置くことができようになったってことです。

とはいえ事前にプロビジョニングしたとしても、設定してすぐに効果を発揮するわけではなくて徐々に準備されていきます。このときも先ほどのトラフィックバーストと同様の挙動です。つまり、東京リージョンであれば一度に最大1,000をコールドスタートして初期化した後、1分ごとに500ずつ設定された値までプロビジョニングされていく感じです。

あと大事なこととしてお金かかります。

何が解決するのか

予測されるスパイクに対してはあらかじめWarm状態で待ち受けられるのでコールドスタートによるレイテンシ増を減らせます。予測されるスパイクに対してはってのがポイントです。結局のところ、Provisioned Concurrencyを設定していても、それを上回るリクエストが来てしまうと同じです。この場合、上回ったリクエストに関しては前述のバーストの制限の影響も受けます。

あとはスパイクはそれほど発生しないけれどもコールドスタートが重い処理なんかのために使うのもいいかもしれません。

設定のコツ

事前にプロビジョニングしたいLambdaファンクションがいくつあるかによって変わってきます。Lambdaの同時実行の制限自体はアカウント全体で共有されます。なので、各Lambdaファンクションでどのくらいのスループットを出したいかによって変わってきます。

デフォルトの同時実行1000の状況だとProvisioned Concurrencyとして設定できるのは900までです。設定したいLambdaファンクションが1つの場合はそれに全振りすればいいのですが、例えば2種類ある場合は、各ファンクションで処理したいスループット、例えばxx rpsとかと各関数の実行時間を考慮して決めていきます。

例えばファンクションがAとBの2つあって、それぞれ実行時間が500msと1000msだった場合に両方とお600rps処理するにはAに300、Bに600プロビジョニングすることになります。

もう少し詳しく説明すると、デフォルトの場合に設定可能な値は合計900までという話をしました。これをファンクションA、Bにどうやって割り振っていくかです。Aは500ms/リクエストなので要件の600rpsを処理可能にしようとすると1リクエストが500ms = 0.5秒なので1つのコンテナでは秒間で2リクエスト処理できます。したがって600rps、つまり秒間600リクエストを処理するには600/2=300必要ということになります。同様の計算でBは600をプロビジョニングすることになります。

ポイントは同時実行の制限を共有するということです。

まとめ

Provisioned Concurrencyは銀の弾丸ではないけど金の弾丸ではあるかも知れない。つまりお金で解決する手段ってことですね。前述のとおり、Provisioned Concurrencyは無料で設定可能な機能ではないので。

必要に応じてコールドスタートの時間自体を短くするように基本のチューニングを施す、アーキテクチャを見直すなども当然ながら必要ということですね。

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