| name | ansible |
| description | [Applies to: **/*] This guide defines definitive best practices for writing maintainable, performant, and robust Ansible code, focusing on modern YAML hygiene, role design, and CI/CD integration. |
| source | cursor_mdc |
ansible Best Practices
Ansible excels when treated as infrastructure-as-code. These guidelines ensure your playbooks, roles, and modules are consistent, idempotent, and production-ready.
1. Code Organization and Structure
Always structure your Ansible content into roles and collections. Roles encapsulate specific functionalities, while collections group related roles, modules, and plugins, promoting reusability and discoverability.
- Role Scoping: Each role or module must focus on a single, well-defined task. Avoid monolithic roles.
❌ BAD:
- name: Install Nginx
ansible.builtin.apt: name=nginx state=present
- name: Configure Nginx
ansible.builtin.template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
- name: Start Nginx
ansible.builtin.service: name=nginx state=started
- name: Install PHP-FPM
ansible.builtin.apt: name=php-fpm state=present
✅ GOOD:
- name: Ensure Nginx is installed and configured
ansible.builtin.include_tasks: setup.yml
- name: Ensure Nginx service is running
ansible.builtin.service: name=nginx state=started enabled=true
- name: Ensure PHP-FPM is installed and configured
ansible.builtin.include_tasks: setup.yml
- Fact Modules: Create dedicated
_info or _facts modules/tasks for gathering information, rather than overloading existing ones.
✅ GOOD:
- name: Gather custom system facts
ansible.builtin.set_fact:
custom_os_version: "{{ ansible_facts['distribution_major_version'] }}"
2. YAML Hygiene and Syntax
Enforce strict YAML formatting. Consistent syntax improves readability and prevents subtle parsing errors.
- File Start: All YAML files must begin with
---.
- Indentation: Use two spaces for indentation, never tabs.
- Quotes: Quote strings only when necessary (e.g., when they contain special characters or Jinja map references). Prefer single quotes. Use double quotes for escaping characters or multi-line strings.
❌ BAD:
- name: "Install a package"
ansible.builtin.apt: name="my-package" state="present"
- name: Set fact
set_fact:
myvar: '{{ item["key"] }}'
✅ GOOD:
---
- name: Install a package
ansible.builtin.apt:
name: my-package
state: present
- name: Set fact
ansible.builtin.set_fact:
my_var: "{{ item['key'] }}"
- name: Print multi-line message
ansible.builtin.debug:
msg: |
This is line one.
This is line two.
- Colon Spacing: Use exactly one space after a colon in key-value pairs.
❌ BAD:
- name : start service
service:
name : my_service
state : started
✅ GOOD:
- name: start service
ansible.builtin.service:
name: my_service
state: started
- Structured Map Style: Always use the structured map style for module parameters, not the legacy
key=value style.
❌ BAD:
- name: Create directory
ansible.builtin.file: path=/opt/app state=directory mode=0755
✅ GOOD:
- name: Create directory
ansible.builtin.file:
path: /opt/app
state: directory
mode: '0755'
- Booleans: Standardize on
true/false.
❌ BAD:
- name: Enable service
ansible.builtin.service: name=my_service enabled=yes
become: True
✅ GOOD:
- name: Enable service
ansible.builtin.service: name=my_service enabled=true
become: true
3. Naming Conventions
Adopt consistent snake_case naming. This applies to variables, tasks, files, and role names. Prefix role names with the collection namespace to prevent collisions.
4. Idempotency and Declarative Style
Write idempotent tasks. Running a playbook multiple times should yield the same result without unintended side effects. Favor declarative modules over imperative command or shell.
- Declarative Modules:
❌ BAD:
- name: Check if file exists and create if not
ansible.builtin.shell: |
if [ ! -f /etc/myapp.conf ]; then
cp /etc/myapp.conf.template /etc/myapp.conf
fi
args:
creates: /etc/myapp.conf
✅ GOOD:
- name: Ensure myapp.conf exists
ansible.builtin.copy:
src: myapp.conf.template
dest: /etc/myapp.conf
owner: root
group: root
mode: '0644'
backup: true
- Atomic File Operations: Always use
atomic_move (implicitly via copy, template modules) for file modifications to prevent data corruption.
5. Configuration Management
Manage configurations with templates and structured variables.
- Templates: Use Jinja2 templates (
template module) for dynamic configuration files.
- Variables: Define role variables in
defaults/main.yml (overridable) and vars/main.yml (not overridable by inventory/extra-vars). Prefer inventory variables for host-specific data.
❌ BAD:
- hosts: webservers
vars:
nginx_port: 8080
tasks:
- name: Configure Nginx
ansible.builtin.template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
✅ GOOD:
nginx_port: 80
nginx_port: 8080
listen {{ nginx_port }};
6. Performance Considerations
Optimize playbook execution for speed.
7. Error Handling and Logging
Implement robust error handling and informative logging.
- Fail Gracefully: Use
fail_json (in custom modules) or ansible.builtin.fail for clear error messages. Avoid raw tracebacks.
✅ GOOD:
- name: Ensure critical variable is defined
ansible.builtin.fail:
msg: "Required variable 'app_version' is not defined."
when: app_version is not defined
- Debug Verbosity: Use the
ansible.builtin.debug module with verbosity levels for controlled output.
✅ GOOD:
- name: Show debug info
ansible.builtin.debug:
var: my_variable
when: ansible_verbosity >= 2
8. Testing and Linting
Integrate linting and testing into your development workflow.
9. CI/CD Integration
Embed Ansible into your automated CI/CD pipelines.
- Git Triggers: Trigger playbook runs on Git pushes to automatically provision or update infrastructure.
- Collections: Store reusable roles and modules in Ansible Collections (Galaxy or private registries) for easy dependency management.
- Pipeline Checks: Include
ansible-lint and collection dependency checks in your CI pipeline.
✅ GOOD:
name: Ansible CI/CD
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible and dependencies
run: pip install ansible ansible-lint
- name: Run Ansible Lint
run: ansible-lint .
deploy:
runs-on: ubuntu-latest
needs: lint
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Install Ansible Collections
run: ansible-galaxy collection install -r requirements.yml
- name: Run Playbook
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
run: ansible-playbook -i inventory/prod.yml site.yml --vault-password-file /dev/stdin