API Gateway + Lambda + DynamoDB でサーバーレスな API 作成
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
というキーを指定しています。
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になっています。
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 のコードは以下で動作しますが、エラーハンドリングなどは省いているので、その前提で見てください。
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 でのリクエストを想定しているので、body
をjson.loads
で dict に変換しています。todo
という key のデータを、ToDo データとして扱っています。
boto3 のput_item
メソッドで DynamoDB に ToDo を登録しています。登録の際に、todo_id
がプライマリキーで必須なので、英数字のランダムな値を自動生成してセットしています。
serverless.yml の functions
に追加する内容は以下になります。
functions:
postTodo:
handler: todo.post_todo
events:
- httpApi:
path: /todo
method: post
path
が/todo
で、method
をpost
にしているので、パスが/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 は以下になります。
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 に渡されるevent
のpathParameters
に含まれます。
次に Python コードを記載します。
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
パスパラメータは event
の pathParameters
に含まれるので、event["pathParameters"]["todoId"]
とすれば todoId
が取得できます。
POST のときは DynamoDB へデータを新規作成するために put_item
メソッドを使用しました。GET/PUT/DELETE ではそれぞれ get_item
、update_item
、delete_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 のタグが今回のコードです)。
Discussion