All Articles

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.

DevOpsBoysMar 13, 20268 min read
Share:Tweet

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:

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

RHEL/CentOS/Amazon Linux:

bash
sudo yum install -y epel-release
sudo yum install -y ansible

macOS (with Homebrew):

bash
brew install ansible

Via pip (any OS):

bash
pip3 install ansible

Verify the installation:

bash
ansible --version

You 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):

bash
ssh-keygen -t ed25519 -C "ansible-control" -f ~/.ssh/ansible_key

This 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:

bash
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.11

Or manually — paste the contents of ~/.ssh/ansible_key.pub into ~/.ssh/authorized_keys on each target server.

Test SSH access:

bash
ssh -i ~/.ssh/ansible_key user@192.168.1.10

If 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:

bash
mkdir ~/ansible-project
cd ~/ansible-project

Create inventory.ini:

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/python3

Breaking 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 inventory
  • ansible_user — the SSH user to connect as
  • ansible_ssh_private_key_file — path to your private key
  • ansible_python_interpreter — use Python 3 (avoids deprecation warnings)

Test connectivity with an ad-hoc command:

bash
ansible all -i inventory.ini -m ping

If 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:

yaml
---
- 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:

bash
ansible-playbook -i inventory.ini site.yml

Output:

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:

yaml
---
- 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: restarted

Notice {{ }} — 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):

yaml
vars_files:
  - vars/main.yml
  - vars/secrets.yml

vars/main.yml:

yaml
nginx_port: 80
app_domain: myapp.example.com
deploy_dir: /var/www/myapp

Step 6: Loops

When you need to repeat a task for multiple items, use loops:

yaml
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
      - htop

For more complex iterations with multiple values per item:

yaml
  - 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:

yaml
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:

bash
# See all facts for a host
ansible web1 -i inventory.ini -m setup

Common facts:

  • ansible_os_family — "Debian", "RedHat", "Archlinux"
  • ansible_distribution — "Ubuntu", "CentOS", "Amazon"
  • ansible_distribution_version — "22.04", "8", etc.
  • ansible_hostname — server hostname
  • ansible_default_ipv4.address — primary IP
  • ansible_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:

bash
ansible-galaxy init roles/nginx

This 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:

yaml
---
- 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: true

roles/nginx/templates/nginx.conf.j2:

nginx
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:

yaml
---
- 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:

bash
ansible-vault encrypt vars/secrets.yml

You'll be prompted for a vault password. The file is now AES-256 encrypted.

Create an encrypted variable directly:

bash
ansible-vault encrypt_string 'MyS3cur3P@ssw0rd' --name 'db_password'

Output:

yaml
db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          6162333161343...

Paste this directly into your vars file.

Run a playbook with vault:

bash
# 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_pass

Step 10: Dry Run Before Applying

Before running a playbook on production, always do a dry run:

bash
# 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

bash
# 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.docker

Where 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.

Newsletter

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

Comments