Web Server Deployment
AWX Job Summary: Setup Web Server on absubn02 · ABS Lab AWX Playbooks | October 25, 2026
1. Overview
This document records how an Apache web server was set up on the host absubn02 using an AWX job template and a new playbook, 2web.yml. It covers the playbook design, the AWX template configuration, and the result of the first run (job 15), which completed successfully.
Outcome: Apache is installed on absubn02, runs as the apache user and group, and is configured to serve content from /data. The only remaining manual step is uploading index.html.
2. Requirements
Install and configure the Apache web server on absubn02, owned by and running as the apache account and apache group.
Use /data as the content directory, owned by apache:apache.
The main content file is index.html, uploaded manually (not managed by the playbook).
3. AWX Job Template Configuration
| Name |
setup web server |
| Organization |
Default |
| Inventory |
abs lab |
| Limit |
absubn02 |
| Project |
lab-playbooks |
| Playbook |
2web.yml |
| Credentials |
absubn02-awx |
| Execution Environment |
AWX EE |
| Privilege Escalation |
Enabled |
The inventory contains more than the one host, so the Limit field is what restricts the run to absubn02. The playbook itself targets hosts: all.

Figure 1. Job template details for setup web server. Illustrative reproduction built from the template inputs, not a live capture.
4. Playbook Creation
4.1 Steps taken
Wrote the playbook 2web.yml to meet the three requirements above.
Committed 2web.yml to a public GitHub repository.
Synced the existing AWX project lab-playbooks so AWX picked up the new playbook.
Created the job template setup web server with the settings in section 3.
Launched the template. The run was job 15.
4.2 What the playbook does
The playbook is written for Debian/Ubuntu hosts (absubn02 runs Ubuntu, with Python 3.12 discovered by Ansible).
Verifies the OS family. It stops with a clear message if the host is not Debian/Ubuntu.
Installs Apache (apache2).
Creates the apache group and user as a system account with no login shell and no home directory.
Creates /data owned by apache:apache with mode 0755.
Sets APACHE_RUN_USER and APACHE_RUN_GROUP in /etc/apache2/envvars, so Apache runs as apache:apache instead of the Ubuntu default www-data.
Deploys a virtual host (data.conf) with DocumentRoot /data, directory listing disabled and index.html as the index file.
Disables the default site and enables the new one.
Validates the configuration with apache2ctl configtest.
Ensures Apache is enabled and running, then applies the pending restart immediately so the user change takes effect before the checks.
Checks whether /data/index.html exists and prints a reminder if it does not.
Confirms the Apache worker processes run as apache and fails the job if they do not.
Checks that the site returns HTTP 200, only when index.html exists.
The playbook is idempotent. A second run should report no changes once everything is in place.
5. Job Run Result (Job 15)
Status: Successful.
Play recap: ok=16 changed=9 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
| Gathering Facts |
ok |
| Verify target is Debian/Ubuntu family |
ok |
| Install Apache |
changed |
| Create apache group |
changed |
| Create apache user |
changed |
| Create content directory |
changed |
| Run Apache as the apache user (envvars) |
changed (both variables) |
| Deploy virtual host for /data |
changed |
| Disable default site |
changed |
| Enable data site |
changed |
| Validate Apache configuration |
ok |
| Ensure Apache is enabled and running |
ok |
| Restart apache (handler) |
changed |
| Check whether index.html has been uploaded |
ok |
| Remind to upload index.html |
ok (index.html not found yet) |
| Confirm Apache worker processes run as apache |
ok |
| Check the site responds |
skipped (no index.html yet) |
The confirmation task passing means the Apache worker processes were verified to be running as the apache user. The HTTP check was skipped on purpose because index.html has not been uploaded yet.

Figure 2. Job 15 output page, start of the log. Illustrative reproduction using lines from the actual job log.

Figure 3. Job 15 output, end of the log with the play recap. Illustrative reproduction using lines from the actual job log.
Warnings in the log
The log contains two warnings. Neither affected the result.
ANSIBLE_COLLECTIONS_PATHS deprecation warning. The execution environment sets the plural form of this option, which will be removed in ansible-core 2.19. It comes from the environment configuration, not from the playbook.
Python interpreter discovery warning. Ansible found /usr/bin/python3.12 automatically. It can be silenced by setting ansible_python_interpreter for the host or group in the inventory.
6. Full Job Log
Show full job log
Identity added: /runner/artifacts/15/ssh_key_data (awx)
[DEPRECATION WARNING]: ANSIBLE_COLLECTIONS_PATHS option. Reason: does not fit
var naming standard, use the singular form ANSIBLE_COLLECTIONS_PATH instead
Alternatives: none. This feature will be removed in version 2.19. Deprecation
warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
PLAY [Set up Apache web server] ************************************************
TASK [Gathering Facts] *********************************************************
[WARNING]: Platform linux on host absubn02 is using the discovered Python
interpreter at /usr/bin/python3.12, but future installation of another Python
interpreter could change the meaning of that path. See
https://docs.ansible.com/ansible-
core/2.18/reference_appendices/interpreter_discovery.html for more information.
ok: [absubn02]
TASK [Verify target is Debian/Ubuntu family] ***********************************
ok: [absubn02] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [Install Apache] **********************************************************
changed: [absubn02]
TASK [Create apache group] *****************************************************
changed: [absubn02]
TASK [Create apache user] ******************************************************
changed: [absubn02]
TASK [Create content directory] ************************************************
changed: [absubn02]
TASK [Run Apache as the apache user (envvars)] *********************************
changed: [absubn02] => (item={'key': 'APACHE_RUN_USER', 'value': 'apache'})
changed: [absubn02] => (item={'key': 'APACHE_RUN_GROUP', 'value': 'apache'})
TASK [Deploy virtual host for /data] *******************************************
changed: [absubn02]
TASK [Disable default site] ****************************************************
changed: [absubn02]
TASK [Enable data site] ********************************************************
changed: [absubn02]
TASK [Validate Apache configuration] *******************************************
ok: [absubn02]
TASK [Ensure Apache is enabled and running] ************************************
ok: [absubn02]
TASK [Apply pending restart now (so user change takes effect before checks)] ***
RUNNING HANDLER [Restart apache] ***********************************************
changed: [absubn02]
TASK [Check whether index.html has been uploaded] ******************************
ok: [absubn02]
TASK [Remind to upload index.html] *********************************************
ok: [absubn02] => {
"msg": "/data/index.html not found - upload it manually (owner apache:apache)."
}
TASK [Confirm Apache worker processes run as apache] ***************************
ok: [absubn02]
TASK [Check the site responds (only once index.html exists)] *******************
skipping: [absubn02]
PLAY RECAP *********************************************************************
absubn02 : ok=16 changed=9 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
7. Remaining Steps
Upload index.html to /data on absubn02 and set its ownership: chown apache:apache /data/index.html.
Re-run the job template once the file is in place. The HTTP 200 check will then run instead of being skipped.
Check the firewall. The playbook does not manage it. If UFW is enabled on absubn02, allow port 80.
Optional: set ansible_python_interpreter in the inventory to silence the interpreter discovery warning.
Appendix: 2web.yml
Show 2web.yml
---
# 2web.yml - install and configure Apache on Ubuntu, run as apache:apache,
# serving content from /data. index.html is uploaded manually (not managed here).
#
# AWX job template: set Limit = absubn02 so it only runs on that host.
- name: Set up Apache web server
hosts: all
become: true
gather_facts: true
vars:
web_user: apache
web_group: apache
web_root: /data
web_port: 80
server_name: "{{ inventory_hostname }}"
tasks:
- name: Verify target is Debian/Ubuntu family
ansible.builtin.assert:
that: ansible_facts['os_family'] == 'Debian'
fail_msg: "{{ inventory_hostname }} is not Debian/Ubuntu - this playbook uses apache2/a2ensite"
- name: Install Apache
ansible.builtin.apt:
name: apache2
state: present
update_cache: true
cache_valid_time: 3600
- name: Create apache group
ansible.builtin.group:
name: "{{ web_group }}"
system: true
state: present
- name: Create apache user
ansible.builtin.user:
name: "{{ web_user }}"
group: "{{ web_group }}"
system: true
shell: /usr/sbin/nologin
home: /nonexistent
create_home: false
state: present
- name: Create content directory
ansible.builtin.file:
path: "{{ web_root }}"
state: directory
owner: "{{ web_user }}"
group: "{{ web_group }}"
mode: "0755"
- name: Run Apache as the apache user (envvars)
ansible.builtin.lineinfile:
path: /etc/apache2/envvars
regexp: "^export {{ item.key }}="
line: "export {{ item.key }}={{ item.value }}"
loop:
- { key: APACHE_RUN_USER, value: "{{ web_user }}" }
- { key: APACHE_RUN_GROUP, value: "{{ web_group }}" }
notify: Restart apache
- name: Deploy virtual host for /data
ansible.builtin.copy:
dest: /etc/apache2/sites-available/data.conf
owner: root
group: root
mode: "0644"
content: |
# Managed by Ansible - do not edit by hand
<VirtualHost *:{{ web_port }}>
ServerName {{ server_name }}
DocumentRoot {{ web_root }}
<Directory {{ web_root }}>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
DirectoryIndex index.html
ErrorLog ${APACHE_LOG_DIR}/data-error.log
CustomLog ${APACHE_LOG_DIR}/data-access.log combined
</VirtualHost>
notify: Restart apache
- name: Disable default site
ansible.builtin.command: a2dissite 000-default.conf
args:
removes: /etc/apache2/sites-enabled/000-default.conf
notify: Restart apache
- name: Enable data site
ansible.builtin.command: a2ensite data.conf
args:
creates: /etc/apache2/sites-enabled/data.conf
notify: Restart apache
- name: Validate Apache configuration
ansible.builtin.command: apache2ctl configtest
changed_when: false
- name: Ensure Apache is enabled and running
ansible.builtin.service:
name: apache2
state: started
enabled: true
- name: Apply pending restart now (so user change takes effect before checks)
ansible.builtin.meta: flush_handlers
- name: Check whether index.html has been uploaded
ansible.builtin.stat:
path: "{{ web_root }}/index.html"
register: index_file
- name: Remind to upload index.html
ansible.builtin.debug:
msg: "{{ web_root }}/index.html not found - upload it manually (owner {{ web_user }}:{{ web_group }})."
when: not index_file.stat.exists
- name: Confirm Apache worker processes run as apache
ansible.builtin.shell: >
ps -eo user:20,comm | awk '$2=="apache2" && $1!="root"{print $1}' | sort -u
register: worker_users
changed_when: false
failed_when: worker_users.stdout.strip() != web_user
- name: Check the site responds (only once index.html exists)
ansible.builtin.uri:
url: "http://localhost:{{ web_port }}/"
status_code: 200
when: index_file.stat.exists
handlers:
- name: Restart apache
ansible.builtin.service:
name: apache2
state: restarted