Ansible First Playbook (Web Server)

This lesson is the most important practical milestone in Module 3. You will write a complete, working playbook that installs Nginx, configures it, and serves a custom page. This playbook touches every fundamental concept — plays, tasks, variables, modules, and handlers — in a single coherent workflow that delivers a real, observable result.

The Goal

After running this playbook, every server in your [webservers] group will have Nginx installed, configured with a custom server_name, running, enabled to start on boot, and serving an HTML page that displays the server's hostname. You will verify the result by opening a browser.

Step 1: Create the Playbook File

Create a file called install-webserver.yml in your lab directory:

---
- name: Install and configure Nginx web server
  hosts: webservers
  become: true
  
  vars:
    nginx_port: 80
    nginx_document_root: /var/www/html
    nginx_server_name: "{{ inventory_hostname }}"
  
  tasks:
  
    - name: Update apt package cache
      apt:
        update_cache: true
        cache_valid_time: 3600  # Only update if cache is older than 1 hour
    
    - name: Install Nginx
      apt:
        name: nginx
        state: present
      notify: Start and enable Nginx
    
    - name: Ensure document root exists
      file:
        path: "{{ nginx_document_root }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'
    
    - name: Deploy custom index.html
      copy:
        content: |
          <!DOCTYPE html>
          <html>
          <head><title>{{ inventory_hostname }}</title></head>
          <body>
          <h1>Welcome to {{ inventory_hostname }}</h1>
          <p>Managed by Ansible. OS: {{ ansible_distribution }} {{ ansible_distribution_version }}</p>
          </body>
          </html>
        dest: "{{ nginx_document_root }}/index.html"
        owner: www-data
        group: www-data
        mode: '0644'
      notify: Reload Nginx
    
    - name: Deploy Nginx configuration
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
        owner: root
        group: root
        mode: '0644'
        backup: true
      notify: Reload Nginx
    
    - name: Verify Nginx is running
      service:
        name: nginx
        state: started
        enabled: true
  
  handlers:
  
    - name: Start and enable Nginx
      service:
        name: nginx
        state: started
        enabled: true
    
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

Step 2: Create the Nginx Template

Create a templates/ directory and add nginx.conf.j2:

server {
    listen {{ nginx_port }};
    server_name {{ nginx_server_name }};
    root {{ nginx_document_root }};
    index index.html;
    
    location / {
        try_files $uri $uri/ =404;
    }
    
    access_log /var/log/nginx/{{ inventory_hostname }}-access.log;
    error_log  /var/log/nginx/{{ inventory_hostname }}-error.log;
}

Step 3: Run a Syntax Check

ansible-playbook --syntax-check -i inventory.ini install-webserver.yml

Fix any errors reported before proceeding. Common issues: incorrect indentation, missing quotes around values with special characters.

Step 4: Dry Run with Check Mode

ansible-playbook --check -i inventory.ini install-webserver.yml

Check mode shows what Ansible would do without actually doing it. Review the output to confirm the expected tasks will run on the correct hosts.

Step 5: Run the Playbook

ansible-playbook -i inventory.ini install-webserver.yml

Watch the output carefully. Each task shows one of three statuses:

  • ok — Task ran but found the system already in the desired state (no change)
  • changed — Task ran and made a change to the system
  • failed — Task encountered an error; the host is removed from subsequent tasks

Step 6: Verify the Result

Open a browser and navigate to http://192.168.56.11. You should see the custom HTML page showing the server hostname and OS details. Check web02 at http://192.168.56.12 — it shows different content because {{ inventory_hostname }} resolves to web02 on that host.

Verify from the command line:

ansible webservers -i inventory.ini -m uri -a "url=http://localhost return_content=yes" -b

Step 7: Run the Playbook Again

Re-run the playbook without making any changes to the system. All tasks should show ok instead of changed. The PLAY RECAP at the end should show 0 changes. This demonstrates idempotency — the defining characteristic of well-written Ansible automation.

Understanding What Just Happened

You wrote a playbook that works correctly on both web01 and web02 simultaneously, producing customised output for each server using the same code. The template generated a different Nginx config for each server based on its hostname. The handlers ran only when the configuration actually changed — not on the idempotent second run. This is professional-grade Ansible.

Try This: Add a Second Play

Add a second play at the bottom of install-webserver.yml that targets databases and installs PostgreSQL. Run the full playbook and confirm both plays execute against their respective host groups.

Summary

This lesson produced a complete, idempotent, production-quality playbook for web server installation and configuration. Key concepts demonstrated: plays targeting specific host groups, variables used consistently across tasks and templates, the copy and template modules for file deployment, handlers triggered on change, and idempotency verified by re-running the playbook. Every technique in this lesson generalises directly to real-world Ansible work.

Leave a Comment