Deploy Quarkus Application with GitLab-CI to Heroku PaaS

Build a full pipeline and deploy a Quarkus application to different environments with GitLab-CI

Elie Nehmé
10 min readApr 3, 2021
Photo by Sigmund on Unsplash

Introduction

The main purpose of this tutorial is to create and deploy step by step a simple application with Quarkus using GitLab-CI Pipeline and Heroku fully managed data services.

We will initiate three environment applications on Heroku: Staging, Pre-production and Production.

Staging environment will be attached to a staging branch, Pre-production to the master branch and production environment will be updated each time we add a protected tag to the master branch.

Source code could be found at https://gitlab.com/elie29/quarkus-gitlab-ci-Heroku

Prerequisites

We will not cover GraalVM nor docker through this article in order to focus on the pipeline and its different stages. However, we need at least Java 11 for code compilation and optionally maven 3.6.x if we want to create the Quarkus project via the command line.

Quarkus Application

First things first, let us create our application by using the following command (maven 3.6.x required):

We can use “https://code.quarkus.io/” if we don’t want to install Apache maven locally.

For more information on tooling: https://quarkus.io/guides/maven-tooling

The command will generate a ready to use simple application with one endpoint (api/users).

If we check the pom.xml, we can find the following dependencies:

Quarkus dependencies
  1. Quarkus JsonB: Standard binding layer for converting Java objects to/from JSON messages
  2. Quarkus arc: build-time oriented dependency injection
  3. Quarkus resteasy: JAX-RS implementation for Restful services in Quarkus
  4. Junit 5: For unit testing purpose
  5. Rest-assured: For Rest endpoints testing

I did some changes manually to the pom.xml by adding the following property and extensions:

  1. Application name
  2. Quarkus rest-client: Extension for the REST client support
  3. Lombok: Logging variables, Getter/Setter and other useful method automations

Starting the application

The UsersRessource.java has been generated automatically by the command line and has one method:

Default Endpoint created by Quarkus

The maven wrapper is shipped with the application, and the only prerequisites to run the application is JAVA 11:

  • ./mvnw quarkus:dev Launches the application on port 8080 in dev mode
  • curl http://localhost:8080/api/users would return “Hello RESTEasy”
  • Visiting localhost:8080, a default homepage created by Quarkus will be shown.
  • ./mwnw test, launches the test with rest-assured
Quarkus Test

Rest Endpoint Modification (Optional)

Instead of returning “Hello RESTEasy”, let us implement rest client to use jsonplaceholder users API. To do that, we need to add the following dependencies:

  1. Quarkus rest-client: to call rest services
  2. Quarkus resteasy-mutiny: for reactive JAX-RS endpoints

N.B.: to add these quarkus extensions, we can use ./mvnw quarkus:add-extension or directly modifying the pom.xml

Adding the model

We are interested only in some attributes: id, name, username, email and company. The model creation is facilitated by Lombok:

Data would create all setters/getters and toString

Setting the interface

UsersService interface is needed with the proper JAX-RS and MicroProfile annotations to call jsonplaceholder API:

https://jsonplaceholder.typicode.com/users

When returning a Uni, every subscription invokes the remote service asynchronously.

Configure the Client URL

In the application.properties, we configure the client URL as follows:

/users is appended in the UsersService Path

Update the controller and the test

UsersService is ready, let us inject it in the controller and call the getUsers:

The test should be updated as well:

Once the application is launched (./mvnw quarkus:dev), open the browser and call: http://localhost:8080/api/users

GitLab Project Creation

Before making any changes to our Quarkus project, I will create first a GitLab repository in order to push my first commit:

GitLab repository

Once created, we grab the remote repository address: git@gitlab.com:elie29/quarkus-gitlab-ci-heroku.git

In our Quarkus project, we open a terminal, we initiate a local git repository, and we commit all files (.gitignore is shipped with Quarkus) as follows:

N.B.: On windows, we should not forget to add mvnw as executable before pushing the code:

We create an empty staging branch and push it to the remote repository — it is required later in the configuration step:

Heroku Application Creation

As I mentioned above, We need to create a pipeline on GitLab to deploy our application on three environments: so I will create three applications on Heroku.

In order to create an application, we need to have a Heroku account. The account sign up is straightforward and needs only a valid email and a password (no credit card needed for the first 5 applications)

On Heroku Dashboard, select new → Create New App → enter a name (should be unique) and click on Create app — (Repeat the procedure 3 times):

  • For production environment, I chose: play-with-quarkus
  • For staging: play-with-quarkus-staging
  • For pre-production: play-with-quarkus-preprod

That’s it for Heroku✨

GitLab Repository Configuration

Let’s move to GitLab in order to configure a few settings and protect our branches.

Protect the merge request

Settings → General → Merge requests → check Pipeline must succeed and All discussions must be resolved then save changes

Protect branches and tags

Settings → Repository → Protected branches → add staging as protected branch:

We do the same for Protected tags but by using a wildcard → v*:

That means, a tag starting with v (e.g. v1.2.4) would be considered as protected tag. It prevents anyone from updating or removing the tag. Moreover, we can rely on protected tags to deploy to production.

Inject Variables

Last but not least, we need to inject Heroku variables as follows:

  • HEROKU_API_KEY: should contain Api Key found in Heroku account page
  • HEROKU_APP_PREPROD, HEROKU_APP_PRODUCTION and HEROKU_APP_STAGING: should contain Heroku app name. In my case:

We check Protected option for all variables and Masked option for at least HEROKU_API_KEY

Protected variables would be exposed only to protected branches and protected tags otherwise they will not be injected and available in the pipeline.

GitLab-CI Pipeline Initialization

Pipelines are the top-level artifact for CI/CD — continuous integration, delivery and deployment.

With GitLab, creating a pipeline is simple as adding a ‘.gitlab-ci.yml’ in the project’s root directory:

.gitlab-ci.yml

N.B.: file name and extension are case sensitive.

GitLab pipelines are composed of jobs and stages:

  • Jobs are tasks which define what to do: compile, test, package the code
  • Stages regroup jobs and define when to run them.

To understand better the concept, let’s start with a typical pipeline:

Simple Pipeline

This pipeline consists of 3 stages: Build, Test and Deploy, and in each stage we have declared one or two jobs.

Pipeline Syntax

To declare stages, we start with the following block:

stages:
- build
- test
- deploy

Then, we create each job and attach it to a specific stage as follows:

build-job:
stage: build
script:
- echo "Project compilation"

test-job:
stage: test
script:
- echo "Project test execution"

package-job:
stage: deploy
script:
- echo "Project packaging"

deploy-job:
stage: deploy
script:
- echo "Project deployment"

By default, a job with an unspecific stage is attached to the test stage.

Validate GitLab CI Configuration

In order to validate our pipeline before pushing it, we can use the linter editor found at CI/CD → Jobs → CI Lint:

https://gitlab.com/elie29/quarkus-gitlab-ci-heroku/-/ci/lint

Building Quarkus Application

Now, let’s build our Quarkus application through the pipeline.

Define the stages

Our application needs to be compiled, tested, packaged and deployed.

Actually, we don’t need to package the application as Heroku would do the job perfectly. So, we need 3 stages:

stages:
- build
- test
- deploy

build-job:
stage: build
script:
- ./mvnw compile -B
# keep compilation for test stage
artifacts:
paths:
- target

test-job:
stage: test
script:
- ./mvnw test -B

deploy-job:
stage: deploy
script:
- echo "deploying to heroku..."

But in order to launch maven wrapper, we need Java compiler. For that, we add the following:

image: openjdk:11

The pipeline could be find at: https://gitlab.com/elie29/quarkus-gitlab-ci-heroku/-/blob/02-quarkus-build/.gitlab-ci.yml

Deploying Quarkus Application

Heroku variables are created in GitLab, however we need to tweak Heroku to identify our quarkus project and uses Java:11

For that, we need to create two files:

  • system.properties: to define java version “java.runtime.version=11
  • Procfile: case-sensitive file to define the web process responsible of launching our application “web: java -Dquarkus.http.port=$PORT $JAVA_OPTS -jar target/*-runner.jar

To deploy to Heroku, we will use ruby deploy tool “DPL”:

In order to use the “dpl” command, we add a before_script to install ruby:

Changes pushed to remote repository could be found at: https://gitlab.com/elie29/quarkus-gitlab-ci-heroku/-/blob/03-quarkus-deploy/.gitlab-ci.yml

Staging Deployment

We deploy to the staging environment only when a code is merged to the staging protected branch. Two keywords could be useful to specify this condition — only or rules:

deploy-staging:
stage: deploy
before_script:
- apt-get update -qy
- apt-get install -y ruby-dev
- gem install dpl
script:
- dpl --provider=heroku --app=$HEROKU_APP_STAGING --api-key=$HEROKU_API_KEY --skip-cleanup=true
only:
- staging

In order to launch the deploy stage, we create a merge request to staging branch as follows:

Merge Request could be accepted as Pipeline has succeeded

Once the merge request is accepted, the pipeline is launched. The application is now deployed on staging:

We test the application by visiting Heroku staging app: https://play-with-quarkus-staging.herokuapp.com/api/users

Pre-production and Production Deployment

Same as staging, but for pre-production environment, only changes on master would trigger the deployment. However, for production environment, only tags would trigger the deployment.

With some refactoring, and the using of hidden jobs, we completed the pipeline:

deploy-staging:
extends: .deploy
variables:
HEROKU_APP: $HEROKU_APP_STAGING
only:
- staging

deploy-preprod:
extends: .deploy
variables:
HEROKU_APP: $HEROKU_APP_PREPROD
only:
- master

deploy-production:
extends: .deploy
variables:
HEROKU_APP: $HEROKU_APP_PRODUCTION
only:
- tags

After merging to master and creating the first protected tag v1.0.0, the application is now deployed to pre-production and production environments.

URLs are as follows:

Caching dependencies

In order to preserve maven dependencies in subsequent pipelines, GitLab-CI proposes cache keyword, so dependencies once downloaded, they don’t have to be fetched from the internet again.

N.B.: artifacts are used to pass build results between stages(cf. cache vs artifact)

For maven, we define first the local repo in MAVEN_OPTS then we specify the cache paths and key. We may choose different strategy for caching objects (by branch, by commit, …)

cache key using Project Id

Through this configuration, all subsequent pipelines for this project would retrieve maven dependencies from the cache.

Clearing the cache

Sometimes, we need to start the pipeline with a fresh copy of the cache, so to do that, we clear the cache via GitLab Pipeline interface:

Clear Runner Caches

We can clear the cache by changing the key as well.

Security Checks

GitLab-CI offers many templates to check for security vulnerabilities. However, most of them are only available with Ultimate Version. Here, we will use Secret Detection and Static Application Security Testing (SAST) as they are available for all tiers.

The simplest way to use GitLab security scanning tools is by including their templates as follows:

SAST and Secret detection

Once templates are included, we need to configure SAST execution to avoid job compilation twice :

Don’t compile, check only

Now, pushing the modification — branch 06-security — to the remote repository launches the pipeline as follows:

Wrapping Up

This article shows how to create and deploy an application using different strategies with GitLab-ci pipeline.

A step could be added in order to store project artifacts in GitLab Package Registry, Nexus or JFrog Artifactory. We can also improve the pipeline to create and store a container image.

The complete source code is available at: https://gitlab.com/elie29/quarkus-gitlab-ci-heroku, each step has its own branch:

Don’t hesitate to give me your feedback, or contact me on Twitter @elie_nehme

--

--

Elie Nehmé
Elie Nehmé

Written by Elie Nehmé

Lead Web Application Architect. Passionate about refactoring, clean code, and building scalable solutions with simplicity. https://elie29.hashnode.dev/

No responses yet