CloudOpsGuide
fundamentals

Ansible for Beginners: Automate Server Configuration

Beginner
18 minutes
October 2026
CloudOpsGuide Team

Ansible for Beginners: Automate Server Configuration

Ansible is configuration management and orchestration over SSH — no agents, no PKI infrastructure, no central daemon. You write YAML describing the state you want, run it from your laptop or CI, and Ansible pushes that state to every node you point it at. Where Chef and Puppet pull configuration on a schedule, Ansible pushes on demand — which means nodes only need to be reachable, not registered. This guide follows the official Ansible documentation's path — concepts → inventory → ad-hoc commands → playbooks → roles — using the same vocabulary you'll find in the docs.

Table of Contents

The three components

The official docs describe every Ansible environment as three parts — learn these names now because the documentation uses them everywhere:

  • Control node — the machine Ansible is installed on, where you run ansible and ansible-playbook. Your laptop is a control node. Multiple control nodes are fine, but Ansible doesn't coordinate between them.
  • Managed nodes — the remote machines Ansible controls, also called "hosts." Ansible is not installed on managed nodes; they just need SSH (Linux) or WinRM (Windows) plus Python.
  • Inventory — the list of managed nodes, logically organized into groups, living on the control node.

The other official vocabulary you'll meet constantly: a playbook is a YAML list of plays; a play maps managed nodes to an ordered list of tasks; each task invokes a module (the unit of code Ansible copies to and executes on the node); a handler is a task that runs only when notified by a change. Modules, roles, and plugins are distributed in collections via Ansible Galaxy. Every module has a Fully Qualified Collection Name (FQCN) — ping is really ansible.builtin.ping — and docs reference modules by FQCN.

Step 0 — Install and set up a lab

Two packages exist: ansible (the community package — batteries included, ~100 collections) and ansible-core (the engine plus ansible.builtin only — what most teams actually pin). The docs recommend pipx, which installs each package in an isolated environment automatically:

pipx install ansible          # community package
# or: pipx install ansible-core

# classic alternative:
python3 -m venv ~/venvs/ansible && source ~/venvs/ansible/bin/activate
pip install ansible

ansible --version confirms it. You also need target machines — easiest lab: Vagrant or a couple of Docker containers with SSH. Assume two test hosts — 192.168.60.10 and 192.168.60.11 — reachable with your SSH key.

Step 1 — The inventory

The inventory tells Ansible what exists. INI format, one host per line:

# hosts.ini
192.168.60.10
192.168.60.11

Test connectivity with the ping module (Ansible's ping tests Python-over-SSH, not ICMP):

ansible -i hosts.ini all -m ping -u vagrant

all is a built-in group containing every host. A pong reply means Ansible can SSH in and execute modules.

Step 2 — Ad-hoc commands and facts

Before playbooks, Ansible runs modules directly — this is how you debug and explore:

ansible -i hosts.ini all -m command -a "uptime"
ansible -i hosts.ini all -m apt -a "name=htop state=present" --become

Every task is a module call: command, apt, copy, service, user. Modules are idempotent — state=present installs htop once and reports ok (no change) on re-runs, not changed.

Gather facts — Ansible's auto-detected info about each node (OS, IPs, memory, disks):

ansible -i hosts.ini all -m setup | less
ansible -i hosts.ini all -m setup -a "filter=ansible_distribution*"

Step 3 — Groups and variables

Real inventories group hosts and attach variables:

[web]
192.168.60.10

[db]
192.168.60.11

[prod:children]
web
db

[all:vars]
ansible_user=vagrant
app_port=8080

Now -m ping can target web, db, or prod. Variables defined here are usable in playbooks as {{ app_port }}.

Step 4 — Your first playbook

A playbook is a YAML file of plays; a play maps tasks to hosts:

# playbook.yml
- name: Configure web servers
  hosts: web
  become: true
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: true

    - name: Ensure nginx is running
      service:
        name: nginx
        state: started
        enabled: true
ansible-playbook -i hosts.ini playbook.yml

Run it twice — the second run should show all ok, zero changed. That idempotency is the whole point: playbooks are safe to re-run continuously.

Step 5 — Pushing files

    - name: Deploy a static page
      copy:
        src: files/index.html
        dest: /var/www/html/index.html
        mode: "0644"

src resolves relative to the playbook directory — keep files in files/ next to it.

Step 6 — Handling failures

A failed task stops that host's play. Control it:

    - name: Optional health check
      command: /usr/local/bin/healthcheck
      ignore_errors: true

    - name: Restart only when the config changed
      service:
        name: nginx
        state: restarted
      # ...triggered by a handler, below

Pair ignore_errors with register + failed_when when you need finer control:

    - name: Check cluster membership
      command: cluster-tool status
      register: result
      failed_when: "'DEGRADED' in result.stdout"

Step 7 — Conditionals

    - name: Install apache2 on Debian family
      apt:
        name: apache2
        state: present
      when: ansible_os_family == "Debian"

    - name: Open firewall only for prod
      ufw:
        rule: allow
        port: "{{ app_port }}"
      when: env == "production"

Step 8 — Deploying code with the git module

    - name: Deploy app from git
      git:
        repo: "https://github.com/yourorg/app.git"
        dest: /opt/app
        version: v1.4.2
      notify: restart app

notify triggers a handler — a task that runs once at the end, only if something changed:

  handlers:
    - name: restart app
      service:
        name: app
        state: restarted

This is the standard pattern: change file → notify → service restarts once, even if five tasks notified.

Step 9 — Scaling to multiple hosts

Change hosts: web to hosts: prod and the same play runs across the group in parallel. Control it:

  • serial: 2 — rolling execution, 2 hosts at a time (how you do zero-downtime deploys)
  • -l web on the CLI — limit to a subset
  • --check — dry-run mode; add --diff to preview file changes

Step 10 — Templates (Jinja2)

copy pushes static files; template renders Jinja2 on the target:

    - name: Render nginx vhost
      template:
        src: templates/vhost.conf.j2
        dest: /etc/nginx/sites-available/app.conf
      notify: reload nginx
# templates/vhost.conf.j2
server {
    listen 80;
    server_name {{ domain }};
    location / {
        proxy_pass http://127.0.0.1:{{ app_port }};
    }
}

{{ domain }} comes from inventory vars, group_vars/, host_vars/, or -e domain=example.com at the CLI.

Step 11 — Variable precedence (the part that bites)

Ansible has ~20 places to define variables. Remember this simplified order, weakest to strongest:

  1. defaults/main.yml inside a role — lowest priority, always overridable
  2. Inventory group_vars / host_vars
  3. Playbook vars: block
  4. --extra-vars on the CLI — almost always wins

Rule of thumb: defaults in roles, environment differences in group_vars, one-off overrides with -e. When a variable "mysteriously" won't change, something stronger is beating you.

Step 12 — Roles: structure that scales

When a playbook grows past ~50 lines, split it into roles — the standard directory layout:

roles/
└── nginx/
    ├── tasks/main.yml       # the tasks
    ├── handlers/main.yml    # restart/reload handlers
    ├── templates/           # .j2 files (src resolves here automatically)
    ├── files/               # static files
    ├── defaults/main.yml    # lowest-precedence variables
    └── meta/main.yml        # role dependencies

The playbook shrinks to intent:

- name: Production web tier
  hosts: web
  become: true
  roles:
    - common
    - nginx
    - app

Step 13 — Tags: run slices of a playbook

    - name: Render nginx vhost
      template:
        src: templates/vhost.conf.j2
        dest: /etc/nginx/sites-available/app.conf
      tags: [nginx, config]
ansible-playbook -i hosts.ini site.yml --tags config     # only tagged tasks
ansible-playbook -i hosts.ini site.yml --skip-tags slow  # skip a slice

Configuring Ansible: ansible.cfg

Stop passing -i hosts.ini on every command — put defaults in ansible.cfg at the project root:

[defaults]
inventory      = hosts.ini
host_key_checking = False   # lab only — real setups verify host keys
retry_files_enabled = False
forks          = 20          # parallel hosts per play

Config resolution order (later wins): ANSIBLE_CONFIG env var → ./ansible.cfg → ~/.ansible.cfg → /etc/ansible/ansible.cfg. A project-local ansible.cfg is the pattern — it travels with the repo, so every teammate gets identical behavior.

Collections and Galaxy

Almost everything Ansible ships — modules, roles, plugins — is packaged in collections and distributed through Ansible Galaxy. You already used FQCN names implicitly; reference them fully when ambiguity matters:

    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present

Install non-builtin collections with ansible-galaxy:

ansible-galaxy collection install community.docker
ansible-galaxy collection install amazon.aws

Pin them in requirements.yml so teammates get identical collections:

collections:
  - name: community.docker
    version: ">=3.4,<4.0"
ansible-galaxy collection install -r requirements.yml

Commands you'll use daily

ansible-playbook site.yml --check --diff   # dry run with file diffs
ansible-playbook site.yml -l web           # limit to a group
ansible-inventory --list -i hosts.ini      # dump resolved inventory/vars
ansible-vault encrypt secrets.yml          # encrypt variable files
ansible-doc copy                           # module docs offline

Where to go next: the official Ansible documentation (source lives in ansible/ansible-documentation) is organized as getting started → how-to guides → module/plugin indexes — ansible-doc <module> gives you the same reference offline. Then look at ansible-vault for secrets (and compare with Vault for dynamic credentials), the community.* collections on Galaxy, and AWX when the team needs scheduled runs, RBAC, and a UI.

Related Articles


Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Beginner Estimated Reading Time: 18 minutes