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: reloadedStep 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 systemfailed— 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.
