Serverless computing with AWS Lambda, Part 2

Integrate AWS Lambda with DynamoDB, then call Lambda functions from a Java client

The first half of this article presented an overview of serverless computing with AWS Lambda, including building, deploying, and testing AWS Lambda functions in an example Java application. In Part 2, you'll learn how to integrate Lambda functions with an external database, in this case DynamoDB. We'll then use the AWS SDK to invoke Lambda functions from our example Java application.

AWS Lambda and DynamoDB

DynamoDB is a NoSQL document store that is hosted by Amazon Web Services (AWS). DynamoDB defines data abstractions as tables, which accept common database operations such as insert, retrieve, query, update, and delete. As with many other NoSQL databases, DynamoDB's schema isn't fixed, so some items in the same table can have fields that others do not.

One of DynamoDB's best features is its tiered pricing model. Unlike the AWS Relational Database Service (RDS), in which AWS manages your database using EC2 instances that you pay for, DynamoDB is pay-as-you-go. You pay for the storage you use and the throughput of your queries, but you don't directly pay for any underlying virtual machines. Additionally, AWS gives you a free tier supporting up to 25 GB of space, with enough throughput to execute up to 200 million requests per month.

In Serverless computing with AWS Lambda, Part 1, we developed a simple, serverless Java application using Lambda functions. You can download the source code for the GetWidgetHandler application anytime. If you haven't already read Part 1, I suggest familiarizing yourself with the application code and examples from that article before proceeding.

Our first step is to setup the DynamoDB database in our AWS console. After that we'll update the get-widget function from Part 1 to retrieve a widget from a DynamoDB table.

Setup the DynamoDB database in AWS

We'll start by creating the DynamoDB table. From the AWS console, click on Services and choose DynamoDB from the database section, as shown in Figure 1.

jw osjp awslambda fig1 Steven Haines

Figure 1. Launch the DynamoDB dashboard

Once launched, you'll see the DynamoDB dashboard. Click the Create table button to start creating your table, shown in Figure 2.

jw osjp awslambda fig2 Steven Haines

Figure 2. Create a DynamoDB table

Now you'll see the page shown in Figure 3.

jw osjp awslambda fig3 Steven Haines

Figure 3. Create a DynamoDB table

Give your table a name (in this case "Widget") and set the primary key to id, leaving it as a String. Pressing Create when you are finished will direct you to the DynamoDB tables page. If you need to navigate to this page in the future, select Services-->DynamoDB, and click on Tables.

jw osjp awslambda fig4 Steven Haines

Figure 4. DynamoDB tables page

We'll manually create an entry in the new Widget table, so click the Create item button shown in Figure 5.

jw osjp awslambda fig5 Steven Haines

Figure 5. Create item page

DynamoDB will pre-populate the Create Item page with the id field. Enter an ID that is easy to remember, such as "1". Next, press the plus (+) next to the new ID, adding another field called name. Enter a value for the name field, such as "Widget 1". Press Save when you are finished.

Update the GetWidgetHandler class

With data in our database, the next thing we need to do is update the GetWidgetHandler class from Part 1. We'll start by adding the DynamoDB dependency to our original POM file. The updated pom.xml file is shown in Listing 1.

Listing 1. pom.xml (updated with DynamoDB dependency)

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.javaworld.geekcap</groupId>
    <artifactId>aws-lambda-java</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>aws-lambda-java</name>
    <url>http://maven.apache.org</url>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-dynamodb</artifactId>
            <version>1.11.135</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Listing 1 adds the aws-java-sdk-dynamodb dependency to the POM file from Part 1. Listing 2 shows the updated GetWidgetHandler class.

Listing 2. GetWidgetHandler.java (updated to load data from DynamoDB)


package com.javaworld.awslambda.widget.handlers;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.javaworld.awslambda.widget.model.Widget;
import com.javaworld.awslambda.widget.model.WidgetRequest;

public class GetWidgetHandler implements RequestHandler<WidgetRequest, Widget> {
    @Override
    public Widget handleRequest(WidgetRequest widgetRequest, Context context) {
        //return new Widget(widgetRequest.getId(), "My Widget " + widgetRequest.getId());

        // Create a connection to DynamoDB
        AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
        DynamoDB dynamoDB = new DynamoDB(client);

        // Get a reference to the Widget table
        Table table = dynamoDB.getTable("Widget");

        // Get our item by ID
        Item item = table.getItem("id", widgetRequest.getId());
        if(item != null) {
            System.out.println(item.toJSONPretty());

            // Return a new Widget object
            return new Widget(widgetRequest.getId(), item.getString("name"));
        }
        else {
            return new Widget();
        }
    }
}

The main interface to DynamoDB is the DynamoDB object. In order to create a DynamoDB instance, we need a DynamoDB client. Because our Lambda function will run in AWS, we do not need to provide credentials, so we can use the default client. Note that we'll only be able to query the database without credentials because the get-widget-role from Part 1 has the dynamodb:GetItem permission.

From the DynamoDB instance, we can call getTable("Widget") to retrieve a Table instance. Then we can call getItem() on the Table instance, passing it the primary key of the item we want to retrieve. If there is an item with the specified primary key then it will return a valid response; otherwise it will return null. The Item class provides access to the response parameters, so we finish up the implementation by creating a new Widget object with its name loaded from DynamoDB.

download
Get the code for the updated GetWidgetHandler application. Created by Steven Haines for JavaWorld.

Querying DynamoDB with DynamoDBMapper

There are several APIs for querying DynamoDB, from a RESTful service call, to the low-level interface above, to a couple of higher level interfaces. One of the more popular interfaces is DynamoDBMapper. This interface provides a similar construct to what you might find when mapping objects to relational data in a tool like Hibernate. Let's briefly review how to retrieve a Widget from DynamoDB using the DynamoDBMapper API.

The first thing that we need to do is add a few annotations to the Widget class, which is shown in Listing 3.

Listing 3. Widget.java (updated with DynamoDBMapper annotations)


package com.javaworld.awslambda.widget.model;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;

@DynamoDBTable(tableName="Widget")
public class Widget {
    private String id;
    private String name;

    public Widget() {
    }

    public Widget(String id) {
        this.id = id;
    }

    public Widget(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @DynamoDBHashKey(attributeName="id")
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @DynamoDBAttribute(attributeName="name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The DynamoDBTable annotation specifies the name of the DynamoDB table to which the Widget maps. The DynamoDBHashKey annotation identifies the primary key of the Widget table. And the DynamoDBAttribute annotation identifies other class attributes that map to database attributes in DynamoDB. If you had other attributes that you wanted to ignore, you could add the @DynamoDBIgnore annotation.

With the Widget class annotated, we can now update the GetWidgetHandler class to use the DynamoDBMapper, which is shown in Listing 4.

Listing 4. GetWidgetHandler.java (updated with DynamoDBMapper)


package com.javaworld.awslambda.widget.handlers;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.javaworld.awslambda.widget.model.Widget;
import com.javaworld.awslambda.widget.model.WidgetRequest;

public class GetWidgetHandler implements RequestHandler<WidgetRequest, Widget> {
    @Override
    public Widget handleRequest(WidgetRequest widgetRequest, Context context) {
        // Create a connection to DynamoDB
        AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();

        // Build a mapper
        DynamoDBMapper mapper = new DynamoDBMapper(client);

        // Load the widget by ID
        Widget widget = mapper.load(Widget.class, widgetRequest.getId());
        if(widget == null) {
            // We did not find a widget with this ID, so return an empty Widget
            context.getLogger().log("No Widget found with ID: " + widgetRequest.getId() + "\n");
            return new Widget();
        }
        
        // Return the widget
        return widget;
    }
}

In the former (Part 1) version of the GetWidgetHandler we created an AmazonDynamoDB instance, using a AmazonDynamoDBClientBuilder.defaultClient() call. Now we'll use that client to initialize a DynamoDBMapper instance instead.

The DynamoDBMapper class provides access to execute queries, load objects by ID, save objects, delete objects, and so forth. In this case, we pass DynamoDBMapper the widget's class (Widget.class) and its primary key. If DynamoDB has a Widget with the specified primary key it will return it; if not it will return null.

Rebuild and then re-upload your new JAR file by opening your Lambda function dashboard, then click on the Code tab and press Upload. When you re-upload and subsequently call your function, AWS Lambda will create a new container for the new JAR file and push that to an EC2 instance. You should expect the first run to be slow.

If you happen to encounter an OutOfMemoryError when you re-test your function, select the Configuration tab and open the Advanced Settings section. Here you can increase your memory, as shown below.

1 2 Page 1
Page 1 of 2