Featured image for automating JBoss Web Server deployments with Ansible.

In the article Automate Red Hat JBoss Web Server deployments with Ansible, we fully automated the setup of an Apache Tomcat server instance. In this follow-up, we'll further customize the behavior of the Java web server. We will also use this opportunity to replace the Apache Tomcat distribution we deployed previously with Red Hat JBoss Web Server.

Objectives

We want to achieve two goals:

  • Replace Apache Tomcat with JBoss Web Server.
  • Activate the Java web server's mod_cluster feature.

As in the previous installment, we want to automate absolutely everything with Ansible. This presents a slight challenge because we will have to retrieve the JBoss Web Server archive from the Red Hat Customer Portal—a protected resource.

As a reminder, our target environment is a Red Hat Enterprise Linux 8.4 instance (so you can easily reproduce the demonstration):

# cat /etc/redhat-release
Red Hat Enterprise Linux release 8.4 (Ootpa)
# ansible --version
ansible 2.9.22
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.6.8 (default, Mar 18 2021, 08:58:41) [GCC 8.4.1 20200928 (Red Hat 8.4.1-1)]

Note: The playbook might not work if you want to use a different Python version or target operating system.

Before we start, let's recap where the previous article left off by examining the complete playbook we ended up with:

---
- name: "JBoss Web Server installation and configuration"
  hosts: "all"
  become: yes
  collections:
    – middleware_automation.jws
  tasks :
  vars:
    tomcat_version: 9.0.50
    tomcat_base_url: https://archive.apache.org/dist/tomcat/tomcat-9/v
    tomcat_download_url: "{{ tomcat_base_url }}{{ tomcat_version }}/bin/apache-tomcat-{{tomcat_version}}.zip"
    tomcat_install_dir: /opt
    tomcat_zipfile: "{{tomcat_install_dir}}/tomcat.zip"
    tomcat_java_version: 1.8.0
    tomcat_setup: true
  collections:
    - middleware_automation.jws
  roles:
    - jws
  pre_tasks:
    - name: "Download latest Apache Tomcat Zipfile from {{ tomcat_download_url }}."
      get_url:
        url: "{{ tomcat_download_url }}"
        dest: "{{ tomcat_zipfile }}"
      when:
        - tomcat_download_url is defined
  tasks:
    - name: " Checks that server is running"
      uri:
        url: "http://localhost:8080/"
        status_code: 404
        return_content: no

    - name: "Deploy demo webapp"
      get_url:
        url: 'https://people.redhat.com/~rpelisse/info-1.0.war'
        dest: "{{ tomcat_home }}/webapps/info.war"
      notify:
        - Restart Tomcat service

  post_tasks:
    - name: "Sleep for {{ tomcat_sleep }} seconds to let Tomcat starts "
      wait_for:
        timeout: "{{ tomcat_sleep }}"

    - name: "Test application"
      get_url:
        url: "http://localhost:8080/info/"
        dest: /tmp/info.txt

An important reminder: The installation of Apache Tomcat is not described in this playbook, as this is entirely managed by the Ansible collection middleware_automation.jws. We only need to provide the path to the Apache Tomcat ZIP file, and logic within the JBoss Web Server collection of Ansible will execute the installation from there.

Install JBoss Web Server with Ansible

The first change we need to make is to remove the Apache Tomcat distribution details, which means deleting (or commenting out) the following variables: tomcat_version, tomcat_base_url, and tomcat_download_url.

With that complete, we now have to define new variables that will provide the required values for the playbook to download and install JBoss Web Server:

  vars:
    …
    tomcat_install_method: rhn_zipfiles
    tomcat_zipfile: "{{ tomcat_install_dir }}/jws.zip"
    tomcat_home: "{{ tomcat_install_dir }}/jws-5.4/tomcat"

We also can remove the entire pre_tasks section, as it was dedicated to the download of the Apache Tomcat archive that is no longer needed:

  pre_tasks:
    - name: "Download latest Apache Tomcat Zipfile from {{ tomcat_download_url }}."
      get_url:
        url: "{{ tomcat_download_url }}"
        dest: "{{ tomcat_zipfile }}"
      when:
        - tomcat_download_url is defined

This new configuration will alter (at runtime) the behavior of the Ansible collection middleware_automation.jws. As you may recall from the previous article, this collection has a dependency on another collection, middleware_automation.redhat_csp_download, which—as the name suggests—downloads the required artifacts from the Red Hat Customer Portal. This dependency was not used in the previous article, but the changes to the variables will now activate it.

However, for this portion of the JBoss Web Server collection to run properly, we need to modify the playbook to reference the role name explicitly:

collections:
  - middleware_automation.redhat_csp_download
roles:
   - redhat_csp_download
…

To download the appropriate archive from the Red Hat customer portal, Ansible will need to authenticate, so we have to provide the proper credentials. We want to avoid specifying the connection details inside the main playbook, like other variables. So, we will use a separate file, called credentials.yml, to specify them:

---
rhn_username: username@redhat.com
rhn_password: '<password>'

Then, we just need to have Ansible load those variables at execution time by specifying the file location as an additional variable:

$ ansible-playbook -i inventory -e @credentials.yml

Now that everything is in place, the next time Ansible runs, the execution will connect to the Red Hat customer portal, download the archive, and install JBoss Web Server.

Activate mod_cluster

One of the nice added values of the Ansible collection middleware_automation.jws is the support for setting up mod_cluster, a rather advanced functionality of the Java web server. By specifying a few extra variables, Ansible can configure and activate this feature.

For those not familiar with mod_cluster, let us briefly summarize the capability of this tool. mod_cluster is an intelligent load balancer that uses a communication channel to forward requests from the load balancer to one of a set of application server nodes. The beauty of this protocol is that most of the setup is fully dynamic. We just need to provide some basic information to JBoss Web Server for the server to connect to the httpd instance and register its services (deployed applications, address and port, etc.).

How can we achieve this? By adding just three variables to the playbook: The listening port for modcluster (6666), the IP address of the httpd server to connect to, and the listening port for the modcluster service on the JBoss Web Server side. See the following code:

vars :
...
    override_tomcat_modcluster_enable: yes
    override_tomcat_modcluster_ip: httpd.example.com
    override_tomcat_modcluster_port: 6666
    override_tomcat_modcluster_listen_port: 6666

(Okay, we need a fourth variable to enable the feature within the collection, but that’s specific to the automation_middleware.jws collection, not mod_cluster.)

If we execute the playbook again, we can see that the server.xml file is updated, which triggers a restart of the JBoss Web Server systemd service. This ensures the new settings are taken into account and activates the mod_cluster functionality:

...
TASK [jws : Deploy custom configuration file from {{ item.template }}] ***************************************************************************************
Friday 27 August 2021  05:53:26 -0400 (0:00:02.095)       0:00:38.688 *********
changed: [localhost] => (item={'template': 'templates/server.xml.j2', 'dest': '/opt/jws-5.4/tomcat/./conf/server.xml'})
ok: [localhost] => (item={'template': 'templates/web.xml.j2', 'dest': '/opt/jws-5.4/tomcat/./conf/web.xml'})
ok: [localhost] => (item={'template': 'templates/context.xml.j2', 'dest': '/opt/jws-5.4/tomcat/./conf/context.xml'})

TASK [jws : Remove app: {{ item }}] **************************************************************************************************************************
Friday 27 August 2021  05:53:28 -0400 (0:00:02.430)       0:00:41.118 *********
ok: [localhost] => (item=docs)
ok: [localhost] => (item=ROOT)
ok: [localhost] => (item=examples)

TASK [jws : Copy tomcat vault file from control node to remote] **********************************************************************************************
Friday 27 August 2021  05:53:29 -0400 (0:00:01.142)       0:00:42.261 *********
skipping: [localhost]

TASK [jws : Initialize tomcat vault] *************************************************************************************************************************
Friday 27 August 2021  05:53:30 -0400 (0:00:00.076)       0:00:42.337 *********
skipping: [localhost]

RUNNING HANDLER [jws : Restart Tomcat service] ***************************************************************************************************************
Friday 27 August 2021  05:53:30 -0400 (0:00:00.066)       0:00:42.404 *********
changed: [localhost]
...

Customize the server.xml

Quite often, the administrator of the JBoss Web Server (or Apache Tomcat) deployment will require fine-tuning of the server.xml file. Up until this point, we have simply relied on the one generated automatically by the Ansible collection middleware_automation.jws. Indeed, it comes with its own templates that already allow transparent usage of a few complex features of the server (like mod_cluster).

While convenient, this approach cannot support all the possible configurations and implementation of the Java web server. This is why the collection also allows users to override the definition of this template in order to provide one of their own.

However, there is a (small) catch. If you have already activated some features (like mod_cluster) with the help of the template supplied by the collection, those need to remain properly set up. It's easiest to use the server.xml file from the collection as a base for the new template:

$ cp ~/.ansible/collections/ansible_collections/middleware_automation/jws/role/jws/templates/server.xml.j2 templates/server.xml.j2

With the custom template file in place, all that remains is to specify the override_tomcat_conf_server variable so that the automate_middleware.jws collection uses the new template file instead of the default:

vars:
  …
  override_tomcat_conf_server: templates/server.xml.j2

That’s it! Just for the sake of completeness, the following is a bare-minimum template that you can use to build upon:

<Server port="{{ tomcat.shutdown.port }}" shutdown="SHUTDOWN">
  {% if tomcat.mod_cluster.enable  %}
  <Listener className="org.jboss.modcluster.container.catalina.standalone.ModClusterListener"
        connectorPort="{{ override_tomcat_modcluster_port }}"
        advertise="false"
        stickySession="true"
        stickySessionForce="false"
        stickySessionRemove="true"
        proxyList='{{ tomcat.mod_cluster.ip }}:{{ tomcat.mod_cluster.port }}'/>
  {% endif %}
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>
  <Service name="Catalina">
    <Connector executor="tomcatThreadPool"
               port="{{ tomcat.listen.http.port }}"
               protocol="HTTP/1.1"
               allowTrace="false"
{% if tomcat.listen.http.bind_address is defined %}               address="{{ tomcat.listen.http.bind_address }}"
{% endif %}
               connectionTimeout="20000"
               xpoweredBy="false"
               server="My Server"
               clientAuth="true"
               maxHttpHeaderSize="8192"
               redirectPort="{{ tomcat.listen.https.port }}"/>
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
      
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

Conclusion

As a brief recap, this article demonstrated how to:

  • Use JBoss Web Server instead of the Apache Tomcat distribution.
  • Activate its mod_cluster features and customizing the server.xml.
  • Fully automate deployment and configuration using Ansible.

It's noteworthy that no extra Ansible tasks have been added to the playbook. All we had to do was modify a few configuration variables—the middleware_automation.jws collection did all of the automation heavy lifting. Pretty neat, isn’t it?

Thanks to this JBoss Web Server Ansible collection, managing JBoss Web Server (or Apache Tomcat) within an Ansible playbook is as easy and lightweight as it is with other resources, such as Nginx or firewalld. In short, the Java web server is now a first-class citizen inside the Ansible ecosystem.

Last updated: March 13, 2023