How to Manually Deploy a Quarkus Application to Azure with Terraform Thanks to NubesGen

In this blog post, I’ll show you how to manually deploy a simple Quarkus application to Azure using Terraform, thanks to NubesGen.

What’s Covered in This Blog Post?

To break it into more details, these are the topics that will be covered in this blog post:

  • What is Terraform?
  • What is NubesGen?
  • Generate a basic Quarkus REST endpoint.
  • Package the Quarkus application in an Uber-JAR.
  • Use NubesGen to generate Terraform configuration files for a simple Quarkus application.
  • Use the Azure Maven plugin to manually deploy the Quarkus application to Azure.
  • Check the deployed application on Azure thanks to the web console and the Azure CLI.

Use Case

The goal of this blog post is to focus on TerraformAzure and NubesGen. So the idea is to use Quarkus to develop a very simple REST endpoint that says hello (yet another hello world). Then we will package this REST endpoint into an Uber-JAR and deploy it on Azure thanks to Terraform and the Azure Maven plugin.

REST Endpoint Packaged into an Uber-JAR and Deployed to Azure

Prerequisites

To code along and be able to execute the code samples, you will need the following tools installed on your machine:

What Is Terraform?

But before we start coding, let me introduce Terraform. Terraform is an open-source tool written in GO for provisioning and managing infrastructures. This can be done on different cloud providers but also on-premise.

Download the code

As stated on their website, Terraform is an Infrastructure as Code (IaC) tool to “build, change, version and destroy infrastructure safely and efficiently”. The idea is to define, with code, what your infrastructure looks like, and let Terraform provision automatically this infrastructure for you. In a Terraform file, you describe your overall topology (network, security group roles, virtual machines, load balancers, etc) and leave Terraform to create it. If the topology of your application changes over time (eg. you add firewalls, DNS, CDN, etc.), Terraform calculates the delta and only creates/destroys/updates what has changed.

These Terraform files are written in HashiCorp Configuration Language, or HCL, which is human readable and looks like a mixture of JSON and YAML (but it can also be expressed in pure JSON). The advantage of declaring your infrastructure with code is that you can easily read it, change it, store it within your application code, version it, etc.

Terraform Workflow

To be able to provision your infrastructure, Terraform goes through a specific workflow:

Terraform Workflow

The different steps of the workflow are:

  • Init: Initializes the code and downloads the requirements mentioned in the configuration files (eg. the Azure provider).
  • Plan: Reviews the changes between the deployed infrastructure and what will be deployed. Basically, Terraform checks with the Cloud provider what’s running at the moment, so it gets an up-to-date view of what the infrastructure looks like. Then it figures out what it needs to do, reconciling what’s actually running, with what we want to be running (the desired state).
  • Apply: Applies the changes against the real infrastructure to go from what’s running to the desired state.
  • Destroy: Destroys the infrastructure.

Terraform Architecture

One of the strengths of Terraform is its ecosystem. Basically, you can create any kind of resource on most of the Cloud providers or on-premise. Why? Because Terraform uses an extension mechanism where providers can build their extension (eg. the Azure provider allows you to create several Azure resources).

Terraform Architecture

The Terraform architecture is made of a core and several providers:

  • Core: The core is the one dealing with the workflow lifecycle that we just saw. It takes the configuration files, calculates what’s need to be created/updated/destroyed…​ and then delegates the tasks to providers.
  • Set of providers: There are many providers: IaaS (this can be cloud providers such as AWS, Azure, GCP or on-premise such as Open Stack, VM Ware), PaaS (Heroku, Kubernetes, etc.), to SaaS (DataDog, Fastly, Github Teams, etc.)

Terraform and Azure

Terraform can provision an infrastructure across multiple cloud providers. It’s just a matter for this cloud provider to provide the right Terraform modules. Below is a Terraform configuration file declaring the Azure provider (azurerm):

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 2.56"
    }
  }
}

provider "azurerm" {
  features {}
}

What Is NubesGen?

There are several ways to manually deploy a Java application to Azure. You can use the Azure command line, the Azure admin console or the Maven Azure plugin. But you need to create the infrastructure first (security roles, storage, configure the DNS, etc.). That’s when Terraform can be handy.

But depending on the complexity of your infrastructure, writing Terraform configuration files can be tricky. That’s when NubesGen becomes very handy: we can use NubesGen to generate the Terraform files for us.

NubesGen is an open-source project that was initiated in 2021. It lets you generate Terraform configuration files (and maybe Pulumi and Bicep in the future) for Azure (and who knows, maybe for other Cloud providers in the future). These configuration files can either be generated using the NubesGen web interface or a cURL command.

To see NubesGen in action, let’s bootstrap a simple Quarkus application and generate some Terraform configuration files.

Generating a Simple Quarkus Application

First of all, let’s generate a simple Quarkus application. To keep it simple, we will bootstrap an application that exposes a REST endpoint that returns “Hello NubesGen with Terraform”. Nothing too fancy, but this allows us to focus on the deployment, not the business code.

Bootstrapping a Quarkus Application

To easily bootstrap a Quarkus application you can either go to code.quarkus.io or use the following Maven command:

$ mvn -U io.quarkus:quarkus-maven-plugin:create \
        -DprojectGroupId=org.agoncal.article \
        -DprojectArtifactId=quarkus-nubesgen-terraform \
        -DpackageName="org.agoncal.article.nubesgen.terraform" \
        -Dextensions="quarkus-resteasy"

This creates a Maven structure with all the needed Quarkus configuration as well as some generated business code. In fact, once generated, the entire project is all setup and ready to run and be tested. You can use the following commands to execute Quarkus, test the application and invoke the REST endpoint:

$ mvn quarkus:dev
$ mvn test
$ curl http://localhost:8080/hello

Updating the Generated Code

Let’s change the generated code so our REST endpoint returns Hello NubesGen with Terraform with a timestamp:

@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello NubesGen with Terraform " + Instant.now();
    }
}

And if we want the tests to pass, it’s just a matter of changing the RESTAssured test:

@QuarkusTest
public class GreetingResourceTest {

  @Test
  public void testHelloEndpoint() {
    given()
      .when().get("/hello")
      .then()
      .statusCode(200)
      .body(startsWith("Hello NubesGen"));
  }
}

That’s enough business code, time to package and deploy the application.

Packaging the Application

Quarkus has different ways of packaging an application: as a Docker image, a native binary (thanks to GraalVM) or as an executable JAR. In fact, it also allows you to choose between several JAR formats, but that’s another topic. Let’s package the application in an executable Uber-JAR. It’s simple to deploy and portable. To build the Uber-JAR, execute the following command:

$ mvn package -Dquarkus.package.type=uber-jar

As an alternative, you can also set the property in the application.properties (quarkus.package.type=uber-jar) so you don’t have to pass it to the command line and just execute mvn package.

In the target directory, you end up with two JAR files. The .jar.original file is the one that is automatically packaged by Maven and is not executable (it’s just a JAR file with our code). The other one (-runner.jar) is 12 MB and is executable. That’s the one we want to deploy.

$ ll target/
 -rw-r--r--   12M  quarkus-nubesgen-terraform-1.0.0-SNAPSHOT-runner.jar
 -rw-r--r--  6.3K  quarkus-nubesgen-terraform-1.0.0-SNAPSHOT.jar.original

Executing the Packaged Application

Now that we have built the executable JAR, running the application is just a matter of executing:

$ java -jar target/quarkus-nubesgen-terraform-1.0.0-SNAPSHOT-runner.jar

Once Quarkus is up and running, execute the following cURL command, you should see our Hello NubesGen with Terraform:

$ curl http://localhost:8080/hello

Hello NubesGen with Terraform 2021-07-07T14:21:54.314396Z

Generating the Terraform Configuration Files with NubesGen

We have a simple application, with no database, no authentication, nothing, packaged in an executable Uber-JAR. Let’s ask NubesGen to generate the Terraform configuration files so we can create the needed Azure infrastructure. For that, we can either use the NubesGen website or a cURL command.

Generating with the Website

If you decide to generate the Terraform configuration files from the NubesGen website, you need to manually pick up what you need for your infrastructure. In our case, you need to give a name to the project (eg. quarkus-nubesgen-terraform), pick up a region and choose Quarkus from the combo box. Then you can specify if the application needs a relational database, a Redis cache or a MongoDB. In our case, we don’t need anything, just leave the checkboxes unchecked.

Click on Download configuration files, this will download a zip file containing the generated Terraform files that you can add at the root of your project.

 Generating Terraform Files with NubesGen Website

Generating with a cURL Command

If like me, you prefer the command line, NubesGen allows you to download these configuration files through a cURL command. If you click on or copy cURL script you will get the following cURL command:

$ curl "https://nubesgen.com/quarkus-nubesgen-terraform.tgz?iactool=TERRAFORM&region=northeurope&application=APP_SERVICE.free&runtime=QUARKUS&database=NONE.free" | tar -xzvf -

This command has the same parameters as the website. The first parameter is the project name (here quarkus-nubesgen-terraform), then, the region you want to deploy to (here Northern Europe), the Azure plan (Free), the runtime (in our case Quarkus), and we have no database.

Execute this command at the root of your project, you will get a set of generated files.

Generated Files

At the root of our Quarkus project, NubesGens has generated a set of Terraform files under a directory called terraform. Under this directory, despite the README.md and .gitignore files, all the files have the suffix tf for Terraform. The files are:

  • main.tf: Contains the main set of configurations for a module.
  • variables.tf: Contains the variable definitions for a module.
  • outputs.tf: Contains the output definitions for a module.

These files can be found at the root of the terraform directory, but also under the modules/app-service directory:

Generated Terraform Files Directory Structure

Basically, at the root of the terraform directory the files are used to define the needed providers (Azure in our case) and aggregate the other Terraform files. Under the subdirectory modules, you will find all the needed modules of our infrastructure (application, databases, etc.). In our case, we only have a Quarkus application, so it is configured under the app-service directory.

Root Terraform Files

At the root, the main.tf file starts by declaring the Azure provider. That’s important as we need Terraform to create our infrastructure on Azure:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 2.56"
    }
  }
}

provider "azurerm" {
  features {}
}

Then, the main.tf file aggregates all the other modules. In our case, we only have an application, that’s why main.tf only includes the modules under ./modules/app-service:

module "application" {
  source           = "./modules/app-service"
  resource_group   = azurerm_resource_group.main.name
  application_name = local.application_name
  environment      = local.environment
  location         = var.location
}

The variables.tf file defines the variables needed for our infrastructure. Here we define the name of our Quarkus application quarkus-nubesgen-terraform:

variable "application_name" {
  type        = string
  description = "The name of your application"
  default     = "quarkus-nubesgen-terraform"
}

Application Terraform Files

Under the modules directory, there is another sub-directory called app-service. That’s where we have the configuration for the Quarkus application per se. For example, this file sets HTTPs, the version of Java (here, 11), or the listening port for Quarkus:

resource "azurerm_app_service" "application" {
  name                = "app-${var.application_name}-001"
  resource_group_name = var.resource_group
  location            = var.location
  app_service_plan_id = azurerm_app_service_plan.application.id
  https_only          = true

  tags = {
    "environment" = var.environment
  }

  site_config {
    linux_fx_version          = "JAVA|11-java11"
    always_on                 = false
    use_32_bit_worker_process = true
    ftps_state                = "FtpsOnly"
  }

  app_settings = {
    "WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"

    # These are app specific environment variables
    "QUARKUS_HTTP_PORT" = 80
    "QUARKUS_PROFILE"   = "prod"
  }
}

Executing the Terraform Configuration Files

Thanks to NubesGen we have all the needed Terraform files to configure our infrastructure to run our REST endpoint. Time to execute Terraform. There are several ways of doing, but I’ll be using the Terraform CLI and going step-by-step so you understand what’s happening behind the scene.

Initialising the Configuration

First, let’s initialise the Terraform configuration files. For that, you need to be at the root of the terraform directory and execute terraform init:

terraform$ terraform init

Initializing modules... 
- application in modules/app-service

Initializing the backend...

Initializing provider plugins... 
- Finding hashicorp/azurerm versions matching ">= 2.56.0"... 
- Installing hashicorp/azurerm v2.66.0... 
- Installed hashicorp/azurerm v2.66.0 (signed by HashiCorp) 

Terraform has been successfully initialized!

During the initialisation phase, Terraform creates a .terraform directory with generated files, such as the modules.json or .terraform.lock.hcl files. But it also downloads the GO executables needed by the configuration (here the terraform-provider-azurerm_v2.66.0_x5 executable):

 Generated Temporary Files

You can also check if the configuration is valid with the validate command and show the state of the execution plan with the show command:

terraform$ terraform validate
Success! The configuration is valid. 

terraform$ terraform show
No state.

If you have GraphViz installed, you can even generate a visual representation of the execution plan with the graph command:

terraform$ terraform graph 
terraform$ terraform graph | dot -Tpng > graph.png

The visual aspect looks like this:

Graph Representation of the Execution plan

Logging in to Azure

If at that moment, you are not logged in to Azure and want to define the plan (with terraform plan), you will get the following error:

terraform$ terraform plan

│ Error: Error building AzureRM Client: obtain subscription() from Azure CLI: Error parsing json result from the Azure CLI: Error waiting for the Azure CLI: exit status 1: ERROR: Please run 'az login' to set up account.

That’s because you need to be logged in to Azure. You can easily log in with the following Azure CLI command:

terraform$ az login
You have logged in.

terraform$ az account show

{
  "environmentName": "AzureCloud",
  "homeTenantId": "1234-abcd",
  "id": "abcd-1234",
  "isDefault": true,
  "managedByTenants": [],
  "state": "Enabled",
  "tenantId": "1234-abcd",
  "user": {
    "name": "my.email@gmail.com",
    "type": "user"
  }
}

Planning the Execution

Time to plan the execution. The plan command shows the changes required by the current configuration. The execution plan is not executed yet, this command just shows what will be executed if you decide to execute the plan:

terraform$ terraform plan

Terraform will perform the following actions:

   # azurerm_resource_group.main will be created
   # module.application.azurerm_app_service.application will be created
   # module.application.azurerm_app_service_plan.application will be created

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
   + application_hostname = (known after apply)
   + resource_group       = "rg-quarkus-nubesgen-terraform-001"

The logs show that, from what’s currently deployed (nothing yet in our case) and what we’ve defined in the Terraform files, three changes will be made:

  • Create an Azure main group,
  • create the application service,
  • create the plan for the application.

Time to execute this plan!

Applying the Execution Plan

If we agree with the changes that are ready to be made (shown by the plan command), we need to apply them with the apply command. This command takes a bit of time because it needs to communicate with Azure to create the needed resources:

terraform$ terraform apply

azurerm_resource_group.main: Creating... 
azurerm_resource_group.main: Creation complete after 0s [id=/subscriptions/1234-abcd/resourceGroups/rg-quarkus-nubesgen-terraform-001]
module.application.azurerm_app_service_plan.application: Creating...
module.application.azurerm_app_service_plan.application: Creation complete after 19s [id=/subscriptions/1234-abcd/resourceGroups/rg-quarkus-nubesgen-terraform-001/providers/Microsoft.Web/serverfarms/plan-quarkus-nubesgen-terraform-001] module.application.azurerm_app_service.application: Creating...
module.application.azurerm_app_service.application: Creation complete after 30s [id=/subscriptions/1234-abcd/resourceGroups/rg-quarkus-nubesgen-terraform-001/providers/Microsoft.Web/sites/app-quarkus-nubesgen-terraform-001]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed. 

Outputs: 
  application_hostname = "https://app-quarkus-nubesgen-terraform-001.azurewebsites.net" 
  resource_group = "rg-quarkus-nubesgen-terraform-001"

As you can see, the method apply displays two very important pieces of information: the URL of where we can target our application and the Azure resource group. More on that below.

After running apply, you can execute terraform show and it will display the execution plan. On the other hand, if Terraform finds out that the infrastructure is already created and does not need to be updated, you will get a message such as:

terraform$ terraform apply

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed. 

Outputs:
 application_hostname = "https://app-quarkus-nubesgen-terraform-001.azurewebsites.net"
 resource_group = "rg-quarkus-nubesgen-terraform-001"

Cool! Let’s open our browser and point to our URL:

$ open https://app-quarkus-nubesgen-terraform-001.azurewebsites.net

This is disappointing! No Hello NubesGen with Terraform displayed but instead a web page telling us that the application service is up and running. Yes, but without our application. Well, that’s because we need to deploy our Uber JAR!

No Web App Deployed Yet

Manually Deploying the Application

Thanks to the Terraform configuration files, we have our infrastructure ready to host our Quarkus application. We just need to deploy it. In fact there are several ways to deploy our Uber-JAR. But let’s use the Azure Maven plugin to manually deploy it.

Configuring the Azure Maven Plugin

There is a very convenient way to configure the Azure Maven plugin, and that’s by invoking the config goal. When you do so, the plugin asks you a set of questions:

$ mvn com.microsoft.azure:azure-webapp-maven-plugin:2.0.0:config

Auth type: AZURE_CLI
Default subscription: (1234-abcd)
Username: my.email@gmail.com

Java SE Web Apps in subscription:
 * 1: <create>
   2: app-quarkus-nubesgen-terraform-001 (Linux|Java 11|Java SE)

Please confirm webapp properties
 Subscription Id : 1234-abcd
 AppName : app-quarkus-nubesgen-terraform-001
 ResourceGroup : rg-quarkus-nubesgen-terraform-001
 Region : northeurope
 PricingTier : F1
 OS : Linux
 Java : Java 11
 Web server stack: Java SE
 Deploy to slot : false

Confirm (Y/N) [Y]:

Once these questions answered, your pom.xml is automatically configured with plenty of information. As a matter of fact, we don’t need to have all the configuration set in the pom.xml as our infrastructure is already defined in the Terraform configuration files. Just the subscriptionIdresourceGroup and appName would do:

<plugin>
  <groupId>com.microsoft.azure</groupId>
  <artifactId>azure-webapp-maven-plugin</artifactId>
  <version>2.0.0</version>
  <configuration>
    <schemaVersion>v2</schemaVersion>
    <subscriptionId>1234-abcd</subscriptionId>
    <resourceGroup>rg-quarkus-nubesgen-terraform-001</resourceGroup>
    <appName>app-quarkus-nubesgen-terraform-001</appName>
    <deployment>
      <resources>
        <resource>
          <directory>${project.basedir}/target</directory>
          <includes>
            <include>*.jar</include>
          </includes>
        </resource>
      </resources>
    </deployment>
  </configuration>
</plugin>

Deploying the Uber-JAR with the Azure Maven Plugin

Now, it’s just a matter of using the plugin to deploy our Uber-JAR. For that, we invoke the deploy goal:

$ mvn azure-webapp:deploy

Auth type: AZURE_CLI
Default subscription: (1234-abcd)
Username: my.email@gmail.com
[INFO] Updating target Web App app-quarkus-nubesgen-terraform-001...
[INFO] Successfully updated Web App app-quarkus-nubesgen-terraform-001.
[INFO] Trying to deploy artifact to app-quarkus-nubesgen-terraform-001...
[INFO] Deploying (target/quarkus-nubesgen-terraform-1.0.0-SNAPSHOT-runner.jar)[jar] ...
[INFO] Successfully deployed the artifact to https://app-quarkus-nubesgen-terraform-001.azurewebsites.net

That’s it! It looks like the application is deployed, let’s check that out.

Checking the Deployed Application on Azure

First of all, we need to check if our REST endpoint is responding. For that, we use the URL that was displayed once the plan executed, and add our hello path:

$ curl https://app-quarkus-nubesgen-terraform-001.azurewebsites.net/hello

Good! The REST endpoint replies Hello NubesGen with Terraform, so it’s up and running. Let’s check the Azure console.

Checking the Azure Console

If you want to have a look at what’s deployed, one way is to go to the Azure portal. For that, go to https://portal.azure.com and check the resources. You should see the application as well as the plan:

Azure Portal

Checking with the Azure CLI

You can also use the Azure CLI. There are several commands you could use, but the webapp list is the one with the most information:

$ az webapp list -g rg-quarkus-nubesgen-terraform-001
$ az webapp list -g rg-quarkus-nubesgen-terraform-001 --output table

Evolving the Application

The infrastructure is set up and the application is deployed. But as you know, things evolved. If in the future the application needs a database, then you need to update your infrastructure. For that, you change the Terraform configuration files, apply these changes on Azure, and redeploy your application. If the infrastructure does not change, well, you just need to redeploy the application, and that’s it.

Redeploying a New Version of the Application

Now that the Azure Maven plugin is configured, to redeploy a new version of the application is quite easy: you change the code of the REST endpoint, compile it, rebuild a new Uber-JAR and redeploy it:

$ mvn package -Dquarkus.package.type=uber-jar

$ mvn azure-webapp:deploy

$ curl https://app-quarkus-nubesgen-terraform-001.azurewebsites.net/hello

Destroying the Infrastructure

And if one day you need to get rid of the entire infrastructure, Terraform is here to help. A simple destroy command will go through all the resources it needs to destroy and remove them one by one:

terraform$ terraform destroy

Terraform will perform the following actions:
   # azurerm_resource_group.main will be destroyed
   # module.application.azurerm_app_service.application will be destroyed
   # module.application.azurerm_app_service_plan.application will be destroyed

Plan: 0 to add, 0 to change, 3 to destroy.

Changes to Outputs:
   - application_hostname = "https://app-quarkus-nubesgen-terraform-001.azurewebsites.net" -> null
   - resource_group       = "rg-quarkus-nubesgen-terraform-001" -> null

Do you really want to destroy all resources?
   Terraform will destroy all your managed infrastructure, as shown above.
   There is no undo. Only 'yes' will be accepted to confirm.

   Enter a value: 

module.application.azurerm_app_service.application: Destroying... [id=/subscriptions/1234-abcd/resourceGroups/rg-quarkus-nubesgen-terraform-001/providers/Microsoft.Web/sites/app-quarkus-nubesgen-terraform-001]
module.application.azurerm_app_service_plan.application: Destroying... [id=/subscriptions/1234-abcd/resourceGroups/rg-quarkus-nubesgen-terraform-001/providers/Microsoft.Web/serverfarms/plan-quarkus-nubesgen-terraform-001]
azurerm_resource_group.main: Destroying... [id=/subscriptions/1234-abcd/resourceGroups/rg-quarkus-nubesgen-terraform-001]
azurerm_resource_group.main: Destruction complete after 46s

Destroy complete! Resources: 3 destroyed.

At that moment, a terraform show command will show…​ nothing. You can use the Azure CLI to check if the resource group has been deleted:

$ az webapp list -g rg-quarkus-nubesgen-terraform-001
(ResourceGroupNotFound) Resource group 'rg-quarkus-nubesgen-terraform-001' could not be found.

Conclusion

Today Infrastructure as Code is more and more used in projects. It is a nice way to define our infrastructure with code, and Terraform is a major player in this area. But Terraform can be tricky. Its language syntax is simple, but its ecosystem is huge and you need to know all the modules and parameters to configure your infrastructure. Deploying to the cloud, even a simple Java application, can take you a few lines of configuration to write. Writing configuration files is error-prone, and the feedback loop takes time: you change the configuration file, applies it, wait for it to be applied, realize that there is a mistake, change the file, apply it…​That’s when NubesGen helps you.

Download the code

NubesGen is just a generator of Terraform configuration files (Pulumi and Bicep are planned to be supported). The project is young but evolving at a rapid pace. It allows you to configure the infrastructure for a Quarkus application, as well as a Spring application, a simple Java application, packaged as a Docker image or not. You can generate configuration files for simple uses cases (as we just did), but also for more complex ones such as using a relational database, a MongoDB or a Redis cache. And by the way, once you have generated your configuration files, you commit them with your code and change them as you want. NubesGen is just there to generate the files, then, they belong to your project and you make them evolve.

If you plan to deploy your Java application to Azure, you should definitely give NubesGen a try!

By the way. I don’t know about you, but instead of deploying the application with the Azure Maven plugin, I would love to a CI/CD environment that would build my Quarkus application and deploy it automatically to Azure when I commit a new version of the code. Using GitHub Actions maybe. What do you think? But again, that’s GitHub Actions configuration files to be created and I don’t know how to do it. Wait, maybe NubesGen can help? Time for another article…​

References

You can also get my books and on-line courses on Quarkus.

Leave a Reply