# Ansible Architecture and Best Practices

Date: 2025-11-10

A working playbook and a maintainable playbook are two different things. At small scale the difference doesn't matter. At fifty hosts it starts to. At five hundred it determines whether your automation is an asset or a liability. The architecture decisions you make early—how you structure inventory, how you handle secrets, how you separate roles from playbooks—compound in both directions.

<!--more-->

## Recommended Directory Structure

The [Ansible best practices layout](https://docs.ansible.com/ansible/latest/tips_tricks/sample_setup.html) keeps inventory, variables, roles, and playbooks cleanly separated:

```
ansible/
├── inventory/
│   ├── production/
│   │   ├── hosts.yml
│   │   ├── group_vars/
│   │   │   ├── all.yml
│   │   │   ├── webservers.yml
│   │   │   └── dbservers.yml
│   │   └── host_vars/
│   │       └── web01.example.com.yml
│   └── staging/
│       └── hosts.yml
├── roles/
│   └── patching/
│       ├── tasks/
│       │   └── main.yml
│       ├── handlers/
│       │   └── main.yml
│       ├── defaults/
│       │   └── main.yml
│       └── vars/
│           └── main.yml
├── playbooks/
│   ├── patch-linux.yml
│   └── patch-windows.yml
└── ansible.cfg
```

Keep `production/` and `staging/` inventories strictly separate. Avoid a single `hosts` file that mixes environments with comments—it will be wrong within a month.

---

## group_vars and host_vars

Variables in `group_vars/` apply to all hosts in a group. `host_vars/` applies to a single host. Both are loaded automatically when you reference that inventory.

**`inventory/production/group_vars/webservers.yml`:**

```yaml
patch_reboot_allowed: true
patch_schedule: "security"
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/ansible_id
```

**`inventory/production/host_vars/web01.example.com.yml`:**

```yaml
patch_reboot_allowed: false   # this host has no maintenance window yet
```

Host variables always override group variables. Use this for exceptions, not as a primary configuration mechanism.

---

## Roles vs Playbooks

Use **playbooks** as entry points: they define which roles run against which hosts.  
Use **roles** as reusable units of logic: they contain the actual tasks, handlers, defaults, and templates.

```yaml
# playbooks/patch-linux.yml
---
- name: Apply Linux patches
  hosts: linux_servers
  become: true
  roles:
    - patching
```

```yaml
# roles/patching/tasks/main.yml
---
- name: Update package cache
  ansible.builtin.apt:
    update_cache: true
  when: ansible_os_family == "Debian"

- name: Apply security updates
  ansible.builtin.apt:
    upgrade: dist
  when: ansible_os_family == "Debian"
  notify: Reboot if required
```

```yaml
# roles/patching/handlers/main.yml
---
- name: Reboot if required
  ansible.builtin.reboot:
    reboot_timeout: 300
  when: patch_reboot_allowed | bool
```

---

## Idempotency

An idempotent operation produces the same result whether run once or ten times. This is critical for patching: if you re-run a playbook after a partial failure, it should only do what wasn't already done—not undo completed work.

Ansible modules are idempotent by default when used correctly:

```yaml
# Idempotent: will only install if not already present
- ansible.builtin.apt:
    name: openssl
    state: present

# NOT idempotent: runs the command every time regardless
- ansible.builtin.command: apt-get install -y openssl
```

Avoid `command` and `shell` modules for package management. They bypass idempotency entirely and make `--check` mode unreliable.

---

## Ansible Vault: Secrets Management

Never store passwords, API keys, or SSH passphrases in plain text in your repository.

**Encrypt a single variable file:**

```shell
ansible-vault encrypt inventory/production/group_vars/all.yml
```

**Encrypt a single value inline (for embedding in a vars file):**

```shell
ansible-vault encrypt_string 'SuperSecret123!' --name 'become_password'
```

This outputs:

```yaml
become_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  61386637316161...
```

**Running a playbook with a vault password:**

```shell
# Prompt for password interactively
ansible-playbook patch-linux.yml --ask-vault-pass

# Use a password file (for CI/CD — store this in your secrets manager, not the repo)
ansible-playbook patch-linux.yml --vault-password-file ~/.vault_pass
```

---

## The CI/CD Credential Leak Risk

A common mistake is passing vault passwords or SSH keys as environment variables in CI/CD pipelines and then using `-v` verbose mode:

```shell
# Dangerous: -vvv can print variable values including decrypted secrets
ANSIBLE_VAULT_PASSWORD=secret ansible-playbook patch.yml -vvv
```

The safe pattern:

1. Store the vault password in your CI/CD secrets store (GitHub Actions Secrets, GitLab CI Variables, HashiCorp Vault).
2. Write it to a temp file at runtime, not an environment variable.
3. Never run with verbosity above `-v` in CI/CD pipelines that stream logs to persistent storage.

```yaml
# GitHub Actions example
- name: Write vault password
  run: echo "${{ secrets.ANSIBLE_VAULT_PASS }}" > /tmp/.vault_pass && chmod 600 /tmp/.vault_pass

- name: Run playbook
  run: ansible-playbook -i inventory/production patch-linux.yml --vault-password-file /tmp/.vault_pass

- name: Remove vault password file
  if: always()
  run: rm -f /tmp/.vault_pass
```

---

## Using Collections

Ansible collections extend the built-in module library. For patching, `community.general` adds useful modules for reporting and notification.

Install a collection:

```shell
ansible-galaxy collection install community.general
ansible-galaxy collection install ansible.windows
```

Pin versions in `requirements.yml` for reproducible installs:

```yaml
---
collections:
  - name: community.general
    version: ">=9.0.0"
  - name: ansible.windows
    version: ">=2.0.0"
```

Install from requirements:

```shell
ansible-galaxy collection install -r requirements.yml
```

---

**Previous:** [Must-Know Ansible Commands and Core Concepts](/ansible/devops/2025/11/06/ansible-commands-core-concepts.html)

**Next in the series:** [Advanced Ansible Usage for Enterprise Environments](/ansible/devops/2025/11/13/ansible-advanced-enterprise.html) — dynamic inventory, parallelism tuning, error handling, and CI/CD pipeline integration.
