Skip to content
sapessi edited this page Jun 20, 2018 · 23 revisions

AWS Serverless Java Container makes it easy to run your Spring, Jersey, or Spark applications using AWS Lambda and Amazon API Gateway. The library defines a set of interfaces and abstract classes required to support other frameworks in the future. Serverless Java Container starts each framework acting as a container, such as Tomcat, and translates API Gateway proxy events into the request format accepted by the underlying framework, such as an HttpServletRequest or ContainerRequest. HTTP responses from the frameworks are translated in the object structure API Gateway expects as a return value from Lambda.

Quick starts

  • Spring - aws-serverless-java-container-spring
  • Spring Boot - aws-serverless-java-container-spring
  • Jersey - aws-serverless-java-container-jersey
  • Spark - aws-serverless-java-container-spark

How it works

The primary purpose of the library is to act as a container; it receives events object from Lambda and translates them to a request object for the framework. Similarly, it translates responses from the framework into valid return values for API Gateway.

Lambda handler classes

The framework can be used with both POJO and stream handlers. For applications that leverage context values from custom authorizers, we recommend using a stream handler: The framework uses Jackson's @JsonAnySetter/Getter annotations to extract custom values from the authorizer context, the serializer included in AWS Lambda does not process annotated fields. In all our samples, we use the RequestStreamHandler interface and the proxyStream method of the Serverless Java Container library. With a POJO-based handler, you can use the proxy method of the handler object directly.

This is the basic example of a stream handler using Jersey:

public class StreamLambdaHandler implements RequestStreamHandler {
    private static final ResourceConfig jerseyApplication = new ResourceConfig()
                                                             .packages("com.amazonaws.serverless.sample.jersey")
                                                             .register(JacksonFeature.class);
    private static final JerseyLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler
            = JerseyLambdaContainerHandler.getAwsProxyHandler(jerseyApplication);

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context);

        // just in case it wasn't closed by the mapper
        outputStream.close();
    }
}

In the example above, and all other sample applications, the main LambdaContainerHandler is declared in a static block as a class member. This is because static variables are initialized with the runtime (the JVM) as AWS Lambda launches our function, which gives us better performance.

The samples directory in the repository contains a sample pet store application for each framework. All of the samples include a stream handler as well as a SAM template for deployment.

Deploying the sample applications

The samples folder includes a simple pet store application implemented with each framework supported by this library. With each application, we have included its maven pom.xml file as well as a SAM template.

  • Using a shell, navigate to the folder for the sample application
$ cd ~/library-folder/samples/spark/pet-store
  • In the root folder of the application, run the package command to build a jar
$ mvn package
  • Once the build completes, use the AWS CLI to pre-package the SAM template for deployment. You will need an S3 bucket in your deployment region for this step to work
$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket my-deployment-bucket
  • The package step uploads your built jar to the deployment bucket and generates an output SAM template with the correct values. With the AWS CLI, run the deploy command to create a new CloudFormation stack for your API.
$ aws cloudformation deploy --template-file output-sam.yaml --stack-name MySampleStack --capabilities CAPABILITY_IAM 
  • The deployed SAM template outputs the API endpoint you can use to test the sample app. Run the following command to extract the endpoint.
$ aws cloudformation describe-stacks --stack-name MySampleStack
...
"Outputs": [
  {
    "Description": "URL for application", 
    "ExportName": "SparkPetStoreApi", 
    "OutputKey": "SparkPetStoreApi", 
    "OutputValue": "https://xxxxxxxxx.execute-api.xx-xxxx-x.amazonaws.com/Prod/pets"
  }
], 
...
  • With the output endpoint, run a curl command to test that your API is working as expected.
$ curl https://xxxxxxxxx.execute-api.xx-xxxx-x.amazonaws.com/Prod/pets?limit=2 | python -m json.tool
[
    {
        "breed": "Dalmatian",
        "dateOfBirth": 1126208154522,
        "id": "9de24d7a-cdeb-41a9-ac9a-11bd501db728",
        "name": "Jack"
    },
    {
        "breed": "Jack Russell Terrier",
        "dateOfBirth": 1375040154522,
        "id": "1c49bb0e-a2bd-4807-b19f-6581f5fb79f7",
        "name": "Lily"
    }
]

Security and API Gateway context

API Gateway supports authentication and authorization using IAM credentials (SigV4) or bearer tokens via Cognito User Pools or custom authorizers.

The library contains a default implementation of the SecurityContextWriter that supports API Gateway's proxy integration. The generated security context uses the API Gateway $context object to establish the request security context.

The Principal object is populated for all requests in the SecurityContext and can be retrieved from the ServletRequests and Jersey's ContainerRequest or injected in an object.

With Jersey, you can inject the SecurityContext using the @Context annotation.

@Path("/test") @GET
public String testPrincipal(@Context SecurityContext securityContext) {
    Principal principal = securityContext.getUserPrincipal();

    // the possible values for the authentication scheme are 
    // 1. CUSTOM_AUTHORIZER
    // 2. COGNITO_USER_POOL
    // 3. AWS_IAM
    // These are defined as constants in the AwsProxySecurityContext object
    String authScheme = securityContext.getAuthenticationScheme();
}

For servlet-based implementations such as Spring and Spark, you can retrieve the principal from the HttpServletRequest object using the getUserPrincipal() method.

@RequestMapping(path = "/test", method=RequestMethod.GET)
public String test(HttpServletRequest request, ServletResponse response) {
    Principal principal = request.getUserPrincipal();
    return "Hello, " + principal.getName() + "!";
}

Behind the scenes, for requests authorized via IAM credentials, all information about the user is available in the ApiGatewayRequestContext object and its identity property. Custom authorizer data, including any custom values, are stored in the ApiGatewayAuthorizerContext object.

Context information that are not part of the standard HTTP request, such as the Cognito identity or custom authorizer claims, are stored in request attributes by the RequestReader object. From your implementations, you can access this data using the getAttribute(String) method of the request object. The example below extracts the API Gateway context property from the request and reads the "picture" value from the custom authorizer claims.

get("/pets", (req, res) -> {
    ApiGatewayRequestContext ctx = (ApiGatewayRequestContext)req.raw().getAttribute(API_GATEWAY_CONTEXT_PROPERTY);
    ApiGatewayAuthorizerContext authCtx = ctx.getAuthorizer();
    String picture = authCtx.getContextValue("picture");
});

Servlet filters

You can register Filter implementations by implementing a StartupsHandler as defined in the AwsLambdaServletContainerHandler class. The onStartup methods receives a reference to the current ServletContext.

handler.onStartup(c -> {
    FilterRegistration.Dynamic registration = c.addFilter("CustomHeaderFilter", CustomHeaderFilter.class);
    // update the registration to map to a path
    registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
    // servlet name mappings are disabled and will throw an exception
});

Custom domain names

When using this framework with a custom domain name, you need to explicitly enable the domain name in the ContainerConfig object.

LambdaContainerHandler.getContainerConfig().addCustomDomain("api.myserver.com");

Unless the custom domain name is explicitly enabled, the getServerName() method of the HttpServletRequest object will return the default API Gateway domain.

Request logging

The Serverless Java Container library can log requests to the function's CloudWatch log stream. To format the log for each request, the library relies on implementations of the LogFormatter interface. By default, we include an implementation of the interface that generates Apache combined logs. The object is instantiated automatically by the servlet implementation of the library. You can override the formatter to create your own custom log lines using the setLogFormatter( ) method of the ContainerHandler class of your choice.

Container configuration

The library includes a ContainerConfig object. When the handler object is initialized, the config is set to its default values. You can change the configuration by retrieving the singleton config from the LambdaContainerHandler object.

LambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(true);

The configuration variables are:

  • serviceBasePath - The value of this propety tells the library whether the application is running under a base path. For example, you could have configured your API Gateway to have a /orders/{proxy+} and a /catalog/{proxy+} resource. Each resource is handled by a separate Lambda functions. For this reason, the application inside Lambda may not be aware of the fact that the /orders path exists. Use the serviceBasePath property in conjuction with the stripBasePath property to remove the /orders prefix when routing requests inside the application. Defaults to null.
  • stripBasePath - Tells the library to remove the base path specified in the serviceBasePath property before sending the HTTP request for routing to the underlying framework. Defaults to false.
  • uriEncoding - Specifies the charset to use when encoding uri components. Defaults to UTF-8.
  • consolidateCookieHeaders - API Gateway only supports a single header value per key. Some applications could return multiple Set-Cookie header in a response. This property tells the framework to group all of the values for the various Set-Cookie headers into a single Set-Cookie header separating them with ,. Defaults to true.
  • useStageAsServletContext - Tells the framework to include the API Gateway stage path in the HttpServletRequest context path property. Defaults to false.
  • addValidPath - Adds a supported base path to the container. This can be used for API Gateway custom domain base path mappings.
  • addCustomDomainName - Adds a supported custom domain name. Domain names need to be explicitly enabled for the getServerName() method to return the correct address.
  • enableLocalhost - Enables localhost as a valid domain name for the API. This option enables local testing with the SAM CLI for APIs that use a custom domain name when deployed to AWS.
  • setQueryStringCaseSensitive - Configures case sensitivity when looking up query string parameter names. Defaults to false.

Event translation

To translate incoming events, the library declares two abstract classes: The RequestReader and the ResponseWriter. Both these classes use generic types for the input and output objects. Implementing libraries, such as the Jersey one, extend these classes to support their types.

Out of the box, the library supports proxy integration events.

Clone this wiki locally