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向けの似たようなものもそのうち出てくると期待。

Monthly AWS Serverless Update 202010

2020年10月のサーバーレス関連まとめです。こちらのイベントで取り上げる内容です。

serverless-newworld.connpass.com

2020/11/05 Update 配信のアーカイブがあがっています。


Monthly AWS Serverless update 2020/11

2020年10月のリリース

10月は割と大きめのアップデートがありました。

AWS AppSync が AWS WAF のサポートを追加

  • 待望の人も多いんじゃないでしょうか。AWS AppSyncがAWS WAFに対応しました。少し前に質問でもいただいてましたね
  • これまでWAFを使うのが難しかったので泣く泣くAppSyncを諦めてた人には朗報かと思います

Amazon SNS を使用して SMS を送信するアプリケーションが新しい 5 つのリージョンでホスティング可能になりました。

AWS Lambda の拡張機能: Lambda を運用ツールと統合する新しい方法 (プレビュー)

  • これも嬉しい人多いのではないでしょうか
  • 日本語ブログだと拡張機能になってますがいわゆるAWS Lambda Extensionsのことです
  • サードパーティを含む各種運用ツールを統合しやすくなったというもので、現時点ではサードパーティだとAppDynamics、Check Point、Datadog、Dynatrace、Epsagon、HashiCorp、Lumigo、New Relic、Thundra、SplunkあたりのExtensionが使えるようです
  • これまでは難しかったLambdaのライフサイクルに深く関与することが可能になりました。ブログの内容も書き換える必要がある
  • プレビューです

Amazon CloudWatch Lambda Insights の発表 (プレビュー)

AWS Lambda が、AWS PrivateLink のサポートを開始

  • これは金融系のシステムなどより強固なセキュリティを必要とするユーザには待望の機能かもしれません
  • VPC内からインターネットを介さずにLambda関数を呼び出すことができるようになります
    • たまに誤解している方がいるのですが、Lambda関数をVPCアクセスできるようにしてもLambda関数の実行自体はユーザのVPCではなくサービスのVPCで行われたままです。つまり実行呼び出しはインターネット越しにAPIを呼び出すしかなかった
  • 先月のAPI GatewayのMutual TLS対応と合わせてよりサーバーレス採用しやすくなってきたのではないでしょうか

Amplify プロジェクトに既存の Cognito ユーザープールと ID プールを使用する

  • 嬉しい人多そう

Amazon SNS は、メッセージの厳密な順序付けと重複排除が行える先入れ先出し (FIFO) トピックを導入

  • Amazon Simple Notification Service (SNS)でもFIFOをサポート
  • このアップデートは個人的にはちょっと驚いた

AWS Step Functions が Amazon Athena サービスとの統合サポートを開始

Amazon SNS で、SMS メッセージを送信する際に発信番号の選択をサポートできるようになりました

Amazon API Gateway は、デフォルトの REST API エンドポイントの無効化をサポートするように

  • これは何が嬉しいかっていうと、これまではAPI GatewayのRESTでAPIを公開していた場合、カスタムドメインを使っていてもデフォルトで用意されたエンドポイントが利用できたままだったが、それを閉じられるということ
  • つまり、設定したカスタムドメインへのアクセスだけに制限できるということ

Amazon Cognito ユーザープールにより、クォータ管理と使用状況の追跡が容易に

その他ブログなど

Introducing AWS Lambda Extensions – In preview | AWS Compute Blog

  • Lambda Extensionsに関するより詳細な内容が書かれています。英語です

AWS SAM CLIを使ったLambdaのローカル実行と簡単デプロイ - BASEプロダクトチームブログ

  • SAMの紹介もさることながら、ポイントはBASEさんで実際に行っている運用設定の一部が紹介されている

白いマスクから怪人マスクへ、AWSでサーバレスLINE写真処理アプリの開発記 - Qiita

  • Amazon Rekognitionとの組み合わせ

SODANE - AWSと各種SaaSを活用したサーバレス・ライブコマースシステムの開発

  • 北海道テレビさんの水曜どうでしょうエアキャラバンin 赤平2020のオンラインイベント会場の事例
  • 驚くべきはサーバーサイドの担当者、インフラ担当者がおらず、プログラミングにもそれほど明るくない状態からこれを立ち上げてサービスインしているところ
  • これはうかうかしていられない

API Gateway 開発者に読んでほしい、意味がわかると便利な実行ログ - Qiita

  • ぶっちゃけAPI Gatewayのログ周りって難しいところあると思うのですがそのあたりが解説されています

サーバーレスのプラクティスをシンプルに実現する / AWS Lambda Powertools - Speaker Deck

  • AWSJのサーバーレスSA下川さんによるLambdaの便利ツール紹介

サーバーレスアーキテクチャを採用する際のコストに対する重要な考え方 | Serverless Operations

Misreading Chat で "From Laptop to Lambda: Outsourcing Everyday Jobs to Thousands of Transient Functional Containers" について話した - blog.8-p.info

AWS Amplify 実践編 #AWSDevDay / Practical AWS Amplify at AWS Dev Day 2020 - Speaker Deck

  • AWS Amplifyでこういうことできないかな?っていうような話が詰まってます

分散システムにおけるSagaパターンのAWS Step Functions による実装 #AWSDevDay - Speaker Deck

  • 以前のPodcastに福井さんがゲストで出ていただいたときに話していたSagaパターンをAWS StepFunctionsを使ってどうやって実装するかって話
  • Sagaパターンそのもの解説もある

Lambda Extensionに対応した、New Relic サーバーレスモニタリングを試してみた | Developers.IO

  • Lambda Extensionsで使えるNew Relicを試してみた話
  • クラメソブログですが書いているのはNew Relicの人っぽい

localstack有料版をちゃんと使ってみる - 続 カッコの付け方

  • AWSの各サービスのAPIを叩く処理を開発するときにローカルで使えるモックサーバのようなものとしてlocalstackというのがあるんですが、それの有料版の話です
  • このlocalstack、元はAttrasianが提供していたんだけど今見ると違うのかな?ちょっとわからないです
  • 有償版は無償版と比べると使えるサービスの数だったりLambda Layerをサポートしてたり認証周りが使えたりってのがあるみたいです

The Ultimate Guide to AWS Step Functions | Dashbird

  • Ultimate GuideというだけあってかなりボリューミーなAWS StepFunctionsに関する解説記事。英語です

サーバレス革命はなぜ行き詰ったのか

  • まだ道半ばなので行き詰まっているとは思わないけどノーコメントでw

個人開発・スタートアップで採用すべき最強のアーキテクチャを考えた - Qiita

  • ちょっと話題になりましたね
  • 個人的にはあくまでも全部AWSで作らならって感じだとは思います

Jamstack とサーバーレスで提供する「大阪都構想」特設サイトの舞台裏 - JX通信社エンジニアブログ

  • つい先日行われた大阪都構想住民投票についての特設サイトを作った話
    • NewsDigestの特設サイトの裏側だそうです
  • Next.jsを使っていて、Serverless Frameworkのプラグインであるserverless-next.jsを使ってます
  • OSSなども活用することで開発開始から1週間半程度でリリースしたそうです
    • Headless CMSはメイン開発者の人がOSSで開発しているものを採用したとのことですが、それでも速い

AWS Amplifyを無理して使わなくてもいいケースについて挙げてみる

AWS AmplifyといえばAWSが提供しているフロントエンド開発者向けのライブラリやツールセットです。今回はそんなAWS Amplifyについてです。

はじめに

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

serverless-newworld.connpass.com

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

あと、今回Amplifyを取り上げる理由は、以前このシリーズで取り上げるテーマとして何がいいかっていうアンケートを取った結果です。

AWS Amplifyのウリだったりいいところってのはこれまでにいろんなところで語られていると思うのでそちらにお譲りするとして、サーバーレス同様に現実世界の課題すべてを1つのソリューションだけで解決しようとするのはあまりおすすめしないよねってことで、そんな観点でまとめます。

(2020.10.22 update)

上記イベントのアーカイブを公開しています。


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

チャンネル登録してくれると更新通知も飛ぶのでよろしくです。

おさらい

そうは言っても、地ならしがてら簡単にAmplifyの特徴についてまとめておきます。

まず、AWS Amplifyと一言で言っても、いくつかの構成要素がありましてAmplify Library、Amplify UI Component、Amplify CLI、Amplify Consoleから構成されます。LibraryとUI Componentは区別していないこともありますが、わかりやすく言うとフロントエンドアプリケーションの開発に使うもの、バックエンドの構築・設定に使うもの、ビルド/デプロイ/ホスティングに使うものにわかれてると捉えてもらうといいでしょうか。

で、その特徴としてもいろいろあげられていますが、とても乱暴な表現であることをあえて承知で言いますと、

バックエンドにAWSを使ったフロントエンドアプリケーションを(全て手動でやるよりは比較的)手軽に開発するためのもの

と言えるでしょう。この説明に多方面からツッコミが入りそうな気はするが今回の本題ではないので気にしない。

フロントエンドアプリケーションと言っているとおり、JavaScript / iOS / Androidあたりに対応しています。JSについてはReact/Vue/Angularに加えて、ReactNativeとIONICに対応しています。今をときめくFlutterについてもDeveloper Previewとして対応されています。あ、最近Server Side Rendering(SSR)にも対応しました。その他、細かい特徴とかは公式のドキュメントやAWSがやってるセミナーの動画とかを見てもらうととても詳しく説明されています。

ちなみに今回取り上げるのは主にLibraryとCLIに関連する話、つまりAmplifyを使った開発そのものに関わる話。デプロイやホスティングをしてくれるAmplify Consoleに関してはほとんど触れません。というか、Amplify ConsoleについてはSPA(Single Page Application)やSSG(Static Site Generation)のアプリケーションをAWSで簡単にGitを中心としたデリバリパイプラインを作って公開するにはとてもいいものだと思う。Jamstackなんかはビルド9割だと思っていて、そのくらいコンテンツの更新からビルドおよびデリバリのパイプラインが重要だと思うのでそういうときにAmplify Consoleは便利だと思う。

AWSアカウントが必要ってのはあるけど持ってるならばとてもいいと思います。僕も個人的な用途で使っています。

AWS Amplifyはフロントエンド開発者にとっての銀の弾なのか

はい、もちろん違います。そもそも前述のとおりバックエンドをAWSで開発していない限り嬉しさも少ないです。というかゼロだと思います。あくまでもバックエンドにAWSがいる前提で使うもの。

自分が話をすることの多いサーバーレス関連でもよく言ってるのですが『最大のアンチパターンはすべてをサーバーレスにすることにこだわること』と言っています。

そしてこれはあらゆるものに当てはまると思っています。使い古された言葉ですが銀の弾などないのです。もちろんAWS Amplifyについても同様です。

その上でAWS Amplifyについて言うなれば、先ほども言ったとおりAWS Amplifyはあくまでも手動でもできることを比較的手軽にやれるようにしてくれているものです。フロントエンド開発者フレンドリーに。

つまり、言い換えるとAWS Amplifyじゃないとできないことは正直なところほとんどありません。

さらに言うとAWS Amplifyで対応していたとしても実際のところAmplify使ってやるより別の方法のほうがいいんじゃない?って思う点も個人的にはいくつかあります。

無理して使わなくてもいいケース

バックエンド側のライフサイクルや開発部隊が分かれてる場合

先ほども言ったようにAmplifyのメリットはフロントエンドから見たときにバックエンドを簡単に扱えることがメリットです。なのでこのケースの場合はAmplify使うメリットが薄まると思っています。

Amplifyを使う場合ってバックエンドはAmazon API Gateway + AWS Lambdaを使ったサーバーレスなREST APIAWS AppSyncを使ったGraphQL、もしくはAmazon S3などのようにAWSサービスやそのリソースをAPI経由で直接操作するパターンかと思います。

なかでも、REST APIを作る場合においてバックエンド側のライフサイクルや開発部隊が分かれている場合、フロントエンド側がバックエンドをいじることは少ないですよね。つまり、フロントエンドはバックエンドAPI作らないし、バックエンド側にとっても提供するものはAPIまでになるのでクライアントライブラリであるAmplifyは不要です。

また、これはあくまでも個人的な意見ですが、仮にフロントエンド開発者がREST APIまで作る場合だとしてもサーバーレスなリソースのプロビジョニングやデプロイを行うのはAWS SAMであったり最近だとAWS CDK使うほうが圧倒的にいいと思っています。餅は餅屋ということです。その中でも個人的にはAWS謹製ではないですがServerless Frameworkが好みです。理由はプラグイン機構とそのエコシステム、これに尽きる。

なぜ、Amplifyではなくこれらのほうがいいかというと、amplify add apiREST APIを作成するとLambda関数周りはこんな感じで生成されます。

amplify/backend/function/
|-- serverlessnewworld20d765121f
|   |-- amplify.state
|   |-- dist
|   |   `-- latest-build.zip
|   |-- function-parameters.json
|   |-- parameters.json
|   |-- serverlessnewworld20d765121f-cloudformation-template.json
|   `-- src
|       |-- event.json
|       |-- index.js
|       |-- package-lock.json
|       `-- package.json
`-- serverlessnewworld20e0fe6255
    |-- amplify.state
    |-- function-parameters.json
    |-- parameters.json
    |-- serverlessnewworld20e0fe6255-cloudformation-template.json
    `-- src
        |-- event.json
        |-- index.js
        `-- package.json

amplify pushすると作成したリソースをAWS上に反映するんですが、その際Lambda関数はデプロイパッケージにまとめられます。このツリーではamplify/backend/function/serverlessnewworld20d765121f/dist/latest-build.zipがそれです。これが関数ごとにできます。残念なのはこれを個別にデプロイできないんですよね。

また、API Gateway + Lambdaで完結するならまだいいですが大抵の場合はLambdaからアクセスする別のリソースが存在していることも多いです。そういったものについてはAmplifyの対象から外れてしまうんですよね。後述するやり方でできなくもないですが、それだったらシンプルにCDKやサーバーレス系のデプロイツールを使ったほうがいいと思います。

AWSのリソースに直接アクセスしない場合

フロントエンド側からAWSリソースに直接アクセスするケースっていろいろあるとは思いますが、多いのはAmazon CognitoとAmazon S3を利用する場合なのではないかと思います。

さて、ここでAWSリソースに直接アクセスするときのAmplifyの利点について簡単にお話すると、大きく2つの観点があります。1つはAmplifyがサポートしているものであればソースコードの記述がシンプルになるということ。もう1つはそれらリソースへのAmazon CognitoおよびAmazon Identity and Access Management (IAM)を使った認可が簡単に扱えるということです。

実際に以下は公式に載っているサンプルですがS3へのファイルアップロードだとこんな感じにかけます。

import { Storage } from 'aws-amplify';

~~

Storage.put('test.txt', 'Hello')
    .then (result => console.log(result))
    .catch(err => console.log(err));

とてもシンプルです。これだけでS3に対してファイルをpublicにアップロードできます。同じことを普通に書いたらこんな感じです。実際に動かして試してないのであくまでもイメージってことでお願いします。

var upload = new AWS.S3.ManagedUpload({
  params: {
    Bucket: sample,
    Key: "test.txt",
    Body: "Hello",
    ACL: "public-read",
  },
});

var promise = upload.promise();

promise.then(
  function (data) {
    console.log(data);
  },
  function (err) {
    console.log(err);
  }
);

簡単さがわかると思います。このあたりはまさにAmplifyの存在意義を示していると思います。バックエンドのAWSリソースをフロントエンド開発者フレンドリーな形で使いやすくなってますね。

一方、裏を返すとこの2点が不要な場合はAmplifyも不要かもしれません。

認証・認可の手段としてCognito使わない場合

よくAmplifyのサンプルや紹介でCognitoを利用したAuthの実装が簡単だというものを見たことがあるかと思います。確かに、Cognitoで認証する場合は、Cognitoの設定がCLIから簡単にできるだけでなく認証画面も簡単に作れます。また、前述のとおりAWSの認可の仕組みをそのままアプリケーションからも利用したい場合はAmplifyという選択はとてもいいと思います。

一方で、そもそも認証手段としてCognito以外を使いたいケースも多いのではないでしょうか。そしてIAMベースの認可が不要な場合。この場合はもちろんAmplifyは不要です。

そして、認証は別でやりたいけどIAMベースの認可はしたい。そんなときCognitoにはSAML連携やOIDC対応の外部プロバイダ、あとは主要なソーシャルログインプロバイダ(FacebookGoogleなど)と認証部分を連携させることができます。

でもAmplify CLIからこれらの設定がすべてサポートされているわけではないんですね。例えばSAMLやOIDCのプロバイダ、例えば認証にAuth0を使うといったフェデレーションの設定はAmplify CLIからはできないため結局、マネージメントコンソールからの作業が必要になります。
※ソーシャルログインのフェデレーションは設定できます

Server Side Rendering(SSR)の場合

まず、Amplify Consoleの話から。AmplifyがSSRに対応したといってもLibrary側の話であってAuthにおけるLocal Storage依存をやめたとかそういう話です。Amplify ConsoleでSSRもホストできるようになったというわけではないです。つまり、Amplify Consoleでは依然としてSSRを必要とするサイトはホスティングできない。

2021年5月18日にNext.jsに関してはSSRできるようになりました!

AWS Amplify Hosting が Next.js ウェブアプリのサーバーサイドレンダリング (SSR) サポートを発表

でも残念ながらv9にしか対応していないので現行バージョンのv10では使えません。あとIncremental Static Regenerationもまだ無理です。

v10対応についてはFeature Requestが作成されているのでぜひ皆さんVoteしましょう。

Next.js v10 support · Issue #1915 · aws-amplify/amplify-console · GitHub

そしてLibrary側の話をすると、Next.jsとかを使ってSSRしたいときに普通に使えるよってことです。ただ、公式のドキュメントとかではデプロイをamplify pushではなくてServerles FrameworkのServerless Next.js Componentを使ってる。だったら別にAmplify要らないかなーと思ったけど、そこは結局前述のAWSのリソース使いたい場合とかと一緒ですね。

AWS WAF使いたい場合

これもAmplify Consoleの場合の話です。Amplify Consoleの静的サイトのホスティングは、コンテンツ配信にCDNであるCloudFrontを自動でセットアップしてくれます。だがしかし、ここで作られるCloudFrontのリソースはユーザには見えないのです。つまりサービス側が所有するリソースとして作られているのです。というわけで何らかの理由でCloudFrontと連携させる形でAWS WAFを使いたいとなっても現時点ではできない。あとはCloudFrontの細かい設定をしたい場合なども。

代わりにamplify publishを使えばユーザのリソースとしてCloudFrontとS3がセットアップされますが、AWS WAFを使う場合は別途設定が必要です。

Amplify CLIに対応していないバックエンドを利用したい場合

一応このような場合でも、Cloudformationのテンプレートを自分で用意してカスタムカテゴリとして登録するって方法があるにはあります。なので基本的にはAmplifyの機能を使いつつそこだけ自分で用意するってことができます。

でも、自分でCloudformationのテンプレート用意する時点でAmplifyの利点は失われているとも言えます。それを用意するならCDKとか使ったほうが楽だなと僕なら思ってしまいます。もちろん、そのボリューム感見合いの話ではあるのですが。

AWS Amplifyがおすすめのケース

最後にAmplifyがおすすめのケースについても簡単にお話しておきます。基本的にはこれまでの話の裏返しなんですが、ここまで触れてこなかったパターンが1つあります。

それは、

フロントエンド開発者がAppSyncを使ってGraphQLでシンプルなアプリを開発する場合

です。このケースの場合はAmplify使うのを強くおすすめします。とても良い体験が得られると思います。

これに関してはいろんなところでサンプルやドキュメントがあるのでそちらを参照してください。

一方でシンプルと言っているのには理由があります。それはAmplifyからAppSyncを使う場合に利用できる@connectionディレクティブです。これを使うとDynamoDBにも関わらずRDBMSのようなリレーションを表現できます。実際にはリレーションを表現するためのリゾルバが自動で生成されるのと、多対多の場合もマッピングテーブルを用意して実現できます。なので、RDBMS感覚で複雑なリレーションを張ってしまうと…、はいお察しですね。

というわけであくまでもシンプルな構成、リレーション張るにしても感覚的には1階層くらいかなと思っています。繰り返しますが、餅は餅屋です。

まとめ

というわけでAWS Amplifyはドンピシャにはまるケースだととても簡単にフロントエンド側からバックエンドのプロビジョニングや設定も行えるのでそういう場合はおすすめです。

一方でこのようなEase of Developmentを実現する系のツールセットはあくまでもシンプルなものを手軽に素早く立ち上げるのには向いていますが、一歩そこからはみ出て凝ったことをしようと思うとそのメリットを享受できなくなることもしばしばあります。

特にAmplifyのようにビルディングブロックとして存在するバックエンドをWrapする形で実現しているものはその傾向が強いと思います(仕方がないとも思います)。

GraphQLの使い勝手と相まってプロトタイプ開発なんかだとほんといいと思うので時間があればぜひ試してみてください。あとはAWSリソースを直接触る場合も!

Monthly AWS Serverless Update 202009

新たに始めるMonthly AWS Serverless Updateの第一回です。

こちらのイベントです。

serverless-newworld.connpass.com

今後も月1でやっていこうとは思っているものの、どんなフォーマットでやるのがいいか、そもそもライブでやるより録画のほうがいいのではないかなどありますが、手探りでやっていきたいと思います。フィードバック募集中。

9月のリリース

AWS Step Functions はペイロードサイズを 256KB に増加

Amazon SNS を使用して SMS テキストメッセージを送信するアプリケーションが米国西部 (北カリフォルニア) でホスト可能に

API Gateway HTTP API が Lambda および IAM 承認オプションのサポートを開始

  • REST APIで使えたLambdaオーソライザとIAMオーソライザがHTTP APIでも利用可能に

Amazon SQS は、AWS GovCloud (米国西部) リージョンでタグオン作成のサポートを開始

AWS Step Functions が新たに AWS X-Ray をサポート

AWS Lambda が AWS Step Functions ワークフローを視覚化するためのコンソールサポートを追加

  • AWS Step FunctionsのワークフローがAWS Lambdaのコンソールで表示可能になった
  • こんな感じでLambda側のダッシュボード上から見えるようになってます

f:id:Keisuke69:20201006145102p:plain

Amazon API Gateway が相互 TLS 認証のサポートを開始

  • これまでも金融のお客様に切望されていたMutualTLSについに対応
  • Mutual TLSが使えるということはクライアント認証ができるようになったということです
  • 2020年9月時点ではRegionalおよびHTTP API限定

その他ブログなど

9月はCloudNative Days TokyoにJAWS SONIC、そしてAWS Summit Onlineと大きめのイベントがいくつかありました。

bash-lambda-layerのAmazon Linux 2対応をすることを決意した話 - KAYAC engineers' blog

AWS-35 Let’s dive deep into AWS Lambda error handling

【レポート】Let’s dive deep into AWS Lambda error handling #AWSSummit | Developers.IO

  • 上記AWS Summitでのセッションのクラメソさんによるダイジェスト

AWS LambdaでSeleniumとActiveRecordを扱う · フロッピーディスクの残骸

AWSLambdaでPyTorch【EFS編】 - Qiita

AWSLambdaでPyTorch【Lambda import編】 - Qiita

  • EFSとPyTorch使って機会学習やってみる話
  • 今回はLambdaから利用するためのEFSのボリューム作って、EC2からライブラリなどをEFSのボリュームにインストール。Lambdaからマウントして利用。
  • Lambda + EFSの正しい使い方

API Gatewayにカスタムドメインを設定するためのリソースを全てAWS CDKでつくってみた | Developers.IO

Serverless FrameworkでAurora Postgres + VPC Lambda + RDS Proxyをデプロイする | Developers.IO

Chalice を使って AWS Lambda 上に Flask/Bottle のようにWebアプリケーションを構築する - Qiita

  • 従来、FlaskやBottle使って作ってたようなWebアプリをChaliceを使ってサーバーレスでって話
  • FlaskとかBottle使った開発体験はローカルではいいけどサーバ用意するのはそれなりに大変ってことで

LambdaとGoを使ったサーバーレスWebAPI開発実践入門 | フューチャー技術ブログ

  • 本題ではないですが開発にLocalstack使ってますね

【神機能】Serverless Dashboardを使って爆速でCICD環境をセットアップ | Developers.IO

  • Serverless Frameworkのダッシュボード使ったCI/CD環境のセットアップ
  • 一応お伝えしておくと、AWSでもCodeStarってのを使うとサーバーレスに限らず同じようなことができたりはします

Amazon API GatewayでmTLSを試してみた。(1/2) - Qiita

Amazon API GatewayでmTLSを試してみた。 (2/2) - Qiita

ACMのPrivate CAを利用した場合のAmazon API GatewayのmTLS構成を試してみた。 - Qiita

中間CAの証明書期限切れ時のAmazon API Gatewayのトラストストアの更新を試してみた。 - Qiita

  • API GatewayのMutual TLSサポートに伴う細かい話。上のAWS Summit OnlineでLambdaのセッションを担当した人が書いてます

AWS Lambdaの裏側をなるだけ詳しく解説してみる

AWS Lambdaの環境がどのようになっているか、ユーザが用意したLambdaファンクションがどんな感じで実行されるかってあたりを可能な限り詳しく説明したいと思います。

はじめに

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

serverless-newworld.connpass.com

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

(2020.09.30 update)

上記イベントのアーカイブを公開しています。


サーバーレスアンチパターン今昔物語 第五夜 - 解体新書 -

チャンネル登録してくれると更新通知も飛ぶのでよろしくです。

また、次回は10/22に開催します。次回はAmplifyについて話す予定です。

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

新たにアップデートだけ紹介する回も始めます。9月分は10/5なのでよろしければこちらもどうぞ。

Monthly AWS Serverless Update 202010 - connpass

大前提

まず、いろんなところでも言及していますが大事なことなので改めて。

AWS Lambdaは水平方向へのスケーラビリティを特徴とするサービスです。リクエストが来たらそれを処理するLambda関数のコンテナ(インスタンス)を作成し、処理を実行します。

ここで言うインスタンスとはプログラミング言語で言うところの『インスタンス』という意味に近く日本語で『実体』とか訳されているものであり、EC2インスタンスのことではありません。ここ誤解のないように。ドキュメントにあわせてインスタンスという単語を使っていますが、Lambdaにおいてはコンテナのことです。そしてあとのほうに出てきますが、内部的には実行環境とかSandboxと呼ばれてたりもします。いろんなドキュメントや資料で表記が揺れている状態ですね。なのでこのブログでも揺れていますがほぼイコールのものだと読み替えてください。この投稿ではではなるだけ『実行環境』という言い方をしていこうと思います。

なお、Sandboxって呼称については関数の中から色々と覗いてみるとその名残が見えたりします。

このLambda関数のインスタンスは1つあたり同時に1イベントしか処理しません。例えばAmazon API Gatewayをイベントソースとしている場合、APIへのリクエストが同時に100来た場合はそのバックエンドとなるLambda関数のインスタンス100個で処理されます。つまり、1インスタンス(=1コンテナ)で複数のイベントを同時に処理することはありません。ここ重要。ただし、必ずしも毎回100コンテナが1から作成されて起動されるわけではありません。ここで話に出てくるのがコールドスタート/ウォームスタートです。

コールドスタート/ウォームスタート

というわけでまずはこのあたりの話から始めていきましょう。まず、Lambda関数に対してリクエストが行われたときに何がおきるか以下に示しました。

  1. 実行環境の作成
  2. デプロイパッケージのロード
  3. デプロイパッケージの展開
  4. ランタイム起動・初期化
  5. 関数/メソッドの実行
  6. コンテナの破棄

ここでいうデプロイパッケージとはユーザが作成した関数コードならびにそれを実行するために依存するライブラリなどをまとめたZipファイルのことです。このデプロイパッケージはユーザのS3バケットに保存して参照させることも可能ですし、直接アップロードすることも可能です。なお、直接アップロードした場合はサービス側のS3バケットに暗号化されて保存されます。つまり、どちらのパターンであってもLambda関数のデプロイパッケージはS3上にあるということです。

まず、リクエストが届くとユーザが指定したランタイムの実行環境が作成されます。コンテナです。要はこれがインスタンス化だと思ってください。

そして次にそのLambda関数で実行する実際の関数コード、つまりデプロイパッケージがS3からダウンロードされます。

続いて、このデプロイパッケージはZipファイルとなっているのでこれを解凍して展開します。

そして、ランタイムの起動が行われ、初期化されます。グローバルスコープの処理もこのタイミングで実行されます。また、ここでの実行はDurationには含まれませんが一定の時間でタイムアウトします。

最後に、Lambda関数のエントリポイントとして指定されているハンドラメソッドが実行されます。

このプロセスをすべて実行するのが皆さんの大嫌いな『コールドスタート』です。なお、以前はVPCを利用する際は上記の1の前にENIの作成処理も行われていました。

一方、関数・メソッドの実行までの部分というのはLambda関数の設定を変更したり、実行するコードの内容が変更されない限りは毎回同じことを繰り返すことになります。それはさすがに無駄だよねってことで作成した実行環境をそのまま次回以降のリクエストでも使えるようにしているのが『ウォームスタート』です。

全体の実行時間のうち、『コールドスタート』に含まれる時間についてはDurationに含まれない部分が多いため、この『コールドスタート』そのものに厳密にどのくらいの時間がかかっているかはDurationを見てもわかりません。

頑張って計測するならば、End to endで計測して差分の値から推測するしかないわけです。また、End to endで計測するといってもイベントソースとしてAPI Gatewayが手前にいる場合はAPI Gatewayのエンドポイントに到達するまでの遅延も考慮する必要があります。X-Rayも有効です。

さて、この『ウォームスタート』が毎回行われると嬉しいんですが、そうすると作成した実行環境をずっと維持しておく必要があります。それはさすがにリソースの無駄になってしまうのである程度の期間実行されなくなったものについては破棄されます。なお、どのくらいの時間が経過すると破棄されるかについてはいろんな状況によって決められているため一定ではありません。

また、関数の終了時に関数の中から生成するなどした実行中のバックグラウンドプロセスがある場合はLambdaはそのプロセスをfreezeさせ、次回関数を呼び出した際に再開します。ただし、これは実行環境が再利用される場合だけですので、保証はされていません。

加えて、この場合バックグラウンドプロセスは残っていても処理は行われていない状態です。

このように、起動コスト効率化のために行われる『ウォームスタート』ですが、ウォームスタートできる実行環境がない場合はもちろん、あってもその数以上のリクエストを同時に処理する必要が発生した場合は新しく実行環境が作成・起動されます。

コントロールプレーン/データプレーン

さて、ではそんなLambda関数のインスタンスが実行される基盤について話をしていきたいと思います。

まず、AWS Lambdaのサービスは大きくコントロールプレーンとデータプレーンにわけられます。

コントロールプレーンとは関数管理のAPI(CreateFunction、UpdateFunctionCodeなど)を提供していたり、AWSのすべてのサービスとのインテグレーションを管理しています。あとはコンソールとかですね。

一方のデータプレーンはInvoke APIのコントロールです。Lambda関数が新たにInvokeされると実行環境を新たに割り当てる、もしくは既存の実行環境を選択するといったことを行っています。

関数の実行環境はmicroVM上で実行されています。

このmicroVMはAWSアカウントごとに専有で割り当てられ、同一アカウント内では関数をまたがって利用されます。つまり、複数の実行環境は1つのmicroVM上で実行できると言えます。一方、実行環境のほうは異なる関数間で共有されることはなく、microVMはAWSアカウントをまたいで共有されることもありません。

microVMはAWSが所有し管理する環境上に用意されており、EC2が利用されています。また、この環境はWorkerと呼ばれています。

アイソレーション

実行環境のアイソレーションについて簡単に説明します。まず、実行環境は前述のとおりコンテナベースで用意されているので、アイソレーションもコンテナテクノロジに準じています。また、他の環境に属するデータへのアクセスおよび変更は不可。

具体的には以下のようなものを使って実装されています。

  • cgroup: 実行環境ごとにCPU、メモリ、ディスク帯域、ネットワーク帯域へのリソースアクセスを制限
  • namespace: プロセスID、ユーザID、ネットワーク・インターフェースとその他のリソースをグルーピング。各実行環境は専有のnamespaceで実行される
  • seccomp-bpf: 実行環境内から利用されうるsyscallを制限
  • iptables/routing tables: 実行環境をお互い隔離
  • chroot: ファイルシステムへの特定範囲でのアクセスを提供

cgroupやnamespaceは有名だと思いますが、seccompってのはsecure computing modeのことでLinuxのsandboxを提供するセキュリティ機構です。seccomp-bpfはそのextensionでシステムコールのフィルタリングを可能にするものですね。bpfはBerkeley Packet Filterのことですね。最近、Linuxではいろんなところで使われつつあるものです。パケットフィルタって名前ですが、それだけではなくて実際にはトレーシングなんかでも使われてますね。chrootも昔からあるもので有名ですね。ルートディレクトリを別のディレクトリに変更するものでリソースの隔離とかに使われます。簡単に言うと、これを使うとそのプロセスはその範囲外のファイルにはアクセスできなくなります。このあたりはあまり詳細にふれるとマサカリ飛んできそうなのでこの辺にしておきます。

そして、各実行環境には以下の内容が含まれます。

  • 関数のコードおよび依存ライブラリなど。つまりデプロイパッケージの中身
  • Lambda layer。これも関数コードに準じる
  • 組み込みもしくはカスタムのランタイム
  • Amazon Linuxベースの最小限のユーザーランド

そして、AWS LambdaではあるAWSアカウントは複数の実行環境を1つのmicroVM上で実行できますが、AWSアカウント間で共有や再利用されることはありません。

さて、AWS LambdaではmicroVMを隔離するにあたって、2つの異なるメカニズムを利用しています。歴史的に利用してきたといってもいいかもしれません。それはEC2インスタンスとFirecrackerです。

FirecrackerはAWSが2018年に公開したOSSのハイパーバイザでコンテナやサーバーレスのワークロードに特化してデザインされています。

github.com

EC2モデルではmicroVMとEC2インスタンスは1:1でマッピングされており、Firecrackerの場合はEC2のベアメタルインスタンスで稼働しており、実行環境とmicroVMが1:1でマッピングされているもののmicroVM自体はベアメタルインスタンス上に複数存在することになります。そして、Firecrackerの場合はmicroVMを実行しているベアメタルインスタンス自体は複数AWSアカウントで共有されます。一方で、FirecrackerからユーザのワークロードまではDedicatedに割り当てられます。つまり各関数用のFirecrackerのプロセスはホストOS上ではそれぞれ別プロセスになります。

f:id:Keisuke69:20200928220439j:plain

KVMはハードウェア仮想化のためのプログラムで低レベルな仮想化を行っています。例えばメモリ管理やページングなどはKVMが担当しています。言ってみればハードウェアを抽象化してると言えます。

一方で、Firecrackerはデバイスのエミュレーション、パフォーマンスアイソレーションあたりを担当します。高速に動作してオーバーヘッドが少ないのが特徴で、サーバーレスのワークロードに最適化されています。

Firecrackerそのものの詳細についてはこちらに論文があるので興味ある人はどうぞ。

www.amazon.science

AWS Lambdaのコンポーネント

ここからはAWS Lambda内部の全体的なコンポーネント群に関して簡単にかいつまんで話します。

AWS Lambdaと一言で言っても内部的に大きくいくつかのコンポーネントにわかれているのは想像に難くないと思います。

代表的なものを以下にあげておきます。これらは基本的に先述のデータプレーンと呼ばれるほうに属します。一方、コントロールプレーンにはAPIとかLambdaのコンソールとかSAM CLIが含まれます。

  • Front End: いわゆるUI的な意味でのフロントエンドではない。同期呼び出し、非同期呼び出しの両方をオーケストレーションしたりする
  • Counting Service: 同時実行数の制限を行うための同時実行数のリージョン別のビューを提供
  • Worker Manager: 実行環境のアイドル状態とビジー状態をトラッキングし、呼び出しリクエストを利用可能な実行環境へとスケジューリング
  • Worker: 実行環境のこと。つまりコンテナでありLambda関数インスタンス(いろんな言い方あってややこしい…)。ユーザのコード実行のためのセキュアな環境を提供
  • Placement Service: 集積度を最大化するように効率的にWorkerを配置
  • Poller: イベントを消費し、処理するようにする
  • State Manager / Stream Tracker: Pollerとイベントもしくはストリームリソースを管理してスケーリングを処理
  • Leasing Service: 特定のイベントまたはストリーミングソースで作業するためにPollerを割り当てたり開放したり

Front Endはリクエストされた関数の実行環境を特定し、ペイロードをその実行環境へと渡す仕事もします。

なお、インターネットごしのロードバランサーへのトラフィックTLSで保護されていて、Lambdaサービス内のトラフィックは単一リージョンにおけるインターナルなVPCを通って処理されます。

最後のLeasing Serviceは定期的にPollerのAssignmentのヘルスチェックも行っています。UnhealthyなものやHeart beatが切れたものを検知して新しいPollerを割り当てたりもします。

なお、実際にはFront Endの手前にApplication Load Balancer(ALB)が存在しています。

さて、ここからは代表的な挙動をみていきます。

同期実行かつ初回呼び出し(コールドスタート)、もしくはスケーリング

呼び出しタイプがRequestResponseで実行される場合かつコールドスタートが発生する場合とスケールする場合ですね。ちなみにスケールする場合も当然コールドスタートが発生します。

このときの挙動としては、

  1. ALB経由でFront Endにリクエストが届く
  2. Front EndがWorker Managerに対してReserve Sandboxをリクエス
  3. Worker Managerは利用可能なWarm SandboxがなかったらPlacementに対してWorkerを要求し、アサインされたらWorkerを初期化
  4. WorkerがInvokeされる

こんな感じです。これが冒頭で説明したリクエストが発生したときに内部で何が起きているか、の全体的な部分です。

同期実行かつ再利用(ウォームスタート)

次は同じく同期実行ですが、ウォームスタートする場合ですね。

  1. ALB経由でFront Endに届く
  2. Front EndがWorker Managerに対してReserve Sandboxをリクエス
  3. ウォーム状態のWorkerがリターンされるのでそれをInvoke

非同期実行

今度は呼び出しタイプがEventの場合です。このときは非同期にLambda関数が実行されます。

  1. ALB経由でFront Endにリクエス
  2. Front EndがSQSにSendMessage
  3. PollerがそのQueueを見てRevceiveMessageおよびDeleteMessage
  4. PollerがFront Endに対してInvokeを実行(あとは同期実行と同じ)

ポイントは非同期実行の場合はリクエストを受け取ってからSQSを挟んで処理しているってことです。よくあるSQSを使ったキューイング処理と同じですね。なお、このキューはサービス側で管理されているものでありユーザには見えません。

スケールアップ

  1. State ManagerがSQSキュー上でWorkを探索
  2. State Managerが変更を読み取ってLeaing Service経由でPollerの割当を作成(Poller Assignment Dataに書き込み)。このとき関数の同時実行数の設定が大きい場合、StateManagerは新たにdedicatedなキューを作成する。小さい場合は既存のキューを使う。

なお、State Managerは複数のPollerを用意することで冗長性を確保しています。また、Poller自身はPoller Assignment DataからそのAssignmentを読み取ります。

エラーハンドリング

非同期実行時のエラーハンドリングについてです。とあるイベントソースを処理するためのPollerがいる状態です。

  1. Pollerの割り当てられた処理がストップしてしまう(何らかの理由で)
  2. Leasing Serviceがヘルスチェックしていて、期待した間隔でheartbeatがなくなったがAssignmentを見つける。
  3. そのAssignmentをUnhealty availbleにする
  4. 他のPollerがAssignmentを取って処理を開始する

リトライ

非同期の場合にLambda関数の実行がエラーになった場合のリトライの話です。

  1. 普通に非同期で実行(ALB → Front End → SQS → Poller...っていうやつ)
  2. 実行がエラーになったらPollerはリトライポリシーの設定に従いリトライを行う
  3. それでもダメだった場合はPollerがDLQのキューにsendMessege
  4. Pollerが元のキューからメッセージを削除

だいたいこんな感じです。非同期実行時の話として今回はイベントソースがストリーミングじゃない場合の話をしましたが、Kinesis Data StreamやDynamoDB Streamといったストリームをイベントソースとして使う場合はStream Trackerがストリームの状況(新規/更新/削除など)をトラッキングします。また、実行するPollerの数を決めるためにアクティブなシャード数をチェックします。新しいシャードが追加された場合はStream Trackerが同時実行数を計算する処理を走らせます。

その他

その他の細かいところをいくつか。

/tmpは他の実行環境をまたがってアクセスできません。

実行環境に割り当てる前にメモリを消去しています。消去というかゴシゴシするというか。これは異なるアカウントの関数との間でメモリの中身が共有されてしまうことを防止するためです。もちろん、同一実行環境上での同一関数の再呼び出し、つまりウォームスタートの場合はこの処理は行いません。

ちなみに、不安なのであれば関数の終了前にメモリ内の暗号化ならびにwipeの処理を実装することも可能なのでやってみてもいいかもですね。

ネットワーク

AWS Lambda のすべてのコンピューティングインフラストラクチャは、サービスが所有する VPC 内で実行されています。Lambda関数の呼び出しにはAPI経由しか用意されておらず、関数が実行される実行環境への直接的なネットワークアクセスはありません。

ユーザがVPC(カスタマー VPC)を利用するLambda 関数を設定すると、カスタマー VPC 内に Elastic Network Interfaces(ENI)が作成され、クロスアカウント接続が行われます。この状態でもLambda関数の実行自体は引き続きサービスのVPCで実行され、カスタマー VPC を介したネットワーク経由のリソースのみにアクセスする形になります。

関数用に作成されたすべてのネットワークインターフェースは、VPC サブネットに関連付けられ、IP アドレスを消費します。 ユーザはサブネット内の IP アドレス空間の管理、アカウントレベルのネットワークインターフェイスの制限、新規ネットワークインターフェイス作成の API レート制限に達する可能性、そして冒頭で言及した実行時にENIを作る時間コストがかかるという問題がありました。

これが以前のVPCを利用する際の状況でした。それが大きく改善されたのが昨年(2019年)の秋です。

それより少し遡ること2017年にAWSにおいて内部ネットワークのための内部サービスとしてAWS Hyperplaneというものがローンチされています。これはあくまでも内部サービスなのでユーザが利用することはありません。これを活用したAWSのサービスとしてはAmazon Elastic File System、AWS Managed NAT、AWS Network Load Balancer、AWS PrivateLinkといったものがあります。

そして、AWS Lambdaもこれをもとにした大きな改善を2019年にリリースしました。

VPC のネットワークインターフェースをHyperplane ENI にマッピングして、関数はそれを使用して接続するようになりました。

そして大きなポイントとして、以前と異なりネットワークインターフェースの作成はLambda 関数が作成されるか、VPC 設定が更新されるときに発生するようになりました。

関数が呼び出されると、実行環境は事前に作成されたネットワークインターフェイスを使用するだけよくなったのでコールドスタートでのネットワークインターフェイスの作成と接続で発生していた遅延が劇的に削減されたわけです。

また、このネットワークインターフェイスは実行環境全体で共有されるため、関数ごとに必要なネットワークインターフェイスの数も大幅に減りました。

ネットワーク・インターフェースはアカウント内の関数にまたがるすべての一意のセキュリティグループとサブネットの組み合わせごとに必要です。ただし、この組み合わせがアカウント内の複数の関数で共有されている場合、関数間で同じネットワークインターフェイスを再利用します。

関数のスケーリングは、ネットワークインターフェースの数に直接関係しなくなり、Hyperplane ENI は、多数の同時関数実行をサポートするようにスケールできるようになっています。

まとめ

という感じで、AWS Lambdaの中身について多少なりともお伝えできたと思いますがぶっちゃけ利用者はこんなの知る必要ないと個人的には思います。知る必要ないというか意識する必要ないというか。

説明しておいてなんですが、このあたりを追うのではなく、Lambdaを使って何を作るか、Lambdaをどう活用するかっていうアプリケーション的なところに意識を向けたほうがいいと思っています。

唯一、コールドスタートのときの話だけ頭に入れておくといいでしょう。

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を組み合わせるととてもいい体験が得られるような気がしています。こちらもまたどこかで試して紹介したいと思います。

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

はじめに

サーバーレスでサーバーサイドレンダリングの話です。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は無料で設定可能な機能ではないので。

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

サーバーレスアンチパターン今昔物語というイベントを開催しました

f:id:Keisuke69:20200710093627j:plain

昨日、2020年7月9日に表題のイベントを開催しました。こちらです。

connpass.com

参加者数とか配信環境とか

ありがたいことにイベント公開直後から多くの方にご登録いただいて最終的には1144人の方に登録していただき、結構な人数の方に視聴いただきました。

なお、マイナビBLITZ赤坂(赤坂BLITZ)というライブハウスのキャパが1Fスタンディングで1147人とのことなのでまあまあかな、と。

当初はこんなに集まると思ってなかったのでZoomでクローズドな感じでやろうと思ってたんですが、僕のプランの100人をあっという間に上回ってしまったためYouTube Liveでの配信となりました。

僕はライブ配信とか人生で初だったのでこの人数で失敗したらどうしよってのが一番心配でしたね。今までテストで2人でやったとかだけだったので。

昨今、各種イベントがオンラインで開催されるようになっていて皆さんOBSとか使って画面の作り込みをしているんですが省力をモットーとしているためそういったことはせずにZoomだけで配信しています。そもそも1人で喋って配信もするので凝ったことはできないというのがありました。

Zoomを使ったYouTube Liveの配信は簡単でいいですね。これは2000円/月のプランからできるようになります。安い!簡単に今からすぐ配信みたいなことも可能ですし、あらかじめスケジュールしての配信なんかも可能です。

一番言いたかったこと

今回はイベント概要にも記載したとおり過去にいろんな場で紹介してきたアンチパターンについて、プラットフォームの成長につれて今や解消されているというものがいくつかあるはずだからそれを紹介していきたいというものでした。一番大きなものはLambda+RDBMSの話だったりVPCの話だったわけで、これに関しては紹介できたと思います。

それ以外のアンチパターンについて見直してみたんですが、意外となかったんですね。ほとんどのアンチパターンアーキテクチャ的な考え方やプラットフォーム特性に起因することもあって機能のエンハンスにより解消されるといったものではありませんでした。なので多くのアンチパターンは今も有効であるという結論になりました。

逆に当日いただいた質問の回答でも言いましたが新たなエンハンスメントによって生まれる新しいアンチパターンというのもいくつかあります。このあたりはなかなか難しいところだなと改めて思いました。

あとはイベントの最後にも言いましたが、最大のアンチパターンは全てをサーバーレスにすることにこだわることだと考えています。これが実は今回一番言いたかったことですね。

当日の様子

さて、今回はいつもと違ってふわっと軽い感じで省力的にお届けするってのが試みでして、特に資料を用意することなくお届けするという形でした。とはいえ、今回はもともとRDS ProxyがGAになったことをきっかけに過去の自分のブログの内容を否定するというものだったのでそれを画面に出しながら喋ったので一応体裁は保てたかと思っています。なので今後どうしようかなーというのがあります。

そのブログはこちらです。

www.keisuke69.net

配信中は皆さんがTwitterで結構ツイートしてくれたのでいい感じに盛り上がった感も出ました。これに関しては@beajouneymanさんがまとめてくれました。ありがとうございます。

togetter.com

あと、配信前からSlidoで質問を募集していたのですが、僕個人に対する質問も含めて結構集まりました。当日の配信中も質問をいただき、こちらもありがとうございます。QAについても@hiroga_ccさんが以下にまとめてくれています。ありがとうございます。

hackmd.io

これらも含めて全体的なまとめをこれまた@beajouneymanさんがnoteに書いてくれています。ありがとうございます。

note.com

今後

今回そこそこ人数が集まったのとライブ配信も意外といけたのでこれに味をしめて秋くらいに別の切り口の勉強会もやってみたいと思いました。というわけで『俺のTech Festival 2020 Autumn(仮)』を開催しようと思います。すでに話を聞きたいいろんな人がいるのです。Vue.jsのあの人や、Reactのあの人、DBのあの人やソフトウェアアーキテクチャのあの人とか。なのでそんな人たちを集めたごった煮な勉強会やろうと思います。こちらはもう少し煮詰めてから公開します。

それとは別に『サーバーレスアンチパターン今昔物語』の第2回を7月20日の週に開催しようと思います。こちらも近々公開しますのでもう少しお待ちください。フォーマットは同じです。内容は今回も質問があったProvisioned Concurrencyとともに同時実行数まわりの話をすることを考えてます。

公開しました。

serverless-newworld.connpass.com

はい、というわけでこれからもどうぞよろしく。

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