Modules Usage Patterns
This closing chapter is patterns, not new syntax — package, file, template, systemd, and lineinfile/blockinfile, combined with everything from the rest of this course, across as many realistic scenarios as reasonably fit in one chapter.
Pattern 1: Install, Configure, Restart — The Core Loop
The most fundamental shape, and the one every other pattern here builds on:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Deploy nginx config from template
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
handlers:
- name: Restart nginx
ansible.builtin.systemd:
name: nginx
state: restartedPattern 2: Preparing Directory Structure Before Deployment
file ensuring the target actually exists, with correct ownership, before anything gets written into it:
- name: Ensure app directory exists
ansible.builtin.file:
path: /etc/myapp
state: directory
owner: alice
group: alice
mode: '0755'
- name: Deploy config into it
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.confPattern 3: OS-Conditional Package Names, Handled Once
The genuine limit of package’s abstraction, from earlier in this section, applied here directly:
- name: Install Apache (Debian)
ansible.builtin.apt:
name: apache2
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: Install Apache (RedHat)
ansible.builtin.dnf:
name: httpd
state: present
when: ansible_facts['os_family'] == 'RedHat'Pattern 4: Surgical Config Tweaks With Graceful Reload
lineinfile for one setting, reloaded instead of restarted since nginx supports it:
- name: Ensure worker_processes is set correctly
ansible.builtin.lineinfile:
path: /etc/nginx/nginx.conf
regexp: '^worker_processes'
line: "worker_processes auto;"
notify: Reload nginx
handlers:
- name: Reload nginx
ansible.builtin.systemd:
name: nginx
state: reloadedPattern 5: Resilient Deployment With Alerting
block/rescue, straight from that section, wrapped around a real deployment instead of a debug stand-in:
- name: Deployment attempt
block:
- name: Deploy config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart app
rescue:
- name: Alert on failure
ansible.builtin.debug:
msg: "Deployment failed on {{ inventory_hostname }}"
changed_when: true
notify: Send alert
handlers:
- name: Restart app
ansible.builtin.systemd:
name: myapp
state: restarted
- name: Send alert
ansible.builtin.debug:
msg: "ALERT: deployment failed on {{ inventory_hostname }}"Pattern 6: Deploying Multiple App Configs In A Loop, One Shared Restart Each
loop over a list of apps, each templated independently, each notifying a shared topic — directly exercising the “handler fires once, no matter how many loop iterations changed” rule from the blocks and handlers section:
vars:
apps:
- name: app1
port: 8081
- name: app2
port: 8082
tasks:
- name: "Deploy config for {{ item.name }}"
ansible.builtin.template:
src: app.conf.j2
dest: "/etc/{{ item.name }}/app.conf"
loop: "{{ apps }}"
notify: "restart services"
handlers:
- name: Restart app1
ansible.builtin.systemd:
name: app1
state: restarted
listen: "restart services"
- name: Restart app2
ansible.builtin.systemd:
name: app2
state: restarted
listen: "restart services"Both app1 and app2 can change in the same loop run, and each listening handler still fires exactly once — the exact behavior demonstrated with debug handlers back in that section, now doing genuine work.
Pattern 7: Deploying An Unknown Number Of Local Config Files
fileglob/query() from the lookups section, discovering files rather than hardcoding a list:
- name: "Copy config: {{ item | basename }}"
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/myapp/conf.d/{{ item | basename }}"
loop: "{{ query('fileglob', 'configs/*.conf') }}"
notify: Restart appPattern 8: Safely Templating A Control-Node Secret
The env lookup’s validation pattern, feeding directly into a template:
vars:
db_password: "{{ lookup('env', 'DB_PASSWORD') }}"
tasks:
- name: Fail if the secret is missing
ansible.builtin.fail:
msg: "DB_PASSWORD is not set on the control node"
when: db_password | length == 0
- name: Deploy database config
ansible.builtin.template:
src: db.conf.j2
dest: /etc/myapp/db.conf
notify: Restart appdb_password = {{ db_password }}Worth noting quote isn’t needed here — that filter matters specifically when building a shell command string for pipe or command, not when writing a value directly into a templated config file. Different problem, different tool.
Pattern 9: Verifying Health After Restart, With Retries
until, from the loops section, confirming a restarted service actually came back up before the play declares success:
- name: Wait for the app to respond after restart
ansible.builtin.command: curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health
register: health_check
until: health_check.stdout == "200"
retries: 10
delay: 3Pattern 10: Organizing It All Into Logical Files
import_tasks, from the includes and imports section, splitting a workflow like this across focused files rather than one long play:
- name: Full Deployment
hosts: servers
tasks:
- name: Install packages
ansible.builtin.import_tasks: install.yaml
- name: Configure application
ansible.builtin.import_tasks: configure.yaml
- name: Verify deployment
ansible.builtin.import_tasks: verify.yamlThe Full Synthesis
Most of the above, combined into one coherent play:
- name: Full Application Deployment
hosts: servers
tasks:
- name: Ensure app directory exists
ansible.builtin.file:
path: /etc/myapp
state: directory
owner: alice
group: alice
mode: '0755'
- name: Install Apache (Debian)
ansible.builtin.apt:
name: apache2
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: Install Apache (RedHat)
ansible.builtin.dnf:
name: httpd
state: present
when: ansible_facts['os_family'] == 'RedHat'
- name: Deployment attempt
block:
- name: "Copy discovered config: {{ item | basename }}"
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/myapp/conf.d/{{ item | basename }}"
loop: "{{ query('fileglob', 'configs/*.conf') }}"
notify: Restart app
- name: Deploy main config from template
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart app
- name: Ensure a specific tuning setting is correct
ansible.builtin.lineinfile:
path: /etc/myapp/app.conf
regexp: '^max_connections'
line: "max_connections = 100"
notify: Restart app
rescue:
- name: Alert on any deployment failure
ansible.builtin.debug:
msg: "Deployment failed on {{ inventory_hostname }}"
changed_when: true
notify: Send alert
- name: Wait for the app to respond after any restart
ansible.builtin.command: curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health
register: health_check
until: health_check.stdout == "200"
retries: 10
delay: 3
handlers:
- name: Restart app
ansible.builtin.systemd:
name: myapp
state: restarted
- name: Send alert
ansible.builtin.debug:
msg: "ALERT: deployment failed on {{ inventory_hostname }}"Directory preparation, OS-conditional installation, discovered config files, a templated main config, a surgical tuning tweak, resilient error handling with alerting, and a post-restart health check — one play, nine chapters’ worth of this course’s tools, doing genuinely coherent work together.
What You Just Did
Ten named patterns, and a synthesis combining most of them: package/apt/dnf from earlier in this section, file for directory prep, template and lineinfile for configuration, systemd for real service management, block/rescue and handlers (including listen and the loop-notification rule) from that section, fileglob/query() and env from lookups, until from loops, and import_tasks from includes and imports. Nothing in this chapter was new mechanics — every single pattern was existing knowledge, applied to modules that finally make the whole thing real instead of illustrative.
Best Practices
- Prepare directory structure before deploying into it —
filefirst,template/copysecond, in that order. - Handle OS-specific naming differences explicitly, even when
packagecovers everything else — this ispackage’s one honest limitation. - Wrap genuinely risky deployment sequences in
block/rescue, and alert distinctly on failure rather than letting a play die silently. - Verify a service actually came back healthy after a restart, with
until, rather than assuming a successfulsystemdtask means the application itself is genuinely ready. - Split a workflow like this across logical files the moment it grows past what’s comfortable to read in one play —
install,configure,verifyis a reasonable default division to start from.
That closes out common modules. Every module here has been standing in for something this course referenced but couldn’t yet demonstrate — real package installs, real file management, real templated configuration, real service restarts, and real targeted edits. Combined with everything from every earlier section, you now have what a genuinely production-capable Ansible playbook actually looks like.