15. Automation
Automation
TL;DR
- Ansible is agentless (SSH-based, no persistent agent); this removes agent-management failure modes but depends on SSH and Python being available at runtime.
- Idempotency (task produces same result on re-run) is the property that makes playbooks safe to re-run; built-in modules are idempotent by design; raw
shelltasks are not. - Handlers fire only when a task reports a change, avoiding unnecessary service disruptions on every playbook run.
- Grouped inventory (by role, environment, region) enables targeted execution; flat inventory risks accidental production targeting.
- Roles package tasks, handlers, templates, and variables into reusable units; the alternative is copy-pasted, drift-prone duplication.
Core Concepts
Agentless architecture and SSH dependency
Ansible has no persistent local agent. It connects over SSH, executes Python modules on the target, and disconnects. This removes an entire class of failure mode:
- No outdated agents to manage
- No crashed agent processes to debug
- No agent-controller sync issues
- No “agent is down” maintenance burden
Trade-off: Ansible depends on SSH connectivity and Python being available at runtime. If SSH is down or Python isn’t installed, playbooks fail. This is a real operational constraint (network reachability, Python version compatibility) not present with agent-based tools.
Idempotency: the property of safe re-runs
Idempotency means running a playbook twice with no changes produces no changes. This is what makes “just run the playbook again” a safe response to a failed or interrupted run.
# ❌ NOT idempotent — shell task has no state awareness
- name: Create app directory
shell: mkdir /app/data
# Running this twice either errors (dir exists) or does nothing
# ✅ Idempotent — file module checks current state first
- name: Create app directory
file:
path: /app/data
state: directory
# Running this twice = same result (directory exists, no change reported)
Ansible’s built-in modules (file, package, service, copy, template) are idempotent by design: they check current state before acting and report “changed” only when they actually changed something. Raw shell/command tasks have no built-in state awareness.
Handlers: conditional task execution
Handlers fire only when a task reports a change. This avoids unnecessary, disruptive actions on every run:
- name: Update application config
template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: restart app service # Triggers handler only if changed
handlers:
- name: restart app service
service:
name: app
state: restarted
# This runs only if the config task reported a change
# Not on every playbook run, regardless of whether anything changed
Inventory structure and targeting precision
Inventory defines hosts and their groupings. Grouped inventory enables targeted execution:
[webservers]
web-01.internal
web-02.internal
[databases]
db-01.internal
db-02.internal
[staging:children]
webservers
databases
[production:children]
webservers
databases
Targeted execution becomes possible:
ansible-playbook site.yml -i inventory --limit staging
# Only runs against staging servers
Flat, ungrouped inventory loses this precision. Without groups, “run against staging web servers” requires manually listing individual hostnames every time — error-prone and a real risk of accidentally targeting production.
Roles: reusable configuration units
Roles package tasks, handlers, templates, and variables into reusable, shareable units. Instead of copy-pasting the same 15-task sequence across multiple playbooks:
# playbook.yml
- hosts: webservers
roles:
- role: server_hardening
vars: { open_ports: [80, 443] }
- role: monitoring_agent
vars: { monitoring_enabled: true }
- hosts: databases
roles:
- role: server_hardening
vars: { open_ports: [5432] }
- role: monitoring_agent
vars: { monitoring_enabled: true }
Both playbooks use the same roles with different variable overrides. Updates to the role apply everywhere automatically; no drift from independent copies getting out of sync.
Ansible vs Puppet vs Chef
| Aspect | Ansible | Puppet | Chef |
|---|---|---|---|
| Architecture | Agentless (SSH) | Agent-based (pull) | Agent-based (push/pull) |
| Idempotency | Modules built-in idempotent | DSL built-in idempotent | Ruby code — manual discipline |
| Learning curve | Lower (YAML playbooks) | Medium (Puppet DSL) | Higher (Ruby code) |
| State monitoring | Only on-demand (when run) | Continuous (agents pull regularly) | On-demand or scheduled |
| Scaling | SSH-dependent; scales to ~1000s | Agent-based; scales to many hosts | Agent-based; scales to many hosts |
| Common use | Configuration, ad-hoc tasks, provisioning | Continuous configuration enforcement | Complex infrastructure code |
Configuration Reference
# Basic playbook structure
---
- name: Configure web servers
hosts: webservers
become: yes # Run tasks with elevated privileges
vars:
app_port: 8080
roles:
- common_packages # Install base packages
- firewall # Configure firewall
tasks:
- name: Create application directory
file:
path: /opt/myapp
state: directory
owner: appuser
group: appuser
mode: '0755'
- name: Deploy application
template:
src: app.conf.j2
dest: /opt/myapp/config.conf
notify: restart app
handlers:
- name: restart app
service:
name: myapp
state: restarted
# Common commands
ansible-playbook playbooks/site.yml # Run against all hosts
ansible-playbook playbooks/site.yml -i inventory # Specific inventory
ansible-playbook playbooks/site.yml --limit webservers # Limit to group
ansible-playbook playbooks/site.yml --limit host1 # Single host
ansible-playbook playbooks/site.yml --check # Dry-run (check mode)
ansible-playbook playbooks/site.yml --tags hardening # Only tagged tasks
ansible -i inventory webservers -m setup # Gather host facts
ansible -i inventory webservers -m ping # Connectivity test
Common Pitfalls
-
Pitfall: Raw
shelltask causes non-idempotent behavior; re-run does something unexpected. Why: Shell tasks don’t check current state; no idempotency guarantee. Fix: Replace with built-in modules (file,package,service,copy,template); they’re idempotent by design. -
Pitfall: Service restarts on every playbook run, causing unnecessary brief outages. Why: Restart was unconditional, not wired through a handler. Fix: Use
notifyto trigger handlers only when a change actually occurred. -
Pitfall: Flat inventory across 200 hosts; an accidental production-targeted run happens instead of staging. Why: No group-based targeting available; manual hostname lists are error-prone. Fix: Group inventory by role, environment, region; use
--limitfor targeted execution. -
Pitfall: Same 15-task sequence is duplicated across multiple playbooks; updates drift between copies. Why: Duplication instead of roles. Fix: Extract to a shared role; both playbooks include it with variable overrides.
-
Pitfall: Playbook fails due to “Python not found on target.” Why: Ansible’s SSH/Python dependency wasn’t understood. Fix: Ensure Python is installed on all targets; document SSH and Python requirements as part of system setup.
Interview Questions
-
A shell task is flagged for not being idempotent. Explain exactly why, and what replaces it. — Tests understanding of module-based state checking versus raw shell execution.
-
Design an inventory structure for 200 servers across 3 environments (dev/staging/prod) and 4 roles (web/database/cache/monitoring). — Tests whether grouped, hierarchical inventory design is understood as a real practice.
-
A config file change should trigger a service restart, but only when the config actually changed. How do you implement this correctly? — Tests whether handlers as conditional execution are the immediate answer.
-
An Ansible playbook fails with “Python not found on targets.” What does this mean operationally, and how do you prevent it? — Tests understanding of Ansible’s runtime dependencies and how to architect around them.
-
Two playbooks manage nearly-identical server configuration. How do you structure this without duplication? — Tests whether roles as reusable units are understood.