# Advanced Ansible Usage for Enterprise Environments

Date: 2025-11-13

Static inventory works until your infrastructure is defined somewhere else—a CMDB, a cloud provider, a service registry. At that point maintaining a hand-edited hosts file is a compliance problem as much as an operational one. Enterprise Ansible means dynamic inventory, tuned parallelism, robust error handling, and pipelines that can patch five hundred nodes without manual intervention.

<!--more-->

## Dynamic Inventory

Dynamic inventory scripts or plugins query an external source at runtime and return a JSON structure Ansible treats as a normal inventory.

**AWS EC2 (using the built-in plugin):**

`inventory/aws_ec2.yml`:

```yaml
plugin: amazon.aws.aws_ec2
regions:
  - eu-west-1
  - us-east-1
filters:
  instance-state-name: running
  tag:Environment: production
keyed_groups:
  - key: tags.Role
    prefix: role
  - key: tags.OS
    prefix: os
compose:
  ansible_host: public_ip_address
```

Run against it directly:

```shell
ansible-inventory -i inventory/aws_ec2.yml --list
ansible-playbook -i inventory/aws_ec2.yml patch-linux.yml
```

**Custom CMDB or API source** — write a script that outputs JSON conforming to the [Ansible dynamic inventory format](https://docs.ansible.com/ansible/latest/dev_guide/developing_inventory.html) and mark it executable. Ansible calls it automatically.

```shell
chmod +x inventory/cmdb_inventory.py
ansible-playbook -i inventory/cmdb_inventory.py patch-linux.yml
```

---

## Parallelism and Forks Tuning

By default Ansible runs against 5 hosts in parallel. For large environments, increase this in `ansible.cfg`:

```ini
[defaults]
forks = 20
```

Or override at runtime:

```shell
ansible-playbook patch-linux.yml -f 50
```

For patching specifically, use `serial` to control rollout batch size regardless of forks:

```yaml
- name: Rolling patch across web tier
  hosts: webservers
  become: true
  serial: 10          # patch 10 hosts at a time
  # serial: "20%"     # or patch 20% of the group at a time
  roles:
    - patching
```

`serial` is your primary tool for staggered patching. It ensures that if a batch fails, the remaining hosts are untouched.

---

## Error Handling

**`ignore_errors`** — continue the play even if a task fails (use sparingly; failures should usually stop execution):

```yaml
- name: Try optional post-patch script
  ansible.builtin.command: /opt/scripts/post-patch-hook.sh
  ignore_errors: true
```

**`failed_when`** — define what failure means for a task:

```yaml
- name: Check if reboot is required
  ansible.builtin.command: needs-restarting -r
  register: reboot_check
  failed_when: reboot_check.rc not in [0, 1]  # 0 = no, 1 = yes, anything else is real failure
  changed_when: false
```

**`block` / `rescue` / `always`** — structured try/catch/finally:

```yaml
- block:
    - name: Apply patches
      ansible.builtin.apt:
        upgrade: dist

    - name: Run post-patch validation
      ansible.builtin.command: /opt/validate.sh

  rescue:
    - name: Alert on failure
      ansible.builtin.uri:
        url: "{{ slack_webhook_url }}"
        method: POST
        body_format: json
        body:
          text: "Patch failed on {{ inventory_hostname }}"

  always:
    - name: Log result regardless of outcome
      ansible.builtin.lineinfile:
        path: /var/log/ansible-patch.log
        line: "{{ inventory_hostname }} patched at {{ ansible_date_time.iso8601 }}"
        create: true
```

---

## Conditional Execution

Patch only hosts meeting specific criteria:

```yaml
- name: Patch only if uptime exceeds 30 days
  ansible.builtin.apt:
    upgrade: dist
  when: (ansible_uptime_seconds | int) > (30 * 86400)

- name: Patch only if kernel is outdated
  ansible.builtin.apt:
    upgrade: dist
  when: ansible_kernel != kernel_target_version

- name: Patch only Debian-family hosts
  ansible.builtin.apt:
    upgrade: dist
  when: ansible_os_family == "Debian"
```

---

## Handlers and Service Restarts

Handlers run once at the end of a play, only if notified by a task that made a change. This prevents unnecessary restarts:

```yaml
tasks:
  - name: Apply patches
    ansible.builtin.apt:
      upgrade: dist
    notify:
      - Restart nginx
      - Restart sshd

handlers:
  - name: Restart nginx
    ansible.builtin.service:
      name: nginx
      state: restarted

  - name: Restart sshd
    ansible.builtin.service:
      name: ssh
      state: restarted
```

If the patch task reports `changed: false` (nothing to update), the handlers never fire. If the play fails mid-run, use `--force-handlers` to run handlers anyway during cleanup.

---

## CI/CD Integration

**GitHub Actions:**

```yaml
name: Weekly Patch Run

on:
  schedule:
    - cron: '0 2 * * 0'   # every Sunday at 02:00 UTC
  workflow_dispatch:        # allow manual trigger

jobs:
  patch:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Ansible
        run: pip install ansible

      - name: Install collections
        run: ansible-galaxy collection install -r requirements.yml

      - name: Write vault password
        run: echo "${{ secrets.VAULT_PASS }}" > /tmp/.vault && chmod 600 /tmp/.vault

      - name: Write SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.ANSIBLE_SSH_KEY }}" > ~/.ssh/ansible_id
          chmod 600 ~/.ssh/ansible_id

      - name: Run patch playbook
        run: |
          ansible-playbook -i inventory/production/hosts.yml \
            playbooks/patch-linux.yml \
            --vault-password-file /tmp/.vault \
            --limit staging_first   # always run against a canary group first

      - name: Cleanup secrets
        if: always()
        run: rm -f /tmp/.vault ~/.ssh/ansible_id
```

**GitLab CI:**

```yaml
patch_linux:
  stage: deploy
  image: python:3.12
  before_script:
    - pip install ansible
    - ansible-galaxy collection install -r requirements.yml
    - echo "$VAULT_PASS" > /tmp/.vault && chmod 600 /tmp/.vault
  script:
    - ansible-playbook -i inventory/production/hosts.yml playbooks/patch-linux.yml
        --vault-password-file /tmp/.vault
  after_script:
    - rm -f /tmp/.vault
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
```

---

**Previous:** [Ansible Architecture and Best Practices](/ansible/devops/2025/11/10/ansible-architecture-best-practices.html)

**Next in the series:** [Linux Server Patching with Ansible](/ansible/devops/2025/11/17/ansible-linux-server-patching.html) — apt vs dnf vs zypper, kernel patching, reboot handling, and rolling update workflows.
