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:
- Inventory — define your hosts and groups
- Playbooks — YAML files describing desired state
- Modules — apt, file, user, systemd, template, git...
- Roles — reusable, shareable task collections
- Vault — encrypt secrets at rest
- 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.