Skip to content

Task setup

Main task file

To keep things cleaner, I include chunks of the role in separate files. All must be referenced by the main.yml file within the role (ansible/roles/apache-tomcat/tasks/main.yml). They are called in order by main.yml.

roles/apache-tomcat/tasks/main.yml:

# tasks file for apache-tomcat
- include_tasks: setup-prerequisites.yml
# on RHEL7 - I have a playbook to download/compile OpenSSL as well but that's not needed on RHEL8.  RHEL7 had a really old version of OpenSSL.
- include_tasks: setup-apr.yml
- include_tasks: install.yml
- include_tasks: setup-tomcat-native.yml
- include_tasks: setup-commons-daemon.yml
- include_tasks: configure-tomcat.yml
- include_tasks: post-install.yml

Tip

If you haven't used ansible - you may want to look up idempotency and what it means. An operation is idempotent if the result of performing it once is exactly the same as the result of performing it repeatedly without any intervening actions. For example in these playbooks, there are tasks to create users, create directories, or apply a config template. If Ansible detects that these are already in the desired - it will just mosey right along (indicating "OK" when that task is being processed).

Setup Prerequisites

I've left some of my older stuff in here for example's sake (since again, I still have to maintain some RHEL 7 and JDK 8 systems). I've highlighted the revelant portions for this. The example isn't the cleanest, but you can see how you can alter packages based on different needs (in thise case, dnf is used for RHEL 8 and yum for RHEL 7.).

The Tomcat user and group are created as well at the bottom.

If you use this, the 'jdk_version" needs to be specified in your ansible hosts file, or some variable file. The ansible_distribution and ansible_distribution_major_version don't need to be specified as Ansible reads those from the hosts it connects to.

roles/apache-tomcat/tasks/setup-prerequisites.yml:

- name: Setup prerequisite dnf packages (RHEL 8 & JDK 11)
  ansible.builtin.dnf:
    name:
      - epel-release
      - java-11-openjdk
      - java-11-openjdk-devel
      - haveged
      - gcc
      - libtool
      - make
      - openssl-devel
    state: present
  when: ansible_distribution == 'RedHat' and ansible_distribution_major_version == '8' and jdk_version == 11

- name: Add tomcat group
  ansible.builtin.group:
    name: tomcat

- name: Add tomcat user
  ansible.builtin.user:
    name: tomcat
    group: tomcat
    home: /opt/tomcat
    shell: /sbin/nologin
    createhome: yes
    system: yes

- name: Make sure haveged is running and set to start on boot
  ansible.builtin.systemd:
    name: haveged
    state: started
    enabled: yes
    masked: no

Setup Apache Portable Runtime (APR)

The following section checks if the target version of APR is already installed. If not, it will download, unpack, compile, and install it.

roles/apache-tomcat/tasks/setup-apr.yml:

- name: Check if APR {{ apr_ver }} is already installed
  ansible.builtin.stat:
    path: /opt/apr/apr-{{ apr_ver }}/bin/apr-1-config
  register: apr_binary

- name: Create base APR directory if it doesn't exist
  ansible.builtin.file:
    path: /opt/apr
    state: directory
    owner: root
    group: root

- name: Create APR {{ apr_ver }} directory if it doesn't exist
  ansible.builtin.file:
    path: /opt/apr/apr-{{ apr_ver }}
    state: directory
    owner: root
    group: root   

- name: Check if APR {{ apr_ver }} is already downloaded
  ansible.builtin.stat:
    path: /root/apr-{{ apr_ver }}.tar.gz
  register: apr_tarball

# Only download APR if APR {{ apr_ver }} isn't already installed, and the tarball isn't downloaded
- name: Download APR
  ansible.builtin.get_url:
    url: "{{ apr_archive_url }}"
    dest: "{{ apr_archive_dest }}"
  when: apr_tarball.stat.exists == False and apr_binary.stat.exists == False

# Only unpack apr archive if the apr binary doesn't already exist
- name: Unpack APR archive
  ansible.builtin.unarchive:
    src: "{{ apr_archive_dest }}"
    dest: /root/
    owner: root
    group: root
    remote_src: yes
  when: apr_binary.stat.exists == False

# Only reconfigure if the APR directory for the version didn't already exist
- name: Configure APR source
  ansible.builtin.command: ./configure --prefix=/opt/apr/apr-{{ apr_ver }}
  args:
    chdir: "/root/apr-{{ apr_ver }}"
  when: apr_binary.stat.exists == False

# Only make if the APR directory for the version didn't already exist
- name: Compile/install APR
  ansible.builtin.shell: make && make install
  args:
    chdir: "/root/apr-{{ apr_ver }}"
  when: apr_binary.stat.exists == False

## Handle APR symlink creation or repointing
- name: Check if apr latest symlink exists
  ansible.builtin.stat:
    path: /opt/apr/latest
  register: apr_symlink

# Create symlink if none exists, or repoint it if it is pointing to an older version
- name: Create apr latest symlink to point to newly installed version
  ansible.builtin.file:
    src: "/opt/apr/apr-{{ apr_ver }}"
    dest: "/opt/apr/latest"
    owner: root
    group: root
    state: link
  when: apr_symlink.stat.exists == False or apr_symlink.stat.islnk

# Cleanup
- name: Remove apr source directory
  ansible.builtin.file:
    path: /root/apr-{{ apr_ver }}
    state: absent

- name: Remove apr source tarball
  ansible.builtin.file:
    path: /root/apr-{{ apr_ver }}.tar.gz
    state: absent

Install Apache Tomcat

This section checks if the target version Tomcat is already installed. If not - it will download Tomcat, unpack the archive, and ensure the various directory symlinks (conf, log, temp, webapps, work) are already setup.

If this is the first time Tomcat is being deployed on the target system (or the /etc/tomcat directory is otherwise empty), the contents of 'conf' from the downloaded tarball will be copied to /etc/tomcat.

It will also setup the systemd unit file so it can eventually be started and set to start on boot.

roles/apache-tomcat/tasks/install.yml:

- name: Check if Tomcat {{ tomcat_ver }} directory exists
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}
  register: tomcat_directory

- name: Check if Tomcat {{ tomcat_ver }} tarball exists
  ansible.builtin.stat:
    path: "{{ tomcat_archive_dest }}"
  register: tomcat_tarball

- name: Download Tomcat 8.5.x
  ansible.builtin.get_url:
    url: https://archive.apache.org/dist/tomcat/tomcat-8/v{{ tomcat_ver }}/bin/apache-tomcat-{{ tomcat_ver }}.tar.gz
    dest: "{{ tomcat_archive_dest }}"
  when: tomcat_tarball.stat.exists == False and tomcat_directory.stat.exists == False and tomcat_major_ver == 8.5

- name: Download Tomcat 9.0.x
  ansible.builtin.get_url:
    url: https://archive.apache.org/dist/tomcat/tomcat-9/v{{ tomcat_ver }}/bin/apache-tomcat-{{ tomcat_ver }}.tar.gz
    dest: "{{ tomcat_archive_dest }}"
  when: tomcat_tarball.stat.exists == False and tomcat_directory.stat.exists == False and tomcat_major_ver == 9.0


# Only unpack tomcat archive if the unpacked directory does not exist.
# This cannot be used to install the same version without deletion of the old one
- name: Unpack tomcat archive
  ansible.builtin.unarchive:
    src: "{{ tomcat_archive_dest }}"
    dest: /opt/tomcat
    owner: tomcat
    group: tomcat
    remote_src: yes
  when: tomcat_directory.stat.exists == False


# Create the permanent tomcat conf, log, temp, webapps, work directories for later symlinking
- name: Create Tomcat permanent conf directory
  ansible.builtin.file:
    path: /etc/tomcat
    state: directory
    owner: root
    group: tomcat
    mode: "u=rwx,g=rx"

- name: Create Tomcat permanent log directory
  ansible.builtin.file:
    path: /var/log/tomcat
    state: directory
    owner: tomcat
    group: tomcat
    mode: "u=rwx,g=rx"

- name: Create Tomcat permanent temp directory
  ansible.builtin.file:
    path: /var/cache/tomcat/temp
    state: directory        
    owner: tomcat
    group: tomcat
    mode: "u=rwx,g=rx"

- name: Create Tomcat permanent webapps directory
  ansible.builtin.file: 
    path: /var/lib/tomcat
    state: directory
    owner: tomcat
    group: tomcat
    mode: "u=rwx,g=rx"

- name: Create Tomcat permanent work directory
  ansible.builtin.file:
    path: /var/cache/tomcat/work
    state: directory
    owner: tomcat
    group: tomcat
    mode: "u=rwx,g=rx"


# If server.xml exists - then we can assume /etc/tomcat has been populated.
# - If so - don't change it
# - If not, assume it hasn't been populated and copy the new Tomcat's /conf to /etc/tomcat/
- name: Check if server.xml exists
  ansible.builtin.stat:
    path: /etc/tomcat/server.xml
  register: server_xml

- name: Copy files from conf directory
  ansible.builtin.copy:
    src: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/conf/
    dest: /etc/tomcat/
    remote_src: yes
    directory_mode: yes
  when: server_xml.stat.exists == False

# If ROOT exists - then we can assume /var/lib/tomcat has been populated.
# - If so - don't change it.
# - If not, assume it hasn't been populated and copy the new Tomcat's ROOT over
# - This needs to be updated to handle version updates of ROOT
- name: Check ROOT exists
  ansible.builtin.stat:
    path: /var/lib/tomcat/ROOT
  register: root_webapps

- name: Copy ROOT webapp directory
  ansible.builtin.copy:
    src: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/webapps/ROOT
    dest: /var/lib/tomcat/
    remote_src: yes
    directory_mode: yes
  when: root_webapps.stat.exists == False

## remove directories (conf, logs, temp, webapps, work) and recreate as symlinks
# conf
- name: check {{ tomcat_ver }} conf directory
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/conf
  register: tomcat_conf_dir

- name: Remove conf directory
  ansible.builtin.file:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/conf
    state: absent
  when: tomcat_conf_dir.stat.isdir

- name: Create conf symlink
  ansible.builtin.file:
    src: /etc/tomcat
    dest: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/conf
    owner: root
    group: tomcat
    state: link
  when: tomcat_conf_dir.stat.isdir or tomcat_conf_dir.stat.exists == False

# logs
- name: check {{ tomcat_ver }} logs directory
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/logs
  register: tomcat_logs_dir

- name: Remove logs directory
  ansible.builtin.file:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/logs
    state: absent
  when: tomcat_logs_dir.stat.isdir

- name: Create logs symlink
  ansible.builtin.file:
    src: /var/log/tomcat
    dest: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/logs
    owner: tomcat
    group: tomcat
    state: link
  when: tomcat_logs_dir.stat.isdir or tomcat_logs_dir.stat.exists == False

# temp
- name: check {{ tomcat_ver }} temp directory
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/temp
  register: tomcat_temp_dir

- name: Remove temp directory
  ansible.builtin.file:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/temp
    state: absent
  when: tomcat_temp_dir.stat.isdir

- name: Create temp symlink
  ansible.builtin.file:
    src: /var/cache/tomcat/temp
    dest: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/temp
    owner: tomcat
    group: tomcat
    state: link
  when: tomcat_temp_dir.stat.isdir or tomcat_temp_dir.stat.exists == False

# webapps
- name: check {{ tomcat_ver }} webapps directory
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/webapps
  register: tomcat_webapps_dir

- name: Remove webapps directory
  ansible.builtin.file:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/webapps    
    state: absent
  when: tomcat_webapps_dir.stat.isdir

- name: Create webapps symlink
  ansible.builtin.file:
    src: /var/lib/tomcat
    dest: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/webapps
    owner: root
    group: tomcat
    state: link
  when: tomcat_webapps_dir.stat.isdir or tomcat_webapps_dir.stat.exists == False

# work
- name: check {{ tomcat_ver }} work directory
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/work
  register: tomcat_work_dir

- name: Remove webapps directory
  ansible.builtin.file:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/work
    state: absent
  when: tomcat_work_dir.stat.isdir

- name: Create work symlink
  ansible.builtin.file:
    src: /var/cache/tomcat/work
    dest: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/work
    owner: tomcat
    group: tomcat
    state: link
  when: tomcat_work_dir.stat.isdir or tomcat_webapps_dir.stat.exists == False

# Setup systemd startup/shutdown script
- name: Setup systemd startup/shutdown script
  ansible.builtin.template:
    src: tomcat.service.j2
    dest: /etc/systemd/system/tomcat.service
    mode: 0644
    owner: root
    group: root
  register: tomcat_service

- name: Apply new SELinux file context to tomcat.service
  ansible.builtin.command: restorecon /etc/systemd/system/tomcat.service
  when: tomcat_service.changed

- name: Reload systemd daemons after service update
  ansible.builtin.command: systemctl daemon-reload
  when: tomcat_service.changed

Setup Tomcat Native Library

This is where the Tomcat Native Library (if not already installed) gets unpacked, configured, compiled, and installed.

roles/apache-tomcat/tasks/setup-tomcat-native.yml:

- name: Check if Tomcat Native Library {{ tomcat_native_ver }} is already installed
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/lib/libtcnative-1.so
  register: tomcat_native_library

# Untar tomcat native library
- name: Unpack tomcat native library archive
  ansible.builtin.unarchive:
    src: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/tomcat-native.tar.gz"
    dest: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/
    owner: tomcat
    group: tomcat
    remote_src: yes
  when: tomcat_native_library.stat.exists == False

# Only configure if the Tomcat Native Library directory for the version didn't already exist
- name: Configure Tomcat Native Library (RHEL8)
  ansible.builtin.command: "./configure --with-java-home={{ JAVA_HOME }} --with-apr=/opt/apr/latest/bin/apr-1-config --with-ssl=yes --prefix=/opt/tomcat/apache-tomcat-{{ tomcat_ver }}"
  args:
    chdir: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/tomcat-native-{{ tomcat_native_ver }}-src/native"
  when: tomcat_native_library.stat.exists == False and ansible_distribution == 'RedHat' and ansible_distribution_major_version == '8'

# Only make if the Tomcat Native Library directory for the version didn't already exist
- name: Compile/install Tomcat Native Library
  ansible.builtin.shell: make && make install
  args:
      chdir: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/tomcat-native-{{ tomcat_native_ver }}-src/native"
  when: tomcat_native_library.stat.exists == False

# Cleanup
- name: Remove tomcat native source directory
  ansible.builtin.file:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/tomcat-native-{{ tomcat_native_ver }}-src
    state: absent

Setup Apache Tomcat Commons Daemon

This is where the Tomcat Commons Daemon (if not already installed) gets unpacked, configured, compiled, and installed.

roles/apache-tomcat/tasks/setup-commons-daemon.yml:

- name: Check if Apache Tomcat Commons Daemon {{ commons_daemon_ver }} is already installed
  ansible.builtin.stat:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/jsvc
  register: commons_daemon_jsvc

# Untar tomcat native library
- name: Unpack Tomcat Commons Daemon native library
  ansible.builtin.unarchive:
    src: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/commons-daemon-native.tar.gz"
    dest: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/"
    owner: tomcat
    group: tomcat
    remote_src: yes
  when: commons_daemon_jsvc.stat.exists == False

# Only configure if the Tomcat Native Library directory for the version didn't already exist
- name: Configure Tomcat Commons Daemon native library (JDK 11)
  ansible.builtin.command: "./configure --with-java={{ JAVA_HOME }}"
  args:
    chdir: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/commons-daemon-{{ commons_daemon_ver }}-native-src/unix"
  when: commons_daemon_jsvc.stat.exists == False and jdk_version == 11

# Only make if the Tomcat Native Library directory for the version didn't already exist
- name: Compile Tomcat Commons Daemon native library
  ansible.builtin.shell: make
  args:
    chdir: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/commons-daemon-{{ commons_daemon_ver }}-native-src/unix"
  when: commons_daemon_jsvc.stat.exists == False

- name: Move jsvc file
  ansible.builtin.copy:
    src: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/commons-daemon-{{ commons_daemon_ver }}-native-src/unix/jsvc"
    dest: "/opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/"
    remote_src: yes
    owner: root
    group: tomcat
    mode: 0755
  when: commons_daemon_jsvc.stat.exists == False


# Cleanup
- name: Remove commons-daemon source directory
  ansible.builtin.file:
    path: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}/bin/commons-daemon-{{ commons_daemon_ver }}-native-src
    state: absent

Configure Tomcat

This is where we take the template files that were created earlier for web.xml, server.xml, etc., and make sure they are copied over to the server. If any are updated, it will notify Tomcat to restart when done.

This is basically just configuring the various files in /etc/tomcat. To initially create these files - download copies of them from a default Tomcat install to your Ansible host's roles/apache-tomcat/templates directory and alter them as needed. I will have copies of these files uploaded as well.

roles/apache-tomcat/tasks/configure-tomcat.yml:

- name: Setup catalina.properties
  ansible.builtin.template:
    src: cas6-catalina.properties.j2
    dest: /etc/tomcat/catalina.properties
    mode: 0640
    owner: root
    group: tomcat
  when: ("login" in inventory_hostname)
  notify: restart tomcat

- name: Setup context.xml
  ansible.builtin.template:
    src: cas6-context.xml.j2
    dest: /etc/tomcat/context.xml
    mode: 0640
    owner: root
    group: tomcat
  when: ("login" in inventory_hostname)
  notify: restart tomcat

- name: Setup server.xml
  ansible.builtin.template:
    src: cas6-server.xml.j2
    dest: /etc/tomcat/server.xml
    mode: 0640
    owner: root
    group: tomcat
  when: ("login" in inventory_hostname)

- name: Setup web.xml
  ansible.builtin.template:
    src: cas6-web.xml.j2
    dest: /etc/tomcat/web.xml
    mode: 0640
    owner: root
    group: tomcat
  when: ("login" in inventory_hostname)
  notify: restart tomcat

Post installation tasks

If the installation fails (errors, not warnings) at any point - it won't get to here, so it shouldn't move the symlink and restart tomcat unless the install completed. It also won't 'notify' tomcat to restart if they didn't even have to create the symlink (or if the config files changed earlier)

roles/apache-tomcat/tasks/post-install.yml:

- name: Setup Apache Tomcat {{ tomcat_ver }} symlink
  ansible.builtin.file:
    src: /opt/tomcat/apache-tomcat-{{ tomcat_ver }}
    dest: /opt/tomcat/latest
    owner: root
    group: root
    state: link
  notify: restart tomcat

# Cleanup
- name: Remove tomcat tarball
  ansible.builtin.file:
    path: /root/apache-tomcat-{{ tomcat_ver }}.tar.gz
    state: absent