Ansible: Configuration Management Automation
Ansible has become the de facto standard for configuration management and automation in modern infrastructure. Unlike Puppet and Chef, which require agents on managed nodes, Ansible operates...
Key Insights
- Ansible’s agentless architecture using SSH eliminates the need for installing agents on managed nodes, making it simpler to deploy and maintain than Puppet or Chef
- Idempotency ensures playbooks can run repeatedly without unintended side effects, allowing safe automation of infrastructure changes in production environments
- Roles provide a structured way to organize automation code for reusability, while Ansible Vault secures sensitive data like passwords and API keys directly in your version control system
Introduction to Ansible
Ansible has become the de facto standard for configuration management and automation in modern infrastructure. Unlike Puppet and Chef, which require agents on managed nodes, Ansible operates agentlessly over SSH. This architectural choice eliminates deployment overhead and reduces the attack surface of your infrastructure.
Where Puppet and Chef follow a pull-based model with agents checking in to a master server, Ansible uses push-based execution from a control node. This makes Ansible’s learning curve gentler and its operational model more straightforward. Salt offers similar agentless capabilities, but Ansible’s YAML-based playbooks are more readable than Salt’s Python-centric approach.
Ansible excels at three primary use cases: configuration management (ensuring systems are in desired states), application deployment (orchestrating multi-tier application rollouts), and task automation (executing ad-hoc commands across infrastructure). Its declarative syntax lets you describe what you want, not how to achieve it.
Core Concepts and Architecture
Understanding Ansible’s components is essential before writing automation code. The control node is where Ansible runs—typically your laptop or a CI/CD server. It requires Python and the Ansible package. Managed nodes are the servers you’re automating, requiring only Python and SSH access.
Inventory files define your infrastructure. They list hosts and organize them into groups. Here’s a simple INI-format inventory:
[webservers]
web1.example.com
web2.example.com
[databases]
db1.example.com
db2.example.com
[production:children]
webservers
databases
The same inventory in YAML format:
all:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
databases:
hosts:
db1.example.com:
db2.example.com:
production:
children:
webservers:
databases:
Playbooks are YAML files containing automation instructions. Modules are the units of work—Ansible ships with thousands covering everything from package installation to cloud resource provisioning. Roles package related tasks, variables, and files into reusable components.
Ansible’s idempotency is critical. Run a playbook once or a hundred times—the result is identical. Modules check current state before making changes. If a package is already installed, Ansible skips installation. This property makes automation safe and predictable.
Writing Your First Playbook
Playbooks consist of one or more plays, each targeting specific hosts and containing tasks. Here’s a complete playbook to deploy nginx:
---
- name: Configure web servers
hosts: webservers
become: yes
vars:
nginx_port: 80
server_name: example.com
tasks:
- name: Install nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Copy nginx configuration
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Restart nginx
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
The template file templates/nginx.conf.j2 uses Jinja2 syntax:
server {
listen {{ nginx_port }};
server_name {{ server_name }};
location / {
root /var/www/html;
index index.html;
}
}
Handlers run only when notified and only once per playbook execution, regardless of how many tasks notify them. This prevents unnecessary service restarts.
Variables can come from multiple sources: playbook vars, inventory, command-line arguments, or discovered facts. Facts are system information Ansible gathers automatically:
- name: Display OS information
debug:
msg: "Running on {{ ansible_distribution }} {{ ansible_distribution_version }}"
Conditionals enable environment-specific logic:
- name: Install package manager specific packages
apt:
name: python3-pip
state: present
when: ansible_os_family == "Debian"
- name: Install package on RedHat systems
yum:
name: python3-pip
state: present
when: ansible_os_family == "RedHat"
Inventory Management and Host Patterns
Static inventories work for small, stable environments. For dynamic infrastructure, use inventory scripts or plugins. Here’s a comprehensive static inventory:
all:
vars:
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
children:
production:
children:
webservers:
hosts:
web1.prod.example.com:
ansible_host: 10.0.1.10
web2.prod.example.com:
ansible_host: 10.0.1.11
vars:
nginx_worker_processes: 4
databases:
hosts:
db1.prod.example.com:
ansible_host: 10.0.2.10
vars:
postgresql_max_connections: 200
staging:
children:
webservers:
hosts:
web1.staging.example.com:
For AWS environments, use the EC2 dynamic inventory plugin. Configure it in aws_ec2.yml:
plugin: aws_ec2
regions:
- us-east-1
- us-west-2
filters:
tag:Environment: production
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.region
prefix: region
hostnames:
- tag:Name
Enable it in ansible.cfg:
[inventory]
enable_plugins = aws_ec2
Roles and Reusability
Roles enforce a standard directory structure that promotes reusability. Here’s a complete role for a web application:
roles/webapp/
├── tasks/
│ └── main.yml
├── handlers/
│ └── main.yml
├── templates/
│ ├── app.conf.j2
│ └── systemd.service.j2
├── files/
│ └── app.tar.gz
├── vars/
│ └── main.yml
├── defaults/
│ └── main.yml
└── meta/
└── main.yml
The tasks/main.yml:
---
- name: Create application user
user:
name: "{{ app_user }}"
system: yes
shell: /bin/false
- name: Create application directory
file:
path: "{{ app_path }}"
state: directory
owner: "{{ app_user }}"
mode: '0755'
- name: Extract application
unarchive:
src: app.tar.gz
dest: "{{ app_path }}"
owner: "{{ app_user }}"
- name: Configure systemd service
template:
src: systemd.service.j2
dest: "/etc/systemd/system/{{ app_name }}.service"
notify: Restart application
Use roles in playbooks:
---
- name: Deploy web application
hosts: webservers
roles:
- role: webapp
vars:
app_name: myapp
app_user: myapp
app_path: /opt/myapp
Ansible Galaxy hosts thousands of community roles. Install them with:
ansible-galaxy install geerlingguy.nginx
Advanced Features
Ansible Vault encrypts sensitive data. Create an encrypted file:
ansible-vault create secrets.yml
Store credentials:
db_password: supersecret
api_key: abc123xyz
Reference encrypted variables in playbooks:
- name: Configure database
postgresql_db:
name: myapp
password: "{{ db_password }}"
Run playbooks with:
ansible-playbook site.yml --ask-vault-pass
Tags enable selective execution:
- name: Install packages
apt:
name: "{{ item }}"
loop:
- nginx
- postgresql
tags: packages
- name: Configure application
template:
src: app.conf.j2
dest: /etc/app/config.yml
tags: config
Run only configuration tasks:
ansible-playbook site.yml --tags config
Error handling provides control over failure behavior:
- name: Attempt optional configuration
command: /opt/configure.sh
ignore_errors: yes
- name: Check service status
command: systemctl is-active myapp
register: service_status
failed_when: service_status.rc > 1
Best Practices and Production Tips
Structure projects for maintainability:
ansible/
├── ansible.cfg
├── inventories/
│ ├── production/
│ │ ├── hosts.yml
│ │ └── group_vars/
│ │ └── all.yml
│ └── staging/
│ ├── hosts.yml
│ └── group_vars/
│ └── all.yml
├── roles/
│ ├── common/
│ ├── webserver/
│ └── database/
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── databases.yml
└── files/
The ansible.cfg:
[defaults]
inventory = inventories/production
roles_path = roles
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
[ssh_connection]
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
Test playbooks with ansible-lint:
ansible-lint playbooks/site.yml
Use Molecule for role testing:
molecule init role my-role
molecule test
In CI/CD pipelines, use check mode for validation:
ansible-playbook site.yml --check --diff
Enable pipelining and fact caching for performance. Limit concurrent forks based on your control node’s capacity. Use serial for rolling updates:
- name: Rolling update
hosts: webservers
serial: 2
tasks:
- name: Update application
# tasks here
Version control everything except secrets. Use vault files for credentials and store the vault password in your CI/CD system’s secret management. Keep playbooks idempotent and test thoroughly in staging before production runs.
Ansible transforms infrastructure management from manual, error-prone processes into reliable, repeatable automation. Master these fundamentals, follow best practices, and you’ll build infrastructure as code that scales with your organization.