Tuesday, March 23, 2021

Deploying to the Cloud

 8

Deploying to the cloud

With our app fully ready for its first cloud native deployment, let’s package it up for deployment to our Kubernetes platform as a native image. We’ll use some OpenShift tooling to accomplish this, as outlined in the Quarkus - Deploying on Kubernetes Guide.

OpenShift is a commercially supported distribution of Kubernetes from Red Hat. The platform is also available as open source, in the form of OKD, the Origin Community Distribution of Kubernetes that powers Red Hat OpenShift.

Login to OpenShift

Although your Eclipse Che workspace is running on the Kubernetes cluster, it’s running with a default restricted Service Account that prevents you from creating most resource types. So we’ll log in with your workshop user. Click on Login to OpenShift, and enter your given credentials:

  • Username: user10

  • Password: openshift

login

Use the username and password you were assigned by the instructor.

You should see:

Login successful.

You have one project on this server: "user10-project"

Using project "user10-project".
Welcome! See 'oc help' to get started.

After you log in using Login to OpenShift, the terminal is no longer usable as a regular terminal. You can close the terminal window. You will still be logged in when you open more terminals later!

Congratulations, you are now authenticated to the OpenShift server via the CLI. We’ll use the prettier web console later on in this lab.

The login session might timeout after long periods of inactivity. If this happens, you’ll get messages like Error from server (Forbidden): xxxxx is forbidden: User "system:anonymous" cannot xxxxx. Simply login again!

Namespaces are a top level concept to help you organize your deployments and teams of developers. A namespace allows a community of users (or a user) to organize and manage their content in isolation from other communities. OpenShift projects provide additional functionality for managing Kubernetes namespaces.

For this scenario, a project has been created for you called user10-project. You will use this project to deploy your developed project in the next step.

Build and Deploy native image

Quarkus offers the ability to automatically generate OpenShift resources based on sane default and user supplied configuration. The OpenShift extension is actually a wrapper extension that brings together the kubernetes and container-image-s2i extensions with defaults so that it’s easier for the user to get started with Quarkus on OpenShift.

Add openshift extension via CodeReady Workspaces Terminal:

mvn -q quarkus:add-extension -Dextensions="openshift" -f $CHE_PROJECTS_ROOT/quarkus-workshop-m1m2-labs

you will see:

✅ Extension io.quarkus:quarkus-openshift has been installed

Next, add the following variables in src/main/resources/application.properties for native compilation using Mandrel builder image:

%prod.quarkus.kubernetes-client.trust-certs=true
%prod.quarkus.kubernetes.deploy=true
%prod.quarkus.kubernetes.deployment-target=openshift
%prod.quarkus.openshift.expose=true
We are using self-signed certs in this simple example, so this simply says to the extension to trust them.
Instructs the extension to deploy to OpenShift after the container image is built
Instructs the extension to generate and create the OpenShift resources (like DeploymentConfig and Service) after building the container
Instructs the extension to generate an OpenShift Route.

Rebuild and re-deploy the people application via running the following maven plugin in CodeReady Workspaces Terminal:

mvn clean package -Pnative -DskipTests -Dquarkus.package.uber-jar=false -f $CHE_PROJECTS_ROOT/quarkus-workshop-m1m2-labs

As you recall, the output of this process is a native Linux binary but also running Source-To-Image(S2I) build processor.

Wait for it to finish!. You should get a BUILD SUCCESS message at the end. Once that’s done, make sure it’s actually done rolling out:

oc rollout status -w dc/people

dc in dc/people is shorthand for OpenShift’s DeploymentConfig object type. There are other shortcuts like bc for BuildConfigsvc for Kubernetes Services, and so on.

Wait for that command to report replication controller "people-1" successfully rolled out before continuing.

And now we can access using curl once again. In the Terminal, run this command to access the endpoint:

curl $(oc get route people -o=go-template --template='{{ .spec.host }}')/hello/greeting/quarkus-on-openshift

The above curl command constructs the URL to your running app on the cluster using the oc get route command.

You should see:

hello quarkus-on-openshift from people-1-9sgsm

Your hostname (the Kubernetes pod in which your app runs) name will be different from the above.

So now our app is deployed to OpenShift. You can also see it in the OpenShift Console. Login with your assigned username and password (e.g. user10/openshift):

login

Once logged in, click on the name of your project (user10-project):

project

Switch to the Developer Perspective using the upper-left drop-down:

perspective

This provides a developer-centric Topology view of applications deployed to the project. You can see the single people deployment that we just deployed earlier using the CLI:

project

Click on the circle to get details:

container

Click on the View Logs link to see the console output from the app:

logs

This is the same output you saw earlier when you ran it "locally" with it’s super-fast startup time.

Go back to the Topology view. Since this app is exposed to the world, a Route was created which you can access using the small arrow in the upper right of the circle. Click on the route link:

logs

You can click on the route link to open up the default Quarkus page that’s packaged as part of our workshop application.

Connect MicroProfile health check

Earlier you implemented a series of MicroProfile health checks. To make OpenShift aware of these available health checks and begin using them, run the following commands in a Terminal in CodeReady:

oc set probe dc/people --readiness --initial-delay-seconds=5 --period-seconds=5 --failure-threshold=20 --get-url=http://:8080/health/ready && oc set probe dc/people --liveness --initial-delay-seconds=5 --period-seconds=5 --failure-threshold=20  --get-url=http://:8080/health/live &&
oc rollout latest dc/people

You’ll see in the Topology view that the app is re-deployed with the new settings and the old app will be terminated soon after:

logs

This configures both a readiness probe (is the app initialized and ready to serve requests?) and a liveness probe (is the app still up and ready to serve requests) with default timeouts. OpenShift will not route any traffic to pods that don’t respond successfully to these probes. By editing these, it will trigger a new deployment.

At this point, the probes will be accessed periodically to ensure the app is healthy.

Congratulations!

This step covered the deployment of a native Quarkus application on OpenShift. However, there is much more, and the integration with these cloud native platforms (through health checks, configuration management, and monitoring) has been tailored to make Quarkus applications execution very smooth.

This is the end of the Basic Quarkus Hands-On Lab. You can now continue with the Advanced Quarkus Hands-On Lab if your instructor has included that lab.

Developing Cloud Native with Quarkus

 7

Developing Cloud Native with Quarkus

In this step we will package the application as a Linux Container image, and deploy it to Kubernetes, and add a few features common to cloud native apps that you as a developer will need to handle. We’ll use OpenShift 4 as our deployment target, which is a distribution of Kubernetes from Red Hat.

Health Probes

Quarkus application developers can utilize the MicroProfile Health specification to write HTTP health probes for their applications. These endpoints by default provide basic data about the service however they all provide a way to customize the health data and add more meaningful information (e.g. database connection health, backoffice system availability, etc).

There are of course a category of issues that can’t be resolved by restarting the container. In those scenarios, the container never recovers and traffic will no longer be sent to it (which can have cascading effects on the rest of the system, possibly requiring human intervention, which is why monitoring is crucial to availability).

Add Extension

Let’s build a simple REST application endpoint exposes MicroProfile Health checks at the /health endpoint according to the specification. It will also provide several other REST endpoints to allow us to dynamically query the health of our Quarkus application.

We’ll need to add a Quarkus Extension to enable this feature in our app. Fortunately, adding a Quarkus extension is super easy. We’ll cover extensions in more depth in other sections of this workshop but for now, open a Terminal and execute the following command to add the extension to our project’s pom.xml:

mvn -q quarkus:add-extension -Dextensions="smallrye-health" -f $CHE_PROJECTS_ROOT/quarkus-workshop-m1m2-labs

You should get:

✅ Extension io.quarkus:quarkus-smallrye-health has been installed

This will add the extension below to your pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

With no code, Quarkus still provides a default health check which may be enough for you if all you need is to know the app started. Try to access the /health/ready endpoint on the Terminal:

curl http://localhost:8080/health/ready

You’ll see:

{
    "status": "UP",
    "checks": [
    ]
}

This default health check will return success as long as the app is running - if it crashes, the health check will of course fail.

Add a probe

We can now implement a better Health Check using the MicroProfile APIs. Create a new Java class - org.acme.people.health.SimpleHealthCheck (hint: right-click on the org.acme.people.health package and select New > File and name it SimpleHealthCheck.java). In this file, implement the health check (you can copy/paste this code):

package org.acme.people.health;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
@Readiness
public class SimpleHealthCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("Simple health check").up().build();
    }
}

As you can see health check procedures are defined as implementations of the HealthCheck interface which are defined as CDI beans with the either the @Readiness or @Liveness annotation. HealthCheck is a functional interface whose single method call returns a HealthCheckResponse object which can be easily constructed by the fluent builder API shown above. This simple example will serve as our Readiness probe.

There are two types of probes in Quarkus apps (and Kubernetes):

  • Liveness Probe - Many applications running for long periods of time eventually transition to broken states, and cannot recover except by being restarted. Kubernetes provides liveness probes to detect and remedy such situations. Restarting a container in such a state can help to make the application more available despite bugs.

  • Readiness Probe - Sometimes, applications are temporarily unable to serve traffic. For example, an application might need to load large data or configuration files during startup, or depend on external services after startup. In such cases, you don’t want to kill the application, but you don’t want to send it requests either. Kubernetes provides readiness probes to detect and mitigate these situations. A pod with containers reporting that they are not ready does not receive traffic through Kubernetes Services.

Readiness and liveness probes can be used in parallel for the same container. Using both can ensure that traffic does not reach a container that is not ready for it, and that containers are restarted when they fail. There are various Configuration Paramters you can set, such as the timeout period, frequency, and other parameters that can be tuned to expected application behavior.

Thanks to Live Coding mode, simply open a Terminal window and run:

curl http://localhost:8080/health/ready

The new health check procedure is now present in the checks array:

{
    "status": "UP",
    "checks": [
        {
            "name": "Simple health check",
            "status": "UP"
        }
    ]
}

Congratulations! You’ve created your first Quarkus health check procedure. Let’s continue by exploring what else can be done with the MicroProfile Health specification.

Custom data in health checks

In the previous step we created a simple health check with only the minimal attributes, namely, the health check name and its state (UP or DOWN). However, MicroProfile also provides a way for the applications to supply arbitrary data in the form of key/value pairs sent in the health check response. This can be done by using the withData(key, value)` method of the health check response builder API. This is useful to provide additional info about passing or failing health checks, to give some indication of the problem when failures are investigated.

Let’s create our second health check procedure, a Liveness probe. Create another Java class file in the org.acme.people.health package named DataHealthCheck.java with the following code:

package org.acme.people.health;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
@Liveness
public class DataHealthCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("Health check with data")
        .up()
        .withData("foo", "fooValue")
        .withData("bar", "barValue")
        .build();

    }
}

Access the liveness health checks:

curl http://localhost:8080/health/live

You can see that the new health check with data is present in the checks array. This check contains a new attribute called data which is a JSON object consisting of the properties (e.g. bar=barValue) we have defined in our health check procedure above:

{
    "status": "UP",
    "checks": [
        {
            "name": "Health check with data",
            "status": "UP",
            "data": {
                "bar": "barValue",
                "foo": "fooValue"
            }
        }
    ]
}

Negative Health Checks

In this section we create another health check which simulates a connection to an external service provider such as a database. For simplicity reasons, we’ll use an application.properties setting to toggle the health check from DOWN to UP.

Create another Java class in the same package called DatabaseConnectionHealthCheck.java with the following code:

package org.acme.people.health;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
import org.eclipse.microprofile.health.Liveness;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
@Liveness
public class DatabaseConnectionHealthCheck implements HealthCheck {

    @ConfigProperty(name = "database.up", defaultValue = "false")
    public boolean databaseUp;

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("Database connection health check");

        try {
            simulateDatabaseConnectionVerification();
            responseBuilder.up();
        } catch (IllegalStateException e) {
            // cannot access the database
            responseBuilder.down()
                    .withData("error", e.getMessage());
        }

        return responseBuilder.build();
    }

    private void simulateDatabaseConnectionVerification() {
        if (!databaseUp) {
            throw new IllegalStateException("Cannot contact database");
        }
    }
}

Re-run the health check test:

curl -i http://localhost:8080/health/live

You should see at the beginning the HTTP response:

HTTP/1.1 503 Service Unavailable

And the returned content should begin with "status": "DOWN" and you should see in the checks array the newly added Database connection health check which is down and the error message explaining why it failed:

        {
            "name": "Database connection health check",
            "status": "DOWN",
            "data": {
                "error": "Cannot contact database"
            }
        },

Fix Health Check

We shouldn’t leave this application with a health check in DOWN state. Because we are running Quarkus dev mode, add this to to the end of the src/main/resources/application.properties file:

database.up=true

And access again using the same curl command — it should be UP!

Accessing liveness and readiness separately

Quarkus apps can access the two different types using two different endpoints (/health/live and /health/ready). This is useful when configuring Kubernetes with probes which we’ll do later, as it can access each separately (and configure each with different timeouts, periods, failure thresholds, etc). For example, You may want your Readiness probe to wait 30 seconds before starting, but Liveness should wait 2 minutes and only wait 10 seconds between retries.

Access the two endpoints. Each endpoint will only report on its specific type of probe:

curl http://localhost:8080/health/live

You should only see the two Liveness probes.

curl http://localhost:8080/health/ready

You should only see our single readiness probes.

Later, when we deploy this to our Kubernetes cluster, we’ll configure it to use these endpoints.

Externalized Configuration

Hardcoded values in your code are a no-no (even if we all did it at some point ;-)). In this step, we learn how to configure your application to externalize configuration.

Quarkus uses MicroProfile Config to inject the configuration into the application. The injection uses the @ConfigProperty annotation, for example:

@ConfigProperty(name = "greeting.message")
String message;

When injecting a configured value, you can use @Inject @ConfigProperty or just @ConfigProperty. The @Inject annotation is not necessary for members annotated with @ConfigProperty, a behavior which differs from MicroProfile Config.

Add some external config

In the org.acme.people.rest.GreetingResource class, add the following fields to the class definition below the existing @Inject GreetingService service; line:

    @ConfigProperty(name = "greeting.message")
    String message;

    @ConfigProperty(name = "greeting.suffix", defaultValue="!")
    String suffix;

    @ConfigProperty(name = "greeting.name")
    Optional<String> name;

You’ll get red squiggly errors underneath @ConfigProperty. Hover the cursor over them and select Quick Fix:

quickfix

and select Import 'ConfigProperty' (org.eclipse.microprofile.config.inject).

quickfix

Do the same for the java.util.Optional type to eliminate the errors.

The new import statements can also be added manually:

import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.util.Optional;

MicroProfile config annotations include a name = (required) and a defaultValue = (optional). You can also later access these values directly if declared as a String or other primitive type, or declare them with <Optional> type to safely access them using the Optional API in case they are not defined.

Now, modify the hello() method to use the injected properties:

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return message + " " + name.orElse("world") + suffix;
    }
Here we use the Optional API to safely access the value using name.orElse() and provide a default world value in case the value for name is not defined in application.properties.

Create the configuration

By default, Quarkus reads application.properties. Add the following properties to the src/main/resources/application.properties file:

greeting.message = hello
greeting.name = quarkus

Open up a Terminal window and run a curl command to test the changes:

curl http://localhost:8080/hello

You should get hello quarkus!.

If the application requires configuration values and these values are not set, an error is thrown. So you can quickly know when your configuration is complete.

Update the test

We also need to update the functional test to reflect the changes made to endpoint. Edit the src/test/java/org/acme/people/GreetingResourceTest.java file and change the content of the testHelloEndpoint method to:

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("hello quarkus!")); // Modified line
    }

Since our applcation is still running from before, thanks to Quarkus Live Reload we should immediately see changes. Update application.properties, by changing the greeting.messagegreeting.name, or adding greeting.suffix and running the same curl http://localhost:8080/hello after each change.

Quarkus Configuration options

Quarkus itself is configured via the same mechanism as your application. Quarkus reserves the quarkus. namespace in application.properties for its own configuration.

It is also possible to generate an example application.properties with all known configuration properties, to make it easy to see what Quarkus configuration options are available depending on which extensions you’ve enabled. To do this, open a Terminal and run:

mvn -q quarkus:generate-config -f $CHE_PROJECTS_ROOT/quarkus-workshop-m1m2-labs

This will create a src/main/resources/application.properties.example file that contains all the config options exposed via the extensions you currently have installed. These options are commented out, and have their default value when applicable.

Overriding properties at runtime

As you have seen, in dev mode, properties can be changed at will and reflected in the running app, however once you are ready to package your app for deployment, you’ll not be running in dev mode anymore, but rather building and packaging (e.g. into fat JAR or native executable.) Quarkus will do much of its configuration and bootstrap at build time. Most properties will then be read and set during the build time step. To change them, you have to stop the application, re-package it, and restart.

Extensions do define some properties as overridable at runtime. A canonical example is the database URL, username and password which is only known specifically in your target environment. This is a tradeoff as the more runtime properties are available, the less build time pre-work Quarkus can do. The list of runtime properties is therefore lean.

You can override these runtime properties with the following mechanisms (in decreasing priority):

  • using system properties:

    1. for a runner jar: java -Dquarkus.datasource.password=youshallnotpass -jar target/myapp-runner.jar

    2. for a native executable: ./target/myapp-runner -Dquarkus.datasource.password=youshallnotpass

  • using environment variables:

    1. for a runner jar: QUARKUS_DATASOURCE_PASSWORD=youshallnotpass java -jar target/myapp-runner.jar

    2. for a native executable: QUARKUS_DATASOURCE_PASSWORD=youshallnotpass ./target/myapp-runner

Environment variables names are following the conversion rules of Eclipse MicroProfile Config sources

Configuration Profiles

Quarkus supports the notion of configuration profiles. These allow you to have multiple configuration values in application.properties and select between then via a profile name.

The syntax for this is %{profile}.config.key=value. For example if I have the following: (do not copy this code!):

quarkus.http.port=9090
%dev.quarkus.http.port=8181

The Quarkus HTTP port will be 9090, unless the dev profile is active, in which case it will be 8181.

By default Quarkus has three profiles, although it is possible to use as many as you like (just use your custom profile names in application.properties and when running the app, and things will match up). The default profiles are:

  1. dev - Activated when in development mode (i.e. mvn quarkus:dev)

  2. test - Activated when running tests (i.e. mvn verify)

  3. prod - The default profile when not running in dev or test mode

Exercise Configuration Profile

Let’s give this a go. In your application.properties, add a different message.prefix for the prod profile. To do this, change the content of the greeting. properties in application.properties to be:

greeting.message = hello
greeting.name = quarkus in dev mode
%prod.greeting.name = production quarkus

Verify that in dev mode (which you’re currently running in) that:

curl http://localhost:8080/hello

produces hello quarkus in dev mode!.

Next, let’s re-build the app as an executable JAR (which will run with the prod profile active).

Build an executable JAR using the Package app for OpenShift command to build an Uber-JAR:

livecoding

Next, run the the rebuilt app in a Terminal:

java -Dquarkus.http.port=8081 -jar $CHE_PROJECTS_ROOT/quarkus-workshop-m1m2-labs/target/*-runner.jar

Notice we did not specify any Quarkus profile. When not running in dev mode (mvn quarkus:dev), and not running in test mode (mvn verify), then the default profile is prod.

While the app is running, open a separate Terminal window and test it by running:

curl http://localhost:8081/hello

What did you get? You should get hello production quarkus! indicating that the prod profile was active by default. In other sections in this workshop we’ll use this feature to overrride important variables like database credentials.

In this example we read configuration properties from application.properties. You can also introduce custom configuration sources in the standard MicroProfile Config manner. More Info. This would be useful, for example, to read directly from Kubernetes ConfigMaps.

Cleanup

Stop the app that you ran with java -jar by pressing CTRL+C in the terminal in which the app runs. Make sure to leave the original Live Coding app running!

Congratulations

Cloud native encompasses much more than health probes and externalized config. With Quarkus' container and Kubernetes-first philosophy, excellent performance, support for many cloud native frameworks, it’s a great place to build your next cloud native app.