}

Ansible for Linux Sysadmins: Automate Server Configuration with Playbooks (2026)

Ansible is the go-to tool for automating Linux server configuration without agents or daemons. Everything runs over SSH. This guide focuses on practical, real-world use cases for sysadmins managing fleets of Ubuntu/Debian or RHEL/Rocky Linux servers.

What Ansible Does

Ansible lets you describe your infrastructure as code (YAML playbooks) and apply it idempotently — run the same playbook 100 times, get the same result. Common use cases:

  • Install and configure packages across many servers
  • Deploy application updates
  • Manage users, SSH keys, and sudo rules
  • Set up cron jobs, systemd services, firewalls
  • Enforce compliance and security baselines

Install Ansible

On Ubuntu/Debian control node:

sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible

ansible --version
# ansible [core 2.17.x]

On RHEL/Rocky/AlmaLinux:

sudo dnf install -y epel-release
sudo dnf install -y ansible

Via pip (latest version, any OS):

pip install ansible

SSH Key Setup

Ansible connects via SSH. Set up key-based auth to all managed nodes:

# Generate key if you don't have one
ssh-keygen -t ed25519 -C "ansible"

# Copy to each managed node
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server1
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server2

Inventory File

The inventory tells Ansible which servers to manage:

inventory/hosts.ini:

[webservers]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11

[dbservers]
db1 ansible_host=192.168.1.20

[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/id_ed25519
ansible_python_interpreter=/usr/bin/python3

Or YAML format inventory/hosts.yml:

all:
  children:
    webservers:
      hosts:
        web1:
          ansible_host: 192.168.1.10
        web2:
          ansible_host: 192.168.1.11
    dbservers:
      hosts:
        db1:
          ansible_host: 192.168.1.20
  vars:
    ansible_user: ubuntu
    ansible_ssh_private_key_file: ~/.ssh/id_ed25519

Test connectivity:

ansible all -i inventory/hosts.ini -m ping
# web1 | SUCCESS => {"ping": "pong"}
# web2 | SUCCESS => {"ping": "pong"}

ansible.cfg

Create ansible.cfg in your project root:

[defaults]
inventory = inventory/hosts.ini
remote_user = ubuntu
private_key_file = ~/.ssh/id_ed25519
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml

[privilege_escalation]
become = True
become_method = sudo
become_user = root

Your First Playbook

playbooks/setup-webserver.yml:

---
- name: Configure web servers
  hosts: webservers
  become: true

  vars:
    nginx_port: 80
    app_user: webapp

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

    - name: Install required packages
      apt:
        name:
          - nginx
          - git
          - curl
          - ufw
        state: present

    - name: Create application user
      user:
        name: "{{ app_user }}"
        shell: /bin/bash
        create_home: true
        state: present

    - name: Deploy nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
        owner: root
        group: root
        mode: "0644"
      notify: Reload nginx

    - name: Enable nginx
      systemd:
        name: nginx
        enabled: true
        state: started

    - name: Configure UFW - allow SSH
      ufw:
        rule: allow
        port: "22"
        proto: tcp

    - name: Configure UFW - allow HTTP
      ufw:
        rule: allow
        port: "{{ nginx_port }}"
        proto: tcp

    - name: Enable UFW
      ufw:
        state: enabled
        policy: deny

  handlers:
    - name: Reload nginx
      systemd:
        name: nginx
        state: reloaded

templates/nginx.conf.j2:

server {
    listen {{ nginx_port }};
    server_name _;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
}

Run the playbook:

ansible-playbook playbooks/setup-webserver.yml

# Dry run (check mode)
ansible-playbook playbooks/setup-webserver.yml --check

# Verbose output
ansible-playbook playbooks/setup-webserver.yml -v

Variables and Group Vars

Organize variables by group:

inventory/
  group_vars/
    all.yml          # applies to all hosts
    webservers.yml   # applies to webservers group
    dbservers.yml    # applies to dbservers group
  host_vars/
    web1.yml         # applies only to web1

inventory/group_vars/webservers.yml:

nginx_worker_processes: auto
nginx_worker_connections: 1024
app_domain: example.com

inventory/group_vars/all.yml:

ntp_servers:
  - 0.pool.ntp.org
  - 1.pool.ntp.org
timezone: UTC
admin_email: [email protected]

Ansible Roles

Roles organize playbooks into reusable components:

# Create role structure
ansible-galaxy init roles/nginx

Creates:

roles/nginx/
├── tasks/main.yml
├── handlers/main.yml
├── templates/
├── files/
├── vars/main.yml
├── defaults/main.yml
└── meta/main.yml

roles/nginx/tasks/main.yml:

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

- name: Copy nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Reload nginx

- name: Ensure nginx is running
  systemd:
    name: nginx
    state: started
    enabled: true

roles/nginx/handlers/main.yml:

---
- name: Reload nginx
  systemd:
    name: nginx
    state: reloaded

- name: Restart nginx
  systemd:
    name: nginx
    state: restarted

Use the role in a playbook:

---
- name: Configure web servers
  hosts: webservers
  become: true
  roles:
    - nginx
    - { role: certbot, when: use_ssl | default(false) }

Common Modules You'll Use Every Day

Package management:

- apt:
    name: ["vim", "htop", "tmux"]
    state: present
    update_cache: true

- apt:
    name: "nginx=1.24.*"
    state: present

File and directory operations:

- file:
    path: /opt/myapp
    state: directory
    owner: webapp
    group: webapp
    mode: "0755"

- copy:
    src: files/config.yml
    dest: /etc/myapp/config.yml
    backup: true

- lineinfile:
    path: /etc/ssh/sshd_config
    regexp: "^PasswordAuthentication"
    line: "PasswordAuthentication no"
    notify: Restart sshd

User management:

- user:
    name: deploy
    groups: ["sudo", "docker"]
    append: true
    shell: /bin/bash

- authorized_key:
    user: deploy
    key: "{{ lookup('file', 'files/deploy.pub') }}"
    state: present

Systemd services:

- systemd:
    name: myapp
    state: started
    enabled: true
    daemon_reload: true

- copy:
    dest: /etc/systemd/system/myapp.service
    content: |
      [Unit]
      Description=My Application
      After=network.target

      [Service]
      User=webapp
      WorkingDirectory=/opt/myapp
      ExecStart=/opt/myapp/bin/myapp
      Restart=on-failure

      [Install]
      WantedBy=multi-user.target
  notify: ["Reload systemd", "Restart myapp"]

Running commands:

- command: /opt/myapp/bin/migrate
  args:
    chdir: /opt/myapp
  when: migration_needed | default(false)

- shell: |
    source /etc/profile
    /opt/myapp/bin/setup.sh
  register: setup_result
  changed_when: "'already configured' not in setup_result.stdout"

Secrets with Ansible Vault

Encrypt sensitive variables:

# Create encrypted file
ansible-vault create inventory/group_vars/all/vault.yml

# Edit existing
ansible-vault edit inventory/group_vars/all/vault.yml

# Encrypt a single string
ansible-vault encrypt_string 'supersecret' --name 'db_password'

vault.yml (encrypted at rest):

vault_db_password: supersecret
vault_api_key: abc123

Reference in regular vars:

# group_vars/all/main.yml
db_password: "{{ vault_db_password }}"

Run playbook with vault:

ansible-playbook playbook.yml --ask-vault-pass
# or use a password file
ansible-playbook playbook.yml --vault-password-file ~/.vault_pass

Tags for Selective Execution

- name: Install packages
  apt:
    name: nginx
  tags: [install, nginx]

- name: Deploy config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  tags: [config, nginx]

Run only specific tags:

# Only run config tasks
ansible-playbook playbook.yml --tags config

# Skip install tasks
ansible-playbook playbook.yml --skip-tags install

Practical Example: Deploy a Python App

---
- name: Deploy Python application
  hosts: webservers
  become: true

  vars:
    app_dir: /opt/myapp
    app_user: webapp
    app_repo: https://github.com/myorg/myapp.git
    app_branch: main
    venv_dir: "{{ app_dir }}/venv"

  tasks:
    - name: Install system dependencies
      apt:
        name: [python3, python3-pip, python3-venv, git]
        state: present

    - name: Create app directory
      file:
        path: "{{ app_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_user }}"

    - name: Clone/update repository
      git:
        repo: "{{ app_repo }}"
        dest: "{{ app_dir }}"
        version: "{{ app_branch }}"
        force: true
      become_user: "{{ app_user }}"
      notify: Restart app

    - name: Create virtualenv and install deps
      pip:
        requirements: "{{ app_dir }}/requirements.txt"
        virtualenv: "{{ venv_dir }}"
        virtualenv_command: python3 -m venv
      become_user: "{{ app_user }}"

    - name: Deploy environment file
      template:
        src: templates/env.j2
        dest: "{{ app_dir }}/.env"
        owner: "{{ app_user }}"
        mode: "0600"
      notify: Restart app

    - name: Deploy systemd service
      template:
        src: templates/myapp.service.j2
        dest: /etc/systemd/system/myapp.service
      notify: ["Reload systemd", "Restart app"]

  handlers:
    - name: Reload systemd
      systemd:
        daemon_reload: true

    - name: Restart app
      systemd:
        name: myapp
        state: restarted
        enabled: true

Project Structure

ansible-project/
├── ansible.cfg
├── inventory/
│   ├── hosts.ini
│   ├── group_vars/
│   │   ├── all/
│   │   │   ├── main.yml
│   │   │   └── vault.yml
│   │   └── webservers.yml
│   └── host_vars/
│       └── web1.yml
├── playbooks/
│   ├── setup.yml
│   └── deploy.yml
├── roles/
│   ├── nginx/
│   └── python-app/
└── templates/
    └── nginx.conf.j2

Summary

You now know the core Ansible workflow:

  1. Inventory — define your hosts and groups
  2. Playbooks — YAML files describing desired state
  3. Modules — apt, file, user, systemd, template, git...
  4. Roles — reusable, shareable task collections
  5. Vault — encrypt secrets at rest
  6. Tags — run subsets of your playbook

Ansible's agentless design and idempotent modules make it ideal for sysadmins who want reliable, repeatable server configuration without complex infrastructure. Start with a single playbook to configure one server type, then grow into roles as your fleet expands.

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro