Manage Packages and a Working Service

AnsibleBeginner
Practice Now

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.