With this blog post, we'll learn how to use the Serverless framework to develop and deploy AWS Lambda functions using Java and Maven. To provide a realistic use case, we'll develop an AWS Lambda that will generate and store a thumbnail for images as soon as they're uploaded to an S3 bucket. We're going to use Java 11, Serverless v3 and the AWS Java SDK v2.
AWS Account Setup for Serverless
(If your AWS account is eligible for the free tier, you can mirror this setup in your own account free of cost.)
For deploying our Java function to 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. Alternative tools to get our Java function deployed to AWS Lambda are AWS SAM, CloudFormation, or the AWS CDK.
The Serverless framework takes care to create all required AWS resources for our AWS Lambda function. This includes the S3 Bucket for the Java code, IAM roles, and the Lambda function configuration. We don't have to fiddle around with long CloudFormation YAML templates at all.
We install the Serverless CLI via npm
:
1 2 3 4 5 6 7 8 | $ 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. We can follow the instructions on their documentation for this and create a new IAM user in your AWS account called serverless-admin
.
Once we have the access key and secret for this new user ready, we can store these credentials in a dedicated profile:
1 | serverless config credentials --provider aws --key ABC --secret XYZ --profile serverless-admin |
Maven Java Project Setup for AWS Lambda
Our Maven project is a plain Java 11 project with several AWS-specific dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <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>de.rieckpil.blog</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>3.11.0</aws-lambda-java-events.version> <aws-lambda-java-core.version>1.2.1</aws-lambda-java-core.version> <aws.java.sdk.version>2.16.1</aws.java.sdk.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>bom</artifactId> <version>${aws.java.sdk.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> </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> </dependencies> <!-- build section --> </project> |
As we'll access images in an S3 bucket, we need the official AWS Java SDK for this. Therefore, we include the AWS Java SDK v2 BOM to manage and align our s3
dependency.
Next, the aws-lambda-java-events
contains Java classes that represent various events on AWS. This includes the S3Event
that we can trigger whenever there's a file upload to a specific bucket. Using this library, we get type-safe access to the payload of the event.
Finally, the aws-lambda-java-core
dependency contains the RequestHandler<I, O>
we have to implement to create a Java-based Lambda function.
As of now, AWS Lambda provides Java 8 and 11 runtimes, but we can also ship our own container image to run more recent Java code on AWS Lambda.
Furthermore, we have to create a Uber Jar and package all dependencies with the maven-shade-plugin
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <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 12 MB .jar
large file. As for each new AWS Lambda function deployment we have to upload this file to an S3 bucket (this will be handled by the Serverless framework), we should try to keep the size of our .jar
as small as possible.
Creating Image Thumbnails with Java and AWS Lambda
Writing an AWS Lambda function with Java requires implementing the RequestHandler<I, O>
interface. This interface takes two parameters: one for the incoming input (I) of the Lambda function and one for the return type (aka. output O).
The input Java class can be any plain old Java object that maps to our input payload. AWS Lambda takes care of serializing the incoming payload using Jackson. The same is true for the return type of our AWS Lambda function. AWS Lambda deserializes our return type to JSON by default. If we don't want to use this default serialization behavior, we can implement the RequestStreamHandler
interface instead.
As our Lambda function will be triggered by the S3 ObjectCreated
event, we can use the S3Event
(from aws-lambda-java-events
) as the input type. 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 we download the uploaded image, we 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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 + "-"; private static final String AWS_REGION = System.getenv("AWS_REGION"); @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(); LambdaLogger logger = context.getLogger(); logger.log("Going to create a thumbnail for: " + bucket + "/" + key); try(S3Client s3Client = S3Client.builder().region(Region.of(AWS_REGION)).build(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { logger.log("Connection to S3 established"); ResponseBytes<GetObjectResponse> s3Response = s3Client.getObjectAsBytes(GetObjectRequest.builder() .bucket(bucket) .key(key) .build()); logger.log("Successfully read uploaded S3 file"); BufferedImage img = new BufferedImage(THUMBNAIL_SIZE, THUMBNAIL_SIZE, BufferedImage.TYPE_INT_RGB); img .createGraphics() .drawImage(ImageIO.read(s3Response.asInputStream()) .getScaledInstance(THUMBNAIL_SIZE, THUMBNAIL_SIZE, Image.SCALE_SMOOTH), 0, 0, null); ImageIO.write(img, "png", outputStream); logger.log("Successfully created resized image"); String targetKey = THUMBNAIL_PREFIX + key.replace("uploads/", ""); s3Client.putObject(PutObjectRequest.builder() .bucket(bucket) .key(targetKey) .build(), RequestBody.fromBytes(outputStream.toByteArray())); logger.log("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 our 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 IAM roles and grant access to also the S3 deployment bucket which contains a ZIP file of our .jar
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | service: thumbnail-generator frameworkVersion: '3' provider: name: aws runtime: java11 # replace this with your profile or remove it to use the default profile profile: serverless-admin region: eu-central-1 timeout: 10 memorySize: 1024 logRetentionInDays: 7 iam: role: statements: - Effect: 'Allow' Action: - 's3:*' Resource: - 'arn:aws:s3:::${self:custom.thumbnailBucket}/*' - !Join ['', ['arn:aws:s3:::', !Ref ServerlessDeploymentBucket, '/*']] |
Next, we have to tell Serverless where to find our build artifact and define the function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 ensures to create the bucket for us.
We can now deploy everything with serverless deploy -v
. Make sure to build the Maven project with mvn package
first.
Testing Time
Once the Lambda function is deployed, we can upload a .png file to the S3 bucket and should find the thumbnail shortly after:
1 2 | 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.
To remove the AWS Lambda and all the supporting infrastructure that the Serverless framework created, make sure the S3 image bucket is empty and run:
1 | serverless remove |
For more Java AWS Lambda examples, consider the following blog posts:
- Java AWS Lambda Functions with Spring Cloud Function
- Java AWS Lambda Container Image Support (Complete Guide)
- AWS Lambda with Kotlin and Spring Cloud Function
The source code for this Java Lambda function using the Serverless framework and Maven is available on GitHub.
Have fun deploying Java functions to AWS Lambda using the Serverless framework,
Philip