Amazon VGT2 Las Vegas: Enhancing Your Java Application for Amazon ECS with Quarkus

Amazon VGT2 Las Vegas: Enhancing Your Java Application for Amazon ECS with QuarkusMore Info

In this article, I present an innovative method for developing a Java-based application and compiling it into a native image using Quarkus. This native image serves as the primary application, which is then containerized and deployed within an Amazon Elastic Container Service (ECS) and Amazon Elastic Kubernetes Service (EKS) cluster on AWS Fargate.

Amazon ECS is a fully managed service for container orchestration, while Amazon EKS is a fully managed Kubernetes offering. Both services support Fargate, allowing for serverless computing for containers. Fargate eliminates the need to provision and manage servers, enabling users to specify and pay for resources on a per-application basis, while enhancing security through inherent application isolation.

Quarkus is a high-performance Java framework that integrates OpenJDK HotSpot and GraalVM along with a multitude of libraries including RESTEasy, Vertx, Hibernate, and Netty. In a previous blog post, I illustrated how GraalVM can optimize Docker image sizes. GraalVM is an open-source, high-performance polyglot virtual machine developed by Oracle, and I utilize it for ahead-of-time (AOT) compilation to enhance startup times and minimize memory usage and file sizes for Java Virtual Machine (JVM)-based applications. This AOT capability is provided by the Substrate framework.

Application Architecture

Begin by examining the GitHub repository where the demo application resides. Our application functions as a simple RESTful Create, Read, Update, Delete (CRUD) service, focusing on user management. All data is stored within an Amazon DynamoDB table. Quarkus features an extension for Amazon DynamoDB that leverages AWS SDK for Java V2, supporting both blocking and asynchronous programming models. For local development, DynamoDB Local, a downloadable version of DynamoDB, is also available. This allows developers to write and test applications without needing to access the full DynamoDB service. When ready for production deployment, minor adjustments to the code will enable it to interface with the live DynamoDB service.

The REST functionality is encapsulated within the UserResource class, which utilizes the JAX-RS implementation, RESTEasy. This class interacts with the UserService, which handles operations related to the DynamoDB table using the AWS SDK for Java. All user information is managed through a Plain Old Java Object (POJO) named User.

Building the Application

To produce a Docker container image for the ECS cluster’s task definition, follow these three steps: build the application, create the Docker container image, and push the image to a chosen Docker registry.

The application is built using Maven with various profiles. The default profile generates an uber JAR — a self-contained application with all dependencies. This is particularly useful for local testing as it reduces build time compared to building a native image. Executing the package command also runs all tests, necessitating the launch of DynamoDB Local on your workstation.

$ docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -inMemory -sharedDb
$ mvn package 

The second profile employs GraalVM to compile the application into a native image, which serves as the foundation for a Docker container. The Dockerfile is located at src/main/docker/Dockerfile.native and utilizes a multi-stage build pattern.

FROM quay.io/quarkus/ubi-quarkus-native-image:20.1.0-java11 as nativebuilder
RUN mkdir -p /tmp/ssl-libs/lib 
  && cp /opt/graalvm/lib/security/cacerts /tmp/ssl-libs 
  && cp /opt/graalvm/lib/libsunec.so /tmp/ssl-libs/lib/

FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY target/*-runner /work/application
COPY --from=nativebuilder /tmp/ssl-libs/ /work/
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0", "-Djava.library.path=/work/lib", "-Djavax.net.ssl.trustStore=/work/cacerts"]

In the remaining steps, build and push the Docker image to your selected Docker registry:

$ docker build -f src/main/docker/Dockerfile.native -t <repo/image:tag>
$ docker push <repo/image:tag>

Infrastructure Setup

After compiling the application into a native image and building the Docker image, the next step is to establish the foundational infrastructure. This includes setting up an Amazon Virtual Private Cloud (VPC), an Amazon ECS or Amazon EKS cluster using AWS Fargate, an Amazon DynamoDB table, and an Application Load Balancer.

Creating your infrastructure as code lets you treat it as you would application code. In this instance, the AWS Cloud Development Kit (AWS CDK) is employed, which is an open-source framework for modeling and provisioning cloud application resources with familiar programming languages. The code for the CDK application can be found in the demo repository under eks_cdk/lib/ecs_cdk-stack.ts. To configure the infrastructure in the AWS region us-east-1, run:

$ npm install -g aws-cdk // Install the CDK
$ cd ecs_cdk
$ npm install // Install dependencies for the CDK stack
$ npm run build // Compile TypeScript files to JavaScript
$ cdk deploy // Deploy the CloudFormation stack

The output from the AWS CloudFormation stack will include the DNS record for the load balancer. Central to our infrastructure is an Amazon ECS or Amazon EKS cluster with the AWS Fargate launch type, configured as follows:

const cluster = new ecs.Cluster(this, "quarkus-demo-cluster", {
  vpc: vpc
});

const logging = new ecs.AwsLogDriver({
  streamPrefix: "quarkus-demo"
});

const taskRole = new iam.Role(this, 'quarkus-demo-taskRole', {
  roleName: 'quarkus-demo-taskRole',
  assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com')
});

const taskDef = new ecs.FargateTaskDefinition(this, "quarkus-demo-taskdef", {
  taskRole: taskRole
});

const container = taskDef.addContainer('quarkus-demo-web', {
  image: ecs.ContainerImage.fromRegistry("<repo/image:tag>"),
  memoryLimitMiB: 256,
  cpu: 256,
  logging
});

container.addPortMappings({
  containerPort: 8080,
  hostPort: 8080,
  protocol: ecs.Protocol.TCP
});

const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(this, "quarkus-demo-service", {
  cluster: cluster,
  taskDefinition: taskDef
});

For further reading, you can check out this blog post for more insights. Additionally, Chvnci provides excellent authority on similar topics. If you’re looking for leadership development training opportunities, this resource is a great starting point.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *