Date: March 4, 2025 /  Author: Ralf Eichinger

Deploy a Spring Boot Webapp to a VM with Ansible

In this Post we describe how to deploy a Spring Boot Webapp to a remote VM using Ansible. Make sure you already installed Ansible and tried your first playbook.

We will use Ansible directory layout as described in our post “How to structure Ansible directory layout?”

Create Ansible Playbook

For deploying a local JAR

To keep things simple at first we create a playbook for deploying a webapp from local storage.

We assume that you compiled and packaged your Spring Boot Webapp as JAR with mvn clean install if you’re using Maven or ./gradlew build if you’re using Gradle.

The packaged webapp’s JAR will be placed in ~/.m2/repository (Maven) or in the build/libs directory of your project (Gradle).

Our example uses Maven installed artifact under ~/.m2/repository.

Create a new file playbooks/deploy-my-webapp.yaml and add the following content:

- name: Deploy my Spring Boot Webapp
  hosts: managed_host
  become: true
  vars:
    app_name: spring-boot-app
    app_version: 1.0.0
    app_jar: "{{ app_name }}-{{ app_version }}.jar"
    app_user: springboot
    app_group: webapps
    app_server_port: 10000
    app_management_server_port: 10001
    deploy_dir: /opt/{{ app_name }}
    src_dir: ~/.m2/repository/<apps_groupId_path_see_pom.xml>/{{ app_name }}/{{ app_version }}

  tasks:
    - name: Install Java Runtime Environment
      apt:
        name: default-jre
        state: present

    - name: Create group "{{ app_group }}"
      group:
        name: "{{ app_group }}"
        state: present

    - name: Create user "{{ app_user }}"
      user:
        name: "{{ app_user }}"
        state: present

    - name: Add "{{ app_user }}" to "{{ app_group }}"
      user:
        name: "{{ app_user }}"
        groups: "{{ app_group }}"
        append: yes

    - name: Create deployment directory
      file:
        path: "{{ deploy_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_group }}"

    - name: Create directory containing logs (location same as configured in logback-spring.xml)
      file:
        path: "{{ deploy_dir }}/logs"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        mode: '0755'

    - name: Copy JAR file to managed host
      copy:
        src: "{{ src_dir }}/{{ app_jar }}"
        dest: "{{ deploy_dir }}/{{ app_jar }}"
        owner: "{{ app_user }}"
        group: "{{ app_group }}"

    - name: Create systemd service file
      template:
        src: templates/springboot.service.j2
        dest: /etc/systemd/system/{{ app_name }}.service
        owner: root
        group: root
        mode: '0644'

    - name: Reload systemd manager configuration
      ansible.builtin.command: systemctl daemon-reload
      when: not ansible_check_mode
      
    - name: Start and enable Spring Boot service
      systemd:
        name: "{{ app_name }}.service"
        state: started
        enabled: yes
      when: not ansible_check_mode

Create a Systemd Service Template

The playbook references the template templates/springboot.service.j2.

Create templates/springboot.service.j2 for configuring Linux systemd service:

[Unit]
Description={{ app_name }}
After=syslog.target

[Service]
User={{ app_user }}
Group={{ app_group }}
ExecStart=/usr/bin/java -jar {{ deploy_dir }}/{{ app_jar }} --spring.profiles.active={{ spring_profile }}  --server.port={{ app_server_port }} --management.server.port={{ app_management_server_port }}
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

Set spring_profile variable

The template references variables from the playbook vars section and additionally spring_profile variable we have not defined, yet.

As the spring.profiles.active property value varies from environment to environment (“DEV”, “STG”, “PROD”) we configure the var spring_profile in our production inventory (and the other inventories accordingly) for group “all”.

File inventories/production/group_vars/all.yml:

spring_profile: PROD

Check Ansible Playbook

Before running it against production (or preferable development environment) let’s do a “dry run” for just simulating what would happen using option --check:

$ ansible-playbook -i inventories/production/inventory.yml -l web1 playbooks/deploy-my-webapp.yaml --check
PLAY [Deploy Spring Boot Webapp] ***********************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************************************************************************************************
ok: [web1]

TASK [Install Java Runtime Environment] *****************************************************************************************************************************************************************************************************************
ok: [web1]

TASK [Create group "webapps"] ***************************************************************************************************************************************************************************************************************************
changed: [web1]

TASK [Create user "springboot"] *************************************************************************************************************************************************************************************************************************
changed: [web1]

TASK [Add "springboot" to "webapps"] ********************************************************************************************************************************************************************************************************************
changed: [web1]

TASK [Create user "springboot" with group "webapps"] ****************************************************************************************************************************************************************************************************
changed: [web1]

TASK [Create deployment directory] **********************************************************************************************************************************************************************************************************************
[WARNING]: failed to look up user springboot. Create user up to this point in real play
[WARNING]: failed to look up group webapps. Create group up to this point in real play
changed: [web1]

TASK [Create directory containing logs (location same as configured in logback-spring.xml)] *************************************************************************************************************************************************************
changed: [web1]

TASK [Copy JAR file to managed host] ********************************************************************************************************************************************************************************************************************
changed: [web1]

TASK [Create systemd service file] **********************************************************************************************************************************************************************************************************************
changed: [web1]

TASK [Reload systemd manager configuration] *************************************************************************************************************************************************************************************************************
changed: [web1]

TASK [Start and enable Spring Boot service] *************************************************************************************************************************************************************************************************************
skipping: [web1]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************************
web1                 : ok=8    changed=6    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

Run Ansible Playbook

Run the playbook with the following command to deploy the webapp (limited) to host web1 in production:

$ ansible-playbook -i inventories/production/inventory.yml -l web1 playbooks/deploy-my-webapp.yaml

Check Webservice

You now can login to the host web1 and check running webservice using wget:

$ wget localhost:10000
--2025-03-04 15:30:32--  http://localhost:10000/
Resolving localhost (localhost)... ::1, 127.0.0.1
Connecting to localhost (localhost)|::1|:10000... connected.
HTTP request sent, awaiting response... 200 
Length: 404 [text/html]
Saving to: ‘index.html’

index.html                                           100%[===================================================================================================================>]     404  --.-KB/s    in 0s      

2025-03-04 15:30:32 (79.0 MB/s) - ‘index.html’ saved [404/404]

Assuming you configured Spring Boot Actuator to reside under path /monitoring:

management:
  endpoints:
    web:
      base-path: '/monitoring'

You can get health status calling url http://localhost:10001/monitoring/health.

{"status":"UP"}

Your webservice is up and running!


References:

 Tags:  topics devops

Previous
⏪ Automated installation of Java with Ansible

Next
How to structure Ansible directory layout? ⏩