Have you ever wondered how can you utilize your Java skills in the serverless world?
If not then let me take you on a small trip where we will create AWS Lambda functions with Java, and yes with JBang.
Prerequisites:
- JBang
- Properly configured AWS CLI with Access and Secret key
- Terraform - latest/newest release would work.
I would not like to go through how these tools should be installed, I assume these things are a piece of cake.
My versions
❯ jbang version
0.86.0
❯ aws --version
aws-cli/1.22.5 Python/3.9.5 Linux/5.15.8-76051508-generic botocore/1.23.5
❯ terraform -v
Terraform v1.0.0
on linux_amd64
JBang
JBang is a powerful tool which lets you create .java
files with your requested dependencies, and with different commands you will be able to build and export .jar
files and native binaries.
Okay okay, we have Maven and Gradle, why would I need this?
For my answer for this is the following: If you really want to code just a small app with some dependencies rather than creating and maintaining a project with a pom.xml
or a gradle.build
could be overkill, like in the following use case where we are going to create a Lambda function.
My motivation
I have attended projects where all the Lambda functions were written in Python, and I'm not a Python developer, of course another programming language, can be learned easily, but with deadlines on our back, if the team is more of a Java team, then writing Lambda functions in Java makes more sense.
What I really like in the Python or JavaScript based Lambda functions are their "lightness", the authors of the functions created a small .py
or .js
file, and they could deploy it and invoke it and of course they have the online code editor, with Java we won't have that feature, but we can utilize our Java knowledge. Of course dependency management should happen if we need external dependencies, with Python I know it is relatively easy, and of course with Java too, Maven and Gradle are beautiful tools, but I think they are overkill for smaller functions.
I really wanted to have almost the same "workflow" with Java, just one .java
file per function that can have external dependencies (like Quarkus that we are also going to use because it has a really nice integration with AWS Lambda as well) listed somewhere in the .java
source file as well and can be built by anybody who has the jbang
binary on their workstation.
Our first JBang "script"
After JBang got installed we can start working with that, let's create our very first script with the following command:
❯ jbang init hellojbang.java
[jbang] File initialized. You can now run it with 'jbang hellojbang.java' or edit it using 'jbang edit --open=[editor] hellojbang.java' where [editor] is your editor or IDE, e.g. 'idea'
After the file is created we have a "few" options to edit it. We can use the command it outputs with our installed IDE (IDEA,VSCode): jbang edit --open=idea hellojbang.java
.
At first glance it could be a bit "weird", I was talking about having no build tool involved in the flow, but we see a build.gradle
file, but do not worry, this is just a small helper project that was created for it, to have IDE support, as you can see the whole project sits in the ~/.jbang/cache
folder and a symbolic link was created for it.
For IntelliJ IDEA JBang has a really nice plugin, really young, few weeks old but can do the work: https://plugins.jetbrains.com/plugin/18257-jbang in this case you do not have to use the edit
command, because IDEA will have a feature to download sync all dependencies and have code completion.
If we open the file we will see the following:
///usr/bin/env jbang "$0" "$@" ; exit $?
import static java.lang.System.*;
public class hellojbang {
public static void main(String... args) {
out.println("Hello World");
}
}
We can run the .java
file with the following commands:
jbang hellojbang.java
jbang run hellojbang.java
./hellojbang.java
The output will be the following every time:
❯ ./hellojbang.java
[jbang] Building jar...
Hello World
On the first run JBang creates a .jar
file within its cache folder and it runs it, if codes has no changes compared to earlier run then it will not build it again.
Configuring dependencies and Java version
JBang uses //
based directives to configure the dependencies for the application, and other things as well.
Let's see how we can add some dependencies and set the Java version to 11, because with the AWS Lambda we will only have a Java 11 runtime environment.
We can add dependencies with the //DEPS <gav>
directive and we can set the Java version to 11 with //JAVA 11
directive
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 11
//DEPS org.apache.commons:commons-lang3:3.12.0
import org.apache.commons.lang3.StringUtils;
import static java.lang.System.*;
public class hellojbang {
public static void main(String... args) {
out.println(StringUtils.abbreviate("Hello World", 4));
}
}
Building and running the script and the output will be:
❯ jbang hellojbang.java
[jbang] Building jar...
H...
Nice, we added a dependency and set the Java version to 11. We can add unlimited amount of dependencies and we can use BOMs as well.
That was a brief introduction to JBang and now let's see the AWS stuff.
Quarkus & JBang & AWS Lambda & Terraform
Quarkus & JBang
Create a new .java
file where we can write out Lambda function code.
❯ jbang init AwsLambdaFunction.java
[jbang] File initialized. You can now run it with 'jbang AwsLambdaFunction.java' or edit it using 'jbang edit --open=[editor] AwsLambdaFunction.java' where [editor] is your editor or IDE, e.g. 'idea'
Open the file within our favourite editor: jbang edit --open=idea AwsLambdaFunction.java
and add the following dependencies:
//DEPS io.quarkus:quarkus-bom:2.6.0.Final@pom
//DEPS io.quarkus:quarkus-amazon-lambda
With that we state that we would like to use Quarkus at the "newest" version: 2.6.0 and we are adding a new dependency to the "project" as well: io.quarkus:quarkus-amazon-lambda
. We don't have to provide the version number, JBang is smart enough to have this information from the BOM specified above it.
If we want to create a Lambda function with Quarkus we have to implement the com.amazonaws.services.lambda.runtime.RequestHandler
interface by implementing the handleRequest
method.
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 11
//DEPS io.quarkus:quarkus-bom:2.6.0.Final@pom
//DEPS io.quarkus:quarkus-amazon-lambda
//DEPS org.projectlombok:lombok:1.18.22
//JAVA_OPTIONS -Djava.util.logging.manager=org.jboss.logmanager.LogManager
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import lombok.Builder;
import lombok.Data;
import org.jboss.logging.Logger;
public class AwsLambdaFunction implements RequestHandler<LambdaInput, LambdaOutput> {
public static final Logger LOG = Logger.getLogger("AwsLambdaFunction");
public AwsLambdaFunction() {
}
@Override
public LambdaOutput handleRequest(LambdaInput input, Context context) {
LOG.info("Hello from Lambda: " + input);
return LambdaOutput.builder()
.result("Incoming text: " + input.getInput())
.build();
}
}
@Data
class LambdaInput {
private String input;
}
@Data
@Builder
class LambdaOutput {
private String result;
}
In the "final" code snippet we can some new things:
-
//DEPS org.projectlombok:lombok:1.18.22
- Lombok, which is here to make the POJO classes thinner in the code. -
//JAVA_OPTIONS -Djava.util.logging.manager=org.jboss.logmanager.LogManager
- We would like to log, in this case we need a logger configuration. - We must have a public no-args constructor.
- POJOs should be conventional Beans, with no-arg constructors and with getter/setter pairs.
❯ jbang AwsLambdaFunction.java
[jbang] Resolving dependencies...
[jbang] Artifacts used for dependency management:
io.quarkus:quarkus-bom:pom:2.6.0.Final
[jbang] io.quarkus:quarkus-amazon-lambda
org.projectlombok:lombok:jar:1.18.22
Done
[jbang] Dependencies resolved
[jbang] Building jar...
[jbang] Post build with io.quarkus.launcher.JBangIntegration
Jan 02, 2022 9:14:53 PM org.jboss.threads.Version <clinit>
INFO: JBoss Threads version 3.4.2.Final
Jan 02, 2022 9:14:53 PM io.quarkus.deployment.QuarkusAugmentor run
INFO: Quarkus augmentation completed in 610ms
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2022-01-02 21:14:54,510 INFO [io.quarkus] (main) Quarkus 2.6.0.Final on JVM started in 0.428s.
2022-01-02 21:14:54,515 INFO [io.quarkus] (main) Profile prod activated.
2022-01-02 21:14:54,516 INFO [io.quarkus] (main) Installed features: [amazon-lambda, cdi]
It means basically our code is using Quarkus and we are "almost done". Of course it would be nice to test it, right now we are not going to write unit tests for it, we would be able to, lets cover that topic in another time, right now just utilize Quarkus's dev mode with the following command:
❯ jbang -Dquarkus.dev AwsLambdaFunction.java
[jbang] Building jar...
[jbang] Post build with io.quarkus.launcher.JBangIntegration
2022-01-02 21:17:08,318 INFO [io.qua.ama.lam.run.MockEventServer] (build-10) Mock Lambda Event Server Started
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2022-01-02 21:17:08,647 INFO [io.qua.ama.lam.run.AbstractLambdaPollLoop] (Lambda Thread (DEVELOPMENT)) Listening on: http://localhost:8080/_lambda_/2018-06-01/runtime/invocation/next
2022-01-02 21:17:08,654 INFO [io.quarkus] (Quarkus Main Thread) Quarkus 2.6.0.Final on JVM started in 1.135s.
2022-01-02 21:17:08,659 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2022-01-02 21:17:08,659 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [amazon-lambda, cdi]
--
Tests paused
Press [r] to resume testing, [o] Toggle test output, [h] for more options>
Using the dev mode, Quarkus will start a mock HTTP event server so we can use curl
or other tools to invoke an HTTP endpoint where we can POST our input object, and then we can examine the result/response as well:
❯ curl -X POST --location "http://localhost:8080" \
-H "Content-Type: application/json" \
-d "{
\"input\": \"Hello World\"
}"
{"result":"Incoming text: Hello World"}%
By the way, using Quarkus's dev mode lets you change the code "on-the-fly", and on the next invocation it will rebuild automatically. You do not have to build it every time by yourself.
AWS Lambda & Terraform
Make sure the AWS CLI is configured: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html
Let's deploy our code to AWS with Terraform.
# Terraform basic configuration
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = "~> 3.70.0"
local = "2.1.0"
}
}
# Set AWS region to eu-central-1 -> Frankfurt
provider "aws" {
region = "eu-central-1"
}
# We have to create a role for the Lambda function, it is mandatory.
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda_function"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
# We have to somehow create the jar file that we will deploy, we are going to use the "local-exec" provision feature for it.
# First we will build the .java file, then we have to export it, export means we are copying it from the jbang cache to the current working directory
# After that we have to update the jar file, we have to move the exported "lib" folder to the jar file, we have to bundle all dependencies that we are relaying on.
resource "local_file" "jar_file" {
filename = "AwsLambdaFunction.jar"
content_base64 = filebase64sha256("AwsLambdaFunction.java")
provisioner "local-exec" {
command = "jbang build --fresh AwsLambdaFunction.java && jbang export portable --fresh --force AwsLambdaFunction.java && jar uf AwsLambdaFunction.jar lib/"
}
}
# Lambda function we want to create and invoke.
resource "aws_lambda_function" "function" {
filename = local_file.jar_file.filename
source_code_hash = local_file.jar_file.content_base64
function_name = "AwsLambdaFunction"
role = aws_iam_role.iam_for_lambda.arn
#Handler method must be io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
handler = "io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest"
depends_on = [local_file.jar_file]
runtime = "java11"
memory_size = 256
}
Let's run the following commands:
First we have to call the terraform init
, it will initialize the terraform state, and after that we can call terraform plan
or terraform apply
. plan
will just only show what it would do if apply
would be called.
After calling terraform apply
we have to write yes
when it asks for approval.
❯ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 3.70.0"...
- Finding hashicorp/local versions matching "2.1.0"...
- Installing hashicorp/aws v3.70.0...
- Installed hashicorp/aws v3.70.0 (signed by HashiCorp)
- Installing hashicorp/local v2.1.0...
- Installed hashicorp/local v2.1.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
...
❯ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_iam_role.iam_for_lambda will be created
+ resource "aws_iam_role" "iam_for_lambda" {
+ arn = (known after apply)
+ assume_role_policy = jsonencode(
{
+ Statement = [
+ {
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ Service = "lambda.amazonaws.com"
}
+ Sid = ""
},
]
+ Version = "2012-10-17"
}
)
+ create_date = (known after apply)
+ force_detach_policies = false
+ id = (known after apply)
+ managed_policy_arns = (known after apply)
+ max_session_duration = 3600
+ name = "iam_for_lambda"
+ name_prefix = (known after apply)
+ path = "/"
+ tags_all = (known after apply)
+ unique_id = (known after apply)
+ inline_policy {
+ name = (known after apply)
+ policy = (known after apply)
}
}
# aws_lambda_function.function will be created
+ resource "aws_lambda_function" "function" {
+ architectures = (known after apply)
+ arn = (known after apply)
+ filename = "AwsLambdaFunction.jar"
+ function_name = "AwsLambdaFunction"
+ handler = "io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest"
+ id = (known after apply)
+ invoke_arn = (known after apply)
+ last_modified = (known after apply)
+ memory_size = 256
+ package_type = "Zip"
+ publish = false
+ qualified_arn = (known after apply)
+ reserved_concurrent_executions = -1
+ role = (known after apply)
+ runtime = "java11"
+ signing_job_arn = (known after apply)
+ signing_profile_version_arn = (known after apply)
+ source_code_hash = "zxCVmQSXmb7Zf3EPLyKKVgL5Tv61WGLArpHz8QSum2c="
+ source_code_size = (known after apply)
+ tags_all = (known after apply)
+ timeout = 3
+ version = (known after apply)
+ tracing_config {
+ mode = (known after apply)
}
}
# local_file.jar_file will be created
+ resource "local_file" "jar_file" {
+ content_base64 = "zxCVmQSXmb7Zf3EPLyKKVgL5Tv61WGLArpHz8QSum2c="
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "AwsLambdaFunction.jar"
+ id = (known after apply)
}
Plan: 3 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
After entring yes
and pressing Enter we should see the following output:
local_file.jar_file: Creating...
local_file.jar_file: Provisioning with 'local-exec'...
local_file.jar_file (local-exec): Executing: ["/bin/sh" "-c" "jbang build --fresh AwsLambdaFunction.java && jbang export portable --fresh --force AwsLambdaFunction.java && jar uf AwsLambdaFunction.jar lib/"]
local_file.jar_file (local-exec): [jbang] Resolving dependencies...
local_file.jar_file (local-exec): [jbang] Artifacts used for dependency management:
local_file.jar_file (local-exec): io.quarkus:quarkus-bom:pom:2.6.0.Final
local_file.jar_file (local-exec): [jbang] io.quarkus:quarkus-amazon-lambda
local_file.jar_file (local-exec): org.projectlombok:lombok:jar:1.18.22
local_file.jar_file (local-exec): Done
local_file.jar_file (local-exec): [jbang] Dependencies resolved
local_file.jar_file (local-exec): [jbang] Building jar...
aws_iam_role.iam_for_lambda: Creating...
local_file.jar_file (local-exec): [jbang] Post build with io.quarkus.launcher.JBangIntegration
local_file.jar_file (local-exec): Jan 02, 2022 9:40:29 PM org.jboss.threads.Version <clinit>
local_file.jar_file (local-exec): INFO: JBoss Threads version 3.4.2.Final
local_file.jar_file (local-exec): Jan 02, 2022 9:40:30 PM io.quarkus.deployment.QuarkusAugmentor run
local_file.jar_file (local-exec): INFO: Quarkus augmentation completed in 652ms
aws_iam_role.iam_for_lambda: Creation complete after 2s [id=iam_for_lambda_function]
local_file.jar_file (local-exec): [jbang] Resolving dependencies...
local_file.jar_file (local-exec): [jbang] Artifacts used for dependency management:
local_file.jar_file (local-exec): io.quarkus:quarkus-bom:pom:2.6.0.Final
local_file.jar_file (local-exec): [jbang] io.quarkus:quarkus-amazon-lambda
local_file.jar_file (local-exec): org.projectlombok:lombok:jar:1.18.22
local_file.jar_file (local-exec): Done
local_file.jar_file (local-exec): [jbang] Dependencies resolved
local_file.jar_file (local-exec): [jbang] Building jar...
local_file.jar_file (local-exec): [jbang] Post build with io.quarkus.launcher.JBangIntegration
local_file.jar_file (local-exec): Jan 02, 2022 9:40:33 PM org.jboss.threads.Version <clinit>
local_file.jar_file (local-exec): INFO: JBoss Threads version 3.4.2.Final
local_file.jar_file (local-exec): Jan 02, 2022 9:40:34 PM io.quarkus.deployment.QuarkusAugmentor run
local_file.jar_file (local-exec): INFO: Quarkus augmentation completed in 705ms
local_file.jar_file (local-exec): [jbang] Updating jar manifest
local_file.jar_file (local-exec): [jbang] Exported to /media/nandi/Data/VCS/GIT/jbang-terraform-aws/devto/AwsLambdaFunction.jar
local_file.jar_file: Creation complete after 8s [id=352a94713061363fa798146c96e188a5dd35a975]
aws_lambda_function.function: Creating...
aws_lambda_function.function: Creation complete after 8s [id=AwsLambdaFunction]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
If you want to test it from the AWS console you can do it here.
If you want to use the AWS CLI invoke the following command:
echo "{\"input\": \"Hello World from AWS Lambda\"}" > payload.json
aws lambda invoke response.txt --function-name AwsLambdaFunction --log-type Tail --output text --query 'LogResult' --payload file://payload.json | base64 --decode
The output should look like this (will be different for you, date-time and IDs):
START RequestId: b58e9171-4d9c-4d92-8056-aa7e20317619 Version: $LATEST
2022-01-02 20:45:51,864 INFO [RequestHandlerExample] (main) Hello from Lambda: LambdaInput(input=Hello World from AWS Lambda)
END RequestId: b58e9171-4d9c-4d92-8056-aa7e20317619
REPORT RequestId: b58e9171-4d9c-4d92-8056-aa7e20317619 Duration: 1.47 ms Billed Duration: 2 ms Memory Size: 256 MB Max Memory Used: 118 MB
If we make any changes to our .java
file and we want to deploy it to AWS, we just have to run terraform to do the heavy lifting for us.
Outro
It is a quick and brief article on how to create and deploy Java based functions to AWS Lambda using JBang and Terraform. I really like all the used technologies here.
One thing before I close the article: Quarkus is NOT a mandatory framework to use, I just used it because I really love working with that, and if the function would need database handling libs or would like to use dependency injection then we would be able to just add more and more dependencies to it and use it. We just barely touched the topic.
Follow me on Twitter@TheRealHNK for more good stuff,
If you would like to learn more please check out the following sites:
- https://www.jbang.dev/documentation/guide/latest/intro.html
- https://quarkus.io/guides/amazon-lambda
- https://quarkus.io/guides/funqy-amazon-lambda - Funq is a Quarkus based super thing. Take a look on it.
- https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function
Upcoming
I'm planning to make new articles about exploring AWS Lambda triggers like SQS, S3, SNS. Stay tuned!
Cover (Photo by Gábor Molnár): https://unsplash.com/photos/Y7ufx8R8PM0
Top comments (0)