Ansible is a powerful yet straightforward automation tool that can save you countless hours of repetitive system administration tasks.
In this guide, you'll learn the essentials of Ansible and complete a practical project by setting up a web server automatically.
By the end of this article, you'll have hands-on experience with Ansible's core concepts and a working web server deployment you can build upon for your own projects.
What is Ansible?
Ansible is an open-source automation platform that simplifies configuration management, application deployment, and task automation.
Unlike some other automation tools, Ansible doesn't require special software to be installed on the machines you want to manage. Instead, it communicates with your systems using SSH, making it significantly easier to get started.
At its core, Ansible works by sending instructions from a control machine (your computer) to target machines (servers you want to manage). These instructions, written in YAML, tell the target machines exactly what state they should be in—what software should be installed, how services should be configured, and so on.
Why use Ansible?
The beauty of Ansible lies in its simplicity. Ansible is agentless, meaning you don't need to install and maintain client software on your servers. It uses YAML syntax, which is human-readable and easy to learn even for beginners.
Ansible operations are idempotent, so you can run the same instructions multiple times without causing problems—it only makes changes when needed.
Additionally, Ansible comes with hundreds of built-in modules for managing various systems and services, making it versatile for different automation needs.
Setting up your environment
Before you can start automating, you'll need to install Ansible on your control machine (the computer from which you'll run your commands).
I'll focus on installation for Linux and macOS, as these are the most common platforms for Ansible controllers.
For Ubuntu/Debian systems:
sudo apt update
sudo apt install ansible
For macOS (using Homebrew):
brew install ansible
Verifying your installation
Once installed, verify that Ansible is working correctly by checking its version:
ansible --version
You should see output similar to this:
ansible [core 2.16.14]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/ayo/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.13/site-packages/ansible
ansible collection location = /home/ayo/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.13.2 (main, Feb 4 2025, 00:00:00) [GCC 14.2.1 20250110 (Red Hat 14.2.1-7)] (/usr/bin/python3)
jinja version = 3.1.5
libyaml = True
Understanding the inventory
The inventory is simply a list of systems that Ansible will manage. While Ansible supports dynamic inventories that can pull server information from cloud providers or other sources, we'll start with a basic static inventory file.
Creating a simple inventory file
Create a new file named inventory.ini
in your working directory:
[webservers]
webserver1 ansible_host=192.168.1.10
[database]
db1 ansible_host=192.168.1.11
[all:vars]
ansible_user=your_ssh_user
ansible_ssh_private_key_file=/path/to/your/private_key
This inventory defines a group called webservers
containing one server with IP
192.168.1.10, a group called database
with one server, and variables that
apply to all servers, specifying how to connect via SSH.
For beginners, it's perfectly fine to start with just one server. You can even use localhost for testing:
[webservers]
localhost ansible_connection=local
Testing connectivity
Let's verify that Ansible can communicate with your servers using the ping
module:
ansible webservers -i inventory.ini -m ping
If successful, you should see:
localhost | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
If you see "pong" in the response, your connection is working correctly. If you encounter errors, check your SSH configuration and make sure you can manually SSH into the target servers.
Running ad-hoc commands
Before diving into playbooks, let's explore ad-hoc commands. These are one-liner tasks that you might need to perform quickly across multiple servers.
Basic command structure
Ad-hoc commands follow this structure:
ansible [group] -i [inventory_file] -m [module] -a "[module arguments]"
Let's try some useful ad-hoc commands, starting with checking system information:
ansible webservers -i inventory.ini -m gather_facts
localhost | SUCCESS => {
"ansible_facts": {
. . .
},
"changed": false,
"deprecations": [],
"warnings": []
}
This command returns detailed information about your target servers, including operating system, memory, and network configuration.
Another command ad-hoc command is one that installs a package. On Ubuntu/Debian, use:
ansible webservers -i inventory.ini -m apt -a "name=nginx state=present" --become
This installs Nginx on your webservers. The --become
flag tells Ansible to use
privilege escalation (sudo
).
Finally, you can create a file on your target servers with:
ansible webservers -i inventory.ini -m copy -a "content='Hello from Ansible' dest=/tmp/ansible_test.txt"
localhost | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"checksum": "5332d55f8fbdb22d9875ff5650211ed9630b1d68",
"dest": "/tmp/ansible_test.txt",
"gid": 1000,
"group": "ayo",
"md5sum": "4ed8eb0db4371dcd7ca9d773455bdd82",
"mode": "0644",
"owner": "ayo",
"secontext": "unconfined_u:object_r:user_home_t:s0",
"size": 18,
"src": "/home/ayo/.ansible/tmp/ansible-tmp-1742183730.0935683-2183216-224482429836629/source",
"state": "file",
"uid": 1000
}
This creates a file with the specified content in the /tmp
directory.
Ad-hoc commands are great for quick tasks, but for repeatable operations, playbooks are the way to go so lets explore that next.
Writing your first playbook
Playbooks are Ansible's configuration, deployment, and orchestration language. They describe a policy you want your systems to enforce, using a simple, declarative format.
To get started, create a file named webserver.yml
:
- name: Setup web server
hosts: webservers
become: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install packages
apt:
name:
- nginx
- ufw
state: present
- name: Ensure Nginx is running
service:
name: nginx
state: started
enabled: yes
This simple playbook targets the webservers
group from your inventory,
requests privilege escalation (sudo
), updates the apt cache if it hasn't been
updated in the last hour, installs Nginx, and makes sure Nginx is running and
set to start on boot.
Once saved, execute your playbook with:
ansible-playbook -i inventory.ini webserver.yaml
You should see output showing the execution of each task:
PLAY [Setup web server] ***********************************************
TASK [Gathering Facts] ***********************************************
ok: [webserver1]
TASK [Update apt cache] **********************************************
ok: [webserver1]
TASK [Install Nginx] ************************************************
changed: [webserver1]
TASK [Ensure Nginx is running] **************************************
changed: [webserver1]
PLAY RECAP **********************************************************
webserver1 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Congratulations! You've just automated the installation and configuration of a web server. If you run the playbook again, you'll notice most tasks report as "ok" rather than "changed"—this is Ansible's idempotence at work. It only makes changes when needed.
Expanding your web server project
Now let's enhance our web server by adding a custom homepage and configuring firewall rules.
First, let's create a templates directory and a template for our custom homepage:
mkdir templates
Create templates/index.html.j2
:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to {{ ansible_hostname }}</title>
</head>
<body>
<h1>Hello from {{ ansible_hostname }}</h1>
<p>This server was configured automatically using Ansible on {{ ansible_date_time.date }}.</p>
<p>Server details:</p>
<ul>
<li>Distribution: {{ ansible_distribution }} {{ ansible_distribution_version }}</li>
<li>System: {{ ansible_system }}</li>
<li>Architecture: {{ ansible_architecture }}</li>
</ul>
</body>
</html>
Now, update your playbook to use this template:
- name: Setup web server
hosts: webservers
become: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install packages
apt:
name:
- nginx
- ufw
state: present
- name: Create custom index page
template:
src: templates/index.html.j2
dest: /var/www/html/index.html
owner: www-data
group: www-data
mode: '0644'
notify: Reload Nginx
- name: Ensure Nginx is running
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
The template module uses Jinja2 templating to replace variables in your template
with actual values. The variables with ansible_
prefix are gathered by Ansible
automatically as facts about the system.
Also notice the introduction of a handler. Handlers are special tasks that only run when notified by another task, typically used for restarting services when their configuration changes.
Configuring firewall rules
Let's add firewall configuration to our playbook to allow HTTP traffic:
- name: Setup web server
hosts: webservers
become: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install packages
apt:
name:
- nginx
- ufw
state: present
- name: Create custom index page
template:
src: templates/index.html.j2
dest: /var/www/html/index.html
owner: www-data
group: www-data
mode: '0644'
notify: Reload Nginx
- name: Allow HTTP traffic
ufw:
rule: allow
name: Nginx Full
state: enabled
- name: Enable UFW
ufw:
state: enabled
policy: deny
- name: Ensure Nginx is running
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
This updated playbook installs both Nginx and UFW (Uncomplicated Firewall), creates a custom index page, configures the firewall to allow HTTP and HTTPS traffic, enables the firewall with a default deny policy, and ensures Nginx is running.
Testing your completed web server
Run your updated playbook:
ansible-playbook -i inventory.ini webserver.yml
After the playbook completes, open a web browser and navigate to your server's IP address. You should see your custom homepage with the server's information.
If everything works correctly, you've successfully used Ansible to install necessary packages, configure a web server, deploy custom content, and set up security rules.
Troubleshooting common issues
Even with a tool as straightforward as Ansible, you might encounter some issues. Here are some common problems and how to solve them:
Connection problems
If Ansible can't connect to your servers, check SSH connectivity by making sure you can manually SSH into the target servers. Verify hostnames and IP addresses in your inventory file, and ensure your SSH key or password authentication is working.
For more detailed connection debugging, add the -vvv
flag:
ansible webservers -i inventory.ini -m ping -vvv
Permission errors
If tasks fail due to permission issues, check that the become: yes
directive
is included in your playbook. Ensure your user has sudo privileges on the target
system. For specific files, check ownership and permissions.
Syntax mistakes
YAML is sensitive to indentation. If you get syntax errors, use a YAML linter or editor with YAML support. Check your indentation (spaces, not tabs) and verify that all colons, dashes, and quotes are correctly placed.
You can validate your playbook syntax without running it:
ansible-playbook -i inventory.ini webserver.yml --syntax-check
Final thoughts
Ansible makes automation accessible even to beginners. Through this guide, you've learned how to install and configure Ansible, create an inventory of managed systems, run one-off commands across multiple servers, write playbooks for repeatable configurations, and deploy a working web server with custom content and security settings.
The skills you've gained provide a foundation for more complex automation tasks. To further develop your Ansible skills, explore Ansible's official documentation and experiment with different modules from the module library.
Think about repetitive tasks in your workflow that could be automated. You might want to deploy your favorite application stack, set up regular backups, configure monitoring tools, or standardize security settings across servers.
By investing time in learning Ansible, you're taking a significant step toward more efficient and consistent system administration. Happy automating!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github