Skip to content

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: restarted

Pattern 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.conf

Pattern 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: reloaded

Pattern 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 app

Pattern 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 app
templates/db.conf.j2
db_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: 3

Pattern 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:

site.yaml
- 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.yaml

The Full Synthesis

Most of the above, combined into one coherent play:

full_deployment.yaml
- 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 itfile first, template/copy second, in that order.
  • Handle OS-specific naming differences explicitly, even when package covers everything else — this is package’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 successful systemd task 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, verify is 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.

Last updated on