--- - name: Assurer le repertoire des trousseaux apt ansible.builtin.file: path: /etc/apt/keyrings state: directory owner: root group: root mode: "0755" # Recuperee UNE FOIS. Sans garde, chaque deploiement recontactait le serveur du # fournisseur : cinq cles x quatorze hotes = soixante-dix allers-retours externes pour # des cles deja installees, et autant d'occasions qu'un tiers lent fasse tomber le # deploiement. Arbitrage rendu le 2026-08-09 : une plateforme souveraine ne depend pas # de six serveurs etrangers pour redeployer ce qu'elle possede deja. # # CONSEQUENCE ASSUMEE : une rotation de cle amont n'est plus recuperee toute seule. Elle # ne passe pas inapercue pour autant — `apt` refuse alors le depot, bruyamment. Pour # forcer le rafraichissement : supprimer le fichier et rejouer le role. # UN FICHIER VIDE EXISTE (mesure du 2026-09-10). # # Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503, # un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache # est donc sautee a tous les passages suivants, et la machine garde une ressource morte # pour toujours, en silence. # # CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une # epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide : # # E: Le depot http://packages.smallstep.com/... n'est pas signe. # # Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332 # octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il # se fait passer pour un succes. # # On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? », # ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours. - name: Cette ressource est-elle deja recuperee ? (Telecharger la cle de signature Gr) ansible.builtin.stat: path: "{{ serveur_loki_depot_cle_fichier }}" register: telecharger_la_cle_de_signature_grafana_present # ... ET IL FAUT LE RETIRER, PAS SEULEMENT LE REDEMANDER (mesure du 2026-09-10). # # Laisser la garde autoriser la tache ne suffit PAS. `get_url` sur une destination qui # existe emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend # `ok`, et le fichier corrompu reste en place. Mesure : la tache s'executait bien # (`ok: [infra-mail-01]`), et la cle faisait toujours 0 octet au passage suivant. Un # `force: yes` manuel l'a reparee en une seconde. # # On efface donc la ruine avant de reconstruire. `state: absent` ne mord que sur un # fichier vide : une cle valide n'est jamais retiree, et la doctrine « on ne retelecharge # pas ce qu'on possede deja » reste intacte. - name: Retirer une ressource VIDE avant de la redemander ansible.builtin.file: path: "{{ serveur_loki_depot_cle_fichier }}" state: absent when: - telecharger_la_cle_de_signature_grafana_present.stat.exists - (telecharger_la_cle_de_signature_grafana_present.stat.size | default(0)) == 0 - name: Telecharger la cle de signature Grafana when: not (telecharger_la_cle_de_signature_grafana_present.stat.exists and telecharger_la_cle_de_signature_grafana_present.stat.size | default(0) > 0) # `force` PARCE QU'UNE REPRISE CONDITIONNELLE NE REPREND RIEN (2026-09-12). # # Sans lui, `get_url` envoie un `If-Modified-Since` des que le fichier existe. Le cache # repond `304 Not Modified`, la tache reussit sans rien ecrire, et la boucle `until` # — qui exige une taille non nulle — retente cinq fois contre un serveur qui repondra # toujours 304 : # # HTTP Error 304: Not Modified size: 0 attempts: 5 # # C'est la boucle de reprise qui se bat contre elle-meme. Le `when:` ci-dessus garantit # deja qu'on ne retelecharge pas une cle valide ; `force` ne concerne donc que le cas # ou l'on a DECIDE d'aller chercher. ansible.builtin.get_url: force: true url: "{{ serveur_loki_depot_cle_url }}" dest: "{{ serveur_loki_depot_cle_fichier }}" owner: root group: root mode: "0644" # LA CLE PASSE PAR LE CACHE, ELLE AUSSI (mesure du 2026-09-10). # # `get_url` ignore la configuration d'apt : le mandataire pose dans # `/etc/apt/apt.conf.d/` ne vaut que pour apt. Les cles de signature sortaient donc # TOUJOURS en direct, meme apres que tous les depots soient passes par le cache — un # trou reste ouvert derriere une porte qu'on croyait fermee. # # Et ce n'est pas theorique : depuis `collab-01`, la route directe vers # `www.collaboraoffice.com` EXPIRE, quand le cache l'atteint sans peine. # # en direct grafana 200 collabora TIMEOUT # via le cache collabora 200 # # Le meme geste corrige donc les deux : plus rien ne sort, et la machine qui n'avait # pas de route en trouve une. # # DEGRADE, JAMAIS DEVINE : sans cache d'amorcage declare, la valeur est VIDE et # `get_url` sort en direct comme avant. On n'invente pas un mandataire. environment: http_proxy: >- {{ ('http://' ~ artefacts_amorcage) if (artefacts_amorcage | default('') | string | length > 0) else '' }} https_proxy: "" register: serveur_loki_cle_depot retries: 5 delay: 6 until: serveur_loki_cle_depot is succeeded and (serveur_loki_cle_depot.size | default(0)) > 0 # `apt_repository` AJOUTE, IL NE REMPLACE PAS (mesure du 2026-09-10). # # Passer une source de `https://` a `http://` y ecrit une SECONDE ligne au lieu de # corriger la premiere. apt interroge alors les deux — et l'ancienne continue de sortir # en direct sur Internet, ce que le passage au cache visait justement a supprimer. Le # symptome le disait, sur les quatorze machines : # # W: La cible Packages est specifiee plusieurs fois dans grafana.list:1 et :2 # # On retire donc explicitement la forme precedente. `state: absent` ne mord que si la # ligne existe : sur une machine neuve, cette tache ne fait rien. - name: Mesurer la cle de signature Grafana qui vient d'etre posee ansible.builtin.stat: path: "{{ serveur_loki_depot_cle_fichier }}" register: cle_grafana_posee - name: Exiger une cle NON VIDE avant de s'en servir # UNE REUSSITE N'EST PAS UN CONTENU (mesure du 2026-09-11). Sur une reconstruction a # froid, `get_url` a rendu 0 octet sans erreur sur UNE machine des quatorze : le cache # a servi un 200 au corps vide. La garde « retirer une ressource VIDE » ne mord qu'au # passage SUIVANT, quand le fichier existe deja — au premier, elle n'a rien a retirer. # # Le defaut ne se lisait pas ici. Il se lisait deux cents lignes plus loin, dans un # `apt` qui accusait la SIGNATURE : « Missing key ..., which is needed to verify # signature ». Un message qui envoie chercher une cle revoquee chez le fournisseur, # alors que le fichier local fait zero octet. ansible.builtin.assert: that: - (cle_grafana_posee.stat.size | default(0)) > 0 fail_msg: >- La cle de signature Grafana ({{ serveur_loki_depot_cle_fichier }}) fait 0 octet. Le depot a repondu sans corps — cache vide, ou amont injoignable. apt echouera plus loin en accusant la signature : c'est ICI qu'il faut regarder. - name: Retirer la forme HTTPS du depot (elle contournait le cache) ansible.builtin.apt_repository: repo: "{{ serveur_loki_depot_source | replace('http://', 'https://') }}" filename: grafana state: absent when: serveur_loki_depot_schema == 'http' - name: Ajouter le depot apt Grafana ansible.builtin.apt_repository: repo: "{{ serveur_loki_depot_source }}" filename: grafana state: present # LE CACHE DU CONTROLEUR D'ABORD, LE DEPOT DISTANT POUR LE RESTE (2026-09-03). # # Les depots tiers sont en HTTPS, et `client_artefacts` pose # `Acquire::https::Proxy "DIRECT"` — ils CONTOURNENT donc le cache du site et sortent sur # Internet a chaque construction de VM. `make cacher-paquets` les tire une fois, versions # epinglees et empreintes verifiees ; ce role les depose depuis ce cache. - name: Poser les paquets tiers depuis le cache du controleur ansible.builtin.include_role: name: paquets_tiers vars: paquets_tiers_noms: "{{ serveur_loki_paquets }}" - name: Installer Loki ansible.builtin.apt: # CE QUE LE CACHE N'A PAS FOURNI, ET RIEN D'AUTRE. Les dependances Debian passent # par le cache du site en HTTP ; seuls les paquets tiers non caches exigent Internet. # Reinstaller ce que le cache vient de poser ferait un `update_cache` inutile — et # hors ligne, il echouerait APRES un travail deja fait. name: "{{ serveur_loki_paquets | difference((paquets_tiers_disponibles | default({})).keys() | list) }}" state: present update_cache: true when: (serveur_loki_paquets | difference((paquets_tiers_disponibles | default({})).keys() | list)) | length > 0 - name: Assurer le groupe loki (le paquet cree l'utilisateur en nogroup) ansible.builtin.group: name: loki system: true - name: Rattacher l'utilisateur loki a son groupe ansible.builtin.user: name: loki group: loki - name: Assurer le repertoire de donnees Loki ansible.builtin.file: path: "{{ serveur_loki_chemin }}" state: directory owner: loki group: loki mode: "0750" # --- TLS : synchroniser le cert step_ca AVANT d'activer http_tls_config --- - name: TLS — repertoire des certificats (loki) ansible.builtin.file: path: "{{ serveur_loki_tls_dir }}" state: directory owner: loki group: loki mode: "0700" when: serveur_loki_tls_actif | bool - name: TLS — script de synchronisation du certificat ansible.builtin.template: src: setops-loki-cert-sync.sh.j2 dest: /usr/local/sbin/setops-loki-cert-sync owner: root group: root mode: "0755" when: serveur_loki_tls_actif | bool - name: TLS — unites de synchronisation (service + path) ansible.builtin.template: src: "{{ item.s }}" dest: "{{ item.d }}" owner: root group: root mode: "0644" loop: - { s: "setops-loki-cert-sync.service.j2", d: "/etc/systemd/system/setops-loki-cert-sync.service" } - { s: "setops-loki-cert-sync.path.j2", d: "/etc/systemd/system/setops-loki-cert-sync.path" } loop_control: label: "{{ item.d | basename }}" when: serveur_loki_tls_actif | bool - name: TLS — activer la surveillance du certificat (path) ansible.builtin.systemd: name: setops-loki-cert-sync.path enabled: true state: started daemon_reload: true when: - serveur_loki_tls_actif | bool - not ansible_check_mode - name: TLS — synchroniser le certificat maintenant ansible.builtin.command: /usr/local/sbin/setops-loki-cert-sync changed_when: false when: - serveur_loki_tls_actif | bool - not ansible_check_mode - name: TLS — verifier la presence du certificat synchronise ansible.builtin.stat: path: "{{ serveur_loki_tls_dir }}/{{ item }}" register: serveur_loki_tls_stat loop: - loki.crt - loki.key when: - serveur_loki_tls_actif | bool - not ansible_check_mode - name: TLS — garde-fou, ne pas activer sans certificat en place ansible.builtin.assert: that: - item.stat.exists fail_msg: "Cert Loki absent ({{ item.item }}) — TLS non active." loop: "{{ serveur_loki_tls_stat.results | default([]) }}" loop_control: label: "{{ item.item }}" when: - serveur_loki_tls_actif | bool - not ansible_check_mode - name: Deployer la configuration Loki ansible.builtin.template: src: config.yml.j2 dest: "{{ serveur_loki_config }}" owner: root group: loki mode: "0640" notify: Redemarrer loki - name: Activer et demarrer Loki when: not ansible_check_mode ansible.builtin.systemd: name: "{{ serveur_loki_service }}" enabled: true state: started # --- Sonde de supervision (docs/supervision-conception.md) -------------------------- # Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la # fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. - name: Assurer le repertoire des sondes de supervision ansible.builtin.file: path: /usr/local/lib/setops/sondes state: directory owner: root group: root mode: "0755" - name: Deposer la sonde « ingestion » ansible.builtin.template: src: sonde-ingestion.sh.j2 dest: /usr/local/lib/setops/sondes/ingestion.sh owner: root group: root mode: "0750" # LA SONDE DE LA FRONTIERE N'EXISTE QUE LA OU ELLE EST ECOUTEE. Un tenant n'a pas de # frontiere a lui : sans etiquette declaree, rien n'est depose et rien n'est attendu. - name: Deposer la sonde des journaux de la frontiere ansible.builtin.template: src: sonde-frontiere.sh.j2 dest: /usr/local/lib/setops/sondes/journaux-frontiere.sh owner: root group: root mode: "0750" when: serveur_loki_sonde_frontiere_etiquette | length > 0 - name: Retirer la sonde de frontiere la ou aucune n'est ecoutee ansible.builtin.file: path: /usr/local/lib/setops/sondes/journaux-frontiere.sh state: absent when: not (serveur_loki_sonde_frontiere_etiquette | length > 0)