CDKを使って、一つのLambda関数でAPI設計してみた

2022.02.14
CDKやAWS SDKの勉強として、DynamoDBに対してCRUD操作を行うAPIを作ってみることにしました。AWSの構成としては、API GatewayとLambda、DynamoDBを使いました。API GatewayとLambdaで複数のAPIを作成する場合、APIの数だけLambda関数を用意する場合が多いようです。しかし、自分は一つのLambda関数でAPIを作成しました。逆にそういう構成は少ないと思いブログにしてみることにしました。

複数のAPIを作る際のLambda関数の構成

今回は一つのLambda関数を用いて複数のAPIを作りました。しかし、APIの数だけLambda関数を作成して複数のAPIを作ることもできます。それぞれの構成のメリットとデメリットについて紹介します。

今回の一つのLambda関数だけを用いた構成だと、Lambda関数が一つであるためデプロイの時間を短くすることができるというメリットがあります。一方で、複数のAPIを作るために一つのLambda関数内で、httpメソッド(GET,POST,PUT,DELETE)ごとに条件分岐を行い、さらにURLごとに条件分岐する必要があります。またURLの条件分岐では正規表現が出てきます。他の人が見た時に、ぱっと見ではわかりにくいコードになってしまうというデメリットがあります。

それに対して、APIの数だけLambda関数を作成する構成だと、何度も条件分岐が出てくることは無く、正規表現も出てこないため分かりやすいコードに出来るというメリットがあります。一方で、Lambda関数が多いとその分デプロイに時間がかかるというデメリットがあります。

両方の構成のメリットとデメリットを比較すると多くの場合、APIの数だけLambda関数を作成する構成の方が良いのではと思います。しかし、今回はお勉強のために一つのLambda関数だけを用いた構成を試してみることにしました。

ググって見つけたものですが、以下のリポジトリでは、APIの数だけLambda関数を作成しています。また自分が今回行ったのと同じように、DynamoDBに対してCRUD操作を行うAPIとなっています。

AWSの構成

AWS上での環境構築には、CDK(v2)を使いました。CDKでは、AWS環境の定義をプログラミング言語を使って行うことができます。CDKで利用できる言語には、JavaScript、TypeScript、Python、Java、C#がありますが、自分はTypeScriptを使いました。 CDKのコーディングには、以下のページを参考にしました。しかし、リンク先のページではCDK v1が利用されていたため、少し変更を加えてCDK v2を使いました。

リンク先のページの画像の転載ですが、AWSの構成図を以下に示します。

API Gatewayを通してLambdaを呼び出して、Lamnbdaの側では各パスに合わせて処理を実行します。またLambda上ではAWS SDK(v3)を利用して、DynamoDBに対してCRUD操作を行います。CURDとは、作成(Create)・参照(Read)・更新(Update)・削除(Delete)の四つの基本機能のことを言います。

CDK v1からv2へのマイグレーション

v1からv2へのマイグレーションに関しては以下のブログが参考にしました。

REST API

API Gatewayでは、REST APIを利用します。URLの切り方によって、どのリソースかを指定し、HTTPメソッドによって、CRUDのどの操作かを指定します。実際に作成するAPIのパスとしては、以下のようなものがあります。

Lambda関数

今回書いたLambda関数のコードです。コードが長いので、所々省略して貼り付けました。 最初にhandler関数内で、httpリクエストの種類に応じてswitchで条件分岐を行なっています。その後、getReceiver関数などを作成し、その関数内で再びswitch文を使ってURLに応じて分岐を行います。それ以降はDynamoDBに対してCRUD操作を行っています。

import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import {
  DynamoDBClient,
  ScanCommand,
  ScanCommandInput,
  ...
} from "@aws-sdk/client-dynamodb";

const dynamoDBClient = new DynamoDBClient({});
const TABLE_NAME = "items";

export const handler: APIGatewayProxyHandlerV2 = async (
  event: any = {}
): Promise => {
  const requestedMethod = event.httpMethod;
  const requestedPath = event.path;
  const requestedPathParameters = event.pathParameters;
  switch (requestedMethod) {
    case "GET": {
      return getReceiver(
        requestedPath,
        requestedPathParameters ? requestedPathParameters.id : ""
      );
    }
    case "POST": {
        return postReceiver(...)
    }
    ...
    default: {
      return {
        statusCode: 400,
        body: `Error: You are missing the path parameter id`,
      };
    }
  }
};

async function getReceiver(
  requestedPath: string,
  requestedItemId: string | null
) {
  switch (true) {
    //   path=/api/v1/items/
    case /\/api\/v1\/items\/?$/.test(requestedPath): {
      const params: ScanCommandInput = {
        TableName: TABLE_NAME,
        ProjectionExpression: "itemId, itemName",
      };
      try {
        const response = await dynamoDBClient.send(new ScanCommand(params));
        return { statusCode: 200, body: JSON.stringify(response) };
      } catch (dbError) {
        return { statusCode: 500, body: JSON.stringify(dbError) };
      }
    }
    //   path=/api/v1/items/000/
    case /\/api\/v1\/items\/[0-9]{3}\/?$/.test(requestedPath): {
      ...
    }
    default: {
      return {
        statusCode: 400,
        body: `Error: You are missing the path parameter id`,
      };
    }
  }
}
...

詰まった箇所

この構成にしたから、詰まったというわけではありませんが、DynamoDBにデータを書き込む際にエラーが発生しました。詰まった箇所は、Lambdaが受け取ったPOSTリクエストのbody部に含まれるデータをDynamoDBに挿入する、という部分です。デプロイ後にAPIを呼び出してみると500番が返ってきました。Lambdaの方でエラーログをみると以下のように書かれていました。

TypeError: Cannot read property '0' of undefined

最初DynamoDBの設定が間違っているのかと思いましたが、DynamoDBに挿入しようとしている文字列がundefinedになっていることに気づきました。

// postリクエストで受け取るbodyの型
interface WriteBody {
  itemId: string;
  itemName: string;
}

// Lambdaのhandler関数
export const handler: APIGatewayProxyHandlerV2 = async (
  event: any = {}
): Promise => {
    ...
    case "POST": {
      return postReceiver(requestedPath, event.body);
    }
    ...
}

postReceiver関数の引数にevent.bodyが指定されていますが、event.body.itemIdとしてもundefinedが返ってきます。そのため上記のコードの13行目は、

return postReceiver(requestedPath, JSON.parse(event.body));

このように変更して、postReceiver関数にはevent.bodyをjsonにパースしてから渡すことで値を参照できるようになりました。

感想

CDKをしっかり触ったことも、APIを自分で作った経験も、ほとんど無かったのでとても勉強になりました。クラウドのデータベースに対してCRUD操作を行うAPIを一人で作れるなんて、自分で出来ることが増えてきて嬉しいです。今回はCDKを使ってAPIを作りましたが、Express.jsのようなフレームワークを使ってAPIを使ってみてもいいのかなと思いました。時間があれば今度はExpressも勉強してみようと思います。Lambda関数を使ったAPI設計が主題だったので、あまり触れませんでしたが、AWS SDK(v3)を使ってDynamoDBに対して操作を行うのも少し難しかったです。その辺りに関しては以下のブログが参考になると思います。