In the previous article we prepared an Azure Kubernetes cluster to host our containerized Java applications. In this final article of the series, we’ll create a Java microservice to work with Azure Table storage. We’ll then containerize this microservice, push it to the container registry, and deploy the containerized service to our Azure Kubernetes cluster.
Creating a Java Microservice
We’ll start by creating our microservice using the Quarkus framework. Follow these instructions to get started. Or, as an easier alternative use Visual Studio Code. It has a dedicated Quarkus extension that helps you generate the project, which will also contain Dockerfiles. After installing the Quarkus extension, use the Visual Studio Code command palette to develop a Quarkus project.
Configure your Quarkus project as follows:
- Build tool: Maven
- Project groupId: com.temperatureservice
- Project artifactId: temperature-service
- Project version: 1.0.0-SNAPSHOT
- Package name: com.temperatureservice
- Resource name: Temperature
The generated project will contain four Dockerfiles (in the docker folder), as well as the Temperature.java file (under java/com/temperatureservice) with a simple REST API containing the hello world
method. We’ll replace this method with a method that reads recent temperature values from our SensorReadings
table. To enable this, augment the dependencies
section of the pom.xml file with one entry:
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-storage</artifactId>
<version>4.2.0</version>
</dependency>
This adds a reference to the Azure Storage SDK for Java. Next, add to the project the SensorReadings
class:
import com.microsoft.azure.storage.table.TableServiceEntity;
public class SensorReading extends TableServiceEntity{
private String PartitionKey;
private String RowKey;
private String Temperature;
public String getPartitionKey() { return this.PartitionKey; }
public void setPartitionKey(String key) { this.PartitionKey = key; }
public String getRowKey() { return this.RowKey; }
public void setRowKey(String key) { this.RowKey = key; }
public String getTemperature() { return this.Temperature; }
public void setTemperature(String temperature) { this.Temperature = temperature; }
}
The above class has the same structure as the one we had created in the first article of the series. It derives from TableServiceEntity
— the base class the Azure Storage SDK uses to serialize and deserialize objects transferred to or from Azure Table Storage.
Now let’s modify Temperature.java as follows:
@Path("/temperature")
public class Temperature {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String Get() {
try {
CloudTable cloudTable = GetCloudTable("SensorReadings");
String filter = GetTimeFilter();
TableQuery<SensorReading> tableQuery =
TableQuery.from(SensorReading.class)
.where(filter);
Iterable<SensorReading> sensorReadings = cloudTable.execute(tableQuery);
if(sensorReadings.iterator().hasNext()){
SensorReading sensorReading = sensorReadings.iterator().next();
return sensorReading.getTemperature();
}
else
{
return "No recent sensor readings available";
}
}
catch (Exception e) {
return(e.toString());
}
}
}
The above code will connect to the Azure Storage table instance and retrieve sensor readings from the last minute. To connect to the table, add the following helper method:
private static CloudTable GetCloudTable(String tableName) {
final String connectionString =
"DefaultEndpointsProtocol=https;" +
"AccountName=<YOUR_ACCOUNT_NAME>;" +
"AccountKey=<YOUR_KEY>" +
"TableEndpoint=<YOUR_ENDPOINT>;";
try {
CloudStorageAccount storageAccount = CloudStorageAccount.parse(connectionString);
CloudTableClient tableClient = storageAccount.createCloudTableClient();
return tableClient.getTableReference(tableName);
} catch (Exception e) {
return null;
}
}
The method returns an instance of CloudTable
, from which we get the items. To create the filter, use the GetTimeFilter
helper method:
private static final String GetTimeFilter() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SS'Z'");
String dateTimeString = ZonedDateTime.now(ZoneId.of("UTC"))
.minusMinutes(1).format(formatter);
return String.format("Timestamp ge datetime'%s'", dateTimeString);
}
This helper method creates a filter shaped as Timestamp ge datetime'2021-03-09T08:09:07.54Z'
, where the datetime
part is created dynamically.
Remember that we had a problem generating the filter dynamically when implementing Azure Functions. Here, we solve this by programmatically accessing the cloud table with our own integration code.
To test the microservice, invoke the following command:
mvn compile quarkus:dev
This will start the service.
Go to your Azure Table and add an item. Then, send a GET request to the local service: http://localhost:8080/temperature. It will return the temperature as shown in the screenshot below.
Using Azure Container Registry
Basic Docker taxonomy requires that you push the Docker image to the registry before deploying it to the Kubernetes cluster. Here, create our own private registry using Azure Container Registry (ACR).
Let’s go to Azure portal and create the ACR registry instance with a basic SKU. We’ll use the ACR instance dbacr
. Then, we need to attach the ACR to the AKS cluster. The easiest way to do so is through the Azure CLI, where you invoke the following command (see the screenshot below):
az aks update -g aks-group -n akscluster --attach-acr dbacr
Building the Docker Image
To build the Docker image, we could use one of the four Dockerfiles that were created along with the project. However, these only work locally, after invoking the mvn package command. To overcome this issue, modify Dockerfile.jvm to use the multi-stage build, in which the app is first built, and then a lighter image is created for deployment:
FROM maven:3.6.3-jdk-11 AS MAVEN_BUILD
# Copy the pom and src code to the container
COPY ./ ./
# Package our application code
RUN mvn clean package
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https:
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
# We make four distinct layers so if there are application changes the library layers can be reused
COPY --from=MAVEN_BUILD --chown=1001 target/quarkus-app/lib/ /deployments/lib/
COPY --from=MAVEN_BUILD --chown=1001 target/quarkus-app
You can test the Dockerfile by invoking the following commands from the app folder (note the last . points to the docker build context /current directory/):
docker build -f src/main/docker/Dockerfile.jvm -t quarkus/temperature-service-jvm .
docker run -i --rm -p 8080:8080 quarkus/temperature-service-jvm
Deployment to AKS
Now everything is ready, and we can deploy the app to the AKS cluster through the Azure DevOps pipeline. The most straightforward way to do so is through the Deployment center available in the Azure Portal.
The Deployment center enables you to quickly create CI/CD pipelines and the necessary Kubernetes manifests. To start, push your code to Azure Repos. Then click Next in the Deployment center. In the page that opens, choose your project and the branch.
In the next step, configure Dockerfile path, change Port to "8080," and type "." in Docker build context.
Finally, create the new Kubernetes namespace and select your Azure Container Registry.
The Deployment center will provision the Azure DevOps pipelines. You can access them by clicking the Build hyperlink.
This will redirect you to Azure DevOps. Here is an example of the running build
pipeline:
Here is an example of the release
pipeline:
Testing the Service
Under the hood, the Deployment center created the Kubernetes manifest that is composed of:
- The deployment that uses the Docker image pushed to the Azure Kubernetes Service
- The service that exposes the app through the Azure Load Balancer
To access the Quarkus microservice, you need to obtain the public IP of your service. The easiest way to do so is through Azure Cloud Shell, by invoking the following commands:
az aks get-credentials -n akscluster -g aks-group
kubectl get svc --all-namespaces=true
The public IP of your service appears in the EXTERNAL-IP
column. Use this IP address to send a GET request to your temperature service, like this:
curl http:
Next Steps
In this article, we created a Java microservice that works with Azure Table storage. Then, we dockerized it, and deployed it to Azure Kubernetes Service. The connection string to Azure Table storage was hardcoded into the service. In practice, you could pass it to the Docker image through a Kubernetes secret.
To learn more about working with Kubernetes on Azure, explore The Kubernetes Bundle and Hands-On Kubernetes on Azure.
In the next Java on Azure series, we’ll learn more about monitoring and scaling containerized apps, cloud native authentication, and leveraging cloud native services.