AWS Lambda Powertools を用いた Amazon API Gateway & AWS Lambda の汎用化構成検証 (概要編)

AWS Lambda Powertools を用いた Amazon API Gateway & AWS Lambda の汎用化構成検証 (概要編)

Clock Icon2025.03.25

こんにちは!コンサルティング部のくろすけです!
以前 Amazon API Gateway (以降 APIGateway) & AWS Lambda (以降 Lambda) の構成をよく使用していました。
そこで、以前から課題に感じていた以下の点について検討・検証してみました。

  • このインフラ構成の汎用性を向上することができないか
  • API仕様書の保守し忘れによる実仕様との不一致をなるべく回避できないか

※本記事は下記の連載記事を参考にしています。
AWS Lambda Powertools Python 入門 第 1 回 - 変化を求めるデベロッパーを応援するウェブマガジン | AWS

結論

Lambda Powertools を活用すればある程度下記を達成できそう結論です。

  • APIGateway & Lambda 構成の汎用性向上
  • API仕様書と実仕様とのsync

構成

既存の構成と課題

krsk-aws-lambda-powertools-generic-api-design-20250325-001-1.png

課題

  1. APIGatewayのソースが可変のため、汎用的なIaCテンプレートの作成が難しい
  2. Lambdaが複数に分かれることで、アプリの実態とAPI仕様書双方を保守する必要がある
    • 人がアプリの実態とAPI仕様書をsyncする必要がある(FastAPIなどの仕様書生成を使いたい)
    • openapi仕様書からAPIGatewayを作成する手もあるが、こちらも結局は双方の保守が必要
  3. 1及び2により、APIごとにインフラエンジニアが出張る必要がある
    • 状況次第では、アプリ側とインフラ側のタスク切り分けが曖昧になる可能性も
    • この点は体制次第では気にならない場合もあり
  4. Lambdaが複数に分かれることで、APIの全体像がアプリ側のコードがわかりづらい
    • どのパス(リソース)に紐づいているLambdaなのか?など

根本的な課題

  • 「APIGatewayのリソースの数 = APIGatewayに紐付けたい機能(Lambda)の数」である

提案する構成

krsk-aws-lambda-powertools-generic-api-design-20250325-002-1.png

解決策

  • Lambda Powertoolsを使用してリクエストパスでメソッドをハンドルし、「APIGatewayの数 = Lambdaの数」とする

ソリューションのメリット

  1. 「APIGatewayの数 = Lambdaの数」と固定できるため、IaCテンプレートが汎用化できる
  2. Lambda Powertoolsの機能で、アプリのコードをベースにしたswagger仕様書を生成および仕様書へのパスを作成できる
  3. 1により、アプリケーションエンジニアがほぼインフラを気にせずAPIを起動できる
  4. リクエストパスでメソッドをハンドルするため、APIの全体像がコードからわかりやすい

という具合でLambda Powertoolsを活用することで、これらの課題をほぼ解決できるのではと考えました!

やってみた

Lambda

ディレクトリ構成

.
├── app.py
├── models
│   ├── __init__.py
│   └── user.py
└── requirements.txt

アプリケーションコード

app.py
from aws_lambda_powertools import Logger, Metrics, Tracer
from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver
from aws_lambda_powertools.logging.correlation_paths import API_GATEWAY_REST
from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent
from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext

from models.user import User, UserCreate

app = APIGatewayRestResolver(enable_validation=True)
app.enable_swagger(path="/swagger")
tracer = Tracer()
logger = Logger()
metrics = Metrics()


users = [
    {
        "id": "1",
        "email": "foo.krsk@example.com",
        "last_name": "Krsk",
        "first_name": "Foo",
        "password": "Password123!",
    },
    {
        "id": "2",
        "email": "bar.krsk@classmethod.jp",
        "last_name": "Krsk",
        "first_name": "Bar",
        "password": "Password456!",
    },
]


@app.post("/user")
@tracer.capture_method
def create_user(user: UserCreate) -> User:
    res = User(**user.model_dump())
    logger.info(res)
    return res


@app.get("/user/<id>")
@tracer.capture_method
def get_user(id: str) -> User:
    res = [User(**user) for user in users if user["id"] == id][0]
    logger.info(res)
    return res


@app.get("/users")
@tracer.capture_method
def get_users() -> list[User]:
    res = [User(**user) for user in users]
    logger.info(res)
    return res


@app.exception_handler(Exception)
def handle_value_error(e: Exception):
    logger.error(f"Exception: {str(e)}")
    return {"message": str(e)}


@tracer.capture_lambda_handler
@logger.inject_lambda_context(log_event=True, correlation_id_path=API_GATEWAY_REST)
@metrics.log_metrics(capture_cold_start_metric=True)
def handler(event: APIGatewayProxyEvent, context: LambdaContext) -> dict:
    return app.resolve(event, context)

user.py
import re

from pydantic import BaseModel, EmailStr, Field, field_validator


class User(BaseModel):
    id: str = Field(..., min_length=1, max_length=36)
    email: EmailStr = Field(..., max_length=254)
    last_name: str = Field(..., max_length=30)
    first_name: str = Field(..., max_length=30)


class UserCreate(User):
    password: str = Field(..., min_length=8, max_length=100)

    @field_validator("password")
    def validate_password(cls, v):
        # 少なくとも1つの数字、1つの大文字、1つの小文字、1つの特殊文字を含む
        if not re.match(
            r'^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+\-=\[\]{};:\'",.<>/?]).{8,}$', v
        ):
            raise ValueError(
                "パスワードは少なくとも1つの数字、大文字、小文字、特殊文字を含む必要があります"
            )
        return v

APIGateway

すべてのリクエストを単一のLambdaにルーティングするために、HTTPプロキシ統合を使用します。
これがAPIGateway側のポイントになるかなと思います。

上記のために下記の機能を利用しています。

網羅的なプロキシリソース ({proxy+}) と、多様な状況に対応できる HTTP メソッドの ANY 動詞を使用すれば、HTTP プロキシ統合を使用して、単一の API メソッドの API を作成することができます。

チュートリアル: HTTP プロキシ統合を使用して REST API を作成する - Amazon API Gatewayより引用

実際のリソースは下記のような形です。
※Terraformで作成したので、作成過程は別途記事にできればと思います。
CleanShot 2025-03-25 at 16.38.15@2x.png

テスト

パス次第でそれぞれのハンドラを呼び出せることを確認できました。良さそうです!

  • POST: /user
    CleanShot 2025-03-25 at 16.50.41@2x.png
  • GET: /users
    CleanShot 2025-03-25 at 16.45.07@2x.png
  • GET: /user/<id>
    CleanShot 2025-03-25 at 16.48.45@2x.png
  • GET: /swagger
    CleanShot 2025-03-25 at 16.52.34@2x-2.png

あとがき

かなり悩みましたが、なんとか第一弾の記事にできました。
今回の課題は、結構困っていたんですが本記事のソリューションはある程度有効な解決策になりそうです。

とはいえ気になる点も...

  • swaggerに認証方法などの仕様を記載できないのでは?
    まだ試せてはいないのでもしかするとできるのかもしれません。
    ただ実態としてはAPIGateway部分となるので、アプリ側の仕様ではなくインフラ側の仕様なんですよね...
    せっかくスコープを分けたので個人的には若干の違和感。
  • X-rayなどがLambdaで勝手に作成されるので、スコープ的にはIaC側で作成して紐付ける方が良さそう
    これの検証結果は別途記事にしたい。

より良いソリューションがあれば、また記事にしようと思います!今回はここまで!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.