Appearance
Ansible Overview
Introduction
Ansible is an open-source automation platform that enables infrastructure as code, configuration management, application deployment, and orchestration without requiring any agent software on managed nodes. It uses a declarative, human-readable YAML syntax called Playbooks to describe the desired state of systems, making it one of the most accessible automation tools in modern DevOps. Understanding Ansible is essential for anyone working with cloud infrastructure, CI/CD pipelines, or large-scale system administration.
Core Concepts
What Makes Ansible Different
Ansible stands apart from tools like Puppet, Chef, and SaltStack due to three fundamental design decisions:
- Agentless Architecture: Ansible communicates with managed nodes over SSH (Linux) or WinRM (Windows). There is no daemon to install, no database to maintain, and no additional attack surface on your servers.
- Push-Based Model: The control node pushes configurations to managed nodes on demand, rather than nodes polling a central server on a schedule.
- Idempotency: Running the same Ansible playbook multiple times produces the same result. If a system is already in the desired state, Ansible makes no changes.
Architecture Overview
Ansible's architecture is intentionally simple. The control node is the machine where Ansible is installed and from which all automation is driven. Managed nodes (also called hosts) are the target systems. The control node reads an inventory to discover managed nodes, executes modules on those nodes, and organizes work through playbooks.
Key Terminology
| Term | Description |
|---|---|
| Control Node | The machine where Ansible is installed and playbooks are executed from |
| Managed Node | Any server or device managed by Ansible (no agent required) |
| Inventory | A file or script that defines hosts and groups of hosts |
| Module | A unit of work Ansible can execute (e.g., apt, copy, service) |
| Task | A single call to a module with specific arguments |
| Play | A mapping of hosts to a set of tasks |
| Playbook | A YAML file containing one or more plays |
| Role | A structured, reusable collection of tasks, files, templates, and variables |
| Handler | A task that runs only when notified by another task |
| Facts | System information gathered automatically from managed nodes |
| Vault | Ansible's encryption mechanism for sensitive data |
Inventory
The inventory tells Ansible which machines to manage. It can be a simple INI or YAML file, or a dynamic script that queries cloud APIs.
Static Inventory (INI Format)
ini
# inventory/hosts.ini
[webservers]
web1.example.com ansible_host=10.0.1.10
web2.example.com ansible_host=10.0.1.11
[dbservers]
db1.example.com ansible_host=10.0.2.10
db2.example.com ansible_host=10.0.2.11
[loadbalancers]
lb1.example.com ansible_host=10.0.0.10
[production:children]
webservers
dbservers
loadbalancers
[production:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/prod_keyStatic Inventory (YAML Format)
yaml
# inventory/hosts.yml
all:
children:
production:
children:
webservers:
hosts:
web1.example.com:
ansible_host: 10.0.1.10
web2.example.com:
ansible_host: 10.0.1.11
dbservers:
hosts:
db1.example.com:
ansible_host: 10.0.2.10
loadbalancers:
hosts:
lb1.example.com:
ansible_host: 10.0.0.10
vars:
ansible_user: deploy
ansible_ssh_private_key_file: ~/.ssh/prod_keyPlaybooks
Playbooks are the heart of Ansible. They declare the desired state of your infrastructure in YAML.
Basic Playbook Structure
yaml
# site.yml — Full stack deployment
---
- name: Configure web servers
hosts: webservers
become: yes
vars:
http_port: 80
app_version: "2.4.1"
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Deploy application configuration
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: '0644'
notify: Restart Nginx
- name: Ensure Nginx is running and enabled
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart Nginx
service:
name: nginx
state: restartedPlaybook Execution Flow
Multi-Play Playbook
yaml
# deploy-stack.yml
---
- name: Update database servers
hosts: dbservers
become: yes
tasks:
- name: Install PostgreSQL 15
apt:
name:
- postgresql-15
- postgresql-client-15
state: present
- name: Configure PostgreSQL to listen on all interfaces
lineinfile:
path: /etc/postgresql/15/main/postgresql.conf
regexp: "^#?listen_addresses"
line: "listen_addresses = '*'"
notify: Restart PostgreSQL
- name: Allow application subnet in pg_hba
lineinfile:
path: /etc/postgresql/15/main/pg_hba.conf
line: "host all all 10.0.1.0/24 md5"
notify: Restart PostgreSQL
handlers:
- name: Restart PostgreSQL
service:
name: postgresql
state: restarted
- name: Deploy application to web servers
hosts: webservers
become: yes
serial: 1 # Rolling deployment — one server at a time
vars:
app_repo: "https://github.com/company/webapp.git"
app_branch: "release/v2.4"
tasks:
- name: Pull latest application code
git:
repo: "{{ app_repo }}"
dest: /opt/webapp
version: "{{ app_branch }}"
force: yes
- name: Install application dependencies
pip:
requirements: /opt/webapp/requirements.txt
virtualenv: /opt/webapp/venv
- name: Restart application service
systemd:
name: webapp
state: restarted
daemon_reload: yes
- name: Wait for application to become healthy
uri:
url: "http://localhost:8080/health"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 10
delay: 5Modules
Modules are the building blocks of Ansible. Each module performs a specific action. Ansible ships with thousands of modules organized into collections.
Common Module Examples
yaml
# File and directory management
- name: Create application directory
file:
path: /opt/myapp
state: directory
owner: appuser
group: appuser
mode: '0755'
# Copy files with validation
- name: Deploy sudoers file
copy:
src: files/custom-sudoers
dest: /etc/sudoers.d/custom
validate: visudo -cf %s
mode: '0440'
# Template with Jinja2
- name: Generate application config
template:
src: templates/app.conf.j2
dest: /etc/myapp/app.conf
backup: yes
# User management
- name: Create application user
user:
name: appuser
shell: /bin/bash
groups: [docker, sudo]
append: yes
create_home: yes
# Execute commands with guards
- name: Initialize database
command: /opt/myapp/init-db.sh
args:
creates: /opt/myapp/.db-initializedRoles
Roles provide a structured way to organize playbooks into reusable components. A role encapsulates tasks, variables, files, templates, and handlers under a standardized directory layout.
Role Directory Structure
roles/
└── webserver/
├── defaults/
│ └── main.yml # Default variables (lowest precedence)
├── vars/
│ └── main.yml # Role variables (higher precedence)
├── tasks/
│ └── main.yml # Main task list
├── handlers/
│ └── main.yml # Handler definitions
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── index.html # Static files
├── meta/
│ └── main.yml # Role dependencies
└── README.mdRole Task File
yaml
# roles/webserver/tasks/main.yml
---
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
tags: [install]
- name: Create web root directory
file:
path: "{{ web_root }}"
state: directory
owner: www-data
mode: '0755'
- name: Deploy Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s
notify: Reload Nginx
- name: Deploy site configuration
template:
src: site.conf.j2
dest: "/etc/nginx/sites-available/{{ site_name }}.conf"
notify: Reload Nginx
- name: Enable site
file:
src: "/etc/nginx/sites-available/{{ site_name }}.conf"
dest: "/etc/nginx/sites-enabled/{{ site_name }}.conf"
state: link
notify: Reload Nginx
- name: Ensure Nginx is started and enabled
service:
name: nginx
state: started
enabled: yesyaml
# roles/webserver/defaults/main.yml
---
web_root: /var/www/html
site_name: default
http_port: 80
worker_processes: auto
worker_connections: 1024yaml
# Using roles in a playbook
---
- name: Configure full stack
hosts: webservers
become: yes
roles:
- common
- role: webserver
vars:
http_port: 8080
site_name: myapp
- role: monitoring
tags: [monitoring]Variables and Templating
Ansible uses Jinja2 for templating. Variables can be defined at many levels with a well-defined precedence order.
Variable Precedence (Lowest to Highest)
Jinja2 Template Example
jinja2
{# templates/nginx.conf.j2 #}
worker_processes {{ worker_processes }};
events {
worker_connections {{ worker_connections }};
}
http {
upstream app_backend {
{% for host in groups['webservers'] %}
server {{ hostvars[host]['ansible_host'] }}:{{ app_port | default(8080) }};
{% endfor %}
}
server {
listen {{ http_port }};
server_name {{ server_name | default('_') }};
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
{% if enable_ssl | default(false) %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
}
}Conditionals and Loops
yaml
# Using conditionals
- name: Install packages based on OS family
apt:
name: "{{ item }}"
state: present
loop:
- curl
- wget
- jq
when: ansible_os_family == "Debian"
- name: Install packages on RedHat
yum:
name: "{{ item }}"
state: present
loop:
- curl
- wget
- jq
when: ansible_os_family == "RedHat"
# Using registered variables
- name: Check if application is installed
stat:
path: /opt/myapp/bin/myapp
register: app_binary
- name: Install application
shell: /tmp/install.sh
when: not app_binary.stat.exists
# Looping over dictionaries
- name: Create application users
user:
name: "{{ item.key }}"
groups: "{{ item.value.groups }}"
shell: "{{ item.value.shell | default('/bin/bash') }}"
loop: "{{ users | dict2items }}"
vars:
users:
alice:
groups: [sudo, docker]
bob:
groups: [docker]
shell: /bin/zshAnsible Vault
Vault encrypts sensitive data (passwords, API keys, certificates) so they can be safely stored in version control.
yaml
# group_vars/production/vault.yml (encrypted)
# Created with: ansible-vault encrypt group_vars/production/vault.yml
vault_db_password: "s3cureP@ssw0rd!"
vault_api_key: "ak-12345-abcde-67890"
vault_ssl_private_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----
# group_vars/production/vars.yml (plaintext, references vault vars)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"Ad-Hoc Commands
Before writing playbooks, Ansible supports quick one-off commands:
bash
# Ping all hosts
ansible all -i inventory/hosts.ini -m ping
# Check disk space on web servers
ansible webservers -m shell -a "df -h /"
# Install a package on database servers
ansible dbservers -m apt -a "name=htop state=present" --become
# Copy a file to all production servers
ansible production -m copy -a "src=./motd dest=/etc/motd"
# Restart a service
ansible webservers -m service -a "name=nginx state=restarted" --become
# Gather facts from a specific host
ansible web1.example.com -m setupAnsible Galaxy and Collections
Ansible Galaxy is the community hub for sharing roles and collections. Collections bundle modules, plugins, and roles from a specific vendor or domain.
yaml
# requirements.yml — install dependencies before running playbooks
---
collections:
- name: amazon.aws
version: ">=6.0.0"
- name: community.postgresql
version: ">=3.0.0"
- name: community.docker
version: ">=3.0.0"
roles:
- name: geerlingguy.docker
version: "6.1.0"
- name: geerlingguy.certbot
version: "5.1.0"bash
# Install collections and roles
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install amazon.aws
ansible-galaxy role install geerlingguy.dockerAnsible with AWS
Ansible integrates deeply with AWS through the amazon.aws and community.aws collections.
yaml
# provision-aws.yml — Provision EC2 instances
---
- name: Provision AWS infrastructure
hosts: localhost
connection: local
gather_facts: false
vars:
region: us-east-1
instance_type: t3.medium
ami_id: ami-0c55b159cbfafe1f0
key_name: deploy-key
tasks:
- name: Create VPC
amazon.aws.ec2_vpc_net:
name: app-vpc
cidr_block: 10.0.0.0/16
region: "{{ region }}"
tags:
Environment: production
register: vpc
- name: Create subnet
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc.vpc.id }}"
cidr: 10.0.1.0/24
az: "{{ region }}a"
tags:
Name: app-subnet
register: subnet
- name: Create security group
amazon.aws.ec2_security_group:
name: app-sg
description: Application security group
vpc_id: "{{ vpc.vpc.id }}"
rules:
- proto: tcp
ports: [22, 80, 443]
cidr_ip: 0.0.0.0/0
register: security_group
- name: Launch EC2 instances
amazon.aws.ec2_instance:
name: "web-{{ item }}"
instance_type: "{{ instance_type }}"
image_id: "{{ ami_id }}"
key_name: "{{ key_name }}"
vpc_subnet_id: "{{ subnet.subnet.id }}"
security_group: "{{ security_group.group_id }}"
network:
assign_public_ip: true
wait: yes
tags:
Role: webserver
loop: [1, 2, 3]
register: ec2_instances
- name: Add instances to inventory
add_host:
name: "{{ item.instances[0].public_ip_address }}"
groups: webservers
loop: "{{ ec2_instances.results }}"Execution Strategies and Performance
yaml
# Performance tuning in ansible.cfg
[defaults]
forks = 20 # Parallel host connections
gathering = smart # Cache facts, don't re-gather
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
[ssh_connection]
pipelining = True # Reduces SSH operations
ssh_args = -o ControlMaster=auto -o ControlPersist=60sError Handling and Debugging
yaml
# Error handling patterns
- name: Attempt risky operation
block:
- name: Try deploying new version
shell: /opt/app/deploy.sh --version={{ new_version }}
- name: Verify deployment
uri:
url: http://localhost:8080/health
status_code: 200
register: health
retries: 5
delay: 3
until: health.status == 200
rescue:
- name: Rollback to previous version
shell: /opt/app/deploy.sh --version={{ old_version }}
- name: Notify team of failure
slack:
token: "{{ slack_token }}"
channel: '#deployments'
msg: "Deployment of {{ new_version }} FAILED. Rolled back."
always:
- name: Write deployment log
lineinfile:
path: /var/log/deployments.log
line: "{{ ansible_date_time.iso8601 }} - version={{ new_version }} - status={{ 'failed' if ansible_failed_task is defined else 'success' }}"
create: yesAnsible vs Other Tools
| Feature | Ansible | Puppet | Chef | Terraform |
|---|---|---|---|---|
| Primary Use | Config mgmt + orchestration | Config mgmt | Config mgmt | Infrastructure provisioning |
| Agent | No | Yes | Yes | No |
| Language | YAML | Puppet DSL | Ruby DSL | HCL |
| Model | Push | Pull | Pull | Push |
| State | Stateless | Stateful (PuppetDB) | Stateful (Chef Server) | Stateful (tfstate) |
| Learning Curve | Low | Medium | High | Medium |
Best Practices
- Use roles for reusability: Break playbooks into roles that encapsulate a single responsibility—one role per application or service component.
- Keep secrets in Vault: Never store passwords, API keys, or certificates in plaintext. Use
ansible-vaultand prefix encrypted variables withvault_. - Use meaningful names: Every task should have a descriptive
namefield that explains what and why, not just how. - Leverage tags: Tag tasks and roles so you can selectively run subsets of your playbook with
--tagsor--skip-tags. - Test with check mode: Always run
ansible-playbook --check --diffbefore applying changes to production to preview what will change. - Pin versions: Pin collection, role, and package versions in
requirements.ymland playbooks to ensure reproducible deployments. - Use
serialfor rolling deployments: When updating web servers behind a load balancer, useserial: 1orserial: "25%"to avoid downtime. - Prefer modules over shell/command: Modules are idempotent by design;
shellandcommandare not. Usecreates,removes, orchanged_whenif you must use shell. - Organize variables by environment: Use
group_vars/andhost_vars/directories to separate environment-specific configuration from shared defaults. - Implement CI/CD for playbooks: Lint playbooks with
ansible-lint, test with Molecule, and run in CI before applying to production.
Related Concepts
- Serverless and Container Workloads — Ansible can provision container orchestration platforms and serverless infrastructure
- Eventual Consistency — Ansible's idempotent design aligns with eventual consistency models in distributed systems