All about Cloud, mostly about Amazon Web Services (AWS)

Writing AWS Lambda functions in Java

 2020-05-23 /  1569 words /  8 minutes

Although Python and JavaScript (via Node.js) seem to be the most popular programming languages for developing AWS Lambda functions, there are cases where Java must still be used. In these cases, it’s important to know about writing AWS Lambda functions in Java.

One example is in highly regulated industries with strict coding standards which may prevent dynamically typed languages like Python and JavaScript in favor of strongly typed languages like Java. Another example might be where code was previously written in Java and it rather than rewriting it in a different programming language and go through another cycle of bugfixes and redeployments it makes sense to convert the existing Java code to a Lambda function.

Typical Problems When Writing AWS Lambda functions in Java

Problems with writing AWS Lambda functions in Java seem to fall into several categories:

  1. Build Pipeline and Cycle Time
  2. Straight to CloudFormation
  3. Development Approach

Build Pipeline and Cycle Time

Often in environments where Java will be used for Lambda functions, there will be a build pipeline which enforces corporate standards. Now, this is a good thing when there are enhancements or bugfixes being applied to code which has previously worked, but these build pipelines can introduces two major impediments when first writing AWS Lambda functions in Java.

First, there may be very specific rules which must be followed when configuring these build pipelines. The build pipeline may do deployments using AWS CloudFormation. It may be necessary to support multiple system levels (such as dev, test and production) and support multiple AWS regions (such as us-east-1 and us-west-2). There may be very specific subnets that the Lambda functions should be attached to, which may involve calling CloudFormation custom resources.

Second, since often the build system must be used to deploy in higher syslevels (such as test and prod), developers may only deploy through the build system. This means that rather than testing a changes from their local machine, they may commit a change, push it, then wait for the build pipeline to detect the change, and run through the CloudFormation create-stack or update-stack process. This is a problem that programming languages have solved with REPL (Read–eval–print loop) features.

Straight to CloudFormation

When the build system uses CloudFormation there is a tendancy to start writing CloudFormation. Although the AWS Console and AWS CLI might be available, they aren’t available when using higher syslevels (such as prod). Complex integration configuration between various AWS Services and AWS Lambda are developed in CloudFormation rather than first being tested in the AWS Management Console.

This makes the problem described in “Build Pipeline and Cycle Time” so much worse.

Development Approach

Java allows you to develop some very robust code. Interfaces and implementations allow levels of abstraction. Patterns like Factory Methods also hide unnecessary details from developers. As the codebase grows, these language features and development patterns become critical to ensuring the quality of the program.

I’ve seen situations though were I’ve been sent a URL to a git repository and asked if I can tell why the Lamdba function isn’t working. When I follow the link, there are a dozen classes and when I find one that implements the com.amazonaws.services.lambda.runtime.RequestHandler interface, it uses a bunch of Factory classes and layers of abstraction that need to be followed to figure out what is going on.

For sure, this would be less of a problem if an IDE such as IntelliJ, Eclipse or NetBeans were used to view the Lambda function, but the most minimal amount of code required to show success makes like easier even with IDEs.

My Approach to Writing AWS Lambda functions in Java

First, we need to build some Java code, so I start with a Maven pom.xml project file (although you may use Gradle, etc). I configure the Maven project with the Shade plugin, and run that locally. An example pom.xml file is:

 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
<?xml version="1.0" encoding="UTF-8"?>

<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>cloud.cloudninja.java.cw</groupId>
  <artifactId>logger</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>logger</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>1.2.1</version>
    </dependency>
  </dependencies>

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

I then develop the smallest possible AWS Lambda function implementation that will generate some output. An example minimal Java class would be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package cloud.cloudninja.java.cw;

import java.util.Map;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Handler implements RequestHandler<Map<String,String>, String> {

    @Override
    public String handleRequest(Map<String,String> event, Context context) {

        StringBuffer sb = new StringBuffer();
        sb.append( "\nContext (" + context.getClass().getName() + ") : " + context.toString() + "\n" );

        sb.append( "\nEvent (" + event.getClass().getName() + "): " + event.toString() + "\n" );
        System.out.println( sb );

        return sb.toString();
    }
}

I then test the build to ensure it works and then unpack the JAR using jar tvf to ensure that my Java class exists in the the JAR file. Typical output from the jar command is:

 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
$ jar tvf target/logger-1.0-SNAPSHOT.jar
     0 Sat May 23 16:05:08 EDT 2020 META-INF/
   134 Sat May 23 16:05:08 EDT 2020 META-INF/MANIFEST.MF
     0 Sat May 23 16:05:10 EDT 2020 cloud/
     0 Sat May 23 16:05:10 EDT 2020 cloud/cloudninja/
     0 Sat May 23 16:05:10 EDT 2020 cloud/cloudninja/java/
     0 Sat May 23 16:05:10 EDT 2020 cloud/cloudninja/java/cw/
  1842 Sat May 23 16:05:10 EDT 2020 cloud/cloudninja/java/cw/Handler.class
     0 Sat May 23 16:04:20 EDT 2020 META-INF/maven/
     0 Sat May 23 16:04:20 EDT 2020 META-INF/maven/cloud.cloudninja.java.cw/
     0 Sat May 23 16:04:20 EDT 2020 META-INF/maven/cloud.cloudninja.java.cw/logger/
  1711 Sat May 23 16:04:20 EDT 2020 META-INF/maven/cloud.cloudninja.java.cw/logger/pom.xml
   122 Sat May 23 16:05:10 EDT 2020 META-INF/maven/cloud.cloudninja.java.cw/logger/pom.properties
     0 Tue Apr 28 15:12:12 EDT 2020 com/
     0 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/
     0 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/
     0 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/
     0 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/
   367 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/ClientContext.class
   288 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/Client.class
   408 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/RequestHandler.class
   636 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/Context.class
   688 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/LambdaRuntime.class
   580 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/LambdaRuntimeInternal.class
  1022 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/LambdaRuntime$1.class
   197 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/LambdaLogger.class
   224 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/CognitoIdentity.class
   327 Tue Apr 28 15:12:12 EDT 2020 com/amazonaws/services/lambda/runtime/RequestStreamHandler.class
     0 Tue Apr 28 15:11:18 EDT 2020 META-INF/maven/com.amazonaws/
     0 Tue Apr 28 15:11:18 EDT 2020 META-INF/maven/com.amazonaws/aws-lambda-java-core/
  4138 Tue Apr 28 15:11:18 EDT 2020 META-INF/maven/com.amazonaws/aws-lambda-java-core/pom.xml
   118 Tue Apr 28 15:12:14 EDT 2020 META-INF/maven/com.amazonaws/aws-lambda-java-core/pom.properties
$

Next, we want to be able to make frequent repeatable updates to the Lambda function, so I develop a script that I can run locally that will build the code and register it as an AWS Lambda function. The prerequisite to this is an AWS Identity and Access Management (IAM) role but that can be created once in the AWS Console. The script runs the Maven build, deletes the old AWS Lambda function, then creates the new AWS Lambda function:

1
2
3
mvn clean package
aws lambda delete-function --function-name CloudWatchLogger
aws --region us-east-1 lambda create-function --function-name CloudWatchLogger --zip-file fileb://./target/logger-1.0-SNAPSHOT.jar --runtime java8 --role arn:aws:iam::0123456789012:role/MyLambdaRole --handler cloud.cloudninja.java.cw.Handler

Once we run this script, we should see the AWS Lambda function in the AWS Console:

AWS Console showing a Lambda function

The next step is to test the Lambda function from the AWS Console. We can do this with the “Test” option. First we need to setup the test:

AWS Console showing a Lambda function test

Once the test is defined, we can execute it. There is a expandable details section which provides more details:

AWS Console showing a Lambda function test

We now have a working Lambda function. From this point, stepwise refinement is the key. There are several paths forward:

  • Improve the Lambda function itself, making the test JSON which is passed in more representative of what the Lambda function will actually receive and repeatedly improving the Lambda function, redeploying it using the script, and retesting it from the AWS Console.
  • Improve the integration. The work so far has provided a stable implementation to work with, but it isn’t tied to anything. I suggest using the AWS Console to attach the Lambda function to its trigger, such as an AWS Kinesis Data Stream, an Amazon Simple Queue Service (SQS) queue, or AWS CloudWatch Events / Amazon EventBridge. Once the integration is working in the AWS Console, try to replicate it using the AWS CLI, or AWS CloudFormation.
  • Switch from Lambda CLI to CloudFormation CLI. Calling AWS Lambda via the CLI provide very fast feedback, but if CloudFormation is used to deploy to production, there will be a point at which a CloudFormation template must be developed. A script can still be used to run the CloudFormation create-stack locally though to avoid the delay of committing the template, pushing it, and waiting for the build pipeline to process it. Simply update the shell script from AWS Lambda CLI calls to Amazon CloudFormation CLI calls.

I hope this makes writing AWS Lambda functions in Java a little more efficient!


Tags:  lambda  functions  faas  cloudformation  java  jar  CLI  console  maven  mvn  shade  test  aws
Categories:  AWS  Compute  AWS Lambda  FaaS

See Also

 Top Ten Tags

AWS (43)   Kinesis (9)   Streams (8)   AWS Console (5)   Go (5)   Analytics (4)   Data (4)   database (4)   Amazon DynamoDB (3)   Amazon Elastic Compute Cloud (EC2) (3)  


All Tags (173)

Disclaimer

All data and information provided on this site is for informational purposes only. cloudninja.cloud makes no representations as to accuracy, completeness, currentness, suitability, or validity of any information on this site and will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use. All information is provided on an as-is basis.

This is a personal weblog. The opinions expressed here represent my own and not those of my employer. My opinions may change over time.