Automate Your Server Setup with Ansible: A Practical Getting Started Guide
If you manage more than one server — or even just one that you rebuild occasionally — you’ve probably felt the pain of manually installing packages, editing config files, and repeating the same steps over and over. Ansible solves this by letting you describe your server’s desired state in simple YAML files and applying that configuration automatically, every time, consistently.
Unlike tools like Puppet or Chef, Ansible is agentless. It connects to your servers over SSH and runs commands remotely. There’s nothing to install on the target machines. That simplicity is what makes it one of the most popular automation tools in the DevOps world.
In this guide, you’ll go from zero Ansible knowledge to writing real playbooks that install packages, deploy apps, manage services, and configure multiple servers at once.
What Is Ansible?
Ansible is an open-source IT automation engine maintained by Red Hat. It handles three main categories of automation:
- Provisioning — Setting up new servers with the right software and configuration
- Configuration management — Keeping servers in a consistent, desired state
- Application deployment — Pushing code and restarting services across your fleet
Everything in Ansible is defined in YAML, which reads almost like plain English. There are no proprietary scripting languages to learn.
Key Concepts
| Term | Meaning |
|---|---|
| Control node | The machine where Ansible is installed (your laptop, CI server, etc.) |
| Managed node | A remote server Ansible connects to and manages |
| Inventory | A file listing your managed nodes (hosts) |
| Module | A unit of work Ansible can execute (e.g., apt, copy, service) |
| Task | A single call to an Ansible module |
| Play | A set of tasks applied to a group of hosts |
| Playbook | A YAML file containing one or more plays |
| Role | A reusable, organized collection of tasks, files, and templates |
Installing Ansible
Ansible runs on Linux and macOS. On the control node (your local machine or a management server), install it with:
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 ansible
macOS (Homebrew)
brew install ansible
pip (any platform with Python)
pip install ansible
Verify the installation:
ansible --version
You should see output showing the installed version (e.g., ansible [core 2.16.x]), the config file path, and the Python version it’s using.
Note: You do not need to install anything on the remote servers. Ansible only requires Python (which is pre-installed on virtually all Linux distributions) and SSH access.
Setting Up Your Inventory
The inventory file tells Ansible which servers to manage. Create a file called inventory.ini:
[webservers]
web-01 ansible_host=203.0.113.10 ansible_user=deploy
web-02 ansible_host=203.0.113.11 ansible_user=deploy
[databases]
db-01 ansible_host=203.0.113.20 ansible_user=deploy
[all:vars]
ansible_python_interpreter=/usr/bin/python3
Here we’ve defined two groups — webservers (two servers) and databases (one server). The ansible_user tells Ansible which SSH user to connect as.
Test Connectivity
Ping all servers to verify Ansible can connect:
ansible all -i inventory.ini -m ping
If SSH key authentication is configured (and it should be — see our SSH key guide), you’ll get a SUCCESS response from each host:
web-01 | SUCCESS => {
"changed": false,
"ping": "pong"
}
You can also run ad-hoc commands against your inventory:
# Check uptime on all web servers
ansible webservers -i inventory.ini -a "uptime"
# Check disk space on all servers
ansible all -i inventory.ini -a "df -h"
# Install a package on database servers
ansible databases -i inventory.ini -m apt -a "name=htop state=present" --become
The --become flag tells Ansible to use sudo for commands that need root privileges.
Writing Your First Playbook
A playbook is a YAML file that describes the desired state of your servers. Create a file called setup-webserver.yml:
---
- name: Set up web servers
hosts: webservers
become: true
tasks:
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 3600
- name: Install required packages
apt:
name:
- nginx
- curl
- ufw
- fail2ban
state: present
- name: Start and enable Nginx
service:
name: nginx
state: started
enabled: true
- name: Allow HTTP through firewall
ufw:
rule: allow
port: "80"
proto: tcp
- name: Allow HTTPS through firewall
ufw:
rule: allow
port: "443"
proto: tcp
- name: Enable UFW
ufw:
state: enabled
default: deny
Run the playbook:
ansible-playbook -i inventory.ini setup-webserver.yml
Ansible will connect to each server in the webservers group and execute every task in order. The output shows whether each task was ok (already in desired state), changed (a modification was made), or failed.
Idempotency — The Key Principle
Every Ansible module is designed to be idempotent: running the same playbook twice produces the same result. If Nginx is already installed and running, Ansible recognizes that and skips the task. This makes playbooks safe to run repeatedly without side effects.
Using Variables and Templates
Real-world configurations need dynamic values. Ansible supports variables at multiple levels.
Defining Variables in the Playbook
---
- name: Deploy application
hosts: webservers
become: true
vars:
app_port: 3000
app_user: appuser
domain_name: example.com
tasks:
- name: Create application user
user:
name: "{{ app_user }}"
shell: /bin/bash
create_home: true
- name: Deploy Nginx config from template
template:
src: templates/nginx-site.conf.j2
dest: "/etc/nginx/sites-available/{{ domain_name }}"
owner: root
group: root
mode: "0644"
notify: Reload Nginx
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
Jinja2 Templates
Create templates/nginx-site.conf.j2:
server {
listen 80;
server_name {{ domain_name }};
location / {
proxy_pass http://127.0.0.1:{{ app_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Ansible replaces {{ domain_name }} and {{ app_port }} with the actual variable values when deploying the file. This is incredibly powerful for managing configurations across multiple environments (staging, production) with different values.
Handlers
Notice the notify: Reload Nginx directive. A handler is a special task that only runs when notified — meaning Nginx only gets reloaded if the config file actually changed. This prevents unnecessary service restarts.
Organizing with Roles
As your playbooks grow, Ansible roles let you organize tasks, templates, files, and variables into a reusable directory structure.
Create a role skeleton:
ansible-galaxy init roles/webserver
This generates:
roles/webserver/
├── defaults/
│ └── main.yml # Default variables
├── files/ # Static files to copy
├── handlers/
│ └── main.yml # Handlers
├── meta/
│ └── main.yml # Role metadata
├── tasks/
│ └── main.yml # Main task list
├── templates/ # Jinja2 templates
└── vars/
└── main.yml # Role variables
Move your tasks into roles/webserver/tasks/main.yml, templates into roles/webserver/templates/, and then use the role in your playbook:
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- webserver
This keeps your project clean and makes roles shareable across different projects.
Real-World Example: Full Stack Deployment
Here’s a more complete playbook that sets up a Node.js application with Nginx as a reverse proxy and PostgreSQL as the database:
---
- name: Set up database server
hosts: databases
become: true
tasks:
- name: Install PostgreSQL
apt:
name:
- postgresql
- postgresql-contrib
- python3-psycopg2
state: present
- name: Start PostgreSQL
service:
name: postgresql
state: started
enabled: true
- name: Create application database
become_user: postgres
postgresql_db:
name: myapp_production
state: present
- name: Create database user
become_user: postgres
postgresql_user:
db: myapp_production
name: myapp_user
password: "{{ db_password }}"
priv: ALL
state: present
- name: Deploy application to web servers
hosts: webservers
become: true
vars:
node_version: "20"
app_dir: /opt/myapp
tasks:
- name: Install Node.js repository
shell: curl -fsSL https://deb.nodesource.com/setup_{{ node_version }}.x | bash -
args:
creates: /etc/apt/sources.list.d/nodesource.list
- name: Install Node.js
apt:
name: nodejs
state: present
- name: Create app directory
file:
path: "{{ app_dir }}"
state: directory
owner: deploy
group: deploy
- name: Copy application files
synchronize:
src: ./app/
dest: "{{ app_dir }}/"
delete: true
- name: Install npm dependencies
npm:
path: "{{ app_dir }}"
production: true
- name: Deploy systemd service
template:
src: templates/myapp.service.j2
dest: /etc/systemd/system/myapp.service
notify:
- Reload systemd
- Restart myapp
handlers:
- name: Reload systemd
systemd:
daemon_reload: true
- name: Restart myapp
service:
name: myapp
state: restarted
Run it with:
ansible-playbook -i inventory.ini deploy.yml -e "db_password=supersecretpass"
The -e flag passes extra variables at runtime — useful for secrets you don’t want to hardcode.
Ansible Vault: Managing Secrets
Never store passwords in plain text. Ansible Vault encrypts sensitive data:
# Create an encrypted variables file
ansible-vault create vars/secrets.yml
# Edit an encrypted file
ansible-vault edit vars/secrets.yml
# Run a playbook with vault-encrypted files
ansible-playbook -i inventory.ini deploy.yml --ask-vault-pass
Inside vars/secrets.yml:
db_password: supersecretpass
api_key: sk-abc123xyz789
ssl_private_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBg...
-----END PRIVATE KEY-----
Reference these in your playbook:
vars_files:
- vars/secrets.yml
For CI/CD pipelines, store the vault password in a file and reference it:
ansible-playbook deploy.yml --vault-password-file .vault_pass
Add .vault_pass to your .gitignore so it’s never committed.
Useful Ansible Tips
Dry Run (Check Mode)
Preview what changes a playbook would make without actually applying them:
ansible-playbook -i inventory.ini setup.yml --check
Limit to Specific Hosts
Run a playbook against only one server from a group:
ansible-playbook -i inventory.ini setup.yml --limit web-01
Tags for Selective Execution
Tag your tasks and run only specific ones:
- name: Install Nginx
apt:
name: nginx
state: present
tags: [nginx, packages]
ansible-playbook setup.yml --tags "nginx"
Ansible Galaxy — Community Roles
Browse pre-built roles at https://galaxy.ansible.com. Install them directly:
# Install a popular Docker role
ansible-galaxy install geerlingguy.docker
# Install a Certbot/Let's Encrypt role
ansible-galaxy install geerlingguy.certbot
Use them in your playbook:
roles:
- geerlingguy.docker
- geerlingguy.certbot
Jeff Geerling’s roles (username geerlingguy on Galaxy) are particularly well-maintained and cover hundreds of common tasks.
Project Structure Best Practice
For a well-organized Ansible project, structure your files like this:
ansible-project/
├── ansible.cfg
├── inventory/
│ ├── production.ini
│ └── staging.ini
├── group_vars/
│ ├── all.yml
│ ├── webservers.yml
│ └── databases.yml
├── roles/
│ ├── common/
│ ├── webserver/
│ └── database/
├── templates/
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── databases.yml
└── vars/
└── secrets.yml (encrypted with ansible-vault)
Create an ansible.cfg in your project root to set defaults:
[defaults]
inventory = inventory/production.ini
remote_user = deploy
private_key_file = ~/.ssh/id_ed25519
host_key_checking = False
retry_files_enabled = False
[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False
Where to Go Next
Once you’re comfortable with the basics, explore:
- Ansible AWX / Semaphore — Web UIs for running Ansible playbooks with a dashboard, scheduling, and team access control. AWX is the open-source upstream of Red Hat Ansible Tower. Semaphore (https://semaphoreui.com) is a lightweight alternative.
- Dynamic inventory — Instead of static
.inifiles, pull your host list from AWS EC2, DigitalOcean, or any cloud API automatically. - Ansible Lint — Run
ansible-linton your playbooks to catch style issues and potential bugs before execution. - Molecule — A testing framework for Ansible roles that spins up containers or VMs to verify your roles work correctly.
Wrapping Up
Ansible removes the tedium and risk of manual server configuration. With a few YAML files, you get repeatable, version-controlled infrastructure that you can apply to one server or a thousand. The agentless architecture means there’s almost zero barrier to getting started — if you can SSH into a server, you can automate it with Ansible.
Start small: write a playbook that installs your essential packages and configures your SSH settings. Once you see how much time it saves, you’ll want to automate everything.
The official documentation at https://docs.ansible.com is excellent and covers every module in detail. The Ansible community on GitHub (https://github.com/ansible/ansible) is active and welcoming for contributions and questions.