API Gateway

One of the most powerful ways to use AWS Lambda is to make function publicly available over HTTPS. This is accomplished by connecting the AWS Lambda function with the API Gateway. In this section we’ll start with a simple “echo” example and move on to a lambda function that accepts user parameters and returns an expiring S3 URL.

  • Echo
  • To start, we’ll create a HTTPS accessible lambda function that simply echoes back the contents of incoming API Gateway Lambda event. The source for this is the SpartaHTML. For reference, the helloWorld function is below. import ( awsLambdaEvents "github.com/aws/aws-lambda-go/events" spartaAPIGateway "github.com/mweagle/Sparta/aws/apigateway" ) func helloWorld(ctx context.Context, gatewayEvent spartaAWSEvents.APIGatewayRequest) (*spartaAPIGateway.Response, error) { logger, loggerOk := ctx.Value(sparta.ContextKeyLogger).(*logrus.Logger) if loggerOk { logger.Info("Hello world structured log message") } // Return a message, together with the incoming input.

  • Request Parameters
  • Request Parameters This example demonstrates how to accept client request params supplied as HTTP query params and return an expiring S3 URL to access content. The source for this is the s3ItemInfo function defined as part of the SpartaApplication. Lambda Definition Our function will accept two params: bucketName : The S3 bucket name storing the asset keyName : The S3 item key Those params will be passed as part of the URL query string.

  • Request Context
  • This example demonstrates how to use the Context struct provided as part of the APIGatewayRequest. The SpartaGeoIP service will return Geo information based on the inbound request’s IP address. Lambda Definition Our function will examine the inbound request, lookup the user’s IP address in the GeoLite2 Database and return any information to the client. As this function is only expected to be invoked from the API Gateway, we’ll unmarshall the inbound event:

  • CORS
  • Cross Origin Resource Sharing defines a protocol by which resources on different domains may establish whether cross site operations are permissible. Sparta makes CORS support a single CORSEnabled field of the API struct: // Register the function with the API Gateway apiStage := sparta.NewStage("v1") apiGateway := sparta.NewAPIGateway("SpartaHTML", apiStage) // Enable CORS s.t. the S3 site can access the resources apiGateway.CORSEnabled = true Setting the boolean to true will add the necessary OPTIONS and mock responses to all resources exposed by your API.

  • Slack SlashCommand
  • In this example, we’ll walk through creating a Slack Slash Command service. The source for this is the SpartaSlackbot repo. Our initial command handler won’t be very sophisticated, but will show the steps necessary to provision and configure a Sparta AWS Gateway-enabled Lambda function. Define the Lambda Function This lambda handler is a bit more complicated than the other examples, primarily because of the Slack Integration requirements. The full source is:

  • S3 Sites with CORS
  • Sparta supports provisioning an S3-backed static website as part of provisioning. We’ll walk through provisioning a minimal Bootstrap website that accesses API Gateway lambda functions provisioned by a single service in this example. The source for this is the SpartaHTML example application. Lambda Definition We’ll start by creating a very simple lambda function: import ( spartaAPIGateway "github.com/mweagle/Sparta/aws/apigateway" spartaAWSEvents "github.com/mweagle/Sparta/aws/events" ) type helloWorldResponse struct { Message string Request spartaAWSEvents.APIGatewayRequest } //////////////////////////////////////////////////////////////////////////////// // Hello world event handler func helloWorld(ctx context.

Concepts

Before moving on to the examples, it’s suggested you familiarize yourself with the API Gateway concepts.

The API Gateway presents a powerful and complex domain model. In brief, to integrate with the API Gateway, a service must:

  1. Define one or more AWS Lambda functions
  2. Create an API Gateway REST API instance
  3. Create one or more resources associated with the REST API
  4. Create one or more methods for each resource
  5. For each method:
    1. Define the method request params
    2. Define the integration request mapping
    3. Define the integration response mapping
    4. Define the method response mapping
  6. Create a stage for a REST API
  7. Deploy the given stage

See a the echo example for a complete version.

Request Types

AWS Lambda supports multiple function signatures. Some supported signatures include structured types, which are JSON un/marshalable structs that are automatically managed.

To simplify handling API Gateway requests, Sparta exposes the APIGatewayEnvelope type. This type provides an embeddable struct type whose fields and JSON serialization match up with the Velocity Template that’s applied to the incoming API Gateway request.

Embed the APIGatewayEnvelope type in your own lambda’s request type as in:

type FeedbackBody struct {
  Language string `json:"lang"`
  Comment  string `json:"comment"`
}

type FeedbackRequest struct {
  spartaEvents.APIGatewayEnvelope
  Body FeedbackBody `json:"body"`
}

Then accept your custom type in your lambda function as in:

func myLambdaFunction(ctx context.Context, apiGatewayRequest FeedbackRequest) (map[string]string, error) {
  language := apiGatewayRequest.Body.Language
  ...
}

Response Types

The API Gateway response mappings must make assumptions about the shape of the Lambda response. The default application/json mapping template is:

$input.json('$.body')
## Ok, parse the incoming map of headers
## and for each one, set the override header in the context.
## Ref: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference
## Ref: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-override-request-response-parameters.html
#set($headers = $input.path("$.headers"))##
#foreach($key in $headers.keySet())##
#set($context.responseOverride.header[$key] = $headers[$key])##
#end##
## And use the code rather than integration templates so that
## the creation time is reduced
#if($input.path("$.code") != "")##
#set($context.responseOverride.status = $input.path("$.code"))##
#end##

This template assumes that your response type has the following JSON shape:

{
  "code" : int,
  "body" : {...},
  "headers": {...}
}

The apigateway.NewResponse constructor is a utility function to produce a canonical version of this response shape. Note that header keys must be lower-cased.

To return a different structure change the content-specific mapping templates defined by the IntegrationResponse. See the mapping template reference for more information.

Custom HTTP Headers

API Gateway supports returning custom HTTP headers whose values are extracted from your response. To return custom HTTP headers using the default VTL mappings, provide them as the optional third map[string]string argument to NewResponse as in:

func helloWorld(ctx context.Context,
  gatewayEvent spartaAWSEvents.APIGatewayRequest) (*spartaAPIGateway.Response, error) {

  logger, loggerOk := ctx.Value(sparta.ContextKeyLogger).(*logrus.Logger)
  if loggerOk {
    logger.Info("Hello world structured log message")
  }

  // Return a message, together with the incoming input...
  return spartaAPIGateway.NewResponse(http.StatusOK, &helloWorldResponse{
    Message: fmt.Sprintf("Hello world 🌏"),
    Request: gatewayEvent,
  },
    map[string]string{
      "X-Response": "Some-value",
    }), nil
}

Other Resources