Zabbix

Rôle

Plateforme de supervision centrale de l'infrastructure Indio : collecte de métriques (agents fleet-wide, vCenter, SNMP FortiGate, JMX), alerting et source de données pour les tableaux de bord Grafana. Zabbix Server tourne en cluster HA natif à 2 nœuds (mécanisme propre à Zabbix, indépendant de toute VIP), adossé à un backend PostgreSQL hautement disponible. Services et SLA sont modélisés pour suivre la disponibilité des briques critiques de l'infra (HAProxy, PostgreSQL, Vault, AD, VMware) avec une cible mensuelle de 99.9%.

Architecture

  • 3 VM provisionnées par Terraform sur le segment seg-supervision (10.100.2.224/28) :
    • INDSUPV001 (10.100.2.226) et INDSUPV002 (10.100.2.227) — cluster Zabbix Server HA natif (bascule gérée nativement par le process zabbix_server via la table interne ha_node, pas de keepalived côté serveur — voir contraste avec PostgreSQL ci-dessous).
    • INDSUPV004 (10.100.2.229) — frontend web (zabbix-web sur httpd/php-fpm). Installé hors IaC : ce dépôt ne fait que provisionner la VM (Terraform) et poser le TLS (rôle zabbix_tls) ; aucun rôle Ansible ne déploie le frontend lui-même ni sa propre connexion à la base (zabbix.conf.php, configuré manuellement).
  • Backend base de données : PostgreSQL, via la VIP 10.100.2.195 (cluster repmgr/keepalived, voir PostgreSQL), base zabbix. Les 2 nœuds Zabbix Server pointent sur cette VIP plutôt que sur un nœud PostgreSQL nommément désigné, ce qui rend le backend transparent à un failover PostgreSQL.
  • Contraste HA important : contrairement à PostgreSQL (IP flottante keepalived unique), le cluster Zabbix Server HA natif n'expose aucune IP virtuelle. Les clients (agents fleet-wide) doivent connaître explicitement les deux IP réelles des nœuds — c'est cette différence de mécanisme qui est à l'origine de l'incident du 2026-07-16 (voir ci-dessous).
  • Frontend exposé en HTTPS (supervision.infra.indio) via un certificat Vault PKI (pki_int/roles/infra-indio), redirection HTTP→HTTPS forcée. Certificats déployés en fichiers statiques (pas d'intégration Vault dynamique dans ce rôle), même schéma que GLPI/ELK.
  • zabbix-java-gateway local (port 10052) pour le polling JMX (Kafka notamment, partiellement fonctionnel — voir Points d'attention).
  • Collecteurs VMware natifs activés (StartVMwareCollectors=2 dans zabbix_server.conf), certificat CA vCenter (VMCA auto-signé) déployé dans le trust store des nœuds serveur.
  • Aucun objet Zabbix n'est versionné en IaC : templates (VMware, FortiGate, PostgreSQL, HAProxy, Vault, Elasticsearch), hosts, macros, groupes d'utilisateurs, comptes de service, Services et SLA sont tous créés via l'API/UI Zabbix — seule la configuration serveur/agent (zabbix_server.conf, zabbix_agentd.conf) est versionnée dans ce dépôt et dans /root/baseline. Un rebuild complet depuis Terraform+Ansible seuls ne restaurerait donc aucun de ces objets (voir Procédure manuelle).
  • Agents supervisés : déployés fleet-wide par le dépôt baseline-config (rôle zabbix_agent), avec des variantes zabbix_agent2_postgresql (nœuds PostgreSQL) et zabbix_agent2_mysql (GLPI/OCS/Guacamole, qui utilisent chacun un MariaDB local) pour exposer les clés de métriques natives (pgsql.*, mysql.*) que l'agent classique ne connaît pas.

Structure du dépôt

Fichier/Dossier Contenu
main.tf 3 VM (for_each), clone du template, IP via NetBox
variables.tf vsphere_network = "seg-supervision", nodes = [INDSUPV001, INDSUPV002, INDSUPV004], vm_cpu = 2, vm_ram = 4096
versions.tf Providers hashicorp/vsphere (>= 2.6.0), e-breuninger/netbox (>= 5.0.0, < 6.0.0)
ansible/site.yml 2 plays : zabbix_server (001/002) puis zabbix_tls (004)
ansible/inventory.ini Groupes zabbix_server/zabbix_web, ha_node_name/node_address par hôte
ansible/group_vars/zabbix_server.yml DBHost = VIP PostgreSQL, identifiants DB vaultés
ansible/group_vars/zabbix_web.yml zabbix_fqdn = supervision.infra.indio
ansible/roles/zabbix_server/ Paquets serveur + Java Gateway, certificat vCenter (trust store), zabbix_server.conf, SELinux, firewalld 10050/10051
ansible/roles/zabbix_tls/ mod_ssl, certificat Vault PKI, vhost SSL, redirection HTTP→HTTPS, vérification post-déploiement

Topologie HA

flowchart TB
    subgraph SEGSUPV["seg-supervision — 10.100.2.224/28"]
        S1["INDSUPV001<br/>10.100.2.226<br/>zabbix-server (HA)"]
        S2["INDSUPV002<br/>10.100.2.227<br/>zabbix-server (HA)"]
        WEB["INDSUPV004<br/>10.100.2.229<br/>frontend zabbix-web<br/>(install hors IaC)"]
    end
    PGVIP(("VIP PostgreSQL<br/>10.100.2.195"))

    S1 -. "table ha_node — 1 actif / 1 standby" .-> S2
    S1 --> PGVIP
    S2 --> PGVIP
    WEB --> PGVIP
    FLEET["Toute la flotte<br/>agents zabbix_agent / agent2"] -- "Server= / ServerActive=<br/>10.100.2.226,10.100.2.227 (CSV)" --> S1
    FLEET --> S2

Incident du 2026-07-16 — bascule HA spontanée

sequenceDiagram
    participant S1 as INDSUPV001 (actif)
    participant S2 as INDSUPV002 (standby)
    participant DB as ha_node (PostgreSQL)
    participant A as Agents (toute la flotte)

    Note over S1,S2: Fonctionnement normal
    S1->>DB: heartbeat (status=actif)
    S2->>DB: heartbeat (status=standby)
    A->>S1: checks actifs/passifs (Server= ne listait que .226)

    Note over S1: Bascule spontanée (cause non identifiée)
    S2->>DB: devient status=actif
    Note over S2: INDSUPV002 devient le nœud actif

    S2->>A: tentative de connexion depuis .227
    Note over A: .227 absent de Server= -> agent ferme la connexion<br/>« Assuming that agent dropped connection because of access permissions »
    Note over A,S2: Toute la flotte remonte en rouge (message trompeur, pas un vrai problème de permissions)

    Note over A: Fix : zabbix_server_ip = "10.100.2.226,10.100.2.227" (CSV)<br/>appliqué en urgence par SSH direct, puis versionné dans /root/baseline

Services et SLA (parent/enfant)

flowchart TD
    SLA["SLA « Core Infra 99.9% »<br/>mensuel, tag sla:core"]
    SLA --> HAP["HAProxy (repo.infra.infra)<br/>algorithm=2"]
    SLA --> PG["PostgreSQL HA (cluster INDDATA)<br/>algorithm=2"]
    SLA --> VLT["Vault / PKI (INDSERV016-018)<br/>algorithm=2"]
    SLA --> AD["Active Directory (infra.indio)<br/>feuille — INDIDEN008 seul"]
    SLA --> VMW["vCenter / Infra VMware<br/>feuille — host vcenter"]

    HAP --> HAP1["INDSERV002"]
    HAP --> HAP2["INDSERV003"]
    HAP --> HAP3["INDSERV004"]
    PG --> PG1["INDDATA001"]
    PG --> PG2["INDDATA002"]
    VLT --> V1["INDSERV016"]
    VLT --> V2["INDSERV017"]
    VLT --> V3["INDSERV018"]

algorithm=2 signifie « problème seulement si TOUS les enfants sont en problème » — tolère la perte d'un nœud (ou deux sur trois pour HAProxy) sans faire chuter le SLA du service parent, tant qu'au moins un nœud reste up.

Provisioning Terraform

  • /root/zabbix/main.tf clone 3 VM (for_each sur nodes) depuis tmpl-rocky96-hardened, IP assignées par NetBox.
  • lifecycle.ignore_changes sur annotation, clone[0].template_uuid, clone[0].customize, disk[0].io_share_count (pattern standard).
  • Variables (variables.tf) : vsphere_network = "seg-supervision", vm_gateway = "10.100.2.238", vm_netmask = 28, nodes = [INDSUPV001, INDSUPV002, INDSUPV004], vm_cpu = 2, vm_ram = 4096.
  • Providers : hashicorp/vsphere (>= 2.6.0), e-breuninger/netbox (>= 5.0.0, < 6.0.0).
  • Identifiants vCenter/NetBox via terraform.tfvars (non versionné) ; modèle dans terraform.tfvars.example.

Configuration Ansible

Playbook /root/zabbix/ansible/site.yml, 2 plays :

  1. zabbix_server (groupe zabbix_server = INDSUPV001/002) : installe zabbix-server-pgsql + zabbix-java-gateway, déploie le certificat CA vCenter (vcenter-ca.crt, VMCA auto-signé) dans le trust store système (update-ca-trust extract en handler), déploie zabbix_server.conf depuis le template, active le booléen SELinux zabbix_can_network, ouvre les ports firewalld 10050/tcp (agent) et 10051/tcp (server/HA), démarre zabbix-server et zabbix-java-gateway.
  2. zabbix_tls (groupe zabbix_web = INDSUPV004) : installe mod_ssl, crée /etc/httpd/certs (root:apache, 0750), déploie certificat/clé/CA Vault PKI, un vhost SSL dédié (ssl.conf.j2, remplace le ssl.conf par défaut du RPM mod_ssl — les alias /zabbix livrés par zabbix-apache-conf sont définis hors de tout VirtualHost et donc hérités automatiquement), une redirection HTTP→HTTPS (zabbix-redirect.conf.j2, RewriteRule plutôt qu'un simple Redirect pour être évaluée avant la fusion des alias hérités), ouvre le port 443, valide la configuration (apachectl configtest) puis vérifie via des requêtes HTTP/HTTPS réelles (uri, retries) que le frontend répond correctement après redémarrage.

zabbix_server.conf.j2 — champs notables au-delà de DBHost/DBName/DBUser/DBPassword (base zabbix sur la VIP) : StartVMwareCollectors=2 (active le polling natif vCenter), JavaGateway=127.0.0.1/JavaGatewayPort=10052/StartJavaPollers=5 (JMX local), HANodeName/NodeAddress (propres à chaque nœud, injectés depuis ha_node_name/node_address de l'inventaire — c'est ce qui active le mode HA natif au lieu du mode standalone implicite), StatsAllowedIP=127.0.0.1, EnableGlobalScripts=0.

group_vars/zabbix_server.yml : zabbix_db_host = VIP PostgreSQL (10.100.2.195), zabbix_db_password chiffré Ansible Vault. group_vars/zabbix_web.yml : uniquement zabbix_fqdn = "supervision.infra.indio" (aucun secret — la connexion DB du frontend lui-même n'est pas gérée par ce rôle, voir Procédure manuelle).

Procédure manuelle

Le cluster Zabbix concentre la plus grande part de configuration non versionnée de toute l'infra — à documenter précisément puisqu'un rebuild Terraform+Ansible seul ne suffit pas à retrouver un système fonctionnel.

1. Frontend applicatif (INDSUPV004) — installation manuelle, hors playbook :

  1. Installer zabbix-web, httpd, php-fpm et les paquets associés (paquets stock Zabbix, pas gérés par ce dépôt).
  2. Configurer zabbix.conf.php avec les identifiants de connexion à la base zabbix sur la VIP PostgreSQL 10.100.2.195 — geste manuel, indépendant du rôle zabbix_tls qui ne pose que la couche TLS/httpd par-dessus.
  3. Seulement ensuite, appliquer ansible-playbook site.yml (play zabbix_tls) pour le certificat et la redirection.

2. Bootstrap du cluster HA natif (si l'on repart d'un nœud déjà en service standalone) : Zabbix refuse de démarrer un 2ᵉ nœud HA tant qu'un nœud existant tourne encore en standalone implicite (HANodeName vide) — erreur cannot change mode to HA while standalone node is active. Il faut déployer HANodeName/NodeAddress aux deux nœuds simultanément (jamais un seul à la fois) puis redémarrer les deux. Le tout premier redémarrage du 2ᵉ nœud échoue quasi systématiquement (le 1ᵉʳ nœud n'a pas fini sa propre transition) ; le Restart=on-failure de l'unit systemd livrée retente automatiquement en quelques secondes — ne pas s'inquiéter d'un premier échec si le service est active (running) juste après.

3. Objets Zabbix (templates, hosts, macros) — 100% via l'API https://supervision.infra.indio/zabbix/api_jsonrpc.php, aucune trace en IaC :

  1. VMware : host vcenter rattaché au template racine VMware (templateid 10173 — pas VMware FQDN, qui n'a ni interface ni macros et ne remonte rien), macros {$VMWARE.URL}=https://vcenter.infra.indio/sdk, {$VMWARE.USERNAME}, {$VMWARE.PASSWORD} (macro secrète, type 1). Le certificat VMCA auto-signé doit être en confiance côté serveur (déjà couvert par le rôle Ansible) — sans cela le collector échoue en boucle (unable to get local issuer certificate).
  2. FortiGate : host fortigate-edge, interface SNMPv2 vers 10.15.91.14:161 (IP SNAT FortiGate), template FortiGate by SNMP (10604). Nécessite au préalable l'activation de SNMP côté FortiOS lui-même (config system snmp sysinfo, communauté dédiée, allowaccess snmp sur l'interface uplink) — configuration CLI FortiOS, hors de tout Terraform/Ansible de cette infra (pas de provider Fortios dans ce projet), identifiants gérés hors dépôt.
  3. PostgreSQL : le template stock « PostgreSQL by Zabbix agent » (10357) définit ses items avec la signature [Host, Port, User, Password, Database] (5 paramètres), incompatible avec le plugin réellement installé (zabbix-agent2-plugin-postgresql 8.0.0-beta2), qui attend [ConnString, User, Password] (3 paramètres, ConnString = host:port). Correctif appliqué via item.update sur les 16 items type=0 du template. Piège additionnel : la macro {$PG.HOST} par défaut vaut localhost, qui déclenche une connexion socket Unix côté plugin Go (échoue faute de mapping peer) — toujours la surcharger explicitement à 127.0.0.1 (force une vraie connexion TCP). Rôle Ansible dédié dans /root/baseline (zabbix_agent2_postgresql) bascule INDDATA001/002 de l'agent classique vers zabbix-agent2 (seul agent avec le plugin PostgreSQL).
  4. HAProxy : même famille de piège que PostgreSQL — la macro par défaut du template stock ({$HAPROXY.STATS.HOST}=localhost) pointe vers le mauvais hôte côté items HTTP exécutés par le serveur ; à surcharger avec l'hôte réel.
  5. Avant de faire confiance à un template stock non modifié (PostgreSQL/HAProxy), toujours tester une clé en live (zabbix_get) plutôt que de supposer la signature/macro par défaut correcte — particulièrement sur une version beta de Zabbix (8.0.0-beta2 à ce jour).

4. Services, SLA et alerting — également 100% API, créés le 2026-07-17 :

  1. Schéma de tags : chaque trigger de disponibilité pertinent reçoit un tag service:<slug> (fusionné, jamais en écrasement du tableau tags existant).
  2. 5 Services au sommet (voir schéma ci-dessus), tous tagués sla:core : HAProxy/PostgreSQL/Vault en parents algorithm=2, AD et vCenter en feuilles simples.
  3. SLA « Core Infra 99.9% » (slo=99.9, période mensuelle, service_tags scopé sur sla:core uniquement — pas les enfants, pour éviter le bruit dans le rapport).
  4. Alerting : media type Email rebranché sur le relais Postfix interne (INDSERV009:25, sans authentification), utilisateur Admin avec media email actif 24/7 toutes sévérités, action de type Service (eventsource=4) filtrée sur le tag sla=core. Le courriel reste livré localement sur INDSERV009 (/var/mail/root) — pas de relais vers une boîte aux lettres externe réelle à ce jour.
  5. Gap connu, non couvert : FreeIPA (aucun host Zabbix) et INDIDEN009 (2ᵉ DC AD, absent comme host Zabbix) ne font partie d'aucun Service/SLA.

5. Correctif d'urgence fleet-wide (2026-07-16) : au moment de l'incident, le correctif zabbix_server_ip a d'abord été appliqué en SSH direct (sed + restart de l'agent) sur toute la flotte en parallèle, pour ne pas attendre un plein re-run Ansible du dépôt /root/baseline (qui réapplique aussi d'autres rôles plus lourds) — puis committé proprement dans group_vars/fleet.yml pour que tout futur provisioning fleet-wide parte déjà de la bonne valeur.

Procédure de déploiement

  1. terraform apply (dans /root/zabbix) : clone les 3 VM et attribue leurs IP via NetBox.
  2. Installer et configurer manuellement le frontend (zabbix-web/httpd/php-fpm + zabbix.conf.php) sur INDSUPV004 — préalable non automatisé (voir Procédure manuelle).
  3. ansible-playbook site.yml (dans /root/zabbix/ansible) : configure le cluster Zabbix Server HA sur INDSUPV001/002 (déployer HANodeName/NodeAddress aux deux nœuds dans la même exécution, jamais un seul à la fois), puis le TLS du frontend sur INDSUPV004.
  4. Recréer manuellement, via l'API Zabbix, tous les objets applicatifs : templates VMware/FortiGate/PostgreSQL/HAProxy/Vault/Elasticsearch, hosts, macros, groupes d'utilisateurs, comptes de service, Services/SLA, alerting (voir Procédure manuelle).
  5. S'assurer que /root/baseline (group_vars/fleet.yml, zabbix_server_ip) liste bien les deux nœuds en CSV avant de déployer/rejouer les agents fleet-wide.

Contrôle de santé / Vérification

  • SELECT name, status, lastaccess FROM ha_node; (psql, via la VIP PostgreSQL) : identifie le nœud réellement actif (status=3) et le standby (status=0) — seule source de vérité fiable.
  • getsebool zabbix_can_network sur les 2 nœuds serveur : doit renvoyer on (persistant).
  • Sur un agent quelconque : grep -E 'Server=|ServerActive=' /etc/zabbix/zabbix_agentd.conf doit lister les deux IP 10.100.2.226,10.100.2.227.
  • curl -sk https://supervision.infra.indio/zabbix/ → 200 (ou 302 vers la page de login) ; curl -sI http://<IP INDSUPV004>/ → 301 vers HTTPS.
  • Vérification applicative : host.get/item.get via l'API pour confirmer que les items VMware/FortiGate/PostgreSQL remontent des valeurs récentes (lastclock proche de l'heure courante) plutôt qu'une erreur (ZBX_NOTSUPPORTED, No Such Instance, read timeout).
  • sla.getsli (API) : vérifie que le SLA « Core Infra 99.9% » calcule effectivement un SLI (pas d'erreur, historique cohérent).

Points d'attention

HA natif sans VIP : Server=/ServerActive= doit toujours lister les DEUX nœuds

Contrairement à PostgreSQL (VIP keepalived unique), le cluster Zabbix Server HA natif n'expose pas d'IP flottante. Les hôtes supervisés (variable zabbix_server_ip du dépôt /root/baseline) doivent donc lister les deux IP (10.100.2.226,10.100.2.227) dans Server=/ServerActive=, sous peine de refuser les checks passifs si le nœud actif bascule. Une bascule HA spontanée du 2026-07-16 (cause non identifiée) a fait passer toute la flotte en rouge le temps du correctif — symptôme caractéristique : message trompeur « Assuming that agent dropped connection because of access permissions » (ce n'est pas un problème de permissions, l'IP source du nouveau nœud actif n'est simplement pas dans Server=). Réflexe de diagnostic : si « tout est rouge en même temps », vérifier ha_node avant toute autre piste (agent, DFW, firewalld).

  • SELinux zabbix_can_network : le process zabbix_server tourne dans le domaine confiné zabbix_t, avec ce booléen off par défaut — quand il est off, zabbix_server ne peut faire aucune connexion réseau sortante, y compris vers son propre Java Gateway local (127.0.0.1:10052), bloquant silencieusement VMware ET JMX. Piège de diagnostic : invisible dans ausearch -m avc (même avec dontaudit off) et dans les logs du Java Gateway lui-même (le blocage a lieu avant que la requête n'atteigne le gateway) — le symptôme est un Permission denied générique et immédiat, à ne pas confondre avec un problème de port applicatif. setsebool -P zabbix_can_network on sur les 2 nœuds HA résout le blocage.
  • JMX Kafka non résolu : une fois zabbix_can_network activé, l'erreur passe de Permission denied à read timeout — la connectivité progresse mais bute sur le second port RMI aléatoire qu'utilise JMX après la connexion initiale (non ouvert côté DFW SOC). Décision explicite : non traité, jugé non critique — ne pas relancer cette investigation sans demande explicite. Le reste du monitoring Kafka (agent classique, port 10050) fonctionne normalement ; seul le template « Apache Kafka by JMX » reste incomplet. Point également à faire figurer dans le registre de dette technique global (voir Points de vigilance).
  • Aucun objet Zabbix versionné en IaC : templates, hosts, macros, groupes, comptes de service, Services/SLA, alerting — tout vit uniquement dans la base zabbix, créé/modifié via API ou UI. Un rebuild complet depuis Terraform+Ansible ne restaure que la plateforme serveur nue ; toute la configuration applicative (voir Procédure manuelle) doit être reconstruite à la main ou via un script externe qui n'existe pas encore.
  • Frontend hors IaC : le rôle zabbix_tls suppose que zabbix-web/httpd/php-fpm sont déjà installés, fonctionnels et connectés à la base (zabbix.conf.php configuré manuellement) — ce dépôt ne les installe pas. Un redéploiement complet de cette VM nécessite de réinstaller et reconnecter le frontend manuellement avant de rejouer zabbix_tls.
  • Groupe de sécurité NSX déjà prêt pour la HA native : grp-zabbix-server (/root/nsx/groups.tf) référence déjà les deux IP (10.100.2.226 et .227) — un failover HA ne nécessite donc aucun changement DFW. Le commentaire du code Terraform à cet endroit (« INDSUPV002 est passif/standby, pas de VIP HA à ce jour ») est resté rédigé pour l'ancien état (avant l'activation du HA natif) alors que la définition du groupe elle-même était déjà correcte — écart purement documentaire dans le code, sans impact fonctionnel.
  • Compte Admin par défaut toujours actif : le mot de passe d'usine du compte Admin n'a jamais été changé sur cette installation Zabbix — même famille de constat que le compte par défaut de Guacamole, à corriger. Également à faire figurer dans le registre de dette technique global (voir Points de vigilance).
  • Secrets : mot de passe de connexion base de données (zabbix_db_password) chiffré via Ansible Vault (group_vars/zabbix_server.yml) ; certificat/clé TLS du frontend déployés depuis roles/zabbix_tls/files/tls/ ; macros secrètes Zabbix ({$VMWARE.PASSWORD} etc.) et token API svc-grafana gérés uniquement côté base/API, jamais en IaC — voir gestion des secrets.