AWS Lambda Functions in Java with SAM

SAM (Serverless Application Model) is an alleviate version of CloudFormation and is one of the preferred methods to describe AWS infrastructures in a IaC (Infrastructure as Code) manner.

The AWS console allows the user to create and update, in an user-friendly manner, cloud infrastructure resources. Despite all the advantages that such a high level tool might have, using it is repetitive and error prone. For example, each time we create a Lambda Function using the AWS console, we need to repeat the same operations, again and again and, even if these operations are intuitive and easy as graphical widget manipulations, the whole process is time consuming and laborious. This working mode is convenient for the rapid prototyping but, as soon as we have to work on real projects, with relatively large scope and duration, it doesn't meet anymore the team's goals and wishes. In such a case, the preferred solution is IaC.

IaC essentially consists in using a declarative notation in order to specify infrastructure resources. In the case of AWS, this notation, expressed as a formalism based on a JSON or YAML syntax, is captured in configuration files and submitted to the CloudFormation IaC utility.

CloudFormation is a vast topic that couldn't be detailed in a blog ticket. The important point that we need to retain here is that, this service is able to process input configuration files and to guarantee the creation and the update of the associated AWS cloud infrastructure resources. While the benefits of the CloudFormation IaC approach are obvious, this tool has the reputation of being verbose, unwieldy and inflexible. Fortunatelly, AWS Lambda developers have the choice of using SAM, a superset of CloudFormation which includes some special commands and shortcuts aiming at easing the development, testing and deployment of the Java serverless code.

Installing SAM

Installing SAM is very simple, one only has to follow the guide here. For example, installing it on Ubuntu 22.04 LTS is as simple as shown below:

Install SAM on Ubuntu
$ sudo apt-get update
...
$ sudo apt-get install awscli
...
$ aws --version
aws-cli/2.9.12 Python/3.9.11 Linux/5.15.0-57-generic exe/x86_64.ubuntu.22 prompt/off

Creating AWS Lambda Functions in Java with SAM

Now that SAM is installed on your workstation, you can write and deploy your first Java serverless function. Of course, we assume here that your AWS account has been created and that your environment is configured such that you can run AWS CLI commands, as explained here.

Like CloudFormation, SAM is based on the notion of template which is a YAML formatted text file that describes an AWS infrastructure. This template file, which name by default is template.yaml, has to be authored manually, such that to be aligned with the SAM template anatomy, which complete specifications can be found here. But writing a template.yaml file from scratch is difficult. Hence the idea of automatically generating it. Enters CookieCutter.

CookieCutter is an open source project allowing the automatic code generation. It is very used in the Python world but, here, we'll use it in the Java world. Its modus operandi is very similar to the one of the Maven archetypes, in the sense that it is able to automatically generate full Java projects, including but not limited to packages, classes, configuration files, etc. The generation process is highly customizable and is able to replace string occurrences, flagged by placeholders expressed in a dedicated syntax, by values defined in an external JSON formatted file.

This GitHub repository provides such a CookieCutter based generator able to generate a simple but complete Java project, ready to be deployed as an AWS Lambda serverless function. The listing below shows how:

Generate a Lambda Java project
$ sam init --location https://github.com/nicolasduminil/sam-template
You've downloaded /home/nicolas/.cookiecutters/sam-template before. Is it okay to delete and re-download it? [yes]: 
project_name [my-project-name]: aws-lambda-simple
aws_lambda_resource_name [my-aws-lambda-resource-name]: AwsLambdaSimple
java_package_name [fr.simplex_software.aws.lambda.functions]: 
java_class_name [MyAwsLambdaClassName]: AwsLambdaSimple
java_handler_method_name [handleRequest]: 
maven_group_id [fr.simplex-software.aws.lambda]: 
maven_artifact_id [my-aws-function]: aws-lambda-simple
maven_version [1.0.0-SNAPSHOT]: 
function_name [AwsLambdaTestFunction]: AwsLambdaSimpleFunction
Select architecture:
1 - arm64
2 - x86_64
Choose from 1, 2 [1]: 
timeout [10]: 
Select tracing:
1 - Active
2 - Passthrough
Choose from 1, 2 [1]:

The command sam init above mentions the location of the CookieCutter based template used to generate a new Java project. This generation process takes the form of a dialog where the utility is asking questions and accepts answers. Each question has default responses and, in order to accept them, the user just need to type Enter.

Everything starts by asking the project name and we chose aws-lambda-simple. Further information to be entered are:

  • the AWS resource name;

  • the Maven GAV (GroupId, ArtifactId, Version);

  • the Java package name;

  • the Java class name;

  • the processor architecture;

  • the timeout value;

  • the tracing profile.

As soon as the command terminates, you may open the new project in your preferred IDE and inspect the generated code. Once finished, you may proceed with a first build, as follows:

Maven build
$ cd aws-lambda-simple/
nicolas@nicolas-XPS-13-9360:~/sam-test/aws-lambda-simple$ mvn package
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------< fr.simplex-software.aws.lambda:aws-lambda-simple >----------
[INFO] Building aws-lambda-simple 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ aws-lambda-simple ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/nicolas/sam-test/aws-lambda-simple/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ aws-lambda-simple ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /home/nicolas/sam-test/aws-lambda-simple/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ aws-lambda-simple ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/nicolas/sam-test/aws-lambda-simple/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ aws-lambda-simple ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ aws-lambda-simple ---
[INFO] No tests to run.
[INFO] 
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ aws-lambda-simple ---
[INFO] Building jar: /home/nicolas/sam-test/aws-lambda-simple/target/aws-lambda-simple-1.0.0-SNAPSHOT.jar
[INFO] 
[INFO] --- maven-shade-plugin:3.2.1:shade (default) @ aws-lambda-simple ---
[INFO] Replacing /home/nicolas/sam-test/aws-lambda-simple/target/aws-lambda-simple.jar with /home/nicolas/sam-test/aws-lambda-simple/target/aws-lambda-simple-1.0.0-SNAPSHOT-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.604 s
[INFO] Finished at: 2023-01-12T19:09:23+01:00
[INFO] ------------------------------------------------------------------------

Our new Java project has been built and packaged as a JAR (Java ARchive). The generated template.yaml file defines the required AWS cloud infrastructure, as shown below:

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: aws-lambda-simple

Resources:

  AwsLambdaSimple:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: AwsLambdaSimpleFunction
      Architectures: 
        - arm64
      Runtime: java11
      MemorySize: 128
      Handler: fr.simplex_software.aws.lambda.functions.AwsLambdaSimple::handleRequest
      CodeUri: target/aws-lambda-simple.jar
      Timeout: 10
      Tracing: Active

This file has been created based on the value entered during the generation process. Things like the AWS template version and the transformation version are constants and should be use as such. All the other elements are known as they mirror the input data. A special consideration have to be given to the CodeUri element which specifies the location of the JAR to be deployed as the Lambda function. It contains the class AwsLambdaSimple below:

public class AwsLambdaSimple 
{
  private static Logger log = Logger.getLogger(AwsLambdaSimple.class.getName());

  public String handleRequest (Map<String, String> event) 
  {
    log.info("*** AwsLambdaSimple.handleRequest: Have received: " + event);
    return event.entrySet().stream().map(e -> e.getKey() + "->" + e.getValue()).collect(Collectors.joining(","));
  }
}

A Lambda function in Java can be ran in the following two modes:

  • a synchronous or RequestResponse mode in which the caller waits for whatever response the Lambda function returns;

  • an asynchronous or Event mode in which the caller is responded without waiting, by the Lambda platform itself, while the function proceeds with the request processing, without returning any further response.

In both cases, the method handleRequest() above is processing the request, as its name implies. This request is an event implemented as a Map<String, String>. If you need more details and explanations concerning the Lambda functions invocation, please don't hesitate to read my book (in French).

All right, now our new Java project is generated and, while the class AwsLambdaSimple, presented above, which will be deployed in fine as an AWS Lambda function, doesn't do much, it is sufficiently complete in order to demonstrate our use case. So let's deploy our cloud infrastructure. But first, we need to create an AWS S3 bucket in order to store in it our Lambda function. The simplest way to do that is shown below:

$ aws s3 mb s3://bucket-$$
make_bucket: bucket-18468

Here we just created an S3 bucket having the name of bucket-18468. The AWS S3 buckets are constrained to have a unique name across regions. And since it's difficult to guarantee the uniqueness of a name, we use here the Linux $$ function which generates a random number.

Now we can deploy our SAM template as follows:

sam deploy --s3-bucket bucket-18468 --stack-name simple-lambda-stack --capabilities CAPABILITY_IAM
Uploading to 44774b9ed09001e1bb31a3c5d11fa9bb  4031 / 4031  (100.00%)

        Deploying with following values
        ===============================
        Stack name                   : simple-lambda-stack
        Region                       : eu-west-3
        Confirm changeset            : False
        Disable rollback             : False
        Deployment s3 bucket         : bucket-18468
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {}
        Signing Profiles             : {}

Initiating deployment
=====================
Uploading to 3af7fb4a847b2fea07d606a80de2616f.template  555 / 555  (100.00%)

Waiting for changeset to be created..
CloudFormation stack changeset
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                                                  LogicalResourceId                                          ResourceType                                               Replacement                                              
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                                      AwsLambdaSimpleRole                                        AWS::IAM::Role                                             N/A                                                      
+ Add                                                      AwsLambdaSimple                                            AWS::Lambda::Function                                      N/A                                                      
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:eu-west-3:495913029085:changeSet/samcli-deploy1673620369/0495184e-58ca-409c-9554-ee60810fec08


2023-01-13 15:33:00 - Waiting for stack create/update to complete

CloudFormation events from stack operations (refresh every 0.5 seconds)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                                             ResourceType                                               LogicalResourceId                                          ResourceStatusReason                                     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                                         AWS::IAM::Role                                             AwsLambdaSimpleRole                                        -                                                        
CREATE_IN_PROGRESS                                         AWS::IAM::Role                                             AwsLambdaSimpleRole                                        Resource creation Initiated                              
CREATE_COMPLETE                                            AWS::IAM::Role                                             AwsLambdaSimpleRole                                        -                                                        
CREATE_IN_PROGRESS                                         AWS::Lambda::Function                                      AwsLambdaSimple                                            -                                                        
CREATE_IN_PROGRESS                                         AWS::Lambda::Function                                      AwsLambdaSimple                                            Resource creation Initiated                              
CREATE_COMPLETE                                            AWS::Lambda::Function                                      AwsLambdaSimple                                            -                                                        
CREATE_COMPLETE                                            AWS::CloudFormation::Stack                                 simple-lambda-stack                                        -                                                        
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - simple-lambda-stack in eu-west-3

Our Java class has been successfully deployed as an AWS Lambda function, let's test it using the two invocation methods presented above.

$ aws lambda invoke --function-name AwsLambdaSimpleFunction --payload $(echo "{\"Hello\":\"Dude\"}" | base64) outputfile.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
$ cat outputfile.txt 
"Hello->Dude"

The listing above demonstrates the synchronous or RequestResponse invocation. We pass a JSON formatted payload as the input event and, since its default format is base64, we need to convert it first. Since the invocation is synchronous, the caller waits for the response which is captured in the file outputfile.txt. The returned status code is HTTP 200, as expected, meaning that the request has been correctly processed. Let's see now the asynchronous or Event invocation.

$ aws lambda invoke --function-name AwsLambdaSimpleFunction --payload $(echo "{\"Hello\":\"Dude\"}" | base64) --invocation-type Event outputfile.txt
{
    "StatusCode": 202
}

This time the --invocation-type is Event and, consequently, the returned status code is HTTP 202, meaning that the request has been accepted, but not yet processed. The file output.txt is empty, as there is no result.

This concludes our use case showing the AWS Lambda functions deployment in Java via the SAM tool. Don't forget to clean-up your environment before leaving by running:

$ aws s3 rm --recursive s3://bucket-18468
$ aws s3 rb --force s3://bucket-18468
$ aws cloudformation delete-stack --stack-name simple-lambda-stack

Enjoy !

Last updated