ansible
Updated: November 24, 2025
Ansible is a configuration tool, that can setup and administer multiple machines on a network.
Note: Ansible only needs to be installed on a controller node.
Table of Contents
- SSH Keys
- Install
- Create User
- Config
- Vault
- Inventory
- Collections
- Variables
- Facts
- Conditionals
- Loops
- Templates
- ADHOC Commands
- Commands
- Includes
- Galaxy
- Roles
- Playbooks
- Copying Files
- Webserver
- Handlers
- Secrets
- Setup Database
- Best Practices
- Troubleshooting
- Ansible Lint
SSH KEYS
For ansible to work ssh access is required for the machines you want to config and admin.
INSTALL
Ansible needs to be installed only on the control machine (the machine from which you run Ansible commands). Managed nodes require no Ansible installation, just SSH access and Python (version 2.6+ or 3.5+).
Ansible versions: As of 2024, Ansible Core (formerly Ansible) is at version 2.16.x. For enterprise features, consider AWX (open-source Tower) or Red Hat Ansible Automation Platform.
Ubuntu/Debian
sudo apt update
sudo apt install ansible
Note: Ubuntu packages may lag behind; for latest version, use pip.
macOS
brew install ansible
Via pip (recommended for latest version)
pip install ansible
# Or for user-specific install
pip install --user ansible
Requires Python 3.8+ for Ansible 2.10+.
Other platforms
For Windows (as control machine), use WSL or pip in a virtual environment. For more options, see official installation guide.
After installation, verify with ansible --version.
Create User
Create an ansible user for carrying out ansible playbooks. This user helps to distinguish if it was ansible that did something in the logs from all other users – even root.
- user will need to be created on every node and controller.
- needs to be sudoer and use without password on all managed nodes.
- must be able to ssh from control to managed node without a password.
useradd -m ansible # create ansible user with home directory
usermod -s /bin/bash ansible # set users shell
su - ansible # switch to ansible user
Edit visudo and search for with /wheel First set editor for visudo
EDITOR=vim visudo
## Samthing without a password
# %wheel ALL=(ALL) NOPASSWD: ALL
ansible ALL=(ALL) NOPASSWD: ALL
CONFIG
Ansible configuration is managed via ansible.cfg files. The default is /etc/ansible/ansible.cfg, but you can create project-specific configs to avoid affecting global settings.
Configuration precedence (highest to lowest):
ANSIBLE_CONFIGenvironment variable pointing to a file./ansible.cfg(current directory)~/.ansible.cfg(home directory)/etc/ansible/ansible.cfg
Copy the default config and customize it:
cp /etc/ansible/ansible.cfg ./ansible.cfg
Common configuration options:
[defaults]
inventory = ./inventory.ini # Path to inventory file
remote_user = ansible # Default SSH user
private_key_file = ~/.ssh/id_rsa # SSH private key
forks = 5 # Number of parallel processes
nocows = 1 # Disable cowsay (set to 0 to enable)
retry_files_enabled = True # Create .retry files on failure
retry_files_save_path = ~/.ansible-retry # Where to save retry files
collections_paths = ~/.ansible/collections # Path for collections
roles_path = ~/.ansible/roles # Path for roles
host_key_checking = False # Disable SSH host key checking (for dev)
deprecation_warnings = False # Hide deprecation warnings
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/ansible-ssh-%h-%p-%r
pipelining = True # Speed up execution by reducing SSH calls
For a full list of options, see ansible.cfg documentation.
VAULT
Ansible Vault encrypts sensitive data like passwords, API keys, and certificates. Encrypted files can be committed to version control safely.
Basic commands:
# Create a new encrypted file
ansible-vault create secrets.yml
# Encrypt an existing file
ansible-vault encrypt secrets.yml
# Edit an encrypted file (opens in editor)
ansible-vault edit secrets.yml
# View encrypted file contents
ansible-vault view secrets.yml
# Decrypt a file (removes encryption)
ansible-vault decrypt secrets.yml
# Change vault password
ansible-vault rekey secrets.yml
# Encrypt a string and output to stdout
ansible-vault encrypt_string --stdin-name 'db_password'
Using in playbooks:
# Prompt for password at runtime
ansible-playbook playbook.yml --ask-vault-pass
# Use a password file
ansible-playbook playbook.yml --vault-password-file ~/.vault_pass
# Use multiple vaults (vault IDs)
ansible-vault create --vault-id dev@prompt dev_secrets.yml
ansible-playbook playbook.yml --vault-id dev@prompt --vault-id prod@prompt
Best practices:
- Store vault passwords in a secure location (e.g., password manager, not in repo)
- Use vault IDs to separate environments (dev, prod)
- Encrypt entire variable files rather than individual vars
- Use
ansible-vault encrypt_stringfor inline secrets in playbooks
INVENTORY FILES
Inventory files define the hosts Ansible manages. They can be static (INI/YAML) or dynamic (scripts/plugins).
Static inventory (INI format):
# inventory.ini
[webservers]
web1.example.com ansible_user=ubuntu
web2.example.com ansible_user=ubuntu
[dbservers]
db1.example.com ansible_user=centos
[all:vars]
ansible_python_interpreter=/usr/bin/python3
YAML format:
# inventory.yml
all:
children:
webservers:
hosts:
web1.example.com:
ansible_user: ubuntu
web2.example.com:
ansible_user: ubuntu
dbservers:
hosts:
db1.example.com:
ansible_user: centos
vars:
ansible_python_interpreter: /usr/bin/python3
Inventory precedence:
- Default:
/etc/ansible/hosts - Override with
-i /path/to/inventory - Multiple inventories:
-i inv1 -i inv2
Dynamic inventory: Ansible supports cloud providers, CMDBs, etc.
# AWS EC2
pip install boto3
ansible-inventory -i aws_ec2.yml --list
# Example aws_ec2.yml
plugin: aws_ec2
regions:
- us-east-1
filters:
instance-state-name: running
Host variables and group variables:
- Host vars:
host_vars/web1.yml - Group vars:
group_vars/webservers.yml
For more, see inventory documentation.
Collections
Collections are packaged Ansible content including modules, roles, and plugins. They replace the monolithic Ansible distribution.
Installing collections:
# Install specific collection
ansible-galaxy collection install community.general
# Install multiple
ansible-galaxy collection install community.general community.mysql
# Install from requirements file
ansible-galaxy collection install -r requirements.yml
Using collection modules:
- name: Use community module
community.general.ufw:
rule: allow
port: "80"
- name: Use AWS collection
amazon.aws.ec2_instance:
name: my-instance
state: present
Popular collections:
community.general: General-purpose modulescommunity.mysql: MySQL database modulescommunity.postgresql: PostgreSQL modulesamazon.aws: AWS modulesazure.azcollection: Azure modulesgoogle.cloud: GCP modules
Creating collections:
ansible-galaxy collection init mynamespace.mycollection
For more, see collections documentation.
Variables
Variables store values for reuse in playbooks. They can be defined at multiple levels with different precedence.
Variable precedence (highest to lowest):
- Extra vars (
-eflag) - Task vars
- Block vars
- Role vars
- Play vars
- Host vars
- Group vars
- Role defaults
- Facts
Defining variables:
# In playbook
- hosts: webservers
vars:
http_port: 80
server_name: "{{ ansible_hostname }}"
# In inventory
[webservers:vars]
http_port=80
# Host vars (host_vars/web1.yml)
http_port: 8080
# Group vars (group_vars/webservers.yml)
package_name: nginx
Variable types:
# Strings
app_name: "myapp"
# Numbers
port: 8080
# Booleans
debug: true
# Lists
packages: [nginx, git, vim]
# Dictionaries
config:
port: 80
host: localhost
Using variables:
- name: Install package
ansible.builtin.package:
name: "{{ package_name }}"
state: present
- name: Configure service
ansible.builtin.template:
src: config.j2
dest: "/etc/{{ app_name }}/config"
Variable files:
# vars/main.yml
app_vars:
name: myapp
version: "1.0"
# Include vars
- name: Load variables
ansible.builtin.include_vars:
file: vars/main.yml
Facts
Facts are variables containing information about hosts, automatically gathered by Ansible.
Common facts:
ansible_hostname: Hostnameansible_distribution: OS distribution (Ubuntu, CentOS, etc.)ansible_distribution_version: OS versionansible_default_ipv4.address: Primary IP addressansible_memory_mb.real.total: Total RAM in MBansible_processor_vcpus: Number of CPU cores
Gathering facts:
- name: Gather all facts
ansible.builtin.setup:
- name: Gather specific facts
ansible.builtin.setup:
filter: ansible_distribution*
- name: Gather custom facts
ansible.builtin.setup:
fact_path: /etc/ansible/facts.d
Using facts:
- name: Install OS-specific packages
ansible.builtin.package:
name: "{{ 'apache2' if ansible_distribution == 'Ubuntu' else 'httpd' }}"
state: present
- name: Show system info
ansible.builtin.debug:
msg: "Host {{ ansible_hostname }} has {{ ansible_processor_vcpus }} CPUs"
Custom facts:
Create /etc/ansible/facts.d/custom.fact:
[packages]
webserver=nginx
database=mysql
Access as ansible_local.custom.packages.webserver.
Disabling fact gathering:
- hosts: all
gather_facts: false
Conditionals
Conditionals control task execution based on variables, facts, or previous task results.
Basic when clause:
- name: Install nginx on Ubuntu
ansible.builtin.package:
name: nginx
state: present
when: ansible_distribution == "Ubuntu"
- name: Install httpd on CentOS
ansible.builtin.package:
name: httpd
state: present
when: ansible_distribution == "CentOS"
Multiple conditions:
- name: Install package
ansible.builtin.package:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
when:
- ansible_distribution in ["Ubuntu", "Debian"]
- ansible_kernel is version('4.0', '>=')
Registering results:
- name: Check if service exists
ansible.builtin.command: systemctl status nginx
register: nginx_status
ignore_errors: true
- name: Start nginx if not running
ansible.builtin.service:
name: nginx
state: started
when: nginx_status.rc != 0
Complex conditions:
- name: Run only on production
ansible.builtin.debug:
msg: "Production task"
when: inventory_hostname in groups['prod'] and env == 'production'
- name: Skip on certain hosts
ansible.builtin.command: echo "skip me"
when: "'skip' not in ansible_hostname"
Conditional imports:
- name: Include Ubuntu tasks
ansible.builtin.include_tasks: ubuntu.yml
when: ansible_distribution == "Ubuntu"
Loops
Loops iterate over lists, dictionaries, or generated sequences.
Basic loop:
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- git
- vim
Loop with dictionary:
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
state: present
loop:
- { name: "alice", uid: 1001 }
- { name: "bob", uid: 1002 }
Loop variables:
- name: Show loop info
ansible.builtin.debug:
msg: "Item {{ item }} at index {{ loop_index }}"
loop:
- one
- two
- three
loop_control:
extended: true
Loop over inventory:
- name: Ping all hosts
ansible.builtin.command: ping -c 1 {{ item }}
loop: "{{ groups['all'] }}"
delegate_to: localhost
Until loops:
- name: Wait for service
ansible.builtin.uri:
url: "http://localhost:80"
register: result
until: result.status == 200
retries: 10
delay: 5
Loop control:
- name: Install packages with pause
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- mysql
loop_control:
pause: 10 # Pause 10 seconds between iterations
Templates
Templates use Jinja2 to generate configuration files with variables.
Basic template:
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
Template file (templates/nginx.conf.j2):
server {
listen {{ http_port | default(80) }};
server_name {{ server_name | default(ansible_hostname) }};
location / {
proxy_pass http://{{ backend_host }}:{{ backend_port }};
proxy_set_header Host $host;
}
{% if ssl_enabled | default(false) %}
listen 443 ssl;
ssl_certificate /etc/ssl/certs/{{ server_name }}.crt;
ssl_certificate_key /etc/ssl/private/{{ server_name }}.key;
{% endif %}
}
Template filters:
# String filters
{{ username | upper }}
{{ path | dirname }}
{{ list | join(', ') }}
# Default values
{{ variable | default('default_value') }}
# Conditionals in templates
{% if condition %}
content
{% endif %}
{% for item in items %}
{{ item }}
{% endfor %}
Validating templates:
- name: Deploy config with validation
ansible.builtin.template:
src: config.j2
dest: /etc/myapp/config
validate: myapp --check-config %s
Inline templates:
- name: Create config file
ansible.builtin.copy:
content: |
# {{ ansible_managed }}
port = {{ app_port }}
host = {{ app_host }}
dest: /etc/myapp/config
ADHOC Commands
Adhoc commands run single tasks without playbooks. Useful for quick checks, one-off tasks, and testing.
Basic syntax:
ansible [pattern] -m [module] -a '[module_args]' [options]
Common examples:
# Test connectivity
ansible all -m ansible.builtin.ping
# Get hostnames
ansible all -m ansible.builtin.command -a 'hostname'
# Check disk usage
ansible all -m ansible.builtin.command -a 'df -h'
# Check uptime
ansible webservers -m ansible.builtin.command -a 'uptime'
# Dry run (check mode)
ansible all -m ansible.builtin.command -a 'reboot' --check
Privilege escalation:
# Become root with password prompt
ansible all -m ansible.builtin.user -a 'name=testuser state=present' -b -K
# Become specific user
ansible all -m ansible.builtin.user -a 'name=testuser state=present' -b --become-user=root -K
Advanced examples:
# Install package
ansible webservers -m ansible.builtin.package -a 'name=nginx state=present' -b
# Check service status
ansible all -m ansible.builtin.service -a 'name=sshd state=started'
# Copy file
ansible all -m ansible.builtin.copy -a 'src=/local/file dest=/remote/file' -b
# Run shell command with pipe
ansible all -m ansible.builtin.shell -a 'ps aux | grep nginx'
Options:
-i inventory: Specify inventory file-u user: SSH user-k: Prompt for SSH password-K: Prompt for sudo password-b: Become (sudo)--limit host: Run on specific host-f forks: Number of parallel processes-v: Verbose output
COMMANDS
Common Ansible command-line options and patterns.
Gathering facts:
# Show all facts for a host
ansible hostname -m ansible.builtin.setup
# Show specific fact
ansible hostname -m ansible.builtin.setup -a 'filter=ansible_distribution*'
# Gather facts without running tasks
ansible hostname -m ansible.builtin.setup --check
Common flags:
-i inventory: Inventory file/path-m module: Module to use-a 'args': Module arguments-u user: SSH user-k: SSH password prompt-b: Become (sudo)-K: Become password prompt-f forks: Parallel processes (default 5)--limit pattern: Limit to hosts matching pattern-v/-vv/-vvv: Verbosity levels--check: Dry run--diff: Show changes-t tags: Run only tagged tasks--skip-tags tags: Skip tagged tasks--start-at-task 'task name': Start playbook at specific task
Includes
Includes allow reusing tasks across playbooks. Use include_tasks for task files, include_role for roles.
Including task files:
# In playbook.yml
tasks:
- name: Include common tasks
ansible.builtin.include_tasks: common.yml
- name: Include tasks with variables
ansible.builtin.include_tasks:
file: setup.yml
vars:
package_name: nginx
Including roles:
- name: Include a role
ansible.builtin.include_role:
name: common
vars:
some_var: value
Conditional includes:
- name: Include tasks only on Ubuntu
ansible.builtin.include_tasks: ubuntu_tasks.yml
when: ansible_distribution == 'Ubuntu'
Best practices:
- Use
include_tasksfor reusable task snippets - Prefer roles for complex, reusable functionality
- Includes are processed at runtime, so variables must be defined before inclusion
Galaxy
Ansible Galaxy hosts community-contributed roles and collections.
Role management:
# Install a role
ansible-galaxy role install geerlingguy.nginx
# Install role to specific path
ansible-galaxy role install geerlingguy.nginx -p ./roles
# List installed roles
ansible-galaxy role list
# Remove a role
ansible-galaxy role remove geerlingguy.nginx
# Create a new role
ansible-galaxy role init myrole
Collection management:
# Install a collection
ansible-galaxy collection install community.general
# Install to specific path
ansible-galaxy collection install community.general -p ./collections
# List installed collections
ansible-galaxy collection list
# Remove a collection
ansible-galaxy collection remove community.general
Publishing:
# Build and publish a collection
ansible-galaxy collection build
ansible-galaxy collection publish ./my-collection-1.0.0.tar.gz --api-key YOUR_API_KEY
Requirements files:
# requirements.yml
roles:
- name: geerlingguy.nginx
version: 3.0.0
collections:
- name: community.general
version: 5.0.0
ansible-galaxy role install -r requirements.yml
ansible-galaxy collection install -r requirements.yml
Roles
Roles organize playbooks into reusable components. A role is a directory structure with tasks, handlers, variables, etc.
Creating a role:
# Initialize role structure
ansible-galaxy role init myrole
# Or manually create directories
mkdir -p roles/myrole/{tasks,handlers,vars,defaults,files,templates,meta}
Role directory structure:
roles/myrole/
├── tasks/
│ └── main.yml # Main tasks
├── handlers/
│ └── main.yml # Event handlers
├── vars/
│ └── main.yml # Role variables
├── defaults/
│ └── main.yml # Default variables
├── files/ # Static files
├── templates/ # Jinja2 templates
├── meta/
│ └── main.yml # Role metadata
└── README.md
Example tasks/main.yml:
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- vim
- git
- curl
- htop
- name: Ensure service is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
Role variables:
# defaults/main.yml
nginx_port: 80
# vars/main.yml (higher precedence)
nginx_user: www-data
Using roles in playbooks:
- hosts: webservers
roles:
- role: myrole
vars:
nginx_port: 8080
PLAYBOOK
Playbooks are YAML files defining automation tasks. They specify hosts, variables, tasks, and roles.
Basic structure:
---
- name: My playbook
hosts: webservers
become: true
vars:
http_port: 80
max_clients: 200
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
roles:
- common
- nginx
Running playbooks:
# Basic run
ansible-playbook playbook.yml
# With vault password
ansible-playbook playbook.yml --ask-vault-pass
# Limit to specific hosts
ansible-playbook playbook.yml --limit webservers
# Dry run
ansible-playbook playbook.yml --check --diff
# Start at specific task
ansible-playbook playbook.yml --start-at-task "Install nginx"
# Verbose output
ansible-playbook playbook.yml -v
Multiple plays:
---
- name: Setup webservers
hosts: webservers
roles:
- web
- name: Setup databases
hosts: dbservers
roles:
- db
Delegate
Delegation runs tasks on different hosts than the playbook target. Useful for load balancers, monitoring, or local actions.
Basic delegation:
- name: Update load balancer
hosts: webservers
tasks:
- name: Remove from load balancer
ansible.builtin.uri:
url: "http://lb.example.com/remove/{{ inventory_hostname }}"
delegate_to: loadbalancer.example.com
Async tasks with delegation:
- name: Restart server
hosts: app_servers
tasks:
- name: Restart application
ansible.builtin.command: systemctl restart myapp
async: 45
poll: 0
- name: Wait for server to respond
ansible.builtin.wait_for:
host: "{{ inventory_hostname }}"
port: 80
delay: 10
timeout: 60
delegate_to: localhost
Delegating facts:
- name: Gather facts from monitoring server
hosts: webservers
tasks:
- name: Get monitoring facts
ansible.builtin.setup:
delegate_to: monitoring.example.com
delegate_facts: true
Local actions:
- name: Run local command
hosts: all
tasks:
- name: Update local inventory
ansible.builtin.command: ./update_inventory.sh
delegate_to: localhost
run_once: true
COPYING FILES
Copy static files or deploy templates with variable substitution.
Copying files:
- name: Copy configuration file
ansible.builtin.copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
backup: true # Create backup if file exists
Copying content directly:
- name: Create motd
ansible.builtin.copy:
content: |
Welcome to {{ ansible_hostname }}
Managed by Ansible
dest: /etc/motd
mode: "0644"
Using templates:
- name: Deploy nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
validate: nginx -t -c %s # Validate config before deploying
File operations:
- name: Ensure directory exists
ansible.builtin.file:
path: /opt/myapp
state: directory
mode: "0755"
- name: Remove old config
ansible.builtin.file:
path: /etc/old_config
state: absent
Backup and restore:
- name: Backup current config
ansible.builtin.copy:
src: /etc/nginx/nginx.conf
dest: /etc/nginx/nginx.conf.backup
remote_src: true
when: ansible_hostname == 'web1'
MAKING WEBSERVER WITH REVERSE PROXY
Example role for setting up Apache with reverse proxy.
tasks/main.yml:
- name: Install Apache
ansible.builtin.package:
name: apache2
state: present
- name: Enable required modules
ansible.builtin.apache2_module:
name: "{{ item }}"
state: present
loop:
- proxy
- proxy_http
notify: restart apache
- name: Deploy proxy configuration
ansible.builtin.template:
src: proxy.conf.j2
dest: /etc/apache2/conf-available/proxy.conf
mode: "0644"
notify: enable proxy config
- name: Enable site
ansible.builtin.command: a2ensite proxy
notify: restart apache
templates/proxy.conf.j2:
<VirtualHost *:80>
ServerName {{ ansible_hostname }}
ProxyPass / http://{{ hostvars[groups['appservers'][0]]['ansible_default_ipv4']['address'] }}:8000/
ProxyPassReverse / http://{{ hostvars[groups['appservers'][0]]['ansible_default_ipv4']['address'] }}:8000/
ErrorLog ${APACHE_LOG_DIR}/proxy_error.log
CustomLog ${APACHE_LOG_DIR}/proxy_access.log combined
</VirtualHost>
handlers/main.yml:
- name: enable proxy config
ansible.builtin.command: a2enconf proxy
- name: restart apache
ansible.builtin.service:
name: apache2
state: restarted
HANDLERS
Handlers are tasks that run only when notified by other tasks. They prevent unnecessary service restarts.
Example handlers/main.yml:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
- name: restart php-fpm
ansible.builtin.service:
name: php8.1-fpm
state: restarted
Using handlers in tasks:
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: reload nginx
- name: Update PHP config
ansible.builtin.template:
src: php.ini.j2
dest: /etc/php/8.1/fpm/php.ini
notify:
- restart php-fpm
- reload nginx
Handler execution:
- Handlers run once per play, after all tasks complete
- Multiple notifications for the same handler result in single execution
- Use
listenfor handler groups:
handlers:
- name: restart services
ansible.builtin.service:
name: "{{ item }}"
state: restarted
loop:
- nginx
- php-fpm
listen: restart web services
tasks:
- name: Update configs
# ... task ...
notify: restart web services
Force handler execution:
ansible-playbook playbook.yml --force-handlers
SECRETS (PASSWORDS)
Handle sensitive data using Ansible Vault, environment variables, or external secret managers.
Using Ansible Vault:
# In vars file (encrypted with ansible-vault)
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
# Or use lookup
db_password: "{{ lookup('ansible.builtin.password', 'credentials/dbpassword.txt') }}"
Environment variables:
- name: Run application
ansible.builtin.command: myapp
environment:
DB_PASSWORD: "{{ db_password }}"
API_KEY: "{{ api_key }}"
Supervisor config example:
[program:testapp]
command=/usr/bin/gunicorn --bind 0.0.0.0:8000 testapp:app
directory=/opt/testapp
user=nobody
autostart=true
autorestart=true
redirect_stderr=true
environment=DB_PASSWORD="{{ db_password }}"
Handlers for supervisor:
- name: reread supervisor config
ansible.builtin.command: supervisorctl reread
- name: restart testapp
ansible.builtin.supervisorctl:
name: testapp
state: restarted
Playbook with appserver role:
- hosts: appservers
become: true
roles:
- appserver
vars:
db_password: "{{ vault_db_password }}"
SETUP DATABASE
Example role for MySQL database setup. Requires community.mysql collection.
Install collection:
ansible-galaxy collection install community.mysql
tasks/main.yml:
- name: Install MySQL packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- mysql-server
- python3-mysqldb
- name: Start and enable MySQL
ansible.builtin.service:
name: mysql
state: started
enabled: true
- name: Set root password
community.mysql.mysql_user:
name: root
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: Create database
community.mysql.mysql_db:
name: "{{ db_name }}"
state: present
login_unix_socket: /var/run/mysqldb/mysqld.sock
- name: Create database user
community.mysql.mysql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
priv: "{{ db_name }}.*:ALL"
host: "%"
state: present
- name: Configure MySQL for remote access
ansible.builtin.template:
src: mysqld.cnf.j2
dest: /etc/mysql/mysql.conf.d/mysqld.cnf
mode: "0644"
notify: restart mysql
- name: Setup backup cron job
ansible.builtin.cron:
name: "MySQL Backup"
hour: "10"
minute: "22"
user: root
job: "/usr/bin/mysqldump --all-databases > /root/dbbackup.dump"
templates/mysqld.cnf.j2:
[mysqld]
bind-address = 0.0.0.0
handlers/main.yml:
- name: restart mysql
ansible.builtin.service:
name: mysql
state: restarted
Playbook usage:
- hosts: dbservers
become: true
roles:
- dbserver
vars:
mysql_root_password: "{{ vault_mysql_root_password }}"
db_name: myapp
db_user: myapp_user
db_password: "{{ vault_db_password }}"
Best Practices
Guidelines for writing maintainable, secure Ansible code.
Project Structure:
myproject/
├── ansible.cfg
├── inventory/
│ ├── hosts.ini
│ └── group_vars/
├── playbooks/
├── roles/
│ └── myrole/
│ ├── tasks/
│ ├── handlers/
│ ├── vars/
│ ├── defaults/
│ ├── files/
│ ├── templates/
│ └── meta/
├── collections/
├── requirements.yml
└── README.md
Playbook Best Practices:
- Use descriptive names for tasks
- Group related tasks in roles
- Use variables for values that change
- Test playbooks with
--checkand--diff - Use
ansible-lintfor code quality
Security:
- Store secrets in Ansible Vault
- Use least privilege (avoid running as root when possible)
- Validate user input
- Keep vault passwords secure
- Use
ansible_managedin templates
Performance:
- Use
serialfor rolling updates - Limit parallel tasks with
forks - Use
asyncfor long-running tasks - Gather facts only when needed
- Use
tagsto run specific parts
Idempotency:
- Ensure tasks can run multiple times safely
- Use
state: present/absentfor packages/services - Check for existing resources before creating
Version Control:
- Commit playbooks to git
- Use
.gitignorefor sensitive files - Document changes in commit messages
- Use branches for different environments
Documentation:
- Use comments in playbooks
- Maintain README files
- Document variables and their purpose
- Keep inventory organized and documented
Troubleshooting
Common issues and debugging techniques.
Connection Issues:
# Test SSH connection
ssh -i ~/.ssh/key user@host
# Test Ansible connection
ansible host -m ansible.builtin.ping -u user -i inventory
# Debug connection
ansible host -m ansible.builtin.ping -vvv
Task Failures:
# Run with verbose output
ansible-playbook playbook.yml -v
# Check syntax
ansible-playbook playbook.yml --syntax-check
# Dry run
ansible-playbook playbook.yml --check --diff
# Run specific task
ansible-playbook playbook.yml --start-at-task "Install nginx"
Variable Issues:
# Debug variables
- name: Show variables
ansible.builtin.debug:
var: variable_name
- name: Show all variables
ansible.builtin.debug:
var: hostvars[inventory_hostname]
Common Errors:
- SSH Key Issues: Check permissions (
chmod 600 ~/.ssh/id_rsa) - Python Interpreter: Set
ansible_python_interpreterfor non-standard paths - Sudo Password: Use
-Kor setansible_become_pass - Module Not Found: Install required collections
- Facts Not Available: Ensure
gather_facts: true
Performance Issues:
- Reduce
forksin ansible.cfg - Use
serialfor large inventories - Optimize slow tasks with
async - Use
tagsto run only needed tasks
Logs:
- Ansible logs to stdout/stderr
- Use
-vto-vvvfor more detail - Check system logs on managed hosts
- Use
ansible.builtin.debugfor custom logging
Ansible Lint
Ansible Lint checks playbooks for best practices and potential issues.
Installation:
pip install ansible-lint
Basic usage:
# Lint a playbook
ansible-lint playbook.yml
# Lint entire directory
ansible-lint .
# Lint with specific rules
ansible-lint --tags yaml playbook.yml
Common rules:
yaml: YAML syntax issuessyntax-check: Ansible syntax errorsdeprecated: Deprecated featurescommand-instead-of-module: Use modules instead of command/shellno-changed-when: Missing changed_whenunnamed-task: Tasks without names
Configuration (.ansible-lint):
exclude_paths:
- .git/
- test/
rules:
yaml: enable
syntax-check: enable
deprecated: enable
command-instead-of-module: enable
CI/CD Integration:
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install ansible-lint
run: pip install ansible-lint
- name: Run ansible-lint
run: ansible-lint .
Fixing common issues:
- Use FQCN (fully qualified collection names)
- Add names to all tasks
- Use
ansible.builtin.debuginstead ofdebug - Replace
with_itemswithloop - Use
ansible_managedin templates