Ansible, c'est bien pour mettre à jour tous ses petits serveurs tranquillou en buvant un café.
Mais c'est aussi bien pour installer toujours la même application sur tous les serveurs, ou pour réaliser toujours la même configuration sans la recopier encore... et encore... et encore... et...
Ce document est un état des lieux de ma config ansible.
J'ai opté pour un dossier dans le répertoire personnel de root. Ce sont des opération sensibles que seul root peux lancer.
# tree ansible-config/
ansible-config/
├── ansible.cfg
├── group_vars
│ └── all.yml
├── host_vars
│ ├── srv1.yml
│ ├── srv2.yml
(...)
│ ├── pc1.yml
│ ├── pc2.yml
(...)
├── inventory.yml
├── post_install.yml
├── roles
│ ├── apt_config
│ │ ├── tasks
│ │ │ └── main.yml
│ │ └── templates
│ │ └── sources.list.j2
│ ├── bash_config
│ │ └── tasks
│ │ └── main.yml
│ ├── debian_upgrade
│ │ └── tasks
│ │ └── main.yml
│ ├── etckeeper
│ │ └── tasks
│ │ └── main.yml
│ ├── firmware
│ │ └── tasks
│ │ └── main.yml
│ ├── network_check
│ │ └── tasks
│ │ └── main.yml
│ ├── network_security
│ │ ├── handlers
│ │ │ └── main.yml
│ │ ├── tasks
│ │ │ └── main.yml
│ │ └── templates
│ │ ├── jail.local.j2
│ │ └── nftables.conf.j2
│ ├── node_exporter
│ │ └── tasks
│ │ └── main.yml
│ ├── promtail
│ │ ├── defaults
│ │ │ └── main.yml
│ │ ├── handlers
│ │ │ └── main.yml
│ │ ├── tasks
│ │ │ └── main.yml
│ │ └── templates
│ │ ├── promtail.service.j2
│ │ └── promtail.yml.j2
│ ├── shutdown_machine
│ │ └── tasks
│ │ └── main.yml
│ ├── vm_optimization
│ │ ├── handlers
│ │ │ └── main.yml
│ │ └── tasks
│ │ └── main.yml
│ └── wake_machine
│ └── tasks
│ └── main.yml
├── servers_upgrade.yml
├── setup_apt.yml
├── setup_bash.yml
├── setup_etckeeper.yml
├── setup_firmware.yml
├── setup_monitoring.yml
├── setup_security.yml
├── setup_vm_optimization.yml
└── wake_all.yml
35 directories, 49 files
Je définis de manière explicite mes dossiers et mon interpréteur python qu'il faudra un jour penser à faire évoluer si nécessaire.
[defaults]
inventory = ./inventory.yml
roles_path = ./roles
interpreter_python = /usr/bin/python3
remote_user = root
Fichier contenant l'ensemble des valeurs par défaut de mes variables ansible.
trusted_ip: "192.168.x.y"
monitoring_ip: "192.168.x.z"
## GLOBAL DEFAULT VALUES
# hypervisor does not autoreboot
hypervisor: False
# is always on, do not wakeup, do not shutdown
always_on: false
# wakeup method : wakeonlan, webhook, ssh
wakeup_type: "none"
# - for wakeonlan method
mac_address: ""
# - for webhook method
webhook_url: ""
## POST INSTALL VARS DEFAULT VALUES
# sources.list
debian_version: trixie
manage_sources_list: true
# vm optimisation switch
manage_vm_optimization: true
## SECURITY VARS DEFAULT VALUES
# security managed by ansible
manage_security: True
# fail2ban default settings
fail2ban_defaults:
backend: "systemd"
banaction: "nftables-multiport"
fail2ban_ignoreip: "127.0.0.1/8 ::1 {{ trusted_ip }}"
# nftables default settings
enable_forwarding: false
base_allowed_services:
- name: "sshd"
port: 22
proto: "tcp"
f2b_jail: "sshd"
f2b_conf:
journalmatch: "_SYSTEMD_UNIT=ssh.service + _COMM=sshd"
- name: 'node_exporter'
port: 9100
proto: 'tcp'
source: "{{ monitoring_ip }}"
Dans ces fichiers, j'écris les valeurs spécifiques de mes variables pour une machine spécifique. Le nom du fichier doit être celui de la machine dans le fichier inventory.
hypervisor: True
specific_services:
- { name: 'http', port: 80, proto: 'tcp' }
- { name: 'https', port: 443, proto: 'tcp', f2b_jail: 'nginx-http-auth' }
wakeup_type: wol
mac_address: "xx:xx:xx:xx:xx:xx"
Fichier listant toutes les machines. J'ai fait le choix de la simplicité : il ne contient que les noms des machines, totes les configurations sont déportées dans d'autres fichiers. Ainsi, ajouter une nouvelle machine avec les valeurs par défaut ne nécessite que 3 actions : l'ajouter à mon /etc/hosts, la configurer pour communiquer avec ssh, écrire son nom dans l'inventory. (TODO: automatiser ces tâches pour n'avoir qu'à l'écrire dans l'inventory, et en profiter pour ajouter les tâches d'install)
all:
hosts:
srv1:
srv2:
(...)
pc1:
pc2:
(...)
Une fois une nouvelle machin ajoutée dans l'inventory, ce playbook réalise toute la configuration de base de ma machine.
---
- name: Setup Post-Installation VM
hosts: all
become: true
roles:
# Système de base & Dépôts
- apt_config
- firmware
- vm_optimization # Installe le noyau cloud, l'agent et vire les génériques
# Mise à jour globale
- debian_upgrade
# Configurations
- bash_config
- etckeeper
- promtail
- node_exporter
- network_security
Il commence par réveiller les machines éteintes en mémorisant leur états avant réveil, réalise les mises à jour, puis remet toutes les machines dans leur état initial (allumée ou éteinte).
---
# PREMIER PLAY : Préparation et Réveil
- name: "Préparation : Réveil des machines"
hosts: all
gather_facts: no
connection: local
become: no
tasks:
- name: "Vérification de l'état des machines"
wait_for:
host: "{{ inventory_hostname }}"
port: "{{ check_port | default(22) }}"
state: started
timeout: 2
register: connection_status
ignore_errors: yes
failed_when: false
changed_when: false
- name: "Mémoriser l'état initial"
set_fact:
was_offline_at_start: "{{ connection_status.state is undefined or connection_status.state != 'started' }}"
- name: "Affichage de l'état"
debug:
msg: "{{ inventory_hostname }} : {{ 'OFFLINE - réveil nécessaire' if was_offline_at_start else 'ONLINE' }}"
- name: "Réveil des machines hors ligne"
include_role:
name: wake_machine
when: was_offline_at_start | bool
- name: "Attente du démarrage (120s max)"
wait_for:
host: "{{ inventory_hostname }}"
port: "{{ check_port | default(22) }}"
state: started
timeout: 120
delay: 10
when: was_offline_at_start | bool
register: wake_check
ignore_errors: yes
failed_when: false
changed_when: false
- name: "Résultat du réveil"
debug:
msg: "{{ inventory_hostname }} : {{ 'Démarrage réussi ✓' if (wake_check.state is defined and wake_check.state == 'started') else 'Échec du démarrage ✗' }}"
when: was_offline_at_start | bool
- name: "Marquer pour extinction si réveillée avec succès"
set_fact:
needs_shutdown: true
when:
- was_offline_at_start | bool
- not (always_on | default(false))
- wake_check.state is defined
- wake_check.state == 'started'
- name: "Construire le groupe d'extinction"
add_host:
name: "{{ item }}"
groups: machines_to_shutdown
loop: "{{ ansible_play_hosts }}"
when: hostvars[item].needs_shutdown | default(false)
run_once: true
delegate_to: localhost
- name: "═══ Résumé ═══"
debug:
msg:
- "Machines actives : {{ ansible_play_hosts | length - (groups['machines_to_shutdown'] | default([]) | length) }}"
- "Machines réveillées : {{ groups['machines_to_shutdown'] | default([]) | length }}"
- "{% if groups['machines_to_shutdown'] | default([]) | length > 0 %}Seront éteintes : {{ groups['machines_to_shutdown'] | default([]) | join(', ') }}{% endif %}"
run_once: true
# DEUXIÈME PLAY : upgrade
- name: "Upgrade all debian"
hosts: all
become: yes
gather_facts: yes
roles:
- debian_upgrade
# TROISIÈME PLAY : Extinction automatique
- name: "Nettoyage : Extinction des machines réveillées"
hosts: machines_to_shutdown
gather_facts: no
become: yes
tasks:
- name: "Extinction de {{ inventory_hostname }}"
command: /sbin/shutdown -h now
async: 1
poll: 0
ignore_unreachable: yes
ignore_errors: yes
failed_when: false
changed_when: false
- name: "✓ Extinction"
debug:
msg: "Signal d'extinction envoyé à {{ inventory_hostname }}"
delegate_to: localhost
Modification des dépôts debian.
---
- name: Configuration des dépôts APT
hosts: all
become: true
roles:
- apt_config
Réglage du bashrc avec les paramètres et alias par défaut.
---
- name: "Configuration de Bash sur les serveurs"
hosts: all
roles:
- bash_config
Installation et paramétrage de etckeeper.
---
- name: Déploiement de etckeeper
hosts: all
become: yes
roles:
- etckeeper
Installation des firmwares.
---
- name: Installation des firmwares
hosts: all
become: true
roles:
- firmware
Mise en place de la stratégie de sécurité.
La variable manage_security (True par défaut) dira quelle machine sera prise en charge via ansible.
- hosts: all
become: yes
roles:
- role: network_security
when: manage_security | default(false)
---
- name: Optimisation noyau et agents VM
hosts: all
become: true
roles:
- vm_optimization
Réglage de la configuration de apt (dépôts...).
---
- name: Mise à jour du sources.list
ansible.builtin.template:
src: sources.list.j2
dest: /etc/apt/sources.list
when: manage_sources_list | bool
register: apt_sources
- name: Update cache
ansible.builtin.apt:
update_cache: yes
when: (manage_sources_list | bool and apt_sources.changed)
# Géré par Ansible - {{ inventory_hostname }}
deb http://deb.debian.org/debian/ {{ debian_version }} main contrib non-free non-free-firmware
deb-src http://deb.debian.org/debian/ {{ debian_version }} main contrib non-free non-free-firmware
deb http://security.debian.org/debian-security {{ debian_version }}-security main contrib non-free non-free-firmware
deb-src http://security.debian.org/debian-security {{ debian_version }}-security main contrib non-free non-free-firmware
deb http://deb.debian.org/debian/ {{ debian_version }}-updates main contrib non-free non-free-firmware
deb-src http://deb.debian.org/debian/ {{ debian_version }}-updates main contrib non-free non-free-firmware
Paramétrage du bashrc avec mes alias par défaut, la complétion, ...
---
- name: "Activer la complétion Bash dans /etc/bash.bashrc"
replace:
path: /etc/bash.bashrc
regexp: '^#\s*(if ! shopt -oq posix; then\n#\s* if \[ -f /usr/share/bash-completion/bash_completion \]; then\n#\s* \. /usr/share/bash-completion/bash_completion\n#\s* elif \[ -f /etc/bash_completion \]; then\n#\s* \. /etc/bash_completion\n#\s* fi\n#\s*fi)'
replace: |
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
become: yes
- name: "Ajouter les alias et la couleur dans /etc/bash.bashrc"
blockinfile:
path: /etc/bash.bashrc
marker: "# {mark} ANSIBLE MANAGED BLOCK: CUSTOM ALIASES"
block: |
export LS_OPTIONS='--color=auto'
eval "$(dircolors)"
alias ls='ls $LS_OPTIONS'
alias ll='ls $LS_OPTIONS -lh'
alias la='ls $LS_OPTIONS -a'
alias lla='ls $LS_OPTIONS -lah'
alias lt='ls $LS_OPTIONS -lrth'
become: yes
Mise à jour des machines debian.
# Tasks for debian_upgrade
- name: Update apt repo and cache on all Debian/Ubuntu boxes
apt: update_cache=yes force_apt_get=yes cache_valid_time=3600
- name: Upgrade all packages on servers
apt: upgrade=dist
- name: Autoremove unused deps
apt: autoremove=yes
- name: Clean cache
apt: autoclean=yes
- name: Check if a reboot is needed on all servers
register: reboot_required_file
stat: path=/var/run/reboot-required get_checksum=no
- name: Reboot the box if kernel updated
reboot:
msg: "Reboot initiated by Ansible for kernel updates"
connect_timeout: 5
reboot_timeout: 300
pre_reboot_delay: 0
post_reboot_delay: 30
test_command: uptime
when:
- hypervisor == False
- reboot_required_file.stat.exists
Installation et paramétrage de etckeeper.
---
- name: Installer etckeeper
apt:
name: etckeeper
state: present
update_cache: yes
- name: Vérifier si /etc/.git existe
stat:
path: /etc/.git
register: etc_git
- name: Initialiser etckeeper (si nécessaire)
command: etckeeper init
when: not etc_git.stat.exists
# Cette commande ne change rien si c'est déjà fait,
# mais on la protège avec "when" par sécurité.
- name: Premier commit de configuration
command: etckeeper commit "Initialisation via Ansible"
when: not etc_git.stat.exists
Installation des firmawares.
---
- name: Installation des firmwares
ansible.builtin.apt:
name:
- firmware-linux-nonfree
- firmware-misc-nonfree
state: present
when: manage_sources_list | bool
Teste si les machines sont allumées ou éteintes. (Nom du rôle à revoir...)
---
- name: "Vérification de disponibilité pour {{ inventory_hostname }}"
wait_for:
host: "{{ inventory_hostname }}"
port: "{{ check_port | default(22) }}"
state: started
timeout: "{{ check_timeout | default(2) }}"
register: connection_status
ignore_errors: yes
no_log: true # Masque l'affichage mais garde le statut failed/success
Gestion de la sécurité réseau pour les machines gérées par ansible.
- name: restart nftables
service:
name: nftables
state: restarted
- name: restart fail2ban
service:
name: fail2ban
state: restarted
---
- name: "Configuration de la sécurité réseau"
when: manage_security | default(true) | bool
block:
- name: Installer nftables et fail2ban
apt:
name: ['nftables', 'fail2ban']
state: present
- name: Generer la configuration nftables
template:
src: nftables.conf.j2
dest: /etc/nftables.conf
mode: '0750'
notify: restart nftables
- name: Generer la configuration fail2ban
template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
mode: '0644'
notify: restart fail2ban
- name: Activer les services
service:
name: "{{ item }}"
state: started
enabled: yes
loop: ['nftables', 'fail2ban']
[DEFAULT]
ignoreip = {{ fail2ban_ignoreip | default('127.0.0.1/8 ::1') }}
backend = {{ fail2ban_defaults.backend }}
banaction = {{ fail2ban_defaults.banaction }}
{% for srv in (base_allowed_services + (specific_services | default([]))) %}
{% if srv.f2b_jail is defined %}
[{{ srv.f2b_jail }}]
enabled = true
port = {{ srv.port }}
{% if srv.f2b_conf is defined and srv.f2b_conf.journalmatch is defined %}
journalmatch = {{ srv.f2b_conf.journalmatch }}
{% endif %}
{% endif %}
{% endfor %}
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
# Autoriser ICMP et ICMPv6 (essentiel pour le ping et le réseau IPv6)
ip protocol icmp accept comment "Allow ICMP"
ip6 nexthdr icmpv6 accept comment "Allow ICMPv6"
{% for srv in (base_allowed_services + (specific_services | default([]))) %}
{% if srv.source is defined %}ip saddr {{ srv.source }} {% endif %}{{ srv.proto }} dport {{ srv.port }} counter accept comment "Allow {{ srv.name }}"
{% endfor %}
}
chain forward {
type filter hook forward priority 0;
policy {{ 'accept' if enable_forwarding | default(false) else 'drop' }};
# Si on est en mode strict (drop), on peut quand même autoriser
# explicitement le trafic déjà établi
ct state established,related accept
}
chain output {
type filter hook output priority 0; policy accept;
}
}
Installation de prometheus pour le monitoring des ressources.
---
- name: Installer Node Exporter (Métriques)
apt:
name: prometheus-node-exporter
state: present
Installation et paramétrage de promtail pour le monitoring des journaux.
promtail_version: "2.9.4"
loki_url: "http://{{ monitoring_ip }}:3100/loki/api/v1/push"
---
- name: Restart Promtail
ansible.builtin.service:
name: promtail
state: restarted
daemon_reload: yes # Indispensable si le fichier .service a été modifié
---
# 1. Préparation locale sur la machine de contrôle (Ansible Controller)
- name: Télécharger Promtail localement
become: false
delegate_to: localhost
run_once: true
ansible.builtin.get_url:
url: "https://github.com/grafana/loki/releases/download/v{{ promtail_version }}/promtail-linux-amd64.zip"
dest: "/tmp/promtail-{{ promtail_version }}.zip"
mode: '0644'
- name: Décompresser l'archive localement
become: false
delegate_to: localhost
run_once: true
ansible.builtin.unarchive:
src: "/tmp/promtail-{{ promtail_version }}.zip"
dest: "/tmp/"
creates: "/tmp/promtail-linux-amd64"
# 2. Configuration système sur les serveurs cibles
- name: Créer l'utilisateur système pour Promtail
ansible.builtin.user:
name: promtail
system: true
shell: /usr/sbin/nologin
groups: adm # Permet de lire /var/log/
append: true
- name: Vérifier si Promtail est déjà installé
ansible.builtin.stat:
path: /usr/local/bin/promtail
register: promtail_bin
# 3. Installation du binaire (Transfert du fichier déjà décompressé)
- name: Installer le binaire Promtail
when: not promtail_bin.stat.exists
block:
- name: Transférer le binaire décompressé vers les serveurs
ansible.builtin.copy:
src: "/tmp/promtail-linux-amd64"
dest: "/usr/local/bin/promtail-linux-amd64"
owner: root
group: root
mode: '0755'
- name: Créer le lien symbolique vers /usr/local/bin/promtail
ansible.builtin.file:
src: /usr/local/bin/promtail-linux-amd64
dest: /usr/local/bin/promtail
state: link
force: yes
# 4. Configuration et Service
- name: Créer le répertoire de configuration
ansible.builtin.file:
path: /etc/promtail
state: directory
owner: root
group: root
mode: '0755'
- name: Déployer la configuration Promtail
ansible.builtin.template:
src: promtail.yml.j2
dest: /etc/promtail/config.yml
owner: root
group: root
mode: '0644'
notify: Restart Promtail
- name: Déployer le service Systemd
ansible.builtin.template:
src: promtail.service.j2
dest: /etc/systemd/system/promtail.service
owner: root
group: root
mode: '0644'
notify: Restart Promtail
- name: Démarrer et activer Promtail
ansible.builtin.service:
name: promtail
state: started
enabled: true
[Unit]
Description=Promtail service
After=network.target
[Service]
Type=simple
User=promtail
ExecStart=/usr/local/bin/promtail -config.file=/etc/promtail/config.yml
Restart=on-failure
[Install]
WantedBy=multi-user.target
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: {{ loki_url }}
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
host: {{ inventory_hostname }}
__path__: /var/log/*log
Arrêt machine.
---
- name: "Extinction de la machine {{ inventory_hostname }}"
command: /sbin/shutdown -h now
async: 1 # Exécution asynchrone
poll: 0 # Ne pas attendre le retour
ignore_unreachable: yes # Ignorer les erreurs de connexion
ignore_errors: yes # Ignorer toutes les erreurs
Optimisations pour les VM (noyau plus léger...).
---
- name: Reboot system
ansible.builtin.reboot:
msg: "Redémarrage provoqué par Ansible après optimisation du noyau"
reboot_timeout: 600
---
- name: Optimisation VM
when:
- ansible_facts['virtualization_role'] == "guest"
- manage_vm_optimization | bool
block:
- name: Installation noyau Cloud et QEMU Agent
ansible.builtin.apt:
name:
- linux-image-cloud-amd64
- qemu-guest-agent
state: present
register: kernel_installed
- name: Redémarrage pour basculer sur le noyau Cloud
ansible.builtin.reboot:
msg: "Basculement vers le noyau Cloud"
when: kernel_installed is changed
- name: Activation QEMU Agent
ansible.builtin.systemd:
name: qemu-guest-agent
state: started
enabled: yes
- name: Identification des paquets noyaux génériques (méta-paquets et images)
# On cherche linux-image-amd64 (le méta-paquet) et les versions spécifiques non-cloud
ansible.builtin.shell: "dpkg -l | grep -E 'linux-image-([0-9].*|amd64)' | grep -v 'cloud' | awk '{print $2}'"
register: old_kernels
changed_when: false
- name: Suppression propre des noyaux génériques
ansible.builtin.apt:
name: "{{ old_kernels.stdout_lines }}"
state: absent
purge: yes
when: old_kernels.stdout_lines | length > 0
notify: Reboot system
---
- name: "Envoi du signal de réveil"
shell: |
{% if wakeup_type == 'wol' %}
wakeonlan {{ mac_address }}
{% elif wakeup_type == 'webhook' %}
curl -s -X POST {{ webhook_url }}
{% elif wakeup_type == 'ssh' %}
ssh pve qm start {{ vmid }}
{% endif %}
async: 45
poll: 0
when:
- wakeup_type is defined
- not (always_on | default(false))
- was_offline_at_start | default(false) # Utiliser la variable du playbook