AWSでのサーバーレスWebアプリケーションの設計
2016年5月9日
0 分で読めます編集者注
このブログ記事は、もともとfugue.coに掲載されていました。Fugueは2022年にSnykの一員となり、Snyk IaCの重要な構成要素となっています。
FugueのWebチームは小規模ながら活気にあふれた少数派です。JavaScript、60フレーム/秒、そしてシンプルなDevOpsを支持しています。流行や見た目の華やかさより、実質と洗練を重視した実験や新しいコンピューティングのアプローチを好みます。しばらく前から、SNSトピックやvotebotsとともにAWS Lambdaを使ってきましたが、大規模なものはまだ試していませんでした。今までは。Serverless frameworkが、必要な後押しをしてくれました。目標は、LambdaとAPI Gatewayで構築したAPIを通じて、業務に役立つアプリケーションを実現すること。その過程でEC2インスタンスには一切負荷をかけません。
AWS Lambdaについて簡単に説明するため、少しだけ話を戻しましょう。IBM OpenWhisk、Google Cloud Functions、Azure Functionsと同様に、Lambdaは「Amazon S3へのファイルアップロード、イベントストリーム、API Gatewayへのリクエストなど、特定のイベントに応じてコードを実行する」サービスです。Fintan Ryanによる概要(引用を一部編集)はこちらをご覧ください。リクエストに応じてコードが自動的にスケーリングされます。こうした直接的なコンピューティングは、急速な拡大の一途をたどっています。
アプリケーション
私たちが構築したフロントエンドWebアプリケーションはシンプルです。ユーザーがログインすると、ソフトウェアのビルドに関する情報と、安全なダウンロードURLが表示されます。アプリケーションはS3上にあります。このエクスペリエンスを実現するため、REST APIを構築しています。APIのアーキテクチャをご覧ください。

API Gatewayの各エンドポイントがLambda関数を呼び出しているのがわかります。User Service LambdaはDynamoDBにデータを保存します。Content Service Lambdaは配信サービスのAPIに接続してコンテンツを取得し、表示を整えてからフロントエンドアプリケーションに返します。Authorization Lambdaはユーザーセッションのトークンを検証し、保護されたエンドポイントへのアクセスを許可します。
これらのサービスはAWSで手動設定することもできますが、私たちはデプロイツールとして、またプロジェクトに構造を持たせるためにServerless frameworkを使用しています。
Serverlessの紹介
Serverlessは、LambdaとAPI Gatewayを使ってアプリケーションを構築するためのフレームワークです。CloudFormationテンプレートを使った他のAWSリソースの管理にも対応しています。充実したドキュメントを備えたNode.js CLIとして提供されています。
Serverlessが強力なのは、コードとAWSの設定をどちらも管理できるからです。プロジェクトには専用の構成があり、Lambda関数の設定にはJSONファイルを使います。関数の設定ファイルには、API Gatewayのエンドポイントや、関数を呼び出すその他のイベントの定義も含まれます。プロジェクトをデプロイするには、AWSアカウントにServerlessユーザーを作成し、使用するサービスの権限を付与します。その後、CLIからデプロイすると、使用するAWSサービスが設定され、Lambda関数のコードが公開されます。
課題の解決
Serverlessは、フレームワークを使わずにAWSでこの種のプロジェクトをセットアップしようとした際に直面した、いくつかの課題を効果的に解決してくれます。
1)Lambda関数は独立しており、コードを共有できない。
複数の関数を使うプロジェクトでは、たとえutilsライブラリだけでも、関数間でコードを共有したくなるでしょう。Lambda関数は隔離されたコンテナにデプロイされるため、共通ライブラリのディレクトリに依存できません。Serverlessでは、デプロイパッケージに含めるディレクトリ階層を指定できます。そのため、上位階層のフォルダにあるコードを関数に含めることができ、他の関数でも利用できます。対象ファイルはデプロイパッケージとともにzip化されます。
2)Lambda関数は環境変数に対応していない。
APIキーなどの秘密情報をコードに直接記述して、GitHubリポジトリの利用者に見られることなく、Lambda関数に安全に組み込むにはどうすればよいでしょうか。Serverlessでは、環境変数の定義をgitignore対象の_metaフォルダ内のJSONファイルに保存し、デプロイ時に値を関数へ埋め込みます。チームで作業している場合は、Meta Sync pluginを使って、S3経由で環境変数の定義を安全に同期できます。
3)API GatewayとLambdaの連携は難しい。
API Gatewayの各エンドポイントには、呼び出すLambda関数がリクエストのどの情報を利用できるかを指定するリクエストテンプレートが必要です。Lambda関数では、リクエストパスやメソッド、URLパラメーター、クエリパラメーター、場合によってはカスタムヘッダーやPOSTリクエストの本文も必要になるでしょう。リクエストテンプレートがなければ、これらの情報は利用できません。同様に、API GatewayはデフォルトではLambda関数のレスポンスを解釈しません。そのため、関数がエラーを返しても、HTTPエラーコードへの自動マッピングは行われません。Lambda関数のエラーメッセージ文字列を適切なレスポンスコードに対応づけるには、レスポンステンプレートを追加する必要があります。Serverlessではリクエストテンプレートとレスポンステンプレートの継承に対応しているため、すべてのエンドポイントに共通するテンプレートを定義し、必要に応じて個々のエンドポイントで上書きできます。
プロジェクト構成
以下は、アプリケーションのServerlessプロジェクトのディレクトリ構成です。
s-project.json
プロジェクト名、説明、使用するカスタムプラグインが記載されています。
s-resources-cf.json
LambdaとAPI Gateway以外にこのプロジェクトで使用するリソースのCloudFormationテンプレートです。今回は、DynamoDBテーブルと、Lambda関数がDynamoDBの読み書きを行えるようにするIAMロールおよびポリシーが含まれています。
_meta
プロジェクトの環境変数を記述したJSONファイルが格納されるフォルダです。デプロイステージやリージョンごとに異なる値を設定することもできます。このフォルダはgitignore対象です。
functions
各Lambda関数のサブフォルダが格納されるフォルダです。Serverlessはここでのフォルダ構成に依存しないため、好きなように関数を入れ子にできます。関数フォルダには、関数のコードを記述したファイル(この場合はhandler.js)と、関数の設定や呼び出し元のエンドポイントを記述したs-function.jsonファイルが必要です。また、後ほど説明するテストオブジェクトを格納するevent.jsonも含められます。前述のとおり、関数フォルダ(user、contentなど)から利用できるlibフォルダを、各関数フォルダと同じ階層に置いています。
Serverlessプロジェクトの構成要素を詳しく見る
Serverlessプロジェクトのアーキテクチャを構成する重要な要素は3つあります。Lambda関数、API Gateway REST API、そしてDynamoDBテーブルやIAMロールなどのその他のAWSリソースです。これらの要素をどのように利用しているか、詳しく見ていきましょう。
Lambda関数
後ほど説明するauthorizer Lambda関数を除き、コードはcontentとuserの2つの関数に分けています。なぜでしょうか。
Lambdaのコードは、技術的には自由に分割できます。すべてのエンドポイントから1つの関数を呼び出し、その関数でリクエストを解析して応答を決めることもできます。また、プロジェクト内のエンドポイントやイベントごとに関数を作成することも可能です。私たちはいくつかの要素を考慮し、コードをマイクロサービス関数ごとにまとめる折衷的な方法を採用しました。
コードの整理
関連性の高いエンドポイントを1つの関数にまとめるのは合理的です。ユーザー関連のコードはすべて同じデータベーステーブルを扱い、同じ外部ライブラリを使います。関連するコードを1つの関数にまとめることで、コードの変更をすべてのエンドポイントにすぐ反映できます。
パフォーマンス
Lambdaのドキュメントによると、関数は小さいほどパフォーマンスが高くなります。最近呼び出されていない関数は、レイテンシが大幅に高くなります。私たちの経験では、約5分間呼び出されていない場合が該当します。最初の呼び出しの後、レイテンシは大きく低下します。初回のレイテンシ、つまり「コールドスタート時間」は関数のサイズに直接関係しています。関数が小さいほど、コールドスタート時間は短くなります。(関数に割り当てるメモリを増やすとCPUも比例して増えるため、コールドスタート時間を短縮できます。)
このコールドスタートの遅延は、Lambdaを非同期で使う場合は問題になりませんが、トラフィックが限られるAPIでは問題になります。APIの応答が遅いと、アプリケーションのユーザーエクスペリエンスが損なわれるからです。
関連する機能を大きなLambda関数にまとめることで、関数をある程度ウォームアップした状態に保てます。たとえば、ユーザーがアカウントにログインしてからプロフィールを表示する場合、API呼び出しは2回発生する可能性があります。関数が最近呼び出されていなければ最初の呼び出しは遅くなるかもしれませんが、両方のエンドポイントが同じ関数を呼び出すなら、2回目は確実に速くなります。
注:5分ごとにエンドポイントへpingを送るだけで関数をウォームアップしておく方法も検討しました。確実に機能するでしょうが、現時点では実装する必要性を感じていません。
関数ハンドラー
s-function.jsonファイルで関数ハンドラーを定義します。今回は、handler.js内のhandlerという関数です。Lambda関数が呼び出されたときに実行されます。content関数のhandler.jsは次のようになっています。
ご覧のとおり、このファイルは関数のルーターとして機能しています。重要なコードはすべて/lib/download.jsに切り出されており、Lambda関数の一部であることを意識しません。/lib/download.jsは特殊なLambda依存関係のない単なるJavaScriptファイルなので、コードの単体テストも簡単になります。
API Gateway REST API
エンドポイントはs-function.jsonファイルで設定します。エンドポイント設定の例を見てみましょう。
このエンドポイントには、GET /downloadsでアクセスできます。
リクエストテンプレートとレスポンステンプレート
上記のエンドポイント設定にある$${requestTemplate}と$${responseTemplate}をご覧ください。これは、downloadsエンドポイントが、プロジェクトのグローバルテンプレートファイルで定義したrequestTemplateとresponseTemplateを継承することを示しています。
リクエストテンプレートとレスポンステンプレートはJSONで表現され、JSONPath式を使用します。Apache Velocity Template Language(VTL)を使ってJSONPath式を操作できます。構文は少し複雑です。
Serverlessは最近、YAML形式のリクエストテンプレートとレスポンステンプレートに対応しましたが、現在はJSON形式を使っています。リクエストテンプレートをご覧ください。
ご覧のとおり、JSONの中にエスケープされたJSONが含まれているため、少し奇妙に見えます。しかし、Lambda関数がリクエストに関する必要な情報をすべて取得できるイベントオブジェクトを生成します。
bodyには、リクエストのJSON本文を解析した内容が含まれます
pathは、リクエストされたエンドポイントのパスです
methodは、リクエストで使用されたHTTPメソッドです
headersには、リクエストのすべてのHTTPヘッダーが含まれます
paramsには、リクエストのパスパラメーターが含まれます
queryには、リクエストのクエリパラメーターが含まれます
authorizedUserは、エンドポイントで認証が必要な場合に、認証済みユーザーのユーザー名を示します
こちらがレスポンステンプレートです。Lambda関数が成功すると、結果の文字列はJSONとしてそのままユーザーに渡されます(response200テンプレートフラグメントを使用)。関数が失敗した場合は、正規表現で結果の文字列を照合します。エラーメッセージに基づき、異なるHTTPエラーコードを返します。
認証の処理
アプリケーションの権限管理には、API Gatewayの2つの機能、APIキーとカスタムオーソライザー関数を活用しています。どちらの機能も、認可に関する処理を中核となるLambda関数から切り離し、アプリケーションコードをシンプルにできます。
APIキー
API Gatewayでは、APIキーを作成して個々のエンドポイントに割り当てることができます。ユーザー管理など、クライアント側からアクセスさせたくない機密性の高いエンドポイントを保護するために、この機能を使っています。API Gatewayは、選択したエンドポイントへのリクエストに含まれるx-api-keyヘッダーを確認し、その値が作成したAPIキーと一致するかを検証します。ヘッダーがない場合、または値が正しくない場合、リクエストは403エラーを返し、Lambda関数は呼び出されません。
カスタムオーソライザー
API Gatewayには最近、カスタム認可機能が追加されました。これにより、Lambda関数でエンドポイントへのアクセスを制御できます。リクエストが届くと、指定したカスタムリクエストヘッダーの認可トークンを使ってカスタムオーソライザー関数が呼び出されます。カスタムオーソライザーは認可トークン(ここではJSON Web Token)を検証し、リクエストを許可するIAMポリシーを返します。ポリシーがリクエストに対して無効な場合、または認可に失敗した場合、API Gatewayは403を返します。ポリシーが有効な場合は、そのエンドポイントに割り当てられたLambda関数が呼び出されます。
カスタムオーソライザーが返すIAMポリシーの例を紹介します。
このWebアプリには細かな権限設定がないため、カスタムオーソライザーを使うAPI内のすべてのエンドポイントへのアクセスを許可しています。生成されたIAMポリシーは、ポリシーの生成に使用した認可トークンとともにAPI Gatewayにキャッシュされます。以降、同じ認可トークンをヘッダーに含む保護対象エンドポイントへのリクエストには、指定したTTLの間、カスタムオーソライザーを経由せずにこのポリシーが自動的に適用されます。
Serverlessプロジェクトのワークフロー
環境
最低限、プロジェクトには開発、テスト、本番の環境を分けて用意する必要があります。
Serverlessには「ステージ」という概念があります。ステージの実装方法はAWSサービスごとに異なりますが、組み合わせることで環境として機能します。
Lambda:Lambda関数のコードを変更するたびに、新しいバージョンが自動的に作成されます。Lambda関数ではバージョンにエイリアスを設定でき、Serverlessはステージごとに異なるバージョンを指すエイリアスを使用します。たとえば、devステージを公開してLambda関数のバージョン1になると、Serverlessはバージョン1を指すdevエイリアスも作成します。次に関数のprodステージを公開すると、本番用コードはバージョン2になりますが、devエイリアスは引き続きバージョン1を指します。
API Gateway:API Gatewayの1つのAPIに複数のステージを設定できます。APIエンドポイントの各ステージは、異なるLambdaエイリアスを呼び出せます。そのため、devのAPI Gatewayステージでは、現在どのバージョンを指しているかにかかわらず、Lambda関数のdevエイリアスを使用できます。ステージ名はエンドポイントのパスの一部になります。
DynamoDBテーブルやS3バケットなど、その他のAWSサービスのリソースは、ステージごとに個別に存在します。
テスト
各Lambda関数のフォルダには、テスト用のモックイベントオブジェクトをevent.jsonファイルに格納できます。
serverless function runを実行すると、event.jsonをイベントとして関数が実行されます。関数のエンドツーエンドテストに便利です。
テスト用にAPI GatewayをローカルでシミュレートするServerlessプラグインはいくつかあり、OfflineやServeなどが使えます。API Gatewayには現在も機能が追加されているため、これらのプラグインでは使いたい機能が常にサポートされているとは限りませんでした。そのため、API Gatewayのテストの大半はAWSコンソールで行うことにしました。
JavaScriptのlibファイルのユニットテストにはJasmineを使っています。
デプロイ
プロジェクトを利用できる状態にするには、関数、エンドポイント、プロジェクトリソース(前述のs-resources.jsonに記載したAWSリソース)の3つをデプロイする必要があります。以下のCLIコマンドを使えば、それぞれ個別にデプロイできます。
または、serverless dash deployコマンドで、便利な対話型デプロイダッシュボードを使うこともできます。
どちらの方法でも細かくデプロイできるため、ほかの部分に変更を加えずに、1つの関数やエンドポイントだけを更新できます。
対話型ダッシュボードはローカルでの開発やテストにとても便利ですが、継続的インテグレーションツールでは最初のコマンド群を使っています。CLIはデフォルトで不足しているオプションを尋ねますが、環境変数CIをtrueに設定すると、この対話機能を無効にできます。継続的インテグレーションとデプロイにはTravis CIを使っているため、Travisでユニットテストを実行した後、CLIをCIモードで実行してコードをAWSにデプロイしています。
詳しくはServerless CLIリファレンスをご覧ください。
最後に
数カ月前、CEOのJoshがAWS Lambdaと進化を続けるクラウドについて考えを共有しました。彼は次のように述べています。
Lambdaは、従来のPaaSとは異なり、アプリケーションアーキテクチャに関して明確な見解を持つサービスです。従来型のコンピューターやコンピュータークラスターを装うことはありません。Lambdaは本質的にイベント駆動型で、関数レベルでステートレスであることを求めます。そのため、アプリケーションの構成方法を少し違った視点で考える必要があります。コンピューティングインスタンスやコンテナと比べて、ほぼ無限のスケーラビリティと非常に低いコストを得られる一方、学習に時間をかける必要があります。しかし、試行錯誤や探求を重ねるうちに、新しいパターンを生み出せるかもしれません。
そのとおりです。時間をかけて学び、実践する価値は十分にあります。ここで紹介した例が参考になれば幸いです。今後、Serverlessのようなフレームワークがさらに登場するでしょう。まず試してみて、しっかりしたWebアプリを構築するのに、Serverlessはよい選択肢です。
開発者のために設計されたIaCセキュリティ
Snykは、統合されたポリシー・アズ・コードエンジンにより、SDLCからクラウドでの実行時までInfrastructure as Codeを保護します。すべてのチームが安全に開発、デプロイ、運用できるよう支援します。



