Ansible Tutorial for Beginners: Playbook & Commands
โก Smart Summary
Ansible is an open source, agentless automation engine that configures servers over SSH, runs ad-hoc commands, and orchestrates complex deployments through YAML playbooks, roles, and reusable templates across Linux and Windows infrastructure.

Ansible is one of the most widely adopted open source automation tools in modern DevOps, valued for its agentless design and its simple, readable configuration files. This Ansible tutorial for beginners explains what Ansible is, how to install it, and how to use ad-hoc commands, playbooks, roles, and templates through practical examples.
What is Ansible?
Ansible is an open source automation and orchestration tool for software provisioning, configuration management, and software deployment. It configures Unix-like systems and Windows systems alike, providing infrastructure as code through its own declarative language. Ansible is often compared with agent-based tools such as Puppet.
Ansible is popular for its simple installation, its easy client connectivity, and its agentless design. It connects to clients over SSH, so no special agent is required on the client side. Ansible pushes modules to each client, the modules run locally, and their output is returned to the Ansible server.
Because it relies on SSH, Ansible connects to clients easily using SSH keys, which simplifies the whole process. Client details such as hostnames or IP addresses and SSH ports are stored in files called inventory files. Once you create and populate an inventory file, Ansible can use it.
Why use Ansible?
Here are some important benefits of using Ansible:
- One of the most significant advantages of Ansible is that it is free for everyone to use.
- It does not require special system administrator skills to install and use, and the official documentation is very comprehensive.
- Its modularity across plugins, modules, inventories, and playbooks makes Ansible an excellent companion for orchestrating large environments.
- Ansible is lightweight and consistent, with no constraints on the operating system or the underlying hardware.
- It is very secure thanks to its agentless design and its use of OpenSSH security features.
- Ansible has a smooth learning curve, supported by thorough documentation and an easy-to-learn structure and configuration.
These strengths, especially when weighed against other alternatives to Ansible, help explain its rapid adoption. Before installing it, it helps to understand how the project evolved.
History of Ansible
Here are the important landmarks in the history of Ansible:
- The Ansible project began in February 2012. It was first developed by Michael DeHaan, the creator of Cobbler and Func (Fedora Unified Network Controller).
- Initially called AnsibleWorks Inc, the company funding Ansible was acquired by RedHat in 2015 and later, along with RedHat, moved under the IBM umbrella.
- Today, Ansible is included in distributions such as Fedora Linux, RHEL, CentOS, and Oracle Linux.
Important terms used in Ansible
The following terms appear throughout this Ansible tutorial and are worth knowing before you start:
Ansible server
The machine where Ansible is installed and from which all tasks and playbooks are run.
Module
A module is a command, or a set of similar Ansible commands, meant to be executed on the client side.
Task
A task is a section that consists of a single procedure to be completed.
Role
A way of organizing tasks and related files so they can be called later in a playbook.
Fact
Information fetched from the client system global variables through the gather_facts operation.
Inventory
A file containing data about the Ansible client servers. It is defined in later examples as the hosts file.
Play
The execution of a playbook.
Handler
A task that is called only when a notifier is present.
Notifier
A section attributed to a task that calls a handler if the output changes.
Tag
A name set on a task that can be used later to run just that specific task or group of tasks.
Ansible Installation in Linux
Once you have weighed your options and decided to adopt Ansible, the next step is to install it on your system. The following short walkthrough covers installation on the most popular Linux distributions.
Install Ansible on Centos/RedHat systems
Step 1) Install the EPEL repo.
[root@ansible-server ~]# sudo yum install epel-release
Step 2) Install the ansible package.
[root@ansible-server ~]# sudo yum install -y ansible
Install ansible on Ubuntu/Debian systems
Step 1) Perform an update to the packages.
$ sudo apt update
Step 2) Install the software-properties-common package.
$ sudo apt install software-properties-common
Step 3) Install the ansible personal package archive.
$ sudo apt-add-repository ppa:ansible/ansible
Step 4) Install ansible.
$ sudo apt update $ sudo apt install ansible
Ansible ad-hoc commands
One of the simplest ways to use Ansible is through ad-hoc commands. These are useful when you want to issue a command on a single server or on a group of servers. Ad-hoc commands are not stored for future use, but they are a fast way to interact with the servers you choose.
For this Ansible tutorial, a simple two-server hosts file is configured, containing host1 and host2.
You can confirm that the hosts are reachable from the Ansible server by issuing a ping command on all hosts.
[root@ansible-server test_ansible]# ansible -i hosts all -m ping
host1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
host2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
Explanation:
- Status of the command, in this case, SUCCESS
- Host on which the command ran
- The command issued via the -m parameter, in this case, ping
- With the -i parameter, you can point to the hosts file.
You can also issue the same command on a single host when needed.
[root@ansible-server test_ansible]# ansible -i hosts all -m ping --limit host2
host2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
Explanation:
- Limit parameter can be used to issue commands only on specific hosts in the host’s file
- Name of the host as defined in the inventory file
If you need to copy a file to multiple destinations quickly, use the copy module, which relies on SCP. The command and its output look like the following:
[root@ansible-server test_ansible]# ansible -i hosts all -m copy -a "src=/root/test_ansible/testfile dest=/tmp/testfile"
host1 | SUCCESS => {
"changed": true,
"checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"dest": "/tmp/testfile",
"gid": 0,
"group": "root",
"md5sum": "d41d8cd98f00b204e9800998ecf8427e",
"mode": "0644",
"owner": "root",
"size": 0,
"src": "/root/.ansible/tmp/ansible-tmp-1562216392.43-256741011164877/source",
"state": "file",
"uid": 0
}
host2 | SUCCESS => {
"changed": true,
"checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"dest": "/tmp/testfile",
"gid": 0,
"group": "root",
"md5sum": "d41d8cd98f00b204e9800998ecf8427e",
"mode": "0644",
"owner": "root",
"size": 0,
"src": "/root/.ansible/tmp/ansible-tmp-1562216392.6-280302911361278/source",
"state": "file",
"uid": 0
}
Explanation:
- Copy module defined
- Module arguments, in this case, are source absolute path and destination absolute path.
- Ansible command output reflecting the success of the copy command and other details like the sha1 or md5 checksums for file integrity check and metadata like owner, size, or permissions.It is effortless to have a package installed on a bunch of servers. Ansible has several modules that interact with used installers, like yum, apt, dnf, etc.
In the next example, you will install a package with the yum module on two CentOS hosts.
[root@ansible-server test_ansible]# ansible -i hosts all -m yum -a 'name=ncdu state=present'
host1 | SUCCESS => {
"changed": true,
"msg": "",
"rc": 0,
"results": [
"Loaded plugins: fastestmirror\nLoading mirror speeds from cached hostfile\n * base: mirror.netsite.dk\n * elrepo: mirrors.xservers.ro\n * epel: fedora.mirrors.telekom.ro\n * extras: centos.mirrors.telekom.ro\n * remi-php70: remi.schlundtech.de\n * remi-safe: remi.schlundtech.de\n * updates: centos.mirror.iphh.net\nResolving Dependencies\n--> Running transaction check\n---> Package ncdu.x86_64 0:1.14-1.el7 will be installed\n--> Finished Dependency Resolution\n\nDependencies Resolved\n\n========================================================================\n Package Arch Version Repository Size\n========================================================================\nInstalling:\n ncdu x86_64 1.14-1.el7 epel 51 k\n\nTransaction Summary\n========================================================================\nInstall 1 Package\n\nTotal download size: 51 k\nInstalled size: 87 k\nDownloading packages:\nRunning transaction check\nRunning transaction test\nTransaction test succeeded\nRunning transaction\n Installing : ncdu-1.14-1.el7.x86_64 1/1 \n Verifying : ncdu-1.14-1.el7.x86_64 1/1 \n\nInstalled:\n ncdu.x86_64 0:1.14-1.el7 \n\nComplete!\n"
]
}
host2 | SUCCESS => {
"changed": true,
"msg": "",
"rc": 0,
"results": [
"Loaded plugins: fastestmirror\nLoading mirror speeds from cached hostfile\n * base: mirror.netsite.dk\n * elrepo: mirrors.leadhosts.com\n * epel: mirrors.nav.ro\n * extras: centos.mirrors.telekom.ro\n * remi-php70: mirrors.uni-ruse.bg\n * remi-safe: mirrors.uni-ruse.bg\n * updates: centos.mirror.iphh.net\nResolving Dependencies\n--> Running transaction check\n---> Package ncdu.x86_64 0:1.14-1.el7 will be installed\n--> Finished Dependency Resolution\n\nDependencies Resolved\n\n========================================================================\n Package Arch Version Repository Size\n========================================================================\nInstalling:\n ncdu x86_64 1.14-1.el7 epel 51 k\n\nTransaction Summary\n========================================================================\nInstall 1 Package\n\nTotal download size: 51 k\nInstalled size: 87 k\nDownloading packages:\nRunning transaction check\nRunning transaction test\nTransaction test succeeded\nRunning transaction\n Installing : ncdu-1.14-1.el7.x86_64 1/1 \n Verifying : ncdu-1.14-1.el7.x86_64 1/1 \n\nInstalled:\n ncdu.x86_64 0:1.14-1.el7 \n\nComplete!\n"
]
}
Explanation:
- Yum module is used in this example
- It defines the module arguments, and in this case, you will choose the name of the package and its state. If the state is absent, for example, the package will be searched and if found, removed
- When colored in yellow, you will see the output of the ansible command with the state changed, meaning in this case, that the package was found and installed.
- Status of the yum install command issued via ansible. In this case the package ncdu.x86_64 0:1.14-1.el7 was installed.
Of course, all of the yum installer options can be used through Ansible, including update, install, latest version, and remove.
In the example below, the same command is issued to remove the previously installed ncdu package.
[root@ansible-server test_ansible]# ansible -i hosts all -m yum -a 'name=ncdu state=absent'
host1 | SUCCESS => {
"changed": true,
"msg": "",
"rc": 0,
"results": [
"Loaded plugins: fastestmirror\nResolving Dependencies\n--> Running transaction check\n---> Package ncdu.x86_64 0:1.14-1.el7 will be erased\n--> Finished Dependency Resolution\n\nDependencies Resolved\n\n========================================================================\n Package Arch Version Repository Size\n========================================================================\nRemoving:\n ncdu x86_64 1.14-1.el7 @epel 87 k\n\nTransaction Summary\n========================================================================\nRemove 1 Package\n\nInstalled size: 87 k\nDownloading packages:\nRunning transaction check\nRunning transaction test\nTransaction test succeeded\nRunning transaction\n Erasing : ncdu-1.14-1.el7.x86_64 1/1 \n Verifying : ncdu-1.14-1.el7.x86_64 1/1 \n\nRemoved:\n ncdu.x86_64 0:1.14-1.el7 \n\nComplete!\n"
]
}
host2 | SUCCESS => {
"changed": true,
"msg": "",
"rc": 0,
"results": [
"Loaded plugins: fastestmirror\nResolving Dependencies\n--> Running transaction check\n---> Package ncdu.x86_64 0:1.14-1.el7 will be erased\n--> Finished Dependency Resolution\n\nDependencies Resolved\n\n========================================================================\n Package Arch Version Repository Size\n========================================================================\nRemoving:\n ncdu x86_64 1.14-1.el7 @epel 87 k\n\nTransaction Summary\n========================================================================\nRemove 1 Package\n\nInstalled size: 87 k\nDownloading packages:\nRunning transaction check\nRunning transaction test\nTransaction test succeeded\nRunning transaction\n Erasing : ncdu-1.14-1.el7.x86_64 1/1 \n Verifying : ncdu-1.14-1.el7.x86_64 1/1 \n\nRemoved:\n ncdu.x86_64 0:1.14-1.el7 \n\nComplete!\n"
]
}
Explanation:
- The output of the yum command shows that the package was removed.
Another essential Ansible feature is its ability to gather facts about a system. It collects hardware, software, and version information and stores each value in a variable that you can reuse later.
When you need detailed information about the systems that Ansible will modify, the setup module gathers these facts from the system variables.
Ansible Playbooks
Ansible Playbooks are the way to send commands to remote systems through scripts. Playbooks configure complex system environments and increase flexibility by running a single script against one or more systems. They behave more like a configuration language than a programming language.
Playbook commands use the YAML format, so little syntax is needed, but indentation must be respected. As the name suggests, a playbook is a collection of plays. Through a playbook, you can assign specific roles to some hosts and different roles to others, orchestrating many servers in one file.
Before continuing with the playbook examples, it helps to define a task. Tasks are the interface to Ansible modules for roles and playbooks.
Now, consider an Ansible playbook with one play that contains multiple tasks, as shown below:
---
- hosts: group1
tasks:
- name: Install lldpad package
yum:
name: lldpad
state: latest
- name: check lldpad service status
service:
name: lldpad
state: started
In the playbook above, the group1 hosts in the hosts file are targeted for lldpad package installation using the yum module. The lldpad service created after installation is then started using the service module, which mostly interacts with the systemd ensemble.
Explanation:
- Group of hosts on which the playbook will run
- Yum module is used in this task for lldpad installation
- The service module is used to check if the service is up and running after installation
Each Ansible playbook works with an inventory file. The inventory file contains a list of servers divided into groups, giving better control over details such as the IP address and SSH port for each host.
The inventory file for this playbook example looks like the following. There are two groups, group1 and group2, each containing host1 and host2 respectively.
[group1] host1 ansible_host=192.168.100.2 ansible_ssh_port=22 [group2] host2 ansible_host=192.168.100.3 ansible_ssh_port=22
Explanation:
- Group name
- Hostname, with IP address and ssh port, in this case, the default one, 22.
The next Ansible playbook example contains two plays for two host groups. For the first group, group1, SELinux is enabled, and when it is enabled a message appears on the host screen.
For the second group, the httpd package is installed only if ansible_os_family is RedHat and ansible_system_vendor is HP.
ansible_os_family and ansible_system_vendor are variables gathered with the gather_facts option, and they can be used as in this conditional example.
---
- hosts: group1
tasks:
- name: Enable SELinux
selinux:
state: enabled
when: ansible_os_family == 'Debian'
register: enable_selinux
- debug:
Imsg: "Selinux Enabled. Please restart the server to apply changes."
when: enable_selinux.changed == true
- hosts: group2
tasks:
- name: Install apache
yum:
name: httpd
state: present
when: ansible_system_vendor == 'HP' and ansible_os_family == 'RedHat'
Explanation:
- Example of the when clause, In this case, when OS type is Debian. The ansible_os_family variable is gathered via gather_facts functionality.
- The task output is registered for future use, with its name enable_selinux
- Another example of the when clause. In this case, a message will be displayed for the host user if the SELinux was indeed enabled before.
- Another example of the when clause consisting of two rules
Besides tasks, there are also special tasks called handlers. A handler must have a unique name throughout the playbook. Handlers work like a regular task, but a handler can be notified through a notifier.
If a handler is not notified during a playbook run, it does not run. However, if more than one task notifies a handler, it runs only once after all the tasks are finished.
In the example below, a specific task has a notify section that calls another task. If the output of the first task changes, a handler task is called. A common example is changing a configuration file and then restarting the affected service.
---
- hosts: group2
tasks:
- name: sshd config file modify port
lineinfile:
path: /etc/ssh/sshd_config
regexp: 'Port 28675'
line: '#Port 22'
notify:
- restart sshd
handlers
- name: restart sshd
service: sshd
name: sshd
state: restarted
In this case, if the first task, “sshd config file modify port”, changes, meaning the port is not already 28675, the port is modified and the task notifies the handler of the same name, which restarts the sshd service.
Explanation:
- Example of a notifier
- Example of a handler
Ansible Roles
When working with large playbooks, it is easier to split the tasks into roles. Roles also make it easier to reuse work later. A role is a collection of tasks that can be moved from one playbook to another and run independently, though only through a playbook file.
Roles are stored in separate directories and follow a specific directory structure.
[root@ansible-server test2]# tree
.
`-- role1
|-- defaults
| `-- main.yml
|-- handlers
| `-- main.yml
|-- meta
| `-- main.yml
|-- README.md
|-- tasks
| `-- main.yml
|-- tests
| |-- inventory
| `-- test.yml
`-- vars
`-- main.yml
7 directories, 8 files
The YAML file in the defaults directory holds the default variables used with the playbook. The handlers directory stores handlers, and the meta directory holds information about the author and role dependencies. The tasks directory contains the main YAML file for the role.
The tests directory contains a sample YAML playbook and a sample inventory file, and it is mostly used for testing before the actual role is created.
The vars directory contains the YAML file where all variables used by the role are defined. The templates and files directories hold the templates and files used by the tasks in the role.
To create the directory tree for a role, use the following command with the role name as the last parameter:
[root@ansible-server test2]# ansible-galaxy init role1
Ansible also works well with templates, and it uses Jinja2 as its templating language.
The next example shows what a basic Jinja2 template looks like and how to use it in a role.
At run time, depending for example on which datacenter your server is located in, you can select from several nameservers, each corresponding to a datacenter, using the variable resolver_ip_addresses.
{% for resolver in resolver_ip_addresses %}
nameserver {{ resolver }}
{% endfor %}
options timeout:1
options attempts:5
options rotate
In this example, the playbook directory defines several variables, including resolver_ip_addresses, with different values depending on the datacenter.
- name: Set resolver for server
template:
src: dns.j2
dest: /etc/resolv.conf
group: root
owner: root
mode: "0644"
tag: resolver
Explanation:
- Name of the template to be used. Template is located in templates dir in the role path
- Destination path of the filename to be replaced with the template, on the client-side.
- Permissions of the destination file
Role tasks can also have a tag field with an assigned name. More than one task can share the same tag, and when you run a playbook you can specify a tag so that only the tagged tasks are executed.
Ansible Case Study
In this section, we analyze a case study of an essential Ansible playbook that has three roles. The goal is to give a practical example of the concepts covered so far. Some earlier examples from this tutorial are adapted and reused in this playbook.
Below is the directory structure of the playbook. The YAML file used is p4.yml.
[root@ansible-server test_ansible]# ls -lrth total 16K -rw-r--r--. 1 root root 0 Jul 3 10:13 testfile -rw-r--r--. 1 root root 203 Jul 3 13:30 p1.yml -rw-r--r--. 1 root root 125 Jul 3 15:00 hosts -rw-r--r--. 1 root root 488 Jul 3 16:40 p2.yml -rw-r--r--. 1 root root 188 Jul 4 17:33 p4.yml drwxr-xr-x. 5 root root 47 Jul 4 17:35 roles [root@ansible-server test_ansible]# cd roles [root@ansible-server roles]# ls -lrth total 12K drwxr-xr-x. 9 root root 4.0K Jul 4 12:52 httpd drwxr-xr-x. 9 root root 4.0K Jul 4 13:55 selinux drwxr-xr-x. 9 root root 4.0K Jul 4 16:54 resolver
The playbook has three roles. The resolver role sets a specific nameserver on the servers by copying a file to the /etc/resolv.conf destination. The httpd role installs the httpd package with the yum module, and the third role enables SELinux and notifies the logged-in user to reboot the system. Each role was created with the ansible-galaxy command.
Resolver role, main.yml task:
Httpd role, main.yml task:
Selinux role, main.yml task:
Below is the p4.yml playbook. It runs on all hosts unless otherwise specified on the command line, runs as the root user on port 22 (SSH), gathers facts before running the roles, and runs all three roles. Each role can be run independently by specifying its tag on the ansible-playbook command line with the โt parameter.
---
- hosts: all
user: root
port: 22
gather_facts: True
roles:
- { role: selinux, tags: selinux }
- { role: httpd, tags: httpd }
- { role: resolver, tags: resolver }
You can then run the p4.yml playbook on the two hosts and interpret the output. The same command can be run with the โcheck parameter for a dry run, and with the -k parameter when you want to use password authentication.
Explanation:
- Ansible-playbook command that runs p4.yml
- Playbook skipsSELinux role because it is already enabled.
- Ansible found that httpd package is already installed, so it returns ok.
- Resolver was set, and role resolver got status changed.
Ansible Commands Cheat Sheet
Install EPEL repo on Centos/RHEL systems
[root@ansible-server ~]# sudo yum install epel-release
Install ansible package on Centos/RHEL systems
[root@ansible-server ~]# sudo yum install -y ansible
Perform an update to the packages on Debian/Ubuntu systems
$ sudo apt update
Install the software-properties-common package on Debian/Ubuntu systems
$ sudo apt install software-properties-common
Install ansible personal package archive on Debian/Ubuntu systems
$ sudo apt-add-repository ppa:ansible/ansible
Install ansible on Debian/Ubuntu systems
$ sudo apt update $ sudo apt install ansible
Issue a ping command on all servers defined in the inventory file named hosts
[root@ansible-server test_ansible]# ansible -i hosts all -m ping
Issue a ping command only on host2
[root@ansible-server test_ansible]# ansible -i hosts all -m ping --limit host2
Copy the file “testfile” on all hosts in the inventory file
[root@ansible-server test_ansible]# ansible -i hosts all -m copy -a "src=/root/test_ansible/testfile dest=/tmp/testfile"
Install ncdu package on all hosts
[root@ansible-server test_ansible]# ansible -i hosts all -m yum -a 'name=ncdu state=present'
Remove ncdu package on all hosts
[root@ansible-server test_ansible]# ansible -i hosts all -m yum -a 'name=ncdu state=absent'
Build the directory structure for role named role1.
[root@ansible-server test2]# ansible-galaxy init role1
Dry-run p4.yml playbook
[root@ansible-server test_ansible]# ansible-playbook -i hosts p4.yml --check
Run p4.yml playbook with password authentication for all hosts
[root@ansible-server test_ansible]# ansible-playbook -i hosts p4.yml -k















