Introduction
Apache Tomcat serves Java Servlets, JSPs, and WebSocket applications. Ansible automates the complete lifecycle — install JDK, deploy Tomcat, configure connectors, manage WAR files, tune the JVM, set up SSL, and perform zero-downtime deployments. This guide covers standalone through clustered production setups.
Install JDK and Tomcat
---
- name: Deploy Apache Tomcat
hosts: app_servers
become: true
vars:
tomcat_version: "10.1.28"
tomcat_major: "10"
java_package: java-17-openjdk
tomcat_user: tomcat
tomcat_home: /opt/tomcat
tomcat_port: 8080
tomcat_shutdown_port: 8005
tasks:
- name: Install Java JDK
ansible.builtin.package:
name: "{{ java_package }}"
state: present
- name: Create Tomcat user
ansible.builtin.user:
name: "{{ tomcat_user }}"
home: "{{ tomcat_home }}"
shell: /bin/false
system: true
- name: Download Tomcat
ansible.builtin.get_url:
url: "https://dlcdn.apache.org/tomcat/tomcat-{{ tomcat_major }}/v{{ tomcat_version }}/bin/apache-tomcat-{{ tomcat_version }}.tar.gz"
dest: /tmp/tomcat.tar.gz
checksum: "sha512:https://dlcdn.apache.org/tomcat/tomcat-{{ tomcat_major }}/v{{ tomcat_version }}/bin/apache-tomcat-{{ tomcat_version }}.tar.gz.sha512"
- name: Extract Tomcat
ansible.builtin.unarchive:
src: /tmp/tomcat.tar.gz
dest: "{{ tomcat_home }}"
remote_src: true
extra_opts: [--strip-components=1]
owner: "{{ tomcat_user }}"
group: "{{ tomcat_user }}"
creates: "{{ tomcat_home }}/bin/catalina.sh"
- name: Deploy server.xml
ansible.builtin.template:
src: server.xml.j2
dest: "{{ tomcat_home }}/conf/server.xml"
owner: "{{ tomcat_user }}"
mode: '0600'
notify: restart tomcat
- name: Deploy tomcat-users.xml
ansible.builtin.template:
src: tomcat-users.xml.j2
dest: "{{ tomcat_home }}/conf/tomcat-users.xml"
owner: "{{ tomcat_user }}"
mode: '0600'
notify: restart tomcat
- name: Create systemd service
ansible.builtin.template:
src: tomcat.service.j2
dest: /etc/systemd/system/tomcat.service
mode: '0644'
notify:
- daemon reload
- restart tomcat
- name: Start Tomcat
ansible.builtin.service:
name: tomcat
state: started
enabled: true
handlers:
- name: daemon reload
ansible.builtin.systemd:
daemon_reload: true
- name: restart tomcat
ansible.builtin.service:
name: tomcat
state: restarted
Systemd Service
# templates/tomcat.service.j2
[Unit]
Description=Apache Tomcat {{ tomcat_major }}
After=network.target
[Service]
Type=forking
User={{ tomcat_user }}
Group={{ tomcat_user }}
Environment="JAVA_HOME=/usr/lib/jvm/jre"
Environment="CATALINA_HOME={{ tomcat_home }}"
Environment="CATALINA_BASE={{ tomcat_home }}"
Environment="CATALINA_PID={{ tomcat_home }}/temp/tomcat.pid"
Environment="CATALINA_OPTS={{ tomcat_catalina_opts | default('-Xms512m -Xmx1024m -XX:+UseG1GC') }}"
ExecStart={{ tomcat_home }}/bin/startup.sh
ExecStop={{ tomcat_home }}/bin/shutdown.sh
RestartSec=10
Restart=always
[Install]
WantedBy=multi-user.target
Deploy WAR Files
- name: Deploy application WAR
ansible.builtin.copy:
src: "{{ war_file }}"
dest: "{{ tomcat_home }}/webapps/{{ app_name }}.war"
owner: "{{ tomcat_user }}"
mode: '0644'
notify: restart tomcat
# Or download from artifact repository
- name: Deploy WAR from Nexus
ansible.builtin.get_url:
url: "https://nexus.example.com/repository/releases/com/myorg/myapp/{{ app_version }}/myapp-{{ app_version }}.war"
dest: "{{ tomcat_home }}/webapps/myapp.war"
url_username: "{{ nexus_user }}"
url_password: "{{ vault_nexus_password }}"
owner: "{{ tomcat_user }}"
mode: '0644'
notify: restart tomcat
Zero-Downtime Deployment
---
- name: Zero-downtime WAR deployment
hosts: app_servers
become: true
serial: 1
tasks:
- name: Remove from load balancer
ansible.builtin.uri:
url: "http://{{ lb_host }}/api/backends/{{ inventory_hostname }}/disable"
method: POST
delegate_to: localhost
- name: Wait for connections to drain
ansible.builtin.pause:
seconds: 30
- name: Undeploy old version
ansible.builtin.file:
path: "{{ tomcat_home }}/webapps/{{ app_name }}.war"
state: absent
- name: Wait for undeploy
ansible.builtin.wait_for:
path: "{{ tomcat_home }}/webapps/{{ app_name }}"
state: absent
timeout: 60
- name: Deploy new version
ansible.builtin.copy:
src: "builds/{{ app_name }}-{{ app_version }}.war"
dest: "{{ tomcat_home }}/webapps/{{ app_name }}.war"
owner: "{{ tomcat_user }}"
- name: Wait for deployment
ansible.builtin.uri:
url: "http://localhost:{{ tomcat_port }}/{{ app_name }}/health"
status_code: 200
register: health
until: health.status == 200
retries: 30
delay: 5
- name: Re-enable in load balancer
ansible.builtin.uri:
url: "http://{{ lb_host }}/api/backends/{{ inventory_hostname }}/enable"
method: POST
delegate_to: localhost
JVM Tuning
tomcat_catalina_opts: >-
-Xms{{ tomcat_heap_min | default('1g') }}
-Xmx{{ tomcat_heap_max | default('2g') }}
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath={{ tomcat_home }}/logs/
-Djava.awt.headless=true
-Dfile.encoding=UTF-8
-Djava.security.egd=file:/dev/urandom
SSL/TLS Configuration
<!-- In server.xml template -->
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true"
scheme="https" secure="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="{{ tomcat_home }}/conf/keystore.p12"
certificateKeystorePassword="{{ vault_keystore_password }}"
type="RSA" />
</SSLHostConfig>
</Connector>
- name: Create Java keystore from PEM
ansible.builtin.command: >
keytool -importkeystore
-srckeystore /tmp/cert.p12
-srcstoretype PKCS12
-srcstorepass {{ vault_keystore_password }}
-destkeystore {{ tomcat_home }}/conf/keystore.p12
-deststoretype PKCS12
-deststorepass {{ vault_keystore_password }}
no_log: true
Manager App Access
<!-- templates/tomcat-users.xml.j2 -->
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users>
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="admin-gui"/>
<user username="{{ tomcat_admin_user }}"
password="{{ vault_tomcat_admin_password }}"
roles="manager-gui,manager-script,admin-gui"/>
</tomcat-users>
Health Monitoring
- name: Check Tomcat status
ansible.builtin.uri:
url: "http://localhost:{{ tomcat_port }}"
status_code: 200
register: tomcat_health
- name: Check deployed applications
ansible.builtin.uri:
url: "http://localhost:{{ tomcat_port }}/manager/text/list"
url_username: "{{ tomcat_admin_user }}"
url_password: "{{ vault_tomcat_admin_password }}"
force_basic_auth: true
register: app_list
- name: Show deployed apps
ansible.builtin.debug:
var: app_list.content
Troubleshooting
OutOfMemoryError
Increase heap in CATALINA_OPTS:
tomcat_heap_max: "4g"
Port Already in Use
- name: Check port availability
ansible.builtin.wait_for:
port: "{{ tomcat_port }}"
state: stopped
timeout: 5
ignore_errors: true
register: port_check
Related Articles
Conclusion
Ansible automates Tomcat from install to production — JDK setup, Tomcat extraction, server.xml templating, WAR deployment from build artifacts, JVM tuning, SSL configuration, and rolling zero-downtime updates with serial: 1. Use systemd services for process management and the Manager API for health monitoring. Every Tomcat configuration is a template; every deployment is idempotent.