Ansible Project Structure
Production repo layout
ansible.cfg
The Ansible configuration file. It sets defaults so you do not have to repeat flags on every command.
[defaults]
# Where to look for inventory by default
inventory = inventories/production/hosts.ini
# Where roles are found
roles_path = roles
# Remote user for SSH connections
remote_user = ansible
# Disable host key checking (use carefully)
host_key_checking = False
# How many hosts to run tasks on simultaneously
forks = 10
# Retry files clutter the repo — disable
retry_files_enabled = False
[privilege_escalation]
become = True
become_method = sudo
become_user = root
Ansible reads ansible.cfg from the current directory first, then ~/.ansible.cfg, then /etc/ansible/ansible.cfg. Keep a project-level ansible.cfg so the repo is self-contained.
site.yml
The main playbook that ties everything together. Running ansible-playbook site.yml should apply the full desired state to all hosts.
---
# site.yml — full infrastructure deployment
- name: Apply common baseline to all hosts
hosts: all
roles:
- role: common
- name: Deploy web servers
hosts: webservers
roles:
- role: nginx
- role: chrony
- name: Deploy mail servers
hosts: mailservers
roles:
- role: postfix
- role: dovecot
- role: chrony
- name: Deploy log servers
hosts: logservers
roles:
- role: rsyslog_server
playbooks/.
inventories/ — multiple environments
A separate inventory directory for each environment. This lets you use the same roles and playbooks against different hosts with different variable values.
# inventories/production/hosts.ini
[webservers]
web01.example.com
web02.example.com
[mailservers]
mail01.example.com
[logservers]
log01.example.com
[all:vars]
# Variables here are lowest priority — prefer group_vars files
ansible_python_interpreter=/usr/bin/python3
Run against a specific environment by pointing to its inventory:
# Default (uses ansible.cfg setting)
ansible-playbook site.yml
# Staging environment explicitly
ansible-playbook -i inventories/staging/hosts.ini site.yml
# Production with a limit
ansible-playbook site.yml --limit web01.example.com
group_vars/ and host_vars/
Variable files that Ansible loads automatically based on what group or host it is running against. These live inside the inventory directory, not in the repo root.
# inventories/production/group_vars/all.yml
# Applies to EVERY host in this inventory
---
# NTP servers used by all hosts
chrony_servers:
- ntp1.example.com
- ntp2.example.com
# All hosts forward logs here
rsyslog_forward_server: log01.example.com
# inventories/production/group_vars/webservers.yml
# Applies only to hosts in the [webservers] group
---
nginx_worker_processes: auto
nginx_client_max_body_size: 64m
enable_tls: true
# inventories/production/host_vars/web01.yml
# Applies only to web01
---
nginx_server_name: web01.example.com
nginx_port: 8443
The load order is: all.yml → groupname.yml → hostname.yml. More specific files override less specific ones. The same variable defined in webservers.yml overrides its value in all.yml.
playbooks/
Focused playbooks for specific operations. Unlike site.yml which deploys everything, these are for targeted actions:
---
# playbooks/update-packages.yml
# Rolling package update with serial to avoid taking down all hosts at once
- name: Update all packages
hosts: all
serial: 1 # update one host at a time
become: true
tasks:
- name: Update all packages
ansible.builtin.package:
name: '*'
state: latest
notify: Reboot if kernel updated
handlers:
- name: Reboot if kernel updated
ansible.builtin.reboot:
reboot_timeout: 300
roles/
Each subdirectory is one role. The role name matches the directory name, which matches what you reference in playbooks.
roles/
├── common/ # Referenced as role: common
├── nginx/ # Referenced as role: nginx
├── postfix/ # Referenced as role: postfix
├── chrony/ # Referenced as role: chrony
└── rsyslog_server/ # Referenced as role: rsyslog_server
Ansible searches the roles/ directory (and the path in ansible.cfg) when resolving role names. You can also set roles_path = roles:~/.ansible/roles to search multiple locations.
requirements.yml
Declares external roles or collections your project depends on. Download them with ansible-galaxy install -r requirements.yml.
---
# requirements.yml
roles:
- name: geerlingguy.docker
version: "6.1.0"
collections:
- name: community.general
version: ">=7.0.0"
- name: ansible.posix
version: ">=1.5.0"
Pin versions in production repos. An unpinned geerlingguy.docker might update and break your deployment.
How to navigate a real repo
When you join a team and are handed an unfamiliar Ansible repo, work through it in this order:
- Read README.md — conventions, how to run, environment notes
- Read ansible.cfg — which inventory is default, any unusual settings
- Read site.yml — which roles run on which hosts
- Look at inventories/production/hosts.ini — what groups exist
- Check group_vars/all.yml — baseline variables for every host
- Browse roles/ — skim the
defaults/main.ymlof each role to understand what it configures
Finding what controls a specific setting
Someone says: "the NTP server pointing to the wrong host, can you fix it?" Here is how to trace it:
- Search for the variable name:
grep -r "chrony_servers" inventories/ roles/ - Find where it is defined — likely in
inventories/production/group_vars/all.yml - Check if any group or host var overrides it for the affected host
- Find the template that uses it:
grep -r "chrony_servers" roles/chrony/templates/ - Verify what the rendered config looks like on the host:
cat /etc/chrony.conf
# Search for a variable across the whole repo
grep -r "chrony_servers" .
# Search only in inventory variables
grep -r "chrony_servers" inventories/
# Search only in role templates
grep -r "chrony_servers" roles/