Ansible is the Swiss Army knife of infrastructure automation. No agents to install, no complex master-client architecture to manage, no proprietary DSL to learn. Just Python, SSH, and YAML. If you can write a shell script and you know your way around Linux, you can write a useful Ansible playbook today.
No Agents, No Drama: The Ansible Model
Ansible’s architecture is deliberately simple. The control node (your workstation, a bastion host, or a CI/CD runner) connects to managed nodes over SSH (or WinRM for Windows), executes tasks, and disconnects. There’s no agent process running on managed nodes consuming resources or requiring updates. The only requirement on managed nodes is Python (which is present on virtually every Linux system) and a user account with appropriate sudo privileges.
Idempotency is Ansible’s most important design principle. Every built-in module is designed so that running a task multiple times produces the same result as running it once. If nginx is already installed and running, the package and service modules report “OK” rather than reinstalling or restarting. This means you can run your playbooks safely at any time — to enforce desired state, to verify compliance, or to bring a drifted system back in line — without worrying about destructive side effects.
Your Inventory File
Ansible’s inventory defines the hosts and groups your playbooks target. The simplest format is an INI-style text file. You can also use YAML format or dynamic inventory plugins that query cloud provider APIs (AWS EC2, Azure VMs, GCP instances) to build the inventory automatically from your actual running infrastructure — no manual list maintenance required.
Groups make targeting flexible. You might have a [webservers] group, a [dbservers] group, and a [production] group containing hosts from both. Group variables (stored in group_vars/ directories) let you define different settings per group — staging might use a self-signed certificate while production uses a real one, but the same playbook handles both.
# inventory/hosts.ini — Sample inventory file
[webservers]
web01.example.com ansible_user=ubuntu
web02.example.com ansible_user=ubuntu
[dbservers]
db01.example.com ansible_user=ubuntu ansible_port=2222
[production:children]
webservers
dbservers
[production:vars]
env=production
nginx_worker_processes=4
# Test connectivity to all hosts:
# ansible all -i inventory/hosts.ini -m ping
Writing Your First Playbook
A playbook is a YAML file containing one or more plays. Each play targets a group of hosts and defines a list of tasks to execute in order. Tasks use modules — the unit of work in Ansible. There are thousands of built-in modules covering package management, file operations, service management, cloud resources, network devices, and more. The module name is self-documenting: ansible.builtin.apt, ansible.builtin.copy, ansible.builtin.systemd.
The playbook below installs nginx, deploys a custom configuration, and ensures the service is running and enabled. Run it with ansible-playbook -i inventory/hosts.ini playbooks/nginx.yml. Add --check for a dry run that shows what would change without making any changes.
# playbooks/nginx.yml — Install and configure nginx
---
- name: Configure web servers
hosts: webservers
become: true
vars:
nginx_port: 80
server_name: "{{ inventory_hostname }}"
tasks:
- name: Ensure nginx is installed
ansible.builtin.package:
name: nginx
state: present
- name: Deploy nginx configuration
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
notify: Reload nginx
- name: Ensure nginx is started and enabled
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
handlers:
- name: Reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
Variables, Templates, and Handlers
Variables in Ansible can be defined at multiple levels — in the playbook itself, in separate variable files, in the inventory, or passed on the command line with -e. Jinja2 templates (files ending in .j2) use double-brace syntax ({{ variable_name }}) to interpolate variables into configuration files. This is how one nginx template can produce correctly configured files for dozens of different servers, each with the right hostname, port, and SSL certificate path.
Handlers are tasks that only run when notified by another task. The classic use case: a task that updates a configuration file notifies the “Reload nginx” handler. If the configuration file didn’t change (because it was already correct), the handler never fires and nginx isn’t unnecessarily reloaded. If multiple tasks in a play all notify the same handler, the handler runs only once at the end of the play. This prevents redundant service restarts and makes your playbooks more efficient.