Serverless on Azure with Spring Cloud Function
This article will teach you how to create and run serverless apps on Azure using the Spring Cloud Function and Spring Cloud Azure projects. We will integrate with the Azure Functions and Azure Event Hubs services.
It is not my first article about Azure and Spring Cloud. As a preparation for that exercise, it is worth reading the article to familiarize yourself with some interesting features of Spring Cloud Azure. It describes an integration with Azure Spring Apps, Cosmos DB, and App Configuration services. On the other hand, if you are interested in CI/CD for Spring Boot apps you can refer to the following article about Azure DevOps and Terraform.
Source Code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. The Spring Boot apps used in the article are located in the serverless
directory. After you go to that directory you should just follow my further instructions.
Architecture
In this exercise, we will prepare two sample Spring Boot apps (aka functions) account-function
and customer-function
. Then, we will deploy them on the Azure Function service. Our apps do not communicate with each other directly but through the Azure Event Hubs service. Event Hubs is a cloud-native data streaming service compatible with the Apache Kafka API. After adding a new customer the customer-function
sends an event to the Azure Event Hubs using the Spring Cloud Stream binder. The account-function
app receives the event through the Azure Event Hubs trigger. Also, the customer-function
is exposed to the external client through the Azure HTTP trigger. Here’s the diagram of our architecture:
Prerequisites
There are some prerequisites before you start the exercise. You need to install JDK17+ and Maven on your local machine. You also need to have an account on Azure and az
CLI to interact with that account. Once you install the az
CLI and log in to Azure you can execute the following command for verification:
$ az account show
If you would like to test Azure Functions locally, you need to install Azure Functions Core Tools. You can find detailed installation instructions in Microsoft Docs here. For macOS, there are three required commands to run:
$ brew tap azure/functions
$ brew install azure-functions-core-tools@4
$ brew link --overwrite azure-functions-core-tools@4
Create Resources on Azure
Before we proceed with the source code, we need to create several required resources on the Azure cloud. In the first step, we will prepare a resource group for all required objects. The name of the group is spring-cloud-serverless
. The location depends on your preferences. For me it is eastus
.
$ az group create -l eastus -n spring-cloud-serverless
In the next step, we need to create a storage account. The Azure Function service requires it, but we will also use that account during the local development with Azure Functions Core Tools.
$ az storage account create -n pminkowsserverless \
-g spring-cloud-serverless \
-l eastus \
--sku Standard_LRS
In order to run serverless apps on Azure with e.g. Spring Cloud Function, we need to create the Azure Function App instances. Of course, we use the previously created resource group and storage account. The name of my Function App instances are pminkows-account-function
and pminkows-customer-function
. We can also set a default OS type (Linux), functions version (4
), and a runtime stack (Java) for each Function App.
$ az functionapp create -n pminkows-customer-function \
-c eastus \
--os-type Linux \
--functions-version 4 \
-g spring-cloud-serverless \
--runtime java \
--runtime-version 17.0 \
-s pminkowsserverless
$ az functionapp create -n pminkows-account-function \
-c eastus \
--os-type Linux \
--functions-version 4 \
-g spring-cloud-serverless \
--runtime java \
--runtime-version 17.0 \
-s pminkowsserverless
Then, we have to create the Azure Event Hubs namespace. The name of my namespace is spring-cloud-serverless
. The same as before I choose the East US location and the spring-cloud-serverless resource group. We can also set the pricing tier (Standard) and the upper limit of throughput units when the AutoInflate
option is enabled.
$ az eventhubs namespace create -n spring-cloud-serverless \
-g spring-cloud-serverless \
--location eastus \
--sku Standard \
--maximum-throughput-units 1 \
--enable-auto-inflate true
Finally, we have to create topics on Event Hubs. Of course, they have to be assigned to the previously created spring-cloud-serverless
Event Hubs namespace. The names of our topics are accounts
and customers
. The number of partitions is irrelevant in this exercise.
$ az eventhubs eventhub create -n accounts \
-g spring-cloud-serverless \
--namespace-name spring-cloud-serverless \
--cleanup-policy Delete \
--partition-count 3
$ az eventhubs eventhub create -n customers \
-g spring-cloud-serverless \
--namespace-name spring-cloud-serverless \
--cleanup-policy Delete \
--partition-count 3
Now, let’s switch to the Azure Portal. Find the spring-cloud-serverless
resource group. You should have the same list of resources inside this group as shown below. It means that our environment is ready and we can proceed to the source code.
App Dependencies
Firstly, we need to declare the dependencyManagement
section inside the Maven pom.xml
for three projects used in the app implementation: Spring Boot, Spring Cloud, and Spring Cloud Azure.
<properties>
<java.version>17</java.version>
<spring-boot.version>3.1.4</spring-boot.version>
<spring-cloud-azure.version>5.7.0</spring-cloud-azure.version>
<spring-cloud.version>2022.0.4</spring-cloud.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-dependencies</artifactId>
<version>${spring-cloud-azure.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Here’s the list of required Maven dependencies. We need to include the spring-cloud-function-context
library to enable Spring Cloud Functions. In order to integrate with the Azure Functions service, we need to include the spring-cloud-function-adapter-azure
extension. Our apps also send messages to the Azure Event Hubs service through the dedicated Spring Cloud Stream binder.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-azure</artifactId>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-stream-binder-eventhubs</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
</dependency>
</dependencies>
Create Azure Serverless Apps with Spring Cloud
Expose Azure Function as HTTP endpoint
Let’s begin with the customer-function
. To simplify the app we will use an in-memory H2 for storing data. Each time a new customer is added, it is persisted in the in-memory database using Spring Data JPA extension. Here’s our entity class:
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
private String status;
// GETTERS AND SETTERS
}
Here’s the Spring Data repository interface for the Customer
entity:
public interface CustomerRepository extends ListCrudRepository<Customer, Long> {
}
Let’s proceed to the Spring Cloud Functions implementation. The CustomerInternalFunctions
bean defines two functions. The addConsumer
function (1) persists a new customer in the database and sends the event about it to the Azure Event Hubs topic. It uses the Spring Cloud Stream StreamBridge
bean for interacting with Event Hubs. It also returns the persisted Customer
entity as a response. The second function changeStatus
(2) doesn’t return any data, it just needs to react to the incoming event and change the status of the particular customer. That event is sent by the account-function
app after generating an account for a newly created customer. The important information here is that those are just Spring Cloud functions. For now, they have nothing to do with Azure Functions.
@Service
public class CustomerInternalFunctions {
private static final Logger LOG = LoggerFactory
.getLogger(CustomerInternalFunctions.class);
private StreamBridge streamBridge;
private CustomerRepository repository;
public CustomerInternalFunctions(StreamBridge streamBridge,
CustomerRepository repository) {
this.streamBridge = streamBridge;
this.repository = repository;
}
// (1)
@Bean
public Function<Customer, Customer> addCustomer() {
return c -> {
Customer newCustomer = repository.save(c);
streamBridge.send("customers-out-0", newCustomer);
LOG.info("New customer added: {}", c);
return newCustomer;
};
}
// (2)
@Bean
public Consumer<Account> changeStatus() {
return account -> {
Customer customer = repository.findById(account.getCustomerId())
.orElseThrow();
customer.setStatus(Customer.CUSTOMER_STATUS_ACC_ACTIVE);
repository.save(customer);
LOG.info("Customer activated: id={}", customer.getId());
};
}
}
We also need to provide several configuration properties in the Spring Boot application.properties
file. We should set the Azure Event Hubs connection URL and the name of the target topic. Since our app uses Spring Cloud Stream only for sending events we should also turn off the autodiscovery of functional beans as messaging bindings.
spring.cloud.azure.eventhubs.connection-string = ${EVENT_HUBS_CONNECTION_STRING}
spring.cloud.stream.bindings.customers-out-0.destination = customers
spring.cloud.stream.function.autodetect = false
In order to expose the functions on Azure, we will take advantage of the Spring Cloud Function Azure Adapter. It includes several Azure libraries to our Maven dependencies. It can invoke the Spring Cloud Functions directly or through the lookup approach with the FunctionCatalog
bean (1). The method has to be annotated with @FunctionName
, which defines the name of the function in Azure (2). In order to expose that function over HTTP, we need to define the Azure @HttpTrigger
(3). The trigger exposes the function as the POST endpoint and doesn’t require any authorization. Our method receives the request through the HttpRequestMessage
object. Then, it invokes the Spring Cloud function addCustomer
by name using the FunctionCatalog
bean (4).
// (1)
@Autowired
private FunctionCatalog functionCatalog;
@FunctionName("add-customer") // (2)
public Customer addCustomerFunc(
// (3)
@HttpTrigger(name = "req",
methods = { HttpMethod.POST },
authLevel = AuthorizationLevel.ANONYMOUS)
HttpRequestMessage<Optional<Customer>> request,
ExecutionContext context) {
Customer c = request.getBody().orElseThrow();
context.getLogger().info("Request: {}" + c);
// (4)
Function<Customer, Customer> function = functionCatalog
.lookup("addCustomer");
return function.apply(c);
}
Integrate Function with Azure Data Hubs Trigger
Let’s switch to the account-function
app directory inside our Git repository. The same as customer-function
it uses an in-memory H2 database for storing customer accounts. Here’s our Account
entity class:
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String number;
private Long customerId;
private int balance;
// GETTERS AND SETTERS ...
}
Inside the account-function
app, there is a single Spring Cloud Function
addAccount
. It generates a new 16-digit account number for each new customer. Then, it saves the account in the database and sends the event to the Azure Event Hubs with Spring Cloud Stream StreamBridge
bean.
@Service
public class AccountInternalFunctions {
private static final Logger LOG = LoggerFactory
.getLogger(AccountInternalFunctions.class);
private StreamBridge streamBridge;
private AccountRepository repository;
public AccountInternalFunctions(StreamBridge streamBridge,
AccountRepository repository) {
this.streamBridge = streamBridge;
this.repository = repository;
}
@Bean
public Function<Customer, Account> addAccount() {
return customer -> {
String n = RandomStringUtils.random(16, false, true);
Account a = new Account(n, customer.getId(), 0);
a = repository.save(a);
boolean b = streamBridge.send("accounts-out-0", a);
LOG.info("New account added: {}", a);
return a;
};
}
}
The same as for customer-function
we need to provide some configuration settings for Spring Cloud Stream. However, instead of the customers
topic, this time we are sending events to the accounts
topic.
spring.cloud.azure.eventhubs.connection-string = ${EVENT_HUBS_CONNECTION_STRING}
spring.cloud.stream.bindings.accounts-out-0.destination = accounts
spring.cloud.stream.function.autodetect = false
The account-function
app is not exposed as the HTTP endpoint. We want to trigger the function in reaction to the event delivered to the Azure Event Hubs topic. The name of our Azure function is new-customer
(1). It receives the Customer
event from the customers
topic thanks to the @EventHubTrigger
annotation (2). This annotation also defines a property name, that contains the address of the Azure Event Hubs namespace (EVENT_HUBS_CONNECTION_STRING
). I’ll show you later how to set such a property for our function in Azure. Once, the new-customer
function is triggered, it invokes the Spring Cloud Function addAccount
(3).
@Autowired
private FunctionCatalog functionCatalog;
// (1)
@FunctionName("new-customer")
public void newAccountEventFunc(
// (2)
@EventHubTrigger(eventHubName = "customers",
name = "newAccountTrigger",
connection = "EVENT_HUBS_CONNECTION_STRING",
cardinality = Cardinality.ONE)
Customer event,
ExecutionContext context) {
context.getLogger().info("Event: " + event);
// (3)
Function<Customer, Account> function = functionCatalog
.lookup("addAccount");
function.apply(event);
}
At the end of this section, let’s switch back once again to the customer-function
app. It fires on the event sent by the new-customer
function to the accounts
topic. Therefore we are using the @EventHubTrigger
annotation once again.
@FunctionName("activate-customer")
public void activateCustomerEventFunc(
@EventHubTrigger(eventHubName = "accounts",
name = "changeStatusTrigger",
connection = "EVENT_HUBS_CONNECTION_STRING",
cardinality = Cardinality.ONE)
Account event,
ExecutionContext context) {
context.getLogger().info("Event: " + event);
Consumer<Account> consumer = functionCatalog.lookup("changeStatus");
consumer.accept(event);
}
All our functions are ready. Now, we can proceed to the deployment phase.
Running Azure Functions Locally with Maven
Before we deploy our functions on Azure, we can run and test them locally. I assume you have already the Azure Functions Core Tools according to the “Prerequisites” section. Our function still needs to connect with some services on the cloud like Azure Event Hubs or the storage account. The address to the Azure Event Hubs should be set as the EVENT_HUBS_CONNECTION_STRING app property or environment variable. In order to find the Event Hubs connection string, we should switch to the Azure Portal and find the spring-cloud-serverless
namespace. Then, we need to go to the “Shared access policies” menu item and click the “RootManagedSharedAccessKey” policy. The connection string value is available in the “Connection string-primary key” field.
We also need to obtain the connection credentials to the Azure storage account. In your storage account, you should find the “Access keys” section and copy the value from the “Connection string” field.
After that, we can create the local.settings.json
file in each app’s root directory. Alternatively, we can just set the environment variables AzureWebJobsStorage
and EVENT_HUBS_CONNECTION_STRING
.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": <YOUR_ACCOUNT_STORAGE_CONNECTION_STRING>,
"FUNCTIONS_WORKER_RUNTIME": "java",
"EVENT_HUBS_CONNECTION_STRING": <YOUR_EVENT_HUBS_CONNECTION_STRING>
}
}
Once you place your credentials in the local.settings.json
file, you can build the app with Maven.
$ mvn clean package
After that, you can use the Maven plugin included in the spring-cloud-function-adapter-azure
module. In order to run the function, you need to execute the following command:
$ mvn azure-functions:run
Here’s the command output for the customer-function
app. As you see, it contains two Azure functions: add-customer
(HTTP) and activate-customer
(Event Hub Trigger). You can test the function by invoking the http://localhost:7071/api/add-customer
URL.
Here’s the command output for the account-function
app. As you see, it contains a single function new-customer
activated through the Event Hub trigger.
Deploy Spring Cloud Serverless on Azure Functions
Let’s take a look at the pminkows-customer-function
Azure Function App before we deploy our first app there. The http://pminkows-customer-function.azurewebsites.net
base URL will precede all the HTTP endpoint URLs for our functions. We should remember the name of the automatically generated service plan (EastUSLinuxDynamicPlan
).
Before deploying our Spring Boot app we should create the host.json
file with the following content:
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
Then, we should add the azure-functions-maven-plugin
Maven plugin to our pom.xml
. In the configuration section, we have to set the name of the Azure Function App instance (pminkows-customer-function
), the target resource group (spring-cloud-serverless
), the region (eastus
), the service plan (EastUSLinuxDynamicPlan
), and the location of the host.json
file. We also need to set the connection string to the Azure Event Hubs inside the EVENT_HUBS_CONNECTION_STRING
app property. So before running the build, you should export the value of that environment variable.
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<version>1.30.0</version>
<configuration>
<appName>pminkows-customer-function</appName>
<resourceGroup>spring-cloud-serverless</resourceGroup>
<region>eastus</region>
<appServicePlanName>EastUSLinuxDynamicPlan</appServicePlanName>
<hostJson>${project.basedir}/src/main/resources/host.json</hostJson>
<runtime>
<os>linux</os>
<javaVersion>17</javaVersion>
</runtime>
<appSettings>
<property>
<name>FUNCTIONS_EXTENSION_VERSION</name>
<value>~4</value>
</property>
<property>
<name>EVENT_HUBS_CONNECTION_STRING</name>
<value>${EVENT_HUBS_CONNECTION_STRING}</value>
</property>
</appSettings>
</configuration>
<executions>
<execution>
<id>package-functions</id>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
</plugin>
Let’s begin with the customer-function
app. Before deploying the app we need to build it first with the mvn clean package
command. Once you do it you can run your first function Azure with the following command:
$ mvn azure-functions:deploy
Here’s my output after running that command. As you see, the function add-customer
is exposed under the http://pminkows-customer-function.azurewebsites.net/api/add-customer
URL.
We can come back to the pminkows-customer-function
Azure Function App in the portal. According to the expectations, two functions are running there. The activate-customer
function is triggered by the Azure Event Hub.
Let’s switch to the account-function
directory. We will deploy it to the Azure pminkows-account-function
function. Once again we need to build the app with mvn clean package
command, and then deploy it using the mvn azure-functions:deploy
command. Here’s the output. There are no HTTP triggers defined, but just a single function triggered by the Azure Event Hub.
Here are the details of the pminkows-account-function
Function App in the Azure Portal.
Invoke Azure Functions
Finally, we can test our functions by calling the following endpoint using e.g. curl
. Let’s repeat the similar command several times with different data:
$ curl https://pminkows-customer-function.azurewebsites.net/api/add-customer \
-d "{\"name\":\"Test\",\"age\":33}" \
-H "Content-Type: application/json"
After that, we should switch to the Azure Portal. Go to the pminkows-customer-function
details and click the link “Invocation and more” on the add-customer
function.
You will be redirected to the Azure Monitor statistics for that function. Azure Monitor displays a list of invocations with statuses.
We can click one of the records from the invocation history to see the details.
As you probably remember, the add-customer
function sends messages to Azure Event Hubs. On the other hand, we can also verify how the new-customer
function in the pminkows-account-function
consumes and handles those events.
Final Thoughts
This article gives you a comprehensive guide on how to build and run Spring Cloud serverless apps on Azure Functions. It explains the concept of the triggers in Azure Functions and shows the integration with Azure Event Hubs. Finally, it shows how to run such functions locally and then monitor them on Azure after deployment.
Leave a Reply