这是用户在 2025-6-10 17:40 为 https://learn.datascientest.com/lesson/1434/4181 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
COURSE

Ansible DevOps - Playbook

DIFFICULTY
Normal
APPROXIMATE TIME
1h30
RELATED MACHINE
webshellStopped
Ubuntu Server 20.04 LTS
SSD Volume Type 64- bit x86
Stopped


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 groups web server and database 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 the group_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 the group_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 named all.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 the host_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 to RedHat:

- 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 add web and bd 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 the always tags with the web tag, and the never tag with the bd 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.

  To learn more about tags, here is the documentation: https://docs.ansible.com/ansible/latest/user_guide/playbooks_tags.html


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 our jinja2 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.

Show / Hide solution

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.

Show / Hide solution

Write the content in of the file host_vars/serveurweb1.datascientest.en.yaml.

Show / Hide solution

Run your playbook.

Show / Hide solution

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

Lesson done

Lesson finished?