Ansible Playbook Basics for Linux Server Configuration (Step-by-Step)

ansible playbook basics linux custom-ansible-featured.png

Once you’re managing more than two or three Linux servers, doing the same apt install and config edits by hand on each one stops being reasonable — you forget a step, servers drift apart, and nobody remembers exactly what “web02” has installed that “web01” doesn’t. Ansible solves this without requiring an agent on every managed host — it just needs SSH.

This guide builds a small two-server lab: an inventory file, ad-hoc commands to sanity-check connectivity, a real playbook that installs and hardens nginx, and finally reorganizing that playbook into a reusable role.

Step 1: Install Ansible on the Control Node

Ansible only needs to be installed on one machine — the control node you run commands from. On Ubuntu/Debian: sudo apt install ansible. Managed hosts need nothing but Python and an SSH server, which almost every Linux distro ships with by default.

Install Ansible on control node for Linux automation
Step 1: Install Ansible on the Control Node

Step 2: Set Up SSH Key Access to Every Managed Host

Generate a dedicated key for Ansible (ssh-keygen -f ~/.ssh/ansible_key) and copy it to each managed host: ssh-copy-id -i ~/.ssh/ansible_key.pub deploy@web01. Ansible connects over plain SSH, so if you can SSH in manually without a password prompt, Ansible can too.

SSH key authentication setup for Ansible managed hosts
Step 2: Set Up SSH Key Access to Every Managed Host

Step 3: Create the Inventory File

The inventory tells Ansible which hosts exist and how to group them. Create inventory.ini with a [webservers] group listing your hosts, plus a [webservers:vars] section for the SSH user and key path so you don’t repeat them on every command.

Ansible inventory.ini file with webservers group
Step 3: Create the Inventory File

Step 4: Test Connectivity With an Ad-Hoc Command

Before writing any YAML, confirm everything is wired up: ansible webservers -m ping. This uses the built-in ping module (not ICMP — it’s an SSH connectivity + Python check) and should return SUCCESS / “pong” for every host in the group.

Ansible ad-hoc ping module testing SSH connectivity
Step 4: Test Connectivity With an Ad-Hoc Command

Step 5: Write Your First Playbook

Create harden.yml with a single play targeting webservers, using become: true for sudo, and two tasks: apt: name=nginx state=present to install nginx, and a lineinfile or template task to disable password SSH login in /etc/ssh/sshd_config.

Writing first Ansible playbook YAML for nginx and SSH hardening
Step 5: Write Your First Playbook

Step 6: Dry-Run With –check Before Applying

Run ansible-playbook harden.yml --check --diff first. This simulates the run and shows what would change without touching anything — invaluable before you trust a new playbook against real servers. Only drop --check once the diff output looks correct.

Ansible playbook dry run with check and diff flags
Step 6: Dry-Run With –check Before Applying

Step 7: Run It for Real and Read the Recap

Run ansible-playbook harden.yml. Each task reports ok (no change needed, already matches the desired state), changed (it made a change), or failed. The PLAY RECAP at the end summarizes counts per host — that’s your quick pass/fail signal.

Ansible playbook run PLAY RECAP output ok changed failed
Step 7: Run It for Real and Read the Recap

Step 8: Reorganize Into a Reusable Role

Once a playbook grows past a handful of tasks, split it into a role: ansible-galaxy init roles/nginx creates the standard tasks/, templates/, handlers/, defaults/ layout. Move your tasks into roles/nginx/tasks/main.yml and your config file into a Jinja2 .j2 template — now the same role can be reused across projects.

Ansible role directory structure with tasks templates handlers
Step 8: Reorganize Into a Reusable Role

Example harden.yml

---
- name: Harden and configure web servers
  hosts: webservers
  become: true
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: true

    - name: Disable SSH password authentication
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication no'
      notify: Restart sshd

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

Idempotency — the core idea

Every Ansible module is designed to be idempotent: running the same playbook twice in a row should report the second run as all ok, with nothing changed. If a task keeps showing changed on every run, something in it isn’t idempotent — that’s usually a sign to use a proper module instead of a raw shell task.

Related tutorials

Terminal screenshots are original illustrations created for Gnome IT Solutions (blog.gnomeitsolutions.com).