Skip to main content

Anti-Patterns

The following patterns are banned in this codebase. Each one introduces fragility, security risk, or maintenance burden that outweighs any short-term convenience.

Shelling Out Instead of Using Modules

Problem: ansible.builtin.shell and ansible.builtin.command bypass idempotency guarantees and structured error handling.

Fix: Use a purpose-built module. If none exists, write a custom module.

# Bad
- name: Create the application directory
ansible.builtin.shell: mkdir -p /opt/myapp

# Good
- name: Create the application directory
ansible.builtin.file:
path: /opt/myapp
state: directory
mode: "0755"

Hardcoding Secrets in Variables or Templates

Problem: Secrets checked into source control are compromised, even in private repositories.

Fix: Retrieve secrets at runtime from a secrets manager (Vault, AWS Secrets Manager) or use ansible-vault for encrypted variable files.

# Bad
db_password: supersecret123

# Good: retrieved at runtime
- name: Read the database password from Vault
community.hashi_vault.vault_kv2_get:
path: secret/myapp/database
register: vault_secret

Global become: true at the Play Level

Problem: Elevating every task to root increases the blast radius of errors and violates least-privilege principles.

Fix: Set become: false globally in ansible.cfg and enable become only on tasks that require it.

# Bad
- hosts: webservers
become: true
tasks:
- name: Read the application config # Does not need root
...

# Good
- hosts: webservers
tasks:
- name: Install the nginx package
ansible.builtin.package:
name: nginx
state: present
become: true

- name: Read the application config # No become needed
ansible.builtin.slurp:
src: /etc/myapp/config.yml

Ignoring Errors Silently

Problem: ignore_errors: true hides real failures and allows playbooks to continue in a broken state.

Fix: Handle errors explicitly with failed_when, rescue blocks, or block/rescue/always structures.

# Bad
- name: Stop the old service
ansible.builtin.service:
name: old-service
state: stopped
ignore_errors: true

# Good
- name: Stop the old service
ansible.builtin.service:
name: old-service
state: stopped
failed_when: false # Only when the service may legitimately not exist
register: stop_result

- name: Log that the old service was not found
ansible.builtin.debug:
msg: "old-service was not running; skipping"
when: stop_result.failed

Using latest for Package Versions

Problem: state: latest introduces uncontrolled upgrades that can break services during routine playbook runs.

Fix: Pin to a specific version or use state: present for a predictable install.

# Bad
- name: Install nginx
ansible.builtin.package:
name: nginx
state: latest

# Good
- name: Install nginx
ansible.builtin.package:
name: nginx=1.24.*
state: present

Monolithic Playbooks

Problem: Large, single-file playbooks are hard to test, reuse, and maintain.

Fix: Extract logic into roles. Playbooks should be thin wrappers that import roles and set host targets.