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):
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 |
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:
and select Import 'ConfigProperty' (org.eclipse.microprofile.config.inject)
.
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 |
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.message
, greeting.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):
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:
dev
- Activated when in development mode (i.e.mvn quarkus:dev
)test
- Activated when running tests (i.e.mvn verify
)prod
- The default profile when not running indev
ortest
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:
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 |
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.
No comments:
Post a Comment