こんにちは。エンジニアの佐々木です。目黒川沿いを深夜2時にランニングしています。
みなさん、Serverless Frameworkを使ってますでしょうか。
Serverless Frameworkとはサーバーレスなアプリケーションを簡単に開発、デプロイするためのNode.js製のツールです。
AWS、Azure、GCP等の各クラウドに対応しており、ランタイムの言語もクラウドサービス側でサポートされているものであれば利用することができます。(この記事ではAWSを例に説明します。)
デプロイコマンドを実行すると、各クラウドサービスのリソース構築処理が実行されます。AWSを例に挙げると、デプロイコマンド実行時にCloudFormationのテンプレートが作成され、Lambdaおよびその他必要なリソースが作成されます。
Linc’wellでも、ちょっとしたバッチ処理やAPIを作りたいときにServerless Frameworkを利用してLambdaやAPI Gatewayを構築、運用しています。
この記事ではServerless Frameworkを使った安全なAPIの作り方のTipsを紹介します。
安全なAPI
記事のタイトルにも「安全」と書きましたが、以下を満たせていることを安全と呼んでいます。
静的に型付けされること
コンパイル時にエラーを出してくれることはもちろん、コードの可読性も増すのでバグを減らすことに繋がります。
Lambdaは小規模な処理であることが多いため、パパッと動的型付け言語で書くことも多いと思いますが、規模が少し大きくなるとやはり静的型付け言語で書かれている方が安心できます。
バリデーションが適切に設定されていること
Lambdaはパブリックな空間に置かれていることも多いので、バリデーションで弾くことができれば悪意を持ったリクエストによるセキュリティリスクを軽減できます。
また、リクエストの形式が異なる場合にバリデーションエラーであることを返してあげることで、API呼び出し元におけるバグ修正のスピードも早くなります。
ローカル環境にてエンドツーエンドで動作を検証できること
Serverless Frameworkは `sls invoke local` というコマンドでローカル環境にて実行できるのですが、あくまでも関数の実行しかできません。
APIの検証という面だとエンドツーエンドで検証できることが望ましいです。
適切にエラーハンドリングされていること
APIはエラー発生時にエラーをトラッキングできることはもちろん、どのようなエラーが起きたかわかるようなレスポンスをAPI呼び出し元に返すことが求められます。
リリース後に処理を追加する際にもできるだけエラーハンドリングをすることを忘れないような、そして手軽にエラーハンドリングできるような仕組みがあると良いです。
雛形がありメンテナンスされていること
Lambda関数は細々した処理がいくつも作られると思います。その度に一から環境構築や設定をすると、関数間で統一感が無くなり、各関数をメンテナンスすることが負担になります。
チーム内でLambda関数の雛形があるとベストですが、その雛形をメンテナンスしていくのはやはり負担になるので、OSSでしっかりとメンテナンスされているものがあると良いです。
安全なAPIを作成する
それでは、上で述べた安全なAPIを作成するために何をしていくかを説明します。
上では色々書きましたがやることはそれほど多くありません。
環境
特にバージョンによる影響はないと思いますが、以下を利用しています。
node: 14.15.5
serverless: 2.63.0
aws-nodejs-typescriptの利用
aws-nodejs-typescriptはServerless Frameworkが公式に出しているTypeScriptの雛形です。
実はこれを導入するだけで、「静的に型付けされること」「バリデーションが適切に設定されていること」「雛形がありメンテナンスされていること」の3つが満たせてしまいます。
このaws-nodejs-typescriptという雛形では、名前にあるとおりTypeScriptでLambda関数を記述することができます。静的型付け言語の中でも、バックエンドエンジニア、フロントエンドエンジニアの両方がとっつきやすいのはTypeScriptではないでしょうか。Lambdaなのでバックエンドエンジニアが触る機会が多いですが、フロントエンドエンジニアもAPIの実装を読んだり、簡単な修正をするために、TypeScriptで書かれていることは大きなメリットです。
Serverless Frameworkの雛形はテンプレートと呼ばれていて、以下のようなコマンドをプロジェクト開始時に実行すると、GitHub管理されているテンプレートのディレクトリがクローンされます。
$ sls create --template aws-nodejs-typescript
テンプレートはどれも最低限の実行の準備がされています。具体的には、aws-nodejs-typescriptでは以下コマンドを実行することでLambdaの関数部分がローカル実行されてレスポンスが返ってきます。
$ sls invoke local -f hello --path src/functions/hello/mock.json
aws-nodejs-typescriptのテンプレートは、Serverless Frameworkが出しているいくつものテンプレートの中の一つということもあり、毎日、毎週メンテナンスされるということはないですが、公式のテンプレートなので安心感はあります。少し前にはビルドツールがwebpackからesbuildに変更され、ビルド速度が大幅に向上しました。
また、このテンプレートではjson-schema-to-tsというライブラリを用いて、APIのバリデーションを設定しやすい仕組みになっています。このライブラリによって、APIにリクエストするJSONの各プロパティの型とLambda関数上で使われる各プロパティの型を一元管理することができ、メンテナンスしやすくなります。またこの一元管理している型と異なるものがリクエストされた場合は、バリデーションエラーを出力してくれるため、バリデーションエラーの仕組みを実装する手間が省けます。
その他、middyと呼ばれるLambda専用の軽量なミドルウェアエンジンがデフォルト搭載されています。middyには多くの公式もしくはサードパーティのミドルウェアが存在し、それらを組み合わせてLambdaに機能付与することができます。また、カスタムミドルウェアも作成することができ、痒いところに手が届かない場合にも活躍します。
例えば以下のようなものがあります。
・Lambdaのevent、responseを自動でロギング
・JSON形式のbodyをオブジェクト型にパース
・HTTPエラー発生時にエラーハンドリングし、エラーコード等付与されたレスポンス返信
なおTypeScriptは使いたいけどAPIを作りたいわけではないという場合は、上に書いた機能の中で不要なものもあります。以下のプラグインの利用でも十分だと思われます。
serverless-offlineを用いたローカル検証
Serverless Frameworkにはinvokeというコマンドがありますが、このコマンドはLambda関数部分の実行しかできません。稼働中のAPIの場合、invokeコマンドで関数部分のみの検証をするだけでは心許なく、やはり本番環境と同じようにエンドポイント、ヘッダー、リクエストボディを設定した状態でAPIを実行しローカル検証できた方が安心できます。
serverless-offlineはAPI GatewayとLambdaの動作をローカルで擬似的に実現するプラグインです。このプラグインは、Lambdaが記述されている言語によらず利用できます。
設定も非常に簡単でserverless.tsにてプラグインとして登録するだけです。
// serverless.ts
plugins: [
‘serverless-esbuild’,
‘serverless-offline’,
],
`sls offline` とコマンドを叩くだけで、ローカル実行環境が起動し、本番環境と同じようにエンドポイント、ヘッダー、リクエストボディを設定した状態でAPIを実行することが可能になります。
API Gatewayのいくつかの設定を自動でエミュレートできるようになっており、例えば本番環境でAPIキー認証をするような設定をserverless.tsに記述しておけば、serverless-offlineでも起動時にAPIキーが発行され、そのAPIキーを利用しないと認証エラーが発生するというように、API Gatewayの動きを本番環境に近い形で再現することができます。
トラッキングツールとmiddyを用いたエラーハンドリング
エラートラッキング用のツールと、middyのhttp-error-handlerと呼ばれるミドルウェアを用いて、メンテナビリティの高いエラートラッキングとエラーハンドリングの仕組みを構築していきます。エラートラッキングのツールはここではSentryを用いますが、他のツール、例えばRollbar等でも同様のことが可能です。
@sentry/serverlessは以下のように使うことができます。既存のLambda関数をwrapHandler関数でラップすることで、ラップした関数内でエラーが発生した場合に、Sentryにエラーの内容を送信してくれます。これにより、try...catch等でエラーを手動でトラッキングする機会も減少します。もちろん、ラップした関数内であっても手動でトラッキングすることは可能です。
import * as Sentry from '@sentry/serverless';
const hello: ValidatedEventAPIGatewayProxyEvent<typeof schema> = Sentry.AWSLambda.wrapHandler(
async (event) => {
...
},
);
次に、middyのhttp-error-handlerについてです。このミドルウェアはエラー発生時に自動でエラーレスポンスを返してくれるミドルウェアです。このミドルウェアを使わない場合は、適切にエラーハンドリングを行ってエラーレスポンスを返す処理を記述する必要があり、それを忘れるとエラー時であってもステータス200のレスポンスが返る可能性があります。
使い方も以下のような関数を作成し、Lambda関数をラップしてあげるだけです。これだけで一通りのエラーレスポンス処理ができてしまうため、エラーレスポンス処理の記述漏れや誤りを削減することができます。
import midi from ‘@middy/core’
import httpErrorHandler from ‘@middy/http-error-handler’;
import middyJsonBodyParser from ‘@middy/http-json-body-parser’
export const middyfy = (handler) => {
return
middy(handler)
.use(middyJsonBodyParser())
.use(httpErrorHandler(errorHandlerOptions))
};
おわりに
Serverless FrameworkとTypeScriptを使った安全なAPIの作り方について紹介しました。
Serverless Frameworkはサーバーレスアプリケーションを高速で開発、デプロイできるツールですが、高速であることと同じくらい(もしくはそれ以上)、メンテナンス性が高くセキュアで実行時に確かな動作をすることが大切です。今後もより良いサーバーレスアプリケーションを構築できるようにServerless Frameworkの動向に注視していきたいと思います。
株式会社Linc'well (リンクウェル)では一緒に働く仲間を募集しています