Tutorial

Ansible for Beginners: Automate Server Configuration in 30 Minutes

May 31, 2026

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

What Is Ansible and Why Should You Care?

Imagine you manage 10 servers. A security patch needs to be applied to all of them. You SSH into each one, run the same commands, and hope you do not miss a step or make a typo on server number seven. Now imagine you manage 50 servers. Or 200. Manual server configuration does not scale, and it is frighteningly error-prone.

Ansible is an open-source automation tool that lets you describe your server configuration in simple, human-readable YAML files and apply those configurations across hundreds of servers with a single command. Unlike Puppet or Chef, Ansible is agentless — it connects to your servers over SSH and runs commands. There is nothing to install on the managed servers, which makes getting started incredibly fast.

0
Agents to install on managed servers
SSH
Only requirement: SSH access

By the end of this guide, you will have Ansible installed, understand inventory files, write your first playbook, and automate a complete LAMP stack deployment. All in about 30 minutes of reading and practicing.

Installing Ansible

Ansible runs on your control machine (your laptop or a management server) and connects to managed nodes via SSH. You only need to install Ansible on the control machine.

# Ubuntu/Debian
$ sudo apt update
$ sudo apt install -y ansible

# macOS (with Homebrew)
$ brew install ansible

# pip (any platform)
$ pip install ansible

# Verify installation
$ ansible --version
ansible [core 2.17.0]
python version = 3.12.3

The Inventory File: Defining Your Servers

The inventory file tells Ansible which servers to manage. You can group servers by role, environment, or any other criteria that makes sense for your infrastructure.

INI Format (Simple)

# inventory.ini
[webservers]
web1.example.com
web2.example.com
192.168.1.50 ansible_port=2222

[dbservers]
db1.example.com ansible_user=dbadmin
db2.example.com

[production:children]
webservers
dbservers

[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key

YAML Format (Structured)

# inventory.yml
all:
  vars:
    ansible_user: deploy
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
    dbservers:
      hosts:
        db1.example.com:
          ansible_user: dbadmin

Ad-Hoc Commands: Quick Tasks Without Playbooks

Before writing playbooks, you can run one-off commands across your servers using ad-hoc commands. These are perfect for quick tasks like checking connectivity, gathering information, or making simple changes.

# Test connectivity to all servers
$ ansible all -i inventory.ini -m ping
web1.example.com | SUCCESS => {"ping": "pong"}
web2.example.com | SUCCESS => {"ping": "pong"}
db1.example.com | SUCCESS => {"ping": "pong"}

# Check disk space on web servers
$ ansible webservers -i inventory.ini -m shell -a "df -h /"

# Check uptime on all servers
$ ansible all -i inventory.ini -m command -a "uptime"

# Install a package on all web servers
$ ansible webservers -i inventory.ini -m apt -a "name=nginx state=present" -b

# Restart a service
$ ansible webservers -i inventory.ini -m service -a "name=nginx state=restarted" -b
The -b flag: The -b (or --become) flag tells Ansible to use privilege escalation (sudo) on the remote server. Most system administration tasks require root privileges, so you will use this flag frequently.

Your First Playbook

Playbooks are YAML files that describe a series of tasks to execute on your servers. They are the core of Ansible automation. Let us start with a simple playbook that installs and configures Nginx:

# setup-nginx.yml
---
- name: Setup Nginx web server
  hosts: webservers
  become: true

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

    - name: Install Nginx
      apt:
        name: nginx
        state: present

    - name: Copy custom nginx config
      copy:
        src: files/nginx.conf
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Restart Nginx

    - name: Ensure Nginx is running and enabled
      service:
        name: nginx
        state: started
        enabled: yes

  handlers:
    - name: Restart Nginx
      service:
        name: nginx
        state: restarted
# Run the playbook
$ ansible-playbook -i inventory.ini setup-nginx.yml

PLAY [Setup Nginx web server] ************************************

TASK [Gathering Facts] *******************************************
ok: [web1.example.com]
ok: [web2.example.com]

TASK [Update apt cache] ******************************************
ok: [web1.example.com]
ok: [web2.example.com]

TASK [Install Nginx] *********************************************
changed: [web1.example.com]
changed: [web2.example.com]

PLAY RECAP *******************************************************
web1.example.com : ok=4 changed=2 unreachable=0 failed=0
web2.example.com : ok=4 changed=2 unreachable=0 failed=0

Understanding Idempotency

The most important concept in Ansible is idempotency. An idempotent task produces the same result whether you run it once or a hundred times. If Nginx is already installed, the apt module will report "ok" instead of "changed" and skip the installation. This means you can safely run your playbooks repeatedly without worrying about unintended side effects.

Run it again, nothing breaks: If you run the playbook above a second time, every task will report "ok" instead of "changed". Ansible checks the current state before making changes. This is fundamentally different from a shell script, which would try to install Nginx again regardless.

Essential Ansible Modules

Ansible ships with hundreds of modules for managing every aspect of your servers. Here are the ones you will use most often:

ModulePurposeExample
apt / yumPackage managementapt: name=nginx state=present
copyCopy files to remote serverscopy: src=app.conf dest=/etc/nginx/
templateCopy with Jinja2 variable substitutiontemplate: src=vhost.j2 dest=/etc/nginx/
serviceManage system servicesservice: name=nginx state=started
fileManage files and directoriesfile: path=/var/www state=directory
userManage system usersuser: name=deploy shell=/bin/bash
lineinfileEnsure a line exists in a filelineinfile: path=/etc/ssh/sshd_config ...
commandRun a command (not through shell)command: /opt/app/migrate.sh
shellRun a command through shellshell: cat /etc/passwd | grep deploy
gitClone or update a Git repositorygit: repo=https://... dest=/opt/app

Variables and Facts

Variables make your playbooks reusable across different environments. Facts are variables automatically gathered from your servers (OS version, IP addresses, memory, CPU count, etc.).

# playbook with variables
---
- name: Deploy application
  hosts: webservers
  become: true
  vars:
    app_name: mywebapp
    app_port: 3000
    app_user: deploy
    node_version: "20"

  tasks:
    - name: Create app user
      user:
        name: "{{ app_user }}"
        shell: /bin/bash
        home: "/home/{{ app_user }}"

    - name: Show server info (using facts)
      debug:
        msg: "Server {{ ansible_hostname }} runs {{ ansible_distribution }} {{ ansible_distribution_version }} with {{ ansible_memtotal_mb }}MB RAM"

Templates with Jinja2

Templates let you create configuration files with dynamic values. Ansible uses the Jinja2 templating engine, which supports variables, conditionals, loops, and filters.

# templates/vhost.conf.j2
server {
    listen 80;
    server_name {{ domain_name }};

    root /var/www/{{ app_name }}/public;

    location / {
        proxy_pass http://127.0.0.1:{{ app_port }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

{% if enable_ssl %}
    listen 443 ssl;
    ssl_certificate /etc/ssl/{{ domain_name }}.crt;
    ssl_certificate_key /etc/ssl/{{ domain_name }}.key;
{% endif %}
}
# Using the template in a playbook
- name: Configure Nginx virtual host
  template:
    src: templates/vhost.conf.j2
    dest: "/etc/nginx/sites-available/{{ domain_name }}.conf"
    owner: root
    group: root
    mode: '0644'
  notify: Reload Nginx

Roles: Organizing Your Playbooks

As your playbooks grow, you need a way to organize them. Roles provide a standardized directory structure for grouping tasks, templates, files, variables, and handlers.

roles/
nginx/
tasks/
main.yml Tasks to execute
handlers/
main.yml Service restart handlers
templates/
nginx.conf.j2 Jinja2 templates
files/
custom-error.html Static files
vars/
main.yml Role variables
defaults/
main.yml Default variables (overridable)
meta/
main.yml Role dependencies
# Using roles in a playbook
---
- name: Setup web servers
  hosts: webservers
  become: true
  roles:
    - common
    - nginx
    - { role: php, php_version: "8.3" }
    - mysql

Ansible Galaxy: Community Roles

Ansible Galaxy is a hub for sharing Ansible roles. Instead of writing everything from scratch, you can install community-maintained roles for common tasks:

# Install a role from Galaxy
$ ansible-galaxy install geerlingguy.nginx
$ ansible-galaxy install geerlingguy.mysql
$ ansible-galaxy install geerlingguy.php

# Or use a requirements file
$ cat requirements.yml
---
roles:
  - name: geerlingguy.nginx
  - name: geerlingguy.mysql
    version: "4.0.0"

$ ansible-galaxy install -r requirements.yml

Error Handling

Not every task will succeed, and not every failure should stop the entire playbook. Ansible provides several mechanisms for handling errors gracefully.

# ignore_errors: continue even if this task fails
- name: Check if app is running
  command: pgrep myapp
  register: app_status
  ignore_errors: yes

# block/rescue: try-catch equivalent
- name: Deploy with rollback
  block:
    - name: Deploy new version
      shell: ./deploy.sh
    - name: Run health check
      uri:
        url: "http://localhost:3000/health"
        status_code: 200
  rescue:
    - name: Rollback on failure
      shell: ./rollback.sh
    - name: Notify team
      debug:
        msg: "Deployment failed, rolled back!"

Practical Example: Complete LAMP Stack

Let us put everything together with a real-world example that deploys a complete LAMP (Linux, Apache/Nginx, MySQL, PHP) stack:

# lamp-stack.yml
---
- name: Deploy LAMP Stack
  hosts: webservers
  become: true
  vars:
    php_version: "8.3"
    mysql_root_password: "{{ vault_mysql_root_password }}"
    app_domain: "example.com"

  tasks:
    - name: Install required packages
      apt:
        name:
          - nginx
          - "php{{ php_version }}-fpm"
          - "php{{ php_version }}-mysql"
          - "php{{ php_version }}-mbstring"
          - mysql-server
          - python3-mysqldb
        state: present
        update_cache: yes

    - name: Configure Nginx vhost
      template:
        src: templates/vhost.conf.j2
        dest: "/etc/nginx/sites-available/{{ app_domain }}"
      notify: Reload Nginx

    - name: Enable site
      file:
        src: "/etc/nginx/sites-available/{{ app_domain }}"
        dest: "/etc/nginx/sites-enabled/{{ app_domain }}"
        state: link

    - name: Start and enable services
      service:
        name: "{{ item }}"
        state: started
        enabled: yes
      loop:
        - nginx
        - "php{{ php_version }}-fpm"
        - mysql

  handlers:
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

Ansible vs Puppet vs Chef

FeatureAnsiblePuppetChef
ArchitectureAgentless (SSH)Agent requiredAgent required
LanguageYAML (simple)Puppet DSLRuby
Learning CurveLowMediumHigh
Push/PullPushPull (with server)Pull (with server)
CommunityLargestLargeMedium
Best ForGeneral automationLarge enterpriseDeveloper-heavy orgs

Quick Reference Checklist

  • Ansible installed on your control machine
  • SSH keys configured for passwordless access to managed servers
  • Inventory file created with server groups defined
  • Tested connectivity with ansible all -m ping
  • First playbook written with proper YAML indentation
  • Handlers used for service restarts triggered by config changes
  • Variables used to make playbooks reusable
  • Templates used for dynamic configuration files
  • Roles used to organize complex playbooks
  • Ansible Vault used for encrypting sensitive variables

What to Learn Next

You now have a solid foundation in Ansible. In 30 minutes, you have gone from zero to writing playbooks that can configure servers automatically. Here is where to go next: Ansible Vault for encrypting secrets, dynamic inventories for cloud environments (AWS, Azure, GCP), Ansible Tower/AWX for a web-based management UI, and writing custom modules in Python for specialized tasks.

The key to mastering Ansible is practice. Take a task you do manually on your servers today — installing packages, configuring services, deploying applications — and automate it with a playbook. Each playbook you write is one less manual process that can go wrong at 3 AM on a Saturday night.

Security-first hosting panel

Stop bolting tools onto a legacy panel.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
No monthly renewals.