Ansible DevOps - Playbook


IV - Playbook
IV - Playbook
1 - Presentation
The playbook
Ansible is used for orchestrating IT tasks. A playbook
is a YAML
file containing, one or more _"plays"_ that is used to set the desired state of a remote system.
This is one of the core components of Ansible, as they store and execute the Ansible configuration.
The playbook
also contributes to our automation efforts by bringing together all the resources needed to orchestrate ordered processes or avoid repeating manual actions. The playbook
can be reused and shared between people, we will also be able to version it, as it is source code which allows us to work in groups to improve the writing and quality of our playbook
.
2 - Structure of the playbook
A playbook
is composed of one or more plays to be executed in a specific order. A playbook is an ordered list of tasks to run on the desired host group.
Each task is associated with a module responsible for an action and its configuration parameters. Since most tasks are idempotent, we can safely rerun a playbook
without any problems.
Playbooks are written in yaml using the standard .yaml extension with minimal syntax.
Let's write a
playbook
that will consist of two sets and allow us to set up configurations for our two groupsweb server
anddatabase server
.
Let's start by creating a file called
datascientest-playbook.yaml
:
#/HOME
cd
nano datascientest-playbook.yaml
Let's add this content:
- name: Installs apache , copies files to server group serverweb # name of first game
hosts: serverweb # target groups or hosts to deploy to
become: yes # allow elevation of privileges actions
tasks:
- name: apache2 installation with privilege elevation # task name
apt: # call the apt module
name: apache2
state: present
- name: start apache2 service with service module # task name
service: # call the service module
name: apache2 # we want to start the Apache service
state: started # state we want the service in
enabled: yes # we also want the service to be started at server startup
- name: Install apache, copy files with elevations of privileges # task name
copy: # call the copy module
src: index.html # file to copy from Ansible server
dest: /var/www/html # directory where the index.html file will be copied to the targets
mode: "0644"
- name: postgresql installation and service activation # name of the second play
hosts: serverdatabase # groups or target hosts on which we will deploy
become: yes # allow elevation of privileges actions
tasks:
- name: Installing postgresql to the latest version # task name
apt: # call the apt module
name: postgresql # we want to install postgresql with the apt module
state: latest # we want postgresql to be the latest version
- name: start potgresql service with service module # name of the task
service: # call the service module
name: postgresql # we want to start the postgresql service
state: started # state we want the service in
enabled: yes # we also want the service to be started when the server starts
We define a descriptive name for each set based on what we want to set up. Next, we represent the group of hosts on which the game will be run, taken from the inventory. Finally, we define that these games should be run as a root
user with the become
option set to _"yes"_.
We can also set many other playbook keywords at different levels such as the play
, the task
, the playbook
to configure Ansible's behavior. Moreover, most of them can be set at runtime as command line flags in the Ansible configuration file, the ansible.cfg
file or the inventory. Let's look at the priority rules to understand how Ansible behaves in these cases.
Next, we use the tasks
parameter to define the list of tasks for each set. For each task, we define a clear and descriptive name. Each task leverages a module to perform a specific operation.
The first task in the first set uses the apt
module. With this module, we typically need to define module arguments. For the second task of the first play, we use the service
module that helps us manage the state of services on our remote system.
3 - Running the playbook
When we run a playbook
, Ansible runs each task in order, one at a time, for all the hosts we have selected. This default behavior can be adjusted for different use cases using policies.
If a task fails, Ansible stops running the playbook
on that specific host but continues the others that succeeded. During execution, Ansible displays information about the connection status, task names, execution status, and whether any changes were made.
Once complete, Ansible provides a summary of the playbook run with resources created and errors.
Create the inventory file:
nano inventory.yaml
We will have this content written in YAML for our server web
and database
:
all:
vars:
ansible_user: datascientest
ansible_ssh_private_key_file: ~/.ansible/key.pem
ansible_become_pass: Datascientest2024
webserver:
hosts:
serverweb1.datascientest.com:
ansible_host: 172.31.x.x
vars:
tier: web
databaseserver:
hosts:
serverdatabase1.datascientest.com:
ansible_host: 172.31.x.x
tier: bd
Let's download our server's homepage so that Ansible can copy it to our target:
wget https://dst-de.s3.eu-west-3.amazonaws.com/ansible_fr/index.html
Now let's run the playbook with the ansible-playbook
command.
ansible-playbook -i inventory.yaml datascientest-playbook.yaml
We get this output:
PLAY [Installs apache , Copies files to server group web] ********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [apache2 installation with privilege elevations] ************************************************************************************************************
changed: [serverweb1.datascientest.com]
TASK [start apache2 service with service module] **********************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [Install apache, copy files with elevations of privileges] **************************************************************************************************************
changed: [serverweb1.datascientest.com]
changed: [serverdatabase1.datascientest.fr]
PLAY [postgresql installation and service activation] ***********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ok: [serverdatabase1.datascientest.com]
TASK [Installing postgresql to the latest version] ***********************************************************************************************************
TASK [starting potgresql service with the service module] ********************************************************************************************************
ok: [serverdatabase1.datascientest.fr]
PLAY RECAP ***********************************************************************************************************************************************************
serveurdatabase1.datascientest.fr : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverweb1.datascientest.com : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You will need to find the following page on your browser by going to the IP address of the dedicated web server
:

To verify, we can go to the host in the database server:
root@serveurdatabase1.datascientest.fr:~# sudo -i -u postgres
postgres@serveurdatabase1.datascientest.fr:/home/datascientest$ psql
psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1))
Type "help" for help.
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=# \du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
postgres=#
postgres=#
We can use the --limit
flag to limit the execution of the playbook
to specific hosts
ansible-playbook -i inventory.yaml datascientest-playbook.yaml --limit serverdatabase1.datascientest.fr
4 - Variables
Variables are placeholders for values that we can reuse in a playbook
or other Ansible objects. They can only contain letters, numbers, and underscores and start with letters.
Variables can be defined at multiple levels according to priorities. We can define variables at the global level for all hosts, at the host level for a particular host, or at the role level for a specific read.
To define host and group variables, we can create the group_vars
and host_vars
directories. At the group_vars
directory, we need to define renamed files with the name of the group for which we want to define variables.
Let's create the file
web-server.yaml
in thegroup_vars
directory and define the default variables common to all hosts in this group:
mkdir group_vars
cd group_vars
nano serverweb.yaml
Now let's move the variables for this group into this file. Write this content to the file
group_vars/serverweb.yaml
:
tier: web
Let's create the
serverdatabase.yaml
file in thegroup_vars
directory. Let's define the default variables common to all hosts in this group:
nano serverdatabase.yaml
Let's now move the variables for this group into this file. Let's put this content in the group_vars/serverdatabase.yaml
:
tier: db
Let's change the contents of the
inventory.yaml
:
all:
databaseserver:
hosts:
serverdatabase1.datascientest.com
web server:
hosts:
serverweb1.datascientest.com
Let's also add to the
group_vars
directory a file namedall.yaml
in which we'll define the variables that will be available to all the hosts in our inventory.
nano group_vars/all.yaml
Let's add this content to the
group_vars/all.yaml
:
ansible_user: datascientest
ansible_ssh_private_key_file: ~/.ansible/key.pem
ansible_become_pass: Datascientest2024
In the host_vars
directory, we need to define renamed files with the names of the hosts for which we want to set variables.
Let's create the file
web server1.datascientest.en.yaml
in thehost_vars
directory. Let's set the default variables for this host:
mkdir host_vars
cd host_vars
nano serverweb1.datascientest.com.yaml
In the serverweb1.datascientest.com.yaml
file, let's add this content:
ansible_host: 172.31.x.x
Let's create the serverdatabase1.datascientest.en.yaml
file in the host_vars
directory:
nano serverdatabase1.datascientest.en
In the serverdatabase1.datascientest.en.yaml
file, let's add this content:
ansible_host: 172.31.x.x
View your inventory tree:
ansible-inventory -i inventory.yaml --graph
You can also display your tree with the tree
command:
sudo apt-get install tree
tree
# Result
.
├── ansible.cfg
├── datascientest-playbook.yaml
├── group_vars
│ ├── all.yaml
│ ├── serverdatabase.yaml
│ └── serverweb.yaml
├── host_vars
│ ├── serverdatabase1.datascientest.en.yaml
│ └── serverweb1.datascientest.com.yaml
├── index.html
├── inventory.ini
└── inventory.yaml
2 directories, 10 files
So we can run an Ad Hoc command to validate that everything works:
ansible all -i inventory.yaml -m ping
We have this result:
serverdatabase1.datascientest.co.uk | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
serverweb1.datascientest.com | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
To substitute variables during the execution of the ansible_playbook
command, we use the -e flag.
ansible-playbook -i inventory.yaml -e ansible_user=datascientest datascientest-playbook.yaml
The easiest way to define variables is to use a vars
block at the beginning of a game. They are defined using the standard yaml
syntax:
- name: this is a variable
hosts: web server
vars:
ansible_user: Datascientest
ansible_sudo_pass: Datascientest2024
Another method is to define variables in external yaml files with the vars_files
argument.
- name: this is a variable
hosts: web server
vars_files:
- variables/datascientest_variables.yaml
To use them in tasks, they must be referenced by placing their name in double braces using the Jinja2 syntax:
- name: this is a variable
hosts: web server
vars:
user: datascientest
tasks:
- name: add user {{ user }}
user:
name: "{{ user }}"
state: present
If a variable's value begins with braces, we must quote the entire expression to allow YAML
to interpret the syntax correctly.
We can also define multi-valued variables as lists.
packages:
- git
- tree
- apache2
It is also possible to reference individual values from a list. For example, to select the first git
value, we would call it like this:
packages: "{{ packages[0] }}"
Another possible option is to define variables using yaml
dictionaries.
dictionary:
- lastname: datascientest
- first name: google
Similarly, to get the first field in the dictionary:
dictionary['name']
To reference nested variables, we must use parenthesis or dot notation. To get the value cursus-datascientist
from this structure:
vars:
variable:
training:
devops: cursus-devops
datascientist: cursus-datascientist
tasks:
- name:creation cursus-devops
user:
name: "{{ variable['training']['devops'] }}"
We can create variables using the register
statement that captures the output of a command or task, then allows us to use them in other tasks.
- name: register test
hosts: all
tasks:
- name: run a script and save the output as a variable
shell: "find datascientest.sh"
args:
chdir: "/tmp"
register: output_register
- name: displays the result of the previous task
debug:
var: output_register
5 - Ansible facts
By default, before running the set of tasks defined in a playbook
, Ansible will go and collect information about the remote hosts. This information, called facts
, contains details such as:
The network interfaces and addresses
The operating system running on the distant nodes
The available memory
The number of CPUs
The manufacturer of the machine
Other information is also available.
Ansible stores facts in json
format, with elements grouped into nodes. Ansible will run the setup
module before each game run to harvest this information. By default, in Ansible's configuration the gather_facts
option has the value "yes"
. If we pass this option to False
or "no"
, Ansible will not run the setup
module before running the various sets and it will therefore be impossible for it to use information from remote hosts. For example, we will be able to put conditions on the execution of certain variables.
The Ansible's facts
are the system data and properties specific to the host we are connecting to. A fact can be the IP address, BIOS
information, software information and even hardware information of a system.
The Ansible's facts
helps the administrator manage hosts based on their current state, rather than taking action directly without having any information about the health of the system.
6 - Conditional Tasks
To further control the flow of execution in Ansible, we can take advantage of conditions. Conditions allow us to execute or ignore tasks based on meeting certain conditions. Variables, facts or results from previous tasks, and operators can be used to create such conditions.
Some example use cases could be to update a variable based on a value of another variable, to skip a task if a variable has a specific value, to execute a task only if a host fact returns a value above a threshold.
To apply a simple conditional statement, we use the when parameter on a task. If the condition is met, the task is executed. Otherwise, it is ignored:
- hosts: all
tasks:
- package:
name: "httpd"
state: present
when ansible_facts["hostname"] == "serverweb1.datascientest.com"
Another common pattern is to control task execution based on remote host attributes that we can obtain from facts
. Let's look at this list of commonly used facts to get an idea of all the facts
we can use under certain conditions.
- name: Example Conditional Facts
hosts: all
vars:
os_support: # Here we define the list of systems supported by our tasks
- RedHat
- Fedora
- Centos
tasks:
- name: nginx installation
yum:
name: "nginx"
state: present
when: ansible_facts['distribution'] in os_support # installs nginx if the target distribution is included in the list of those that our deployment must support
If we need to install the Apache web server on all our Linux hosts. We know that Red Hat Enterprise Linux (RHEL
) based hosts work with Red Hat Package Manager (RPM
) and yum/dnf
.
Other Linux distributions use their own package managers, so it would not be possible to perform the same task on different systems without modification and reference to these differences. Package names also differ from one distribution to another.
For example, on RHEL systems, the Apache web server package is httpd
, while on other distributions the name is different.
Let's write an example playbook that will deploy
httpd
on hosts only if the remote host family does belong toRedHat
:
- name: Distibution-based apache installation
# Define the remote server on which Ansible will run
hosts: web server
remote_user: datascientest # remote user
become: true # elevation of privileges
tasks:
# (Task-1) Check if ansible_os_family == "RedHat" then install Apache on the remote node
- name: Install Apache on CentOS Server
yum: name=httpd state=present
become: yes
when: ansible_os_family == "RedHat"
# (Task-2) Check if ansible_os_family == "Debian" then install Apache on the remote node
- name: Install Apache on Ubuntu Server
apt:name=apache2 state=present
become: yes
when: ansible_os_family == "Debian"
It is possible to combine multiple conditions with logical operators and group them in parentheses:
when: (color=="red" or color=="black") and (size="large" or size="medium")
Next, when the statement supports the use of a list in cases where we have multiple conditions that must all be true:
when:
- ansible_facts['distribution'] == "Ubuntu"
- ansible_facts['distribution_version'] == "22.04"
- ansible_facts['distribution_release'] == "bionic"
Another option is to use conditions based on stored variables that we defined in the previous tasks:
- name: Example Registered Variables Conditional
hosts: all
tasks:
- name: Register a sample variable
shell: cat /etc/hosts
register: contents_hosts_file
- name: Let's check if the hosts file contains "localhost
shell: echo "/etc/hosts contains localhost"
when: hosts_contents.stdout.find(localhost) != -1
7 - Loops
Ansible allows us to iterate over a set of items in a task to run it multiple times with different parameters without rewriting it. For example, to create multiple files, we would use a task that iterates over a list of directory names instead of writing five tasks with the same module.
To iterate over a simple list of items, let's use the loop
keyword. We can reference the current value with the item
loop variable.
- name: "Creating multiple files in the /tmp directory"
file:
state: touch
path: /tmp/{{ item }}
loop:
- file1
- file2
- file3
The output of the above task that uses loop
and item
:
TASK [Creating multiple files in /tmp directory] *********************************
changed: [vagrant1] => (item=file1)
changed: [vagrant1] => (item=file2)
changed: [vagrant1] => (item=file3)
It is also possible to iterate over dictionaries:
- name: "Create files with dictionaries"
file:
state: touch
path: "/tmp/{{ item.name }}" # allows us the values (file1 , file2 , file3 ) by passing the name key at each iteration
mode: "{{ item.mode }}" # allows us the mode values by passing the mode key at each iteration
loop:
- { name: "file1", mode: "755" }
- { name: "file2", mode: "775" }
- { name: "file3", mode: "777" }
Another useful pattern is to browse a group of hosts in the inventory:
- name: Retrieves the hosts in the databases group
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ groups['databases'] }}"
By combining conditions and loops, we can choose to run the task only on some items in the list and ignore it for others:
- name: Run when list values are greater than 90
command: echo {{ item }}
loop: [100, 200, 3, 600, 7, 11]
when: item > 90
Another option is to use the until
keyword to retry a task until a condition is true.
- name: Let's retry a task until we find the word "success" in the logs
shell: cat /var/log/datascientest.log
register: logoutput # register the content of the datascientest.log file
until: logoutput.stdout.find("success") != -1 #
retries: 10 # number of retries
delay: 15 # waiting time between 2 tries
You can check out the official Ansible guide on loops for more advanced use cases.
8 - Tags (TAGS)
Ansible, gives us ways to simplify playbooks and save time when debugging. One of the features for writing Ansible playbooks is its support for tags.
Tags are metadata that we can attach to tasks in an Ansible playbook. They allow us to selectively target certain tasks at runtime, telling Ansible to run (or not run) certain tasks. Tags are extremely useful for specific task executions in an on-demand playbook.
Let's modify our
datascientest-playbook.yaml
and addweb
andbd
tags:
- name: Installs apache , Copies files to server group serverweb # primary play name
hosts: serverweb # target groups or hosts to deploy to
become: yes # allows to set up actions where we have to elevate our privileges like doing a sudo
tasks:
- name: apache2 installation with elevated privileges # task name
apt: # call the apt module
name: apache2
state: present
tags:
- web # add web tag
- name: start apache2 service with service module # task name
service: # calling the service module
name: apache2 # we want to start the postgresql service
state: started # state we want the service in
enabled: yes # we also want the service to be started when the server starts
tags:
- web # add web tag
- name: Install apache , Copy files with elevations of privileges # task name
copy: # call the copy module
src: index.html # file to copy from Ansible server
dest: /var/www/html # directory where the index.html file will be copied to the targets
mode: "0644"
tags:
- web # add web tag
- name: postgresql installation and service activation # name of the second play
hosts: database server # groups or target hosts on which we will deploy
become: yes # allows to set up actions where we have to elevate our privileges like doing a sudo
tasks:
- name: Installing postgresql to the latest version # task name
apt: # call the apt module
name: postgresql # we want to install postgresql with the apt module
state: latest # we want to install postgresql at the latest version
tags:
- bd # add db tag
- name: start potgresql service with service module # name of the task
service: # call the service module
name: postgresql # we want to start the postgresql service
state: started # state we want the service in
enabled: yes # we also want the service to be started when the server starts
tags:
- bd # add db tag
We can tell Ansible to run only the tasks or plays with the requested tags. We use the ansible-playbook
command with the -t
flag or the--tags
option.
ansible-playbook -i inventory.yaml --tags web datascientest-playbook.yaml
Exit:
PLAY [Installs apache , Copies files to server group web] ********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [apache2 installation with privilege elevations] ************************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [start apache2 service with service module] **********************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [Install apache , Copy files with elevation of privileges] **************************************************************************************************************
ok: [serverweb1.datascientest.com]
PLAY RECAP ***********************************************************************************************************************************************************
serveurdatabase1.datascientest.fr : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverweb1.datascientest.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We can also tell Ansible to ignore tasks with certain tags using the --skip-tags
flag:
ansible-playbook -i inventory.yaml --skip-tags web datascientest-playbook.yaml
Exit:
PLAY [Installs apache , Copies files to server group web] ********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ansible-playbook -i inventory.yaml --skip-tags web datascientest-playbook.yaml
ok: [serverweb1.datascientest.com]
PLAY [postgresql installation and service activation] ***********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ok: [serverdatabase1.datascientest.fr]
TASK [Installing postgresql to the latest version] **************************************************************************************************************
ok: [serverdatabase1.datascientest.fr]
TASK [Start potgresql service with service module] ********************************************************************************************************
ok: [serverdatabase1.datascientest.fr]
PLAY RECAP ***********************************************************************************************************************************************************
serveurdatabase1.datascientest.fr : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverweb1.datascientest.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Finally, you can list the tags in your playbook
using the --list-tags
flag:
ansible-playbook -i inventory.yaml --list-tags datascientest-playbook.yaml
playbook: datascientest-playbook.yaml
play #1 (serveurweb): Install apache , Copy files on the server of the group serveurweb TAGS: []
TASK TAGS: [web]
play #2 (database server): install postgresql and activate the service TAGS: []
TASK TAGS: [bd]
When we use tags in our playbook
, we find that we sometimes have tasks that we always want to run, even when we use --tags
or --skip-tags
. We can accomplish this with the always
tag.
When no tags are passed, the entire playbook
is simply executed. Ansible also provides the never
tag, which does the opposite of always
. It will ensure that a task will never run unless the user specifically targets it.
Even if no tag is specified, the verification task will not run because it contains the never
tag.
Let's modify our
playbook
and add thealways
tags with theweb
tag, and thenever
tag with thebd
tag:
- name: Installs apache , Copies files to server group serverweb # primary play name
hosts: serverweb # target hosts or groups to deploy to
become: yes # allows setting up actions where we have to elevate our privileges
tasks:
- name: apache2 installation with privilege elevation # task name
apt: # call the apt module
name: apache2
state: present
tags:
- web # add web tag
- name: start apache2 service with service module # task name
service: # calling the service module
name: apache2 # we want to start the postgresql service
state: started # state we want the service in
enabled: yes # we also want the service to be started when the server starts
tags:
- web # add the tag web
- always # add the always tag
- name: Install apache , Copy files with elevations of privileges # task name
copy: # call the copy module
src: index.html # file to copy from ansible server
dest: /var/www/html # directory where the index.html file will be copied to the targets
mode: "0644"
tags:
- web # add the tag web
- always # add the always tag
- name: postgresql installation and service activation # name of the second play
hosts: databaseserver # groups or target hosts on which to deploy
become: yes # # allows setting up actions where we need to elevate our privileges
tasks:
- name: Installing postgresql to the latest version # task name
apt: # call the apt module
name: postgresql # we want to install postgresql with the apt module
state: latest # we want to install postgresql at the latest version
tags:
- bd # add db tag
- name: start potgresql service with service module # name of the task
service: # call the service module
name: postgresql # we want to start the postgresql service
state: started # state we want the service in
enabled: yes # we also want the service to be started when the server starts
tags:
- bd # add the tag db
- never # added the never tag
Let's run our playbook only on spots tagged
db
ansible-playbook -i inventory.yaml --tags db datascientest-playbook.yaml
Exit:
PLAY [Copying files to server group web] ********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [Copy files with elevations of privileges] **************************************************************************************************************
ok: [serverweb1.datascientest.com]
PLAY [postgresql installation and service activation] ***********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ok: [serverdatabase1.datascientest.fr]
PLAY RECAP ***********************************************************************************************************************************************************
serveurdatabase1.datascientest.fr : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverweb1.datascientest.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We can see that tasks with the always
tag are always executed. A tagged task always executes if it is explicitly called.
9 - Templating with Jinja2
9.1 - Introduction
Jinja2 is a very powerful and advanced python templating language. It is very fast, reliable and widely used to generate dynamic data. It is a text-based template language and thus can be used to generate any markup as well as source code. Ansible uses jinja2
templates for template files.
Most of the time, we use templates to replace configuration files or place other files such as scripts, documents and other text files on the remote server. Templates allow us to create real files by passing them to Ansible's template
module.The file extension of a jinja2 template is .j2
.
There are several delimiters in order to pass instructions with Jinja2 .
{# #}
We use it for comments that are not included in the template output.
{%%}
:
We use it for control statements such as loops and conditions.
{{ }}
:
These double braces are the most commonly used tags in a template file and are used to call variables and ultimately print their values when running Ansible playbooks.
The Jinja2
templates are simple template files that store variables that may change from time to time. When the playbook
is run, these variables are replaced with the actual values defined in our Ansible playbook
. In this way, templates provide an efficient and flexible solution to easily create or modify a configuration file.
9.2 - Implementation
Let's create a
models
directory that we will use in order to store ourjinja2
templates.
mkdir templates
Now let's create a file named
index.html.j2
and put this content:
A message from the {{ inventory_hostname }}
{{ serverweb_message }}
This is the new structure of our project:
├── ansible.cfg
├── datascientest-playbook.yaml
├── group_vars
│ ├── all.yaml
│ ├── serverdatabase.yaml
│ └── serverweb.yaml
├── host_vars
│ ├── serverdatabase1.datascientest.en.yaml
│ └── serverweb1.datascientest.com.yaml
├── index.html
├── inventory.ini
├── inventory.yaml
└── models
└── index.html.j2
3 directories, 12 files
The Inventory_hostname
variable is natively built into Ansible, and that refers to the current host being iterated in the read. It is retrieved when launching the setup
module before running our Playbook. serverweb_message
is a variable we will set in your playbook.
Let's create a playbook named
apache2.yaml
that will install and start the httpd package and also copy the template to the remote machine.
touch apache2.yaml
Let's put this content in our
apache2.yaml
:
- name: Playbook to install and start the apache package
hosts: web server
become: yes
vars:
serverweb_message: "Datascientest ANSIBLE."
tasks:
- name: Install apache2 package
apt:
name: apache2
state: present
- name: Start httpd service
service:
name: apache2
state: started
- name: Create index.html using Jinja2
template:
src: template/index.html.j2
dest: /var/www/html/index.html
In this playbook, we will install and start Apache. We then use the template
module to create the index.html
file using Jinja2
to process and transfer the Jinja2
index.html.j2
template file we created to the /var/www/html/index.html
destination.
Let's run the
playbook
now:
ansible-playbook -i inventory.yaml apache2.yaml
We have this result:
PLAY [playbook to install and start the apache package] **********************************************************************************************************
TASK [Gathering Facts] ***********************************************************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [Installer le paquet apache2] ***********************************************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [Démarrer le service httpd] *************************************************************************************************************************************
ok: [serverweb1.datascientest.com]
TASK [Create index.html using Jinja2] **************************************************************************************************************************
changed: [serverweb1.datascientest.com]
PLAY RECAP ***********************************************************************************************************************************************************
serverweb1.datascientest.com : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

10 - Importing a playbook
Ansible
With the import_playbook
directive, one or more playbooks
can be imported. The import_playbook
directive statically imports a playbook
into another playbook
. This directive is very useful when we want to split our game into several functions, so we will create several of them as well as a main parent file that will be used to call our playbook
children:
- name: Installe php
import_playbook: install_php.yaml
- name: Install apache2
import_playbook: install_apache2.yaml
- name: installs wordpress and copies configuration files
import_playbook: install_wordpress.yaml
- name: start apache services, mariadb
hosts: group1
tasks:
- name: start services
service:
name: { { item } }
enabled: true
state: started
loop:
- apache2
- mariadb
- name: open iptables ports
import_playbook: allow_apache2.yaml
To import a task into a playbook, the import_tasks
keyword will be used.
_Guided exercise_
You are responsible for setting up the automation of the deployment of a WordPress CMS web server by creating playbooks. You can check out the various installation steps at the address.
To simplify the deployment, you will have only one machine on which to install WordPress
and MySql
. This machine will be part of a group called production.
You will also need to create three playbooks
:
One
playbook
will need to deploy all WordPress-related tasks.A second
playbook
for tasks relating to PHP.A third
playbook
for tasks related to mysql.
You will then create a file named master.yaml
that will deploy Worpdress
in its entirety.
The project will have a structure similar to this (you can create a new root folder named "wordpress"
):
├── group_vars
│ ├── all.yaml
│ └── production.yaml
├── host_vars
│ └── serverweb1.datascientest.com.yaml
├── install_wordpress.yaml
├── inventory.yaml
├── models
│ ├── nginx-vhost.j2
│ └── wp-config.php.j2
├── ansible.cfg
├── mysql.yaml
├── nginx.yaml
└── wordpress.yaml
Write the contents of the
inventory.yaml
file.
Now we write the content of the
all.yaml
file that will gather all our variables.
#wordpress
wp_version: latest
wp_install_dir: "/var/www/html"
#nginx
wp_webserver: nginx
wp_sitename: your-website.com
#mysql
wp_db_name: "datascientest"
wp_db_password: "datascientest"
wp_db_user: "datascientest"
wp_db_host: "localhost"
wp_db_charset: "utf8"
Copy this content into the
wordpress.yaml
file:
- name: WordPress
hosts: all
become: true
tasks:
- name: WordPress - Erasing of index.html in /var/www/html/
file:
path: /var/www/html/index.html
state: absent
when: wp_install_dir == "/var/www/html"
- name: WordPress - Erasing of index.nginx-debian in /var/www/html/
file:
path: /var/www/html/index.nginx-debian.html
state: absent
when: wp_install_dir == "/var/www/html"
- name: WordPress - Creation of the wordPress directory
file:
path: "{{ wp_install_dir }}/wordpress"
state: directory
mode: 0755
- name: WordPress - Extrating the archive in {{ wp_install_dir }}/wordpress
unarchive:
src: "https://wordpress.org/{{ wp_version }}.tar.gz"
dest: "{{ wp_install_dir }}"
remote_src: yes
- name: WordPress - Retrieval of random salts for wp-config.php
delegate_to: localhost
uri:
url: https://api.wordpress.org/secret-key/1.1/salt/
return_content: yes
become: False
register: "wp_salt_array"
- name: WordPress - Definition of facts wp_salt
set_fact:
wp_salt: "{{ wp_salt_array.content }}"
- name: WordPress - Copy wp-config.php files
template:
src: modeles/wp-config.php.j2
dest: "{{ wp_install_dir }}/wordpress/wp-config.php"
- name: WordPress - Change the owner of the wordpress directory
file:
path: "{{ wp_install_dir }}/wordpress"
owner: www-data
group: www-data
state: directory
recurse: yes
Copy this content into the
nginx.yaml
file:
- name: Nginx
hosts: all
become: true
tasks:
- name: Nginx - Find the location of PHP FPM socket
shell: dpkg -l | grep php-fpm | awk '{print $3}' | grep -o '[0-9]\.[0-9]' | head -n 1
register: "php_ver"
- name: Nginx - Installation of nginx
apt:
pkg: nginx
update_cache: yes
cache_valid_time: 86400
state: present
when: wp_webserver == "nginx"
- name: Nginx - Starting of the php{{ php_ver.stdout }} service
service:
name: "php{{ php_ver.stdout }}-fpm"
state: started
- name: Nginx - Starting the nginx service
service:
name: "nginx"
state: started
- name: Nginx - Copy configuration file of the virtual host
template:
src: "modeles/nginx-vhost.j2"
dest: "/etc/nginx/conf.d/wordpress.conf"
owner: root
group: root
mode: 0644
- name: Nginx - Restart
service:
name: "nginx"
state: restarted
Copy this content into the
install_wordpress.yaml
file:
- name: LEMP Installation
hosts: production
become: true
- name: Mysql Installation
import_playbook: mysql.yaml
- name: WordPress Installation
import_playbook: wordpress.yaml
- name: Nginx Installation
import_playbook: nginx.yaml
Copy this content into the
mysql.yaml
file:
- name: Mysql
hosts: all
become: true
vars:
tasks:
- name: MySql - Installation of mysql-server
apt:
pkg: mysql-server
update_cache: yes
cache_valid_time: 86400
state: present
when: ansible_distribution == 'Ubuntu'
- name: MySql - Installation of mariadb-server
apt:
pkg: mariadb-server
update_cache: yes
cache_valid_time: 86400
state: present
when: ansible_distribution == 'Debian'
- name: MySql - Installation of php
apt:
pkg:
[
"php",
"php-fpm",
"php-curl",
"php-mysql",
"php-gd",
"php-mbstring",
"php-xml",
"php-imagick",
"php-zip",
"php-xmlrpc"
]
update_cache: yes
cache_valid_time: 86400
state: present
- name: MySql - Starting of MariaDB
service:
name: mysql
state: started
- name: MySql - Installation of dependancies
apt:
pkg: ["php-mysql", "python3-pymysql"]
update_cache: yes
cache_valid_time: 86400
state: present
- name: MySql - Erasing of Apache if present
apt:
name: apache2
state: absent
- name: MySql - Creation of the database
mysql_db:
name: "{{ wp_db_name }}"
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: MySql - Creation of the user for the database
mysql_user:
name: "{{ wp_db_user }}"
password: "{{ wp_db_password }}"
host: localhost
priv: "{{ wp_db_name }}.*:ALL"
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
Copy this content into the
modeles/wp-config.php.j2
file:
/**
* The base configurations of the WordPress.
*
* This file has the following configurations: MySQL settings, Table Prefix,
* Secret Keys, and ABSPATH. You can find more information by visiting
* {@link http://codex.wordpress.org/Editing_wp-config.php Editing wp-config.php}
* Codex page. You can get the MySQL settings from your web host.
*
* This file is used by the wp-config.php creation script during the
* installation. You don't have to use the web site, you can just copy this file
* to "wp-config.php" and fill in the values.
*
* @package WordPress
*/
// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', '{{wp_db_name}}');
/** MySQL database username */
define('DB_USER', '{{wp_db_user}}');
/** MySQL database password */
define('DB_PASSWORD', '{{wp_db_password}}');
/** MySQL hostname */
define('DB_HOST', '{{wp_db_host}}');
/** Database Charset to use in creating database tables. */
define('DB_CHARSET', '{{wp_db_charset}}');
/** The Database Collate type. Don't change this if in doubt. */
define('DB_COLLATE', '');
/**#@+
* Authentication Unique Keys and Salts.
*
* Change these to different unique phrases!
* You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
* You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
*
* @since 2.6.0
*/
/**#@-*/
/**
* WordPress Database Table prefix.
*
* You can have multiple installations in one database if you give each a unique
* prefix. Only numbers, letters, and underscores please!
*/
$table_prefix = '{{wp_db_name}}_';
/**
* For developers: WordPress debugging mode.
*
* Change this to true to enable the display of notices during development.
* It is strongly recommended that plugin and theme developers use WP_DEBUG
* in their development environments.
*/
define('WP_DEBUG', 'false');
/* That's all, stop editing! Happy blogging. */
/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') )
define('ABSPATH', dirname(__FILE__) . '/');
/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');
Copy this content into the
modeles/nginx-vhost.j2
file:
server {
listen 80;
root {{ wp_install_dir }}/wordpress;
index index.php index.html index.htm;
server_name {{ wp_sitename }};
client_max_body_size 500M;
location / {
try_files $uri $uri/ /index.php?$args;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
log_not_found off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php{{ php_ver.stdout }}-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Write the contents of the
group_vars/production.yaml
.
Write the content in of the file
host_vars/serveurweb1.datascientest.en.yaml
.
Run your
playbook
.
We've seen how to use playbooks to automate the deployment of our tasks. But isn't there a more flexible way to deploy our configurations? We'll talk about roles in the next notebook and see how to improve our Ansible code writing. You can test your configuration on a browser with the public IP address of the server you just deployed
