Dockerize your Java Spring App, Deploy it on AWS
Java Migrations: The tale of eeny, meeny, miny, moe
Last year, I wrote a blogpost about alternatives for migrating a Java Spring app onto the cloud using different kinds of AWS services. There was a new service called AWS App Runner which seemed to be the simplest way of deploying a containerized Java Spring app, but back in the day it wasn’t capable of connecting to a private RDS instance so I only mentioned it briefly. Around February that issue went away thanks to a new feature called “VPC Connector”, so I can finally conclude my research by trying it out!
I Love My Local Farmer is a fictional company inspired by customer interactions with AWS Solutions Architects. Any stories told in this blog are not related to a specific customer. Similarities with any real companies, people, or situations are purely coincidental. Stories in this blog represent the views of the authors and are not endorsed by AWS.
One thing that stands out is that App Runner runs on top of ECS Fargate (a container offering), our chosen winner at that time. It provides a layer of abstraction of top of Fargate, which lets you just pick an image to run and it does all the autoscaling, load balancing, and SSL configuration for you. It reminds me a little of the simplicity of Elastic Beanstalk and can be set up fairly quickly.
Another really cool feature is that you can actually connect it to a GitHub project and it will automatically build & deploy when it detects changes (watch out for those build fees though!!). This sounds great for Java applications that you can execute directly on the command line on the chosen Java runtime. We could go this route by embedding our Tomcat server in our Maven pom.xml file and providing instructions in an apprunner.yaml file to build and execute the app.
The other option for using App Runner is to upload a container image, which will have everything that’s needed already built, packaged, and ready to go. This option is also a good one for us, since we’re porting a Java app that needs to run within the environment of a Tomcat application server. In the end, we felt more comfortable with this image deployment approach, since we can leverage the benefits of using images such as being able to rollback easily to a previous one, and standardizing the environment where the app runs.
For more info, check out App Runner’s complete list of features.
So how do we get the Java App AND Tomcat server deployed onto this service? The answer is by creating a Docker image. To be honest, I’m not a container fan, so the whole concept makes me cringe a little. I’d much rather port onto AWS Lambda, but this is a Spring app and so it would require a bit more work and time than we have right now. And so, we will follow the path of least resistance on this.
In this example, we will talk about our “provman” application, so if you’re following the instructions, feel free to replace “provman” with the name of your project on the commands.
Let’s Docker it up!
I don’t usually give walkthroughs in these blog posts… but we’re talking Docker with the Java community. I feel most of us cringe as a group when containers are mentioned, so let me remove the multi-hour nebulous cloud of uncertainty and guesswork I had to go through to get this to work and expand more on how to dockerize a Java Tomcat app.
First things first.. yes, install Docker! Next, shop around for a Docker image that has as much already installed as possible. For us, this means Tomcat and Java, so I went to DockerHub, searched for Tomcat and picked the “Docker official image” from the list. In the Tags tab, there’s a huge list of offerings.. but our app uses Java 11 and Tomcat 9 and I want to make sure our existing infra works in the container before even attempting any upgrades. I chose tomcat:9.0.64-jre11 and clicking that option displayed a list of comforting, “hello, old friend” settings/commands that make up the chosen image. Seeing the JAVA_HOME and CATALINA_HOME env vars set and the catalina.sh call just made me feel a little bit more at ease in this obscure world of container magic.
After building and packaging your Maven/Gradle project like you always do, create a “Dockerfile” file in your project’s root directory with these contents:
FROM tomcat:9.0.64-jre11 (the chosen DockerHub image)
ADD target/provman.war /usr/local/tomcat/webapps/
(The relative location of your WAR file)
EXPOSE 8080 (The port that the Docker container will listen to)
This configuration file basically tells Docker what to do. It will grab the Tomcat/JRE image, then copy our WAR file over to the Tomcat webapps folder so that it can be deployed, start up Tomcat and allow communication to the container via port 8080.
Now we can build a new image containing Java 11, Tomcat 9, and our WAR and then run it:
docker build -t provman . (don’t forget the dot!)
docker run -p 80:8080 provman
You should start seeing Tomcat logs and when it’s successful, you will find your app under
http://localhost:80. Note that port 80 may not be allowed on your computer, in that case just use another one (8080 for example).
Push it Real Good
Now that you verified your app runs within Docker, let’s publish the image in Amazon Elastic Container Registry (ECR) so that the App Runner service can use it.
Create a Repository using the ECR console and get credentials to push your local image to the new repo by executing (change the [REGION] and the [ACCOUNT ID] with your own values):
aws ecr get-login-password --region [REGION] | docker login --username AWS --password-stdin [ACCOUNT ID].dkr.ecr.[REGION].amazonaws.com
Then tag and push the image using its Image ID (you can get it from executing
docker tag [IMAGE ID] [ACCOUNT ID].dkr.ecr.[REGION].amazonaws.com/provman-privdocker push [ACCOUNT ID].dkr.ecr.[REGION].amazonaws.com/provman-priv
In the ECR console you will see your newly pushed image.
App Runner Configuration
All you need now is to create your new App Runner service. It’s easy to configure, so try to wing it in the AWS Console, otherwise follow the directions in “Create a service from an Amazon ECR image” section.
Do take note though: In the Networking section, choose Custom VPC and add a new VPC Connector using your RDS database’s VPC, Security Group and Subnets. This will ensure the App Runner service can talk to the non-public RDS database.
Deploying takes a few minutes and was a bit of a hassle when I was doing my initial round of testing connectivity. At first I connected an App Runner service to a publicly available RDS database, since trying to connect from the get-go to a private RDS instance in the past was like walking blindfolded through a maze. I basically wanted to make sure the entire setup worked and there were no connectivity issues.
Once that was working I felt courageous enough to try connecting to the private RDS database, where I ran into numerous AWS docs interpretation problems. I suspect that some of the changes I tried may have needed a redeploy of the App Runner service in order to take effect, but at 5 minutes per redeploy let’s say I got greedy and/or lazy and/or careless. In the end, I figured out that the answer was simple and I could’ve saved myself quite a bit of time.. just choose the same networking constructs as the database, and you’re good to go.
In any case, after deployment you can verify your app works by using your App Runner service’s “Default domain”. You also have the option to associate your own domain.
App Runner vs Fargate
Like I mentioned before, App Runner sits on top of Fargate and offers basically an easier way into the container world without the difficulties of learning container orchestration.
So why would you use anything else other than App Runner to run your containers? The answer depends on how much control you need or are used to having, and how much infrastructure you want to manage. If you’re already running containers and you are an expert in tuning them, App Runner will probably seem too simplistic to you. If you’re a total container novice, App Runner is probably the easiest and fastest way to get started.
If you’re somewhere in the middle, you should know that App Runner has a 1:1 relationship between an App Runner service and an image, i.e. you cannot have extra “sidecar” containers to handle things like observability by using AWS X-Ray or do log processing. Also, you do not have the possibility of mounting volumes nor adjusting how scaling happens. For people used to doing this it might seem limiting, and for people that just want to get their app ported it’ll seem freeing. It just depends on what perspective and requirements you’re coming from..
App Runner seems like an easy, good alternative for running a dockerized Java Spring app on AWS. The only real trouble is the extra hassle of creating that pesky Tomcat Docker image, which App2Container kind of did for us when we ported the app onto ECS Fargate (though it also has several more steps and only works on Windows and Linux). What I especially like about App Runner vs Fargate is that it automates even more things like autoscaling than Fargate does, which means (hopefully) less OPS work for me. Also an added plus, I never had to mess with Task definitions nor any of the ECS/EKS constructs nor manage load balancers and target groups.
In conclusion, this is a perfectly viable option for porting Java Spring apps onto the cloud, and avoiding the entire container orchestration services that many of us find daunting. The next step would’ve been to stand up the infrastructure using the Cloud Development Kit (CDK), which has a handy library already for propping up App Runner.
Since we’re already running this particular workload on Fargate, we’re not planning on spending more resources in moving everything at the moment. However, had this been a possibility back when we were deciding what to migrate to, we probably would’ve picked App Runner. Now it’s up to you to see what makes more sense for your own project!
How to Migrate a Spring App to the Cloud: Part II
Picking a Cloud Infrastructure — ECS & FARGATE
Architecting App Runner for Resiliency
Compare Concurrency App Runner vs Lambda vs Fargate