La validación de AWS API Gateway con OpenAPI es una práctica recomendada para garantizar que las peticiones y respuestas cumplen con los estándares esperados. OpenAPI, también conocido como Swagger, es un estándar abierto para describir y documentar APIs. Al utilizar OpenAPI en conjunto con AWS API Gateway, se pueden validar automáticamente las peticiones y respuestas y garantizar que solo se procesan las que cumplen con los estándares esperados.
OpenAPI
Es un estándar para describir la estructura de una API, incluidos los endpoints (puntos finales URL), los formatos de solicitud y respuesta y las operaciones HTTP (GET, POST, PUT, etc) que se implementa en una API generalmente. Este documento también llamado Swagger utiliza el formato JSON o YAML para definir una API. Para más información revisar: OpenAPI
API Gateway y OpenAPI
El servicio API Gateway de AWS es compatible con la especificación OpenAPI, esto permite estructurar información de la API, puntos finales, autorizadores (token y api-keys), esquemas y validar entradas que se realizan.
La validación de las peticiones en AWS API Gateway se puede realizar utilizando los esquemas definidos en el archivo OpenAPI. Los esquemas especifican el formato esperado de las peticiones, incluyendo los parámetros de la URL, los encabezados y el cuerpo de la petición. Al utilizar estos esquemas, se pueden validar automáticamente las peticiones para garantizar que cumplen con los estándares esperados.
Funcionamiento API Gateway
Todo empieza cuando un cliente realiza una petición (GET para este ejemplo) hacia nuestra Api Gateway. Api Gateway lo que hace es crear un objeto json típico de una petición HTTP, y mediante una integración que se especifica en la plantilla OpenApi, realiza un POST a un servicio de AWS (una lambda para este caso). Esto ejecuta la lambda y realiza la transacción, para el retorno de los datos se debe modelar como una respuesta HTTP válida, esto para que lo transforme Api Gateway y pueda retornar la solicitud HTTP de manera exitosa. Mediante la especificación de Open API la idea es realizar una validación a nivel de ese Api Gateway y no en la lambda.
Escenario a probar
Para este caso se valida algunos endpoints implementados en API Gateway, estos endpoints mandan a llamar una función lambda. La configuración de la plantilla del stack y de open api es independiente del lenguaje que se use, para este caso se usa Java.
Código Actual
Se muestra las funciones lambdas en Java para esta prueba:
Función para recuperar todos los clientes:
package com.kc.cloud.labs.aws.lambda.customers;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.kc.cloud.labs.aws.lambda.customers.models.Customer;
import com.kc.cloud.labs.aws.lambda.customers.services.CustomerService;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
public class LabsCustomersGETAll implements RequestHandler
<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final Logger logger = Logger.getLogger(LabsCustomersGETAll.class.getName());
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
logger.info("LabsCustomersGETAll.handleRequest() invoked");
List<Customer> customers = CustomerService.getAllCustomers();
response.setHeaders(getHeaders());
response.setBody(getBody(customers));
response.setStatusCode(200);
return response;
}
public String getBody(List<Customer> customers){
StringBuilder customersJson = new StringBuilder("[");
for (Customer customer : customers) {
customersJson.append(customer.toJSON()).append(",");
}
customersJson.deleteCharAt(customersJson.length() - 1);
customersJson.append("]");
return customersJson.toString();
}
public HashMap<String, String> getHeaders() {
HashMap<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
return headers;
}
}
Función para recuperar un Cliente por id:
package com.kc.cloud.labs.aws.lambda.customers;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.kc.cloud.labs.aws.lambda.customers.models.Customer;
import com.kc.cloud.labs.aws.lambda.customers.services.CustomerService;
import java.util.HashMap;
import java.util.logging.Logger;
public class LabsCustomersGETById implements RequestHandler
<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final Logger logger = Logger.getLogger(LabsCustomersGETById.class.getName());
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
logger.info("LabsCustomersGETById.handleRequest() invoked");
String id = input.getPathParameters().get("id");
Customer customer = CustomerService.getCustomerById(Integer.parseInt(id));
if (customer == null) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(404)
.withBody("{\"message\": \"Customer not found\"}");
}
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withHeaders(getHeaders())
.withBody(customer.toJSON());
}
public HashMap<String, String> getHeaders() {
HashMap<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
return headers;
}
}
Función para crear un Cliente
package com.kc.cloud.labs.aws.lambda.customers;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.kc.cloud.labs.aws.lambda.customers.models.Customer;
import com.kc.cloud.labs.aws.lambda.customers.services.CustomerService;
import java.util.HashMap;
import java.util.logging.Logger;
public class LabsCustomersPSTCreate implements RequestHandler
<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final Logger logger = Logger.getLogger(LabsCustomersPSTCreate.class.getName());
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
logger.info("LabsCustomersPSTCreate.handleRequest() invoked");
String body = input.getBody();
Customer customerCreated = CustomerService.createCustomer(body);
return new APIGatewayProxyResponseEvent()
.withStatusCode(201)
.withHeaders(getHeaders())
.withBody("{\"message\": \"Customer created\", \"customerId\": \"" + customerCreated.getId() + "\"}");
}
public HashMap<String, String> getHeaders() {
HashMap<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
return headers;
}
}
Plantilla para crear el stack SAM yaml
Se tiene una API con tres endpoints una para listar todos los clientes, obtener un cliente y crear un cliente, la idea es validar las solicitudes que se realizan utilizando la especificación Open API que se está hablando.
Se muestra la plantilla SAM para desplegar el stack:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
kc-labs-serverless
Sample SAM Template
Globals:
Function:
CodeUri: kc-labs-app
Timeout: 20
Tracing: Active
Runtime: java11
MemorySize: 512
Architectures:
- x86_64
Api:
TracingEnabled: True
Resources:
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
StageName: v1
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: ./specs/open_api.yaml
LabsCustomersGETAllLambda:
Type: AWS::Serverless::Function
Properties:
Handler: com.kc.cloud.labs.aws.lambda.customers.LabsCustomersGETAll::handleRequest
FunctionName: !Sub ${AWS::StackName}-LabsCustomersGETAll
Environment:
Variables:
PARAM1: VALUE
JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
Events:
EventApiLabsCustomersGETAll:
Type: Api
Properties:
Path: /labs/customers
Method: get
RestApiId: !Ref ApiGatewayApi
LabsCustomersGETByIdLambda:
Type: AWS::Serverless::Function
Properties:
Handler: com.kc.cloud.labs.aws.lambda.customers.LabsCustomersGETById::handleRequest
FunctionName: !Sub ${AWS::StackName}-LabsCustomersGETById
Environment:
Variables:
PARAM1: VALUE
JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
Events:
EventApiLabsCustomersGETById:
Type: Api
Properties:
Path: /labs/customers/{id}
Method: get
RestApiId: !Ref ApiGatewayApi
LabsCustomersPSTCreateLambda:
Type: AWS::Serverless::Function
Properties:
Handler: com.kc.cloud.labs.aws.lambda.customers.LabsCustomersPSTCreate::handleRequest
FunctionName: !Sub ${AWS::StackName}-LabsCustomersPSTCreate
Environment:
Variables:
PARAM1: VALUE
JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
Events:
EventApiLabsCustomersPSTCreate:
Type: Api
Properties:
Path: /labs/customers
Method: post
RestApiId: !Ref ApiGatewayApi
Tomar en cuenta que se crea el recurso para desplegar una Api en AWS, y se especifica la plantilla de Open API para que pueda tomar esa configuración al momento de realizar el despliegue:
Nota: Toda esa implementación de OpenAPI puede ir en la misma plantilla SAM, pero es conveniente separarlo, ya que generalmente la plantilla de OpenAPI es grande.
Se muestra a continuación la plantilla OpenAPI utilizada.
openapi: 3.0.1
info:
title:
Fn::Sub: Labs Validate Request - ${AWS::StackName}
version: 1.0.0
description: |
This API validates the request body and returns the request body as a response.
This API is used for testing the API Gateway Labs.
x-amazon-apigateway-request-validators:
all:
validateRequestBody: true
validateRequestParameters: true
params-only:
validateRequestBody: false
validateRequestParameters: true
paths:
/labs/customers:
get:
summary: Get all customers
description: Get all customers
operationId: LabsCustomersGETAllLambda
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Customer'
x-amazon-apigateway-integration:
type: aws_proxy
timeoutInMillis: 20000
httpMethod: POST
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LabsCustomersGETAllLambda.Arn}/invocations
responses:
default:
statusCode: 200
post:
summary: Get a customer by ID
description: Get a customer by ID
operationId: LabsCustomersPSTCreateLambda
x-amazon-apigateway-request-validator: all
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Customer'
responses:
201:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Customer'
x-amazon-apigateway-integration:
type: aws_proxy
timeoutInMillis: 20000
httpMethod: POST
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LabsCustomersPSTCreateLambda.Arn}/invocations
responses:
default:
statusCode: 201
/labs/customers/{id}:
get:
summary: Get a customer by ID
description: Get a customer by ID
operationId: LabsCustomersGETByIdLambda
x-amazon-apigateway-request-validator: params-only
parameters:
- name: id
in: path
description: Customer ID
required: true
schema:
type: integer
minimum: 1
- name: custom
in: header
description: custom
required: true
schema:
type: boolean
- name: page
in: query
description: Page number
required: true
schema:
type: integer
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Customer'
x-amazon-apigateway-integration:
type: aws_proxy
timeoutInMillis: 20000
httpMethod: POST
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LabsCustomersGETByIdLambda.Arn}/invocations
responses:
default:
statusCode: 200
components:
schemas:
Customer:
type: object
properties:
id:
type: integer
fullName:
type: string
isPremium:
type: boolean
required:
- id
- fullName
- isPremium
Nota: En las plantillas OpenAPI también podemos usar las funciones intrínsecas para inyectar valores de los recursos o del mismo stack.
A continuación se detalla cada una de las partes de esta plantilla mostrada anteriormente:
Información general de la API
Se detalla la información general de la API que se está implementado como el título, la descripción y la versión:
Exensiones para validar las solicitudes
AWS ofrece algunas extensiones que se usan con el estándar OpenAPI y para validar la data entrante de usa x-amazon-apigateway-request-validators, para más información consultar OpenAPI extensiones:
- all: Indica que la validación lo hace en el cuerpo de la solicitud y en los parámetros (headers, paths y queries)
- params-only: Solo válida los parámetros
Configuración de los endpoints
En esta parte se define los puntos finales, el tipo de solicitud, información general y mediante la extensión x-amazon-apigateway-integration se puede anexar la función lambda a utilizar.
Obtener todos los clientes:
Para este caso no se realiza ninguna validación en la solicitud, simplemente se configura información general del endpoint, las respuestas esperadas y la integración con el servicio de AWS.
Nota: La parte del esquema $ref: '#/components/schemas/Customer', hace referencia a un modelo de dato que se lo define en el mismo documento OpenAPI por lo general aunque se puede tener por archivos separados.
Crear un cliente:
Para este caso se valida el cuerpo de la solicitud y los parámetros.
Obtener un cliente por ID:
Para este caso se valida los parámetros de la solicitud, en este caso se tiene path, header y una query que todas son obligatorias.
Componentes
Los componentes son una manera de modelar datos y ser referenciadas para validar entrada de datos, tomar en cuenta que en este caso todos los campos son obligatorios.
Despliegue de Stack
Una vez realizadas esas configuraciones en las plantillas y creadas las funciones se puede desplegar el stack usando SAM.
sam build
sam deploy
Si es un nuevo stack:
sam deploy --guided
Probando funcionalidad
Después podemos probar los endpoints y ver si la especificación de OpenAPI realizada esta funcionando de manera adecuada:
Obtener todos los clientes:
Recordar que para el endpoint de obtener todos los clientes no se configuró ninguna validación, por lo que funciona sin problemas.
Obtener un cliente por ID:
Para este caso recordar que se configuró la validación a nivel de parámetros, en este caso para los tres tipos, en headers, paths y queries.
Si se enviá todas esas configuraciones la ejecución de la API lo realiza sin problemas.
Pero si faltan algunos de los puntos que son requeridos la petición es rechazada:
Esto quiere decir que la falla fue a nivel de la API Gateway y la lambda nunca ha sido llamada, eso se lo puede comprobar en X-RAY:
Crear un cliente:
Para este caso la validación era en ambos casos, sin embargo solo se envía un cuerpo en la solicitud, que es la información del cliente, se envía un body válido y se ejecuta sin problemas:
Si se envía un campo con un tipo de dato que no está registrado en el componente Customer, la ejecución no lo realiza correctamente:
Conclusiones
- AWS API Gateway con OpenAPI es una excelente manera de garantizar que las peticiones y respuestas cumplen con los estándares esperados y de documentar de forma automatizada la API.
- Al utilizar OpenAPI en conjunto con AWS API Gateway, se pueden validar automáticamente las peticiones y respuestas, lo que ayuda a aumentar la confianza en la API y a disminuir los errores.
- No todas las propiedades del esquema OpenAPI son compatibles, en este caso la validación lo realiza solo verificando si el campo requerido efectivamente viene en la solicitud, sin embargo otras propiedades como el maximum o minimum solo se coloca por temas de documentación.
Top comments (0)
Some comments have been hidden by the post's author - find out more