Spring Boot Systemd Installer
Problem Description
Every now and then I run into a weird or odd requirement that just needs to be worked on or that I find interesting enough to work on in spite of it not really being in the vein of best or common practices.
This project on my github is one of those
I had a basic web host that I needed to run a backend API on top of that was simple enough to not warrant having a separate host or docker host configured. This API was just to collect contact-us information into a mysql database from clients filling out a simple form.
I decided that I wanted to run this simple spring boot application as a systemd service for ease of management but I didn’t have a clean way to deploy the compiled API to the server and manage it in a mainstream linux-y way. What an annoying problem to have.
I reasoned that using a shell script would make deploying and updating the API on the server simpler and would fit the need for this to work on only one flavor / variety of linux.
Solution Goals
- I wanted the script to run on a Ubuntu based host where a jar file has already been successfully copied
- The script must deploy a spring boot binary to a Ubuntu based webhost
- Needs to be easy for the end user to utilize
UX Requirements
- The script must be dead simple to use with the following general user interface
./springboot-systemd-installer.sh \
my-service-name \
"My Service Desription" \
my-app-name.jar \
my_env_file \
syslog
-
This will be accomplished by mapping 5 positional arguments to global variables used by the script as follows
- $1 = Service Name – In the form of my-service-name – The name of the service to be used as a systemd service
- $2 = Service Description – In the form of “My service description” – The service description to be displayed in the description field output of a systemd unit listing
- $3 = Jar File Name – In the form of my-app-name-.jar – the name of the jar file containing the app
- $4 = Environment file name – In the form of my_env_file – the name of the environment file to be written to /etc containing mapped app properties to be read on startup
- syslog
- $5 = syslog opts – One of syslog or no syslog. If syslog then send logs to syslog otherwise if nosyslog only use systemd journal
Solution Runtime Requirements
-
Upon execution the script must perform the following actions:
- Map the end users positional arguments to global variables used by the script
- Confirm with the end user that we’re going to make the right changes
- Create a service account / user + group for the service to run as with no shell
- Copy and modify a unit file template to suit the parameters passed in to /lib/systemd/system/my-service-name.service
- Write the environment file to /etc so that your environment mapped app properties are read by the service on start up
- Modify the syslog configuration thus enabling imptcp on port 514 and generate the syslog config file for the deployed service
- Install the jar file at /opt/my-service-name/my-app-name.jar and change the permissions so that the service user can execute it
- Reload the systemd daemon
- Enable the new service
-
Start the service
- The successful execution of the script must empower the end user to manage the script as a service as follows:
Enable the service for use with
systemctl enable service-name.service
Start the service with
systemctl start service-name.service
Check the status of the service with
systemctl status service-name.service
Show the logs for the service with
journalctl -f -u - service-name.service
For more about the above commands see the primary commands’ respective man pages.
Writing the script / Putting it all together
The ultimate goal of this script is to produce a unit file and then get all the constituent pieces in the right place. The unit file is how systemd knows it has a service registered. (for our purposes…unit files can mean much more than that)
The unit file template will be called unit_file_template.service and will have the following form. I decided on double curly bracket tokens.
[Unit]
Description=
After=syslog.target
[Service]
User=
Group=
SuccessExitStatus=143
EnvironmentFile=/etc//
ExecStart=/opt//
#StandardOutput=syslog
#StandardError=syslog
#SyslogIdentifier=
[Install]
WantedBy=multi-user.target
The action part
For our script first we want to get the positional variables and then verify with the user that we understand their intentions:
if [[ -z "$1" && -z "$2" && -z "$3" && -z "$4" && -z "$5" ]]
then
echo -e "Not enough arguments supplied\nUsage:\n\t${0} service-name service-description jar-file-name environment-file-name syslog|nosyslog\n"
exit 1
fi
##
# Set up our environment variables
##
service_name="${1}"
service_description="${2}"
jar_file_name="${3}"
environment_file_name="${4}"
use_syslog="${5}"
##
# Confirm with the end user that we're going to make the right changes
##
echo "It looks like you entered the following arguments:"
echo -e " Service Name: ${service_name}"
echo -e " Service Description: ${service_description}"
echo -e " Jar File Name: ${jar_file_name}"
echo -e " Environment File: ${environment_file_name}"
echo -e " Use Syslog: ${use_syslog}"
read -n1 -p "Is this correct [Y/N]?" user_choice
echo ""
case $user_choice in
y|Y) echo "Installing..." ;;
n|N) echo "Please correct your errors and try again" && exit ;;
*) echo "Invalid selection" && exit ;;
esac
Next we check to see if the service user that we will run the service as exists:
# Create the user for the service if the user doesn't already exist
user_exists=$(id -u ${service_name})
if [[ user_exists -eq 0 ]]
then
useradd ${service_name} -s /sbin/nologin -M
fi
Here’s the magic part. We’ll use SED to substitute the tokens in our template with the users inputs:
echo "Writing unit file from template unit_file_template.service to /lib/systemd/system/${service_name}.service"
cp unit_file_template.service /lib/systemd/system/${service_name}.service
sed -i "s||${service_name}|g" /lib/systemd/system/${service_name}.service
sed -i "s||${service_description}|g" /lib/systemd/system/${service_name}.service
sed -i "s||${jar_file_name}|g" /lib/systemd/system/${service_name}.service
chown root:root /lib/systemd/system/${service_name}.service
chmod 755 /lib/systemd/system/${service_name}.service
…and publish the rendered template to /etc
##
# Write the environment file to /etc
##
echo "Writing environment file to /etc/${service_name}/${service_name}"
mkdir -p /etc/${service_name}
cp ${environment_file_name} /etc/${service_name}/${service_name}
Then write the syslog config for our new unit…
##
# Write the syslog config for the unit
##
if [ $use_syslog = "syslog" ]
then
echo "Syslog usage was elected...setting up syslog space in /var/log/${service_name}"
mkdir -p /var/log/${service_name}
chown syslog:${service_name} /var/log/${service_name}
chmod 755 /var/log/${service_name}
touch /var/log/${service_name}/${service_name}.log
chown syslog:${service_name} /var/log/${service_name}/${service_name}.log
chmod 755 /var/log/${service_name}/${service_name}.log
echo "Configuring syslog"
sed -i "s|#module(load=\"imtcp\")|module(load=\"imtcp\")|g" /etc/rsyslog.conf
sed -i "s|input(type=\"imtcp\" port=\"514\")|input(type=\"imtcp\ port\"514\")|g" /etc/rsyslog.conf
echo -e "if \$programname == '${service_name}' or \$syslogtag == '${service_name}' then /var/log/${service_name}/${service_name}.log & stop" > /etc/rsyslog.d/30-${service_name}.conf
echo "Reconfiguring systemd unit to use syslog"
sed -i "s|#StandardOutput=syslog|StandardOutput=syslog|g" /lib/systemd/system/${service_name}.service
sed -i "s|#StandardError=syslog|StandardError=syslog|g" /lib/systemd/system/${service_name}.service
sed -i "s|#SyslogIdentifier=${service_name}|SyslogIdentifier=${service_name}|g" /lib/systemd/system/${service_name}.service
echo "Reloading rsyslog"
systemctl restart rsyslog
fi
Installing the jar file is a snap:
##
# Install the jar file
##
echo "Copying the jar file ${jar_file_name} to /opt/${service_name}/${jar_file_name}"
mkdir -p /opt/${service_name}
cp ${jar_file_name} /opt/${service_name}/${jar_file_name}
chown -R /opt/${service_name}/
chmod g+x root:${service_name} /opt/${service_name}/${jar_file_name}
Reload the systemd daemon, enable the new unit and then restart the new unit.
echo "Reloading systemd daemon"
systemctl daemon-reload
echo "Enabling systemd unit ${service_name}.service"
systemctl enable ${service_name}.service
echo "Restarting systemd unit ${service_name}"
systemctl restart ${service_name}
For extra credit you could try adding verification that the unit is running properly at the end of your version of the script.
See the script at the repo home page for the whole context: