Ansible earned its place in nearly every operations team for one reason: it automates servers using tools they already have. No agents to install, no certificates to distribute — just SSH (or WinRM for Windows) and Python on the target. You describe the state you want in YAML, and Ansible makes it so.

The moving parts

  • Control node — the machine you run Ansible from (your laptop, a bastion, a CI runner).
  • Inventory — the list of hosts you manage, organised into groups.
  • Modules — the units of work: apt, copy, service, user, and thousands more.
  • Playbooks — YAML files that map groups of hosts to ordered lists of tasks.
  • Roles — a directory convention for packaging tasks, templates, and defaults into reusable components.

Inventory: name your world

# inventory/hosts.ini
[web]
web01.example.com
web02.example.com

[db]
db01.example.com

[production:children]
web
db

[production:vars]
ansible_user=deploy

Groups are the unit of targeting: playbooks run against web or production, not hard-coded hostnames. In cloud environments, dynamic inventory plugins can build this list live from your provider's API (OCI, AWS, Azure all have one), so the inventory never goes stale.

Your first playbook

# site.yml
---
- name: Configure web servers
  hosts: web
  become: true

  vars:
    http_port: 80

  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Deploy site configuration
      ansible.builtin.template:
        src: templates/site.conf.j2
        dest: /etc/nginx/conf.d/site.conf
      notify: Reload nginx

    - name: Ensure nginx is running and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Run it with ansible-playbook -i inventory/hosts.ini site.yml. Notice the handler: nginx reloads only if the template task actually changed the file. That's the heart of the Ansible model — react to change, don't repeat work.

Idempotency: the mindset shift

A shell script says "do these steps." A playbook says "this is the desired state." Run a good playbook twice and the second run reports changed=0 — nothing needed doing. This property, idempotency, is what makes automation safe to run repeatedly, on a schedule, or against a half-configured server. Practical habits that preserve it:

  • Prefer purpose-built modules (apt, lineinfile, template) over shell/command.
  • When you must use command, add creates: or changed_when: so Ansible knows whether anything changed.
  • Test with --check (dry run) and --diff before touching production.

Variables, templates, and secrets

Variables layer up from role defaults → group_vars → host_vars → extra vars, letting one playbook serve every environment. Templates use Jinja2 ({{ http_port }}, loops, conditionals) to render config files per host. For secrets, Ansible Vault encrypts variable files at rest — ansible-vault encrypt group_vars/production/vault.yml — so credentials never sit in plain text in your repository.

Where Ansible fits next to Terraform

A question every team asks. The clean division: Terraform provisions the infrastructure (VCNs, instances, load balancers) and Ansible configures what runs on it (packages, files, services, users). They meet in the middle — Terraform outputs feeding a dynamic inventory is a particularly tidy pattern.

Takeaway

Start small: one inventory, one playbook, one role. The pay-off arrives quickly — the third time you configure a server by running one command instead of an afternoon of SSH sessions, you'll wonder how you tolerated anything else. The next post takes this foundation to its most valuable production use case: patching entire fleets safely.

back to all posts