😽

API Gateway + Lambda + DynamoDB でサーバーレスな API 作成

2021/04/17に公開

Serverless Framework を使って API Gateway と Lambda、DynamoDB を組み合わせた API の作成方法を説明します。

前提

Serverless Framework がインストール済みで、AWS のアカウント設定済みの前提で進めます。
実行環境の Serverless Framework のバージョンは 2.29.0 で、Lambda で使用する言語は Python 3.8 です。

作成する API

簡単な ToDo リストの API を作成します。作成する API は以下の 4 つです。

POST   /todo          : 新規ToDoリスト作成
GET    /todo/{todoId} : ToDoの取得
PUT    /todo/{todoId} : ToDoの更新
DELETE /todo/{todoId} : ToDoの削除

DynamoDB のテーブル作成

まずは、ToDo リストを管理する DB を作成します。

serverless.yml のresourcesに CloudFormation の形式で記載することで DynamoDB のリソースを作成できます。
今回、以下に記載しているToDoTableというテーブルを作成します。DynamoDB のテーブルにはプライマリーキーが必須なので、プライマリーキーにtodo_idというキーを指定しています。

serverless.yml
resources:
  Resources:
    todoTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ToDoTable
        AttributeDefinitions:
          - AttributeName: todo_id
            AttributeType: S
        KeySchema:
          - AttributeName: todo_id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

AWS の無料枠で試したいのでプロビジョニングモードで設定してます。オンデマンドモードにしたい場合は、ProvisionedThroughputのところをBillingMode: PAY_PER_REQUESTに変更すればオンデマンドで作成されます。設定の詳細はAWS のマニュアルなどを参照してください。

API の流量制限をしない場合、DynamoDB がプロビジョニングモードで動いていると、DynamoDB のキャパシティが原因で API エラーになることがあります。そのため、オートスケールしてくれる、オンデマンドモードのほうが運用は楽になります。
ただし、オンデマンドモードの場合はオートスケールしてくれる反面、DDoS のようなアクセスがあっても正常に動いてくれてしまうので、そのようなケースでは想定以上の利用料が発生します。いずれにせよ、API のアクセス制限などは適切におこなうようにしてください。

DynamoDB へのアクセス権限付与

作成した ToDoTable に Lambda からアクセスすることになるので、事前にアクセス権限を付与しておきます。

以下のようにprovider.iam.role.statementsを serverless.yml に追加すれば、作成する Lambda に IAM Policy が付与されます。ちなみに、以前はprovider.iamRoleStatementsで設定していましたが、現在はDeprecationになっています。

serverless.yml
provider:
  # name, runtimeなどの記述は省略
  iam:
    role:
      statements:
        - Effect: "Allow"
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/ToDoTable"

DynamoDB 関連の設定が完了したので、ToDoTableにアクセスする API を作っていきます。

ToDo 登録 API の作成

まずは、ToDo を登録するために POST メソッドの API を作成します。

Python のコードは以下で動作しますが、エラーハンドリングなどは省いているので、その前提で見てください。

todo.py
import json
import random
import string

import boto3

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("ToDoTable")

def post_todo(event, context):
    body = json.loads(event.get("body"))
    todo = body.get("todo")
    todo_id = "".join(random.choices(string.ascii_letters + string.digits, k=12))
    item = {"todo_id": todo_id, "todo": todo}
    table.put_item(Item=item)
    response = {"statusCode": 200, "body": json.dumps(item)}
    return response

API のリクエストは JSON でのリクエストを想定しているので、bodyjson.loadsで dict に変換しています。todo という key のデータを、ToDo データとして扱っています。
boto3 のput_itemメソッドで DynamoDB に ToDo を登録しています。登録の際に、todo_id がプライマリキーで必須なので、英数字のランダムな値を自動生成してセットしています。

serverless.yml の functions に追加する内容は以下になります。

serverless.yml
functions:
  postTodo:
    handler: todo.post_todo
    events:
      - httpApi:
          path: /todo
          method: post

path/todoで、methodpostにしているので、パスが/todo の POST メソッドを作成するという意味になります。

これをデプロイします。

$ sls deploy

POST /todo の実行で API の動作を確認

デプロイした API を実行してみます。body に JSON を入れて、POST でリクエストします。
また、ヘッダーにContent-Type: application/jsonをセットしないと body を JSON として扱わないので、その点は注意してください。

$ curl -XPOST -H "Content-Type: application/json" 'https://<yourapigatewayid>.execute-api.ap-northeast-1.amazonaws.com/todo' -d '{"todo":"コーヒーを買う"}' | jq .
{
  "todo_id": "iAx50hjXLiFB",
  "todo": "コーヒーを買う"
}

{"todo":"コーヒーを買う"}をリクエストして、それが正常に登録されたので todo_id と一緒にレスポンスが返ってきました。

次に、このデータにアクセスする GET/PUT/DELETE メソッドの API を作成します。

GET/PUT/DELETE メソッドの API 作成

GET/PUT/DELETE メソッドの API を作成する際の serverless.yml は以下になります。

serverless.yml
functions:
  getTodo:
    handler: todo.get_todo
    events:
      - httpApi:
          path: /todo/{todoId}
          method: get
  putTodo:
    handler: todo.put_todo
    events:
      - httpApi:
          path: /todo/{todoId}
          method: put
  deleteTodo:
    handler: todo.delete_todo
    events:
      - httpApi:
          path: /todo/{todoId}
          method: delete

POST のときとの違いは、パスパラメータに {todoId} が含まれることです。GET/PUT/DELETE については、特定の ToDo に対しての操作になるので {todoId} をセットしています。
また、ここで指定した中括弧の中の変数は、Lambda に渡されるeventpathParametersに含まれます。

次に Python コードを記載します。

todo.py
def get_todo(event, context):
    todo_id = event["pathParameters"]["todoId"]
    res = table.get_item(Key={"todo_id": todo_id})
    item = res.get("Item")
    response = {"statusCode": 200, "body": json.dumps(item)}
    return response

def put_todo(event, context):
    todo_id = event["pathParameters"]["todoId"]
    body = json.loads(event.get("body"))
    todo = body.get("todo")
    item = {"todo_id": todo_id, "todo": todo}
    table.update_item(
        Key={"todo_id": todo_id},
        UpdateExpression="set todo=:todo",
        ExpressionAttributeValues={":todo": todo},
    )
    response = {"statusCode": 200, "body": json.dumps(item)}
    return response

def delete_todo(event, context):
    todo_id = event["pathParameters"]["todoId"]
    table.delete_item(Key={"todo_id": todo_id})
    response = {"statusCode": 200, "body": json.dumps({"todo_id": todo_id})}
    return response

パスパラメータは eventpathParameters に含まれるので、event["pathParameters"]["todoId"]とすれば todoId が取得できます。

POST のときは DynamoDB へデータを新規作成するために put_item メソッドを使用しました。GET/PUT/DELETE ではそれぞれ get_itemupdate_itemdelete_item を使用しています。
各メソッドの使い方は、AWS のドキュメントなどを参照してください。

また、これら DynamoDB 操作用のメソッドを実行するには、権限が必要なります。そのため、最初の方で serverless.yml のprovider.iam.role.statementsに IAM Policy を追加していました。

それでは、作成した API をデプロイします。

$ sls deploy

作成した GET/PUT/DELETE メソッドの API を確認

作成した GET/PUT/DELETE メソッドの API を実行します。

GET /todo/{todoId} の実行

GET メソッドの実行は以下のとおりです。DynamoDB から対象の ToDo が取得できています。

$ curl -XGET 'https://<yourapigatewayid>.execute-api.ap-northeast-1.amazonaws.com/todo/iAx50hjXLiFB' | jq .
{
  "todo_id": "iAx50hjXLiFB",
  "todo": "コーヒーを買う"
}

PUT /todo/{todoId} の実行

PUT メソッドの実行は以下のとおりです。

$ curl -XPUT -H "Content-Type: application/json" 'https://<yourapigatewayid>.execute-api.ap-northeast-1.amazonaws.com/todo/iAx50hjXLiFB' -d '{"todo":"牛乳を買う"}' | jq .
{
  "todo_id": "iAx50hjXLiFB",
  "todo": "牛乳を買う"
}

PUT メソッド実行後に GET メソッドを実行することでも、対象の更新が確認できます。

$ curl -XGET 'https://<yourapigatewayid>.execute-api.ap-northeast-1.amazonaws.com/todo/iAx50hjXLiFB' | jq .
{
  "todo_id": "iAx50hjXLiFB",
  "todo": "牛乳を買う"
}

DELETE /todo/{todoId} の実行

DELETE メソッドの実行は以下のとおりです。

$ curl -XDELETE 'https://<yourapigatewayid>.execute-api.ap-northeast-1.amazonaws.com/todo/iAx50hjXLiFB' | jq .
{
  "todo_id": "iAx50hjXLiFB"
}

DELETE メソッド実行後に GET メソッドを実行すると、対象が存在しないので削除が確認できます。

$ curl -XGET 'https://<yourapigatewayid>.execute-api.ap-northeast-1.amazonaws.com/todo/iAx50hjXLiFB' | jq .
null

おまけ

今回使用したコードは以下の Github リポジトリにアップしているので、全体のコードを確認したい方はそちらを見てください(v3.0 のタグが今回のコードです)。
https://github.com/ombran/serverless-sample/tree/v3.0

Discussion