This walkthrough assumes you have deployed an API built with Java/Spring to a Virtual Machine.
If you need a refresher check out the Todo Tasks Deployment Single EC2 walkthrough.
That walkthrough ended with us running the application by manually issuing a bash command: sudo java -jar todo-app.jar --DB_HOST=localhost --DB_PORT=5444 --DB_NAME=todo --DB_USER=todo_user --DB_PASS=todopass --APP_PORT=80
.
While this worked for us in this educational environment, it is not a reliable solution for running our application. In the case of the server crashing, the EC2 instance going down, or experiencing a power outage the application would need to be manually started with the command used above.
There is an elegant solution for this in Linux. We can configure and run this Java application as a Linux SystemD service.
SystemD is initialization software on most Linux distributions.
It gives us, the developers, a way of defining how an application can be run and managed by the Linux Operating System. When an application is ran, and managed by SystemD it is referred to as a SystemD service
.
As the developers we can define whatever services we want. It is essentially instructions attached to an executable or an application.
To define a new service we will create a unit
file that defines the executable and additional instructions.
As a best practice we will be storing all of our Unit files in one place on our linux machines: /etc/systemd/system/
.
As an additional best practice we will name the unit file after the service we are creating. In this case the service is our Todo App. So we will create a new file named: todo-app.service
.
Create the file and add the following code:
[Unit]
Description=Todo Tasks API
After=syslog.target
[Service]
Type=simple
User=ubuntu
EnvironmentFile=/etc/opt/todo-app/todo-app.config
ExecStart=/usr/bin/java -jar /home/ubuntu/todo-app.jar SuccessExitStatus=143
Restart=always
[Install]
WantedBy=multi-user.target
Let’s break down the configuration of this unit file:
# Unit Section
[Unit]
# Human Readable Description of the Service
Description=Todo Tasks API
# After=list of units that must be started before this unit works
After=syslog.target
# Service Section
[Service]
# simple is the default value of Type, it is commonly left off
Type=simple
# User= sets the user, login shell, and home directory associated with this unit
User=ubuntu
# EnvironmentFile= points to the environment variables needed by the executable in this unit file
EnvironmentFile=/home/ubuntu/todo-app.config
# ExecStart= the exact command, or application to be run when this unit starts
ExecStart=/usr/bin/java -jar /home/ubuntu/todo-app.jar SuccessExitStatus=143
# Restart= if the unit should restart after failing
Restart=always
# Install Section
[Install]
# WantedBy=multi-user.target is the command neccessary so this systemd unit can be enabled (which is what allows it to automatically start when the server boots up)
WantedBy=multi-user.target
We can now start our application with sudo systemctl start todo-app.service
!
Before we get to enabling and starting our new service we need to define the environment variables that are needed by our todo-app.service
.
As a best practice we will be storing the environment variables for this service in: /etc/opt/todo-app/todo-app.config
.
We will need to create the todo-app/
directory inside of the /etc/opt/
directory:
sudo mkdir /etc/opt/todo-app
We will then need to create and edit the environment variables with:
sudo vim /etc/opt/todo-app/todo-app.config
Within vim change into insert mode by hitting i
and paste in our environment variables:
DB_HOST=localhost
DB_PORT=5444
DB_NAME=todo
DB_USER=todo_user
DB_PASS=todopass
APP_PORT=8080
After pasting in these environment variables save and quit the file by hitting esc
in vim and then entering :wq
and hitting enter
.
This will be the environment variables that are passed to the ExecStart
directive in our Unit File. Note that these environment variables match up with the application.properties
file of our Todo Tasks API. We are at runtime injecting a series of environment variables that are unique to this application environment.
As we progress through multiple deployments we will change some of these in the environment file to reflect the location and port of our database.
Now that we have defined our service with the unit file, and have created it’s necessary config file we can enable and start our service.
From the terminal of the EC2 enable the service to be managed by Linux:
sudo systemctl enable todo-app.service
Enabling the service is what tells our EC2 instance to automatically start this service when the machine first boots up. This will ensure that if the server restarts for any reason it will automatically restart our API!
Although we enabled our service above, it will only start if the EC2 reboots. Instead of doing that, let’s simply start the service ourselves:
sudo systemctl start todo-app.service
And our service should attempt to start and is being managed by SystemD.
The first few times you are defining a new service you will undoubtedly make mistakes, or in some cases the application may encounter a bug and print out information to the Tomcat logs.
We can view these logs by accessing the journal of this service:
journalctl -fu todo-app.service
journalctl
options:
-u
: the unit file we are accessing-f
: print out the last 10 lines of the journal, update as new logs come inReading these logs are invaluable for determining why a service cannot start. Common issues with Java Spring APIs:
/etc/systemd/system/
/etc/opt/<app-name>/<app-name>/config
/opt/<app-name>/<app-name-artifacts>
We are already following the first two best practices. However, we need to make some changes in order to achieve the remaining three best practices.
We will need to create a new directory and move our JAR file into this new appropriate directory.
sudo mkdir /opt/todo-app
Now we need to move our JAR file into this location:
sudo mv /home/ubuntu/todo-app.jar /opt/todo-app
note
Now would be a good time to update our Service unit file, but we are about to make another change that will facilitate even more changes to the unit file, so let’s make those changes first and update our unit file in one pass.
One service should be owned by a non root user.
useradd -M todo
chown -R todo:todo /opt/todo-app /etc/opt/todo-app
We need to reflect the changes made to the user, and the new JAR location in our unit file:
[Unit]
Description=Todo Tasks API
After=syslog.target
[Service]
Type=simple
User=todo # this line changes to reflect the new todo user
EnvironmentFile=/etc/opt/todo-app/todo-app.config
ExecStart=/usr/bin/java -jar /opt/todo-app/todo-app.jar SuccessExitStatus=143 # this line changes to reflect the new location of the JAR file
Restart=always
[Install]
WantedBy=multi-user.target
Since our Service unit file has been changed we need to do a few steps to update it:
sudo systemctl stop todo-app.service
sudo systemctl disable todo-app.service
sudo systemctl daemon-reload
sudo systemctl enable todo-app.service
sudo systemctl start todo-app.service
Now that we have updated our Service unit file to follow the best practices check that the application started up correctly by viewing it’s journalctl
.
This walkthrough converted our Todo Tasks API into a SystemD service.
The service is now manageable by Linux through the use of systemctl
. In addition, the service is configured to start automatically on server startup, and to attempt to restart if the application crashes for any reason.
You may have noticed that we changed the port of this application to 8080
from what we used in the previous walkthrough using 80
. You may have a Security Group that is configured to allow traffic on port 80, but not port 8080.
You could update this yourself, or hang tight for the next walkthrough where we will see how to use NGINX as a Proxy to Spring Apache Tomcat between port 80, and our running application on port 8080.