Serverless computing is something I’ve known about for quite some time, but never really gotten around
to doing anything with. Finally deciding to see what the fuss was about, I set out to build something
that could be run on AWS Lambda. This was surprisingly easy, and in this article I will illustrate how
to create a simple function that converts a long
representing seconds since the Unix epoch to an ISO-8601 formatted date.
Security considerations
AWS has a lot of services to offer, and generally speaking your AWS root account is tied to your (or your boss’) creditcard. If your credentials fall into the wrong hands, you can get into a lot of financial trouble.
When going through this article I recommend you create separate API accounts for:
- Creating and managing the Docker image for your function in Amazon Elastic Container Registry
- Invoking the lambda function
And never, under any circumstance, put the keys to these accounts in a public Git repository.
Getting started
Maven is my go-to tool for managing projects, and to develop a lambda function you need to create simple project and add the following dependency:
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>${amazon.aws.lambda.java.core.version}</version>
</dependency>
This requires you to set a property for the latest SDK version. I used version 1.2.1:
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<amazon.aws.lambda.java.core.version>1.2.1</amazon.aws.lambda.java.core.version>
</properties>
AWS Lambda currently has base images for Java versions 8 and 11, so I set my source level to 11. With all this configured we can now create our handler.
Function handler
The function handler is a class that implements the RequestHandler
interface:
public class ISODateFormatter implements RequestHandler<Long,String> {
@Override
public String handleRequest(Long seconds, Context context)
{
LocalDateTime instant = LocalDateTime.ofEpochSecond(seconds, 0, ZoneOffset.UTC);
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(instant);
}
}
And that’s basically it. AWS Lambda is pretty flexible in the sort of inputs and outputs it supports. You can easily have it receive and return JSON objects.
Deploying the function
To deploy the function, we need to do several things. First we need to compile our function and copy any dependencies it includes to our target folder. Next, we need to package all this in a Docker container that uses Amazon’s Java images as a base. Then we need to deploy the image to a registry that AWS can read from, such as an Amazon ECR registry, and finally we need to create the actual function in AWS Lambda.
Configuring Maven to include dependencies
If you use any external libraries, you need to include them in your Docker image. Add the following snippet to your pom.xml
:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<configuration>
<includeScope>runtime</includeScope>
</configuration>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Creating the Docker image
We need to use one of Amazon’s base images to package our Lambda function:
# Use Amazon's Java 11 base image
FROM public.ecr.aws/lambda/java:11
# Copy all classes to the Java root of the image
COPY target/classes ${LAMBDA_TASK_ROOT}
# Copy all dependency libraries to the library folder
COPY target/dependency/* ${LAMBDA_TASK_ROOT}/lib/
# Set the handler class and method as the command to execute
CMD [ "nl.jeroensteenbeeke.tech.lambda.isodate.ISODateFormatter::handleRequest" ]
This can easily be built using a standard docker build .
command, but to get this image deployed
it’s best to follow the instructions on Amazon ECR.
For now, for testing purposes, we can do the following:
docker build -t isodate-example:latest .
docker run -p 9000:8080 isodate-example:latest
Then from another window/tab we can use curl
to send a request:
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '1623923209'
"2021-06-17T09:46:49"
This way, we can test the lambda function before deploying it on Amazon and paying for it.
Deploying the Docker image
To deploy the Docker image, you need to set up an Amazon Elastic Container Registry (ECR) repository for your image. Pick a suitable
name for your repository (I used isodate-example
for this one), and then from the ECR page select
your repository, and click “View Push Commands”. This will give you instructions on how to push your
image.
Doing so requires that you have the aws
command line utility installed, and that you have access to
the registry with the credentials configured for this command.
I strongly recommend creating a separate access key just for registry access. This can be configured using Amazon IAM, and Amazon has good documentation on this subject.
Creating the function
Creating the function is simply a matter of going to the AWS Lambda interface, giving it a name, and selecting an image using the “Browse images” functionality. Amazon will also give you the option of creating a role specifically for this function, but by default this role will not have the ability to invoke (execute) the lambda function.
I solved this by creating a new policy in IAM, and assigning this policy to an API user specifically meant for invoking the lambda function.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction",
"lambda:InvokeAsync"
],
"Resource": [
"ARN-OF-YOUR-LAMBDA-GOES-HERE"
]
}
]
}
When creating an API user to invoke the lambda, remember to write down the secret before you click okay, there is no way of retrieving it once your close the page.
Using the function
To use the function, you need to use the Amazon SDK. First we need to add the SDK dependency:
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-lambda</artifactId>
<version>${amazon.aws.lambda.sdk.version}</version>
</dependency>
I used version 1.12.2
:
<properties>
<amazon.aws.lambda.sdk.version>1.12.2</amazon.aws.lambda.sdk.version>
</properties>
To use the SDK, you first need to create the client:
public AWSLambda createClient(String key, String secret, String region) {
return AWSLambdaClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(key,secret))
.withRegion(region)
.build();
}
Use the key and secret for the user you created earlier.
Let’s try the function we wrote earlier. Keep in mind that all inputs and outputs are formatted in JSON:
// Use Jackson to convert the output
ObjectMapper objectMapper = new ObjectMapper();
String arn = "MY_ARN";
InvokeResult invokeResult = lambda.invoke(new InvokeRequest()
.withFunctionName(arn)
.withPayload("5"));
if (invokeResult.getStatusCode() == 200) {
String result = objectMapper.readValue(invokeResult.getPayload().array(), String.class);
System.out.printf("Result : %s%n", result);
} else {
// Invocation failed
System.err.printf("Error : %s%n", invokeResult.getFunctionError());
System.err.printf("Log result: %s%n", invokeResult.getLogResult());
}
If the permissions are configured correctly, this will output:
Result : 1970-01-01T00:00:05
In conclusion
This is just a simple example of a direct invocation of an AWS Lambda function, but we’ve barely scratched the surface. AWS Lambda can be configured to respond to events, and Lambda functions can be chained to achieve more complex functionality.
To check this out for yourself you can check the code I used for this blog post on Github.