AWSのラムダTypeScript関数をミドルウェアで簡素化する方法

サーバーのコードを書くのは面倒なものです。

 

実際のビジネスロジックに加えて、ヘッダー、コール、セキュリティ、バリデーションなどにも気を配る必要があります。

 

AWS lambdaでサーバーレスの世界に移行しても、この責任がなくなるわけではありません。

 

サーバーの場合、これらは通常、ミドルウェアのパターンで解決されます。

 

例えばAWS lambdaの場合は、複数の方法で対処します。

 

  • lambdaミドルウェアを手動で書く
  • lambdaミドルウェアを使う
  • middyミドルウェアを使う
  • APIゲートウェイの利用

 

手動でラムダミドルウェアを書く

 

簡単な例を使って説明してみましょう。

 

2つの数字の和を返すエンドポイントはこんな感じです。

 

import { APIGatewayProxyResult, APIGatewayEvent, Context } from "aws-lambda";

export async function add(event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> {
  const { a, b } = JSON.parse(event.body ?? "{}");
  const sum = a + b;
  return {
    statusCode: 200,
    body: JSON.stringify({ result: sum })
  }
}
 これでヘッダーやバリデーションの処理をしていないにもかかわらず、 すでに2つのミドルウェアを抽出することができました。
import { APIGatewayProxyResult, APIGatewayEvent, Context } from "aws-lambda";

type Summands = {
  a: number;
  b: number;
};

async function sum({ a, b }: Summands): Promise<{ result: number }> {
  return { result: a + b };
}

function inputParser<Result>(
  handler: ({ a, b }: Summands) => Promise<Result>
): (event: APIGatewayEvent) => Promise<Result> {
  return (event: APIGatewayEvent) => {
    const { a, b } = JSON.parse(event.body ?? "{}");
    return handler({ a, b });
  };
}

function jsonSerializer<Event>(
  handler: (event: Event) => Promise<object>
): (event: Event) => Promise<APIGatewayProxyResult> {
  return async (event: Event) => {
    return {
      statusCode: 200,
      body: JSON.stringify(await handler(event)),
    };
  };
}

export const add: (
  event: APIGatewayEvent,
  context: Context
) => Promise<APIGatewayProxyResult> = jsonSerializer(inputParser(sum));

 

この例を目の前にして、私たちは疑問を抱くかもしれません。

 

ここでいうミドルウェアとは何でしょうか?

 

それは最も単純な形で、ハンドラ関数を受け取り、拡張されたハンドラ関数を返す高次関数です。

 

例:wrapApiResponseは、オブジェクトを返すハンドラを受け取り、ApiGatewayProxyResultを返すハンドラに変換するような。

 

Pro

  • ミドルウェアの機能を完全にコントロールできる
  • 不要なコードの肥大化がない

Contra

  • ミドルウェアの作成に時間がかかる
  • カスタムソリューションは安全ではないかもしれない

 

ラムダミドルウェアの使用

 

上記の例を、ラムダミドルウェアを使うとどのようになるでしょうか?

 

import { APIGatewayProxyResult, APIGatewayEvent, Context } from "aws-lambda";
import { IsNumber } from "class-validator";
import { compose } from "@lambda-middleware/compose";
import { classValidator } from "@lambda-middleware/class-validator";
import { errorHandler } from "@lambda-middleware/http-error-handler";
import { jsonSerializer } from "@lambda-middleware/json-serializer";

class Summands {
  @IsNumber()
  a!: number;

  @IsNumber()
  b!: number;
}

async function sum({
  body: { a, b },
}: {
  body: Summands;
}): Promise<{ result: number }> {
  return { result: a + b };
}

export const add: (
  event: APIGatewayEvent,
  context: Context
) => Promise<APIGatewayProxyResult> = compose(
  errorHandler(),
  jsonSerializer(),
  classValidator({ bodyType: Summands })
)(sum);

 

jsonSerializerは、上記のカスタムメイドのソリューションが行うこととほぼ同じことを行いますが、

 

値がapplication/jsonのContent-Typeヘッダーも追加します。

 

classValiadorは、上記のinputParserをより精巧にしたものです。

 

class-validatorライブラリとSummandsクラスを利用して、入力を検証し、

 

aとbが実際に数字に設定されているかどうかを確認します。

 

JSON.parseはただ単にany typeを返すだけですが、このミドルウェアはevent.bodyを正しくタイプすることで、

 

検証済みのデータのみに依存することを保証します。

 

errorHandlerは、バリデーションが失敗したときにclassValidatorから投げられるバリデーションエラーをhttpレスポンスに変換するために必要です。

Pro

 

書くコードが減る
無料で機能を追加
型の安全性が高い

Contra

必要のないコードを含まざるを得ない場合がある(この場合は検証やエラー処理)。
他の人が書いたコードを理解しなければならないことがある

ミディミドルウェアの利用

AWSラムダのためのもう一つのミドルウェアフレームワークがmiddyです。同じ機能でも、以下のようになります。

import { APIGatewayEvent, APIGatewayProxyResult, Callback, Context } from 'aws-lambda'
import middy from "@middy/core";
import jsonBodyParser from "@middy/http-json-body-parser";
import httpErrorHandler from "@middy/http-error-handler";
import responseSerializer from "@middy/http-response-serializer";
import validator from "@middy/validator";

interface Summands {
  a: number;
  b: number;
}

const summandsSchema = {
  type: "object",
  properties: {
    body: {
      type: "object",
      properties: {
        a: { type: "number" },
        b: { type: "number" },
      },
      required: ["a", "b"],
    },
  },
};

async function sum({
  body: { a, b },
}: {
  body: Summands;
}): Promise<{ result: number }> {
  return { result: a + b };
}

export const add: (
  event: APIGatewayEvent,
  context: Context,
  callback: Callback
) => Promise<APIGatewayProxyResult> | void = middy(
  (sum as unknown) as (event: APIGatewayEvent) => Promise<APIGatewayProxyResult>
)
  .use(jsonBodyParser())
  .use(validator({ inputSchema: summandsSchema }))
  .use(responseSerializer({
    serializers: [{
      regex: /^application\/json$/,
      serializer: ({ body }) => JSON.stringify(body)
    }],
    default: 'application/json'
  }))
  .use(httpErrorHandler());

middyの場合、ミドルウェアは.useで追加されるカスタムフォーマットで定義されています。これは残念ながら、TypeScriptがミドルウェアから型付けを推測できないため、ハンドラの型付けを強制する必要があることを意味します。

JSONスキーマを使った検証は、非同期の検証が使えないなど、少し制限があります。しかし、パフォーマンスは格段に向上しています。

Pro

 

書くコードが少ない
無料で機能を追加
多くの既存ミドルウェアの中から選べる

Contra

タイピングのサポートが不十分
必要のないコードを入れざるを得ないことがある(ここでは検証とエラー処理)。
他の人が書いたコードを理解しなければならないことがある

APIゲートウェイの利用

これまで見てきた機能のほとんどは、AWS API Gatewayを使うことでも解決できます。ハンドラを見てみましょう。

 

interface Summands {
  a: number;
  b: number;
}

export async function sum({
  a,b
}: Summands): Promise<{ result: number }> {
  return { result: a + b };
}
 また、関連するサーバーレスの構成
functions:
  create:
    handler: handlers.add
    events:
      - http:
          path: /
          method: post
          request:
            schema:
              application/json:
                definitions: {}
                $schema: http://json-schema.org/draft-04/schema#
                type: object
                title: Summands
                required: ["a", "b"],
                properties:
                  a:
                    type: "number"
                  b:
                    type: "number"
            template:
              application/json: '#set($body = $util.parseJson($input.body)) {"a": $body.a, "b": $body.b}'

Lambdaのバージョン2では、JSONオブジェクトを直接返すことができ、statusCodeが定義されていない限り、ApiGatewayで文字列化されます。

定義のスキーマに基づいてAPI Gatewayで検証が行われ、テンプレートを使って関連情報が抽出されます。

Pro

  • 書くコードが少ない
  • リクエストはAPIゲートウェイで停止されるため、Lambdaの呼び出しにかかるコストを削減できる
  • ビジネスロジックがミドルウェアから強く分離される

Contra

  • コードベースのソリューションに比べて機能が制限される
  • 強力な型付けができない
  • コーディングだけでなく、Infrastructure-as-codeも理解しなければならない

どのミドルウェアを使うべきか?

このように、ミドルウェアはラムダコードを簡素化し、実際のビジネスロジックを可視化します。ミドルウェアは状況に応じて使い分ける必要があります。個人的には、AWSを最大限に活用するためのAPI Gateway構成か、AWSでサポートされていない機能のための関数ベースのミドルウェアのどちらかを使いたいと思っています。

藤沢瞭介(Ryosuke Hujisawa)
  • りょすけと申します。18歳からプログラミングをはじめ、今はフロントエンドでReactを書いたり、AIの勉強を頑張っています。off.tokyoでは、ハイテクやガジェット、それからプログラミングに関する情報まで、エンジニアに役立つ情報を日々発信しています!

TypeScript

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です