Ansible Playbooks: Task Automation
Ansible playbooks are the foundation of infrastructure automation, turning repetitive manual tasks into reproducible, version-controlled configurations. Unlike ad-hoc commands that execute single...
Key Insights
- Ansible playbooks transform infrastructure management from imperative scripts to declarative, idempotent configurations that describe desired state rather than sequential commands
- Proper playbook organization using roles, variables, and templates creates reusable automation that scales from single servers to thousands of nodes without code duplication
- Handlers, conditionals, and error handling blocks turn basic task lists into production-grade automation that responds intelligently to system state and failures
Ansible playbooks are the foundation of infrastructure automation, turning repetitive manual tasks into reproducible, version-controlled configurations. Unlike ad-hoc commands that execute single modules, playbooks orchestrate complex multi-step workflows across your infrastructure. They’re written in YAML, making them human-readable while remaining powerful enough to manage enterprise-scale deployments.
Introduction to Ansible Playbooks
Playbooks describe your infrastructure’s desired state. Ansible reads these declarations and makes whatever changes necessary to achieve that state, skipping actions that are already complete. This idempotency means running the same playbook multiple times produces identical results without breaking your systems.
Here’s a minimal playbook that demonstrates the basic structure:
---
- name: My first playbook
hosts: webservers
become: yes
tasks:
- name: Ensure a file exists
ansible.builtin.file:
path: /tmp/hello.txt
state: touch
mode: '0644'
- name: Display a message
ansible.builtin.debug:
msg: "Playbook executed successfully"
This playbook targets hosts in the webservers group, escalates privileges with become, and executes two tasks. Each task uses a module (file and debug) with specific parameters.
Playbook Anatomy and Core Components
A playbook contains one or more “plays,” each targeting specific hosts and defining tasks to execute. Understanding the execution flow and component interaction is crucial for building reliable automation.
---
- name: Complete component demonstration
hosts: appservers
become: yes
vars:
app_port: 8080
app_user: apprunner
tasks:
- name: Install application dependencies
ansible.builtin.package:
name:
- python3
- python3-pip
state: present
- name: Create application user
ansible.builtin.user:
name: "{{ app_user }}"
system: yes
create_home: no
- name: Deploy application configuration
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
owner: "{{ app_user }}"
mode: '0640'
notify: Restart application service
handlers:
- name: Restart application service
ansible.builtin.systemd:
name: myapp
state: restarted
Tasks execute sequentially. The vars section defines playbook-level variables accessible to all tasks. Handlers are special tasks that only run when notified by other tasks, typically used for service restarts. This playbook only restarts the service if the configuration file actually changes.
Building a Practical Web Server Deployment
Let’s build a complete playbook that deploys a production-ready Nginx web server. This demonstrates how multiple tasks combine to achieve a complex outcome.
---
- name: Deploy Nginx web server
hosts: webservers
become: yes
vars:
nginx_port: 80
document_root: /var/www/html
site_name: example.com
tasks:
- name: Install Nginx
ansible.builtin.package:
name: nginx
state: present
- name: Configure firewall for HTTP
ansible.posix.firewalld:
service: http
permanent: yes
state: enabled
immediate: yes
when: ansible_os_family == "RedHat"
- name: Create document root
ansible.builtin.file:
path: "{{ document_root }}"
state: directory
owner: nginx
group: nginx
mode: '0755'
- name: Deploy website content
ansible.builtin.copy:
src: files/index.html
dest: "{{ document_root }}/index.html"
owner: nginx
group: nginx
mode: '0644'
- name: Deploy Nginx configuration
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: 'nginx -t -c %s'
notify: Reload Nginx
- name: Ensure Nginx is running
ansible.builtin.systemd:
name: nginx
state: started
enabled: yes
handlers:
- name: Reload Nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
This playbook installs Nginx, configures the firewall, creates directory structures, deploys content and configuration, then ensures the service runs. The validate parameter in the template task prevents deploying broken configurations.
Variables, Templates, and Dynamic Configuration
Variables make playbooks reusable across different environments. Combine them with Jinja2 templates to generate configuration files dynamically.
Create a directory structure:
inventory/
group_vars/
webservers.yml
host_vars/
web01.yml
templates/
nginx.conf.j2
In group_vars/webservers.yml:
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
In host_vars/web01.yml:
server_name: web01.example.com
nginx_worker_processes: 4
The template templates/nginx.conf.j2:
user nginx;
worker_processes {{ nginx_worker_processes }};
error_log /var/log/nginx/error.log;
events {
worker_connections {{ nginx_worker_connections }};
}
http {
keepalive_timeout {{ nginx_keepalive_timeout }};
server {
listen 80;
server_name {{ server_name }};
root {{ document_root }};
location / {
index index.html;
}
}
}
Variables follow a precedence hierarchy: host_vars override group_vars, which override playbook vars. This allows environment-specific customization without duplicating playbooks.
Handlers, Conditionals, and Loops
Real-world automation requires responding to different conditions and processing multiple items efficiently.
---
- name: Advanced control structures
hosts: all
become: yes
vars:
packages_to_install:
- git
- curl
- vim
- htop
tasks:
- name: Install packages on Debian-based systems
ansible.builtin.apt:
name: "{{ packages_to_install }}"
state: present
update_cache: yes
when: ansible_os_family == "Debian"
- name: Install packages on RedHat-based systems
ansible.builtin.yum:
name: "{{ packages_to_install }}"
state: present
when: ansible_os_family == "RedHat"
- name: Configure service files
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- { src: 'app.service.j2', dest: '/etc/systemd/system/app.service' }
- { src: 'app-worker.service.j2', dest: '/etc/systemd/system/app-worker.service' }
notify: Reload systemd
- name: Enable memory-intensive service only on large instances
ansible.builtin.systemd:
name: analytics
enabled: yes
when: ansible_memtotal_mb > 8000
handlers:
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: yes
Conditionals use the when clause to check facts or variables. Loops iterate over lists or dictionaries, executing the task once per item. Handlers trigger only once even if notified multiple times, preventing unnecessary service restarts.
Roles and Playbook Organization
As automation grows, roles provide structure and reusability. A role packages related tasks, handlers, templates, and variables into a portable unit.
Directory structure for a database role:
roles/
postgresql/
tasks/
main.yml
handlers/
main.yml
templates/
postgresql.conf.j2
pg_hba.conf.j2
defaults/
main.yml
vars/
main.yml
files/
init_script.sql
In roles/postgresql/tasks/main.yml:
---
- name: Install PostgreSQL
ansible.builtin.package:
name: postgresql-server
state: present
- name: Initialize database
ansible.builtin.command:
cmd: postgresql-setup --initdb
creates: /var/lib/pgsql/data/PG_VERSION
- name: Deploy configuration
ansible.builtin.template:
src: postgresql.conf.j2
dest: /var/lib/pgsql/data/postgresql.conf
notify: Restart PostgreSQL
In roles/postgresql/defaults/main.yml:
postgresql_max_connections: 100
postgresql_shared_buffers: 128MB
Use the role in a playbook:
---
- name: Setup database servers
hosts: dbservers
become: yes
roles:
- postgresql
Roles make automation modular. Share them via Ansible Galaxy or internal repositories.
Error Handling and Testing Strategies
Production playbooks need robust error handling and validation mechanisms.
---
- name: Deployment with error handling
hosts: appservers
become: yes
tasks:
- name: Risky deployment block
block:
- name: Stop application
ansible.builtin.systemd:
name: myapp
state: stopped
- name: Deploy new version
ansible.builtin.copy:
src: /tmp/app-v2.jar
dest: /opt/app/app.jar
backup: yes
- name: Start application
ansible.builtin.systemd:
name: myapp
state: started
rescue:
- name: Rollback on failure
ansible.builtin.copy:
src: /opt/app/app.jar.backup
dest: /opt/app/app.jar
remote_src: yes
- name: Restart with old version
ansible.builtin.systemd:
name: myapp
state: started
always:
- name: Log deployment attempt
ansible.builtin.lineinfile:
path: /var/log/deployments.log
line: "Deployment attempted at {{ ansible_date_time.iso8601 }}"
create: yes
- name: Validate deployment
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
tags: ['validate']
Test playbooks with check mode before applying changes:
ansible-playbook deploy.yml --check --diff
Use tags for selective execution:
ansible-playbook deploy.yml --tags validate
The block, rescue, and always structure provides try-catch-finally semantics. Blocks execute tasks sequentially; if any fail, rescue tasks run. Always tasks execute regardless of success or failure, perfect for cleanup or logging.
Ansible playbooks transform infrastructure management from manual processes to reliable, repeatable automation. Start with simple playbooks, organize them into roles as complexity grows, and leverage handlers, conditionals, and error handling for production-grade deployments. The investment in well-structured playbooks pays dividends in reduced deployment time, fewer errors, and infrastructure that’s truly treated as code.