Article
Automate WildFly deployments with Ansible
Deploy enterprise Java AI apps with zero manual configurationDeploying enterprise Java applications shouldn't require hours of manual server configuration and SSH sessions. Yet that's exactly how many teams still operate copying WAR files, editing XML configurations, restarting services, and hoping everything works. There's a better way, and it's built right into the Ansible ecosystem.
This article demonstrates how to leverage the official middleware_automation.wildfly collection to deploy a complete WildFly-based AI application from scratch. You go from a bare server to a running application server hosting an AI-powered REST API, all with a single Ansible playbook. No manual steps, no SSH sessions, no configuration drift.
Why the WildFly collection matters
The middleware_automation.wildfly collection, maintained by IBM and the Ansible community, encapsulates years of WildFly deployment expertise into reusable roles and modules. Instead of writing hundreds of lines of tasks to download WildFly, extract it, configure systemd services, set up users and permissions, and manage deployments, you get battle-tested roles that handle all of this automatically.
What makes this collection particularly valuable is that it follows WildFly best practices out of the box. Proper file permissions, systemd integration, secure defaults, and support for both standalone and domain modes. It encapsulates knowledge that engineers typically possess, now codified as infrastructure as code.
The demo application: AI meets enterprise middleware
To showcase the collection's capabilities, this article demonstrates how to deploy a conversational AI REST API built on WildFly 39.0.0.Final, LangChain4j, and Ollama. The demo application is a standard Jakarta EE WAR file JAX-RS endpoints, CDI services, and integration with a local LLM. But the interesting part isn't the application itself; it's how effortlessly the WildFly collection handles the entire deployment lifecycle.
The application accepts plain text via a REST endpoint, processes it through a local language model (Google's gemma:2b running on Ollama), and returns conversational AI responses. It's a real-world application with actual dependencies and configuration requirements perfect for demonstrating what the collection can do under realistic conditions.
Inside the application: Enterprise Java meets AI
Before diving into Ansible automation, let's understand what you're deploying. This application demonstrates that modern AI capabilities integrate seamlessly with Jakarta EE standards, and more importantly, it shows the kind of sophisticated application the WildFly collection can handle.
The application has a clean four-layer architecture.
At the REST layer sits
DemoResource, a standard JAX-RS endpoint: DemoResource.java
There's nothing AI-specific here. It's just JAX-RS accepting a POST request and delegating to a service the same pattern you'd use for any enterprise application. The
@Injectannotation brings in CDI dependency injection, one of Jakarta EE's core features. WildFly handles this automatically; no Spring, no external frameworks needed.The service layer is where things get interesting.
AnalystServiceis marked@ApplicationScoped, making it a singleton managed by WildFly's CDI container:
The
@PostConstructmethod runs when WildFly instantiates the service, setting up the connection to Ollama and creating the AI service implementation. Notice how configuration comes from environment variables, which is crucial for the Ansible automation that is explored later in this article. The same WAR file can connect to different Ollama instances or use different models just by changing environment variables.The temperature setting of 0.7 balances creativity and consistency in the AI responses. Lower values make responses more deterministic; higher values increase variety. For conversational AI, 0.7 is a good middle ground.
The real magic happens with LangChain4j's declarative approach. The
Analystinterface has no implementation:
LangChain4j generates an implementation at runtime through
AiServices.create(). The@SystemMessagesets the AI's personality—similar to a system prompt in ChatGPT. The@UserMessagemarks which parameter contains the user's input. When you callanalyze(), LangChain4j constructs a prompt combining both messages, sends it to Ollama, and returns the response.This declarative style fits perfectly with Jakarta EE's philosophy. It's the same pattern used by Jakarta Persistence for database access or Jakarta REST for web services: define interfaces and annotations, and let the framework generate implementations.
Finally, there's
RestConfig, which activates JAX-RS in the application:
That's the entire file. The @ApplicationPath annotation tells WildFly to expose REST endpoints under the /api path. No web.xml, no servlet configuration pure annotation-driven setup.
When a request comes in, the flow is straightforward: WildFly's JAX-RS subsystem routes the request to DemoResource.check(), which calls the injected AnalystService.analyze(), which delegates to the LangChain4j-generated implementation, which sends a prompt to Ollama running locally. Ollama performs inference with the gemma:2b model and returns the response, which flows back through the stack to the client.
The application's Maven build is conventional. The pom.xml imports WildFly's Bill of Materials for Jakarta EE dependencies and adds LangChain4j with its Ollama integration. The build produces a standard WAR file with no special packaging, no custom deployment descriptors. Just a regular Jakarta EE application that happens to integrate with AI.
This is exactly the kind of application the WildFly collection excels at deploying. It has external dependencies (Ollama), requires environment configuration, needs a proper Jakarta EE runtime, and represents real production complexity. If the collection can handle this seamlessly, it can handle your applications too.
Setting up the collection
Before writing any playbooks, you need to install the collection from Ansible Galaxy:
ansible-galaxy collection install middleware_automation.wildfly
This single command pulls down everything you need: the wildfly_install role for setting up WildFly, the wildfly_systemd role for service configuration, and various modules for managing deployments and server configuration. The collection supports WildFly versions from 18 through the latest releases, giving you flexibility in choosing your target version.
The beauty of Ansible collections is that they bundle related automation into a single, versioned package. You're not hunting for individual roles across Galaxy or GitHub. Everything you need for WildFly automation lives in one place with consistent interfaces and documentation.
Structuring your Ansible project
The demo project follows Ansible best practices for organization. Under the ansible directory, you'll find the typical structure: an inventory file defining target servers, group_vars/all.yml for shared configuration, and site.yml as the main playbook.
The inventory is intentionally flexible:
[local]
localhost ansible_connection=local
[dev]
[staging]
[production]
[wildfly:children]
local
dev
staging
production
This structure lets you deploy locally for testing or target remote environments by simply adding server entries under the appropriate group. The wildfly:children group aggregates all environments, allowing you to apply common variables while still maintaining environment-specific overrides.
The configuration lives in group_vars/all.yml:
app_war_path: "../target/ai-demo.war"
wildfly_version: "39.0.0.Final"
wildfly_install_dir: "/opt/wildfly-39.0.0.Final"
wildfly_user: "wildfly"
wildfly_group: "wildfly"
java_home: "/usr/lib/jvm/java-17-openjdk"
wildfly_port_http: 8380
wildfly_port_https: 8743
wildfly_management_port: 10290
These variables drive the entire deployment. Want to upgrade to a different WildFly version? Change wildfly_version. Need different ports for your environment? Modify the port variables. The collection's roles consume these variables automatically, requiring no additional configuration.
Notice there's no mention of Ollama configuration here. That's because the application gets its Ollama settings from environment variables, which the wildfly_systemd role will inject into the service. This separation of concerns keeps the Ansible configuration focused on infrastructure while application configuration remains flexible.
The Ansible playbook: Where automation happens
The site.yml playbook orchestrates the entire deployment using the collection's roles. The following sections walk through how it works.
The playbook starts by importing the collection:
---
- name: Deploy Enterprise AI Stack on WildFly
hosts: all
become: true
collections:
- middleware_automation.wildfly
That collections directive makes all the collection's roles and modules available without needing to use fully qualified names. You can reference wildfly_install instead of middleware_automation.wildfly.wildfly_install, keeping the playbook clean and readable.
Before touching the target servers, the playbook validates that the WAR file exists locally:
pre_tasks:
- name: Verify WAR file exists
stat:
path: "{{ app_war_path }}"
register: war_file
delegate_to: localhost
become: false
- name: Fail if WAR file not found
fail:
msg: "WAR file not found at {{ app_war_path }}. Run 'mvn clean package' first."
when: not war_file.stat.exists
This fail-fast approach saves time. If you forgot to build the application, Ansible tells you immediately instead of going through the entire WildFly installation only to fail at deployment time. The delegate_to: localhost ensures this check runs on your control machine, not the target server.
Installing WildFly with the collection
The heart of the automation is the wildfly_install role:
roles:
- role: wildfly_install
tags: [install, wildfly]
This single role reference does an enormous amount of work. When you run the playbook, the role:
- Creates the
wildflysystem user and group with appropriate permissions. - Downloads the specified WildFly version from the official distribution server.
- Verifies the download checksum to ensure integrity.
- Extracts the archive to the configured installation directory.
- Sets proper ownership and permissions on all files.
- Configures the standalone configuration with the specified ports.
- Creates necessary directories for deployments and logs.
All of this happens idempotently. Run the playbook again, and it won't re-download or re-extract if WildFly is already installed at the correct version. Ansible's declarative approach means you're describing the desired state, not scripting individual steps.
The role respects all the variables you defined in group_vars/all.yml. The wildfly_version determines what gets downloaded, wildfly_install_dir controls where it goes, and wildfly_user/wildfly_group set the ownership. Port configurations automatically update the standalone.xml file.
What makes this particularly powerful is that the role encapsulates WildFly expertise. It knows where configuration files live, which XML elements to modify for port changes, what permissions are needed for security. You don't need to be a WildFly expert, the role brings that knowledge.
Systemd integration
After installation comes service management, handled by the wildfly_systemd role:
- role: wildfly_systemd
tags: [systemd, service]
This role transforms WildFly from a directory of files into a proper system service. It creates a systemd unit file at /etc/systemd/system/wildfly.service, configures the service to start on boot, and sets up environment variables.
The systemd integration is where the AI application's configuration comes into play. The role creates /etc/sysconfig/wildfly.conf where environment variables like OLLAMA_MODEL and OLLAMA_BASE_URL are defined. The service loads these variables when it starts, allowing runtime configuration without modifying the WAR file.
For this demo, we would extend the role's variables to include:
wildfly_service_config:
OLLAMA_MODEL: "gemma:2b"
OLLAMA_BASE_URL: "http://localhost:11434"
The role injects these into the service environment, making them available to the Java application through System.getenv(). This is exactly how the AnalystService class reads its configuration during the @PostConstruct initialization.
The service runs as the wildfly user, following the principle of least privilege. It uses systemd's Type=simple for straightforward process management and includes proper dependency ordering to ensure the network is available before WildFly starts.
Once this role completes, WildFly is running as a system service. You can manage it with standard systemd commands: systemctl status wildfly, systemctl restart wildfly, and so on. The service automatically restarts on server reboot, and systemd handles logging through journald.
Application deployment
With WildFly installed and running, the playbook moves to application deployment. This part doesn't use a role because deployment needs are often application-specific. Instead, the playbook includes custom tasks that leverage WildFly's deployment scanner:
tasks:
- name: Ensure deployment directory exists
file:
path: "{{ wildfly_install_dir }}/standalone/deployments"
state: directory
owner: "{{ wildfly_user }}"
group: "{{ wildfly_group }}"
mode: '0755'
tags: [deploy]
The deployment directory is where WildFly watches for applications. While the wildfly_install role creates this directory, explicitly ensuring it exists makes the playbook more robust if you're deploying to an existing WildFly installation.
Before deploying the new WAR, the playbook cleans up any stale marker files:
- name: Remove stale deployment markers
file:
path: "{{ item }}"
state: absent
loop:
- "{{ wildfly_install_dir }}/standalone/deployments/ai-demo.war.dodeploy"
- "{{ wildfly_install_dir }}/standalone/deployments/ai-demo.war.deployed"
- "{{ wildfly_install_dir }}/standalone/deployments/ai-demo.war.failed"
WildFly uses marker files to track deployment state. A .deployed marker indicates successful deployment, .failed indicates an error, and .dodeploy triggers deployment. Removing these ensures a clean deployment cycle, especially important when redeploying an updated version.
The actual deployment is a simple file copy:
- name: Deploy the AI Java App
copy:
src: "{{ app_war_path }}"
dest: "{{ wildfly_install_dir }}/standalone/deployments/ai-demo.war"
owner: "{{ wildfly_user }}"
group: "{{ wildfly_group }}"
mode: '0644'
tags: [deploy]
Ansible's copy module handles transferring the WAR from your build machine to the server. Setting ownership to the wildfly user ensures the process can read and manage the file. The playbook then triggers deployment by creating the .dodeploy marker:
- name: Trigger deployment with .dodeploy marker
file:
path: "{{ wildfly_install_dir }}/standalone/deployments/ai-demo.war.dodeploy"
state: touch
owner: "{{ wildfly_user }}"
group: "{{ wildfly_group }}"
tags: [deploy]
WildFly's deployment scanner notices the marker and begins deploying the application. The playbook doesn't just trigger deployment without waiting; it waits for confirmation:
- name: Wait for deployment to complete
wait_for:
path: "{{ wildfly_install_dir }}/standalone/deployments/ai-demo.war.deployed"
timeout: 120
register: deployment_wait
ignore_errors: true
tags: [deploy]
This task waits up to 120 seconds for the .deployed marker to appear. If deployment succeeds, the marker appears and the playbook continues. If something goes wrong, the timeout expires, and the playbook's error handling kicks in:
- name: Show deployment error if failed
when: deployment_wait is failed
tags: [deploy]
block:
- name: Read server log on failure
command: tail -50 {{ wildfly_install_dir }}/standalone/log/server.log
register: server_log
failed_when: false
- name: Display error information
fail:
msg: |
Deployment failed or timed out after 120 seconds!
Last 50 lines of server.log:
{{ server_log.stdout }}
Rather than leaving you to SSH into the server and hunt through logs, the playbook automatically retrieves the last 50 lines of the server log and displays them. This immediate feedback makes troubleshooting much faster. If the application failed to connect to Ollama, or if there's a class loading issue, you'll see it right in the Ansible output.
Running the deployment
With everything in place, running the deployment is remarkably simple:
cd ansible
ansible-playbook -i inventory site.yml
Ansible connects to the target servers, executes the roles and tasks, and provides real-time feedback. The output shows each step: downloading WildFly, configuring systemd, copying the WAR file, waiting for deployment. When it completes successfully, you see:
TASK [Display deployment information]
ok: [localhost] => {
"msg": [
"WildFly AI Demo Deployed Successfully!",
"Application URL: http://192.168.1.100:8380/ai-demo",
"API Endpoint: http://192.168.1.100:8380/ai-demo/api/demo/check",
"Management Console: http://192.168.1.100:10290"
]
}
The entire process from bare server to running application takes just a few minutes. More importantly, it's completely reproducible. Run the same playbook against ten servers, and you'll get ten identical WildFly installations, all running the AI application with identical configurations.
Leveraging Ansible tags
The playbook uses tags extensively to control what runs:
# Just install WildFly, don't deploy
ansible-playbook -i inventory site.yml --tags install
# Just deploy the application, skip installation
ansible-playbook -i inventory site.yml --tags deploy
# Verify deployment status
ansible-playbook -i inventory site.yml --tags verify
This granularity is crucial for real-world operations. Your first deployment runs everything. Subsequent deployments might only need the deploy tag, skipping the lengthy WildFly installation. When you update the WAR file with a bug fix or new feature, running with --tags deploy pushes just the application, leaving WildFly untouched.
Tags make the playbook flexible without requiring multiple playbook files. One playbook handles installation, configuration, and deployment, with tags controlling execution scope.
Configuration management through the collection
One of the collection's strengths is how it handles WildFly configuration. While this demo uses the standalone server with minimal configuration, the collection supports much more sophisticated scenarios.
Want to change the Java heap size? Add variables:
wildfly_java_opts: "-Xms1024m -Xmx2048m"
The wildfly_systemd role incorporates these into the service definition, ensuring WildFly starts with the correct memory settings.
Need to configure datasources, message queues, or security domains? The collection includes modules for managing WildFly's management interface programmatically. You can add datasources, deploy applications to specific server groups in domain mode, and configure subsystems all through Ansible.
For the AI application, the critical configuration happens through environment variables defined in the systemd service. This pattern configuration through environment variables works beautifully with the collection. You define the variables in your playbook, the wildfly_systemd role creates the configuration file, and your application reads them at startup.
If you need to change which Ollama model the application uses, you don't rebuild the WAR. You update the Ansible variables and re-run the playbook with the --tags systemd flag. The role updates the configuration and restarts the service, and the application picks up the new model on its next initialization.
Testing the deployed application
Once the playbook completes, the AI application is immediately available. You can test it with a simple curl command. Below picture will give you an idea about it.

The gemma:2b model adapts its responses based on the input, demonstrating the conversational AI capabilities running on your enterprise middleware stack.
Production readiness
This playbook is a solid foundation for production deployments, but real environments need additional considerations. The collection supports them all.
For multiple servers, simply add them to your inventory:
[production]
app01.example.com
app02.example.com
app03.example.com
Run the playbook, and all three servers get identical WildFly installations. Put them behind a load balancer, and you have a scalable application tier. The collection ensures consistency, every server has the same WildFly version, the same configuration, and the same application deployment.
The collection handles WildFly domain mode for clustered deployments. Instead of managing multiple standalone servers, you can configure a domain controller and host controllers, with the collection managing the entire topology. This is where the collection really shines, it understands WildFly's clustering architecture and configures it correctly.
Security hardening is built into the roles' defaults. The wildfly user has minimal permissions, services run with appropriate SELinux contexts, and the management interface can be configured with authentication and HTTPS. For production deployments, you'd extend the variables to enable TLS:
wildfly_enable_ssl: true
wildfly_keystore_path: /etc/pki/wildfly/keystore.jks
wildfly_keystore_password: "{{ vault_keystore_password }}"
The collection handles the SSL configuration in standalone.xml, ensuring secure communications.
Summary
The WildFly collection represents modern infrastructure automation for enterprise Java. Instead of clicking through management consoles or running manual installation scripts, you declare your desired state and let Ansible make it happen.
For the AI demo application, the collection made deployment trivial. What could have been hours of manual work downloading WildFly, configuring systemd, managing deployments, setting environment variables became a single command. The same approach scales to dozens of servers, complex topologies, and sophisticated configurations.
If you're deploying WildFly applications, the middleware_automation.wildfly collection should be your first stop. It's maintained, documented, and battle-tested in production environments. More than just saving time, it brings consistency, repeatability, and best practices to your deployments.
The future of enterprise Java deployment isn't SSH and manual configuration. It's infrastructure as code, leveraging collections like this one to automate what used to require expert knowledge. The WildFly collection proves that enterprise middleware and modern DevOps practices work beautifully together whether you're deploying traditional web applications or cutting-edge AI services.
Next steps: Beyond basic deployment
This demo scratches the surface of what the WildFly collection can do. The collection includes roles for:
- Configuring data sources and connection pools
- Managing deployments across domain mode clusters
- Applying patches and updates
- Configuring security realms and SSL certificates
- Setting up logging and monitoring integrations
- Managing WildFly configuration through CLI scripts
Each role is documented, tested, and designed to work together. You can build sophisticated automation that handles the entire lifecycle of WildFly applications from initial installation through configuration management, deployment, and updates.
For the AI demo, you could extend the automation to include Ollama installation and configuration. Add a role that installs Ollama, pulls the gemma:2b model, and configures it as a service. The WildFly deployment would then have everything it needs on the same server, or you could point multiple WildFly servers to a centralized Ollama instance.
If you want to know more, please visit our website or go through our GitHub projects.