ansible-basics
When you are new to ansible and want to know what ansible does, read these pages and try the examples given here.
I will try to explain what ansible does and why.
For this tutorial, we wil use a small linux system running through WSL in windows (for windows users).
Linux users wil know already where to find this.
To install the small ubuntu system:
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
PS C:\Users\example> wsl --install Ubuntu
The requested operation requires elevation.
Downloading: Windows Subsystem for Linux 2.7.11
Installing: Windows Subsystem for Linux 2.7.11
Windows Subsystem for Linux 2.7.11 has been installed.
Installing Windows optional component: VirtualMachinePlatform
Deployment Image Servicing and Management tool
Version: 10.0.26100.8737
Image Version: 10.0.26200.8875
Enabling feature(s)
[==========================100.0%==========================]
The operation completed successfully.
The requested operation is successful. Changes will not be effective until the system is rebooted.
Reboot your windows machine (as always...)
What is ansible
Ansible is a piece of software that runs tasks on a machine, without having software installed on that machine (agentless).
It uses the network interface on that system over the (ssh protocol on linux). The authentication method used can be specified
(if not key authentication is tried).
So ansible doesn't work out of the box, you must configure the target systems to accept an ansible connection.
The file with the tasks to run against a system is called a "playbook" and consists of yaml formatted tasks.
Every task in a playbook (we will explain this ) is executed separately on the target system.
Ansible is not a programming language a task in ansible tells ansible to configure something on the machine (or list of machines) to the specifications given in the task. As an example we tell ansible to create a file:
- name: Create a file
ansible.builtin.file:
name: this_file.txt
state: present
With this piece of code, ansible will create a file in the home direcory of the user running the playbook. What we don't know is the other file parameters, like access rights, content and ownership. We didn't tell ansible to consider these parameters, so it will apply defaults for these. Defaults are not always what you need or want, so best is, to specify those parameters.
Ansible is designed to be idempotent, this means that no matter howoften I run this task, it wil create the file only once and only if the file was deleted between 2 runs, it will be recreated.
What ansible is not
Ansible is not a programming language.
Ansible will apply the configuration you specify at runtime, it will not prevent you from changing it afterwards. It will revert changed lines
in configuration files that you have specified, it will not undo all changes in a file (when not templated).
So it will apply the configuration as you specify it on deployment, but it allows for change afterwards, unlike other tools.
Prepare the virtual machine
Now we can access the virtual machine and start to install some packages needed to run ansible playbooks.
What we need:
packages:
- python3.x
- python3-pip
- ansible-core
Run the following commands:
sudo apt update && sudo apt upgrade -y
sudo apt install software-properties-common -y
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible -y
ansible --version
A lot of updates will be installed, but have no fear it wil only take a few minutes.
If all is OK, the last command wil return something like:
ansible [core 2.21.2]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/wilco/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/dist-packages/ansible
ansible collection location = /home/wilco/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.14.4 (main, Jun 18 2026, 14:25:02) [GCC 15.2.0] (/usr/bin/python3)
jinja version = 3.1.6
pyyaml version = 6.0.3 (with libyaml v0.2.5)
If you see something like the above, you have installed ansible successfully (and updated your linux in the process).
What did you really do with the commands you issued?
Here it is..:
sudo apt update && sudo apt upgrade -y
-- update the package database and upgrade installed packages..
sudo apt install software-properties-common -y
-- install the standard package list for a system...
sudo add-apt-repository --yes --update ppa:ansible/ansible
-- add the package repository and install ansible...
sudo apt install ansible -y
ansible --version
-- show the installed ansible version...
First steps
The Ubuntu system within your windows is what we will use for your first steps with ansible, we will not do anything dangerous to your system, just maybe install and configure a piece of sofware. But first we will take the first small step:
Using an editor
Creating playbooks on linux requires some skill with editing files on linux.
Lets start with the 'nano' editor, this is the simplest editor and for linux experts, the least used.
For these exercises we will use the nano editor.
When starting nano without any parameters, this is what you will see:
GNU nano 5.6.1 New Buffer
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location M-U Undo M-A Set Mark
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^_ Go To Line M-E Redo M-6 Copy
You can start typing your text like using notepad on windows, to exit press 'Ctrl-X'
Nano will then ask you if you want to save the typed text, answwer 'Y' or 'N'.
As we didn't give a filename, nano wil now ask for the filename to save, input a filename and press 'Enter'.
The file is now saved in your home directory.
Create your first playbook
Learning ansible is best done hands-on, books can tell a lot, but combined with hands-on experience it becomes much clearer.
As a first playbook, we will get and list some variables that ansible creates by default when a playbook is run on a system.
The playbook:
---
- name: My first playbook
hosts: localhost
gather_facts: true
tasks:
- name: Show all variables
ansible.builtin.debug:
var: ansible_facts
The text file above is a valid playbook in terms of ansible and we will explain this here.
The layout of an ansible file (indentation) is a key part of an ansible playbook. Keep this in mind when writing playbooks, errors in the
indentation will make a playbook unuseable.
The first line --- marks the start of the 'yaml', this is required for ansible to recognise a playbook and it has to be the first line in the file.
The next block:
- name: My first playbook
hosts: localhost
gather_facts: true
Tells ansible the name of the play ( there can be more plays in a file! ) that it will be executing.
The hosts variable will tell ansible where (on wich machines) to execute the tasks.
gather_facts: true tells ansible to collect machine variables during initialization, these are important when using ansible.
The line with tasks: tells ansible that from here the execution of the playbook begins, there are more tags like this, we will dicuss them later.
The last block is the task we want ansible to perform, in this case, just output all the variables found in the initialization process.
- name: Show all variables
ansible.builtin.debug:
var: ansible_facts
name Is a descriptive name for the task at hand, always start this with a capital letter.
ansible.builtin.debug: is the full name of the ansible module we want to execute.
var: ansible_facts is what we want the module to output on the terminal.
You can find the documentation of the module here
Let's create the playbook and run it now:
Open your terminal in windows wsl (start the 'wsl.exe' from the start menu ), or start typing on your linux terminal:
nano example1.yaml 'Enter'
Type or copy/paste the above playbook into the terminal, when done, press Ctrl-X and save the file.
We now have a playbook we can run in ansible, just entr the following command in your terminal:
ansible-playbook example1.yaml -i localhost folowed by 'Enter'
Below the output of your first playbook:
[WARNING]: Unable to parse /home/user/localhost as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [My first playbook] ***********************************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]
TASK [Show all variables] **********************************************************************************************
ok: [localhost] => {
"ansible_facts": {
"all_ipv4_addresses": [
"172.23.17.73",
"10.255.255.254"
],
"all_ipv6_addresses": [
"fe80::215:5dff:fe51:6dc5"
],
"ansible_local": {},
....
Output omitted
...
PLAY RECAP *************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
First you will see a number of warnings, these are caused by the absense of an inventory file (we passed i localhost as inventory).
If you get the output as shown, you have completed your first ansible playbook.
The output of ansible is very well structured, in order you'll see:
| task header | Description |
|---|---|
| 'PLAY [Play name from the yaml file] *****' | This indicates the start of the play. |
| 'TASK [Gathering Facts] *****' | This means ansible is started and collecting system variables. |
| 'TASK [Show all variables] ****' | After this the output of the debug task is presented. |
The result code of the task is given directly below the line as:
- ok: [hostname]
This result can be a number of things:
| result | Description |
|---|---|
| ok: | Nothing was changed, this task was already done |
| changed: | The task did changed things on the system and all is well |
| skipped: | The task is not executed |
| FAILED: | The task failed to configure the item on this system |
| [ERROR]: | The task failed because.... |
If you receieve an error like below:
[ERROR]: YAML parsing failed: Colons in unquoted values must be followed by a non-space character.
Origin: /home/user/example1.yaml:9:28
7
8 - name: Show all variable
9 ansible.builtin.debug:
^ column 28
For example:
raw: echo 'name: ansible'
Should be:
raw: "echo 'name: ansible'"
Then there is probably an indentation error in the file, review the file in the editor to correct the indentation or typo and try running it again.
Now we will actually do something
The next playbook is a little more advanced and will install a simple piece of software (nginx webserver) on your virtual machine.
---
- name: Install and start Nginx on WSL
hosts: localhost
connection: local
become: true
tasks:
- name: Update apt cache and install Nginx
ansible.builtin.apt:
name: nginx
state: latest
update_cache: true
- name: Start Nginx service through sysvinit
ansible.builtin.sysvinit:
name: nginx
state: started
- name: Show success message
ansible.builtin.debug:
msg: "Nginx is installed and started on WSL! Url: http://localhost"
In the above playbook, you will see some familiar items we have seen in our first playbook and some new items.
become: true This means that the playbook in this case doesn't run as your (non-privilleged) user, but as the root (administrator) user.
This is needed for tasks that install software or create services on linux.
The placement of the become statement here wil cause the whole playbook running as the root user, this is not a best practice!.
The 'become: true' should only be enabled for the tasks that need root permissions.
The last task we have already seen before, we output a standard message to the terminal of the caller of the playbook.
The difference here is in the 2 new tasks that really do something on the system:
- name: Update apt cache and install Nginx
ansible.builtin.apt:
name: nginx
state: latest
update_cache: true
The name here is a good description of what the task does, it will install the nginx webserver on the system.
- name: Start Nginx service through sysvinit
ansible.builtin.sysvinit:
name: nginx
state: started
Only installing the software is not enough to have a running webserver, we will have to start the service to enable the webserver.
Let's create the playbook and run it now:
Open your terminal in windows wsl (start the 'wsl.exe' from the start menu ), or start typing on your linux terminal:
nano example2.yaml 'Enter'
Type or copy/paste the above playbook into the terminal, when done, press Ctrl-X and save the file.
We now have a playbook we can run in ansible, just entr the following command in your terminal:
ansible-playbook example2.yaml -i localhost folowed by 'Enter'
When running this playbook, you will get an error:
[WARNING]: Unable to parse /home/user/localhost as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Install and start Nginx on WSL] **********************************************************************************
TASK [Gathering Facts] *************************************************************************************************
[ERROR]: Task failed: Premature end of stream waiting for become success.
>>> Standard Error
sudo: interactive authentication is required
fatal: [localhost]: FAILED! => {"changed": false, "msg": "Task failed: Premature end of stream waiting for become success.\n>>> Standard Error\nsudo: interactive authentication is required"}
PLAY RECAP *************************************************************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
This is because your user has no access to the root account that ansible expects because of the become: true flag. Ansible by default expects you have root access through the sudo command.
To enable this we need to edit the following file:
sudo nano /etc/sudoers
Add the following line to the end of the file:
# See sudoers(5) for more information on "@include" directives:
your_username ALL=NOPASSWD: ALL
@includedir /etc/sudoers.d
And save the file.
Re-run the playbook and you will see that it runs without problems now:
[WARNING]: Unable to parse /home/wilco/localhost as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Install and start Nginx on WSL] **********************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]
TASK [Update apt cache and install Nginx] ******************************************************************************
changed: [localhost]
TASK [Start Nginx service through sysvinit] ****************************************************************************
ok: [localhost]
TASK [Show success message] ********************************************************************************************
ok: [localhost] => {
"msg": "Nginx is installed and started on WSL! Url: http://localhost"
}
PLAY RECAP *************************************************************************************************************
localhost : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
When you go to the url with your local browser in windows, you will see the nginx default welcome page.
Reverting the installation of nginx
The next step is to revert the installation of the nginx service we created is the last playbook.
Therefore we revert the actions taken by the last playbook and change some tasks:
---
- name: stop and de-install Nginx on WSL
hosts: localhost
connection: local
become: true
tasks:
- name: Stop Nginx service through sysvinit
ansible.builtin.sysvinit:
name: nginx
state: stopped
- name: Remove Nginx
ansible.builtin.apt:
name: nginx
state: absent
- name: Show success message
ansible.builtin.debug:
msg: "Nginx is stopped and removed from WSL!"
As you can see we reversed the tasks from the previous playbook, so we first stop the service and the remove the software from the host.
We run the playbook:
ansible-playbook example2.yaml -i localhost folowed by 'Enter'
[WARNING]: Unable to parse /home/user/localhost as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [stop and de-install Nginx on WSL] ********************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]
TASK [Stop Nginx service through sysvinit] *****************************************************************************
changed: [localhost]
TASK [Remove Nginx] ****************************************************************************************************
changed: [localhost]
TASK [Show success message] ********************************************************************************************
ok: [localhost] => {
"msg": "Nginx is stopped and removed from WSL!"
}
PLAY RECAP *************************************************************************************************************
localhost : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Expand the webserver playbook with a default page
In the second example we installed the nginx webserver, we will now expand this play by adding a file creation, that becomes the default web page for this webserver. In this page we will use some variables and will see what they do.
Recap of the previous play:
---
- name: Install and start Nginx on WSL
hosts: localhost
connection: local
become: true
tasks:
- name: Update apt cache and install Nginx
ansible.builtin.apt:
name: nginx
state: latest
update_cache: true
- name: Start Nginx service through sysvinit
ansible.builtin.sysvinit:
name: nginx
state: started
- name: Show success message
ansible.builtin.debug:
msg: "Nginx is installed and started on WSL! Url: http://localhost"
We will now expand this play by adding a html file in the correct location and with a correct content and show you that ansible is indeed idempotent.
first we will copy the file example2.yaml to example4.yaml: cp example2.yaml example4.yaml.
In the last example(3) we removed nginx, now we will reinstall it again by using the copied example4.yaml playbook:
ansible-playbook example4.yaml -i localhost
This installs the nginx server again and starts the service. Notice that the start of the service task returns ok:,
this means the nginx server is already running after installation.
Now if we want to replace the default html page with our own, we must find where this file is.. for most webservers on linux,
pages are server from the 'var/www/html' directory, let us check if there is a file there..
user@DESKTOP:~$ ls /var/www/html/
index.nginx-debian.html
There is a html file and if you edit the file, you would see that it represents the page that you get served by the webserver.
We will now expand the playbook to remove this existing file and write a new file.
---
- name: Install and start Nginx on WSL
hosts: localhost
connection: local
become: true
tasks:
- name: Update apt cache and install Nginx
ansible.builtin.apt:
name: nginx
state: latest
update_cache: true
- name: Start Nginx service through sysvinit
ansible.builtin.sysvinit:
name: nginx
state: started
- name: Remove the default page
ansible.builtin.file:
path: /var/www/html/index.nginx-debian.html
state: absent
- name: Create new index.html file
ansible.builtin.copy:
dest: /var/www/html/index.html
content: |
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx on node {{ inventory_hostname }}!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx on node {{ inventory_hostname }}!</h1>
<p>If you see this page, the nginx web server is successfully installed by ansible
working. Further configuration is not required.</p>
</body>
</html>
mode: '0755'
owner: root
group: root
- name: Show success message
ansible.builtin.debug:
msg: "Nginx is installed and started on WSL! Url: http://localhost"
When running the playbook again, we will see that the tasks that ran in a previous run will return the 'ok' status.
The new added tasks however will give a 'changed' result, like below:
[WARNING]: Unable to parse /home/wilco/localhost as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Install and start Nginx on WSL] **************************************************************************************************
TASK [Gathering Facts] *****************************************************************************************************************
ok: [localhost]
TASK [Update apt cache and install Nginx] **********************************************************************************************
ok: [localhost]
TASK [Start Nginx service through sysvinit] ********************************************************************************************
ok: [localhost]
TASK [Remove the default page] *********************************************************************************************************
changed: [localhost]
TASK [Create new index.html file] ******************************************************************************************************
changed: [localhost]
TASK [Show success message] ************************************************************************************************************
ok: [localhost] => {
"msg": "Nginx is installed and started on WSL! Url: http://localhost"
}
PLAY RECAP *****************************************************************************************************************************
localhost : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
And if you go to the localhost with your browser, you can see that the default page has indeed changed.
Variables in ansible playbooks
The power of any (programming)language is using variables, (ansible is not a programming language) but it uses variables.
This can make the playbooks you write applicable to many hosts, without changing a single line in the playbook. Variables can be defined in
the playbook itself, passed to the playbook on the command line, or can be passed through an inventory, we will show you all these methods.
variables in the playbook
In our last example playbook we silently used a variable in the playbook that variable is created during the startup of ansible, the gather facts stage.
A variable in a playbook is recognized by the folowing structure:
"{{ variable_name }}"
If you look at the last example, you will find {{ inventory_hostname }} while this looks like a ansible variable, it is not, because it doesn't
have the double quotes. This is a jinja2 variable ( we will discuss this later ).
To define a variable in a playbook, we can use 2 methods:
- define a variable in the header
- define a variable when needed
Define the variable in the header:
---
- name: Install and start Nginx on a host
hosts: "{{ instance }}"
connection: local
become: true
vars:
var_name: 'var_value'
In the above example we changed 2 things:
- First we changed 'localhost' to an variable "{{ instance }}"
- Secondly we defined the variable 'var_name' with the value 'var_value'
This changes the following:
When the playbook starts, it will try to translate the variable 'instance' to a host (or list of hosts), and run the playbook against these hosts.
This is a very powerfull feature.
But it will fail with the command from example4, because 'instance is not defined'.
The variable 'var_name' is defined during the run of this play and can be used in tasks like this:
- name: Create new file
ansible.builtin.copy:
dest: "{{ var_name }}.txt"
content: 'This file is created from a variable'
mode: '0655'
This task will create a file named 'var_value.txt' in the home directory of the user on the system the playbook runs against.
Define a variable when needed:
We can also define a variable on the fly, this can be very handy for saving output from a previous task for later use or set a variable to use in a folowing step.
- name: Update apt cache and install Nginx
ansible.builtin.set_fact:
packages_to_install:
- nginx
- nano
- name: Update apt cache and install Nginx
ansible.builtin.apt:
name: "{{ packages_to_install }}"
state: latest
In this example we define a variable 'packages to install' with a list of packages the have to be installed on the system, simply expand the list and on the next run they will be installed.
variables passed on the command line
ansible allows you to pass variable values on the command line, this enables us to create playbooks that will vary tasks in each run, depending on the content of the passed variable. In the previous section we changed the header of the example playbook and said the playbook wouldn't run because 'instance' is not defined.
---
- name: Install and start Nginx on a host
hosts: "{{ instance }}"
connection: local
become: true
vars:
var_name: 'var_value'
When we change our command line of example4 to this:
ansible-playbook example4.yaml -i localhost -e instance=localhost
It will work again.
What we did just now, is passing the instance variable name with the value 'localhost' to the playbook.
We can pass multiple variables to a playbook by repeating the -e parameter folowed by 'variable_name=value' structure.
By using this way of calling playbooks, you can vary playbook outcomes on different systems and even control wich systems are targetted from the
playbook.
When using this, you can surely see the power of ansible playbooks, but the need of large numbers of variables grows exponentially when plybooks grow and this way of passing varibles will soon be unmanageable on the commandline. This is where an inventory can help.
variables from an inventory
Until now we used the -i parameter on the commandline without explanation, -i specifies the inventory to use for the execution of the play. As we passed
'localhost', we got warnings about the inventory that couldn't be parsed. This is because localhost isn't really a inventory file, but a hostname.
Inventories can have multiple formats like: yaml, ini, json and more..
We will use the yaml format here.
An inventory consists of the following:
- groups of hosts
- hostnames
- group variables
- host variables
A host in an inventory is defined in a tree:
[all]
|
- [webservers]
| - webserver1.example.org
| - webserver2.example.org
Below the example from the inventory above in yaml with variables defined in the inventory:
---
all:
# 1. Group variables valid on all hosts in this inventory
vars:
packages_to_install_all:
- nano
common_ntp_server: pool.ntp.org
# 2. Specific subgroups (children) within the all group
children:
webservers:
# Hosts that belong to the webservers group
hosts:
webserver1.example.org:
http_port: 80 # host specific variable for this host only
webserver2.example.org:
http_port: 8080 # The http_port is different for this host
vars: # group variables are defined here
group_packages:
- nginx
As we go through the inventory, we see variables defined on various levels:
[all]
|
- [webservers]
| - webserver1.example.org
| - webserver2.example.org
For example we look at the webserver2.example.org host, we go through the inventory from the base to the host and collect all variables for this host:
the complete list of variables is:
- From the 'all' group:
- packages_to_install_all = nginx
- common_ntp_server = pool.ntp.org
- from the 'webservers' group:
- group_packages = nginx
- from the 'host' vars:
- http_port = 8080
Now try to find the values for the webserver1.example.org and find the differences in variables, you should find '1' difference.
If you save this to a file inventory.yaml and pass that filename on the commandline, ansible will read the inventory for the host to configure and apply the variables to the play on that host. This way a play can vary on a per host basis, without modifying the code.
You can find the variables for a host with the followihg command:
ansible-inventory -i inventory.yaml -- host webserver1.example.org
This will give you the following output:
{
"common_ntp_server": "pool.ntp.org",
"group_packages": [
"nginx"
],
"http_port": 8080,
"packages_to_install_all": [
"nano"
]
}
This was just a brief introduction to ansible. There might be more... let me know though comments.