Deploy a Java Lambda Function and API Gateway with AWS CDK

Photo by Aurélien Grimpard / Unsplash

With AWS, Lambda provides server computation with no infrastructure hassle. It also provides the runtime for many programming languages like JavaScript, PHP, Golang, Java, etc...

Today, we will see how to create a Lambda function with the Java Runtime, Add an API Gateway in front to invoke the function through an endpoint.

Prerequisites

To continue this tutorial, make sure you have the following tools installed on your computer:

  • An AWS account to deploy our Lambda function
  • AWS CLI configured (check out this link to see how to do it)
  • JDK 11 (Amazon Corretto is recommended) and Maven 3.5 or higher
  • AWS CDK installed locally: npm install -g cdk
  • Docker to test the Lambda locally using AWS SAM CLI, but it is optional since you can just deploy on AWS to test

What we will build

We want to build an app that exposes an endpoint where the user can send his Height and weight; the function will calculate the Body Mass Index (BMI) and return the result. The endpoint will accept POST requests.

Create the project with the CDK

We will use the AWS CDK CLI to create a Java project, but before, let's see how to structure the project folder:

  • The first folder will contain the infrastructure code like creating the Lambda function, the API Gateway, etc... We will call it infra.
  • The second folder will contain a Maven project that holds the business logic of the Lambda function. We will call it bmi-calculator.

Let's create the root directory and enter inside; you can give the name you want.

mkdir lambda-java-cdk

cd lambda-java-cdk

Create the infra folder

mkdir infra

cd infra

cdk init sample-app --language java

Create the function folder

This folder will host a Java project with Maven; run the command below to create a project using the CLI:

mvn archetype:generate \
-DgroupId=com.tericcabrel \
-DartifactId=bmi-calculator \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false

cd bmi-calculator

Open the pom.xml file and replace the content with the code below:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tericcabrel</groupId>
<artifactId>bmi-calculator</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>bmi-calculator</name>
<url>https://maven.apache.org</url>
<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <maven.compiler.source>11</maven.compiler.source>
  <maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.4.0</version>
    <scope>test</scope>
  </dependency>
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.2.4</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>shade</goal>
          </goals>
          <configuration>
            <createDependencyReducedPom>false</createDependencyReducedPom>
            <finalName>bmicalculator</finalName>
            <transformers>
              <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>com.tericcabrel.App</mainClass>
              </transformer>
            </transformers>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
</project>

To learn more about what this update is necessary, I wrote a post that explains why.

Create a Java project using an external dependency with Maven
In this tutorial, we will see how the create a Java project using Maven then, add an external dependency, finally package the JAR file and execute it.

Close the pom.xml, then run the command below to update the dependencies:

mvn dependency:resolve

Update the test file located at src/test/java/com/tericcabrel/AppTest.java  with the content below; otherwise, the project packaging will fail:

package com.tericcabrel;


import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;

/**
 * Unit test for simple App.
 */
public class AppTest {
    @Test
    public void testApp()
    {
        System.out.println("Hello world from test!");
        assertTrue( true );
    }
}

Here is what the project directory looks like:

Structure of the project directory

Install the Lambda package for Java

The bmi-calculator is just a classic Java project, but to make it behave as a Lambda Function, we need to install the Java package for AWS Lambda. Let's update the pom.xml to add the dependency:

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-lambda-java-core</artifactId>
  <version>1.2.1</version>
</dependency>
<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-lambda-java-events</artifactId>
  <version>3.11.0</version>
</dependency>
bmi-calculator/pom.xml

Here are the links to the Maven repository:

Update the Maven dependencies: mvn dependency:resolve

Write the Lambda function logic

We will need a library to parse the request body from a string to an object; we will use GSON. We will also need Log4j for logging. Update the pom.xml to add these dependencies:

<dependencies>
  ........
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.32</version>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.32</version>
  </dependency>
  <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.9.0</version>
  </dependency>
</dependencies>
bmi-calculator/pom.xml

Update the Maven dependencies: mvn dependency:resolve

Let's create a file called RequestInput.java that will contain the request body:

package com.tericcabrel;

public class RequestInput {

  private double height;
  private double weight;

  public double getHeight() {
    return height;
  }

  public void setHeight(double height) {
    this.height = height;
  }

  public double getWeight() {
    return weight;
  }

  public void setWeight(double weight) {
    this.weight = weight;
  }
}
bmi-calculator/src/main/java/com/tericcabrel/RequestInput.java

Open the file App.java and replace the content with the code below:

package com.tericcabrel;

import com.google.gson.Gson;
import java.util.HashMap;
import java.util.Map;

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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    static Logger logger = LoggerFactory.getLogger(App.class);

    public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
        logger.info(input.getBody());

        Gson gson = new Gson();
        RequestInput bodyInput = gson.fromJson(input.getBody(), RequestInput.class);

        double result = calculateBodyMassIndex(bodyInput.getHeight(), bodyInput.getWeight());
        String output = String.format("{ \"result\": %s }", result);

        Map<String, String> responseHeaders = new HashMap<>();
        responseHeaders.put("Content-Type", "application/json");
        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent().withHeaders(responseHeaders);

        return response
            .withStatusCode(200)
            .withBody(output);
    }

    private double calculateBodyMassIndex(double height, double weight) {
        double bmi = weight / Math.pow(height, 2);
        double bmiRounded = Math.round(bmi * 10);

        return  bmiRounded / 10;
    }
}
bmi-calculator/src/main/java/com/tericcabrel/App.java

Define the Lambda Function

The business logic for the BMI calculator is ready; now, let's define the Lambda function using the CDK. Go to the infra folder and open the file src/main/java/com/myorg/InfraStack.java and update the code below:

List<String> lambdaFunctionPackagingInstructions = Arrays.asList(
            "/bin/sh",
            "-c",
            "cd bmi-calculator " +
                "&& mvn clean install " +
                "&& cp /asset-input/bmi-calculator/target/bmicalculator.jar /asset-output/"
        );

        BundlingOptions.Builder builderOptions = BundlingOptions.builder()
            .command(lambdaFunctionPackagingInstructions)
            .image(Runtime.JAVA_11.getBundlingImage())
            .volumes(singletonList(
                DockerVolume.builder()
                    .hostPath(System.getProperty("user.home") + "/.m2/")
                    .containerPath("/root/.m2/")
                    .build()
            ))
            .user("root")
            .outputType(ARCHIVED);

        Function bmiCalculatorFunction = new Function(this, "bmi-calculator", FunctionProps.builder()
        	.functionName("bmi-calculator")
            .runtime(Runtime.JAVA_11)
            .code(Code.fromAsset("../", AssetOptions.builder().bundling(
                builderOptions.command(
                    lambdaFunctionPackagingInstructions
                ).build()
            ).build()
            ))
            .handler("com.tericcabrel.App")
            .memorySize(1024)
            .timeout(Duration.seconds(10))
            .logRetention(RetentionDays.ONE_WEEK)
            .build());
            
infra/src/main/java/com/myorg/InfraStack.java

The code above is responsible for bundling the Java source code and defining the Lambda function resource.

This is the step of the Bundling:

  • Create a Docker container from the image of Java 11.
  • Mount the Maven dependencies folder ~/.m2 of the computer inside the Docker container at the path /root/.m2
  • Inside the container, the project is mounted inside the folder asset-input; we enter into bmi-calculator, package the Java project and copy the .jar in the folder asset-output. The files under /asset-outputwill be zipped and uploaded to Amazon S3 as the asset by default.

For the part defining the Lambda, there are two interesting to see:

  • To indicate the path where the Lambda will find the code to execute, we write:
Code.fromAsset("../", AssetOptions.builder()

Why do we do "../"? It is because the base directory is the folder infra , so to find the code inside the bmi-calculator folder, we must go back from one folder.

  • The value of the property handler is  com.tericcabrel.App; this refers to the file App.java inside the folder src/main/java/com/tericcabrel.

The other settings are the basic ones like the function name, memory size, runtime version, time out, etc...

Define the API Gateway

Version 2 of the AWS API gateway is not present in the CDK library, but in a separate package because it is still unstable at the moment I'm writing this post. We will add two Maven packages. Update the pom.xml

<dependencies>
    .....
    <dependency>
        <groupId>software.amazon.awscdk</groupId>
        <artifactId>apigatewayv2-alpha</artifactId>
        <version>${cdk.version}-alpha.0</version>
    </dependency>
    <dependency>
        <groupId>software.amazon.awscdk</groupId>
        <artifactId>apigatewayv2-integrations-alpha</artifactId>
        <version>${cdk.version}-alpha.0</version>
    </dependency>
</dependencies>
infra/pom.xml

Run mvn dependency:resolve to install the packages

Now, update the file src/main/java/com/myorg/InfraStack.java with the content below:

package com.tericcabrel;

import software.amazon.awscdk.CfnOutput;
import software.amazon.awscdk.CfnOutputProps;
import software.amazon.awscdk.services.apigatewayv2.alpha.AddRoutesOptions;
import software.amazon.awscdk.services.apigatewayv2.alpha.HttpApi;
import software.amazon.awscdk.services.apigatewayv2.alpha.HttpMethod;
import software.amazon.awscdk.services.apigatewayv2.alpha.PayloadFormatVersion;
import software.amazon.awscdk.services.apigatewayv2.integrations.alpha.HttpLambdaIntegration;
import software.amazon.awscdk.services.apigatewayv2.integrations.alpha.HttpLambdaIntegrationProps;


// previous code here


HttpApi httpApi = new HttpApi(this, "HttpApi");

        HttpLambdaIntegration httpLambdaIntegration = new HttpLambdaIntegration(
            "this",
            bmiCalculatorFunction,
            HttpLambdaIntegrationProps.builder()
                .payloadFormatVersion(PayloadFormatVersion.VERSION_2_0)
                .build()
        );

        httpApi.addRoutes(AddRoutesOptions.builder()
            .path("/calculate")
            .methods(singletonList(HttpMethod.POST))
            .integration(httpLambdaIntegration)
            .build()
        );

        new CfnOutput(this, "HttApi", CfnOutputProps.builder()
            .description("HTTP API URL")
            .value(httpApi.getApiEndpoint())
            .build());
            
infra/src/main/java/com/myorg/InfraStack.java

Here, we create an endpoint '/calculate' that will invoke the Lambda function. In the end, we use CfnOutput to print the URL of the API Gateway in the console.

Sending a request to <gateway-url>/calculate in the POST method will invoke the Lambda function and return the result.

Deploy the Lambda function to AWS

With the CDK, once you define the resources of your stack, you need to generate the CloudFormation template. The command to execute this step is:

cdk synth

This command will do two things:

  • Build the project bmi-calculator to generate the JAR file
  • Generate the CloudFormation template from the project infra

Once done, initialize the CloudFormation stack necessary to deploy the resources on AWS:

cdk bootstrap

Note: you only need to run this command once; this stack will be reused for your next project.

Finally, deploy on AWS and wait for the execution to complete

cdk deploy

You will get an output similar to this one:

Stack deployed with the CDK.

Log into your AWS console and browse your Lambda functions; you will see two new functions, the BMI calculator function and the Lambda created to handle logs retention.

Lambda functions deployed on AWS with the CDK

Test the Lambda function from the API Gateway

Take an HTTP client of your choice to test; I will use Postman.

Invoke the Lambda from the API gateway.

Delete everything

Deleting all the resources and stack created for this application is straightforward; run the following command:

cdk destroy

Wrap up

You can now build your Serverless backend web application using Java. Here are some resources to learn more about the AWS API Gateway:

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to not miss the upcoming posts and the tips and tricks I share every week.

Happy to see you soon ?