Ansible Fundamentals: Agentless Configuration Management Tutorial

You have 50 servers. You need to install a package, update a config file, and restart a service on all of them. You could SSH into each one. Or you could write one Ansible playbook and let it handle all 50 simultaneously.

Ansible is the configuration management tool that doesn’t require agents on your managed nodes. SSH access and Python – that’s all you need. This makes it perfect for environments where you can’t (or won’t) install agent software everywhere.

Career Impact

Ansible is the gateway drug to Infrastructure as Code.

Configuration management skills separate sysadmins who scale from those who don’t. Ansible appears in 70%+ of DevOps job descriptions. Master it and you’re qualified for Senior Sysadmin, DevOps Engineer, and Platform Engineer roles paying £55k-85k+.

Diagram showing Ansible control node pushing configuration to multiple managed nodes via SSH

What You’ll Learn

  • What Ansible is and why it matters
  • Inventory management
  • Writing your first playbook
  • Essential modules
  • Best practices for real environments

Quick Reference

Concept What It Is Example
Control Node Machine running Ansible Your laptop, a jump server
Managed Node Machines Ansible manages Servers, network devices
Inventory List of managed nodes inventory.ini or dynamic
Playbook YAML file with tasks deploy.yml
Task Single action Install package, copy file
Module Code that performs tasks apt, copy, service
Role Reusable task collection webserver, database
Idempotent Safe to run multiple times Same result every time

Why Ansible?

The Problem It Solves

Without configuration management:

  • SSH into servers manually
  • Run commands one by one
  • Hope you remember to do all of them
  • No record of what you did
  • Drift between servers over time

With Ansible:

  • Define desired state in code
  • Apply to any number of servers
  • Version control your infrastructure
  • Consistent, documented, repeatable

Why Ansible Over Puppet/Chef?

Feature Ansible Puppet/Chef
Agent required No (SSH only) Yes
Language YAML Ruby DSL
Learning curve Gentle Steep
Idempotency Built-in Built-in
Enterprise features AWX/Tower Enterprise versions
Windows support Good Varies

Ansible’s agentless approach is its killer feature. No software to install, no ports to open, no agents to update.

Installation

Control Node Setup

# Ubuntu/Debian
sudo apt update
sudo apt install ansible -y

# RHEL/CentOS
sudo dnf install ansible -y

# pip (any OS with Python)
pip install ansible

# Verify
ansible --version

Managed Node Requirements

  • SSH access (key-based preferred)
  • Python installed (most Linux distros have it)
  • User with sudo privileges

Inventory: Defining Your Targets

Basic Inventory File

Create inventory.ini:

[webservers]
web01.example.com
web02.example.com
192.168.1.50

[databases]
db01.example.com
db02.example.com

[all:vars]
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/ansible_key

Group Structures

[webservers]
web01
web02

[databases]
db01

[london]
web01
db01

[newyork]
web02

# Parent groups
[production:children]
webservers
databases

Testing Connectivity

# Ping all hosts
ansible all -i inventory.ini -m ping

# Ping specific group
ansible webservers -i inventory.ini -m ping

# Run command on all hosts
ansible all -i inventory.ini -m shell -a "uptime"

Your First Playbook

The Structure

Create first-playbook.yml:

---
- name: My first playbook
  hosts: webservers
  become: yes  # Run as sudo

  tasks:
    - name: Ensure nginx is installed
      apt:
        name: nginx
        state: present
        update_cache: yes

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

Running the Playbook

ansible-playbook -i inventory.ini first-playbook.yml

What Happens

  1. Ansible connects to each host in webservers via SSH
  2. For each task, it checks current state
  3. If nginx isn’t installed, it installs it (idempotent)
  4. If nginx isn’t running, it starts it
  5. Reports what changed (or didn’t)

Essential Modules

Package Management

# apt (Debian/Ubuntu)
- name: Install packages
  apt:
    name:
      - nginx
      - vim
      - htop
    state: present
    update_cache: yes

# yum/dnf (RHEL/CentOS)
- name: Install packages
  dnf:
    name:
      - httpd
      - vim
    state: present

# package (generic - auto-detect package manager)
- name: Install package (auto-detect package manager)
  package:
    name: vim
    state: present

File Operations

# copy
- name: Copy config file
  copy:
    src: files/nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes

# template (with variables)
- name: Deploy config from template
  template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Restart nginx

# file (create directories, symlinks)
- name: Create directory
  file:
    path: /var/www/myapp
    state: directory
    owner: www-data
    group: www-data
    mode: '0755'

Service Management

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

- name: Restart service
  service:
    name: nginx
    state: restarted

Command Execution

# command (simple commands)
- name: Check disk space
  command: df -h
  register: disk_output

- name: Show disk space
  debug:
    var: disk_output.stdout_lines

# shell (when you need pipes/redirects)
- name: Find large files
  shell: find /var/log -type f -size +100M
  register: large_files

User Management

- name: Create user
  user:
    name: deploy
    groups: sudo
    shell: /bin/bash
    state: present

- name: Add SSH key for user
  authorized_key:
    user: deploy
    key: "{{ lookup('file', 'files/deploy_key.pub') }}"

Variables and Facts

Defining Variables

# In playbook
---
- hosts: webservers
  vars:
    http_port: 80
    app_name: myapp

  tasks:
    - name: Use variable
      debug:
        msg: "App {{ app_name }} runs on port {{ http_port }}"

In separate file (vars/main.yml):

http_port: 80
app_name: myapp
database_host: db01.example.com

Include in playbook:

- hosts: webservers
  vars_files:
    - vars/main.yml

Ansible Facts

Ansible automatically gathers system information:

- name: Show facts
  debug:
    msg: "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"

- name: Conditional based on OS
  apt:
    name: nginx
  when: ansible_os_family == "Debian"

- name: Different action for RHEL
  dnf:
    name: nginx
  when: ansible_os_family == "RedHat"

Common facts:

  • ansible_hostname
  • ansible_distribution
  • ansible_os_family
  • ansible_memory_mb
  • ansible_processor_cores
  • ansible_default_ipv4.address

Handlers and Notifications

Handlers run only when notified, and only once at the end:

---
- hosts: webservers
  become: yes

  tasks:
    - name: Copy nginx config
      copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: Restart nginx

    - name: Copy site config
      copy:
        src: site.conf
        dest: /etc/nginx/sites-available/default
      notify: Restart nginx

  handlers:
    - name: Restart nginx
      service:
        name: nginx
        state: restarted
Pro Tip: Even if both tasks change files, nginx only restarts once. This is more efficient and reduces service disruption.

Conditionals and Loops

Conditionals

- name: Install on Debian
  apt:
    name: nginx
  when: ansible_os_family == "Debian"

- name: Skip if package exists
  apt:
    name: special-package
  when: special_condition is defined

Loops

- name: Install multiple packages
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - vim
    - htop

- name: Create multiple users
  user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
  loop:
    - { name: 'alice', groups: 'sudo' }
    - { name: 'bob', groups: 'users' }

Roles: Organizing Playbooks

Role Structure

roles/
└── webserver/
    ├── tasks/
    │   └── main.yml
    ├── handlers/
    │   └── main.yml
    ├── templates/
    │   └── nginx.conf.j2
    ├── files/
    │   └── index.html
    ├── vars/
    │   └── main.yml
    └── defaults/
        └── main.yml

Using Roles

---
- hosts: webservers
  become: yes
  roles:
    - webserver
    - monitoring

Creating a Role

# Create role directory structure
ansible-galaxy init roles/webserver

Then populate roles/webserver/tasks/main.yml:

---
- name: Install nginx
  apt:
    name: nginx
    state: present

- name: Deploy config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Restart nginx

- name: Start nginx
  service:
    name: nginx
    state: started
    enabled: yes

Practical Playbooks

Playbook 1: Initial Server Setup

---
- name: Initial server setup
  hosts: all
  become: yes

  vars:
    admin_user: deploy
    ssh_port: 22

  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Install essential packages
      apt:
        name:
          - vim
          - htop
          - curl
          - git
          - ufw
        state: present

    - name: Create admin user
      user:
        name: "{{ admin_user }}"
        groups: sudo
        shell: /bin/bash

    - name: Add SSH key for admin
      authorized_key:
        user: "{{ admin_user }}"
        key: "{{ lookup('file', 'files/admin_key.pub') }}"

    - name: Configure UFW defaults
      ufw:
        direction: incoming
        policy: deny

    - name: Allow SSH
      ufw:
        rule: allow
        port: "{{ ssh_port }}"
        proto: tcp

    - name: Enable UFW
      ufw:
        state: enabled

Playbook 2: Deploy Application

---
- name: Deploy web application
  hosts: webservers
  become: yes

  vars:
    app_name: myapp
    app_path: /var/www/myapp
    app_repo: https://github.com/company/myapp.git

  tasks:
    - name: Install dependencies
      apt:
        name:
          - nginx
          - python3
          - python3-pip
        state: present

    - name: Create app directory
      file:
        path: "{{ app_path }}"
        state: directory
        owner: www-data
        group: www-data

    - name: Clone/update repository
      git:
        repo: "{{ app_repo }}"
        dest: "{{ app_path }}"
        version: main
      notify: Restart app

    - name: Install Python requirements
      pip:
        requirements: "{{ app_path }}/requirements.txt"
      notify: Restart app

    - name: Deploy nginx config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ app_name }}
      notify: Restart nginx

    - name: Enable site
      file:
        src: /etc/nginx/sites-available/{{ app_name }}
        dest: /etc/nginx/sites-enabled/{{ app_name }}
        state: link
      notify: Restart nginx

  handlers:
    - name: Restart nginx
      service:
        name: nginx
        state: restarted

    - name: Restart app
      systemd:
        name: "{{ app_name }}"
        state: restarted

Best Practices

Organization

  1. Use roles – Don’t put everything in one playbook
  2. Separate inventory – Different files for prod, staging, dev
  3. Variables hierarchy – Defaults < group_vars < host_vars
  4. Version control – Your playbooks are code, treat them like it

Security

  1. Ansible Vault – Encrypt sensitive variables
  2. Limit permissions – Use become only when needed
  3. Key-based auth – Never store passwords in inventory
  4. Audit playbooks – Review before running in production
# Ansible Vault commands
ansible-vault create secrets.yml
ansible-vault edit secrets.yml
ansible-playbook --ask-vault-pass playbook.yml

Reliability

  1. Always test – Use --check (dry run) and --diff
  2. Be idempotent – Running twice should have same result
  3. Handle errors – Use ignore_errors, failed_when, blocks
  4. Backup before changebackup: yes on file operations

Interview Questions

Q1: “What makes Ansible different from Puppet or Chef?”

Good Answer: “Ansible is agentless – it connects via SSH and doesn’t require software installed on managed nodes. This makes it easier to adopt in existing environments. It uses YAML for playbooks, which is more approachable than Ruby DSLs. The trade-off is that push-based execution means you need to run playbooks to apply changes, whereas Puppet/Chef agents continuously enforce state.”

Q2: “Explain idempotency and why it matters in configuration management.”

Good Answer: “Idempotency means running something multiple times produces the same result as running it once. In Ansible, if I run a playbook that ensures nginx is installed and running, it checks the current state first. If nginx is already installed and running, it does nothing. This is critical because it means I can safely re-run playbooks without worrying about unintended side effects or errors from trying to install something that’s already there.”

Q3: “How would you handle different configurations for production vs staging environments?”

Good Answer: “I’d use group_vars. Create group_vars/production.yml and group_vars/staging.yml with environment-specific variables like database hosts, credentials, and feature flags. The playbook stays the same but references variables. Inventory files define which hosts belong to which environment. Sensitive variables go in Ansible Vault. This way, one playbook works for all environments with the differences handled by variables.”

Career Application

On Your Resume

  • “Managed 100+ server configuration using Ansible playbooks and roles”
  • “Reduced server deployment time from 2 hours to 15 minutes with Ansible automation”
  • “Implemented infrastructure as code practices using Ansible and Git”

Demonstrate

  • Understanding of idempotency
  • Role organization
  • Security practices (Vault)
  • Real-world problem solving

Next Steps

  • Practice: Pick a manual setup process and convert it to an Ansible playbook
  • Explore: Ansible Galaxy for pre-built roles
  • Continue series: Learn infrastructure provisioning with Terraform

Configuration management is how you scale. What starts as one playbook becomes your entire infrastructure defined as code. Start small, think big.

The RTM Essential Stack - Gear I Actually Use

Enjoyed this guide?

New articles on Linux, homelab, cloud, and automation every 2 days. No spam, unsubscribe anytime.

Scroll to Top