Implement OpenAPI for AWS Lambda in Quarkus Using API Gateway Integrator

About OpenAPI

It is a specification that allows to describe REST API in a standard way. The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for REST APIs. This allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic.

About Swagger 

Swagger is a set of open-source tools built around the OpenAPI Specification that can help software professionals design, build, document, and consume REST APIs. Where OpenAPI deals with specification, Swagger implements the specification to describe a REST API .

Briefing of OpenAPI Specification

Purpose

Main Concepts of Open API

OpenAPI Document

A document (or set of documents) that defines/describes an API. An OpenAPI definition uses and conforms to the OpenAPI Specification. 

Path Templating

Path templating refers to the usage of template expressions, delimited by curly braces ({}), to mark a section of a URL path as replaceable using path parameters.

Each template expression in the path MUST correspond to a path parameter that is included in the Path Item itself and/or in each of the Path Item's Operations.

For example, booklist/{author}.

Media Types

Media type definitions are spread across several resources. The media type definitions should be in compliance with RFC6838.

Some examples of possible media type definitions:

HTTP Status Codes

The HTTP Status Codes are used to indicate the status of the executed operation. The available status codes are defined by RFC7231, and registered status codes are listed in the IANA Status Code Registry.

Reference Implementation of Open API for AWS Lambda Function 

Reference Architecture

Reference Architecture

The reference implementation consists of the following components:

See the subsequent sections for more details on each architecture component.

About AWS Lambda Function

Lambda is a compute service that allows to run code without provisioning or managing servers. Lambda runs the code on a high-availability compute infrastructure and performs all of the administration of the compute resources, including server and operating system maintenance, capacity provisioning and automatic scaling, and logging. With Lambda, it is possible to run code for virtually any type of application or backend service. All it needs is to supply code in one of the languages that Lambda supports.

Lambda runs function only when needed and scales automatically, from a few requests per day to thousands per second. It saves compute cost as it is needed to pay only for the compute time that the function consumes during runtime — there is no charge when the code is not running. The lambda function can be invoked using the Lambda API or events from other AWS services, like as and when the object is created in the S3 bucket.

About AWS API Gateway 

Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. APIs act as the "front door" for applications to access data, business logic, or functionality from backend services.

About AWS API Integration Function

This type of integration lets an API expose AWS service actions. In AWS integration, it is required to configure both the integration request and integration response and set up necessary data mappings from the method request to the integration request and from the integration response to the method response.

It allows specified details of the backend integration used for this method. This extension is an extended property of the OpenAPI Operation object. The result is an API Gateway integration object.

Important Property

The type of integration with the specified backend. Valid values are:

The Lambda proxy integration supports a streamlined integration setup for a single Lambda function. The setup is simple and can evolve with the backend without having to tear down the already-created setup. For these reasons, it is highly recommended for integration with a Lambda function.

In contrast, the Lambda custom integration allows for the reuse of configured mapping templates for various integration endpoints that have similar requirements of the input and output data formats. The setup is more involved and is recommended for more advanced application scenarios.

x-amazon-apigateway-integrations Example

The following example defines  HTTP APIs (One GET and One POST) using the OpenAPI standard and uses one API gateway integration per API to integrate with Lambda Function (By referring to the ARN of function). It uses AWS Identity and Access Management (IAM) Role as a credential for integration. The below format is in YAML. 

YAML
 
openapi: 3.0.1
info:
  description: "This is a definition of Proxy Pattern Service. The service has 2 APIs."
  version: v1
  title: "Proxy Pattern Service"
paths:
  /v1/proxypattern/employee:
    options:
      summary: CORS support
      description: |
        Enable CORS by returning correct headers
      consumes:
        - application/json
      produces:
        - application/json
      tags:
        - CORS
      security:
        - NONE: []
      x-amazon-apigateway-integration:
        type: mock
        requestTemplates:
          application/json: |
            {
              "statusCode" : 200
            }
        responses:
          "default":
            statusCode: "200"
            responseParameters:
              method.response.header.Access-Control-Allow-Headers : "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,x-apigw-api-id,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods : "'*'"
              method.response.header.Access-Control-Allow-Origin : "'*'"
            responseTemplates:
              application/json: |
                {}
      responses:
        200:
          description: Default response for CORS method
          headers:
            Access-Control-Allow-Headers:
              type: "string"
            Access-Control-Allow-Methods:
              type: "string"
            Access-Control-Allow-Origin:
              type: "string"
    post:
      summary: "Save Employee"
      operationId: "saveemployees"
      tags:
        - saveemployees
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/employee'
      responses:
        '200':
          description: "Saved Employee Successfully"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/employee"
        '400':
          description: "Application Errors"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

        '500':
          description: "Other unspecified Errors"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

      x-amazon-apigateway-integration:
        credentials:
          Fn::Sub: arn:aws:iam::${AWS::AccountId}:role/delegate-admin-lambda-proxy-pattern-role
        httpMethod: "POST"
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ProxyPatternService.Arn}:live/invocations
        responses:
          "default":
            statusCode: "200"
            responseParameters:
              method.response.header.Access-Control-Allow-Origin : "'*'"
          "BAD.*":
            statusCode: "400"
            responseParameters:
              method.response.header.Access-Control-Allow-Origin : "'*'"
          "INT.*":
            statusCode: "500"
            responseParameters:
              method.response.header.Access-Control-Allow-Origin : "'*'"
        type: "aws_proxy"
    get:
      summary: "Get All the Employees"
      operationId: "getemployees"
      tags:
        - getemployees
      responses:
        '200':
          description: "All The Employees retrieved successfully"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/employee"
        '204':
          description: "Employees not found"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
        '500':
          description: "Other unspecified Errors"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

      x-amazon-apigateway-integration:
        credentials:
          Fn::Sub: arn:aws:iam::${AWS::AccountId}:role/delegate-admin-lambda-proxy-pattern-role
        httpMethod: "POST"
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ProxyPatternService.Arn}:live/invocations
        responses:
          "default":
            statusCode: "200"
            responseParameters:
              method.response.header.Access-Control-Allow-Origin : "'*'"
          "BAD.*":
            statusCode: "204"
            responseParameters:
              method.response.header.Access-Control-Allow-Origin : "'*'"
          "INT.*":
            statusCode: "500"
            responseParameters:
              method.response.header.Access-Control-Allow-Origin : "'*'"
        type: "aws_proxy"

components:
  schemas:
    Employee:
      type: object
      description: EMPLOYEE
      properties:
        employeeId:
          type: string
          description: Id of the Employee
        employeeName:
          type: string
          description: Name of the Employee
      required:
        - employeeId
    ErrorResponse:
      type: "object"
      properties:
        errorCode:
          type: string
          description: |
            indicates an error in processing.
            XXXX - Error in saving message
        errorMessage:
          type: string
          description: "message description of error."


Below is the Cloud Formation Template for AWS Lambda Function, which is to be exposed through AWS API Gateway. The format is  YAML. Here, the OpenAPI definition file has been referenced  (/openapi-apigateway-PxyPtrnSvc.yaml) so that API Gateway is aware of the integration points defined via the OpenAPI definition file.

YAML
 
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AWS Serverless Quarkus HTTP - Proxy Pattern Poc Service
Globals:
  Api:
    EndpointConfiguration:
      Type: PRIVATE
    Auth:
      ResourcePolicy:
        CustomStatements: {
          Effect: 'Allow',
          Action: 'execute-api:Invoke',
          Resource: ['execute-api:/*/*/*'],
          Principal: '*'
        }
Resources:
  ProxyPatternServiceAWSApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      TracingEnabled: true
      Name: ProxyPatternServiceSvcApiGateway
      StageName: dev
      DefinitionBody:
        'Fn::Transform':
          Name: 'AWS::Include'
          Parameters:
            Location: './openapi-apigateway-PxyPtrnSvc.yaml'
  ProxyPatternServiceLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: '/aws/lambda/ProxyPatternService'
      RetentionInDays: 30

  ProxyPatternService:
    Type: AWS::Serverless::Function
    Properties:
      Handler: not.used.in.provided.runtime
      Runtime: provided
      CodeUri: component1/function.zip
      MemorySize: 512
      Timeout: 900
      FunctionName: ProxyPatternService
      Environment:
        Variables:
          LOG_LEVEL: INFO
          DISABLE_SIGNAL_HANDLERS: true
      Role: !Sub "arn:aws:iam::${AWS::AccountId}:role/delegate-admin-lambda-proxy-pattern-role"
      Tracing: Active
Outputs:
  ProxyPatternServiceAWSApiGateway:
    Description: 'API Gateway endpoint URL for dev stage for Proxy Pattern Service Template'
    Value: !Sub 'https://${ProxyPatternServiceAWSApiGateway}.execute-api.${AWS::Region}.amazonaws.com/dev/'
  ProxyPatternServiceAWSApiGatewayRestApiId:
    Description: 'API Gateway ARN for Basic AWS API Gateway'
    Value: !Ref ProxyPatternServiceAWSApiGateway
    Export:
      Name: ProxyPatternServiceAWSApiGateway-RestApiId
  ProxyPatternServiceAWSApiGatewayRootResourceId:
    Value: !GetAtt ProxyPatternServiceAWSApiGateway.RootResourceId
    Export:
      Name: ProxyPatternServiceAWSApiGateway-RootResourceId


The example API definition can be related to below Controller interface which uses Java and can be implemented in Java Quarkus:

Java
 
package com.example.proxypattern.controller;

import com.example.proxypattern.exception.model.ErrorResponse;
import com.example.proxypattern.model.Employee;

import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("v1/proxypattern/")
public interface IProxyPatternController {

    @GET
    @Path("/employee")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Get All the Employees", description = "getEmployees")
    @APIResponses(value = {
            @APIResponse(responseCode = "204", description = "Employees not found"),
            @APIResponse(responseCode = "200", description = "All The Employees retrieved successfully", content = @Content(schema = @Schema(implementation = Employee.class))),
            @APIResponse(responseCode = "500", description = "Other unspecified Errors", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) })
    public Response getAllEmployee();

    @POST
    @Path("/employee")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(summary = "Save Employee", description = "saveEmployees")
    @APIResponses(value = {
            @APIResponse(responseCode = "400", description = "Application Errors", content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
            @APIResponse(responseCode = "200", description = "Saved Employee Successfully", content = @Content(schema = @Schema(implementation = Employee.class))),
            @APIResponse(responseCode = "500", description = "Other unspecified Errors", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) })
    public Response saveEmployee(Employee employee);
}


The implementation language can vary. Java Quarkus can be selected as the implementation language for the Lambda function if the technical decision is to use Java as the coding language . Lambda supports custom runtime, and hence Quarkus code can be built natively to resolve response time issues related to Lambda Cold start.

The controller implementation code can look like the below. Here, the business logic has been abstracted via a business service class:

Java
 
package com.example.proxypattern.controller.impl;

import com.example.proxypattern.controller.IProxyPatternController;
import com.example.proxypattern.model.Employee;
import com.example.proxypattern.service.impl.EmployeeServiceImpl;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.opentracing.Traced;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.core.Response;import java.util.List;

@Slf4j
@Traced
@Singleton
public class ProxyPatternControllerImpl implements IProxyPatternController {

    @Inject
    EmployeeServiceImpl employeeService;

    @Override
    public Response getAllEmployee() {
        Response.Status status = Response.Status.OK;
        //The business logic is abstracted here in Service code
        List<Employee> listOfEmployee = employeeService.getAllEmployee();
        if(listOfEmployee.isEmpty()){
            status = Response.Status.NO_CONTENT;
        }
        log.info("Sending employee {}", listOfEmployee);
        return Response.status(status).entity(listOfEmployee).build();
    }

    @Override
    public Response saveEmployee(Employee employee) {
    	//The business logic is abstracted here in Service code
        Employee employeeResponse = employeeService.saveEmployee(employee);
        return Response.status(Response.Status.CREATED).entity(employeeResponse).build();
    }
}


Maven can be used to build the project, and the below dependencies can also be used to build the project:

XML
 
 <dependencies>
      
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy</artifactId>
        </dependency>

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-amazon-lambda-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-smallrye-openapi</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy-jackson</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-logging-json</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.slf4j</groupId>
            <artifactId>slf4j-jboss-logmanager</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-hibernate-validator</artifactId>
        </dependency>

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-smallrye-opentracing</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-smallrye-health</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5-mockito</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.agent</artifactId>
            <classifier>runtime</classifier>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>


Conclusion

AWS API Gateway integration makes it easy to implement REST API using Lambda Function, which conforms to OpenAPI standard and makes it easy to integrate the same with AWS API Gateway.

 

 

 

 

Top