Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Java on Azure: Adding Containers

0.00/5 (No votes)
27 Apr 2021 1  
In this article we’ll create a Java microservice to work with Azure Table storage.

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:

XML
<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:

Java
import com.microsoft.azure.storage.table.TableServiceEntity;
 
public class SensorReading extends TableServiceEntity{
    private String PartitionKey;
    private String RowKey;
    private String Temperature;
 
    // PartitionKey
    public String getPartitionKey() { return this.PartitionKey; }
    public void setPartitionKey(String key) { this.PartitionKey = key; }
 
    // RowKey
    public String getRowKey() { return this.RowKey; }
    public void setRowKey(String key) { this.RowKey = key; }
 
    // Temperature
    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:

Java
@Path("/temperature")
public class Temperature {    
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String Get() {        
 
        try {
            CloudTable cloudTable = GetCloudTable("SensorReadings");
 
            // Filter items by the timestamp
            String filter = GetTimeFilter();                
 
            TableQuery<SensorReading> tableQuery =
                TableQuery.from(SensorReading.class)
                .where(filter);                    
 
            Iterable<SensorReading> sensorReadings = cloudTable.execute(tableQuery);                
            
            // Return recent temperature
            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());
        }
    }
 
    // Other methods
}

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:

Java
private static CloudTable GetCloudTable(String tableName) { 
    // Connection string
    final String connectionString = 
        "DefaultEndpointsProtocol=https;" +
        "AccountName=<YOUR_ACCOUNT_NAME>;" +
        "AccountKey=<YOUR_KEY>" + 
        "TableEndpoint=<YOUR_ENDPOINT>;";
 
    try {
        // Get storage account
        CloudStorageAccount storageAccount = CloudStorageAccount.parse(connectionString);
 
        // Create the table client
        CloudTableClient tableClient = storageAccount.createCloudTableClient(); 
 
        // Get table reference
        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:

Java
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://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
    && 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/*.jar /deployments/
COPY --from=MAVEN_BUILD --chown=1001 target/quarkus-app/app/ /deployments/app/
COPY --from=MAVEN_BUILD --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/
 
EXPOSE 8080
USER 1001
 
ENTRYPOINT [ "/deployments/run-java.sh" ]

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://<YOUR_PUBLIC_IP>:8080/temperature

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here