Serverless compute with Java, on AWS Lambda

Serverless computing is a cloud computing execution model, in which the cloud provider dynamically manages the allocation of machine resources. Serverless computing allows running applications and services, without thinking much about servers, runtime resources, or scaling issues.

A very simple serverless computing application is intended to be used as a template project, a model, or starting point, for your next Java-based serverless cloud project. Here are the cornerstones:

  • Java 8 is used as the implementation language of the serverless function(s)
  • AWS Lamba is used as the serverless runtime
  • Gradle is used as the build automation system to compile, build, and deploy the serverless function(s).
  • JUnit 4 is used for unit-testing
  • Jackson is used as the the JSON processor to serialize and deserialize objects
  • Apache Log4J 1.2 is used as the remote logger on the serverless runtime system

In the last section of this article, I show how the lambda function can be adjusted, to provide the business logic (serve as fulfillment) for Amazon Lex, and how the Lex chatbot can be exposed as a Slack application.

 

Task

Using Java, to implement an AWS-Lambda function and expose it through the AWS-API-Gateway. The task of the AWS Lambda function is simple, to determine if a provided number is a prime number or not. Here is an example of the input and output:

  • PrimeCheck: Find out if the number posted in a json-payload is a prime number, and return a response like this:
    • {“number”:139} -> {“answer”:”Yes, 139 is a prime number, divisible only by itself and 1″,”n”:139,”d”:1}
    • {“number”:141} -> {“answer”:”No, 141 is not a prime number. For instance, it’s divisible by 3″,”n”:141,”d”:3}

For instance, by sending an HTTP POST request like so:

curl -H "Content-Type: application/json" -X POST -d '{"number":141}' https://123.execute-api.us-east-1.amazonaws.com/beta/primecheck

Before looking at Java code and Gradle build script, let’s get some administrative stuff out of the way 1st …

1 Creating an AWS Account

Create an AWS Account (aka Root User Account) here:
https://portal.aws.amazon.com/gp/aws/developer/registration/index.html
A credit card number and working phone is needed during the setup process. Select “Basic” for the support plan. (This will take a while, take your time.)

2 Creating an IAM Account

Create an IAM Account (aka AWS Identity and Access Management (IAM) user) here:
https://console.aws.amazon.com/iam

2.1 Group

Select ‘Group’ on the left side and then click the “Create new Group” button.
Enter a name (e.g. MyEduGroup) and in the next step add these three policies:

  • AWSLambdaFullAccess
  • IAMFullAccess
  • AmazonAPIGatewayAdministrator

Review and click the “Create Group” button.

2.2 User

Select ‘User’ on the left side and then click the “Add user” button, to create a new IAM user.
Create a user name (e.g. MyEduUser) and select “Programmatic access” only.
Next, add the user to the group, mentioned above, (e.g. MyEduGroup)
Review and click the “Create user” button and don’t forget to download the csv file, containing the credentials.

2.3 Role

Select ‘Roles’ on the left side and then click the “Create new role” button, to create a new role.
Select AWS Lambda
Select the AWSLambdaBasicExecutionPolicy
Assign a name to the new role (e.g. MyEduLambdaExecRole) and click the “Create role” button.

 

3 AWS CLI

Having the AWS Command Line Client available can be convenient, but almost everything it does, can also be accomplished using the web UI; Moreover, we will do the more repetitive things, like creating, updating, or invoking Lambda functions, with Gradle tasks. So installing the AWS CLI is optional.

Download Python 3.x.x from here https://www.python.org/downloads/ and run the package installer.

In terminal, run these commands to install pip and awscli

mkdir ~/awstmp
cd ~/awstmp
curl -O https://bootstrap.pypa.io/get-pip.py
python3 get-pip.py --user
pip3 install --user --upgrade awscli
rm -rf ~/awstmp

The AWS Command Line Client is now installed in this directory: ~/Library/Python/3.6/bin and after adding this line to the hidden ~/.bash_profile file:

export PATH=$PATH:~/Library/Python/3.6/bin

the aws cli can be launched easily, e.g.:

~/aws --version

4 AWS Profile and Credentials

The default file location for the config file can be overridden by setting the AWS_CONFIG_FILE environment variable. However, by default, a ~/.aws/credentials and a ~/.aws/config file are expected. The AWS CLI can be used to create those files (e.g. aws configure –profile MyEduUser) or the files can be manually created, copying values from the csv file, downloaded in Step 2.2. The awscli will not consume the credentials file directly. I.e., you have to copy and paste the key_id and access_key values.

~/.aws/credentials

[MyEduUser]
aws_access_key_id = AKIA****************
aws_secret_access_key = fnlBt/nB********************************

~/.aws/config

[profile MyEduUser]
region = us-east-1
output = json

5 Lambda Function Implementation

The aws-lambda-java-core library contains java interfaces, mostly for convenience. There is this RequestHandler interface for instance:

public interface RequestHandler<I, O> {
    /**
     * Handles a Lambda Function request
     * @param input The Lambda Function input
     * @param context The Lambda execution environment context object.
     * @return The Lambda Function output
     */
    public O handleRequest(I input, Context context);
}

The environment context object provides access to log, configured memory, execution time and more. It also provides access to the ClientContext, which has a mutable custom Map<String,String>

5.1 RequestHandler Implementation

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import org.apache.log4j.Logger;

/**
 * The Prime class implement an RequestHandler interface,
 * i.e. {@link Request} and {@link Response} classes with (Jackson annotations) have been implemented.
 * This demo reads an integer from the Request class and finds out, if that is a prime number.
 * The result will returned in an instance of the Response class.
 */
public class Prime implements RequestHandler<Request, Response> {
    private static final Logger log = Logger.getLogger(Prime.class);

    /**
     * Check if the provided number is evenly divisible only by itself and one.
     *
     * @param n {@link Long}
     * @return {@link boolean} true, if the provided long is a prime number
     */
    static Response check(final long n) {...}

    /**
     * Handles a Lambda Function request
     *
     * @param input   {@link Request} The Lambda Function input
     * @param context {@link Context} The Lambda execution environment context object.
     * @return {@link Response} - The Lambda Function output
     */
    @Override
    public Response handleRequest(final Request input, final Context context) {
        log.info("Request received:" + input);
        return Prime.check(input.getNumber());
    }
}

Declaring that the Prime class implements RequestHandler<Request, Response>, means that it will receive a Request object and return a Response object, both of which still need to be defined. Since Lambda need to deserialize the Request object and eventually serialize the Response object, both classes will need to be carefully crafted, either by providing (JavaBean-style) getter and setter methods or by using jackson.annotation. To show both, the Request class uses getter and setter methods, while the Response class uses Jackson’s JsonProperty annotation.

5.2 Request

public class Request {
    private long number;

    public Request() {
    }

    long getNumber() {
        return number;
    }

    public void setNumber(final long number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return String.valueOf(number);
    }
}

5.3 Response

import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.log4j.Logger;

public class Response {
    static final Logger log = Logger.getLogger(Response.class);

    @JsonProperty("answer")
    public final String answer;
    @JsonProperty("n")
    public long n;
    @JsonProperty("d")
    public long d = 1;

    public Response(final long n, final long d) {
        this.n = n;
        this.d = d;
        answer = String.format("No, %d is not a prime number. It's divisible by %d", n, d);
    }

    public Response(final long n) {
        log.info("Found a prime number " + n);
        this.n = n;
        answer = String.format("Yes, %d is a prime number, divisible only by itself and 1", n);
    }

    public Response() {
        answer = "A prime number is a natural number greater than 1 that has no positive divisors other than 1 and itself.";
    }

    @Override
    public String toString() {
        return answer;
    }
}

5.4 Logging

To see the log statements in cloudwatch, another AWS service, accessible here: https://console.aws.amazon.com/cloudwatch, the ./src/main/resources/log4j.properties file should look like this:

# Root logger option
log4j.rootLogger=INFO, LAMBDA

#Define the LAMBDA appender
log4j.appender.LAMBDA=com.amazonaws.services.lambda.runtime.log4j.LambdaAppender
log4j.appender.LAMBDA.layout=org.apache.log4j.PatternLayout
log4j.appender.LAMBDA.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss} <%X{AWSRequestId}> %-5p %c{1}:%m%n

 

6 Build – Deploy – Invoke

The AWS Command Line Client provides commands to create, update, or invoke a lambda function:

aws lambda create-function \
--profile MyEduUser \
--function-name lambda-pojo \
--role arn:aws:iam::123456789012:role/MyEduLambdaExecRole \
--handler com.wolfpaulus.aws.Prime \
--zip-file fileb:///Users/wolf/IdeaProjects/LambdaTemplate/build/distributions/LambdaTemplate.zip \
--description "Lambda Template" \
--runtime java8 \
--region us-east-1 \
--timeout 15 \
--memory-size 128 \
--publish
aws lambda update-function-code \
--profile MyEduUser \
--function-name lambda-pojo \
--zip-file fileb:///Users/wolf/IdeaProjects/LambdaTemplate/build/distributions/LambdaTemplate.zip \
--region us-east-1 \
--publish
aws lambda invoke \
--profile MyEduUser \
--function-name lambda-pojo \
--region us-east-1 \
--payload '{"number": 17}' output.txt

However, using Gradle talks for this instead, allows for more a convenient deployment from inside an IDE. The first few lines declare the necessary values:

  1. awsProfileName –  The AWS User Profile name, like declared in ~/.aws/credentials
  2. roleName – That is the role ARN, available in the IAM console and should look something like this:  arn:aws:iam::123…….:role/MyEduLambdaExecRole
  3. lambdaFunctionName – The name of the Lambda function, e.g., lambda-pojo
  4. handlerName – That is the fully qualified class name of the class implementing the RequestHandler interface, e.g. com.wolfpaulus.aws.Prime
  5. jsonPayload –  A JSON encoded String that can be used to test the Lambda function, e.g. {\”number\”: 17}
// A Gradle Buildscript for an AWS Lambda Java Project.
// After providing values for the five variables below,
// run the deploy and invoke tasks, to see it working.

def awsProfileName="MyEduUser"
def roleName="arn:aws:iam::123456789012:role/MyEduLambdaExecRole"
def lambdaFunctionName= "lambda-pojo"
def handlerName="com.wolfpaulus.aws.Prime"
def jsonPayload="{\"number\": 17}"


// Gradle AWS Plugin
// more details: https://github.com/classmethod/gradle-aws-plugin (Req. Java 8)
import com.amazonaws.services.lambda.model.InvocationType
import com.amazonaws.services.lambda.model.LogType
import com.amazonaws.services.lambda.model.Runtime
import jp.classmethod.aws.gradle.lambda.AWSLambdaInvokeTask
import jp.classmethod.aws.gradle.lambda.AWSLambdaMigrateFunctionTask

buildscript {
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath "jp.classmethod.aws:gradle-aws-plugin:0.30"
    }
}

apply plugin: 'java'
apply plugin: 'jp.classmethod.aws.lambda'

group 'com.wolfpaulus.aws'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    // AWS Lambda
    compile group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.1.0'
    compile group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '1.3.0'
    // Logging
    compile group: 'com.amazonaws', name: 'aws-lambda-java-log4j', version: '1.0.0'
    // Unit Test
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

task buildZip(type: Zip, dependsOn: test) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtime
    }
}

aws {
    profileName = awsProfileName
}

task deploy(type: AWSLambdaMigrateFunctionTask, dependsOn: build) {
    functionName = lambdaFunctionName
    handler = handlerName
    role = roleName
    runtime = Runtime.Java8
    zipFile = buildZip.archivePath
    memorySize = 128
    timeout = 15
}

task invoke(type: AWSLambdaInvokeTask) {
    functionName = lambdaFunctionName
    invocationType = InvocationType.RequestResponse
    logType = LogType.Tail
    payload = jsonPayload
    doLast {
        println "Status code: " + invokeResult.statusCode
        println "Log result: " + new String(Base64.getDecoder().decode(invokeResult.logResult))
        println "Lambda function result: " + new String(invokeResult.payload.array(), "UTF-8")
    }
}

build.dependsOn buildZip

With this grade build script in place, the deploy task will build the project, package the distribution in a ZIP file, and deploy the Lambda function. If is doesn’t exist already, it will also create the function.

1:32:18 PM: Executing external task 'deploy'...
:compileJava
:processResources
:classes
:jar
:assemble
:compileTestJava
:processTestResources
:testClasses
:test
:buildZip
:check
:build
:deploy

BUILD SUCCESSFUL

Total time: 10.407 secs
1:32:29 PM: External task execution finished 'deploy'.

The invoke task will send the payload and display the tail of the log and the response.

11:10:10 AM: Executing external task 'invoke'...
:invoke
Status code: 200
Log result: START RequestId: 7df67da9-761b-11e7-9eae-976e58fd49e7 Version: $LATEST
2017-07-31 18:10:12 <7df67da9-761b-11e7-9eae-976e58fd49e7> INFO  Prime:Request received:17
2017-07-31 18:10:12 <7df67da9-761b-11e7-9eae-976e58fd49e7> INFO  Response:Found a prime number 17
END RequestId: 7df67da9-761b-11e7-9eae-976e58fd49e7
REPORT RequestId: 7df67da9-761b-11e7-9eae-976e58fd49e7	Duration: 564.62 ms	Billed Duration: 600 ms 	Memory Size: 128 MB	Max Memory Used: 44 MB	

Lambda function result: {"answer":"Yes, 17 is a prime number, divisible only by itself and 1","n":17,"d":1}

BUILD SUCCESSFUL

Total time: 2.273 secs
11:10:12 AM: External task execution finished 'invoke'.

7 Web Service

The lambda function still needs to be made available as a Web service, which can be achieved with the API-Gateway.

Login to the API-Gateway here: https://console.aws.amazon.com/apigateway/home and click “Get Started”, then select ‘New API’ and enter a name, e.g. MyEduLambdaApi. and click the “Create API” button.

On the next page, select ‘Resources’ on the left side and select “Create Resource” in the Actions dropdown. Provide a resource name, e.g. PrimeCheck and click the “Create Resource” button to create the resource.

Now select “Create Method” in the Actions dropdown, select ‘Post’ and click the checkmark icon on its right, to confirm your selection. For the “Integration Type”, choose “Lambda Function”. The Lambda Region must match the region where the Lambda function is hosted and the Lambda function is of course the name of the Lambda function (not the name of the handler).

 

7.1 Testing

Clicking on Test, provides a text field on the bottom of the page, to enter a payload. I entered {“number”:17}, clicked the Test button and saw this response:

Request/
Status: 200
Latency: 3062 ms

Response Body

{
  "answer": "Yes, 17 is a prime number, divisible only by itself and 1",
  "n": 17,
  "d": 1
}

Response Headers

{"X-Amzn-Trace-Id":"sampled=0;root=1-597f7871-76a1d5d932d731163c346fff","Content-Type":"application/json"} 

7.2 Deploying

Finally, select “Deploy API” in the Actions dropdown, select [New Stage] enter provide a name, e.g. beta and click the “Deploy” button. The invoke URL will be displayed on the next page and looks something like this:

Invoke URL: https://123.execute-api.us-east-1.amazonaws.com/beta

It’s important to note that the URL is not complete yet. The Resource Path, created in the step where the post method was generated, e.g., “/primecheck” still needs to be attached.

Therefore, the complete URL to access this AWS Lambda function would look like this:
https://123.execute-api.us-east-1.amazonaws.com/beta/primecheck

Posting a payload

curl -X POST -d '{"number":141}' https://123.execute-api.us-east-1.amazonaws.com/beta/primecheck

Result

{"answer":"No, 141 is not a prime number. It's divisible by 3","n":141,"d":3}

8 Git Repository

https://github.com/wolfpaulus/LambdaTemplate

 

9 Using AWS Lambda with Amazon Lex

Lex is Amazon’s service for building conversational interfaces (CUI). Lex provides the advanced deep learning functionalities of automatic speech recognition (ASR) for converting speech to text, and natural language understanding (NLU) to recognize the intent of the text, to enable you to build applications with highly engaging user experiences and lifelike conversational interactions.

9.1 Request from Lex

Before configuring the NLU, i.e. telling Lex what user intents we want it to identify and which entities (aka slots) we accept, lets add some code to the Prime project.

Eventually, Lex will send a request that contains more than just the payload we are accepting so far. Lex also expects a different response. So we are implementing ./lex/LexRequest and ./lex/Response classes and adding a new method to the ./Prime class:

public LexResponse handleLexRequest(final LexRequest input, final Context context) {
        log.info("LexRequest received:" + input.toString());
        String s;
        try {
            final long p = Long.parseLong(input.currentIntent.slots.get("number"));
            s = Prime.check(p).answer;
        } catch (Exception e) {
            s = e.getLocalizedMessage();
        }
        return new LexResponse(s, input.sessionAttributes);
    }

We leave the old Lambda function out there, create a new one, replacing the value of the lambdFunctionName and handlerName in the gradle build script like so:

//def lambdaFunctionName= "lambda-pojo"
//def handlerName="com.intuit.iat.Prime"
def lambdaFunctionName= "lambda-lex"
def handlerName="com.wolfpaulus.aws.Prime::handleLexRequest"

The implementation of the LexRequest and enclosed CurrentIntent class are direct JSON to Java Class translation, both used when the request arrives and gets deserialized (with Jackson). The same is true for the LexResponse and DialogAction classes. Both are used to deserialize objects into JSON. The model definition was found here.

Deploy the new Lambda function before moving on.

9.2 Bot Creation

Login to the Lex console https://console.aws.amazon.com/lex/home and click the “Create button”

Select ‘Custom Bot’ and give the bot a name (e.g. PrimeCheck). I selected Joanna as the output voice and set the session timeout to 1 min.

Name the intent (e.g. isPrime) and type a sample utterance, using braces to mark a variable, like so: Is ​{number}​ a prime number. Click to blue + sign to confirm. Enter several sample utterances, for how you think one might ask.

Type the name of the variable into the slot name field and select AMAZON.NUMBER for its type. Enter a prompt, for asking the user, if a number could not be found in his input. Again, click the blue + to enter the slot definition.

Select AWS Lambda in fulfillment and select the newly deployed Lambda function (e.g. lambda-lex) and select None for the Follow-up message.

After clicking the build button  (top-right), the bot can be tested.

Publish the bot, before moving on.

10 Creating a Slack Application

Sign into Slack https://slack.com/apps and click the “Build” button, then click “Building Slack apps” and “Create a slack app”

In the left menu, click ‘Bot Users’ on the left add a bot user, (e.g., @prime.) Select “Always Show My Bot as Online”, before adding the Bot User.
Now click ‘Interactive Messages’ on the left and click “Enable Interactive Messages”. Enter any valid URL (e.g. https://wolfpaulus.com) and click the “Enable Interactive Messages” button.
Leave this browser window open, as we will need to copy the values for ClientID, Client Secret, and Verification Token.

10.1 Integrating a Slack app with Lex

Login to the Lex console https://console.aws.amazon.com/lex/home and select your Lex bot, then click the Channels tab on top and select Slack (on the left side).
Fill out the form, by providing a name (e.g.,PrimeSlackIntegration), leave the KMS key as “was/lex”, select the alias (e.g., prime), and copy the three values from the other browser, still showing the Slack app: ClientID, Client Secret, and Verification Token.
Don’t forget to click the “Activate” button and leave this browser open, as we need the here displayed OAuth URL and Postback URL in the next step.

.. back on Slack ..

Now back in the browser with the Slack app, click on ‘OAuth & Permissions’ on the left. Click the Add a new Redirect URL button and paste the OAuth URL and click “Apply”.
In the “Permission Scope” drop-down below, start typing “chat:write:bot”, and select. Then do it again with “team:read”. Click Save Changes.

Now click on ‘Interactive Messages’ on the left. Update the “Request URL” with the Postback URL from step 10.1. Apply by clicking the “Save changes” button.

Click on ‘Event Subscriptions’ on the left and select “On”. Again, set the Request URL to the Postback URL. Click the “Add Bot User Event” button, and find the “message.im” event. Apply by clicking the “Save changes” button.

Click on ‘Manage Distribution’ on the left and click the “Add to Slack” button. After authorizing the app for the slack team, the prime user should appear active and ready to talk …

2 Replies to “Serverless compute with Java, on AWS Lambda”

  1. Nice post. Liked the step by step procedure to build the bot.
    is there a way to integrate a similar bot in a Java web application.

Leave a Reply