You have probably already heard of serverless functions. If you’ve played around with them, it might have been on a platform like AWS Lambda, Azure Functions, Google Cloud Functions or similar. The vendors behind these platforms offer solutions specifically for Java as well. The libraries and deployment methodologies are typically proprietary though, making it hard to move functions from one platform to another. This leaves you locked-in and at the mercy of the vendor’s price hikes or potential outages or other issues.
Serverless Functions
The idea behind serverless functions is really cool though. You provide your code, and the cloud platform’s provided tools packages and deploys it for you. On top of that, they will automatically scale the function to the demand of the moment as well. For most offerings, you get billed only when your function is actually used. This makes it really cheap and easy to start with serverless functions. If your experiment isn’t used, well, then you don’t really pay anything.
Of course, there are some downsides to serverless functions as well. The costs sometimes go up spectacularly when your function does get used a lot. Billing is typically based on a combination of how often a function gets called, how many resources it uses, and other factors such as storage size. In addition, you’ll likely tie in other services from the cloud provider (e.g. gateways, eventing/messaging systems, etc) – and those have a cost as well.
Another downside is that most cloud providers, even though they support multiple languages including Java, require you to write, build and package your application in a specific way. Oftentimes, you’ll need to even include proprietary libraries in your application’s code. This lock-in approach is perhaps not a problem if you’re happy to stay with one provider for the entire life of your application. However if you want to have the freedom of switching to another provider, move your functions on-prem, or use multiple providers at the same time, then you’re out of luck.
Fortunately for Java developers, there are solutions to this problem. You can either use a framework that externalizes the dependencies into ‘helper’ extensions, such as Quarkus and its Funqy extensions, or you can run serverless functions on a cloud provider agnostic platform, such as Kubernetes.
Portable Java Functions with Quarkus Funqy
To use AWS Lambda without Funqy, you typically implement the lambda requestHandler and override the handleRequest method as in the following example.
package dev.kevindubois; import java.util.Map; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; public class MyRequestHandler implements RequestHandler<Map<String, String>, String> { @Override public String handleRequest(Map<String, String> input, Context context) { String name = input.get("name"); String message = input.get("message"); if (name == null || message == null) { return "Please input your name and a message."; } else { return String.format("Welcome to AWS Lambda, %s! %s", name, message); } } }
Then you package the application in a prescribed way which again varies from cloud provider to cloud provider. If you want to use streaming, the libraries to include and dependencies on the cloud provider increase even more.
With Quarkus Funqy on the other hand the necessary libraries and prescribed implementation are abstracted away in the extension’s functionality (in this case, funqy-amazon-lambda). Instead, simply annotate the function with @Funq and the function is transformed to the targeted cloud provider’s implementation during build time. Here’s an example of a Funqy Function:
package dev.kevindubois; import io.quarkus.funqy.Funq; public class Function { @Funq public String myFunction(Map<String, String> input) { String name = input.get("name"); String message = input.get("message"); if (name == null || message == null) { return "Please input your name and a message."; } else { return String.format("Welcome to AWS Lambda, %s! %s", name, message); } } }
As you can see, it’s the same functionality, but notice how there are no more import statements mentioning amazonaws libraries. In addition, we don’t need to follow the prescribed method overriding of an amazonaws provided class. This means that now if we want to target a different provider, say, Azure Functions, or Google Cloud Functions, we don’t have to do any refactoring of the code. We just swap the Funqy extension for the funqy-azure-http in our dependencies and with that we build and deploy the function to Azure Functions instead.
To be fair, Quarkus Funqy is still a bit experimental at the moment. For example, there’s a bit of inconsistency in terms of what build and deploy command to use between the different funqy extensions. Overall though it is a nice approach to creating portable functions that you can deploy to any cloud provider with minimal interventions.
Cloud Agnostic Functions with Knative
There is another even more ‘cloud agnostic’ and open approach to working with serverless functions, and that’s to use a solution that transcends the cloud provider’s proprietary function platforms. You have likely heard of Kubernetes and maybe even used it. Kubernetes is an open source project. It allows you to run containers in a cloud environment (even on-prem). Knative is a complementary open source project that allows you to deploy serverless functions on a Kubernetes platform. It uses the same concepts of helping you to package, build and deploy workloads and autoscale them. It also potentially scales your functions to 0 when not in use.
If you have a Kubernetes cluster available, you can install Knative on top of it using these instructions. Alternatively Red Hat Developer offers it on top of their Openshift Sandbox which is free (though needs to be renewed every 30 days).
One way to use Knative is via the kn CLI tool . E.g. to create Quarkus based function:
kn func create myfunction -l quarkus
Or, if you’d rather use Spring Boot:
kn func create myfunction -l springboot
This command scaffolds a sample function and a func.yaml file that contains the build instructions, such as whether you want to do a Native Build, the Java version you want to use, where to find the build artifacts etc.
An example func.yaml file for Quarkus looks like this:
specVersion: 0.36.0 name: myfunction runtime: quarkus created: 2024-11-26T11:14:38.471661474+01:00 build: buildEnvs: - name: BP_NATIVE_IMAGE value: "false" - name: BP_JVM_VERSION value: "21" - name: MAVEN_S2I_ARTIFACT_DIRS value: target/quarkus-app - name: S2I_SOURCE_DEPLOYMENTS_FILTER value: lib quarkus-run.jar app quarkus
When you create a function with Knative and target Quarkus, you will actually create a Quarkus Funqy function. You are able to easily deploy this function to one of the proprietary FaaS providers as well. The function is in fact structured the exact same as the Function class shown above in the Quarkus Funqy section.
Here’s the simple command to deploy the function to Kubernetes with the Knative CLI:
kn deploy
Furthermore, it’s also possible to generate plain old Kubernetes yaml. This allows you to automate your functions lifecycle using GitOps. You are also able to use any other methodologies you use with a Kubernetes-based platform. Your function then also easily integrates with any other service running on or around Kubernetes. Examples are observability stacks, messaging and eventing systems, etc. Knative also provides native support for the open source Cloud Events specification out of the box. It does so with its own Knative Eventing features. This opens serverless functions to a wealth of true open source, portable and enterprise ready use cases.
This article tried to give you a taste of what’s possible for serverless Java functions. It’s worth to look beyond the proprietary prescribed ways of functions-as-a-service providers. As you could see, there are ways to still leverage their offerings with Quarkus Funqy, especially if you’re interested in billing based on the number of invocations of a function. At the same time, you now have some ideas to break free from the lock-in to specific providers. You will still be able to enjoy the capabilities of serverless functions, and perhaps even go beyond proprietary offerings by using solutions like Knative.
Author: Kevin Dubois
Kevin is a software engineer, author and international speaker with a passion for Open Source, Java, and Cloud Native Development & Deployment practices. He currently works as developer advocate at Red Hat where he gets to enjoy working with Open Source projects and improving the developer experience. He previously worked as a (Lead) Software Engineer at a variety of organizations across the world ranging from small startups to large enterprises and even government agencies.
Kevin is actively involved in Open Source communities, contributing to projects such as Quarkus, Knative, Apache Camel, and Podman (Desktop); and as a member of the Belgian CNCF chapter as well as the Belgian Java User Group.
Kevin speaks English, Dutch, French and Italian fluently and is currently based in Belgium, having lived in Italy and the USA as well.
In his free time you can find Kevin somewhere in the wild hiking, gravel biking, snowboarding or packrafting.