Ansible from Scratch

1 - ansible command and configuration

Create ansible directory.

> mkdir ~/guisam_ansible_tuto && \
cd ~/guisam_ansible_tuto

Create ansible configuration file, ansible.cfg.

> cp /etc/ansible/ansible.cfg .
> grep -v "^#.*\|^$\|^\[" ansible.cfg
inventory      = ./hosts
remote_user = root

Create inventory file, hosts.

echo -e "duncan.guisam.xyz\nmalone.guisam.xyz\n" > hosts

Use ping module to test connections.

> ansible -m ping duncan.guisam.xyz
[WARNING]: Platform linux on host duncan.guisam.xyz is using the discovered Python interpreter at /usr/bin/python, but future installation of another Python interpreter could
change this. See https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
duncan.guisam.xyz | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}

Update ansible.cfg.

> grep -v "^#.*\|^$\|^\[" ansible.cfg
interpreter_python = auto_silent
inventory      = ./hosts
remote_user = root

Use ping module.

> ansible -m ping duncan.guisam.xyz
duncan.guisam.xyz | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
> ansible -m ping all
malone.guisam.xyz | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
duncan.guisam.xyz | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}

Use setup module to get facts.

> ansible -m setup duncan.guisam.xyz
duncan.guisam.xyz | SUCCESS => {
    "ansible_facts": {
[...]
> ansible -m setup all -a "filter=ansible_distribution*"
duncan.guisam.xyz | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Debian",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "10",
        "ansible_distribution_release": "buster",
        "ansible_distribution_version": "10",
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false
}
malone.guisam.xyz | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Debian",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "10",
        "ansible_distribution_release": "buster",
        "ansible_distribution_version": "10",
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false
}

Use shell module to execute commands.

> ansible -m shell all -a "apt update"
malone.guisam.xyz | CHANGED | rc=0 >>
Atteint :1 http://security.debian.org/debian-security buster/updates InRelease
Atteint :2 http://deb.debian.org/debian buster InRelease
Atteint :3 http://deb.debian.org/debian buster-updates InRelease
Atteint :4 http://deb.debian.org/debian buster-backports InRelease
Lecture des listes de paquets…
Construction de l'arbre des dépendances…
Lecture des informations d'état…
Tous les paquets sont à jour.
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
duncan.guisam.xyz | CHANGED | rc=0 >>
Atteint :1 http://security.debian.org/debian-security buster/updates InRelease
Atteint :2 http://deb.debian.org/debian buster InRelease
Atteint :3 http://deb.debian.org/debian buster-updates InRelease
Atteint :4 http://deb.debian.org/debian buster-backports InRelease
Lecture des listes de paquets…
Construction de l'arbre des dépendances…
Lecture des informations d'état…
Tous les paquets sont à jour.
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

2 - variables

Create variables directories, host_vars and group_vars.

> mkdir {host,group}_vars

Create common variables, group_vars/all.yml.

---
common_pkgs:
  - iftop
  - tcpdump
  - tree
  - vim
...

Create host variables, host_vars/duncan.guisam.xyz.yml.

---
hostname: duncan.guisam.xyz
...

Create host variables, host_vars/malone.guisam.xyz.yml.

---
hostname: malone.guisam.xyz
...

Working directory in tree-like format.

guisam_ansible_tuto
../guisam_ansible_tuto
├── ansible.cfg
├── group_vars
│   └── all.yml
├── hosts
└── host_vars
    ├── duncan.guisam.xyz.yml
    └── malone.guisam.xyz.yml

3 - playbooks

Create playbooks directory.

> mkdir playbooks

Create a playbook, playbooks/common.yml.

---
- name: common tasks
  hosts: all
  gather_facts: false

  tasks:
    - name: set hostname
      hostname:
        name: "{{ hostname }}"
    - name: install common pkgs
      apt:
        pkg: "{{ common_pkgs }}"
...

Usage

ansible-playbook command options.

> awk '/^\s{2}-[DC]/' <(ansible-playbook --help)
  -C, --check           don't make any changes; instead, try to predict some
  -D, --diff            when changing (small) files and templates, show the

Execute playbook in check mode.

> ansible-playbook playbooks/common.yml -D -C

PLAY [common tasks] **********************************************************************************************

TASK [set hostname] **********************************************************************************************
--- before
+++ after
@@ -1 +1 @@
-hostname = malone
+hostname = malone.guisam.xyz

changed: [malone.guisam.xyz]
--- before
+++ after
@@ -1 +1 @@
-hostname = duncan
+hostname = duncan.guisam.xyz

changed: [duncan.guisam.xyz]

TASK [install common pkgs] ***************************************************************************************
The following additional packages will be installed:
  libpcap0.8
The following NEW packages will be installed:
  iftop libpcap0.8 tcpdump tree
0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
changed: [duncan.guisam.xyz]
The following additional packages will be installed:
  libpcap0.8
The following NEW packages will be installed:
  iftop libpcap0.8 tcpdump tree
0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
changed: [malone.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
malone.guisam.xyz            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Execute playbook.

> ansible-playbook playbooks/common.yml -D

PLAY [common tasks] **********************************************************************************************

TASK [set hostname] **********************************************************************************************
--- before
+++ after
@@ -1 +1 @@
-hostname = duncan
+hostname = duncan.guisam.xyz

changed: [duncan.guisam.xyz]
--- before
+++ after
@@ -1 +1 @@
-hostname = malone
+hostname = malone.guisam.xyz

changed: [malone.guisam.xyz]

TASK [install common pkgs] ***************************************************************************************
The following additional packages will be installed:
  libpcap0.8
The following NEW packages will be installed:
  iftop libpcap0.8 tcpdump tree
0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
changed: [duncan.guisam.xyz]
The following additional packages will be installed:
  libpcap0.8
The following NEW packages will be installed:
  iftop libpcap0.8 tcpdump tree
0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
changed: [malone.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
malone.guisam.xyz            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
> ansible-playbook playbooks/common.yml -D

PLAY [common tasks] **********************************************************************************************

TASK [set hostname] **********************************************************************************************
ok: [duncan.guisam.xyz]
ok: [malone.guisam.xyz]

TASK [install common pkgs] ***************************************************************************************
ok: [duncan.guisam.xyz]
ok: [malone.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
malone.guisam.xyz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Working directory in tree-like format.

guisam_ansible_tuto
├── ansible.cfg
├── group_vars
│   └── all.yml
├── hosts
├── host_vars
│   ├── duncan.guisam.xyz.yml
│   └── malone.guisam.xyz.yml
└── playbooks
    └── common.yml

4 - roles

Update ansible.cfg.

-#roles_path    = /etc/ansible/roles
+roles_path    = ./roles:/etc/ansible/roles

Create roles directory.

> mkdir -p roles/common/tasks

Create roles/common/tasks/main.yml.

---
- name: set hostname
  hostname:
    name: "{{ hostname }}"
- name: install common pkgs
  apt:
    pkg: "{{ common_pkgs }}"
...

Update playbooks/common.yml.

---
- name: common tasks
  hosts: all
  gather_facts: false
  roles:
    - common
...

Check playbook execution.

> ansible-playbook playbooks/common.yml -D

PLAY [common tasks] **********************************************************************************************

TASK [common : set hostname] *************************************************************************************
ok: [malone.guisam.xyz]
ok: [duncan.guisam.xyz]

TASK [install common pkgs] ***************************************************************************************
ok: [malone.guisam.xyz]
ok: [duncan.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
malone.guisam.xyz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Working directory in tree-like format.

guisam_ansible_tuto
├── ansible.cfg
├── group_vars
│   └── all.yml
├── hosts
├── host_vars
│   ├── duncan.guisam.xyz.yml
│   └── malone.guisam.xyz.yml
├── playbooks
│   └── common.yml
└── roles
    └── common
        └── tasks
            └── main.yml

5 - galaxy roles

Create galaxy roles directory.

> mkdir -p galaxy/roles

Create install_requirements.sh.

#!/bin/bash

error()
{
        echo >&2 "[ERROR]: ${*}"
        exit 1
}

typeset -r ROLES_PATH="galaxy/roles"

ansible-galaxy role install -p ${ROLES_PATH} -r requirements.yml || error "Failed to install roles"

# EOF

Add role in requirements.yml.

---
roles:

# apache
- src: https://github.com/geerlingguy/ansible-role-apache.git
  version: master
  name: geerlingguy.apache

# mysql
- src: https://github.com/geerlingguy/ansible-role-mysql.git
  version: master
  name: geerlingguy.mysql
...

Install galaxy roles.

> bash -x install_requirements.sh
+ typeset -r ROLES_PATH=galaxy/roles
+ ansible-galaxy role install -p galaxy/roles -r requirements.yml
- extracting geerlingguy.apache to /home/gsamson/gsamson_docs/guisam_ansible_tuto/galaxy/roles/geerlingguy.apache
- geerlingguy.apache (master) was installed successfully
- extracting geerlingguy.mysql to /home/gsamson/gsamson_docs/guisam_ansible_tuto/galaxy/roles/geerlingguy.mysql
- geerlingguy.mysql (master) was installed successfully

Working directory in tree-like format over three levels.

guisam_ansible_tuto
├── ansible.cfg
├── galaxy
│   └── roles
│       ├── geerlingguy.apache
│       └── geerlingguy.mysql
├── group_vars
│   └── all.yml
├── hosts
├── host_vars
│   ├── duncan.guisam.xyz.yml
│   └── malone.guisam.xyz.yml
├── install_requirements.sh
├── playbooks
│   └── common.yml
├── requirements.yml
└── roles
    └── common
        └── tasks

6 - roles and galaxy roles

Update roles_path in ansible.cfg.

> grep "^roles_path" ansible.cfg                                                                                                                                    ⏎
roles_path    = ./roles:./galaxy/roles:/etc/ansible/roles

Create web group in inventory, hosts.

malone.guisam.xyz

[web]
duncan.guisam.xyz

Create apache role, roles/apache/tasks/main.yml.

---
- include_role:
    name: geerlingguy.apache
...

Create playbook, playbooks/install_apache.yml.

---
- name: install apache2
  hosts: web
  gather_facts: true
  roles:
    - apache
...

Execute playbook, playbooks/install_apache.yml.

> ansible-playbook playbooks/install_apache.yml -D

PLAY [install apache2] *******************************************************************************************

TASK [Gathering Facts] *******************************************************************************************
ok: [duncan.guisam.xyz]

TASK [include_role : geerlingguy.apache] *************************************************************************

TASK [geerlingguy.apache : Include OS-specific variables.] *******************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Include variables for Amazon Linux.] **************************************************
skipping: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Define apache_packages.] **************************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : include_tasks] ************************************************************************
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/galaxy/roles/geerlingguy.apache/tasks/setup-Debian.yml for duncan.guisam.xyz

TASK [geerlingguy.apache : Update apt cache.] ********************************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Ensure Apache is installed on Debian.] ************************************************
The following additional packages will be installed:
  apache2-bin apache2-data libapr1 libaprutil1 libaprutil1-dbd-sqlite3
  libaprutil1-ldap libbrotli1 libcurl4 libjansson4 liblua5.2-0 ssl-cert
Suggested packages:
  apache2-doc apache2-suexec-pristine | apache2-suexec-custom www-browser
  openssl-blacklist
The following NEW packages will be installed:
  apache2 apache2-bin apache2-data apache2-utils libapr1 libaprutil1
  libaprutil1-dbd-sqlite3 libaprutil1-ldap libbrotli1 libcurl4 libjansson4
  liblua5.2-0 ssl-cert
0 upgraded, 13 newly installed, 0 to remove and 0 not upgraded.
changed: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Get installed version of Apache.] *****************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Create apache_version variable.] ******************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Include Apache 2.2 variables.] ********************************************************
skipping: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Include Apache 2.4 variables.] ********************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Configure Apache.] ********************************************************************
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/galaxy/roles/geerlingguy.apache/tasks/configure-Debian.yml for duncan.guisam.xyz

TASK [geerlingguy.apache : Configure Apache.] ********************************************************************
ok: [duncan.guisam.xyz] => (item={'regexp': '^Listen ', 'line': 'Listen 80'})

TASK [geerlingguy.apache : Enable Apache mods.] ******************************************************************
--- before
+++ after
@@ -1,4 +1,4 @@
 {
     "path": "/etc/apache2/mods-enabled/rewrite.load",
-    "state": "absent"
+    "state": "link"
 }

changed: [duncan.guisam.xyz] => (item=rewrite.load)
--- before
+++ after
@@ -1,4 +1,4 @@
 {
     "path": "/etc/apache2/mods-enabled/ssl.load",
-    "state": "absent"
+    "state": "link"
 }

changed: [duncan.guisam.xyz] => (item=ssl.load)

TASK [geerlingguy.apache : Disable Apache mods.] *****************************************************************

TASK [geerlingguy.apache : Check whether certificates defined in vhosts exist.] **********************************

TASK [geerlingguy.apache : Add apache vhosts configuration.] *****************************************************
--- before
+++ after: /home/gsamson/.ansible/tmp/ansible-local-122853kfmsqxkt/tmppua023h7/vhosts.conf.j2
@@ -0,0 +1,15 @@
+DirectoryIndex index.php index.html
+
+
+<VirtualHost *:80>
+  ServerName local.dev
+  DocumentRoot "/var/www/html"
+
+  <Directory "/var/www/html">
+    AllowOverride All
+    Options -Indexes +FollowSymLinks
+    Require all granted
+  </Directory>
+</VirtualHost>
+
+

changed: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Add vhost symlink in sites-enabled.] **************************************************
--- before
+++ after
@@ -1,4 +1,4 @@
 {
     "path": "/etc/apache2/sites-enabled/vhosts.conf",
-    "state": "absent"
+    "state": "link"
 }

changed: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Remove default vhost in sites-enabled.] ***********************************************
skipping: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Ensure Apache has selected state and enabled on boot.] ********************************
ok: [duncan.guisam.xyz]

RUNNING HANDLER [geerlingguy.apache : restart apache] ************************************************************
changed: [duncan.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=16   changed=5    unreachable=0    failed=0    skipped=5    rescued=0    ignored=0

Create apache role vars directory, roles/apache/vars.

> mkdir roles/apache/vars

Update apache role vars to disable default vhost, roles/apache/vars/main.yml.

---
apache_remove_default_vhost: true
...

Execute playbook, playbooks/install_apache.yml.

ansible-playbook playbooks/install_apache.yml -D

PLAY [install apache2] *******************************************************************************************

[...]

TASK [geerlingguy.apache : Remove default vhost in sites-enabled.] ***********************************************
--- before
+++ after
@@ -1,4 +1,4 @@
 {
     "path": "/etc/apache2/sites-enabled/000-default.conf",
-    "state": "link"
+    "state": "absent"
 }

changed: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Ensure Apache has selected state and enabled on boot.] ********************************
ok: [duncan.guisam.xyz]

RUNNING HANDLER [geerlingguy.apache : restart apache] ************************************************************
changed: [duncan.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=17   changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0

Check webserver response.

> curl -I http://duncan.guisam.xyz
HTTP/1.1 200 OK
Date: Tue, 27 Oct 2020 10:15:49 GMT
Server: Apache/2.4.38 (Debian)
Last-Modified: Tue, 27 Oct 2020 09:57:47 GMT
ETag: "29cd-5b2a4143a698b"
Accept-Ranges: bytes
Content-Length: 10701
Vary: Accept-Encoding
Content-Type: text/html

Update apache role to mask version, roles/apache/tasks/main.yml.

---
- include_role:
    name: geerlingguy.apache

- name: update security configuration > ServerTokens
  lineinfile:
    path: /etc/apache2/conf-available/security.conf
    regexp: '^ServerTokens '
    line: ServerTokens Prod
  notify: restart apache
- name: update security configuration > ServerSignature
  lineinfile:
    path: /etc/apache2/conf-available/security.conf
    regexp: '^ServerSignature '
    line: ServerSignature Off
  notify: restart apache
...

Execute playbook, playbooks/install_apache.yml.

> ansible-playbook playbooks/install_apache.yml -D

PLAY [install apache2] *******************************************************************************************

[...]

TASK [geerlingguy.apache : Ensure Apache has selected state and enabled on boot.] ********************************
ok: [duncan.guisam.xyz]

TASK [apache : update security configuration > ServerTokens] *****************************************************
--- before: /etc/apache2/conf-available/security.conf (content)
+++ after: /etc/apache2/conf-available/security.conf (content)
@@ -22,7 +22,7 @@
 # Set to one of:  Full | OS | Minimal | Minor | Major | Prod
 # where Full conveys the most information, and Prod the least.
 #ServerTokens Minimal
-ServerTokens OS
+ServerTokens Prod
 #ServerTokens Full

 #

changed: [duncan.guisam.xyz]

TASK [apache : update security configuration > ServerSignature] **************************************************
--- before: /etc/apache2/conf-available/security.conf (content)
+++ after: /etc/apache2/conf-available/security.conf (content)
@@ -33,7 +33,7 @@
 # Set to "EMail" to also include a mailto: link to the ServerAdmin.
 # Set to one of:  On | Off | EMail
 #ServerSignature Off
-ServerSignature On
+ServerSignature Off

 #
 # Allow TRACE method

changed: [duncan.guisam.xyz]

RUNNING HANDLER [geerlingguy.apache : restart apache] ************************************************************
changed: [duncan.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=19   changed=4    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0

Create apache role templates directory, roles/apache/templates.

> mkdir roles/apache/templates

Create our own vhosts template, roles/apache/templates/guisam_vhosts.conf.j2.

{% for server_item in apache_vhosts %}
<VirtualHost *:80>
    ServerName {{ server_item.servername }}
    ServerAlias {{ server_item.serveralias }}
    DocumentRoot {{ server_item.documentroot }}

    <Directory {{ server_item.documentroot }}>
        Options -Indexes +FollowSymLinks +MultiViews
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/{{ server_item.servername }}-error.log
    CustomLog ${APACHE_LOG_DIR}/{{ server_item.servername }}-access.log combined

    RewriteEngine on
    RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
    RewriteRule .* – [F]

{% if server_item.httpredirect is defined and server_item.sslcertificatefile is defined and server_item.sslcertificatekeyfile is defined %}
    RewriteCond %{HTTPS} !=on
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
{% endif %}
</VirtualHost>
{% if server_item.sslcertificatefile is defined and server_item.sslcertificatekeyfile is defined %}
<VirtualHost *:443>
    ServerName {{ server_item.servername }}
    ServerAlias {{ server_item.serveralias }}
    DocumentRoot {{ server_item.documentroot }}

    <Directory {{ server_item.documentroot }}>
        Options -Indexes +FollowSymLinks +MultiViews
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/{{ server_item.servername }}-error.log
    CustomLog ${APACHE_LOG_DIR}/{{ server_item.servername }}-access.log combined

    RewriteEngine on
    RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
    RewriteRule .* – [F]

    SSLEngine on
    SSLCertificateFile "{{ server_item.sslcertificatefile }}"
    SSLCertificateKeyFile "{{ server_item.sslcertificatekeyfile }}"
</VirtualHost>
{% endif %}
{% endfor %}

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

Create our vhost variable, host_vars/duncan.guisam.xyz.yml.

---
# common
hostname: duncan.guisam.xyz

# apache
apache_vhosts:
  - servername: "duncan.guisam.xyz"
    serveralias: "www.duncan.guisam.xyz,test.duncan.guisam.xyz"
    documentroot: "/var/www/duncan.guisam.xyz"
...

Deploy.

ansible-playbook playbooks/install_apache.yml -D

PLAY [install apache2] *******************************************************************************************

[...]

TASK [geerlingguy.apache : Add apache vhosts configuration.] *****************************************************
--- before: /etc/apache2/sites-available/vhosts.conf
+++ after: /home/gsamson/.ansible/tmp/ansible-local-242080w_hdjvti/tmpowl9aeuj/guisam_vhosts.conf.j2
@@ -1,15 +1,21 @@
-DirectoryIndex index.php index.html
+<VirtualHost *:80>
+    ServerName duncan.guisam.xyz
+    ServerAlias www.duncan.guisam.xyz,test.duncan.guisam.xyz
+    DocumentRoot /var/www/duncan.guisam.xyz

+    <Directory /var/www/duncan.guisam.xyz>
+        Options -Indexes +FollowSymLinks +MultiViews
+        AllowOverride All
+        Require all granted
+    </Directory>

-<VirtualHost *:80>
-  ServerName local.dev
-  DocumentRoot "/var/www/html"
+    ErrorLog ${APACHE_LOG_DIR}/duncan.guisam.xyz-error.log
+    CustomLog ${APACHE_LOG_DIR}/duncan.guisam.xyz-access.log combined

-  <Directory "/var/www/html">
-    AllowOverride All
-    Options -Indexes +FollowSymLinks
-    Require all granted
-  </Directory>
+    RewriteEngine on
+    RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
+    RewriteRule .* – [F]
+
 </VirtualHost>

-
+# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

changed: [duncan.guisam.xyz]

[...]

RUNNING HANDLER [geerlingguy.apache : restart apache] ************************************************************
changed: [duncan.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=19   changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0

Update apache role to create documentroot and index.html, roles/apache/tasks/main.yml.

---
- include_role:
    name: geerlingguy.apache

- name: update security configuration > ServerTokens
  lineinfile:
    path: /etc/apache2/conf-available/security.conf
    regexp: '^ServerTokens '
    line: ServerTokens Prod
  notify: restart apache
- name: update security configuration > ServerSignature
  lineinfile:
    path: /etc/apache2/conf-available/security.conf
    regexp: '^ServerSignature '
    line: ServerSignature Off
  notify: restart apache
- name: create vhosts documentroot
  file:
    path: "{{ item.documentroot }}"
    mode: '0755'
    owner: www-data
    group: www-data
    state: directory
  loop: "{{ apache_vhosts }}"
  when: not ansible_check_mode
- name: create index.html pages
  template:
    src: ./templates/index.html.j2
    dest: "{{ item.documentroot }}/index.html"
    mode: '0644'
    owner: www-data
    group: www-data
  loop: "{{ apache_vhosts }}"
  when: not ansible_check_mode
...

Create apache role index.html template, roles/apache/templates/index.html.j2.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>The HTML5 Herald</title>
  <meta name="description" content="The HTML5 Herald">
  <meta name="author" content="SitePoint">

  <link rel="stylesheet" href="css/styles.css?v=1.0">

</head>

<body>
  Hello fucking bullshit world !
</body>
</html>

Deploy.

> ansible-playbook playbooks/install_apache.yml -D

PLAY [install apache2] *******************************************************************************************

[...]

TASK [apache : create vhosts documentroot] ***********************************************************************
--- before
+++ after
@@ -1,6 +1,6 @@
 {
-    "group": 0,
-    "owner": 0,
+    "group": 33,
+    "owner": 33,
     "path": "/var/www/duncan.guisam.xyz",
-    "state": "absent"
+    "state": "directory"
 }

changed: [duncan.guisam.xyz] => (item={'servername': 'duncan.guisam.xyz', 'serveralias': 'www.duncan.guisam.xyz,test.duncan.guisam.xyz', 'documentroot': '/var/www/duncan.guisam.xyz'})

TASK [apache : create index.html pages] **************************************************************************
--- before
+++ after: /home/gsamson/.ansible/tmp/ansible-local-351745uoaoa3f1/tmp0es6vxw9/index.html.j2
@@ -0,0 +1,18 @@
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+
+  <title>Hello world</title>
+  <meta name="description" content="The HTML5 Herald">
+  <meta name="author" content="SitePoint">
+
+  <link rel="stylesheet" href="css/styles.css?v=1.0">
+
+</head>
+
+<body>
+  Hello fucking bullshit world !
+</body>
+</html>

changed: [duncan.guisam.xyz] => (item={'servername': 'duncan.guisam.xyz', 'serveralias': 'www.duncan.guisam.xyz,test.duncan.guisam.xyz', 'documentroot': '/var/www/duncan.guisam.xyz'})

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=20   changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0

Working directory in tree-like format over four levels.

guisam_ansible_tuto
├── ansible.cfg
├── galaxy
│   └── roles
│       ├── geerlingguy.apache
│       │   ├── defaults
│       │   ├── handlers
│       │   ├── LICENSE
│       │   ├── meta
│       │   ├── molecule
│       │   ├── README.md
│       │   ├── tasks
│       │   ├── templates
│       │   └── vars
│       └── geerlingguy.mysql
│           ├── defaults
│           ├── handlers
│           ├── LICENSE
│           ├── meta
│           ├── molecule
│           ├── README.md
│           ├── tasks
│           ├── templates
│           └── vars
├── group_vars
│   └── all.yml
├── hosts
├── host_vars
│   ├── duncan.guisam.xyz.yml
│   └── malone.guisam.xyz.yml
├── install_requirements.sh
├── playbooks
│   ├── common.yml
│   └── install_apache.yml
├── requirements.yml
└── roles
    ├── apache
    │   ├── tasks
    │   │   └── main.yml
    │   ├── templates
    │   │   ├── guisam_vhosts.conf.j2
    │   │   └── index.html.j2
    │   └── vars
    │       └── main.yml
    └── common
        └── tasks
            └── main.yml

7 - roles a step beyond

Update apache role main task, roles/apache/tasks/main.yml.

---
- include_role:
    name: geerlingguy.apache

- name: update security configuration > ServerTokens
  lineinfile:
    path: /etc/apache2/conf-available/security.conf
    regexp: '^ServerTokens '
    line: ServerTokens Prod
  notify: restart apache
- name: update security configuration > ServerSignature
  lineinfile:
    path: /etc/apache2/conf-available/security.conf
    regexp: '^ServerSignature '
    line: ServerSignature Off
  notify: restart apache
- name: create vhosts documentroot
  file:
    path: "{{ item.documentroot }}"
    mode: '0755'
    owner: www-data
    group: www-data
    state: directory
  loop: "{{ apache_vhosts }}"
  when: not ansible_check_mode
- name: create index.html pages
  include: index.yml
  loop: "{{ apache_vhosts }}"
  loop_control:
    loop_var: apache_vhost
  when: not ansible_check_mode
...

Create apache role task, roles/apache/tasks/index.yml.

---
- name: create index.html pages
  template:
    src: ./templates/index.html.j2
    dest: "{{ apache_vhost.documentroot }}/index.html"
    mode: '0644'
    owner: www-data
    group: www-data
...

Update apache role template, roles/apache/templates/index.html.j2.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>{{ apache_vhost.servername }}</title>
  <meta name="description" content="{{ apache_vhost.servername }}">
  <meta name="author" content="SitePoint">

  <link rel="stylesheet" href="css/styles.css?v=1.0">

</head>

<body>
  <h1>{{ apache_vhost.servername }} vhost summary</h1>
  servername: {{ apache_vhost.servername }}<br>
  serveralias: {{ apache_vhost.serveralias }}<br>
  documentroot: {{ apache_vhost.documentroot }}<br>
</body>
</html>

Deploy.

> ansible-playbook playbooks/install_apache.yml -D

PLAY [install apache2] *******************************************************************************************

[...]

TASK [apache : create index.html pages] **************************************************************************
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/roles/apache/tasks/index.yml for duncan.guisam.xyz

TASK [apache : create index.html pages] **************************************************************************
--- before: /var/www/duncan.guisam.xyz/index.html
+++ after: /home/gsamson/.ansible/tmp/ansible-local-420650kb73cplm/tmp00g5yq73/index.html.j2
@@ -4,8 +4,8 @@
 <head>
   <meta charset="utf-8">

-  <title>Hello world</title>
-  <meta name="description" content="Hello world">
+  <title>duncan.guisam.xyz</title>
+  <meta name="description" content="duncan.guisam.xyz">
   <meta name="author" content="SitePoint">

   <link rel="stylesheet" href="css/styles.css?v=1.0">
@@ -13,6 +13,9 @@
 </head>

 <body>
-  Hello fucking bullshit world !
+  <h1>duncan.guisam.xyz vhost summary</h1>
+  servername: duncan.guisam.xyz<br>
+  serveralias: www.duncan.guisam.xyz,test.duncan.guisam.xyz<br>
+  documentroot: /var/www/duncan.guisam.xyz<br>
 </body>
 </html>

changed: [duncan.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=21   changed=1    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0

Then just add some vhost, host_vars/duncan.guisam.xyz.yml.

---
# common
hostname: duncan.guisam.xyz

# apache
apache_vhosts:
  - servername: "duncan.guisam.xyz"
    serveralias: "www.duncan.guisam.xyz,test.duncan.guisam.xyz"
    documentroot: "/var/www/duncan.guisam.xyz"
  - servername: "toto.guisam.xyz"
    serveralias: "www.toto.guisam.xyz,test.toto.guisam.xyz"
    documentroot: "/var/www/toto.guisam.xyz"
  - servername: "titi.guisam.xyz"
    serveralias: "www.titi.guisam.xyz,test.titi.guisam.xyz"
    documentroot: "/var/www/titi.guisam.xyz"
...

Deploy.

ansible-playbook playbooks/install_apache.yml -D

PLAY [install apache2] *******************************************************************************************

TASK [Gathering Facts] *******************************************************************************************
ok: [duncan.guisam.xyz]

TASK [include_role : geerlingguy.apache] *************************************************************************

TASK [geerlingguy.apache : Include OS-specific variables.] *******************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Include variables for Amazon Linux.] **************************************************
skipping: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Define apache_packages.] **************************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : include_tasks] ************************************************************************
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/galaxy/roles/geerlingguy.apache/tasks/setup-Debian.yml for duncan.guisam.xyz

TASK [geerlingguy.apache : Update apt cache.] ********************************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Ensure Apache is installed on Debian.] ************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Get installed version of Apache.] *****************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Create apache_version variable.] ******************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Include Apache 2.2 variables.] ********************************************************
skipping: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Include Apache 2.4 variables.] ********************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Configure Apache.] ********************************************************************
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/galaxy/roles/geerlingguy.apache/tasks/configure-Debian.yml for duncan.guisam.xyz

TASK [geerlingguy.apache : Configure Apache.] ********************************************************************
ok: [duncan.guisam.xyz] => (item={'regexp': '^Listen ', 'line': 'Listen 80'})

TASK [geerlingguy.apache : Enable Apache mods.] ******************************************************************
ok: [duncan.guisam.xyz] => (item=rewrite.load)
ok: [duncan.guisam.xyz] => (item=ssl.load)

TASK [geerlingguy.apache : Disable Apache mods.] *****************************************************************

TASK [geerlingguy.apache : Check whether certificates defined in vhosts exist.] **********************************

TASK [geerlingguy.apache : Add apache vhosts configuration.] *****************************************************
--- before: /etc/apache2/sites-available/vhosts.conf
+++ after: /home/gsamson/.ansible/tmp/ansible-local-443758d1i1twbf/tmp_442nj5a/guisam_vhosts.conf.j2
@@ -17,5 +17,43 @@
     RewriteRule .* – [F]

 </VirtualHost>
+<VirtualHost *:80>
+    ServerName toto.guisam.xyz
+    ServerAlias www.toto.guisam.xyz,test.toto.guisam.xyz
+    DocumentRoot /var/www/toto.guisam.xyz
+
+    <Directory /var/www/toto.guisam.xyz>
+        Options -Indexes +FollowSymLinks +MultiViews
+        AllowOverride All
+        Require all granted
+    </Directory>
+
+    ErrorLog ${APACHE_LOG_DIR}/toto.guisam.xyz-error.log
+    CustomLog ${APACHE_LOG_DIR}/toto.guisam.xyz-access.log combined
+
+    RewriteEngine on
+    RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
+    RewriteRule .* – [F]
+
+</VirtualHost>
+<VirtualHost *:80>
+    ServerName titi.guisam.xyz
+    ServerAlias www.titi.guisam.xyz,test.titi.guisam.xyz
+    DocumentRoot /var/www/titi.guisam.xyz
+
+    <Directory /var/www/titi.guisam.xyz>
+        Options -Indexes +FollowSymLinks +MultiViews
+        AllowOverride All
+        Require all granted
+    </Directory>
+
+    ErrorLog ${APACHE_LOG_DIR}/titi.guisam.xyz-error.log
+    CustomLog ${APACHE_LOG_DIR}/titi.guisam.xyz-access.log combined
+
+    RewriteEngine on
+    RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
+    RewriteRule .* – [F]
+
+</VirtualHost>

 # vim: syntax=apache ts=4 sw=4 sts=4 sr noet

changed: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Add vhost symlink in sites-enabled.] **************************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Remove default vhost in sites-enabled.] ***********************************************
ok: [duncan.guisam.xyz]

TASK [geerlingguy.apache : Ensure Apache has selected state and enabled on boot.] ********************************
ok: [duncan.guisam.xyz]

TASK [apache : update security configuration > ServerTokens] *****************************************************
ok: [duncan.guisam.xyz]

TASK [apache : update security configuration > ServerSignature] **************************************************
ok: [duncan.guisam.xyz]

TASK [apache : create vhosts documentroot] ***********************************************************************
ok: [duncan.guisam.xyz] => (item={'servername': 'duncan.guisam.xyz', 'serveralias': 'www.duncan.guisam.xyz,test.duncan.guisam.xyz', 'documentroot': '/var/www/duncan.guisam.xyz'})
--- before
+++ after
@@ -1,6 +1,6 @@
 {
-    "group": 0,
-    "owner": 0,
+    "group": 33,
+    "owner": 33,
     "path": "/var/www/toto.guisam.xyz",
-    "state": "absent"
+    "state": "directory"
 }

changed: [duncan.guisam.xyz] => (item={'servername': 'toto.guisam.xyz', 'serveralias': 'www.toto.guisam.xyz,test.toto.guisam.xyz', 'documentroot': '/var/www/toto.guisam.xyz'})
--- before
+++ after
@@ -1,6 +1,6 @@
 {
-    "group": 0,
-    "owner": 0,
+    "group": 33,
+    "owner": 33,
     "path": "/var/www/titi.guisam.xyz",
-    "state": "absent"
+    "state": "directory"
 }

changed: [duncan.guisam.xyz] => (item={'servername': 'titi.guisam.xyz', 'serveralias': 'www.titi.guisam.xyz,test.titi.guisam.xyz', 'documentroot': '/var/www/titi.guisam.xyz'})

TASK [apache : create index.html pages] **************************************************************************
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/roles/apache/tasks/index.yml for duncan.guisam.xyz
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/roles/apache/tasks/index.yml for duncan.guisam.xyz
included: /home/gsamson/gsamson_docs/guisam_ansible_tuto/roles/apache/tasks/index.yml for duncan.guisam.xyz

TASK [apache : create index.html pages] **************************************************************************
ok: [duncan.guisam.xyz]

TASK [apache : create index.html pages] **************************************************************************
--- before
+++ after: /home/gsamson/.ansible/tmp/ansible-local-443758d1i1twbf/tmpbge9809z/index.html.j2
@@ -0,0 +1,21 @@
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+
+  <title>toto.guisam.xyz</title>
+  <meta name="description" content="toto.guisam.xyz">
+  <meta name="author" content="SitePoint">
+
+  <link rel="stylesheet" href="css/styles.css?v=1.0">
+
+</head>
+
+<body>
+  <h1>toto.guisam.xyz vhost summary</h1>
+  servername: toto.guisam.xyz<br>
+  serveralias: www.toto.guisam.xyz,test.toto.guisam.xyz<br>
+  documentroot: /var/www/toto.guisam.xyz<br>
+</body>
+</html>

changed: [duncan.guisam.xyz]

TASK [apache : create index.html pages] **************************************************************************
--- before
+++ after: /home/gsamson/.ansible/tmp/ansible-local-443758d1i1twbf/tmpm5yu97l7/index.html.j2
@@ -0,0 +1,21 @@
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+
+  <title>titi.guisam.xyz</title>
+  <meta name="description" content="titi.guisam.xyz">
+  <meta name="author" content="SitePoint">
+
+  <link rel="stylesheet" href="css/styles.css?v=1.0">
+
+</head>
+
+<body>
+  <h1>titi.guisam.xyz vhost summary</h1>
+  servername: titi.guisam.xyz<br>
+  serveralias: www.titi.guisam.xyz,test.titi.guisam.xyz<br>
+  documentroot: /var/www/titi.guisam.xyz<br>
+</body>
+</html>

changed: [duncan.guisam.xyz]

RUNNING HANDLER [geerlingguy.apache : restart apache] ************************************************************
changed: [duncan.guisam.xyz]

PLAY RECAP *******************************************************************************************************
duncan.guisam.xyz            : ok=26   changed=5    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0