🛠️

ここから始める Powertools for AWS Lambda (Python) - Parser 編 -

に公開

はじめに

こんばんは。
株式会社 WHERE のプロダクト開発部で、主にインフラを担当している えんどぅ です。

みなさん元気に Lambda Life を送っていますか??

Lambda 関数の開発において、入力データの解析と検証は非常に重要な処理です。
API Gateway からのリクエスト、EventBridge からのイベント、SQS メッセージなど、様々な形式のデータを安全かつ効率的に処理する必要があります。

しかし、手動でのデータ解析と検証は複雑で、型安全性の確保、ネストされた構造の処理、カスタムバリデーションの実装など、多くの課題を抱えています。また、コードの可読性や保守性にも影響を与えることが多いのではないでしょうか。

そんな課題を解決してくれるのが、Powertools for AWS Lambda の Parser 機能です。
今回はこの機能について、実際の使用イメージと共に紹介します。

なお、今後も Powertools for AWS Lambda の各種機能について継続的にブログを書いていく予定なので、興味のある方は楽しみにしていただけると嬉しいです。

※ 過去のブログ記事はコチラ👇

Powertools for AWS Lambda とは

Powertools for AWS Lambda は、サーバーレスベストプラクティスの実装と開発者の生産性向上を支援する開発者ツールキットです。

段階的に導入できる柔軟な設計により、必要な機能から順次採用することができます。
Python, TypeScript, Java, .NET で提供されており、AWS Well-Architected Serverless Lens に基づく推奨事項を簡単に実装できます。

Parser 機能とは

Parser 機能 は、Pydantic を使用してデータの解析と検証を簡素化するユーティリティです。純粋な Python クラスでデータモデルを定義し、入力イベントを解析・検証して、必要なデータのみを抽出できます。

主な特徴

  • Python クラスによるデータモデル定義
  • Lambda イベントペイロードの解析と検証
  • 一般的な AWS イベントソースへの組み込みサポート
  • ユーザーフレンドリーなエラーメッセージによる実行時型チェック
  • Pydantic v2.x との互換性

提供されている機能

Data Model with Parser

  • BaseModel や TypeAdapter を継承したモデル定義
  • Pydantic による自動データ検証と型安全性の確保

Event parser

  • @event_parser デコレータによる自動イベント解析
  • バリデーションエラーの自動ハンドリング
  • Fail-fast アプローチによる早期エラー検出

Parse function

  • parse() 関数によるプログラマティックなデータ解析
  • 柔軟なイベント形式への対応
  • きめ細かいエラーハンドリング制御

Built-in models

  • AWS サービス用の事前定義モデル
  • SQS, EventBridge, API Gateway 等への対応
  • カスタムモデルとの組み合わせ

Envelopes

  • 複雑なネストされた JSON 構造からの特定部分抽出
  • Built-in envelopes による主要イベントソース対応
  • カスタムエンベロープの実装サポート

Data model validation

  • field_validator によるフィールドレベル検証
  • model_validator によるモデル全体の整合性チェック
  • 複雑な関係性の検証

Pydantic とは

Pydantic は、Python の型ヒントを使用してデータの検証とシリアライゼーションを行うライブラリです。Powertools for AWS Lambda の Parser 機能は Pydantic v2 をベースに構築されています。

主な特徴

  • 型安全性: Python の型ヒントを実行時に強制
  • バリデーション: 豊富な検証ルールとカスタム検証
  • パフォーマンス: Rust で実装されたコアによる高速処理
  • IDE 支援: 自動補完とインラインドキュメント
  • エラーハンドリング: 詳細で分かりやすいエラーメッセージ

実践: Parser 機能を使用した Lambda 関数の構築・実装

ここからは、実際に Lambda 関数を構築・実装するイメージを紹介します。

※ プロジェクトの完全なコードは コチラ

使用ツール バージョン
aws-cdk-cli 2.1021.0
aws-cdk-lib 2.199.0
aws-cli 2.13.32
node 22.13.1
python 3.11.8
uv 0.6.14

0. プロジェクトの初期化

Terminal
# プロジェクトディレクトリの作成
mkdir parser && \
cd parser
Terminal
# AWS CDK プロジェクトの初期化
cdk init app --language python
Terminal
# プロジェクトのセットアップ
source .venv/bin/activate && \
uv init && \
uv add --group infra -r requirements.txt && \
uv add --dev -r requirements-dev.txt && \
mkdir lambda && \
touch lambda/function_before.py lambda/function_after.py lambda/schemas.py && \
rm requirements.txt requirements-dev.txt main.py

主なプロジェクト構成は以下のようになります:

parser/
├── parser/
│   └── parser_stack.py           # AWS CDK スタック定義
├── lambda/                       # Lambda 関数のソースコード
│   ├── function_after.py         # Parser 機能使用版
│   ├── function_before.py        # Parser 機能未使用版
│   └── schemas.py                # Pydantic モデル定義
├── app.py                        # AWS CDK アプリケーションのエントリーポイント
├── cdk.json                      # AWS CDK 設定ファイル
├── pyproject.toml                # 依存関係管理ファイル
└── uv.lock                       # 依存関係管理ファイル

1. 依存関係の設定

Terminal
# Powertools for AWS Lambda の追加
uv add --group lambda "aws-lambda-powertools"

2. データモデルの定義

lambda/schemas.py
from __future__ import annotations

from datetime import datetime
from enum import Enum

from pydantic import BaseModel, Field, field_validator


class OrderStatus(str, Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"


class OrderItem(BaseModel):
    product_id: str = Field(..., min_length=1, description="商品ID")
    product_name: str = Field(..., min_length=1, max_length=200)
    quantity: int = Field(..., gt=0, le=100)
    unit_price: float = Field(..., gt=0)
    
    @field_validator('unit_price')
    @classmethod
    def validate_unit_price(cls, v: float) -> float:
        if v <= 0:
            raise ValueError('単価は0より大きい値である必要があります')
        return round(v, 2)


class CustomerInfo(BaseModel):
    customer_id: str = Field(..., min_length=1)
    name: str = Field(..., min_length=1, max_length=100)
    email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$')


class Order(BaseModel):
    order_id: str = Field(..., min_length=1)
    customer: CustomerInfo
    items: list[OrderItem] = Field(..., min_length=1)
    status: OrderStatus = OrderStatus.PENDING
    order_date: datetime = Field(default_factory=datetime.now)
    notes: str | None = Field(None, max_length=500)
    
    @field_validator('items')
    @classmethod
    def validate_items(cls, v: list[OrderItem]) -> list[OrderItem]:
        if not v:
            raise ValueError('注文には少なくとも1つの商品が必要です')
        return v
    
    def calculate_total(self) -> float:
        return sum(item.quantity * item.unit_price for item in self.items)

3. Lambda 関数の実装 (Parser 機能未使用版)

lambda/function_before.py
import json
import logging
from datetime import datetime, timezone

from aws_lambda_powertools.utilities.typing import LambdaContext

logger = logging.getLogger(__name__)


def lambda_handler(event: dict[str, any], context: LambdaContext) -> dict[str, any]:
    """Parser 機能未使用"""
    
    try:
        # イベントボディの解析
        body = json.loads(event.get("body", "{}"))
        
        # 手動でのデータ検証
        validation_errors = validate_order_data(body)
        if validation_errors:
            return {
                "statusCode": 400,
                "body": json.dumps({
                    "error": "Validation failed",
                    "details": validation_errors
                })
            }
        
        # ビジネスロジックの実行
        result = process_order(body)
        
        return {
            "statusCode": 200,
            "body": json.dumps(result)
        }
        
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Invalid JSON format"})
        }
    except Exception as e:
        logger.error(f"Unexpected error: {e!s}")
        return {
            "statusCode": 500,
            "body": json.dumps({"error": "Internal server error"})
        }


def validate_order_data(data: dict[str, any]) -> list[str]:
    """手動でのデータ検証"""
    errors = []
    
    # 注文IDの検証
    if "order_id" not in data:
        errors.append("order_id is required")
    elif not isinstance(data["order_id"], str) or not data["order_id"].strip():
        errors.append("order_id must be a non-empty string")
    
    # 顧客情報の検証
    if "customer" not in data:
        errors.append("customer information is required")
    else:
        customer = data["customer"]
        if not isinstance(customer, dict):
            errors.append("customer must be an object")
        else:
            if "customer_id" not in customer:
                errors.append("customer.customer_id is required")
            if "name" not in customer:
                errors.append("customer.name is required")
            if "email" not in customer:
                errors.append("customer.email is required")
            elif "@" not in customer["email"]:
                errors.append("customer.email format is invalid")
    
    # 商品リストの検証
    if "items" not in data:
        errors.append("items are required")
    else:
        items = data["items"]
        if not isinstance(items, list) or len(items) == 0:
            errors.append("items must be a non-empty array")
        else:
            for i, item in enumerate(items):
                if not isinstance(item, dict):
                    errors.append(f"items[{i}] must be an object")
                    continue
                
                if "product_id" not in item:
                    errors.append(f"items[{i}].product_id is required")
                if "product_name" not in item:
                    errors.append(f"items[{i}].product_name is required")
                if "quantity" not in item:
                    errors.append(f"items[{i}].quantity is required")
                elif not isinstance(item["quantity"], int) or item["quantity"] <= 0:
                    errors.append(f"items[{i}].quantity must be a positive integer")
                if "unit_price" not in item:
                    errors.append(f"items[{i}].unit_price is required")
                elif not isinstance(item["unit_price"], (int, float)) or item["unit_price"] <= 0:
                    errors.append(f"items[{i}].unit_price must be a positive number")
    
    return errors


def process_order(order_data: dict[str, any]) -> dict[str, any]:
    """注文処理"""
    
    # 合計金額の計算
    total_amount = sum(
        item["quantity"] * item["unit_price"] 
        for item in order_data.get("items", [])
    )
    
    return {
        "message": "Order processed successfully",
        "order_id": order_data["order_id"],
        "customer_id": order_data["customer"]["customer_id"],
        "total_amount": total_amount,
        "processed_at": datetime.now(timezone.utc).isoformat(),
        "status": "confirmed"
    }

4. Lambda 関数の実装 (Parser 機能使用版)

lambda/function_after.py
import json
from datetime import datetime, timezone

from aws_lambda_powertools.utilities.parser import ValidationError, event_parser
from aws_lambda_powertools.utilities.parser.envelopes import ApiGatewayEnvelope
from aws_lambda_powertools.utilities.typing import LambdaContext

from schemas import Order


@event_parser(model=Order, envelope=ApiGatewayEnvelope)
def lambda_handler(event: Order, context: LambdaContext) -> dict[str, any]:
    """Parser 機能使用"""
    
    try:
        # 自動的に解析・検証されたデータを使用
        result = process_order(event)
        
        return {
            "statusCode": 200,
            "body": json.dumps(result)
        }
        
    except Exception as e:
        return {
            "statusCode": 500,
            "body": json.dumps({
                "error": "Internal server error",
                "message": str(e)
            })
        }


def process_order(order: Order) -> dict[str, any]:
    """注文処理"""
    
    # Pydantic モデルのメソッドを直接使用
    total_amount = order.calculate_total()
    
    # 型安全なアクセス
    return {
        "message": "Order processed successfully",
        "order_id": order.order_id,
        "customer_id": order.customer.customer_id,
        "customer_name": order.customer.name,
        "items_count": len(order.items),
        "total_amount": total_amount,
        "processed_at": datetime.now(timezone.utc).isoformat(),
        "status": order.status.value
    }

5. AWS CDK スタックの定義

parser/parser_stack.py
from aws_cdk import CfnOutput, Stack
from aws_cdk import aws_lambda as _lambda
from constructs import Construct

class ParserStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Lambda Layer: Powertools for AWS Lambda (Python)
        # Layer ARN: https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer
        layer = _lambda.LayerVersion.from_layer_version_arn(
            self,
            "PowertoolsLayer",
            layer_version_arn="arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:20"
        )

        # Lambda 関数: Parser 機能未使用版
        function_before = _lambda.Function(
            self,
            "FunctionBefore",
            runtime=_lambda.Runtime.PYTHON_3_11,
            handler="function_before.lambda_handler",
            code=_lambda.Code.from_asset("lambda"),
            layers=[layer],
        )

        # Lambda 関数: Parser 機能使用版
        function_after = _lambda.Function(
            self,
            "FunctionAfter",
            runtime=_lambda.Runtime.PYTHON_3_11,
            handler="function_after.lambda_handler",
            code=_lambda.Code.from_asset("lambda"),
            layers=[layer],
        )

        CfnOutput(
            self,
            "FunctionBeforeName",
            value=function_before.function_name,
            export_name="FunctionBeforeName",
        )

        CfnOutput(
            self,
            "FunctionAfterName",
            value=function_after.function_name,
            export_name="FunctionAfterName",
        )

6. デプロイ・動作確認

Terminal
# デプロイの実行
cdk deploy --require-approval never
Terminal
cat > test_valid_order.json << 'EOF'
{
  "resource": "/orders",
  "path": "/orders",
  "httpMethod": "POST",
  "headers": {
    "Accept": "application/json",
    "Content-Type": "application/json",
    "Host": "api.example.com",
    "User-Agent": "Mozilla/5.0",
    "X-Forwarded-For": "192.168.1.1",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "multiValueHeaders": {
    "Accept": ["application/json"],
    "Content-Type": ["application/json"]
  },
  "queryStringParameters": null,
  "multiValueQueryStringParameters": null,
  "pathParameters": null,
  "stageVariables": null,
  "requestContext": {
    "resourceId": "123456",
    "resourcePath": "/orders",
    "httpMethod": "POST",
    "extendedRequestId": "request-id",
    "requestTime": "09/Apr/2025:12:34:56 +0000",
    "path": "/prod/orders",
    "accountId": "123456789012",
    "protocol": "HTTP/1.1",
    "stage": "prod",
    "domainPrefix": "api",
    "requestTimeEpoch": 1712668496000,
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "sourceIp": "192.168.1.1",
      "principalOrgId": null,
      "accessKey": null,
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "Mozilla/5.0",
      "user": null
    },
    "domainName": "api.example.com",
    "apiId": "1234567890"
  },
  "body": "{\"order_id\":\"ORD-12345\",\"customer\":{\"customer_id\":\"CUST-001\",\"name\":\"田中太郎\",\"email\":\"tanaka@example.com\",\"phone\":\"+81-90-1234-5678\"},\"items\":[{\"product_id\":\"PROD-001\",\"product_name\":\"ノートPC\",\"quantity\":1,\"unit_price\":89800.0},{\"product_id\":\"PROD-002\",\"product_name\":\"マウス\",\"quantity\":2,\"unit_price\":2980.0}],\"notes\":\"急ぎの配送希望\"}",
  "isBase64Encoded": false
}
EOF

cat > test_invalid_order.json << 'EOF'
{
  "httpMethod": "POST",
  "path": "/orders",
  "body": "{\"order_id\":\"\",\"customer\":{\"customer_id\":\"CUST-001\",\"name\":\"\",\"email\":\"invalid-email\"},\"items\":[{\"product_id\":\"PROD-001\",\"quantity\":0,\"unit_price\":-100}]}",
  "isBase64Encoded": false
}
EOF
Terminal
# Parser 機能未使用版のテスト (有効なデータ)
aws lambda invoke \
  --function-name <Before 関数名> \
  --cli-binary-format raw-in-base64-out \
  --payload file://test_valid_order.json \
  response_before_valid.json && \
cat response_before_valid.json
Terminal
# Parser 機能未使用版のテスト (無効なデータ)
aws lambda invoke \
  --function-name <Before 関数名> \
  --cli-binary-format raw-in-base64-out \
  --payload file://test_invalid_order.json \
  response_before_invalid.json && \
cat response_before_invalid.json
Terminal
# Parser 機能使用版のテスト (有効なデータ)
aws lambda invoke \
  --function-name <After 関数名> \
  --cli-binary-format raw-in-base64-out \
  --payload file://test_valid_order.json \
  response_after_valid.json && \
cat response_after_valid.json
Terminal
# Parser 機能使用版のテスト (無効なデータ)
aws lambda invoke \
  --function-name <After 関数名> \
  --cli-binary-format raw-in-base64-out \
  --payload file://test_invalid_order.json \
  response_after_invalid.json && \
cat response_after_invalid.json

7. リソースのクリーンアップ

cdk destroy -f

Parser 機能を使用するメリット

型安全性とコード品質の向上

  • コンパイル時エラー検出: 型ヒントによる早期エラー発見
  • 自動補完とIDE支援: 開発効率の大幅な向上

宣言的なデータ検証

  • 複雑なバリデーションルール: フィールドレベル・モデルレベルでの詳細検証
  • ユーザーフレンドリーなエラーメッセージ: 分かりやすいバリデーションエラー

自動化とメンテナンス性

  • Built-in Envelopes: AWS イベントソースの自動処理
  • コードの簡素化: 手動バリデーション処理の大幅削減

Validation 機能との違いと使い分け

Powertools for AWS Lambda には Parser 機能と Validation 機能の両方が提供されており、それぞれ異なるアプローチでデータ検証を行います。

Parser 機能の特徴

  • Pydantic ベース: Python クラスによるモデル定義
  • 型安全性: 実行時の型チェックと自動補完
  • オブジェクト指向: 検証済みデータをオブジェクトとして扱える
  • パフォーマンス: Rust ベースの高速処理
  • 依存関係: Pydantic ライブラリが必要 (パッケージサイズ増加)

Validation 機能の特徴

  • JSON Schema ベース: 標準的な JSON Schema による検証
  • 軽量: 追加依存関係なし
  • 柔軟性: 外部スキーマファイルの参照が可能
  • 標準準拠: 業界標準の JSON Schema 仕様
  • データ型: 検証後も辞書型として扱う

使い分けの指針

Parser 機能を選ぶべき場合:

  • 型安全性を重視する開発
  • 複雑なデータ構造やビジネスロジック
  • IDE の自動補完機能を活用したい
  • オブジェクト指向的なアプローチを好む

Validation 機能を選ぶべき場合:

  • パッケージサイズを最小限に抑えたい
  • JSON Schema の既存資産を活用したい
  • 軽量で高速な検証が必要
  • 標準的な検証仕様に準拠したい

まとめ

本記事では、Powertools for AWS Lambda (Python) の Parser 機能について紹介しました。

Pydantic v2 による強力なデータ検証、Built-in Envelopes による自動的なイベント処理、型安全性の確保など、プロダクションレベルでの Lambda 関数開発において非常に有用だと思います。

特に複雑なデータ構造を扱う API や、厳密な型チェックが必要なビジネスロジックにおいて、Parser 機能は開発効率と品質の両面で大きなメリットをもたらします。

Powertools for AWS Lambda に興味のある方、Lambda 関数でのデータ解析・検証に課題を感じている方の参考になれば嬉しいです。

今後も Powertools for AWS Lambda の各種機能についてブログを書いていく予定ですので、お楽しみに。

参考資料

最後に

株式会社 WHERE (旧: 株式会社 Penetrator) は、シリーズ A ラウンドにおいて総額 5.5 億円の資金調達を実施し、不動産テック業界における更なる成長を目指して採用活動を一層強化しています。

エンジニア, デザイナー, カスタマーサクセス, BizDev, 営業, マーケティングなど、事業拡大を支える多様なポジションで共に挑戦していただける方を待っています!!

▽ 会社のカルチャーを知りたい方はこちら

https://www.wantedly.com/companies/company_9924832

▽ 募集職種を知りたい方はこちら

https://hrmos.co/pages/where/jobs

Discussion