Best Practices
Idempotency
Section titled “Idempotency”The most important principle in Ansible: running a playbook twice should produce the same result as running it once. No unintended side effects, no duplicate changes.
What Makes a Task Idempotent
Section titled “What Makes a Task Idempotent”Most built-in modules are idempotent by design:
# Idempotent — apt checks if nginx is already installed- name: Install nginx apt: name: nginx state: present
# Idempotent — service checks if nginx is already running- name: Start nginx service: name: nginx state: startedWhat Breaks Idempotency
Section titled “What Breaks Idempotency”command and shell modules are not idempotent — they always run and always report changed:
# NOT idempotent — runs every time- name: Create database command: createdb myapp
# Fix: add a condition- name: Check if database exists command: psql -lqt register: db_list changed_when: false
- name: Create database if missing command: createdb myapp when: "'myapp' not in db_list.stdout"Idempotency Helpers
Section titled “Idempotency Helpers”creates— Skip if a file already exists:
- name: Run initial setup command: /opt/myapp/setup.sh args: creates: /opt/myapp/.initializedchanged_when: false— For read-only commands that never change state.state: present/state: absent— Most modules use state to be declarative.
Tagging Tasks
Section titled “Tagging Tasks”Tags let you run only specific parts of a playbook:
tasks: - name: Install packages apt: name: - nginx - curl state: present tags: [packages, setup]
- name: Deploy application copy: src: app/ dest: /opt/myapp/ tags: [deploy]
- name: Run database migrations command: /opt/myapp/migrate.sh tags: [deploy, database]ansible-playbook site.yml --tags deploy # only deploy tasksansible-playbook site.yml --tags "deploy,database" # deploy + databaseansible-playbook site.yml --skip-tags packages # everything except packagesansible-playbook site.yml --list-tags # show all available tagsSpecial Tags
Section titled “Special Tags”always— Task runs no matter what tags are selected.never— Task only runs if explicitly included with--tags.
- name: Debug info (only when asked) debug: msg: "{{ ansible_facts }}" tags: [never, debug]
- name: Send notification (always runs) slack: msg: "Deploy complete" tags: [always]Ansible Vault
Section titled “Ansible Vault”Encrypt sensitive data (passwords, API keys, certificates) that lives alongside your playbooks.
Encrypting a File
Section titled “Encrypting a File”ansible-vault encrypt group_vars/production/secrets.ymlansible-vault decrypt group_vars/production/secrets.ymlansible-vault edit group_vars/production/secrets.yml # decrypt, edit, re-encryptansible-vault view group_vars/production/secrets.yml # view without decrypting to diskEncrypted Variables File
Section titled “Encrypted Variables File”# group_vars/production/secrets.yml (encrypted)db_password: "s3cur3_p4ssw0rd"api_key: "ak_live_abc123"ssl_private_key: | -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----Inline Encrypted Variables
Section titled “Inline Encrypted Variables”Encrypt a single value instead of an entire file:
ansible-vault encrypt_string 's3cur3_p4ssw0rd' --name 'db_password'Output (paste into your vars file):
db_password: !vault | $ANSIBLE_VAULT;1.1;AES256 3565396...Using Vault in Playbook Runs
Section titled “Using Vault in Playbook Runs”ansible-playbook site.yml --ask-vault-pass # prompt for passwordansible-playbook site.yml --vault-password-file ~/.vault_pass # read from fileIn CI/CD, store the vault password as a secret and pass it via file or environment variable:
echo "$VAULT_PASSWORD" > /tmp/.vault_passansible-playbook site.yml --vault-password-file /tmp/.vault_passVault Best Practices
Section titled “Vault Best Practices”- Separate secrets into their own file (
secrets.yml) alongside non-sensitive vars — easier to manage. - Use vault IDs when you have different passwords for different environments:
ansible-vault encrypt --vault-id prod@prompt group_vars/production/secrets.ymlansible-playbook site.yml --vault-id prod@~/.vault_pass_prodTesting With Molecule
Section titled “Testing With Molecule”Molecule is the standard testing framework for Ansible roles. It creates ephemeral instances (Docker, Vagrant, cloud), runs your role, and verifies the result.
Install
Section titled “Install”pip install molecule molecule-dockerInitialize
Section titled “Initialize”cd roles/nginxmolecule init scenario --driver-name dockerCreates:
molecule/ default/ molecule.yml # test config converge.yml # playbook that applies the role verify.yml # assertionsmolecule.yml
Section titled “molecule.yml”driver: name: dockerplatforms: - name: instance image: ubuntu:22.04 pre_build_image: trueprovisioner: name: ansibleverifier: name: ansibleconverge.yml
Section titled “converge.yml”---- name: Converge hosts: all become: true roles: - role: nginx vars: nginx_port: 8080verify.yml
Section titled “verify.yml”---- name: Verify hosts: all tasks: - name: Check nginx is installed command: nginx -v register: result changed_when: false failed_when: result.rc != 0
- name: Check nginx is running service_facts:
- name: Assert nginx is active assert: that: - "'nginx.service' in ansible_facts.services" - "ansible_facts.services['nginx.service'].state == 'running'"Running Tests
Section titled “Running Tests”molecule test # full cycle: create → converge → verify → destroymolecule converge # just run the role (keep the instance)molecule verify # run verification onlymolecule login # SSH into the test instance for debuggingmolecule destroy # tear downMolecule in CI
Section titled “Molecule in CI”# GitHub Actions- name: Run Molecule tests run: | pip install molecule molecule-docker cd roles/nginx molecule testProject Layout
Section titled “Project Layout”Small Project
Section titled “Small Project”ansible/ inventory/ hosts.yml group_vars/ all.yml webservers.yml host_vars/ web1.example.com.yml playbooks/ site.yml webservers.yml dbservers.yml roles/ nginx/ postgresql/ myapp/ files/ templates/ ansible.cfg requirements.ymlLarge Project (Multi-Environment)
Section titled “Large Project (Multi-Environment)”ansible/ inventories/ dev/ hosts.yml group_vars/ all.yml all/ secrets.yml # vault encrypted host_vars/ staging/ hosts.yml group_vars/ production/ hosts.yml group_vars/ playbooks/ site.yml deploy.yml rollback.yml roles/ requirements.yml # Galaxy roles internal/ nginx/ myapp/ ansible.cfgRun per environment:
ansible-playbook -i inventories/production/hosts.yml playbooks/site.ymlansible-playbook -i inventories/dev/hosts.yml playbooks/deploy.ymlansible.cfg
Section titled “ansible.cfg”Set project-wide defaults:
# ansible.cfg[defaults]inventory = ./inventory/hosts.ymlroles_path = ./rolesretry_files_enabled = falsestdout_callback = yaml # readable output formattimeout = 30forks = 20 # parallel connections
[privilege_escalation]become = truebecome_method = sudobecome_user = root
[ssh_connection]pipelining = true # faster executioncontrol_path_dir = /tmp/.ansible/cpPlace ansible.cfg in your project root. Ansible finds it automatically.
CI/CD Integration
Section titled “CI/CD Integration”# GitHub Actionsname: Ansible Deployon: push: branches: [main] paths: ["ansible/**"]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Ansible run: pip install ansible
- name: Install Galaxy roles run: ansible-galaxy install -r ansible/roles/requirements.yml
- name: Run playbook env: ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }} run: | echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/.vault_pass ansible-playbook \ -i ansible/inventories/production/hosts.yml \ ansible/playbooks/deploy.yml \ --vault-password-file /tmp/.vault_pass rm /tmp/.vault_passTroubleshooting and Debugging
Section titled “Troubleshooting and Debugging”Verbosity Levels
Section titled “Verbosity Levels”ansible-playbook site.yml -v # task resultsansible-playbook site.yml -vv # task input parametersansible-playbook site.yml -vvv # SSH connection detailsansible-playbook site.yml -vvvv # full connection + plugin debuggingStart with -v, increase only if needed. -vvv is usually enough to diagnose SSH and permission issues.
The debug Module
Section titled “The debug Module”Print variables and expressions mid-play:
- name: Show all facts debug: var: ansible_facts
- name: Show a specific variable debug: msg: "App port is {{ app_port }}, host is {{ ansible_host }}"
- name: Show registered output debug: var: deploy_result.stdout_linesCommon Errors and Fixes
Section titled “Common Errors and Fixes”| Error | Cause | Fix |
|---|---|---|
Unreachable | SSH can’t connect | Check IP, port, SSH key, security groups, firewall |
Permission denied | Wrong SSH user or key | Set ansible_user and ansible_ssh_private_key_file |
Missing sudo password | become: true but no password | Add --ask-become-pass or configure passwordless sudo |
module not found | Module not installed | Install the collection: ansible-galaxy collection install ... |
/usr/bin/python: not found | Python missing on target | Set ansible_python_interpreter: /usr/bin/python3 or install Python |
Shared connection closed | SSH timeout on slow tasks | Increase timeout in ansible.cfg or use async |
variable undefined | Typo or missing var | Check spelling, check group_vars//host_vars/, use ` |
Testing Connectivity
Section titled “Testing Connectivity”ansible all -m ping # test SSH to all hostsansible web1.example.com -m ping -vvv # detailed connection infoansible all -m setup -a "filter=ansible_distribution" # test facts gatheringStep Mode
Section titled “Step Mode”Run tasks one at a time, confirming each:
ansible-playbook site.yml --stepAnsible prompts before each task: (N)o/(y)es/(c)ontinue.
Start at a Specific Task
Section titled “Start at a Specific Task”Resume from a specific task (e.g. after fixing an error):
ansible-playbook site.yml --start-at-task="Deploy application"Checking Syntax
Section titled “Checking Syntax”ansible-playbook site.yml --syntax-check # parse without runningansible-lint site.yml # lint for best practicesansible-lint catches style issues, deprecated syntax, and risky patterns.
Performance Tuning
Section titled “Performance Tuning”Forks (Parallelism)
Section titled “Forks (Parallelism)”By default, Ansible runs tasks on 5 hosts in parallel. Increase for larger inventories:
# ansible.cfg[defaults]forks = 20Or per-run:
ansible-playbook site.yml -f 50More forks = more parallel SSH connections = faster execution. Limit by your control node’s CPU/memory and target network capacity.
Pipelining
Section titled “Pipelining”Reduces SSH operations by executing modules without copying them to a temp file:
# ansible.cfg[ssh_connection]pipelining = trueRequires requiretty to be disabled in /etc/sudoers on the targets (most modern systems don’t set it). Gives a significant speedup.
SSH Multiplexing
Section titled “SSH Multiplexing”Ansible uses SSH ControlPersist by default — one SSH connection is reused for multiple tasks on the same host. Make sure it’s enabled:
# ansible.cfg[ssh_connection]ssh_args = -o ControlMaster=auto -o ControlPersist=60scontrol_path_dir = /tmp/.ansible/cpFact Caching
Section titled “Fact Caching”Fact gathering runs at the start of every play and can be slow with many hosts. Cache facts between runs:
# ansible.cfg[defaults]gathering = smart # only gather if cache is expiredfact_caching = jsonfilefact_caching_connection = /tmp/ansible_fact_cachefact_caching_timeout = 3600 # cache for 1 hourOr use Redis for shared caching across CI jobs:
fact_caching = redisfact_caching_connection = localhost:6379:0fact_caching_timeout = 3600Selective Fact Gathering
Section titled “Selective Fact Gathering”Gather only what you need:
- name: Quick play hosts: webservers gather_facts: true gather_subset: - network - hardware # skip: distribution, virtual, ohai, facterOr disable entirely with gather_facts: false if you don’t use any facts.
Mitogen Strategy Plugin
Section titled “Mitogen Strategy Plugin”Mitogen is a third-party strategy plugin that dramatically speeds up Ansible by replacing SSH with a more efficient transport:
# ansible.cfg[defaults]strategy_plugins = /path/to/mitogen/ansible_mitogen/plugins/strategystrategy = mitogen_linearReported speedups of 1.25x–7x depending on the workload. Drop-in replacement — no playbook changes needed.
Performance Checklist
Section titled “Performance Checklist”| Setting | Default | Recommended |
|---|---|---|
forks | 5 | 20–50 (match your infra size) |
pipelining | false | true |
gathering | implicit | smart (with caching) |
fact_caching | memory | jsonfile or redis |
ControlPersist | 60s | 60s (already good) |
| Strategy | linear | mitogen_linear (if available) |
Key Takeaways
Section titled “Key Takeaways”- Idempotency first — Prefer declarative modules over
command/shell. Usecreates,changed_when, and conditional checks. - Tag tasks so you can run subsets of a playbook (
--tags deploy,--skip-tags debug). - Encrypt secrets with Ansible Vault. Store the vault password in CI/CD secrets, never in code.
- Test roles with Molecule — create, converge, verify, destroy. Run in CI.
- Organize by environment — separate inventories with their own
group_vars/and secrets. - Set sane defaults in
ansible.cfg(pipelining, forks, yaml output). - Debug with
-v/-vv/-vvv,debugmodule,--step, and--start-at-task. - Tune performance with higher forks, pipelining, fact caching, and Mitogen.