DEV Community

Cover image for JBang + Quarkus + AWS Lambda + Terraform = <3
Nándor Holozsnyák
Nándor Holozsnyák

Posted on • Updated on

JBang + Quarkus + AWS Lambda + Terraform = <3

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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");
    }
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
    }
}

Enter fullscreen mode Exit fullscreen mode

Building and running the script and the output will be:

❯ jbang hellojbang.java
[jbang] Building jar...
H...

Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"}%  
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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: 
Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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:

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

Discussion (0)