Why Ansible Needs Specific AI Rules
Ansible's declarative, agentless model makes it one of the most accessible automation tools — but AI assistants treat it like a Bash script runner. Instead of using Ansible's built-in modules (which are idempotent, cross-platform, and well-tested), the AI generates shell and command tasks with raw commands. The result is playbooks that work once but break on re-run, aren't portable, and can't be safely reviewed.
The most common AI failures: using shell/command modules instead of purpose-built modules (apt, yum, copy, template, service), hardcoding secrets in playbooks instead of using Ansible Vault, creating monolithic playbooks instead of roles, ignoring handlers for service restarts, and writing tasks that aren't idempotent.
Ansible's power is in its modules and its idempotency guarantee: 'run this playbook 10 times and the result is the same as running it once.' AI-generated shell tasks break this guarantee because shell commands are inherently non-idempotent unless carefully written.
Rule 1: Use Modules, Not Shell Commands
The rule: 'Always use Ansible's built-in modules instead of shell or command. Use apt/yum/dnf for package management, copy/template for file management, service/systemd for service control, user/group for user management, file for permissions, and lineinfile/blockinfile for file editing. Only use shell or command when no module exists for the operation — and document why.'
For common translations: 'Instead of shell: apt-get install -y nginx, use: apt: name=nginx state=present. Instead of shell: cp /tmp/config /etc/app.conf, use: copy: src=config dest=/etc/app.conf. Instead of shell: systemctl restart nginx, use handlers with service: name=nginx state=restarted.'
Modules are idempotent by design — apt: name=nginx state=present doesn't reinstall nginx if it's already installed. Shell: apt-get install -y nginx runs every time, wastes time, and might change behavior if a new version is available.
- apt/yum/dnf for packages — not shell: apt-get install
- copy/template for files — not shell: cp
- service/systemd for services — not shell: systemctl
- user/group for user management — not shell: useradd
- file module for permissions — not shell: chmod/chown
- shell/command only when no module exists — always document why
apt: name=nginx state=present doesn't reinstall if present. shell: apt-get install nginx runs every time. Modules are idempotent by design — shell commands are not. Always prefer modules.
Rule 2: Idempotent Tasks
The rule: 'Every task must be idempotent — running the playbook multiple times produces the same result as running it once. For shell/command tasks that must exist, use creates: or removes: to make them idempotent. Use changed_when and failed_when to control task status accurately. Use check mode (--check) to verify idempotency without making changes.'
For testing idempotency: 'Run the playbook twice in CI. The second run should report zero changed tasks. If any task reports changed on the second run, it's not idempotent — fix it. Use Molecule for testing Ansible roles in isolated environments.'
AI-generated shell tasks almost never include creates/removes guards. Without them, every run re-executes every shell command — downloading files that already exist, restarting services unnecessarily, and making playbook runs slow and unpredictable.
Rule 3: Ansible Vault for All Secrets
The rule: 'Never put secrets (passwords, API keys, certificates, private keys) in plaintext in playbooks, inventory, or variable files. Use Ansible Vault for all secrets: ansible-vault encrypt_string to encrypt individual values, ansible-vault encrypt to encrypt entire files. Store the vault password in a password manager or CI secret — never in the repository.'
For variable organization: 'Keep secrets in group_vars/all/vault.yml (encrypted). Keep non-secret variables in group_vars/all/vars.yml (plaintext). Prefix vault variables with vault_: vault_db_password in vault.yml, referenced as db_password: "{{ vault_db_password }}" in vars.yml. This separation makes it clear which values are secret.'
AI assistants put secrets directly in variable files because it's the simplest approach. One rule forces Vault usage, preventing secrets from entering git history.
AI puts passwords directly in variable files. One rule: Ansible Vault for all secrets. Use vault_ prefix in encrypted files, reference in plaintext vars. Secrets never enter git history.
Rule 4: Role-Based Organization
The rule: 'Organize playbooks into roles for reusability and clarity. Each role has: tasks/main.yml (task list), handlers/main.yml (handler list), templates/ (Jinja2 templates), files/ (static files), defaults/main.yml (default variables), vars/main.yml (role variables), and meta/main.yml (role metadata and dependencies). Never create monolithic playbooks with 200+ tasks.'
For playbook structure: 'The top-level playbook (site.yml) includes roles: - role: nginx, - role: app, - role: monitoring. Each role is self-contained and testable independently. Use ansible-galaxy init to create the standard role directory structure.'
For role reuse: 'Parameterize roles with variables in defaults/main.yml. Consumers override defaults in their playbook variables. Publish internal roles to a private Galaxy server or Git repository. Pin role versions in requirements.yml for reproducible deployments.'
- roles/ directory with standard structure: tasks, handlers, templates, defaults, meta
- site.yml composes roles — each role is self-contained and testable
- defaults/main.yml for configurable parameters — vars/main.yml for internals
- ansible-galaxy init for standard structure — requirements.yml for dependencies
- No monolithic playbooks — max 30 tasks per role's tasks/main.yml
Rule 5: Handlers for Service Management
The rule: 'Use handlers for service restarts and reloads — never restart services directly in tasks. Handlers only run when notified, and only run once even if notified multiple times. Pattern: a task that modifies a config file notifies: restart nginx, the handler restarts the service at the end of the play.'
For handler patterns: 'Define handlers in handlers/main.yml. Name handlers descriptively: restart nginx, reload systemd, restart application. Use listen for grouping multiple handlers under one notification name. Use flush_handlers when you need a handler to run immediately (before the next task).'
AI assistants add service: name=nginx state=restarted as a regular task after every config change. This restarts the service on every playbook run, even if the config didn't change. Handlers only restart when the config actually changes — the correct, idempotent pattern.
AI adds 'service: state=restarted' as a regular task — it restarts on every run. Handlers only restart when notified by a changed task. Same result, but only when the config actually changes.
Complete Ansible Rules Template
Consolidated rules for Ansible playbooks and roles.
- Modules over shell: apt, copy, template, service — shell only when no module exists
- Idempotent tasks: creates/removes guards, changed_when/failed_when, test with --check
- Ansible Vault for all secrets — vault_ prefix, separate vault.yml from vars.yml
- Role-based organization: standard directory structure, parameterized defaults
- Handlers for service restarts — notify on config change, never direct restart tasks
- Jinja2 templates over lineinfile for complex config — template module for all configs
- Inventory: group_vars and host_vars — never hardcode hosts in playbooks
- ansible-lint in CI — Molecule for role testing — zero lint warnings policy