Deploying Java Spring Boot on AWS Fargate

Have you ever wondered how to deploy a Java containerized application on AWS? If so, this article will show you a way to do it. We are going to build a small Java Spring Boot application step by step. You will learn how to define resources, classes, controllers, dependencies, etc. You will also learn how to deploy it on AWS Fargate, which is a managed service that allows you to deploy containers without needing to spend any time on orchestration.

What Is Spring Boot? 

What exactly is Spring Boot? Spring Boot is an opinionated version of the Spring framework, which includes by default a set of packages that will give you a ready-to-run Spring application. Check these docs for getting started.

Developing a Spring Boot Application

The application (let’s call it “Booksapp”) that will be deployed on AWS Fargate is a small REST API with two endpoints:

Application endpoints

Booksapp endpoints

For simplicity, the data layer will be a static array of book resources, and it will be located in our main controller. Real applications will require that you connect to a database engine and use an extra layer of logic for data access. 

The final application tree will look like this:

Application tree

Booksapp files tree

Before getting into the code, you’ll need to make sure that you have the following requirements on your machine:

Let’s start by creating the Book.java file, in which you’ll define a resource class for your books.

mkdir -p mkdir -p src/main/java/books 
touch src/main/java/books/Book.java

The code for our resource will look like:

package books;

public class Book {
   private final long id;
   private final String name;
   private final String author;

   public Book(Long id, String name, String author) {
       this.id = id;
       this.name = name;
       this.author = author;
   }

   public long getId() {
       return this.id;
   }

   public String getName() {
       return this.name;
   }

   public String getAuthor() {
       return this.author;
   }
}

Your Book resource class will contain three properties: id, name, and author. This class only contains a few getter methods since it will be a read-only API.

Below you can see how the pom.xml is defined:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>org.springframework</groupId>
   <artifactId>book-service</artifactId>
   <version>0.1.0</version>

   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.0.5.RELEASE</version>
   </parent>

   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>com.jayway.jsonpath</groupId>
           <artifactId>json-path</artifactId>
       </dependency>
   </dependencies>

   <properties>
       <java.version>1.8</java.version>
   </properties>

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>
</project>

There are only two dependencies needed for this example: json-path, which will automatically parse the Book resource objects to JSON, and the Spring Boot framework itself. 

You can now code your controller (BooksController.java):

package books;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;

@RestController
public class BooksController {

   private static final Book[] books = {
       new Book(1L, "Nemesis", "Isaac Asimov"),
       new Book(2L, "Great Expectations", "Charles Dickens"),
       new Book(3L, "The Chronicles of Narnia", "C.S. Lewis")
   };

   @GetMapping("/books")
   public Book[] books() {
       return books;
   }

   @GetMapping("/books/{id}")
   public Book book(@PathVariable int id) {
       for (Book book : books) {
           if (book.getId() == id) {
               return book;
           }
            
       }
       throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Entity not found"
       );       
   }

}

The controller consists of two endpoints, both of which are mapped to handler functions via the GetMapping annotation. The first endpoint will return a list of books, and the second one will return a single book based on its ID. 

Now, you need to define your Java Application:

package books;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BooksApplication {
   public static void main(String[] args) {
       SpringApplication.run(BooksApplication.class, args);
   }
}

To run your app, you need to compile and package it (the latter includes the compilation):

mvn compile
mvn package

If no errors show up, you can run the application as:

java -jar target/book-service-0.1.0.jar

This will launch a tomcat instance that will listen for traffic on port 8080. Fantastic! Now you can try your endpoints using PostMan:

GET /books

GET /books

GET /books/1

GET /books/1 

GET /books/11

GET /books/11

When it comes to Spring Boot, it’s usually a good idea to use Spring Initializr for generating a bootstrapper application. This comes with more utilities plus a basic structure for testing. 

In this post, we have started from scratch on purpose so that you’ll be aware of every piece of code that will be deployed. The final code can be downloaded from github.

Packing a Spring Boot Application in a Container

Now that you have your application running locally, the next step will be to containerize it. To do this, you are going to use Docker. Your goal on this step will be to create a Dockerfile that allows you to create a Docker image and run it on a container.

Let’s take advantage of the fact that the application was built locally. So your first attempt would be to only run the application on a container and skip the building and packaging work. The Dockerfile should then look like this:

FROM openjdk:8-jdk-alpine
COPY target/book-service-0.1.0.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

This will allow you to build an image that contains the jar file and be able to run the application by defining an entry point with the java -jar command. If you’re wondering why an alpine image was used, please check Docker’s documentation on this topic.

For building your image, let’s execute (from the root folder):

docker build -t jorlugaqui/booksapp .

If everything went OK, you can now run the application inside a container:

docker run -p 8080:8080 --rm jorlugaqui/booksapp

Go ahead and check the endpoints again. They should work as expected. Notice how in the run command the port 8080 is mapped from the container to the host OS. This allows you to have the application running on the same port as on the host OS.

Our Dockerfile still needs some love since its present state doesn’t cover the build of the application. Let’s try to do it the right way by having a multi-stage build:

FROM openjdk:8-jdk-alpine as build

RUN apk add --update ca-certificates && rm -rf /var/cache/apk/* && \
 find /usr/share/ca-certificates/mozilla/ -name "*.crt" -exec keytool -import -trustcacerts \
 -keystore /usr/lib/jvm/java-1.8-openjdk/jre/lib/security/cacerts -storepass changeit -noprompt \
 -file {} -alias {} \; && \
 keytool -list -keystore /usr/lib/jvm/java-1.8-openjdk/jre/lib/security/cacerts --storepass changeit

ENV MAVEN_VERSION 3.5.4
ENV MAVEN_HOME /usr/lib/mvn
ENV PATH $MAVEN_HOME/bin:$PATH

RUN wget http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz && \
 tar -zxvf apache-maven-$MAVEN_VERSION-bin.tar.gz && \
 rm apache-maven-$MAVEN_VERSION-bin.tar.gz && \
 mv apache-maven-$MAVEN_VERSION /usr/lib/mvn

WORKDIR /workspace/app

COPY pom.xml .
COPY src src
RUN mvn install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

FROM openjdk:8-jdk-alpine
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","books.BooksApplication"]

What this Dockerfile is telling you is that you’re going to have two stages for building your final image. The first one will take care of the build itself:

  • Install Maven.
  • Copy the source code.
  • Build the application.
  • Extract the final jar, which is generated in the target folder.

The second stage will take advantage of the previous build stage, use it for copying extracted files, and then actually run the application. If you want to dig around further on Docker and Spring Boot applications, feel free to check the official reference.

Amazon AWS Fargate: What Is It?

AWS Fargate is an AWS managed service that is responsible for provisioning and orchestrating your containerized application. This means that you can deploy hundreds of containers without having to define any compute resources because the service will do it for you.

Deploying a Spring Boot Application on AWS Fargate

With the Docker image in place, you are now ready for deploying your Booksapp to AWS Fargate. The steps to follow are:

  • Push the image to AWS ECR.
  • Define a task in AWS ECS for defining a container.
  • Run the task on the default’s cluster.
  • Check if the application is working.

In order to push the image to AWS ECR, you need to first create a repository:

Using Docker for building and running the application

Using Docker for building and running the application

Once this is created, you will be provided with the push commands:

$(aws ecr get-login --no-include-email --region us-east-1)
docker build -t jorlugaqui/booksapp . (already performed in previous steps)
docker tag booksapp:latest xxx.dkr.ecr.us-east-1.amazonaws.com/booksapp:latest
docker push xxx.dkr.ecr.us-east-1.amazonaws.com/booksapp:latest

After pushing the image, you will have something like this:

Image pushed on AWS ECR

Image pushed on AWS ECR

With the image on AWS ECR, you can now define your deployment on AWS ECS/Fargate. Let’s create your task by going to “create a new task definition” option on AWS ECS and then select the type FARGATE:

AWS ECS/Fargate

AWS ECS/Fargate

AWS will ask for a task name, some hardware allocation (take your time with this step, as you will be charged per the size of the allocated resources), and the container definition, where the Docker image URI provided by AWS ECR will be set.

ECS Fargate task definition

ECS Fargate task definition

ECS Fargate hardware allocation

ECS Fargate hardware allocation

ECS Fargate container definition

ECS Fargate container definition

It’s also important to set the port that your container will expose, 8080 in your case. The next step is to actually run the task on a cluster. You can take advantage of the default cluster for this, and you will need to specify which task you want to run on this cluster as well as how many instances of the task you want to have.

Here, you can set the number of tasks to 1 since it’s a small application:

ECS Fargate cluster definition

ECS Fargate cluster definition

You can increase this number and set the service with a load balancer in front of handling the traffic. Take a look at these docs for further details.

ECS Fargate cluster definition

ECS Fargate cluster definition

Once you create the cluster and it’s up and running, you can go ahead to the task definition and obtain the public IP provided by ECS on the details page:

ECS Fargate cluster details

ECS Fargate cluster details

ECS Fargate task details

ECS Fargate task details

With that IP, you can hit your endpoints again (don’t forget to open the port 8080 on the security group assigned to your cluster):

GET /books/ from AWS

GET /books/ from AWS

Great job! Now you have your application running on AWS Fargate!

Monitoring Your Spring Boot Application

AWS Fargate is integrated with AWS CloudWatch, meaning that you can set alarms based on metrics defined by you as well as see the logs generated from every task/container. For logging, make sure that the log integration option is checked:

CloudWatch Logs

CloudWatch Logs

Once the task is running, logs will appear on the Log tag:

CloudWatch Logs

CloudWatch Logs

Take a look at this doc for more monitoring options. If you’re interested in a deeper level of monitoring and troubleshooting, check out Epsagon’s automated tracing for AWS Fargate.

Tracing ECS Fargate with Epsagon

Tracing ECS Fargate with Epsagon

Conclusion

In this post, we took you through how to code and deploy a small Spring Boot application on AWS Fargate. You started by coding a Book resource class and ended up using the default cluster of AWS ECS for deploying a service. You have now seen how AWS Fargate took the server provisioning and server orchestration out of the equation and how you spent more time coding the application than deploying it. 

This is the value that Fargate provides: allowing you to focus on code and business logic, not server configurations and provisioning.