Ansible Middleware : Automation of JBoss Web Server (JWS)
Tags: automation, tomcat, jws, ansible
Published: 2024-06-18 15:00:00

In a previous article, we discussed the [importance of Ansible from a Middleware perspective](https://open011prod.wpengine.com/2021/11/22/why-do-you-need-ansible-to-manage-your-middleware-runtimes/) 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)](https://developers.redhat.com/products/webserver/overview).

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](https://en.wikipedia.org/wiki/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.

In the next step, we'll **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. Because Apache Tomcat is an application server, however 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 four 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 9 and uses Ansible 2.15.9 or above (with Python 3.9.18)._

## Installing the JWS Collection

Installing the collection dedicated to Red Hat JWS uses the ansible-galaxy command like any other collection. As it is provided by Red Hat, however, it requires to use (instead or on top of Ansible Galaxy) the Red Hat Automation Hub. This can be achieved by adding the repository to the ansible.cfg file:

```
[defaults]
...

[galaxy]
server_list = automation_hub, galaxy

[galaxy_server.galaxy]
url=https://galaxy.ansible.com/

[galaxy_server.automation_hub]
url=https://cloud.redhat.com/api/automation-hub/

auth_url=https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token

token=<insert-your-token-here>

```

More information on [how to use Red Hat Automation Hub are available here](https://access.redhat.com/documentation/en-us/red_hat_jboss_web_server/6.0/html/installing_jboss_web_server_by_using_the_red_hat_ansible_certified_content_collection/install_collection). To [obtain the Red Hat Automation Hub token](https://console.redhat.com/ansible/automation-hub/token), follow this documentation.

Once this is setup, `ansible-galaxy `can install the collection:

```
# ansible-galaxy collection install redhat.jws
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://console.redhat.com/api/automation-hub/v3/plugin/ansible/content/published/collections/artifacts/redhat-jws-2.0.0.tar.gz to /root/.ansible/tmp/ansible-local-91vvby1md6/tmpjzlxwbz6/redhat-jws-2.0.0-mj2vji8g
Installing 'redhat.jws:2.0.0' to '/root/.ansible/collections/ansible_collections/redhat/jws'
Downloading https://console.redhat.com/api/automation-hub/v3/plugin/ansible/content/published/collections/artifacts/redhat-runtimes_common-1.2.1.tar.gz to /root/.ansible/tmp/ansible-local-91vvby1md6/tmpjzlxwbz6/redhat-runtimes_common-1.2.1-rwk2o0dw
redhat.jws:2.0.0 was installed successfully
Downloading https://console.redhat.com/api/automation-hub/v3/plugin/ansible/content/published/collections/artifacts/ansible-posix-1.5.4.tar.gz to /root/.ansible/tmp/ansible-local-91vvby1md6/tmpjzlxwbz6/ansible-posix-1.5.4-adyme9vq
Installing 'redhat.runtimes_common:1.2.1' to '/root/.ansible/collections/ansible_collections/redhat/runtimes_common'
redhat.runtimes_common:1.2.1 was installed successfully
Installing 'ansible.posix:1.5.4' to '/root/.ansible/collections/ansible_collections/ansible/posix'
Downloading https://console.redhat.com/api/automation-hub/v3/plugin/ansible/content/published/collections/artifacts/redhat-rhel_system_roles-1.23.0.tar.gz to /root/.ansible/tmp/ansible-local-91vvby1md6/tmpjzlxwbz6/redhat-rhel_system_roles-1.23.0-3gm1eccc
ansible.posix:1.5.4 was installed successfully
Installing 'redhat.rhel_system_roles:1.23.0' to '/root/.ansible/collections/ansible_collections/redhat/rhel_system_roles'
redhat.rhel_system_roles:1.23.0 was installed successfully
```

**Note:** Ansible Galaxy comes with dependency management, which means that it fetches any required dependencies for this collection.

## 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: true
  collections:
    - redhat.jws
```

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 inventory jws_dev_to.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 
```

### Automatically retrieve archive from Red Hat Customer Portal

Now that the Ansible collection for JWS is properly installed and accessible from our playbook, let’s provide the service account information, so that the engine can automatically download the JWS binary files provided by Red Hat.

For the collection to be able to download the JWS archive from the Red Hat Customer Portal, we need to supply the credentials associated with a Red Hat service account. One way to provide those values parameters is to create a `service_account.yml` which can be passed to Ansible as an extra source of variables:

```
---
rhn_username: <service_account_id>
rhn_password: <service_account_password>
```

**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.

Now, we have everything in place to run our playbook and install JWS on the target. But before doing so, we will see how to configure the software to match the requirement of our use case.

### 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:
    jws_java_version: 17
  collections:
    – redhat.jws
  roles:
    - jws
```

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

```
$ ansible -m setup localhost | grep family
    	"ansible_os_family": "RedHat",
```

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:
	[...]
	jws_user: java_servlet_engine
	jws_group: java_web_server
	[...]
  collections:
	- redhat.jws
  roles:
	- jws
  pre_tasks:
	[...]
  tasks:
```

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:
	[...]
        jws_systemd_enabled: True
	jws_service_name: tomcat
	[...]
  collections:
	- redhat.jws
  roles:
[...]
```
**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
     Loaded: loaded (/usr/lib/systemd/system/tomcat.service; enabled; preset: disabled)
     Active: active (running) since Tue 2024-06-18 14:50:11 UTC; 52s ago
   Main PID: 3365 (java)
      Tasks: 44 (limit: 1638)
     Memory: 116.3M
        CPU: 4.170s
     CGroup: /system.slice/tomcat.service
             └─3365 /etc/alternatives/jre_17/bin/java -Djava.util.logging.config.file=/opt/jws-6.0/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogM>

Jun 18 14:50:11 a09c3523b337 systemd[1]: Started Jboss Web Server.
Jun 18 14:50:11 a09c3523b337 systemd-service.sh[3357]: Tomcat started.
Jun 18 14:50:11 a09c3523b337 systemd-service.sh[3356]: Tomcat runs with PID: 3365
```

## 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 service"
```

## 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 }
```

## 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.