With this blog post, you’ll learn how to use the Serverless framework to develop AWS Lambda functions using Java and Maven. To provide you a realistic use case, the AWS Lambda will create a thumbnail whenever someone uploads a new image to an S3 bucket. The free-tier of AWS is enough to mirror this set up in your own account and execute it several thousand times each month for free.

AWS Setup for Serverless

For deploying the AWS Lambda we’ll use the Serverless framework. This framework simplifies the deployment of functions to different cloud providers (e.g. AWS Lambda, Azure Functions, etc.) with an abstraction layer.

Serverless will take care of creating all required AWS resources e.g. S3 Bucket for the code and for the images, IAM roles, Lambda functions itself, etc. using CloudFormation in the background.

You can install Serverless via npm on your machine:

$ npm install -g serverless
   ┌───────────────────────────────────────────────────┐
   │                                                   │
   │   Serverless Framework successfully installed!    │
   │                                                   │
   │   To start your first project run 'serverless'.   │
   │                                                   │
   └───────────────────────────────────────────────────┘

Next, we have to set up the AWS credentials for our AWS account. You can follow the instructions on their documentation for this and create a new IAM user in your AWS account called serverless-admin.

Once you the access key and secret for this new user ready, you can store the credentials on your machine:

serverless config credentials --provider aws --key  ABC --secret XYZ --profile serverless-admin

Maven project setup for an AWS Lambda

Our Maven project configures a Java 11 project (AWS provides a Java 8 and 11 runtime as of now) with several AWS specific dependencies:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.serverless</groupId>
  <artifactId>thumbnail-generator</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <aws-lambda-java-events.version>2.2.8</aws-lambda-java-events.version>
    <aws-lambda-java-core.version>1.2.1</aws-lambda-java-core.version>
    <aws-java-sdk-s3.version>1.11.775</aws-java-sdk-s3.version>
    <jaxb.version>2.3.3</jaxb.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-java-sdk-s3</artifactId>
      <version>${aws-java-sdk-s3.version}</version>
    </dependency>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-events</artifactId>
      <version>${aws-lambda-java-events.version}</version>
    </dependency>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>${aws-lambda-java-core.version}</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jaxb</groupId>
      <artifactId>jaxb-runtime</artifactId>
      <version>${jaxb.version}</version>
    </dependency>
  </dependencies>

  <!-- build section -->

</project>

As we’ll access images in an S3 bucket, we need the official AWS Java SDK for this. Unfortunately, we can’t use v2 of the AWS SDK here already, as the aws-lambda-java-events right now only works with v1. This event dependency includes the S3Event class, that we can use to have typesafe access inside the lambda to react on. If you prefer to use the AWS SDK v2, you can either provide your own representation of the S3Event (e.g. copy it) or use a JSON representation of the incoming Lambda input.

Finally, the aws-lambda-java-core dependency contains the RequestHandler<I, O> we have to implement to create a Java-based Lambda function.

The JAXB dependency is optional, but without it, the AWS Lambda outputs a warning log message as since Java 11 JAXB is removed from the JDK.

Furthermore, we have to create a Uber Jar and package all dependencies with the maven-shade-plugin.

<build>
  <finalName>${project.artifactId}</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.2.3</version>
      <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
      </configuration>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>shade</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

With this setup, we’ll create an 8 MB .jar file. As for each new AWS Lambda function deployment we have to upload this file to an S3 bucket (will be handled by the Serverless framework), you should try to keep the size of your .jar as small as possible.

Creating image thumbnails with Java

Writing an AWS Lambda function with Java requires implementing the RequestHandler<I, O> interface. This interface takes two parameters: one for the incoming input of the Lambda function and one for the return type.

You can specify any plain old Java object for this which maps to your input payload. AWS Lambda takes care of serializing the incoming payload using Jackson. The same is true for the return type of your AWS Lambda function as it is deserialized to JSON by default. If you don’t want to use this default serialization behavior, you can implement the RequestStreamHandler interface instead.

As our Lambda function is triggered by S3 ObjectCreated events, we can use the S3Event as input. The return type can be Void in this case, as we don’t return anything and just process the uploaded image to create a thumbnail of it.

The incoming S3Event comes with information about which file was uploaded. We can extract this information and use the S3 Java client to get the image. AWS Lambda executes the Java function with the specified IAM role of the Lambda function, we don’t have to provide any credentials to construct the client. Just ensure the IAM role has enough rights to perform the S3 operations.

Once the uploaded file is stored in a temporary file, we can perform the thumbnail generation with the help of BufferedImage and ImageIO. The thumbnail size is obtained from an environment variable we’ll configure with Serverless in the next section.

Please note that this is a prototype-ish implementation of this thumbnail generation, there might be more elegant ways of doing it:

public class ThumbnailHandler implements RequestHandler<S3Event, Void> {

  private static final Integer THUMBNAIL_SIZE = Integer.valueOf(System.getenv("THUMBNAIL_SIZE"));
  private static final String THUMBNAIL_PREFIX = "thumbnails/" + THUMBNAIL_SIZE + "x" + THUMBNAIL_SIZE + "-";

  @Override
  public Void handleRequest(S3Event s3Event, Context context) {
    String bucket = s3Event.getRecords().get(0).getS3().getBucket().getName();
    String key = s3Event.getRecords().get(0).getS3().getObject().getKey();
    System.out.println("Going to create a thumbnail for: " + bucket + "/" + key);

    AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
    System.out.println("Connection to S3 established");

    try {
      File tempFile = File.createTempFile(key, ".tmp");
      s3Client.getObject(new GetObjectRequest(bucket, key), tempFile);
      System.out.println("Successfully read S3 object to local temp file");

      BufferedImage img = new BufferedImage(THUMBNAIL_SIZE, THUMBNAIL_SIZE, BufferedImage.TYPE_INT_RGB);
      img.createGraphics().drawImage(ImageIO.read(tempFile).getScaledInstance(100, 100, Image.SCALE_SMOOTH), 0, 0, null);
      File resizedTempFile = File.createTempFile(key, ".resized.tmp");
      ImageIO.write(img, "png", resizedTempFile);
      System.out.println("Successfully created resized image");

      String targetKey = THUMBNAIL_PREFIX + key.replace("uploads/", "");
      s3Client.putObject(bucket, targetKey, resizedTempFile);
      System.out.println("Successfully uploaded resized image with key " + targetKey);
    } catch (IOException e) {
      e.printStackTrace();
    }

    return null;
  }
}

Once the image is processed, we’ll store the thumbnail in S3 with the following file structure: thumbnails/100x100-nameOfImage.png.

Deploying the AWS Lambda with Serverless

As the final step, we’ll configure Serverless to deploy our AWS Lambda function. For this, we’ll create a serverless.yml file in the root of our project (next to pom.xml).

First, we have to configure some information about the provider. Among other things, this includes the name of the cloud provider, the runtime, the region. Furthermore, you can specify the AWS profile you configured your credentials locally (otherwise it will use the default profile).

As the Lambda function needs access to the S3 bucket we’ll use to upload images to, we have to also adjust the iamRoleStatements field and grant access to also the S3 deployment bucket which contains a ZIP file of our .jar :

service: thumbnail-generator

provider:
  name: aws
  runtime: java11
  profile: serverless-admin
  region: eu-central-1
  timeout: 10
  memorySize: 1024
  iamRoleStatements:
    - Effect: 'Allow'
      Action:
        - 's3:*'
      Resource:
        - 'arn:aws:s3:::${self:custom.thumbnailBucket}/*'
        - !Join ['', ['arn:aws:s3:::', !Ref ServerlessDeploymentBucket, '/*']]

custom:
  thumbnailBucket: image-uploads-java-thumbnail-example

Next, we have to tell Serverless where to find our build artifact and define the function:

package:
  artifact: target/thumbnail-generator.jar

functions:
  thumbnailer:
    handler: de.rieckpil.blog.ThumbnailHandler
    events:
      - s3:
          bucket: ${self:custom.thumbnailBucket}
          event: s3:ObjectCreated:*
          rules:
            - prefix: uploads/
            - suffix: .png
    environment:
      THUMBNAIL_SIZE: 100

This points to the .jar file and the handler attribute contains the fully qualified class name of our Java handler class. With the events attribute we specify the way our function is triggered. We limit this to include only the creation of objects in S3 with the uploads/ prefix and .png suffix e.g. uploads/myProfilePicture.png. While specifying the name of the S3 bucket, Serverless also ensure to create the bucket for us.

You can now deploy everything with serverless deploy -v. Make sure to build the project with mvn package first.

Once the Lambda function is deployed, you can upload a .png file to the S3 bucket and should find the thumbnail shortly after:

aws s3api put-object --bucket image-uploads-java-thumbnail-example --key uploads/myPicture.png --body myPicture.png --profile serverless-admin
aws s3api list-objects-v2 --bucket image-uploads-java-thumbnail-example --profile serverless-admin

Please note that the first Lambda execution is a cold start and might require more time. The first run takes approx. 6 seconds for me, but all subsequent executions finish in 400 – 500ms.

You can find the source code for this Java Lambda function using the Serverless framework and Maven on GitHub. Further AWS related blog posts are available here.

Have fun deploying AWS Lambada functions with Java and the Serverless framework,

Phil

The post Java AWS Lambda with Serverless and Maven – Thumbnail Generator appeared first on rieckpil.