Introduction
A package being installed, a configuration file existing, and a service responding are different outcomes. You will manage each explicitly: install and remove a utility, configure nginx, declare its running state, and reload changed configuration through a handler. Actual HTTP responses from both managed nodes will establish that the service works.
Declare Installed and Absent Package States
In this step, you will install a small utility, inspect it, and remove it through a reusable package declaration.
The apt module manages Ubuntu packages on the managed host. Its state: present requests installation and state: absent requests removal. This environment includes prepared package lists and cached downloads, so keep update_cache: false; refreshing lists or requesting latest versions would add unnecessary network dependencies. Production update policies may differ.
Enter the controller project:
cd /home/labex/project
Create a playbook whose variable selects the desired tree package state. Package operations require become on the targets. install_recommends: false avoids optional recommended packages.
cat > packages.yml <<'YAML'
- name: Declare a utility package state
hosts: all
gather_facts: false
become: true
vars:
utility_state: present
tasks:
- name: Converge the tree package
ansible.builtin.apt:
name: tree
state: "{{ utility_state }}"
update_cache: false
install_recommends: false
YAML
Install the utility:
ansible-playbook packages.yml
The tree command displays directory trees; here its version option demonstrates that the executable is really installed on each managed node:
ansible all -m ansible.builtin.command -a 'tree --version'
Each target should return a tree version. Repeat the playbook; present packages should not need another installation:
ansible-playbook packages.yml
Now request absence with an extra variable rather than editing the reusable task:
ansible-playbook packages.yml -e utility_state=absent
Repeat the same desired absence. Both recap rows should now show changed=0, failed=0 and unreachable=0:
ansible-playbook packages.yml -e utility_state=absent
Package state is a declaration about installation, not proof that a service is running or responding. Leave tree absent while the next step deploys nginx.
Configure and Start a Real Web Service
In this step, you will install nginx, deploy a complete validated configuration, and explicitly keep its service running.
The two managed nodes are containers with separate SSH endpoints and filesystems. They do not run systemd. The service module uses their available service manager; do not run systemctl on these targets. We will manage current running state rather than promise a systemd boot policy.
Create a static page on the controller. It ends with a newline so its exact managed bytes are predictable:
printf 'Ansible service ready\n' > page.txt
Write a complete nginx configuration. events and http are top-level nginx contexts, server defines a website, listen chooses port 8080, root selects its document directory, and index selects the page for / requests. These are nginx settings rather than Ansible YAML.
cat > nginx-course.conf <<'NGINX'
events {}
http {
server {
listen 8080;
root /var/www/ansible-site;
index index.html;
}
}
NGINX
Create the deployment playbook. The apt task uses policy_rc_d: 101 to prevent the package installer from implicitly starting nginx; the later service task explicitly declares running state. The copy task's validate runs nginx's configuration test against a temporary candidate (%s) before replacing the destination.
A handler is a named action listed under handlers. A changed configuration task notifies Reload nginx, which runs once at the end of the play. No configuration change means no notification. The ordinary service task uses started, which keeps an already running service running without an unconditional restart.
cat > web.yml <<'YAML'
- name: Deploy a working web service
hosts: all
gather_facts: false
become: true
tasks:
- name: Install nginx without refreshing the prepared package lists
ansible.builtin.apt:
name: nginx
state: present
update_cache: false
install_recommends: false
policy_rc_d: 101
- name: Prepare the site directory
ansible.builtin.file:
path: /var/www/ansible-site
state: directory
owner: root
group: root
mode: "0755"
- name: Deploy the static page
ansible.builtin.copy:
src: /home/labex/project/page.txt
dest: /var/www/ansible-site/index.html
owner: root
group: root
mode: "0644"
- name: Validate and deploy nginx configuration
ansible.builtin.copy:
src: /home/labex/project/nginx-course.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/nginx -t -c %s
notify: Reload nginx
- name: Keep nginx running
ansible.builtin.service:
name: nginx
state: started
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
YAML
Check syntax, then apply the deployment:
ansible-playbook web.yml --syntax-check
ansible-playbook web.yml
Both nodes should install nginx, receive the files, and start successfully. A first configuration change also schedules the reload handler. Checking a package version alone would not establish any of that service behavior.
curl is an HTTP client. Its --fail option treats an unsuccessful HTTP status as a command failure; --silent removes progress output and --show-error retains failure details. Execute it on each managed host against that host's own loopback interface:
ansible all -m ansible.builtin.command -a 'curl --fail --silent --show-error http://127.0.0.1:8080/'
Each result should contain Ansible service ready. The controller and each target have separate loopback interfaces; this command checks both target services rather than the controller.
Apply a Configuration Change Through a Handler
In this step, you will change a live nginx setting, observe the handler, and confirm the setting through an HTTP response.
The nginx server_tokens directive controls whether response headers expose nginx's version. Add server_tokens off inside http by replacing the controller configuration with this complete version:
cat > nginx-course.conf <<'NGINX'
events {}
http {
server_tokens off;
server {
listen 8080;
root /var/www/ansible-site;
index index.html;
}
}
NGINX
Apply the same playbook:
ansible-playbook web.yml
The configuration copy should report changed and notify Reload nginx on both hosts. The handler reloads the running service after the new configuration passes validation. Its execution makes the configuration effective; merely copying a file does not establish what the process has loaded.
curl's -I requests HTTP headers. Inspect the actual service response on both hosts:
ansible all -m ansible.builtin.command -a 'curl --fail --silent --show-error -I http://127.0.0.1:8080/'
Both responses should have a successful HTTP status and a Server header of nginx without a version suffix. Other header fields, such as dates, vary.
Repeat the complete playbook:
ansible-playbook web.yml
Both hosts should report changed=0 with no failed or unreachable tasks, and no reload handler should be scheduled. The static page remains available, tree remains absent, and nginx remains installed and running.
Summary
You declared package presence and absence, separated installation from service running state, and deployed a complete nginx configuration with candidate validation. A changed configuration notified a reload handler, actual HTTP responses established the live behavior, and an unchanged repeat avoided extra changes or reloads.


