Lambda SDP認定企業が、モダンなLambdaハンドラーの実装方法を解説😎middyを使いこなしてLambdaハンドラーを拡張しやすくしましょう

Lambda SDP認定企業が、モダンなLambdaハンドラーの実装方法を解説😎middyを使いこなしてLambdaハンドラーを拡張しやすくしましょう

こんにちは!

AWS Lambda を実装する際に、企業様によって独自のレギュレーションを開発チーム内で定めているケースが多々見受けられますが、私たちとしてはそのレギュレーションに AWS が公式でもアナウンスしている Middy を使用した実装を取り入れて欲しいと考えています。Middy を取り入れることで従来の Lambda 実装をより簡潔にし、ビジネスロジックの見通しを良くすることができます。

本記事では Middy の概念から実装方法等々を解説します。Lambda 実装者は必見です。

想定する読者

  • AWS Lambda を実装する全てのヒト
  • AWS Lambda 開発をよく効率的に保守性高くしたいと考えるヒト
  • Middyの導入を検討しているヒト

はじめに

本記事は NodeJS 向けに解説します

NodeJS 向けに解説しますが、Python 等々の他言語でも応用できる内容となっておりますのでご一読ください。NodeJS 以外の言語でも、Middy に類似しているモジュールを使用すれば同様のことを実現できます。

Middy の概念と目的

Middy は、AWS がスポンサーを行う NPM モジュールで、AWS Lambda のミドルウェア実装を簡略化する機能を提供しています。SaaS のようなものではなく、パブリックに公開されている NPM モジュールです。

Middy を使用する目的は以下になります。

  • AWS Lambda の実装をシンプルにする
  • AWS Lambda の実装にミドルウェアを適用する

Middyは NodeJS の Express のミドルウェアと非常に似ていて、Express でミドルウェアを実装したことがあるヒトなら理解は早いと感じます。以下は簡単な Middy の実装例です。(まだ理解できていなくてOKです、参考までに)

import middy from '@middy/core' // esm Node v14+
import jsonBodyParser from '@middy/http-json-body-parser'
import httpErrorHandler from '@middy/http-error-handler'
import validator from '@middy/validator'
import { transpileSchema } from '@middy/validator/transpile'

const lambdaHandler = async (event, context) => {
  const { creditCardNumber, expiryMonth, expiryYear, cvc, nameOnCard, amount } =
    event.body
  const response = { result: 'success', message: 'payment processed correctly' }
  return { statusCode: 200, body: JSON.stringify(response) }
}

const schema = {
  type: 'object',
  properties: {
    body: {
      type: 'object',
      properties: {
        creditCardNumber: {
          type: 'string',
          minLength: 12,
          maxLength: 19,
          pattern: '\\d+'
        },
        expiryMonth: { type: 'integer', minimum: 1, maximum: 12 },
        expiryYear: { type: 'integer', minimum: 2017, maximum: 2027 },
        cvc: { type: 'string', minLength: 3, maxLength: 4, pattern: '\\d+' },
        nameOnCard: { type: 'string' },
        amount: { type: 'number' }
      },
      required: ['creditCardNumber'] // Insert here all required event properties
    }
  }
}

export const handler = middy()
  .use(jsonBodyParser()) // parses the request body when it's a JSON and converts it to an object
  .use(validator({ eventSchema: transpileSchema(schema) })) // validates the input
  .use(httpErrorHandler()) // handles common http errors and returns proper responses
  .handler(lambdaHandler)

Middy を導入する最もな利点は、ビジネスロジックとそれ以外の処理を疎結合にできる点にあります。Lambda の実装の際に、認可処理や X-RAY へのデータ連携、その他サードパーティへの通信を実装するときに、従来であればハンドラー内にそれらの処理を記述したと思います。それらの処理は service 層や utils 層を用いて開発者の工夫によって実行を簡略化することは可能ですが、各 Handler の実装者が意識して呼び出しをハンドリングする必要があるため、ビジネスロジックと分離出来ているとは言えません。

Middy を使用すると、それらをハンドラー外(ミドルウェア)に逃すことができるため、ビジネスロジックとそれ以外を分離することが可能となります。

Middy の仕組み

Middy を使用すると、ハンドラーの実行前後に処理をフックできます。作成する各ミドルウェアには before と afterを定義し、ハンドラーの処理の前後で任意の処理をフックします。以下は簡単な Middy のミドルウェアの実装例です。

const cacheMiddleware = (options) => {
  let cacheKey

  const cacheMiddlewareBefore = async (request) => {
    cacheKey = options.calculateCacheId(request.event)
    if (options.storage.hasOwnProperty(cacheKey)) {
      // exits early and returns the value from the cache if it's already there
      return options.storage[cacheKey]
    }
  }

  const cacheMiddlewareAfter = async (request) => {
    // stores the calculated response in the cache
    options.storage[cacheKey] = request.response
  }

  return {
    before: cacheMiddlewareBefore,
    after: cacheMiddlewareAfter
  }
}

上記は、before の段階で Event 情報からキャッシュキーを生成し、すでにキャッシュが存在する場合はキャッシュを即座にリターンし処理を終了させています。(ハンドラーを実行せずにキャッシュ情報をリターンして処理を終了)

上記を応用することで、例えば以下のようなサードパーティ製品への通知処理も実現できます。

サードパーティへのインシデント通知の実装例

私たちは普段 Bugsnag を使用してインシデント通知を実装することが多々あるので、Bugsnag の実装例を紹介します。(Bugsnag の解説は本記事のテーマとは外れますので割愛いたします)

import middy from '@middy/core';
import { AppSyncAuthorizerEvent, AppSyncResolverEvent, Context } from 'aws-lambda';
import Bugsnag from '@bugsnag/js';
import logger from 'utils/logger';

const STAGE = process.env.STAGE as string;
const useBugsnag = STAGE === 'stg' || STAGE === 'prd';

export default (): middy.MiddlewareObj<AppSyncAuthorizerEvent | AppSyncResolverEvent<unknown>, unknown, Error, Context> => {
  return {
    before: (e): void => {
      if (useBugsnag) {
        Bugsnag.start({
          apiKey: process.env.BUGSNAG_API_KEY as string,
          releaseStage: process.env.STAGE as string,
          sendCode: false,
          trackInlineScripts: false,
          context: e.context?.functionName,
          metadata: {
            region: process.env.REGION as string,
            function_name: e.context?.functionName,
            function_version: e.context?.functionVersion,
          },
        });
      }
    },
    onError: (request): void => {
      logger.error('request : ', {
        request: { ...request },
      });
      if (useBugsnag) {
        Bugsnag.notify(request);
      }
    },
  };
};

before とは別に onError というプロパティが存在します、これは例外処理を検知した際に呼び出しされるプロパティです。つまり、Middy を使用すると before, after, onError の3つのフックをミドルウェアで実現することが可能となります。

これで Middy の基本的な理解はできたと思いますので、次は Middy に複数のミドルウェアを適用した場合の挙動について解説します。

ミドルウェアの実行順序

Middy には複数のミドルウェアを設定することが可能です。その際の実行順序についてソースコードを交えて解説します。まず、先ほど紹介した以下のソースコード(抜粋)を見てみましょう。

export const handler = middy()
  .use(jsonBodyParser()) // parses the request body when it's a JSON and converts it to an object
  .use(validator({ eventSchema: transpileSchema(schema) })) // validates the input
  .use(httpErrorHandler()) // handles common http errors and returns proper responses
  .handler(lambdaHandler)

3つのミドルウェアが適用されていますが、実行順序は以下のようになります。

  1. jsonBodyParser (before)
  2. validator (before)
  3. httpErrorHandler (before)
  4. handler
  5. jsonBodyParser (after)
  6. validator (after)
  7. httpErrorHandler (after)

Middy ではオニオンアーキテクチャーを採用しており、ハンドラーを境界線にして before、after で処理がフックされます。以下の図を見るとわかりやすいと思います。

Middy middleware engine diagram

Middyの応用

Middy は AWS 公式のモジュールということもあり、AWS 製品との親和性が非常に高いです。例えば APIGateway のBody のパーサーに特化したミドルウェア、リクエスト Body のバリデーションに特化したミドルウェアなどが存在します。

詳しくはこちらをご覧ください

まとめ

本記事では Lambda 実装では必須なMiddyについて解説を行いました。Lambda の実装を行う際には必須なモジュールとなりますので、是非導入を検討してみてください。

AWSサーバーレスフロントエンドの開発はお気軽にお問い合わせください。