Overview
Azure Functions is a serverless solution that lets you run code in the cloud. First-class support is provided for several languages, including C#, Java, JavaScript, PowerShell, Python, and TypeScript. This set will have a good number of people covered (though I’d love to know how many people out there are writing web services using PowerShell), though not everybody. One option is to write Functions in Scala (and, I imagine, Kotlin) using the Java handler, since Java and Scala are largely interoperable, and that is the focus of this post. Another possibility is to use a custom handler, in which case we can use pretty much any other language, and the is the focus of a different post.
Prerequisites
We can develop Azure Functions entirely locally using Azure Functions Core Tools (though you’ll need Azure CLI to publish your apps). At the time this post was written, I was running Ubuntu 22.04, and Microsoft had not released a version of the core tools that I could use. So, I decided to put all the required tools in a Docker container, but I would argue that’s a good way to work anyway. So, we create a Dockerfile
with companion docker-compose.yml
file as follows:
FROM ubuntu:20.04
ENV DEBIAN_FRONTEND=noninteractive
ARG NODE_VERSION=16.15.1
ARG SBT_VERSION=1.6.2
RUN apt-get update && \
apt-get dist-upgrade -y && \
apt-get install -y --no-install-recommends \
&& \
gpg curl lsb-release maven openjdk-8-jdk openjdk-11-jdk openssh-server xz-utils curl -sL https://aka.ms/InstallAzureCLIDeb | bash && \
curl -s https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg && \
mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg && \
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' && \
apt-get update && \
apt-get install -y --no-install-recommends \
&& \
azure-functions-core-tools-4 azure-cli dotnet-sdk-3.1 mkdir -p /usr/local/node && \
curl -sL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz | \
tar xJvf - --strip-components=1 -C /usr/local/node && \
echo "export PATH=\$PATH:/usr/local/node/bin" >> /etc/profile && \
PATH=$PATH:/usr/local/node/bin npm install --location=global azurite && \
echo "export JAVA_HOME=/usr" >> /etc/profile
EXPOSE 22
CMD mkdir -p /root/.ssh && \
echo "$PUB_KEY" >> /root/.ssh/authorized_keys && \
chmod 700 /root/.ssh && chmod 600 /root/.ssh/* && \
service ssh start && \
tail -f /dev/null
version: "3.8"
services:
development:
image: azure
build:
context: ./
dockerfile: Dockerfile
ports:
- "24:22"
environment:
- "PUB_KEY=${PUB_KEY}"
volumes:
- azuredev-azfct:/root/.azure-functions-core-tools
- azuredev-cache:/root/.cache
- azuredev-mvn:/root/.m2
- azuredev-vscode:/root/.vscode-server
- azuredev-npm:/root/.npm
- ./work:/root/work
volumes:
azuredev-azfct:
azuredev-cache:
azuredev-mvn:
azuredev-vscode:
azuredev-npm:
This provides a single container with the following components:
- Azure Functions Core Tools for local development
- Azure CLI to deoploy to Azure
- Azurite for local simulation of cloud storage: event queue, BLOB, and table storage
- Java development kit for compilation of Java (and Scala) code
- Maven to manage projects
- OpenSSH sever to allow us to connect to running containers via SSH
To run an instance (and build the image if it doesn’t already exist):
docker compose up -d
I typically add an entry to my SSH config and then access the running container via the remote extension. That is, I add something like the following to ~/config
:
Host azuredev
HostName localhost
Port 24 User root
A Simple Java Function
To create a basic Java function, run the following command:
mvn archetype:generate \
-DarchetypeGroupId=com.microsoft.azure \
-DarchetypeArtifactId=azure-functions-archetype \
-DjavaVersion=8
When doing this, users will be prompted to provide values for several parameters. In this example, we provide values as follows:
parameter | value |
---|---|
groupId |
org.example |
artifactId |
hellojava |
version |
1.0-SNAPSHOT |
package |
org.example |
After doing this, a folder named hellojava
is created, with the following layout:
hellojava
├── host.json
├── local.settings.json
├── pom.xml
└── src
├── main
│ └── java
│ └── org
│ └── example
│ └── Function.java
└── test
└── java
└── org
└── example
├── FunctionTest.java └── HttpResponseMessageMock.java
The most important files here are Function.java
and pom.xml
. Function.java
contains the logic for our function, and pom.xml
lists all the dependencies and other metadata Maven needs to build and run our project.
package org.cmhh;
import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;
import java.util.Optional;
/**
* Azure Functions with HTTP Trigger.
*/
public class Function {
/**
* This function listens at endpoint "/api/HttpExample". Two ways to invoke it using "curl" command in bash:
* 1. curl -d "HTTP Body" {your host}/api/HttpExample
* 2. curl "{your host}/api/HttpExample?name=HTTP%20Query"
*/
@FunctionName("HttpExample")
public HttpResponseMessage run(
@HttpTrigger(
= "req",
name = {HttpMethod.GET, HttpMethod.POST},
methods = AuthorizationLevel.ANONYMOUS)
authLevel <Optional<String>> request,
HttpRequestMessagefinal ExecutionContext context) {
.getLogger().info("Java HTTP trigger processed a request.");
context
// Parse query parameter
final String query = request.getQueryParameters().get("name");
final String name = request.getBody().orElse(query);
if (name == null) {
return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build();
} else {
return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
}
}
}
<?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>org.cmhh</groupId>
<artifactId>hellojava</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<
name>Azure Java Functions</name>
<
properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<azure.functions.maven.plugin.version>1.18.0</azure.functions.maven.plugin.version>
<azure.functions.java.library.version>2.0.0</azure.functions.java.library.version>
<functionAppName>org-20220618002424789</functionAppName>
<properties>
</
dependencies>
<dependency>
<groupId>com.microsoft.azure.functions</groupId>
<artifactId>azure-functions-java-library</artifactId>
<version>${azure.functions.java.library.version}</version>
<dependency>
</
<!-- Test -->
dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.4.2</version>
<scope>test</scope>
<dependency>
</
dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
<dependency>
</dependencies>
</
build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<configuration>
</plugin>
</plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<version>${azure.functions.maven.plugin.version}</version>
<configuration>
<<!-- function app name -->
appName>${functionAppName}</appName>
<<!-- function app resource group -->
resourceGroup>java-functions-group</resourceGroup>
<<!-- function app service plan name -->
appServicePlanName>java-functions-app-service-plan</appServicePlanName>
<<!-- function app region-->
<!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-regions for all valid values -->
region>westus</region>
<<!-- function pricingTier, default to be consumption if not specified -->
<!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-pricing-tiers for all valid values -->
<!-- <pricingTier></pricingTier> -->
<!-- Whether to disable application insights, default is false -->
<!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details for all valid configurations for application insights-->
<!-- <disableAppInsights></disableAppInsights> -->
runtime>
<<!-- runtime os, could be windows, linux or docker-->
os>windows</os>
<javaVersion>8</javaVersion>
<runtime>
</appSettings>
<property>
<name>FUNCTIONS_EXTENSION_VERSION</name>
<value>~4</value>
<property>
</appSettings>
</configuration>
</executions>
<execution>
<id>package-functions</id>
<goals>
<goal>package</goal>
<goals>
</execution>
</executions>
</plugin>
</<!--Remove obj folder generated by .NET SDK in maven clean-->
plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<filesets>
<fileset>
<directory>obj</directory>
<fileset>
</filesets>
</configuration>
</plugin>
</plugins>
</build>
</project> </
We can build and run our function locally by running:
$ mvn clean package
$ mvn azure-functions:run
All going well, your app will now be running, most likely on port 7071. Visiting http://localhost:7071
we should see:
Function.java
provides a single function, and the @FunctionName
annotation tells us that the name is HttpExample
. The signature of the run
method tells us that this is an HttpTrigger
(meaning it runs every time we make an HTTP request), and that it expects a single parameter called name
, an optional string. We can call the function by GETting or POSTing:
curl http://localhost:7071/api/HttpExample?name=Java!
Hello, Java!
or
curl -d "Java!" http://localhost:7071/api/HttpExample
Hello, Java!
A Simple Scala Function
We can convert our simple Java function by adding Scala as a dependecy to pom.xml
, as well as the Scala Maven plugin; and by replacing src/main/java/org/example/Function.java
with src/main/scala/org/example/Function.java
. These two files are as follows:
package org.cmhh
import com.microsoft.azure.functions.{ExecutionContext, HttpMethod, HttpRequestMessage, HttpResponseMessage, HttpStatus}
import com.microsoft.azure.functions.annotation.{AuthorizationLevel, FunctionName, HttpTrigger}
import java.util.Optional
/**
* Convert java Optional to Scala Option
*/
object utils {
def toOption[T](x: Optional[T]): Option[T] = {
if (x.isPresent) Option(x.get()) else None
}
}
/**
* Azure Functions with HTTP Trigger.
*/
class Function {
/**
* curl -d "HTTP Body" {your host}/api/HttpExample
* curl "{your host}/api/HttpExample?name=HTTP%20Query"
*/
FunctionName("HttpExample")
@def run(
HttpTrigger(
@= "req",
name = Array(HttpMethod.GET, HttpMethod.POST),
methods = AuthorizationLevel.ANONYMOUS
authLevel )
: HttpRequestMessage[Optional[String]],
request: ExecutionContext
context): HttpResponseMessage = {
.getLogger().info("Scala HTTP trigger processed a request.")
context
val name: Option[String] = utils.toOption(request.getBody()) match {
case None => Option(request.getQueryParameters().get("name"))
case Some(s) => Some(s)
}
match {
name case None =>
request.createResponseBuilder(HttpStatus.BAD_REQUEST)
.body("Please pass a name on the query string or in the request body")
.build
case Some(s) =>
request.createResponseBuilder(HttpStatus.OK)
.body(s"Hello, $s")
.build
}
}
}
<?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>org.cmhh</groupId>
<artifactId>helloscala</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Azure Scala Functions</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<azure.functions.maven.plugin.version>1.18.0</azure.functions.maven.plugin.version>
<azure.functions.java.library.version>2.0.0</azure.functions.java.library.version>
<functionAppName>org-20220618002424789</functionAppName>
</properties>
<dependencies>
<dependency>
<groupId>com.microsoft.azure.functions</groupId>
<artifactId>azure-functions-java-library</artifactId>
<version>${azure.functions.java.library.version}</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.13.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<version>${azure.functions.maven.plugin.version}</version>
<configuration>
<appName>${functionAppName}</appName>
<resourceGroup>java-functions-group</resourceGroup>
<appServicePlanName>java-functions-app-service-plan</appServicePlanName>
<region>westus</region>
<runtime>
<os>linux</os>
<javaVersion>8</javaVersion>
</runtime>
<appSettings>
<property>
<name>FUNCTIONS_EXTENSION_VERSION</name>
<value>~4</value>
</property>
</appSettings>
</configuration>
<executions>
<execution>
<id>package-functions</id>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
</plugin>
<!--Remove obj folder generated by .NET SDK in maven clean-->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<filesets>
<fileset>
<directory>obj</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>4.6.3</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
We build and run the function exactly as before but, crucially, we can now write all our code in Scala, as we would for any other Scala project. Cool.