Planning the Deployment Playbook
A typical web application deployment playbook needs to accomplish several distinct phases in order: installing system packages and runtime dependencies (like Python, Node.js, or a JVM), configuring a reverse proxy such as nginx, pulling or copying the application code to the target host, installing application-level dependencies (pip/npm packages), applying database migrations, and finally restarting the application service. Structuring this as an ordered list of roles — common, nginx, app, database — rather than one flat task list makes it possible to reuse the common and nginx roles across many different applications while keeping app-specific logic isolated.
Cricket analogy: This staged deployment is like a well-drilled run chase: first you set the platform (powerplay overs, like installing base packages), then you accelerate in the middle overs (app deployment), and finally you finish strong in the death overs (service restart) — skip a stage and the whole innings collapses.
# site.yml
---
- name: Deploy web application
hosts: webservers
become: true
vars_files:
- group_vars/production/vars.yml
roles:
- common
- nginx
- app
# roles/app/tasks/main.yml
- name: Create app directory
ansible.builtin.file:
path: /opt/myapp
state: directory
owner: appuser
group: appuser
mode: '0755'
- name: Clone application release
ansible.builtin.git:
repo: 'https://github.com/example/myapp.git'
dest: /opt/myapp
version: "{{ app_version }}"
notify: Restart myappTemplating Configuration with Jinja2
Rather than shipping a static nginx.conf or environment file, Ansible's template module renders a Jinja2 .j2 file, substituting variables that can differ per environment — the domain name, upstream port, or database connection string. This means the same nginx.conf.j2 template produces correct configuration for staging, production, and a developer's test host purely by changing the variables in group_vars/<env>/vars.yml, without duplicating the template file itself. Templates also support conditionals and loops, so a single file can render a different upstream block depending on how many app server instances are defined in the inventory.
Cricket analogy: A Jinja2 template is like a single scorecard format used for every match, where only the variables — team names, venue, date — change per game, rather than designing a brand-new scorecard layout for every single fixture.
# roles/nginx/templates/nginx.conf.j2
upstream {{ app_name }}_backend {
{% for host in groups['app_servers'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port }};
{% endfor %}
}
server {
listen 80;
server_name {{ domain_name }};
location / {
proxy_pass http://{{ app_name }}_backend;
proxy_set_header Host $host;
}
}Handlers combined with notify mean nginx only reloads when the rendered template actually changes — if the variables are identical across a re-run, the template task reports no change and the reload handler never fires, keeping the deployment idempotent and avoiding unnecessary downtime.
Achieving Zero-Downtime Rollouts
For applications running behind a load balancer across multiple hosts, the serial keyword controls how many hosts Ansible updates at once, enabling a rolling deployment where only one host (or a percentage) is taken out of rotation at a time. Combined with max_fail_percentage, a rolling deploy automatically halts if too many hosts fail, preventing a bad release from being rolled out to the entire fleet. A typical pattern removes a host from the load balancer pool, deploys the new release, runs a health check, and only re-adds the host to the pool once the health check passes — repeating this one host (or batch) at a time until the whole fleet is updated.
Cricket analogy: The serial keyword is like rotating bowlers through a spell one at a time rather than resting the entire bowling attack simultaneously — you keep enough fielders active on the ground while each bowler takes their turn to be rotated out and refreshed.
---
- name: Rolling deploy of web app
hosts: webservers
become: true
serial: "25%"
max_fail_percentage: 20
tasks:
- name: Remove host from load balancer pool
community.general.haproxy:
state: disabled
host: "{{ inventory_hostname }}"
backend: app_backend
delegate_to: "{{ groups['loadbalancer'][0] }}"
- name: Deploy new release
include_role:
name: app
- name: Wait for health check to pass
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}:{{ app_port }}/healthz"
status_code: 200
retries: 10
delay: 5
register: health
until: health.status == 200
- name: Re-add host to load balancer pool
community.general.haproxy:
state: enabled
host: "{{ inventory_hostname }}"
backend: app_backend
delegate_to: "{{ groups['loadbalancer'][0] }}"Forgetting max_fail_percentage on a rolling deploy means a broken release can march through your entire fleet, host by host, until every server is down — always set a fail threshold so Ansible aborts the play after a small number of hosts fail their health check.
- Structure a deployment as ordered roles (common, nginx, app, database) so pieces are reusable across projects.
- Use the
gitorunarchivemodule to fetch a specific application release, tagged withnotifyhandlers to restart services only on change. - Render environment-specific configuration with Jinja2 templates rather than duplicating static config files per environment.
- Handlers fire only when a watched task reports a change, keeping restarts and reloads minimal and idempotent.
- Use
serialto control rolling deployment batch size andmax_fail_percentageto auto-abort a bad rollout. - Remove a host from the load balancer pool before deploying to it, and re-add it only after a health check passes.
- Always include a health-check step (
urimodule with retries/until) before returning traffic to a freshly deployed host.
Practice what you learned
1. Why is it beneficial to split a web app deployment playbook into roles like common, nginx, and app?
2. What is the purpose of the `serial` keyword in a deployment playbook?
3. What happens when `max_fail_percentage` is exceeded during a rolling deploy?
4. What triggers a handler like 'Restart myapp' to actually run?
5. What is the role of a Jinja2 template like nginx.conf.j2 in a deployment?
Was this page helpful?
You May Also Like
Ansible Best Practices
Field-tested conventions for structuring playbooks, roles, and inventories so Ansible automation stays maintainable, secure, and idempotent as it scales.
Ansible Quick Reference
A condensed cheat sheet of the most-used Ansible CLI commands, playbook keywords, and module patterns for day-to-day automation work.
Ansible vs Other Config Management Tools
A practical comparison of Ansible against Puppet, Chef, SaltStack, and Terraform, covering architecture, agent requirements, and when to pick each.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics