Introduction

This tutorial demonstrates how to build, test, deploy, and monitor a Java Spring
web application, hosted on Apache Tomcat, load-balanced by NGINX, monitored by
ELK, and all containerized with Docker.

The project is based on a sample Java Spring application, Spring Music,
available on GitHub from Cloud Foundry. The Spring Music application, a record
album collection, was originally designed to demonstrate the use of database
services on Cloud Foundry, using the Spring Framework.
Instead of Cloud Foundry, this post's Spring Music application is hosted within
Docker containers on a VirtualBox VM, and optionally on AWS.

The project's source code and all associated build files are stored on the
springmusic_v2 branch of the garystafford/spring-music
repository, also on GitHub. All files necessary to build the Docker portion of
the project are stored on the docker_v2 branch of the garystafford/spring-music-docker
repository on GitHub.

NGINX

To increase the application's performance, the application's static content,
including CSS, images, JavaScript, and HTML files, is hosted by NGINX. The
application's WAR file is hosted by Apache Tomcat. Requests for non-static
content are proxied through NGINX on the front-end, to a set of three
load-balanced Tomcat instances on the back-end. To further increase application
performance, NGINX is configured for browser caching of the static content. In
many enterprise environments, the use of a Java EE application server, such as
Tomcat, WebLogic, or JBoss, is still commonplace, as opposed to the use of
standalone Spring Boot applications.

The three Tomcat instances are manually configured for load-balancing using
NGINX's default round-robin algorithm. Load-balancing is configured through the
default.conf file, in the upstream configuration section:

Client requests are received through port 80 on the NGINX server. NGINX
redirects requests for non-static content, such as HTTP REST calls, to one of
the three Tomcat instances on port 8080.

MongoDB

The Spring Music application was designed to work with many types of data
stores. Options include MySQL, Postgres,
Oracle, MongoDB,
Redis, and H2, an
in-memory Java SQL database. This project uses MongoDB, the popular NoSQL
database, as a data store.

The Spring Music application performs basic CRUD operations against record album
data (documents), stored in the MongoDB database. The MongoDB database is
created and populated with album data, from a JSON file, when the Spring Music
application first starts.

ELK

The ELK Stack with Beats (Filebeat) aggregates Tomcat, NGINX, and Java Log4j
logging, providing debugging and analytics. The ELK Stack resides in a separate
Docker container. Filebeat is installed in the NGINX container and in each
Tomcat container. Filebeat pushes log entries to ELK.

Continuous Integration

The project produces two build artifacts, a WAR file for the Java application,
and a ZIP file for the static web content. Both artifacts are built
automatically by Semaphore, whenever source code is
pushed to the springmusic_v2 branch of the garystafford/spring-music
repository on GitHub.

Semaphore automatically pulls the source code from the project's GitHub
repository. Semaphore then executes a series of Gradle commands to build and
test the source code and build the artifacts. Finally, Semaphore executes a
shell script to deploy the artifacts back to GitHub.

The Gradle commands and shell script require environmentally-specific and
project-specific information. Semaphore projects may be configured with
environment variables.
Since environment variables often contain sensitive information, such as
passwords and GitHub access tokens, Semaphore provides the ability to encrypt
the variables. This project contains three variables.

Following a successful build and unit testing, Semaphore pushes the two build
artifacts, the WAR and the ZIP file, back to a separate build-artifacts branch
of the same garystafford/spring-music
repository on GitHub. The build-artifacts branch acts as a pseudo
binary repository for
the project, much like JFrog's Artifactory.
The build artifacts are later pulled, and used by Docker Compose to create
immutable Docker images.

Automation Code

Semaphore divides the project's Build Settings into three stages: 'Setup',
'Jobs', and 'After job'. Jobs can be run in parallel.

The project's Build Settings, execute a series of commands, which rely on the
gradle.build file and the deploy_semaphore.sh script.

Custom gradle.build Tasks are as follows:

// versioning build artifactsdefmajor='2'defminor=System.env.SEMAPHORE_BUILD_NUMBERminor=(minor!='null')?minor:'0'defartifact_version=major+'.'+minor// new Gradle build taskstaskwarNoStatic(type:War){// omit the version from the war file nameversion=''exclude'**/assets/**'manifest{attributes'Manifest-Version':'1.0','Created-By':currentJvm,'Gradle-Version':GradleVersion.current().getVersion(),'Implementation-Title':archivesBaseName+'.war','Implementation-Version':artifact_version,'Implementation-Vendor':'Gary A. Stafford'}}taskwarCopy(type:Copy){from'build/libs'into'build/distributions'include'**/*.war'}taskzipGetVersion(type:Task){ext.versionfile=newFile("${projectDir}/src/main/webapp/assets/buildinfo.properties")versionfile.text='build.version='+artifact_version}taskzipStatic(type:Zip){from'src/main/webapp/assets'appendix='static'version=''}

To clone the GitHub build project, construct the VirtualBox host VM, pull the
Docker images, and create the containers, run the project's build script, using
the following command: sh ./build_project.sh. The build script is useful when
working with CI/CD automation tools. However, we strongly recommend manually
running the individual commands first, to gain a better understand the build
process.

Deploying to AWS

By changing the Docker Machine driver to AWS EC2 from VirtualBox, and providing
AWS credentials, the springmusic environment can also be built on AWS.

Build Process

Docker Machine provisions a single VirtualBox springmusic VM on which it hosts
the project's containers. VirtualBox provides an easy solution that can be run
locally for initial development and testing of the application.

Next, two shared Docker volumes and a project-specific Docker bridge network are
created.

Following that, using the project's individual Dockerfiles, Docker Compose pulls
base Docker images from Docker Hub for NGINX, Tomcat, ELK, and MongoDB.
Project-specific immutable Docker images are then built for NGINX, Tomcat, and
MongoDB. While constructing the project-specific Docker images for NGINX and
Tomcat, the latest Spring Music build artifacts are pulled and installed into
the corresponding images.

Finally, Docker Compose builds and deploys the containers on to the VirtualBox
VM.

Docker Compose v2 YAML

This project takes advantage of improvements in Docker 1.12,
including Docker Compose's new v2 YAML format.
Improvements to the docker-compose.yml file include eliminating the need to
link containers and expose ports, and the addition of named networks and
volumes.

Testing the Application

Below are partial results of a simple curl test, hitting the NGINX server on
port 80. Note the three different IP addresses in the Upstream-Address field
between requests. This demonstrates NGINX's round-robin load-balancing is
working across the three Tomcat application instances, music_app_1, music_app_2,
and music_app_3.

Also, note the sharp decrease in the Request-Time between the first three
requests and subsequent three requests. The Upstream-Response-Time of the
Tomcat instances doesn't change, yet the total Request-Time is much shorter,
due to caching of the application's static assets by NGINX.

Spring Music Application Links

Assuming the springmusic VM is running at 192.168.99.100, the following
links can be used to access various project endpoints. Note each Tomcat instance
exposes a uniquely random port, such as 32771, which maps to port 8080
internally. These random ports are not required by NGINX, which routes requests
to Tomcat instances on port 8080. The unique port is only required for direct
access to Tomcat's Admin Web Console. Use the docker ps command to retrieve a
Tomcat instance's unique port.

Conclusion

In this tutorial, we went through the process of using Docker and Semaphore to
deploy a non-trivial Java web application to a production-like environment on
AWS. We hope you'll find it helpful. Feel free to share your comments,
questions, or tips in the comments below.