こんにちは、インフラエンジニアのhogeです。本記事はiimonアドベントカレンダー9日目の記事となります。
- はじめに
- Lambda Powertoolsとは?
- 導入方法
- Lambda Powertoolsのイベントハンドラの機能
- コード
- Lambda PowertoolsでAPIを作って感じたメリット
- まとめ
- 最後に
はじめに
今回はLambdaの開発時におすすめのライブラリ、AWS Lambda Powertoolsについて書きたいと思います。
Lambda Powertoolsの機能は多岐に渡りますが、この記事ではAPIを便利に作るということにフォーカスを当てて解説したいと思いますので、イベントハンドラの機能のみの解説になります(他のユーティリティについては触れません)。この記事を見て本ライブラリを使ってLambdaでAPIを作るきっかけになれば幸いです。
Lambda Powertoolsとは?
Lambda Powertoolsは、サーバーレスのベストプラクティスを実装するためのユーティリティライブラリです。
Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity.
公式の説明は上記の通りで、toolkitと記載されているように、フレームワークのようなものではなく、Lambdaを開発する上で便利なツールがまとまったものになります。
現在対応している言語はPython・Java・TypeScript・.NETです。 本記事でのサンプルコードは全てPythonになります。
導入方法
導入方法として、以下の2通りの方法があります。
- コンテナイメージに同梱する
- Lambdaレイヤー
コンテナイメージに同梱する場合は、pip installでaws-lambda-powertools
をインストールします。
pip install aws-lambda-powertools
Lambdaレイヤーを使う場合は、「AWSレイヤー」にPowertoolsが既に用意されているので、それを指定するだけで導入することができます。
Lambda Powertoolsのイベントハンドラの機能
この例ではLambda Function UrlsでAPIを作成します。 よくあるTodoのCRUD処理を行うAPIを例に機能を解説します。
コード
from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver from aws_lambda_powertools import Logger from typing import List,Dict from pydantic import BaseModel, Field from aws_lambda_powertools.utilities.typing import LambdaContext import requests from requests import Response logger = Logger() app = LambdaFunctionUrlResolver(enable_validation=True) app.enable_swagger(path="/_swagger") class Todo(BaseModel): userId: int id_: int = Field(alias="id") title: str completed: bool @app.get("/todos") def get_todos() -> Dict[str,List[Todo]]: todo_id: str = app.current_event.get_query_string_value(name="id", default_value="") endpoint = "https://jsonplaceholder.typicode.com/todos" if todo_id: endpoint = f"{endpoint}/{todo_id}" todos: Response = requests.get(endpoint) todos.raise_for_status() if isinstance (todos.json(),dict): return {"todos": [todos.json()]} return {"todos": todos.json()[:10]} @app.post("/todos") def create_todo(todo: Todo) -> Todo: todo_data: dict = todo.dict(by_alias=True) response: Response = requests.post( "https://jsonplaceholder.typicode.com/todos", json=todo_data ) response.raise_for_status() return Todo(**response.json()) @app.put("/todos/<todo_id>") def update_todo(todo_id: str,todo:Todo) -> Todo: todo_data: dict = todo.dict(by_alias=True) response: Response = requests.put( f"https://jsonplaceholder.typicode.com/todos/{todo_id}", json=todo_data ) response.raise_for_status() return Todo(**response.json()) @logger.inject_lambda_context def lambda_handler(event: dict, context: LambdaContext) -> dict: logger.info("hoge") return app.resolve(event, context)
FlaskやFastAPIのようにデコレータでパスルーティングをする
素のLambda関数でパスルーティングが適切に行えるまともなAPIを作るのはするのはなかなか面倒な作業ですが、event handlerの機能を使えば以下のようにflaskやfastapiライクに簡単に実装することができます。
@app.get("/todos") def get_todos() -> Dict[str,List[Todo]]: todo_id: str = app.current_event.get_query_string_value(name="id", default_value="") endpoint = "https://jsonplaceholder.typicode.com/todos" if todo_id: endpoint = f"{endpoint}/{todo_id}" todos: Response = requests.get(endpoint) todos.raise_for_status() if isinstance (todos.json(),dict): return {"todos": [todos.json()]} return {"todos": todos.json()[:10]} @app.post("/todos") def create_todo(todo: Todo) -> Todo: todo_data: dict = todo.dict(by_alias=True) response: Response = requests.post( "https://jsonplaceholder.typicode.com/todos", json=todo_data ) response.raise_for_status() return Todo(**response.json()) @app.put("/todos/<todo_id>") def update_todo(todo_id: str,todo:Todo) -> Todo: todo_data: dict = todo.dict(by_alias=True) response: Response = requests.put( f"https://jsonplaceholder.typicode.com/todos/{todo_id}", json=todo_data ) response.raise_for_status() return Todo(**response.json())
指定されたパスと HTTP メソッドを処理できるようにリゾルバーを初期化する必要があります。
from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver app = LambdaFunctionUrlResolver(enable_validation=True) def lambda_handler(event: dict, context: LambdaContext) -> dict: return app.resolve(event, context)
以下のように動作確認をすると正しくtodoが取得できます
curl -i https://ohpezd7em4wzbystokhsji2tbm0nodtw.lambda-url.ap-northeast-1.on.aws/todos HTTP/1.1 200 OK Date: Sat, 09 Dec 2023 15:18:21 GMT Content-Type: application/json Content-Length: 830 Connection: keep-alive x-amzn-RequestId: bab20a70-9135-4945-8174-9df5efd5c04e X-Amzn-Trace-Id: root=1-6574853d-6d568d0f4c9d4e4045c2ad2a;sampled=0;lineage=f047d065:0 {"todos":[{"userId":1,"id":1,"title":"delectus aut autem","completed":false},{"userId":1,"id":2,"title":"quis ut nam facilis et officia qui","completed":false},{"userId":1,"id":3,"title":"fugiat veniam minus","completed":false},{"userId":1,"id":4,"title":"et porro tempora","completed":true},{"userId":1,"id":5,"title":"laboriosam mollitia et enim quasi adipisci quia provident illum","completed":false},{"userId":1,"id":6,"title":"qui ullam ratione quibusdam voluptatem quia omnis","completed":false},{"userId":1,"id":7,"title":"illo expedita consequatur quia in","completed":false},{"userId":1,"id":8,"title":"quo adipisci enim quam ut ab","completed":true},{"userId":1,"id":9,"title":"molestiae perspiciatis ipsa","completed":false},{"userId":1,"id":10,"title":"illo est ratione doloremque quia maiores aut","completed":true}]}%
また、無効なパスにリクエストした場合、きちんと404で返してくれます
curl -i https://ohpezd7em4wzbystokhsji2tbm0nodtw.lambda-url.ap-northeast-1.on.aws/invalid_path HTTP/1.1 404 Not Found Date: Sat, 09 Dec 2023 15:17:31 GMT Content-Type: application/json Content-Length: 40 Connection: keep-alive x-amzn-RequestId: da288619-db8f-4764-8b7f-43fb4a77dfc9 X-Amzn-Trace-Id: root=1-6574850b-10fb4c347eb89fe23cc6d23f;sampled=0;lineage=f047d065:0 {"statusCode":404,"message":"Not found"}%
レスポンスが成功の場合、デフォルトでは200ですが、レスポンスと一緒にステータスコードも返すことでカスタマイズすることができます。 また、レスポンスは、辞書型のオブジェクトをリターンすると自動でJSONシリアライズされるので、json.dumps()などは不要です。
return Todo(**response.json()),201
以下はPOSTの例
curl -i --location -X POST 'https://ohpezd7em4wzbystokhsji2tbm0nodtw.lambda-url.ap-northeast-1.on.aws/todos' --header 'Content-Type: application/json' \ --data-raw '{"userId":1,"id":2,"title":"et porro tempora","completed":"True"}' HTTP/1.1 201 Created Date: Sat, 09 Dec 2023 15:21:26 GMT Content-Type: application/json Content-Length: 65 Connection: keep-alive x-amzn-RequestId: 1edc720f-019c-4a5d-aee2-133f1c0409fb X-Amzn-Trace-Id: root=1-657485f5-0cc1733e2e296456399f75f9;sampled=0;lineage=f047d065:0
上記のことを行いたいだけなら、慣れ親しんだツールとしてAWSGIを使用し、Flaskを組み込む方法も良い選択だと思いますが、AWSGIはAPI Gateway経由のeventのみしか処理できません。これに対して、AWS Lambda Powertoolsは、API Gateway、Function URLs、ALB(Application Load Balancer)経由のイベントを処理することができます。例えばAPI Gatewayのイベントを処理したい場合、リゾルバを以下のように書き換えるだけで対応することができます。
# app = LambdaFunctionUrlResolver(enable_validation=True) app = APIGatewayRestResolver(enable_validation=True)
動的ルーティング
デコレータに@app.put("/todos/<todo_id>")のように登録することで動的ルーティングにも対応することができます。
@app.put("/todos/<todo_id>") def update_todo(todo_id: str,todo:Todo) -> Todo: todo_data: dict = todo.dict(by_alias=True) response: Response = requests.put( f"https://jsonplaceholder.typicode.com/todos/{todo_id}", json=todo_data ) response.raise_for_status() return Todo(**response.json())
バリデーション
リゾルバーに引数enable_validation=Trueを渡すことで、リクエスト、レスポンスにバリデーションをすることができます
app = LambdaFunctionUrlResolver(enable_validation=True)
pydantincのbasemodelを継承したクラスを作成します
class Todo(BaseModel): userId: int id_: int = Field(alias="id") title: str completed: bool
basemodelを継承したクラスを型に設定します
@app.post("/todos") def create_todo(todo: Todo) -> Todo: todo_data: dict = todo.dict(by_alias=True) response: Response = requests.post( "https://jsonplaceholder.typicode.com/todos", json=todo_data ) response.raise_for_status() return Todo(**response.json()),201
型に違反したリクエストを送った場合、422 Unprocessable Entityエラーが返されます
$ curl -i --location -X POST 'https://ohpezd7em4wzbystokhsji2tbm0nodtw.lambda-url.ap-northeast-1.on.aws/todos' --header 'Content-Type: application/json' \ --data-raw '{"userId":"a","id":2,"title":1,"completed":"True"}' HTTP/1.1 422 Unprocessable Entity Date: Sat, 09 Dec 2023 15:43:30 GMT Content-Type: application/json Content-Length: 122 Connection: keep-alive x-amzn-RequestId: e1715329-2654-47d7-a3ea-1d3d242ebeb4 X-Amzn-Trace-Id: root=1-65748b22-24ec1e956eb255be5ee2d731;sampled=0;lineage=f047d065:0 {"statusCode":422,"detail":[{"loc":["body","userId"],"type":"int_parsing"},{"loc":["body","title"],"type":"string_type"}]}%
Swagger UIでAPIドキュメントを作成する
swagger UIを有効にすることができます(デフォルトのパスは/swagger)
app.enable_swagger(path="/_swagger")
/_swagger
エンドポイントにアクセスすると、Swagger UIでAPIドキュメントを見ることができます
Lambda PowertoolsでAPIを作って感じたメリット
ざっくり言うと、以下のメリットがあると感じました。
- バリデーションを自前で実装しなくて良くなる
- 型の恩恵を受けることができる
- 型からAPIドキュメントを自動的に生成してくれる
- Lambdaに入ってくるイベント形式を知らなくても実装できる。
以下はLambda function urlsのイベントの構造です。これを見て実装すること自体は難しくないですが、Lambda Powertoolsを使うことで、この複雑なイベント構造を理解する必要が減り、よりビジネスロジックの実装に集中できるようになります。また、API GatewayとLambda function urlsでイベント形式が異なりますが、リゾルバが差異を吸収してくれて同じように処理を書くことができるのもメリットかと思います。
{ "version": "2.0", "routeKey": "$default", "rawPath": "/my/path", "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", "cookies": [ "cookie1", "cookie2" ], "headers": { "header1": "value1", "header2": "value1,value2" }, "queryStringParameters": { "parameter1": "value1,value2", "parameter2": "value" }, "requestContext": { "accountId": "123456789012", "apiId": "<urlid>", "authentication": null, "authorizer": { "iam": { "accessKey": "AKIA...", "accountId": "111122223333", "callerId": "AIDA...", "cognitoIdentity": null, "principalOrgId": null, "userArn": "arn:aws:iam::111122223333:user/example-user", "userId": "AIDA..." } }, "domainName": "<url-id>.lambda-url.us-west-2.on.aws", "domainPrefix": "<url-id>", "http": { "method": "POST", "path": "/my/path", "protocol": "HTTP/1.1", "sourceIp": "123.123.123.123", "userAgent": "agent" }, "requestId": "id", "routeKey": "$default", "stage": "$default", "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, "body": "Hello from client!", "pathParameters": null, "isBase64Encoded": false, "stageVariables": null }
まとめ
Lambda Powertoolsを使うことで素のLambdaで実装する場合と比べて、とても快適にAPIを作ることができました。 大量のエンドポイントを持つ巨大なAPIを作る場合は、Lambdaは適していないかもしれませんが、ルーティングが必要だったり、複数のメソッドをサポートする必要がある場合、Lambda Powertoolsを使ってこれらの処理を簡潔かつ安全に実装できるのは非常に有用だと思いました。 弊社ではAPIサーバから部分的に機能を切り出してLambdaでAPIを作ることがあるので、今後は積極的にLambda Powertoolsを使って効率的にAPIを実装したいと思いました。
最後に
この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします。
次のアドベントカレンダーの記事はなかむーさんです!どんな記事を書くのかとても楽しみです!