diff --git a/apigw-data-validation-tf/README.md b/apigw-data-validation-tf/README.md new file mode 100644 index 000000000..178876d4d --- /dev/null +++ b/apigw-data-validation-tf/README.md @@ -0,0 +1,101 @@ +# Amazon API Gateway data validation models + +This pattern creates an Amazon API Gateway that handles simple data validation at the endpoint without invoking the Lambda function when the data validation fails. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-custom-resource-policy](https://serverlessland.com/patterns/apigw-custom-resource-policy) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to this pattern's directory + ``` + cd serverless-patterns/apigw-data-validation-tf + ``` +1. From the command line, initialize Terraform to downloads and install the providers defined in the configuration: + ``` + terraform init + ``` +1. From the command line, apply the configuration in the main.tf file: + ``` + terraform apply + ``` +1. Note the outputs from the deployment process. These contain the resource names and/or ARNs which are used for testing. + +## API Endpoint + +After running `terraform apply`, you will see outputs including the API endpoint URL. You'll need this URL for testing. The output will look similar to: +``` +api_endpoint = "https://xxxxx.execute-api.us-east-1.amazonaws.com/Prod" +``` + +Note: When testing, append `/123?order=ORD12345` to this base URL. For example, if your API endpoint is `https://xxxxx.execute-api.us-east-1.amazonaws.com/Prod`, your full testing URL would be: +``` +`https://xxxxx.execute-api.us-east-1.amazonaws.com/Prod/123?order=ORD12345` +``` + +## How it works + +The data model is declared in the API Gateway resource. The Lambda function then requires the request body to be validated against this model. + +## Testing + +After the application is deployed try the following scenarios. + +### Create a new vehicle entering valid data: +``` +curl --location --request POST 'https://t9nde3gpp2.execute-api.us-east-1.amazonaws.com/Prod/123?order=ORD12345' \ +--header 'Content-Type: application/json' \ +--header 'custom-agent: MyMobileApp/1.0' \ +--data-raw '{ + "make":"MINI", + "model":"Countryman", + "year": 2010 +}' +``` +Expected response: `{"message": "Data validation succeded", "data": {"make": "MINI", "model": "Countryman", "year": 2010}}` +### Now enter a year less than 2010 +``` +curl --location --request POST 'https://t9nde3gpp2.execute-api.us-east-1.amazonaws.com/Prod/123?order=ORD12345' \ +--header 'Content-Type: application/json' \ +--header 'custom-agent: MyMobileApp/1.0' \ +--data-raw '{ + "make":"MINI", + "model":"Countryman", + "year": 2002 +}' +``` +Expected response: `{"message": "Invalid request body"}` + +Try some other combinations and see what you get! + +## Cleanup + +1. Change directory to the pattern directory: + ``` + cd apigw-data-validation-tf + ``` +1. Delete all created resources by Terraform + ```bash + terraform destroy + ``` +1. Confirm all created resources has been deleted + ```bash + terraform show + ``` + +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-data-validation-tf/example-pattern.json b/apigw-data-validation-tf/example-pattern.json new file mode 100644 index 000000000..3fcb18d63 --- /dev/null +++ b/apigw-data-validation-tf/example-pattern.json @@ -0,0 +1,59 @@ +{ + "title": "API Gateway data validation", + "description": "Creates an API Gateway with request validation, rejecting invalid requests before Lambda invocation using model schema validation.", + "language": "YAML", + "level": "300", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "The data model is declared in the API Gateway resource. The Lambda function then requires the request body to be validated against this model." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-data-validation-tf", + "templateURL": "serverless-patterns/apigw-data-validation-tf", + "projectFolder": "apigw-data-validation-tf", + "templateFile": "main.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "API Gateway model example", + "link": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-models" + }, + { + "text": "JSON Schema", + "link": "https://datatracker.ietf.org/doc/html/draft-zyp-json-schema-04#section-4.1" + } + ] + }, + "deploy": { + "text": [ + "terraform init", + "terraform apply" + ] + }, + "testing": { + "text": [ + "See the Github repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "terraform destroy", + "terraform show" + ] + }, + "authors": [ + { + "name": "Makendran G", + "image": "https://drive.google.com/file/d/1mUObnbmn52UWL-Zn39EpgpneiBNv3LCN/view?usp=sharing", + "bio": "Cloud Support Engineer @ AWS", + "linkedin": "makendran", + "twitter": "@MakendranG" + } + ] +} diff --git a/apigw-data-validation-tf/main.tf b/apigw-data-validation-tf/main.tf new file mode 100644 index 000000000..1225ff756 --- /dev/null +++ b/apigw-data-validation-tf/main.tf @@ -0,0 +1,206 @@ +# Provider configuration +provider "aws" { + region = "us-east-1" # Change this to your desired region +} + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.0" + } + null = { + source = "hashicorp/null" + version = "~> 3.0" + } + } +} + +# Archive the Lambda function code +data "archive_file" "lambda_zip" { + type = "zip" + source_dir = "${path.module}/src" + output_path = "${path.module}/lambda.zip" +} + +# API Gateway REST API +resource "aws_api_gateway_rest_api" "main_api" { + name = "validation-api" + description = "API Gateway with data validation" + + body = jsonencode({ + openapi = "3.0.1" + info = { + title = "validation-api" + version = "1.0" + } + components = { + schemas = { + Vehicle = { + type = "object" + required = ["make", "model", "year"] + properties = { + make = { + type = "string" + } + model = { + type = "string" + } + year = { + type = "integer" + minimum = 2010 + } + color = { + type = "string" + enum = ["green", "red", "blue"] + } + } + } + } + } + }) +} + +# Lambda Function +resource "aws_lambda_function" "process_function" { + filename = data.archive_file.lambda_zip.output_path + source_code_hash = data.archive_file.lambda_zip.output_base64sha256 + function_name = "process-function" + role = aws_iam_role.lambda_role.arn + handler = "app.lambda_handler" + runtime = "python3.13" + architectures = ["arm64"] + timeout = 3 + + depends_on = [ + data.archive_file.lambda_zip + ] +} + +# IAM Role for Lambda +resource "aws_iam_role" "lambda_role" { + name = "process_function_role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +# Basic Lambda execution policy +resource "aws_iam_role_policy_attachment" "lambda_basic" { + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.lambda_role.name +} + +# API Gateway Resource +resource "aws_api_gateway_resource" "api_resource" { + rest_api_id = aws_api_gateway_rest_api.main_api.id + parent_id = aws_api_gateway_rest_api.main_api.root_resource_id + path_part = "{id}" +} + +# API Gateway Method +resource "aws_api_gateway_method" "api_method" { + rest_api_id = aws_api_gateway_rest_api.main_api.id + resource_id = aws_api_gateway_resource.api_resource.id + http_method = "POST" + authorization = "NONE" + + request_parameters = { + "method.request.querystring.order" = true + "method.request.header.custom-agent" = true + } + + request_validator_id = aws_api_gateway_request_validator.validator.id + request_models = { + "application/json" = aws_api_gateway_model.vehicle_model.name + } +} + +# Request Validator +resource "aws_api_gateway_request_validator" "validator" { + name = "validator" + rest_api_id = aws_api_gateway_rest_api.main_api.id + validate_request_body = true + validate_request_parameters = true +} + +# Vehicle Model +resource "aws_api_gateway_model" "vehicle_model" { + rest_api_id = aws_api_gateway_rest_api.main_api.id + name = "Vehicle" + description = "Vehicle model for validation" + content_type = "application/json" + + schema = jsonencode({ + type = "object" + required = ["make", "model", "year"] + properties = { + make = { + type = "string" + } + model = { + type = "string" + } + year = { + type = "integer" + minimum = 2010 + } + color = { + type = "string" + enum = ["green", "red", "blue"] + } + } + }) +} + +# Lambda Integration +resource "aws_api_gateway_integration" "lambda_integration" { + rest_api_id = aws_api_gateway_rest_api.main_api.id + resource_id = aws_api_gateway_resource.api_resource.id + http_method = aws_api_gateway_method.api_method.http_method + type = "AWS_PROXY" + integration_http_method = "POST" + uri = aws_lambda_function.process_function.invoke_arn +} + +# Lambda Permission +resource "aws_lambda_permission" "api_gateway" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.process_function.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.main_api.execution_arn}/*/*/*" +} + +# API Gateway Deployment +resource "aws_api_gateway_deployment" "api_deployment" { + rest_api_id = aws_api_gateway_rest_api.main_api.id + depends_on = [aws_api_gateway_integration.lambda_integration] +} + +# API Gateway Stage +resource "aws_api_gateway_stage" "prod" { + deployment_id = aws_api_gateway_deployment.api_deployment.id + rest_api_id = aws_api_gateway_rest_api.main_api.id + stage_name = "Prod" +} + +# Output +output "api_endpoint" { + description = "API Gateway endpoint URL for Prod stage" + value = "${aws_api_gateway_stage.prod.invoke_url}" +} diff --git a/apigw-data-validation-tf/src/app.py b/apigw-data-validation-tf/src/app.py new file mode 100644 index 000000000..ab542c439 --- /dev/null +++ b/apigw-data-validation-tf/src/app.py @@ -0,0 +1,9 @@ +import json +def lambda_handler(event, context): + return { + "statusCode": 200, + "body": json.dumps({ + "message": "Data validation succeded", + "data": json.loads(event["body"]) + }), + } \ No newline at end of file