Ansible Middleware : Automation of JBoss Web Server (JWS)

March 14, 2022

In a previous article, we discussed the importance of Ansible from a Middleware perspective and why making Middleware a first-class citizen in the Ansible ecosystem is crucial. In this post, we’ll explore the technical details of utilizing Ansible for automating a Middleware solution. 

We’ll focus on one of the most used middleware software: Apache Tomcat. Or rather, the Red Hat supported version of it known as JBoss Web Server (JWS).

Let’s start by defining what exactly JWS is, in case you are not familiar with this specific product. JWS combines a web server (Apache HTTPD), a Java servlet engine (Apache Tomcat), and load balancing components (mod_jk and mod_cluster) to help them scale. All of those elements are supported by Red Hat. In this article, we’ll focus only on the Java servlet engine (Apache Tomcat) as many of the challenges brought up by its automation are typical of other middleware software (such as JBoss EAP, JBoss Data Grid or RH SSO). 

What are we trying to achieve?

Our goal here is to automate the deployment of this Java servlet engine, using Ansible. This may seem like a simple endeavor. However it does entail a few tasks and operations to be performed to achieve a proper result. 

First, we need to configure the target’s operating system. This includes creating a user and associate group to run JWS as well as the integration into systemd, so that the newly spawned server can be managed by the host’s operating system. JWS also requires that dependencies of the servlet’s engine (mostly a Java Virtual Machine in the proper version) are installed.

Having this out of the way it’s time to retrieve the appropriate archive (zipfile) for the software itself. As this is a Red Hat product, it does mean authentication and accessing the Red Hat Customer portal (there are other ways to achieve this, especially utilizing RPM, but in this article we’ll obtain the archive directly from this website). At this point, the archive’s content can be decompressed and the software locally installed, ensuring all the files belong to the previously created user and group.

In the next step, let’s configure JWS itself, which includes specifying which interface it needs to be bound against, defining which ports it will listen to, and so on. We may also customize the Java server during this step, like enabling some features (ex: SSL) or disable some other ones (for instance, removing the demo webapps shipped with the archive).

At this point, one could think we are done and that the server is ready. However, because Apache Tomcat is an application server, we also want to automate the deployment of its workloads. Which means we need to ensure the webapps its hosting are appropriately deployed.

With all this preparation work finished, we should be able to start the systemd service we configured earlier and double-check that the server, and its webapps, are functioning properly and as expected. This includes that all webapps are indeed deployed, accessible and operational. Here again, Ansible will help us in this validation step.

If you are familiar with Ansible primitives and built-in modules, you know it’s quite a lot of work to automate all of these requirements. Fortunately, most of this work has been implemented and is ready for use inside the Ansible JWS Collection.

An important note here: the five steps we’ve laid out above for JWS actually apply to most, if not all, middleware software (at least, the ones provided and supported by Red Hat). Some steps would be easier or more challenging to automate, depending on which one, but in essence, all will have the same kinds of requirements.

One last thing before we jump into the technical bits: if you want to reproduce the automation we describe in this article, keep in mind that the target’s host is running RHEL 8 and uses Ansible 2.11 or above (with Python 3.9).

Installing the JWS Collection

Installing the Ansible Collection for JWS follows the usual process. However if the reader is not familiar with it, here is how to do use Ansible Galaxy to fetch it and set it up:

$ ansible-galaxy collection install middleware_automation.jws
Process install dependency map
Starting collection install process
Installing 'middleware_automation.jws:0.0.3' to '/home/rpelisse/.ansible/collections/ansible_collections/middleware_automation/jws'
Installing 'middleware_automation.redhat_csp_download:1.2.1' to '/home/rpelisse/.ansible/collections/ansible_collections/middleware_automation/redhat_csp_download'
Code language: JavaScript (javascript)

Note: Ansible Galaxy comes with dependency management, which means that it fetches any required dependencies for this collection. The collection we are installing has only one dependency on another collection: the Ansible Collection for Red Hat CSP Download. This content provides a new module for Ansible which allows fetching artifacts from the Red Hat Customer Portal. The collection for JWS leverages this feature to retrieve the archive from the website.

Writing the playbook

We can now start working on our playbook itself. We’ll begin by setting it up to use the collection we just installed and we’ll add content incrementally from there.

Testing the collection

Before incorporating any tasks to our playbook, we’ll add the dependency to the Ansible collection for JWS to confirm that the installation was successful:

---
- name: "JBoss Web Server installation and configuration"
  hosts: "all"
  become: yes
  collections:
	– middleware_automation.jws
  tasks :Code language: JavaScript (javascript)

This playbook will not perform any tasks on the target system. It is only designed to verify that the collection is indeed recognized by Ansible:

# ansible-playbook -i hosts playbook.yml

PLAY [JBoss Web Server installation and configuration] *************************************************************************

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

PLAY RECAP *****************************************************************************************************************************
localhost              	: ok=1	changed=0	unreachable=0	failed=0	skipped=0	rescued=0	ignored=0
Code language: PHP (php)

Retrieve archive from Red Hat Customer Portal

Now that the Ansible collection for JWS is properly installed and accessible from our playbook, let’s configure it to automatically download the archive from the Red Hat Customer Portal website. 

First, we need to add the role itself to the playbook:

collections:
  - middleware_automation.redhat_csp_download
roles:
   - redhat_csp_download
[...]Code language: CSS (css)

With this role now configured within our playbook, the next run will ensure that all the prerequisites for middleware_automation.redhat_csp_download to function are in place on the target system:

Second, we need to add a few variables to the playbook:

 vars:
        [...]
	<strong>tomcat_setup: true
	tomcat_install_method: rhn_zipfiles</strong>
	tomcat_zipfile: "{{ tomcat_install_dir }}/jws.zip"
	tomcat_home: "{{ tomcat_install_dir }}/jws-5.4/tomcat"
Code language: PHP (php)

The variable tomcat_setup: set to true indicates to the collection that the playbook delegates the installation of the Java server to it. The tomcat_install_method: defined to “rhn_zipfiles”

specifies that the collection needs to authenticate against the Red Hat Customer Portal and download the archive from the website. The other variables dictate where to keep the archive on the target system and outline the suitable home folder for the Java server. Be mindful that the value of tomcat_home: here matches the directory tree that will be created when the archive is decompressed.

To download the archive from the website, our playbook needs to authenticate using the appropriate credentials. As we don’t want to store such sensitive information within the playbook itself, we’ll employ a separate file (rhn-creds.yml) to define the required values

---
rhn_username: username@redhat.com
rhn_password: '<password>'Code language: CSS (css)

For Ansible to load those variables when we run the playbook, we just need to add them to the playbook execution:

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

Note: As those variables contain secrets, it is highly recommended to use Ansible Vault to protect its content. Using this feature of the automation tool, however, is out of the scope of this article.

Ensure Java environment is properly configured

JWS is based on Apache Tomcat which means it’s Java software that requires a Java runtime to be executed. So, our automation needs to ensure that this environment is readily available on the target system.

Here again, we just need to add variables to our playbook and the Ansible Collection for JWS will take care of everything. It will check that the appropriate JVM is available or install if it’s missing:

- name: "JBoss Web Server installation and configuration"
  hosts: "all"
  become: yes
  vars:
	[...]
	tomcat_java_version: 1.8.0
  collections:
	– middleware_automation.jws
[...]Code language: CSS (css)

Note that this feature is only available for system belonging to the RedHat family:

$ ansible -m setup localhost | grep family
    	"ansible_os_family": "RedHat",Code language: JavaScript (javascript)

If the target system is not part of the RedHat family, the installation of the JVM must be added to the pre_tasks section of the playbook.

Preparing the target system

As we mentioned at the beginning of this article, before decompressing the archive and starting the server there are a few configurations that need to be done on target’s system. One of them is to ensure that the necessary user and group have been created.

The Ansible collection for JWS comes with default values for both, but often, those would be required to be replaced:

---
- name: "JBoss Web Server installation and configuration"
  hosts: "all"
  become: yes
  vars:
	[...]
	tomcat_user: java_servlet_engine
	tomcat_group: java_web_server
	[...]
  collections:
	- middleware_automation.jws
  roles:
	- jws
  pre_tasks:
	[...]
  tasks:Code language: CSS (css)

Once we execute this playbook, on top of fetching the archive from the website, it ensures that the appropriate group and user exists, before decompressing the servlet’s engine files into the defined TOMCAT_HOME. And upon that, Ansible will guarantee the requested JVM is available on the target host.

It’s already pretty nice to have all of this plumbing work done for us, but we can go further. With just a little more configuration, Ansible Collection for JWS can set up a systemd service to run JWS.

Integration with systemd service

The Ansible collection for JWS comes with a playbook and default templates to help set up JWS as a systemd service. Therefore, all that it’s needed to automate this part of our deployment is again to add a variable to our playbook:

---
- name: "JBoss Web Server installation and configuration"
  hosts: "all"
  become: yes
  vars:
	[...]
	tomcat_service_name: tomcat
	[...]
  collections:
	- middleware_automation.jws
  roles:
[...]Code language: CSS (css)

Note that this feature is only available for target systems belonging to the RedHat family.

After a successful execution of the playbook, you can easily confirm that the Java server is running as a systemd service:

# systemctl status tomcat
● tomcat.service - JBoss Web Server Web Application Container
 	Loaded: loaded (/usr/lib/systemd/system/tomcat.service; enabled; vendor preset: disabled)
 	Active: active (running) since Thu 2021-07-22 16:52:08 UTC; 16h ago
   Main PID: 6234 (java)
  	Tasks: 37 (limit: 307)
 	Memory: 187.4M
    	CPU: 21.528s
 	CGroup: /system.slice/tomcat.service
└─6234 /usr/bin/java -Djava.util.logging.config.file=/opt/apache-tomcat-9.0.50/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /opt/apache-tomcat-9.0.50/bin/bootstrap.jar:/opt/apache-tomcat-9.0.50/bin/tomcat-juli.jar -Dcatalina.base=/opt/apache-tomcat-9.0.50 -Dcatalina.home=/opt/apache-tomcat-9.0.50 -Djava.io.tmpdir=/opt/apache-tomcat-9.0.50/temp org.apache.catalina.startup.Bootstrap start

Jul 22 16:52:08 4414af93c931 systemd[1]: Started Apache Tomcat Web Application Container.
Jul 22 16:52:08 4414af93c931 systemd-service.sh[6220]: Tomcat started.
Jul 22 16:52:08 4414af93c931 systemd-service.sh[6219]: Tomcat runs with PID: 6234Code language: PHP (php)

Deploy a web application

If we come back to our list of requirements we established at the beginning of this article, we can see that the Ansible collection for JWS already took care of most of them. There is only one part of the deployment left to automate: configuring server workloads.

With a different middleware solution than JWS, this part can be complex. Fortunately for us, one of the qualities of the Java server is its simplicity. Deploying webapps only necessitates placing their package file (. war) in the appropriate directory prior to the server start. That’s all!

This can be easily achieved using Ansible primitives, but the collection still helps our automation by supplying a handler to restart the server is a webapp is provisioned for the first time (or updated):

- name: "Deploy demo webapp"
      ansible.builtin.get_url:
        url: 'https://people.redhat.com/~rpelisse/info-1.0.war'
        dest: "{{ tomcat_home }}/webapps/info.war"
      notify:
        - Restart Tomcat serviceCode language: JavaScript (javascript)

Validation

Our Java server is now deployed and running, its workloads also. All that remains to be implemented is the validation part. Firing up a systemd service is good, checking that it is working is better! 

To achieve this, we’ll add a couple tasks to the post_tasks: section of our playbook:

  • Check that the systemd service is indeed running;
  • A wait: task to ensure the Java server HTTP port is accessible (no need to go further if this fails);
  • A get_uri: tasks to get the root path (/) of the server followed by another to check the webapp availability (/info)—availability of the first one not confirming the one of the second one.
tasks:
[...]
post_tasks:
   - name: "Populate service facts"
  	ansible.builtin.service_facts:

   - name: "Check if service is running"
  	ansible.builtin.assert:
    	that:
      	- ansible_facts is defined
      	- ansible_facts.services is defined
      	- ansible_facts.services['tomcat.service'] is defined
      	- ansible_facts.services['tomcat.service']['state'] is defined
      	- ansible_facts.services['tomcat.service']['state'] == 'running'
    - name: "Sleep for {{ tomcat_sleep }} seconds to let Tomcat starts "
  	ansible.builtin.wait_for:
    	timeout: "{{ tomcat_sleep }}"

    - name: " Checks that server is running"
      ansible.builtin.uri:
        url: "http://localhost:8080{{ item.path }}"
        status_code: {{ item.status }}
        return_content: no
     loop:
       { path: '/', status: 404 }
       { path: '/info’, status: 200 }
	- name: "Test application"

Code language: PHP (php)

Conclusion

Keeping the content of the previous article in mind, this demonstration hopefully shows how much the Ansible collection for JWS eases the automation around the Java server. Thanks to it, using Ansible, we fully automated its installation. It has been almost as simple and natural to write this playbook as it would have been for an instance of Nginx. With all this content available, we truly have made JWS a first-class citizen in the Ansible ecosystem.

Note: The playbook used for this article can be found in the Github repository of the Ansible Collection for JWS.