back to all blogsSee all blog posts

Run a MicroProfile application as a serverless function with CloudEvents in IBM Cloud Code Engine

image of author image of author
Alex Butcher and Emily Jiang on Oct 17, 2022
Post available in languages:

Serverless applications, which can scale to zero and allocate resources on demand, are certainly a hot topic in 2022. With MicroProfile widely adopted by cloud native applications, many developers want to know:

Can MicroProfile applications be serverless?

The short answer is YES. This post demonstrates how to run a MicroProfile application on Open Liberty as a serverless function in IBM Cloud Code Engine.

What is serverless?

Serverless is short for serverless computing, which is an execution model in which the cloud provider allocates resources on demand. Serverless enables you to concentrate on your applications without needing to manage servers. It does not mean no server is running in the background. On the contrary, serverless architecture contains servers.

Serverless normally executes code in the form of a function and is sometimes referred to Function as a Service (FaaS) or Container as a Service (CaaS). A few major cloud providers support serverless applications:

  • IBM Cloud Code Engine: IBM Cloud Function

  • AWS: AWS Lambda

  • Microsoft Azure: Azure Functions

  • Google Cloud: Google Cloud Functions

This post uses the following specifications and frameworks to create and run a serverless application.

CloudEvents

CloudEvents is a specification to describe event data in a common language-agnostic way. CloudEvents simplifies event declaration and delivery across services and platforms. This specification is under the Cloud Native Computing Foundation (CNCF). CloudEvents are supported by many serverless frameworks, including IBM Cloud Code Engine.

IBM Cloud Code Engine

IBM Cloud Code Engine (ICCE) is a fully managed, serverless platform designed to run container images, batch jobs, or source code. Code Engine allows you to build event-driven workloads that react to CloudEvents. Code Engine is truly serverless. It automatically scales your workloads up and down, even down to zero when there are no requests. You pay for only the resources you consume. You can easily run your cloud native applications in Code Engine.

MicroProfile

Many developers use MicroProfile specifications for configuring, securing, and observing cloud native applications. MicroProfile offers a set of standard APIs for cloud-native applications that free your applications from vendor lock-in. Open Liberty is the leading implementation for MicroProfile specifications.

As we enter the serverless era, it is very important to get MicroProfile applications running in a serverless environment. Luckily, it is very straightforward to get these applications running as functions in an environment that supports CloudEvents, such as IBM Cloud Code Engine. In the following sections, we demonstrate how to get a MicroProfile application that is deployed to Open Liberty running as a function in IBM Cloud Code Engine with CloudEvents.

Run MicroProfile applications as a serverless function in Code Engine

RESTful MicroProfile applications can run seamlessly as a serverless function in Code Engine with CloudEvent. This section explains how.

Prerequisites

Before you start, you will need the following prerequisites:

Create your MicroProfile application

In this post, we use the OpenLiberty Getting Started guide to get a simple MicroProfile application that is well suited to run in a serverless environment.

If you already have experience with OpenLiberty and MicroProfile, you can use the application files in the finish folder of the Getting Started Guide as your starting point and go to the next step. If you are new to Open Liberty, complete the guide up to and including the Running the application in a Docker container step to generate the application you’ll use in this post.

After you have the application, the next step is to test it to ensure that it is running and accessible.

Run the following command to run your container locally on its HTTP port:

docker run -p 9080:9080 openliberty-getting-started:1.0-SNAPSHOT

Run the following curl command to invoke the application and confirm that it is accessible:

curl http://localhost:9080/dev/system/properties

This command invokes the getProperties() method in the application’s SystemResource class.

Deploy your application to IBM Cloud Container Registry to prepare for IBM Cloud Code Engine

After you successfully build and test your image locally, you now need to put it somewhere that ICCE can get to it.

With ICCE, you can get your code into a runnable format either by using a source code respository, such as GitHub, or an externally accessible container registry, such as a public registry (eg. DockerHub), IBM Cloud Container Registry, or a private registry.

Since you already have a IBM Cloud account, this post focuses on IBM Cloud Container Registry (ICCR), which provides the quickest route to getting your image up and running. Log in to your IBM Cloud account and follow these steps to upload your image.

Create your IBM Cloud Code Engine application

With your image uploaded, you now need to create and configure your ICCE application to make your function available.

Follow these steps to create your IBM Cloud Code Engine application from your image in ICCR.

When you create your application, consider the following options:

Image reference

While the name of your image will stay the same, you will be updating it later. Consider whether to use the image hash that is within the registry or the tag you uploaded with that. Be aware that an update to the image may not be reflected when executed in ICCE if it uses the old tag.

Resource allocation

As part of the application definition, you can tell ICCE how much CPU and Memory to allocate to any running instances. While the application is small, it is still a Java application that needs a period of startup before it can start serving requests. The Getting Started image will eventually start on the minimum values, but giving it slightly more will significantly improve startup and response time.

Listening port

Use port 9080 as the value for the listening port. For more information, see Considerations for HTTP handling.

After you create your application, ensure that it is not showing any errors such as Missing Pull credentials, which indicates that the image cannot be pulled to run. If you have any of these errors, follow the steps in the ICCE documentation to resolve. Some errors might occur only when the application is invoked for the first time.

Invoke your application on IBM Cloud Code Engine

Now that you have the application, you can invoke it within ICCE.

As part of creating your application on ICCE, you obtained the application URL from the test application or the command line. If you did not get the URL, follow these steps to get it.

All ICCE connections are HTTPS. So while HTTP was used locally, the image is configured to support HTTPS without any changes. If you make a request to https://{ICCE_Application_URL}/, you should see the Welcome to Open Liberty page. To call the application on ICCE, we can use the same path that we used for the application locally. Run the following curl command:

curl https://${ICCE_Application_URL}/dev/system/properties

Similar to the local call that you made previously, after a short time, you get a JSON payload that contains all the system properties.

Congratulations! You just called your application on IBM Cloud Code Engine.

Update the MicroProfile application to use CloudEvents

A common use case for serverless applications is to process events coming from non-HTTP sources, such as Kafka topics or object stores. Historically, to consume these events, the application had to use the Cloud provider’s SDK, which locks the application into that provider. This is where CloudEvents comes in. It provides a cross-provider standard around which applications can transmit and receive data, improving portability and reducing large dependencies.

ICCE connects event providers such as IBM Cloud Event Streams or IBM Cloud Object Storage to the application by using CloudEvents. These events are sent to a subscribed application as HTTP POST requests. Because the requests are in HTTP format, a RESTful application can receive these events without needing additional libraries and configuration.

To be able to process CloudEvents in Open Liberty, add the CloudEvents restful-ws-jakarta library to the application by adding the following dependency to your pom.xml:

<dependency>
    <groupId>io.cloudevents</groupId>
    <artifactId>cloudevents-http-restful-ws-jakarta</artifactId>
    <version>2.5.0</version>
</dependency>

When you run the CloudEventsProvider class within the context of Open Liberty, it is automatically configured to marshal and unmarshal CloudEvents.

With the library included, you can now update the SystemResource class from the Getting Started guide application to use CloudEvents.

Review the completed CloudEvents SystemResource class

Before you update the SystemResource class, take a moment to review the the completed CloudEvents SystemResource class, which contains all the code changes you will implement in the following sections. You can refer back to this example to check that changes you make align with the expected result. Once complete, the SystemResource class should look very similar to this:

// tag::copyright[]
/*******************************************************************************
 * Copyright (c) 2017, 2022 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - Initial implementation
 *******************************************************************************/
// end::copyright[]
package io.openliberty.sample.system;

import io.cloudevents.CloudEvent;
import io.cloudevents.CloudEventData;
import io.cloudevents.core.builder.CloudEventBuilder;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.ws.rs.POST;

import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Timed;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

@RequestScoped
@Path("/properties")
public class SystemResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Timed(name = "getPropertiesTime",
           description = "Time needed to get the JVM system properties")
    @Counted(absolute = true, description
             = "Number of times the JVM system properties are requested")
    public CloudEvent getProperties() {
        /* java.util.properties does not have a direct way to obtain a byte[] so store in an intermedietary Map first*/
        Map properties = System.getProperties();
        Jsonb jsonb = JsonbBuilder.create();
        /* convert properties map into a JSON string which can then be converted into a byte[]*/
        String jsonString = jsonb.toJson(properties);
        return CloudEventBuilder.v1()
                .withData(jsonString.getBytes())
                .withDataContentType("application/json")
                .withId("properties")
                .withType("java.properties")
                .withSource(URI.create("http://system.poperties"))
                .build();
    }

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Timed(name = "queryPropertiesTime",
            description = "Time needed to query the JVM system properties")
    @Counted(absolute = true, description
            = "Number of times the JVM system properties are queried")
    public CloudEvent queryProperties(CloudEvent query){
        Map properties = System.getProperties();
        HashMap<String,String> props = new HashMap<>((Map<String,String>)properties);
        HashMap<String,String> qProps = new HashMap<String,String>();
        Jsonb jsonb = JsonbBuilder.create();

        /* Pull data from the data portion of the CloudEvent - this is in binary format so convert it into a standard String*/
        CloudEventData data = query.getData();
        String jsonString = new String(data.toBytes(), StandardCharsets.UTF_8);

        /* Take the Json Array data and use that to pull out the request properties */
        ArrayList<String> tProps = jsonb.fromJson(jsonString, ArrayList.class);
        for(String key: tProps){
            qProps.put(key, props.get(key));
        }

        /* return a CloudEvent with our queried properties */
        return CloudEventBuilder.v1()
                .withData(jsonb.toJson(qProps).getBytes())
                .withDataContentType("application/json")
                .withId("properties")
                .withType("java.properties")
                .withSource(URI.create("http://system.poperties"))
                .build();
    }

}

Return a CloudEvent

First, update the SystemResource class response Type from Response to CloudEvent. In the method declaration, replace Response with CloudEvent:

public CloudEvent getProperties() {

Now, we need to construct a CloudEvent to return. However, first we need to do some work on the system properties to be able to include them as the data within the event.

CloudEvents cannot convert the data from Object to byte[] and requires the data to be in a binary format when it is provided during its building process. As such, we can take the properties from System.getProperties() and make them into a JSON string by using Jsonb.

/* java.util.properties does not have a direct way to obtain a byte[] so store in an intermediary Map first*/
    Map properties = System.getProperties();
    Jsonb jsonb = JsonbBuilder.create();
/* convert properties map into a JSON string which can then be converted into a byte[]*/
    String jsonString = jsonb.toJson(properties);

With our data in string format, we can now get the byte[] representation of the data.

The CloudEventBuilder class provides the necessary components to build our CloudEvent. Use the most recent specification version, which is v1().

    return CloudEventBuilder.v1()
        .withData(jsonString.getBytes())
        .withDataContentType("application/json")
        .withId("properties")
        .withType("java.properties")
        .withSource(URI.create("http://system.poperties"))
        .build();

Besides withData(), the rest of the methods set the values that will be returned as headers in the response. Once all the required properties are set, you can build the CloudEvent Object.

This sample provides only the required properties for a valid CloudEvent. If any of these properties are missing, an exception is thrown. To see which properties are required, you can review the specification.

Because CloudEvents can come from a wide variety of sources that might differ even within a single provider, the majority of the fields are fairly free-form.

Receive a CloudEvent

Now that we’ve returned a CloudEvent, how can we receive one in the application?

First, we’ll enhance the SystemResource class to add a query method that can send a POST request with a body that contains the system properties that we want returned.

The body of the request will be a JSON array that contains each property we want returned as part of the request.

["java.vendor.url","awt.toolkit"]

Add the following method declaration to the SystemResource class.

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Timed(name = "queryPropertiesTime",
            description = "Time needed to query the JVM system properties")
    @Counted(absolute = true, description
            = "Number of times the JVM system properties are queried")
    public CloudEvent queryProperties(CloudEvent query){


}

In this case, we will return a CloudEvent, but you can return any type that confirms the request was received, such as Response.ok().build();.

Inside the method, we need to do some of the same things that we did within the getProperties() method. But we also must handle the CloudEvent input.

For some initial structure, add this block to the top of the method.

Map properties = System.getProperties();
HashMap<String,String> props = new HashMap<>((Map<String,String>)properties);
HashMap<String,String> qProps = new HashMap<String,String>();
Jsonb jsonb = JsonbBuilder.create();

This block gives us the Map of the properties, but in a form that is more useful, as we need to do more processing than we did within the getProperties() method.

To retrieve the data from the CloudEvent, we use .withData(); to extract the payload as an instance of CloudEventData. The data is in binary format, so needs to be converted to make it usable.

/* Pull data from the data portion of the CloudEvent - this is in binary format so convert it into a standard String*/
CloudEventData data = query.getData();
String jsonString = new String(data.toBytes(), StandardCharsets.UTF_8);

The conversion to a String allows us to process the JSON payload later. You can check what data type has been by inspecting the Data Content Type from getDataContentType() on the CloudEvent.

With the data now in a more usable format, we can start to process it and make use of its contents.

Because we have a JSON array, we can use jsonb to convert the JSON to an ArrayList of the keys that are requested from the properties HashMap.

/* Take the Json Array data and use that to pull out the request properties */
ArrayList<String> tProps = jsonb.fromJson(jsonString, ArrayList.class);
for(String key: tProps){
    qProps.put(key, props.get(key));
}

We use the other hashmap created at the start to store the properties we queried for.

Now that we have built our map of queried properties, it can be returned to the user in the same way we returned the full list of properties.

return CloudEventBuilder.v1()
    .withData(jsonb.toJson(qProps).getBytes())
    .withDataContentType("application/json")
    .withId("properties")
    .withType("java.properties")
    .withSource(URI.create("http://system.poperties"))
    .build();

Next, we’re ready to test the new method.

To invoke the method, we make a POST request against /dev/system/properties with the HTTP request being a CloudEvent. You can do this locally, or against a rebuilt Docker image.

To invoke this method, use the following curl command:

curl -X POST http://${ICCE_Application_URL}/dev/system/properties \
-H "Ce-Specversion: 1.0" \
-H "Ce-Type: properties" \
-H "Ce-Source: io.cloudevents.examples/properties" \
-H "Ce-Id: 536808d3-88be-4077-9d7a-a3f162705f78" \
-H "Content-Type: application/json" \
-H "Ce-Subject: resources" \
-d "[\"java.vendor.url\",\"awt.toolkit\"]"

In the same way that we returned a CloudEvent, when we make the request, we need to provide the required set of headers so that the application can correctly convert the request into a CloudEvent.

Update your ICCE application to use the serverless function

The application can now return and receive CloudEvents. We can update our application in ICCE.

To update your application, complete the following steps:

  1. Rebuild your Liberty application with the CloudEvent changes.

  2. Rebuild your docker container and publish to it ICCR, either by updating the image tag or leaving it as is if you are using the image hash.

  3. Update your ICCE application to use the new application version.

After you update your application, you can validate your changes in ICCE by invoking the same curl commands that we used locally, but replacing the protocol and hostname.

curl -X POST https://${ICCE_Application_URL}/dev/system/properties \
-H "Ce-Specversion: 1.0" \
-H "Ce-Type: properties" \
-H "Ce-Source: io.cloudevents.examples/properties" \
-H "Ce-Id: 536808d3-88be-4077-9d7a-a3f162705f78" \
-H "Content-Type: application/json" \
-H "Ce-Subject: resources" \
-d "[\"java.vendor.url\",\"awt.toolkit\"]"

If you open the IBM Cloud Code Engine UI to the Overview tab for your application, you can see the active instances while you are making requests. You can observe IBM Cloud Code Engine deploying the application instance and then scaling down to zero when no new requests are received.

IBM Cloud Code Engine Active Instances

You now have a MicroProfile application on Open Liberty running as a serverless function in IBM Cloud Code Engine with CloudEvents!