How to Set Up Ansible from Scratch (Complete Beginner Guide 2026)
Learn Ansible from zero — install it, configure SSH, write your first playbook, use variables and loops, and automate real server tasks step by step.
Ansible is one of the most practical tools in a DevOps engineer's toolkit. You don't need to install anything on remote servers. No agents, no daemons, no special software. Just SSH access and a text editor — and you can automate configuration across hundreds of servers.
This guide starts from zero and builds up to real, useful automation.
What Is Ansible and Why Should You Care?
Ansible is an open-source automation tool that lets you define infrastructure configuration as code — YAML files called playbooks — and run them against any number of servers over SSH.
Before Ansible, configuring servers meant:
- SSHing into each server manually
- Running the same commands on each one
- Hoping you didn't miss a step on server 7 of 15
- Having no record of what you actually ran
Ansible fixes all of this. You write the configuration once. You run it everywhere. It's idempotent — running it twice gives the same result as running it once. It's version-controlled. It's readable.
When to use Ansible:
- Configuring new servers (install packages, set up users, copy configs)
- Application deployment (pull code, restart services)
- Security hardening (configure firewalls, disable unnecessary services)
- Routine maintenance (update packages, rotate logs)
When NOT to use Ansible:
- Provisioning cloud infrastructure (use Terraform for that)
- Managing Kubernetes resources (use Helm/ArgoCD)
- Anything that needs real-time orchestration with complex dependencies
Prerequisites
You need:
- One control node: your laptop or a dedicated server running Linux/macOS (Windows requires WSL)
- One or more target servers running Linux
- SSH access to those servers (password or key-based)
- Python 3 on target servers (usually pre-installed on most Linux distros)
Step 1: Install Ansible on the Control Node
Ubuntu/Debian:
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansibleRHEL/CentOS/Amazon Linux:
sudo yum install -y epel-release
sudo yum install -y ansiblemacOS (with Homebrew):
brew install ansibleVia pip (any OS):
pip3 install ansibleVerify the installation:
ansible --versionYou should see something like:
ansible [core 2.16.0]
python version = 3.11.6
...
Step 2: Set Up SSH Key Authentication
Ansible uses SSH to connect to target servers. You can use password-based auth, but key-based is faster, more secure, and doesn't require interactive prompts.
Generate an SSH key pair on your control node (if you don't have one):
ssh-keygen -t ed25519 -C "ansible-control" -f ~/.ssh/ansible_keyThis creates:
~/.ssh/ansible_key→ private key (keep this secret)~/.ssh/ansible_key.pub→ public key (copy this to servers)
Copy your public key to each target server:
ssh-copy-id -i ~/.ssh/ansible_key.pub user@192.168.1.10
ssh-copy-id -i ~/.ssh/ansible_key.pub user@192.168.1.11Or manually — paste the contents of ~/.ssh/ansible_key.pub into ~/.ssh/authorized_keys on each target server.
Test SSH access:
ssh -i ~/.ssh/ansible_key user@192.168.1.10If you get a shell prompt without a password, you're ready.
Step 3: Create an Inventory File
An inventory file tells Ansible which servers to manage and how to reach them.
Create a project directory and inventory:
mkdir ~/ansible-project
cd ~/ansible-projectCreate inventory.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/ansible_key
ansible_python_interpreter=/usr/bin/python3Breaking this down:
[webservers]— a group name. You can target all servers in this group at once.ansible_host— the actual IP or hostname to connect to[all:vars]— variables applied to every server in the inventoryansible_user— the SSH user to connect asansible_ssh_private_key_file— path to your private keyansible_python_interpreter— use Python 3 (avoids deprecation warnings)
Test connectivity with an ad-hoc command:
ansible all -i inventory.ini -m pingIf everything is working:
web1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
web2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
db1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
If you see UNREACHABLE, check your SSH key, IP address, and firewall rules.
Step 4: Your First Playbook
A playbook is a YAML file that defines what tasks to run on which servers.
Create site.yml:
---
- name: Configure web servers
hosts: webservers
become: true # Run as sudo/root
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600 # Only update if cache is older than 1 hour
- name: Install nginx
apt:
name: nginx
state: present # present = installed, absent = removed
- name: Start and enable nginx
service:
name: nginx
state: started
enabled: true # Start on boot
- name: Create a custom index page
copy:
content: "<h1>Hello from {{ inventory_hostname }}</h1>"
dest: /var/www/html/index.html
owner: www-data
group: www-data
mode: '0644'Run the playbook:
ansible-playbook -i inventory.ini site.ymlOutput:
PLAY [Configure web servers] ****
TASK [Gathering Facts] **
ok: [web1]
ok: [web2]
TASK [Update apt cache] **
changed: [web1]
changed: [web2]
TASK [Install nginx] **
changed: [web1]
changed: [web2]
TASK [Start and enable nginx] **
changed: [web1]
changed: [web2]
TASK [Create a custom index page] **
changed: [web1]
changed: [web2]
PLAY RECAP *****
web1 : ok=5 changed=4 unreachable=0 failed=0
web2 : ok=5 changed=4 unreachable=0 failed=0
Run it again — because Ansible is idempotent, tasks that are already complete show ok instead of changed. No duplicate work.
Step 5: Variables
Hardcoding values in playbooks is bad practice. Use variables to make playbooks reusable.
Inline variables in the playbook:
---
- name: Configure web servers
hosts: webservers
become: true
vars:
nginx_port: 80
app_user: "www-data"
packages:
- nginx
- curl
- git
tasks:
- name: Install required packages
apt:
name: "{{ packages }}"
state: present
- name: Configure nginx port
lineinfile:
path: /etc/nginx/sites-available/default
regexp: 'listen 80'
line: "listen {{ nginx_port }};"
notify: Restart nginx
handlers:
- name: Restart nginx
service:
name: nginx
state: restartedNotice {{ }} — that's Jinja2 templating syntax. Variables go inside double curly braces.
Handlers run only when notified and only once, even if notified multiple times. Perfect for service restarts.
External variable files (vars_files):
vars_files:
- vars/main.yml
- vars/secrets.ymlvars/main.yml:
nginx_port: 80
app_domain: myapp.example.com
deploy_dir: /var/www/myappStep 6: Loops
When you need to repeat a task for multiple items, use loops:
tasks:
- name: Create application directories
file:
path: "{{ item }}"
state: directory
owner: www-data
mode: '0755'
loop:
- /var/www/myapp
- /var/www/myapp/logs
- /var/www/myapp/uploads
- /var/www/myapp/config
- name: Install multiple packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql-client
- redis-tools
- htopFor more complex iterations with multiple values per item:
- name: Create system users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop:
- { name: "deploy", groups: "sudo,www-data" }
- { name: "monitor", groups: "adm" }
- { name: "backup", groups: "" }Step 7: Conditionals
Run tasks only when certain conditions are true:
tasks:
- name: Install Apache on Debian/Ubuntu
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Install Apache on RHEL/CentOS
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
- name: Restart service only in production
service:
name: myapp
state: restarted
when: env == "production"ansible_os_family is a fact — information Ansible automatically gathers about each target server at the start of each play. Other useful facts:
# See all facts for a host
ansible web1 -i inventory.ini -m setupCommon facts:
ansible_os_family— "Debian", "RedHat", "Archlinux"ansible_distribution— "Ubuntu", "CentOS", "Amazon"ansible_distribution_version— "22.04", "8", etc.ansible_hostname— server hostnameansible_default_ipv4.address— primary IPansible_memtotal_mb— total RAM
Step 8: Ansible Roles (Organizing Large Playbooks)
As your automation grows, a single playbook file becomes unmanageable. Roles are Ansible's way of organizing reusable automation into a standard directory structure.
Create a role:
ansible-galaxy init roles/nginxThis creates:
roles/nginx/
├── tasks/
│ └── main.yml # Main list of tasks
├── handlers/
│ └── main.yml # Handlers (service restarts, etc.)
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── index.html # Static files to copy
├── vars/
│ └── main.yml # Role-specific variables
├── defaults/
│ └── main.yml # Default values (overridable)
└── meta/
└── main.yml # Role metadata and dependencies
roles/nginx/tasks/main.yml:
---
- name: Install nginx
apt:
name: nginx
state: present
- name: Deploy nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
- name: Enable and start nginx
service:
name: nginx
state: started
enabled: trueroles/nginx/templates/nginx.conf.j2:
worker_processes {{ ansible_processor_vcpus }};
events {
worker_connections 1024;
}
http {
server {
listen {{ nginx_port | default(80) }};
server_name {{ app_domain }};
root {{ deploy_dir }};
index index.html;
}
}Use the role in your site.yml:
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- nginx
- { role: deploy_app, app_version: "1.4.2" }Step 9: Ansible Vault (Secrets Management)
Never store passwords, API keys, or database credentials in plain-text YAML files. Use Ansible Vault to encrypt sensitive data.
Encrypt a variable file:
ansible-vault encrypt vars/secrets.ymlYou'll be prompted for a vault password. The file is now AES-256 encrypted.
Create an encrypted variable directly:
ansible-vault encrypt_string 'MyS3cur3P@ssw0rd' --name 'db_password'Output:
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
6162333161343...Paste this directly into your vars file.
Run a playbook with vault:
# Prompt for vault password
ansible-playbook -i inventory.ini site.yml --ask-vault-pass
# Or use a password file
echo "my-vault-password" > ~/.vault_pass
chmod 600 ~/.vault_pass
ansible-playbook -i inventory.ini site.yml --vault-password-file ~/.vault_passStep 10: Dry Run Before Applying
Before running a playbook on production, always do a dry run:
# Check mode — shows what would change without making changes
ansible-playbook -i inventory.ini site.yml --check
# Diff mode — shows file content changes
ansible-playbook -i inventory.ini site.yml --check --diff--check is your safety net. Run it before every production change.
Quick Reference
# Test connectivity
ansible all -i inventory.ini -m ping
# Run a command on all hosts
ansible all -i inventory.ini -a "uptime"
# Run command as root
ansible all -i inventory.ini -a "apt update" --become
# Run specific tasks with tags
ansible-playbook site.yml --tags "nginx,ssl"
# Limit to specific hosts
ansible-playbook -i inventory.ini site.yml --limit web1
# Dry run
ansible-playbook -i inventory.ini site.yml --check --diff
# List tasks without running
ansible-playbook -i inventory.ini site.yml --list-tasks
# Install a role from Ansible Galaxy
ansible-galaxy install geerlingguy.dockerWhere to Go From Here
Once you're comfortable with playbooks, explore:
ansible-lint: Linting tool to catch mistakes before they run- AWX / Ansible Automation Platform: Web UI and API for running Ansible at scale
- Dynamic inventory: Auto-discover AWS EC2 instances instead of static IP lists
- Molecule: Testing framework for Ansible roles
If you want to practice on a real Linux environment with structured labs — including Ansible, Linux administration, and cloud automation — KodeKloud is worth it:
👉 Hands-on Ansible and Linux labs at KodeKloud
Ansible has a gentle learning curve, but the payoff is immediate. Once you've automated a deployment that used to take 45 minutes of SSH, you'll never go back.
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
Linux Commands Every DevOps Engineer Must Know (2026)
The complete Linux command reference for DevOps engineers in 2026. Master file management, process control, networking, system monitoring, SSH, permissions, and shell scripting with real-world examples.
Build a Kubernetes Cluster with kubeadm from Scratch (2026)
Step-by-step guide to building a real multi-node Kubernetes cluster using kubeadm — no managed services, no shortcuts.
How to Set Up GitLab CI/CD from Scratch (2026 Complete Tutorial)
A practical step-by-step guide to setting up GitLab CI/CD pipelines from zero — covering runners, pipeline stages, Docker builds, deployment to Kubernetes, and best practices.