Packaging Spring Boot Applications for Deployment on Kubernetes
by Paul Bourdel
Having recently completed a project using Spring Boot, which makes it easy to configure and package your web application, I wanted to see what it would take to run my application in a Kubernetes cluster. Spring Boot applications are already easy to run because the packaged JAR/WAR file comes with an embedded Tomcat server and is thus self executing. The only thing required on your server is Java. No need for an external application server. While a self executing JAR/WAR is huge improvement in deployability compared to the Java days of old, it still can’t match the versatility of a Docker container. This blog post will guide you through converting a Sprint Boot application to be packaged as a Docker container. Then we will show how to quickly create a basic Kubernetes cluster on AWS. Lastly we will show how to deploy the application on your Kubernetes cluster and have it connect to a database running in a separate container.
For this blog post I’m assuming a familiarity with Docker. If you are new to Docker I have included some links below to help get you acclimated. In short, Docker provides a standard packaging format for a container that then allows it to run directly on any environment that can execute Docker containers. Docker allows you to run multiple containers on a single server by leveraging the resource isolation features of the Linux kernel without incurring the overhead of virtual machines. This gives a good tradeoff between performance, application isolation and ease of deployment.
Kubernetes is a Docker container cluster management system written and open sourced by Google. While Kubernetes was written from scratch for Docker it is said to be similar to how Google deploys, schedules, and manages containers internally, mimicking Google’s Borg system.
Kubernetes extends Docker by providing an additional set of primitives that provide mechanisms for deploying, maintaining, and scaling applications. The official Kubernetes documentation is fairly good but let’s quickly go over the basic building blocks of the system.
- Pods are the smallest deployable unit that can be created, scheduled, and managed. It’s a logical collection of containers. When you deploy a Docker container you will do so as a pod, specifying the number of replicas you want to run as well as other configuration options.
Pods Documentation Link
- Minions are the actual servers performing the work of running the Docker containers.
- Replication Controllers are responsible for ensuring the correct number of pod replicas are running. A replication controller will create new pods if there are not enough or destroy some pods if there are too many.
Replication Controller Documentation Link
- Services provide a consistent way of accessing the functionality for a logical set of pods. As pods are created and destroyed throughout their life, Kubernetes Services ensure that the way those pods are accessed does not change. This allows for loose coupling between pods and their clients.
Services Documentation Link
- Secrets are intended to hold sensitive information such as passwords. This enables you to not have to store sensitive information directly in the pod configuration or the Docker image. Secrets are created and then made available only to the pods that reference them.
Secrets Documentation Link
Kubernetes Concepts Diagram
Spring Boot Overview
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”. It offers an opinionated view of the Spring platform and third-party libraries. Since there are a multitude of frameworks under the Spring umbrella it can become daunting configuring the ones you want to and making sure everything works together.
Spring Boot aims to make this easier by providing a parent project to start with that then let’s you configure which Spring projects(as well as other open source projects) you want to use. The configuration is done programmatically using annotations, removing the need for XML files in most instances. Spring Boot will also auto configure itself based on which Maven(or Gradle) dependencies are included in the project.
For our purposes we will assume you have a typical Spring Boot project that uses Maven to produce an executable JAR/WAR. More information on how to setup such a project can be found in the Spring Boot Quick Start.
Packaging a Spring Boot Application as a Docker Container with Maven
To package our Spring Boot application as a Docker container we will need two pieces. The first is the following Dockerfile which we will place in the “src/main/docker” folder:
This Docker file does a few things. Let’s go through it line by line:
- First line states that the Docker image we’re going to build will be an extension of the Java Version 8 Docker image. This means we will have Java 8 available to us.
- We then define a VOLUME located in the “/tmp” folder since that is where the embedded Tomcat by default will store its files.
- Add the self executing WAR compiled by Maven as “app.war” to our container.
- We run the touch command so the files have a modification time.
- The ENTRYPOINT line configures our container to run as an executable and specifies to run the WAR as an executable JAR. The driver command line parameter is necessary so the PostgreSQL driver is registered on startup.
The second piece we will be adding is a Maven plugin that will build our Docker image from the spec in the Dockerfile. To do this we will need to add the following in the <plugins> section of our pom.xml:
More information about the plugin can be found on the Docker Maven Plugin GitHub Page.command:
Note: if you are using Gradle you can use the Gradle Docker Plugin instead.
To give a quick summary, the plugin will look for our Dockerfile in the “src/main/docker” folder, look at the defined resources and then build a Docker image for us. We can execute this plugin by running the following command:
The command will do a Maven package, then the “docker:build” command is what triggers the plugin to build our container.
Our last step is to push the built Docker image to our repo by running the below command.
Now that we have a Docker image we’re in business and can move on to setting up an environment that can execute our image for us.
Creating a Kubernetes Cluster on AWS
Setting up a Kubernetes Cluster on AWS is fairly straightforward thanks to the configuration and management scripts that come with the platform. The easiest way to get up and running is to use the information in the Getting Started on AWS EC2 Guide.
The process I followed included:
- Installing the AWS command line interface configured to use my Amazon account: AWS CLI Project Home Page
- Installing Kubernetes scripts locally and initializing the cluster in AWS by running:
The script takes about ten minutes to run and at the end you will end up with a fully configured Kubernetes cluster under your AWS account. When the script is done it will output all the relevant endpoints you can use to monitor the cluster. The KubeUI endpoint you can navigate to in your browser for a graphical overview of your shiny new Kubernetes cluster.
Deploying Postgres Docker Container
Before we deploy our application container we need to deploy a container running the PostgreSQL database. This is because our application requires a database to run. This will also demonstrate how you would have containers talk to each other. To deploy a container running PostgreSQL to your cluster with the default settings run the following command:
We will then expose the database to your cluster. There are three service types:
- ClusterIP: use a cluster-internal IP only — this is the default and is discussed above. Choosing this value means that you want this service to be reachable only from inside of the cluster.
- NodePort: on top of having a cluster-internal IP, expose the service on a port on each node of the cluster (the same port on each node). You’ll be able to contact the service on any <NodeIP>:NodePort address.
- LoadBalancer: on top of having a cluster-internal IP and exposing service on a NodePort also, ask the cloud provider for a load balancer which forwards to the Service exposed as a :NodePort for each Node.
For our purpose we will use the ClusterIP since we only want the database to be reachable within the cluster. We can expose our PostgreSQL container to the cluster by running the following command:
We can then get the IP of the postgres service by running the “describe” command:
Deploying Your Spring Boot Application
Now that we have our database available and running in our cluster we can deploy our application container. To do so we will run the following command:
The commands tells Kubernetes to grab the latest Docker image of our application that was pushed to our repository and deploy two replicas of it with a couple environment variables.
- SPRING_PROFILES_ACTIVE variable tells Spring which profile to use when running the application.
- SPRING_DATASOURCE_URL variable sets the location of the database. This IP needs to match the ClusterIP of our PostgreSQL container.
As the application is deployed you can get the status of the pods by running:
You can also get the logs from the pods by running the following command:
Once the application is up and running we will want to expose it as a service. The difference this time compared to when we created the PostgreSQL service is we want our web application to be exposed to the internet. Because of this the service type will be LoadBalancer
This command will create an AWS load balancer for us. We can get the hostname of the load balancer by either going into our AWS console or by again running the describe command:
As we can see the LoadBalancer Ingress field shows the host name of the load balancer. The Endpoints fields shows to which nodes the LoadBalancer will redirect to. We can now view our application by navigating our browser to the host name:
Now our application is up and running!
Once our application is successfully running in our Kubernetes cluster we will want to be able to roll out new versions of it as they are released. To do so we can run the “rolling-update” command which will gradually spin up new pods running our new version while simultaneously destroying pods running the old version.
Very useful for rolling deployments with no down time. If I were to integrate with a continuous integration and deployment tool like Jenkins or Bamboo, I would have the deployment trigger execute this command. Some planning would have to go into making sure the correct version is deployed. The current command will always deploy the latest version in the Docker repository.
Appendix: Storing Database Passwords and Other Secrets
In our example we didn’t have a need for storing any sensitive data because we used the default PostgreSQL configuration which does not have a password set to access the database. In a real environment this would be considered a security risk.
For production environments our database would be locked down with at least a username and a password. The problem then becomes where to store the password so it is not freely viewable to anyone who has access to the application source code or the Docker image we built. This is where secrets come in use. Kubernetes allows us to store our database password as a secret and make it available only to the pods that require it. More information can be found on on the Kubernetes Secrets Documentation Page.
About the Author
Paul Bourdel is a consultant with Slalom’s Cross-Market delivery center team in Chicago. Paul focuses on building web applications using open source tools and frameworks. He is also an advocate of DevOps and spends time thinking of ways to apply modern environment automation tools and optimize the build/deployment pipelines for the projects he works on.