kakakakakku blog

Weekly Tech Blog: Keep on Learning!

JSON Schema で簡単にバリデーションを実装できる Powertools for AWS Lambda (Python) の Validation

Powertools for AWS Lambda (Python)「Validation」を使うと AWS Lambda 関数に渡されたイベント情報のバリデーションを JSON Schema に沿って実現できる.例えば,必須パラメータ・文字数制限・ENUM・正規表現などをチェックできる👌

Powertools for AWS Lambda (Python) 自体は Tracer / Logger / Event Source Data Classes などをよく使うけど,Validation は今まで活用できてなく,試してみたらとても便利だったので,今回試した結果を簡単にまとめておく \( 'ω')/

docs.powertools.aws.dev

検証環境

今回は AWS SAM を使って Amazon API Gateway (REST API) と AWS Lambda 関数を構築する.あくまでサンプルとして Amazon API Gateway の / に POST リクエストを送るとバリデーションロジックを含んだ AWS Lambda 関数が実行されるようにした.また Powertools は Lambda Layer でセットアップする.

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: powertools-validation
      CodeUri: src/
      Handler: app.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Layers:
        - arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:67
      Events:
        Api:
          Type: Api
          Properties:
            Path: /
            Method: POST

最終的なディレクトリ構成は以下のようになる💡

├── events
│   └── event.json
├── samconfig.toml
├── src
│   └── app.py
└── template.yaml

👾 app.py

今回はバリデーションを紹介するサンプルとして,特にロジックはなく Amazon API Gateway への POST リクエストに対して 200 OK もしくは 400 BAD_REQUEST を返す実装にした.また AWS Lambda 関数のベストプラクティスを参考に Handler とロジックを分割して main() にまとめてある(実際にはもっと細かく分割しても良さそう).

そして,今回は超簡易的な「TODO アプリ」を例として,title / category / description / link というパラメータを受け取る API のバリデーションを実装した.パラメータごとのバリデーション要件は以下のコードの SCHEMA を見てもらえればと❗️さらに Powertools for AWS Lambda (Python) の Validation ではバリデーションロジックを @validator デコレータを使った実装と validate() 関数を使った実装から選べる.どちらも試してみて,個人的には以下の2つの理由から validate() 関数を使うのが良いと思った💡

  • validate() 関数は Lambda コンテキストに依存してなくてローカル開発がしやすかった
  • 例外発生時のハンドリングなどを柔軟に実装しやすかった

また validate() 関数を呼び出すときに envelope を指定できて,イベントオブジェクトの中からバリデーションする箇所を限定できる.今回は Amazon API Gateway (REST API) から渡されるイベントをバリデーションするため,envelopes.API_GATEWAY_REST を指定した.すると自動的に body がバリデーション対象になる👌現状は8種類の envelope が提供されている \( 'ω')/

  • API_GATEWAY_HTTP
  • API_GATEWAY_REST
  • CLOUDWATCH_EVENTS_SCHEDULED
  • CLOUDWATCH_LOGS
  • EVENTBRIDGE
  • KINESIS_DATA_STREAM
  • SNS
  • SQS
import json
from aws_lambda_powertools.utilities.validation import SchemaValidationError, envelopes, validate
from http import HTTPStatus
from jmespath.exceptions import JMESPathTypeError

SCHEMA = {
    '$schema': 'http://json-schema.org/draft-07/schema',
    'type': 'object',
    'required': ['title', 'category'],
    'properties': {
        'title': {
            'type': 'string',
            'pattern': '^[a-zA-Z0-9_]*$'
        },
        'category': {
            'type': 'string',
            'enum': ['Python', 'Go', 'Java']
        },
        'description': {
            'type': 'string',
            'minLength': 10,
            'maxLength': 100
        },
        'link': {
            'type': 'string',
            'format': 'uri'
        }
    },
}


def main(event):
    try:
        validate(event=event, schema=SCHEMA, envelope=envelopes.API_GATEWAY_REST)
    except JMESPathTypeError as e:
        return {
            'statusCode': HTTPStatus.BAD_REQUEST,
            'body': json.dumps({'message': str(e)})
        }
    except json.JSONDecodeError as e:
        return {
            'statusCode': HTTPStatus.BAD_REQUEST,
            'body': json.dumps({'message': e.msg})
        }
    except SchemaValidationError as e:
        return {
            'statusCode': HTTPStatus.BAD_REQUEST,
            'body': json.dumps({'message': e.validation_message})
        }

    return {
        'statusCode': HTTPStatus.OK,
        'body': json.dumps({'message': 'ok'})
    }


def lambda_handler(event, context):
    return main(event)


if __name__ == '__main__':
    with open('../events/event.json', 'r') as f:
        event = json.load(f)

    main(event)

動作確認

data must contain ['category', 'title'] properties

event.json

{}

必須の titlecategory プロパティがなくバリデーションエラー🙅‍♂️

$ curl -s -X POST --data @event.json ${ENDPOINT}
{"message": "data must contain ['category', 'title'] properties"}

data.title must match pattern ^[a-zA-Z0-9_]*$

event.json

{
    "title": "Powertoolsを試す",
    "category": "Python"
}

title に日本語を含んでいるためバリデーションエラー🙅‍♂️

$ curl -s -X POST --data @event.json ${ENDPOINT}
{"message": "data.title must match pattern ^[a-zA-Z0-9_]*$"}

data.category must be one of ['Python', 'Go', 'Java']

event.json

{
    "title": "Powertools",
    "category": "AWS"
}

category の ENUM 以外の値を指定しているためバリデーションエラー🙅‍♂️

$ curl -s -X POST --data @event.json ${ENDPOINT}
{"message": "data.category must be one of ['Python', 'Go', 'Java']"}

data.description must be longer than or equal to 10 characters

event.json

{
    "title": "Powertools",
    "category": "Python",
    "description": ""
}

description が10文字以上になっていないためバリデーションエラー🙅‍♂️

$ curl -s -X POST --data @event.json ${ENDPOINT}
{"message": "data.description must be longer than or equal to 10 characters"}

event.json

{
    "title": "Powertools",
    "category": "Python",
    "description": "Try Powertools for AWS Lambda.",
    "link": "docs.powertools.aws.dev"
}

link が URL 形式になっていないためバリデーションエラー🙅‍♂️

$ curl -s -X POST --data @event.json ${ENDPOINT}
{"message": "data.link must be uri"}

まとめ

Powertools for AWS Lambda (Python)「Validation」を使うと AWS Lambda 関数に渡されたイベント情報のバリデーションを柔軟に実装できてとても便利だった❗️

今後は積極的に使っていくぞー \( 'ω')/

関連リンク🔗

github.com