Ansible Tutorial for Beginners — Automate Servers Without Writing a Custom Agent

Stackademic

Learn Ansible from scratch: inventory, playbooks, modules, and roles. Automate Linux configuration with YAML and SSH—no daemon required.

Ansible is one of the most approachable configuration management tools because it does not require installing a long-running agent on every server. You describe desired state in YAML playbooks, Ansible connects over SSH (or WinRM on Windows), and idempotent modules make changes only when needed.

If you are tired of SSH-ing into ten machines to update a package or drop a config file, this tutorial walks you from zero to a repeatable deployment workflow.

What Ansible does

Ansible automates:

  • Provisioning — users, packages, services, firewall rules
  • Application deployment — copy artifacts, restart services, run migrations
  • Orchestration — ordered multi-step workflows across groups of hosts
  • Ad hoc commands — one-off tasks without writing a full playbook

The control node (your laptop or CI runner) holds playbooks and inventory. Managed nodes need Python and SSH access—nothing else.

Install Ansible on the control node

On Ubuntu or Debian:

sudo apt update && sudo apt install -y ansible
ansible --version

On macOS with Homebrew:

brew install ansible

For learning, a control node and one or two VMs (local VirtualBox, cloud EC2, or Multipass) are enough.

Inventory: naming your servers

Inventory lists hosts Ansible manages. The simplest form is an INI file:

[web]
web1.example.com
web2.example.com

[db]
db1.example.com

[web:vars]
ansible_user=ubuntu

YAML inventory works too:

all:
  children:
    web:
      hosts:
        web1.example.com:
        web2.example.com:
    db:
      hosts:
        db1.example.com:

Test connectivity:

ansible all -m ping -i inventory.ini

A successful pong means SSH and Python are reachable.

Your first playbook

Playbooks are YAML files describing plays—ordered tasks targeting host groups.

Create site.yml:

---
- name: Basic web server setup
  hosts: web
  become: yes
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

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

    - name: Deploy index page
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: "<h1>Hello from Ansible</h1>\n"
        mode: '0644'
      notify: Reload nginx

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

Run it:

ansible-playbook -i inventory.ini site.yml

Key concepts in this playbook:

  • become: yes — escalate privileges (sudo)
  • state: present — idempotent install; skips if already installed
  • notify / handlers — run reload only when the copy task changes

Run the playbook again. Ansible should report ok and changed=0 on most tasks—that is idempotency working.

Modules you will use constantly

ModulePurpose
package / apt / yumInstall software
copy / templateDeploy files; template renders Jinja2
serviceStart/stop/restart systemd units
user / groupAccount management
filePermissions, symlinks, directories
command / shellRun commands (prefer dedicated modules when they exist)

Favor dedicated modules over raw shell—they return structured changed/ok status and handle edge cases.

Variables and facts

Define variables in playbooks, inventory, group_vars, or host_vars:

# group_vars/web.yml
app_port: 8080

Reference with Jinja2:

- name: Open app port
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    destination_port: "{{ app_port }}"
    jump: ACCEPT

Ansible gathers facts about each host (OS, IP addresses, mount points):

ansible web -m setup -i inventory.ini | less

Use ansible_facts['distribution'] in templates for OS-specific logic.

Templates with Jinja2

For config files with per-environment values, use template:

# templates/nginx.conf.j2
server {
    listen {{ app_port }};
    server_name {{ ansible_hostname }};
    root /var/www/html;
}
- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/default
  notify: Reload nginx

Roles: organizing larger projects

When playbooks grow, split logic into roles—reusable bundles of tasks, handlers, templates, and defaults:

roles/
  nginx/
    tasks/main.yml
    handlers/main.yml
    templates/nginx.conf.j2
    defaults/main.yml

A play imports roles:

- hosts: web
  become: yes
  roles:
    - nginx
    - app

Generate role scaffolding:

ansible-galaxy init nginx

Community roles on Ansible Galaxy (ansible-galaxy install geerlingguy.docker) accelerate common stacks—pin versions in requirements.yml for reproducibility.

Ad hoc tasks

Quick one-liners without a playbook:

# Restart nginx on all web servers
ansible web -i inventory.ini -b -m service -a "name=nginx state=restarted"

# Check disk usage
ansible all -i inventory.ini -m command -a "df -h"

-b is shorthand for --become.

Ansible Vault for secrets

Never commit plaintext passwords. Encrypt sensitive files:

ansible-vault create group_vars/web/vault.yml
ansible-playbook site.yml --ask-vault-pass

In CI, pass the vault password via environment variable or secret manager integration.

Running from CI/CD

A typical pipeline stage:

  1. Lint playbooks with ansible-lint
  2. Run against a staging inventory
  3. Manual approval gate
  4. Run against production with limited fork count and serial strategy for rolling updates
- hosts: web
  serial: 2  # update two at a time
  max_fail_percentage: 0

Store inventory per environment (inventories/staging, inventories/prod) and pass -i inventories/prod/hosts.

Ansible vs Terraform vs shell scripts

  • Shell scripts run once; re-running may duplicate work or break state.
  • Terraform provisions cloud infrastructure (VPCs, instances, DNS).
  • Ansible configures whatever already exists—or pairs with Terraform output for dynamic inventory.

Many teams Terraform the VMs, then Ansible configures them.

Common pitfalls

SSH host key checking in lab environments
For throwaway labs only, you might disable strict host key checking in ansible.cfg. In production, manage known_hosts properly.

Running as root everywhere
Use become selectively; define a sudo-capable deploy user.

Non-idempotent shell tasks
command: ./install.sh runs every time unless you add creates: or a checks file.

Huge playbooks in one file
Refactor into roles early.

Practice project

Solidify skills with this sequence:

  1. Two Ubuntu VMs in a web group
  2. Playbook installs nginx and a static site
  3. Extract nginx tasks into a role
  4. Add a template for virtual host config with a variable port
  5. Encrypt a fake API key with Vault and template it into an app env file
  6. Run the playbook twice and confirm zero changes on the second run

FAQ

Does Ansible work on Windows?
Yes, via WinRM. Linux-style SSH workflows are still the most common starting point.

How does Ansible compare to Chef or Puppet?
Chef and Puppet traditionally relied on agents and richer DSLs. Ansible's agentless SSH model lowers the adoption bar.

Can Ansible replace Docker?
No—they complement each other. Ansible can install Docker and deploy containers; Kubernetes often uses Helm or operators for in-cluster state.

Is YAML indentation really that strict?
Yes. Use spaces, not tabs, and validate with ansible-playbook --syntax-check.

Debugging failed runs

When a task fails, Ansible stops the play unless ignore_errors: yes is set. Read the stderr output carefully—permission errors often mean missing become, and unreachable usually means SSH or firewall issues.

Re-run with verbose output to see module arguments and connection details:

ansible-playbook -i inventory.ini site.yml -vvv

Use --check (dry run) and --diff to preview changes without applying them. Combine both during change review windows.

Next steps

After this tutorial, explore dynamic inventory (AWS EC2 plugin), Molecule for role testing, and AWX or Ansible Automation Platform for centralized job scheduling. The core loop stays the same: describe desired state, run the playbook, let idempotency keep production drift in check.