
Cette page montre comment construire un plugin d'inventaire dynamique Ansible pour OUTSCALE qui rivalise en richesse avec amazon.aws.aws_ec2. Le plugin utilise le SDK Python officiel osc-sdk-python et l'API ReadVms, expose les 56 filtres du schéma FiltersVm de la spec OAPI, et hérite des fragments Ansible standards (constructed, inventory_cache) pour offrir keyed_groups, groups conditionnels et compose. Le code complet a été validé sur un compte OUTSCALE de référence (région eu-west-2, 6 VMs Talos) en avril 2026. Page tagguée pour les piliers Well-Architected Operational Excellence (inventaire à jour, IaC) et Security (filtrage par tag, pas de credentials en clair).
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Concevoir un plugin d'inventaire Ansible conforme à la grammaire Ansible (DOCUMENTATION, options, héritage
BaseInventoryPlugin). - Authentifier via
osc-sdk-pythonen gérant la divergence de schéma entre le format de profil OSC (region_name) et le SDK Python (region). - Filtrer les VMs avec les 56 filtres du schéma
FiltersVmde la spec OAPI. - Résoudre
inventory_hostnamepar priorité (VmId,PublicIp,tag:Name,tag:Foo=Bar). - Composer
ansible_host, grouper par état, sous-région, tag, génération TINA, et écrire des groupes conditionnels (prod,bastions, etc.). - Valider le plugin sur un compte réel avec un harness de tests reproductible.
Prérequis
Section intitulée « Prérequis »- Compte OUTSCALE actif avec profil OSC configuré dans
~/.osc/config.json. - Python 3.10+, Ansible ≥ 2.16.
osc-sdk-python≥ 0.40.0 (pip install osc-sdk-python).- Si Ansible est installé via
pipx, injecter le SDK :pipx inject ansible osc-sdk-python.
Pourquoi un plugin dédié plutôt que le plugin AWS
Section intitulée « Pourquoi un plugin dédié plutôt que le plugin AWS »OUTSCALE étant compatible AWS sur l'API EC2, le plugin amazon.aws.aws_ec2 fonctionne en pointant sur l'endpoint Outscale. C'est même la voie courte historique. Mais ce raccourci a trois limites :
- Pas de filtres OUTSCALE-spécifiques,
Lifecycles: spot,NicSubregionNames,BootModes(uefi/legacy),TpmEnabledne sont pas exposés. - Pas de format TINA natif,
vm_typeest exposé sous le mapping AWS-style (t2.small), pas en TINA (tinav5.c2r4p2), ce qui complique leskeyed_groupspar génération. - Pas de profils OSC : le plugin AWS lit
~/.aws/credentials, pas~/.osc/config.json.
Un plugin dédié osc_vm adresse ces trois points et expose une grammaire OUTSCALE native : FiltersVm complet, Placement.SubregionName, format TINA, profils OSC.
Anatomie du plugin
Section intitulée « Anatomie du plugin »Le plugin Ansible suit un modèle bien établi : un fichier Python avec trois éléments, un bloc DOCUMENTATION YAML, un bloc EXAMPLES, et une classe InventoryModule qui hérite de BaseInventoryPlugin, Constructable, Cacheable.
Structure du dossier de lab
Section intitulée « Structure du dossier de lab »Le plugin vit dans le projet, pas dans un chemin système : Ansible le découvre parce que ansible.cfg déclare inventory_plugins = ./inventory_plugins. Les fichiers d'inventaire sont regroupés dans examples/ et doivent obligatoirement se terminer par .osc_vm.yml, sinon la méthode verify_file du plugin les refuse.
Répertoirelab-outscale-inventory/
- .envrc
- ansible.cfg
- requirements.txt
Répertoireinventory_plugins/
- osc_vm.py
Répertoireexamples/
- 01-basic.osc_vm.yml
- 02-filters.osc_vm.yml
- 03-keyed-groups.osc_vm.yml
- 04-full-featured.osc_vm.yml
Répertoireplaybooks/
- ping-by-tag.yml
Répertoiretests/
- run-tests.sh
ansible.cfg, activer le plugin local
Section intitulée « ansible.cfg, activer le plugin local »Un plugin d'inventaire n'est jamais actif par défaut : il faut à la fois indiquer où le chercher (inventory_plugins) et l'autoriser dans la liste enable_plugins. L'ordre de cette liste compte, et le bloc [inventory] active en prime le cache que le plugin utilisera pour éviter de rappeler l'API à chaque exécution.
[defaults]inventory_plugins = ./inventory_pluginshost_key_checking = Falsestdout_callback = yaml
[inventory]enable_plugins = osc_vm, host_list, script, auto, yaml, ini, tomlcache = Truecache_plugin = jsonfilecache_connection = ./.osc_vm_cachecache_timeout = 3600osc_vm doit être listé avant auto et yaml pour que les fichiers *.osc_vm.yml ne soient pas traités comme du YAML inventaire générique.
Le plugin Python
Section intitulée « Le plugin Python »Le fichier inventory_plugins/osc_vm.py tient en trois blocs qu'Ansible lit séparément : le bloc DOCUMENTATION, qui déclare les options acceptées, le bloc EXAMPLES, purement informatif, et la classe InventoryModule, qui exécute le travail. Ansible parse le premier avant même d'instancier la classe : une erreur de syntaxe dans le YAML empêche le plugin de se charger, sans que la moindre ligne de Python ne s'exécute. Les sections qui suivent détaillent chaque morceau dans cet ordre.
Bloc DOCUMENTATION
Section intitulée « Bloc DOCUMENTATION »Le bloc YAML décrit toutes les options du plugin pour la documentation Ansible automatique. Il joue aussi un rôle fonctionnel : toute option absente de ce bloc est refusée par _read_config_data() avec une erreur de validation, et les valeurs par défaut déclarées ici sont celles que get_option() retournera quand l'utilisateur ne précise rien. La directive extends_documentation_fragment importe les options des fragments standards constructed (qui apporte compose, groups, keyed_groups) et inventory_cache (qui apporte cache, cache_plugin, cache_timeout), sans avoir à les redéclarer.
DOCUMENTATION = r"""name: osc_vmplugin_type: inventoryshort_description: "Inventaire dynamique des VMs OUTSCALE via le SDK Python"description: - "Récupère la liste des VMs depuis l'API OUTSCALE OAPI et les expose comme hôtes Ansible." - "Le fichier d'inventaire doit se terminer par .osc_vm.yml ou .osc_vm.yaml pour être détecté."extends_documentation_fragment: - constructed - inventory_cacheoptions: plugin: description: "Marque la sélection du plugin - toujours osc_vm." required: true choices: ['osc_vm'] profiles: description: "Liste des profils OSC à interroger." type: list elements: str default: [] regions: description: "Régions à interroger. Si vide, lue depuis le profil. EU only - eu-west-2 et cloudgouv-eu-west-1." type: list elements: str default: [] filters: description: "Dictionnaire de filtres OAPI ReadVms." type: dict default: {} hostnames: description: "Candidats inventory_hostname évalués dans l'ordre." type: list elements: raw default: - PublicIp - "PrivateIps[0]" - VmId hostvars_prefix: description: "Préfixe ajouté aux hostvars." type: str default: "" strict_permissions: description: "Si false, ignore silencieusement les régions inaccessibles." type: bool default: true"""Classe InventoryModule
Section intitulée « Classe InventoryModule »La classe est le point d'entrée exécutable du plugin. Ansible appelle d'abord verify_file() sur chaque fichier d'inventaire candidat pour savoir si ce plugin le revendique, puis parse() sur celui qui a répondu oui. L'import du SDK est enveloppé dans un try/except ImportError : sans cette précaution, un osc-sdk-python manquant produirait une trace Python illisible au lieu du message d'erreur explicite levé par AnsibleError. La logique de cache est explicite parce que le fragment inventory_cache fournit le stockage, pas la stratégie de lecture et d'écriture.
from ansible.errors import AnsibleErrorfrom ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheablefrom ansible.utils.display import Display
try: from osc_sdk_python import Gateway HAS_OSC_SDK = True OSC_SDK_IMPORT_ERROR = Noneexcept ImportError as exc: HAS_OSC_SDK = False OSC_SDK_IMPORT_ERROR = exc
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): NAME = "osc_vm"
def verify_file(self, path): """Le plugin n'accepte que les fichiers se terminant par .osc_vm.yml/yaml.""" if not super().verify_file(path): return False return path.endswith((".osc_vm.yml", ".osc_vm.yaml"))
def parse(self, inventory, loader, path, cache=True): if not HAS_OSC_SDK: raise AnsibleError( "Le SDK Python OUTSCALE n'est pas installé : " "`pip install osc-sdk-python` (erreur : %s)" % OSC_SDK_IMPORT_ERROR ) super().parse(inventory, loader, path) self._read_config_data(path)
# Cache (clé propre au plugin pour éviter les collisions). cache_key = self.get_cache_key(path) user_cache_setting = self.get_option("cache") attempt_to_read = user_cache_setting and cache cache_needs_update = user_cache_setting and not cache
if attempt_to_read: try: results = self._cache[cache_key] self._populate(results) return except KeyError: cache_needs_update = True
results = self._collect_vms() if cache_needs_update: self._cache[cache_key] = results self._populate(results)Authentification, gérer region_name
Section intitulée « Authentification, gérer region_name »Pilier Operational Excellence. Le SDK osc-sdk-python 0.40.0 rejette silencieusement les profils contenant la clé legacy region_name (héritée d'oapi-cli) en raison d'un TypeError: unexpected keyword arguments. Comme l'exception est swallow par from_standard_configuration, le profil utilisateur n'est jamais chargé et la suite du code échoue avec un cryptique TypeError: can only concatenate str (not "NoneType") to str.
Le contournement consiste à lire le fichier de config manuellement et à normaliser les clés avant de les passer au SDK :
@staticmethoddef _load_profile_credentials(profile): """Lit ~/.osc/config.json et normalise vers le format SDK Python.""" import json, os with open(os.path.expanduser("~/.osc/config.json")) as fh: config = json.load(fh) section = config.get(profile) if section is None: raise AnsibleError("Profil '%s' introuvable" % profile)
creds = {} for key in ("access_key", "secret_key", "x509_client_cert", "x509_client_key", "tls_skip_verify", "login", "password"): if key in section: creds[key] = section[key]
# region_name (oapi-cli legacy) → region (format SDK Python). if "region_name" in section: creds["region"] = section["region_name"] elif "region" in section: creds["region"] = section["region"]
# https: true/false → protocol: "https"/"http" if "https" in section: creds["protocol"] = "https" if section["https"] else "http" else: creds["protocol"] = "https"
return credsAppel ReadVms
Section intitulée « Appel ReadVms »C'est ici que le plugin parle réellement à l'API OUTSCALE. L'objet Gateway du SDK dispatche tout attribut inconnu en appel d'action OAPI, ce qui rend l'écriture fluide (gw.ReadVms(...)) mais transforme la moindre faute de frappe en appel d'API inexistant. Le paramètre Filters n'est envoyé que s'il est non vide : ReadVms sans filtre retourne l'intégralité des VMs de la région, quel que soit leur état.
@staticmethoddef _read_vms(profile, region, filters): creds = InventoryModule._load_profile_credentials(profile) if region: creds["region"] = region
# Gateway() est un context manager - `with` libère proprement les ressources. # Le SDK dispatche tout attribut inconnu en tant qu'action OAPI ; # éviter `gw.close()` qui serait interprété comme un appel API "close". with Gateway(**creds) as gw: payload = {"Filters": filters} if filters else {} response = gw.ReadVms(**payload) return list(response.get("Vms", []))Résolution de inventory_hostname
Section intitulée « Résolution de inventory_hostname »L'inventory_hostname est le nom sous lequel Ansible désignera la VM dans les playbooks et les rapports. L'option hostnames liste des candidats évalués dans l'ordre : le premier qui retourne une valeur non vide gagne, ce qui permet de nommer les VMs par leur tag Name tout en gardant VmId comme filet de sécurité pour celles qui ne sont pas taguées. La fonction ci-dessous gère quatre formes de spécification : un champ direct du schéma Vm, un tag simple, un tag avec valeur attendue, et un index dans la liste des IPs privées.
@staticmethoddef _extract_hostname_field(vm, spec): """Résout une spec hostname (VmId, PublicIp, tag:Name, tag:Foo=Bar, PrivateIps[N]).""" # Tag avec valeur attendue : ne retient la VM que si le tag matche. if spec.startswith("tag:") and "=" in spec: raw = spec[4:] tag_key, expected = raw.split("=", 1) for tag in vm.get("Tags", []) or []: if tag.get("Key") == tag_key and tag.get("Value") == expected: return tag.get("Value", "") return ""
# Tag simple : retourne la valeur du tag. if spec.startswith("tag:"): tag_key = spec[4:] for tag in vm.get("Tags", []) or []: if tag.get("Key") == tag_key: return tag.get("Value", "") return ""
# PrivateIps[N] - index dans la liste des IPs privées. import re m = re.match(r"^PrivateIps\[(\d+)\]$", spec) if m: idx = int(m.group(1)) ips = [] for nic in vm.get("Nics", []) or []: for pip in nic.get("PrivateIps", []) or []: if pip.get("PrivateIp"): ips.append(pip["PrivateIp"]) if not ips and vm.get("PrivateIp"): ips = [vm["PrivateIp"]] return ips[idx] if idx < len(ips) else ""
# Champ Vm direct (PublicIp, PrivateDnsName, VmId, etc.). return str(vm.get(spec, "") or "")Construction des hostvars
Section intitulée « Construction des hostvars »Pilier Operational Excellence. Les hostvars exposées dérivent du schéma Vm de l'OAPI, normalisées en snake_case, avec deux ajouts pratiques :
vm_tags(et nontagsréservé par Ansible), dict des tags pour usage en Jinja2.private_ips, liste flat des IPs privées multi-NIC.
@staticmethoddef _build_hostvars(vm): hostvars = {}
# Champs flat issus du schéma OAPI Vm. for key in _VM_HOSTVAR_KEYS: if key in vm: hostvars[InventoryModule._snake(key)] = vm[key]
# Tags transformés en dict - `vm_tags` plutôt que `tags` (réservé Ansible). tags_dict = {} for tag in vm.get("Tags", []) or []: tags_dict[tag.get("Key", "")] = tag.get("Value", "") hostvars["vm_tags"] = tags_dict hostvars["tags_raw"] = vm.get("Tags", []) or [] hostvars.pop("tags", None)
# Sous-région dérivée de Placement. placement = vm.get("Placement", {}) or {} if placement.get("SubregionName"): hostvars["subregion"] = placement["SubregionName"]
# Liste plate des IPs privées (utile pour ansible_host derrière bastion). private_ips = [] for nic in vm.get("Nics", []) or []: for pip in nic.get("PrivateIps", []) or []: if pip.get("PrivateIp"): private_ips.append(pip["PrivateIp"]) if not private_ips and vm.get("PrivateIp"): private_ips = [vm["PrivateIp"]] hostvars["private_ips"] = private_ips
return hostvarsFichiers d'inventaire
Section intitulée « Fichiers d'inventaire »Un fichier d'inventaire est le seul artefact que l'utilisateur du plugin écrit au quotidien. Sa clé plugin: osc_vm sélectionne le plugin, et son nom de fichier doit se terminer par .osc_vm.yml pour que verify_file() le revendique. Les quatre exemples ci-dessous vont du minimum viable à la configuration complète, chacun n'ajoutant qu'une notion à la fois.
Exemple basic, toutes les VMs running
Section intitulée « Exemple basic, toutes les VMs running »Le strict minimum : un profil OSC et un filtre d'état. Sans la clé hostnames, les hôtes prennent le premier candidat par défaut, soit leur PublicIp.
plugin: osc_vmprofiles: - defaultfilters: VmStateNames: - runningExemple avec filtres OAPI
Section intitulée « Exemple avec filtres OAPI »Ici le périmètre se resserre sur une sous-région précise et les hôtes sont nommés d'après leur tag Name. Le tag:Name doit être quoté en YAML, sinon les deux-points en font une clé de mapping.
plugin: osc_vmprofiles: - defaultregions: - eu-west-2filters: VmStateNames: - running SubregionNames: - eu-west-2ahostnames: - "tag:Name" - PublicIp - VmIdExemple avec keyed_groups
Section intitulée « Exemple avec keyed_groups »Les keyed_groups viennent du fragment constructed : chaque entrée crée automatiquement un groupe Ansible nommé <prefix>_<valeur> à partir d'une hostvar. Ils s'appuient sur le préfixe osc_ déclaré par hostvars_prefix, donc la clé doit s'écrire osc_state et non state. Le filtre default('untagged') évite qu'une VM sans le tag attendu ne fasse échouer le rendu Jinja.
plugin: osc_vmprofiles: - defaultfilters: VmStateNames: - runninghostnames: - "tag:Name" - VmIdhostvars_prefix: "osc_"keyed_groups: - prefix: state key: osc_state - prefix: az key: osc_subregion - prefix: tina key: "osc_vm_type | regex_search('^(tinav\\d+)') | default('unknown')" - prefix: app key: "osc_vm_tags['Application'] | default('untagged')" - prefix: env key: "osc_vm_tags['env'] | default('untagged')"Exemple complet avec compose et groups conditionnels
Section intitulée « Exemple complet avec compose et groups conditionnels »Cette configuration ajoute deux mécanismes du fragment constructed. Les groups définissent des groupes conditionnels : la valeur est une expression Jinja booléenne, et l'hôte rejoint le groupe quand elle est vraie. Le compose calcule des hostvars supplémentaires, typiquement ansible_host pour joindre les VMs par leur IP privée derrière un bastion.
plugin: osc_vmprofiles: - defaultregions: - eu-west-2filters: VmStateNames: - runninghostnames: - "tag:Name" - VmIdhostvars_prefix: "osc_"keyed_groups: - prefix: state key: osc_state - prefix: az key: osc_subregion - prefix: tina key: "osc_vm_type | regex_search('^(tinav\\d+)') | default('unknown')" - prefix: env key: "osc_vm_tags['env'] | default('untagged')"groups: prod: "osc_vm_tags.env | default('') == 'prod'" bastions: "'bastion' in (osc_vm_tags.Name | default(''))" has_public_ip: "osc_public_ip is defined and osc_public_ip"compose: ansible_host: "osc_private_ips[0] | default(osc_public_ip)" ansible_user: "'outscale'"Validation sur un compte réel
Section intitulée « Validation sur un compte réel »Un plugin d'inventaire se valide avec ansible-inventory, jamais directement avec un playbook : la commande n'ouvre aucune connexion SSH et isole donc les erreurs d'inventaire des erreurs de connectivité. L'option --list affiche le JSON complet, --graph la hiérarchie des groupes générés, et --host les hostvars d'une machine unique. Un plugin qui ne se charge pas se signale par un inventaire vide accompagné d'un warning, pas par une erreur fatale : il faut donc lire les warnings avant de conclure que le compte ne contient rien.
-
Préparer l'environnement :
Fenêtre de terminal pipx inject ansible osc-sdk-python # si Ansible installé via pipxdirenv allow # charge OSC_PROFILE -
Lancer un inventaire basique :
Fenêtre de terminal ansible-inventory -i examples/01-basic.osc_vm.yml --list | head -20 -
Vérifier les groupes générés :
Fenêtre de terminal ansible-inventory -i examples/04-full-featured.osc_vm.yml --graphSortie attendue (sur compte de référence) :
@all:|--@state_running:| |--talos-prod-vault| |--talos-prod-bastion| |--talos-prod-cp-1| |--talos-prod-cp-2| |--talos-prod-cp-3|--@az_eu_west_2a:| |--talos-prod-vault| |--talos-prod-bastion|--@az_eu_west_2b:| |--talos-prod-cp-2|--@az_eu_west_2c:| |--talos-prod-cp-3|--@tina_tinav5:| |--talos-prod-cp-1| |--talos-prod-cp-2| |--talos-prod-cp-3|--@tina_tinav7:| |--talos-prod-bastion|--@bastions:| |--talos-prod-bastion -
Inspecter les hostvars d'un hôte :
Fenêtre de terminal ansible-inventory -i examples/04-full-featured.osc_vm.yml --host talos-prod-bastion
Exclure les VMs non SSH-able (Talos, Bottlerocket…)
Section intitulée « Exclure les VMs non SSH-able (Talos, Bottlerocket…) »Pilier Operational Excellence. Certaines distributions cloud, Talos Linux, Bottlerocket, Flatcar : n'exposent pas SSH. Talos administre ses VMs via l'API talosctl sur le port 50000 avec authentification mTLS ; aucun port 22 ouvert, aucun shell utilisateur. Inclure ces VMs dans un inventaire Ansible produit des erreurs ssh: connection refused à chaque playbook, qui polluent les logs et masquent les vraies pannes.
Le plugin osc_vm propose deux options pour les exclure :
exclude_tags: préféré quand les VMs concernées portent un tag distinctif (os=talos,managed-by=talos, etc.). Liste de couplesKey=ValueouKeyseul (matche toute valeur).exclude_vm_ids: fallback quand le tagging n'est pas en place, liste explicite d'i-XXXXXXXX. À mettre à jour à chaque création/destruction de VM Talos.
plugin: osc_vmprofiles: - defaultfilters: VmStateNames: - runninghostnames: - "tag:Name" - VmId
# Méthode 1 - exclusion par tag (recommandée si tagging discipliné).exclude_tags: - "os=talos" - "managed-by=talos"
# Méthode 2 - fallback par ID explicite.exclude_vm_ids: - "i-da44d982" - "i-404893e4"Validation observée sur le compte de référence (6 VMs Talos en running) :
- Sans exclusion :
6 hôtesretournés → 6 erreurs SSH au prochain playbook. - Avec
exclude_vm_idslistant les 6 IDs :0 hôtesretournés → l'inventaire est exact.
Bonne pratique : poser systématiquement un tag os=<distribution> à la création des VMs (via Terraform ou Packer). L'inventaire devient maintenable même quand les VmIds changent (recréation, scaling), l'exclude_vm_ids reste un fallback temporaire.
Patterns Well-Architected
Section intitulée « Patterns Well-Architected »Un inventaire dynamique n'est pas qu'un confort d'écriture : il devient la source de vérité de tout ce que l'automatisation touche. Les six pratiques ci-dessous alignent le plugin sur les piliers Operational Excellence, Security, Cost et Performance Efficiency du référentiel Well-Architected. Chacune règle un problème observé en exploitation réelle, du quota d'API consommé inutilement au compte d'accès surprivilégié dans un runner CI.
Filtrer côté API plutôt que côté Ansible
Section intitulée « Filtrer côté API plutôt que côté Ansible »Pilier Operational Excellence, Cost. Toujours pousser le filtrage vers l'API ReadVms via la clé filters du plugin. Filtrer ensuite dans Ansible avec --limit ou des when: est plus coûteux (transfert réseau + parsing) et expose l'inventaire complet en cache, ce qui complique la traçabilité d'audit. Avec filters: {VmStateNames: [running]}, les VMs stopped et terminated n'apparaissent jamais.
Préfixer toutes les hostvars
Section intitulée « Préfixer toutes les hostvars »Pilier Operational Excellence. Activer hostvars_prefix: "osc_" évite les collisions avec les variables Ansible standard (ansible_host, inventory_hostname, tags, etc.) et rend le code de playbook auto-documenté ({{ osc_subregion }} est sans ambiguïté plus parlant que {{ subregion }}).
Résoudre ansible_host avec entry_role (recommandé)
Section intitulée « Résoudre ansible_host avec entry_role (recommandé) »Pilier Security. Dans un parc avec bastion central, chaque VM doit voir son ansible_host adapté à son rôle :
- Bastion atteint directement →
ansible_host = PublicIp - Toutes les autres VMs atteintes via ProxyJump →
ansible_host = PrivateIps[0]
L'option entry_role du plugin résout ce choix automatiquement, sans compose Jinja conditionnel :
plugin: osc_vm
# Convention : les VMs taguées role=bastion sont les seuls entry points.# Toutes les autres VMs prennent leur PrivateIps[0] (ProxyJump à configurer# en group_vars/role_*.yml ou ~/.ssh/config).entry_role: bastion
compose: ansible_user: "'outscale'" # ansible_host géré par entry_rolePourquoi pas un compose conditionnel :
# Marche, mais hard-code le test métier dans le compose ; pas réutilisable.compose: ansible_host: >- public_ip if vm_tags.role | default('') == 'bastion' else private_ips[0]entry_role met cette logique côté plugin plutôt que côté config. Conventionnel, paramétrable (entry_role: jumpbox, edge, etc.), réutilisable d'un projet à l'autre.
Pattern legacy, compose Jinja sans entry_role
Section intitulée « Pattern legacy, compose Jinja sans entry_role »Si vous utilisez encore osc_vm sans l'option entry_role (rétro-compat), le pattern qui couvre les deux cas (bastion + VMs privées) est de privilégier l'IP privée :
# Avec hostvars_prefix: "osc_"compose: ansible_host: "osc_private_ips[0] | default(osc_public_ip)" ansible_user: "'outscale'"Limite : le bastion lui-même tombe alors sur son IP privée, qui n'est pas joignable depuis le poste opérateur. Pour le contourner, mettre un host_vars/<bastion>.yml au niveau playbook qui force ansible_host: <PublicIp>.
Host bastion-prod HostName 148.253.109.96 User outscale
Host 10.0.* 10.100.* ProxyJump bastion-prod User outscaleCompte EIM scopé pour le pipeline
Section intitulée « Compte EIM scopé pour le pipeline »Pilier Security. Le pipeline qui interroge l'inventaire (CI, AWX, AAP) s'authentifie avec un compte EIM dédié scopé à ReadVms + ReadTags + ReadSubregions, pas de droits d'écriture. Si les credentials sont compromis depuis le runner CI, l'attaquant ne peut que lister l'inventaire, pas le modifier.
Cache jsonfile activé
Section intitulée « Cache jsonfile activé »Pilier Performance Efficiency. Activer cache = True dans ansible.cfg avec cache_plugin = jsonfile réduit drastiquement le nombre d'appels ReadVms lors d'exécutions répétées (par exemple ansible-playbook lancé plusieurs fois sur le même inventaire pendant une session de développement). Avec cache_timeout = 3600, le cache est invalidé toutes les heures, assez fréquent pour rester à jour, assez rare pour ne pas saturer l'API.
Antipatterns à éviter
Section intitulée « Antipatterns à éviter »Ces cinq erreurs reviennent systématiquement sur les premiers inventaires dynamiques. Aucune ne provoque d'échec immédiat, ce qui les rend difficiles à repérer : elles se paient plus tard, en fuite de credentials, en quota d'API consommé, ou en logs saturés d'erreurs SSH qui masquent les vraies pannes.
Hardcoder access_key/secret_key dans l'inventaire. Faille de sécurité massive dès qu'on commit. Toujours passer par les profils OSC ou les variables d'environnement OUTSCALE.
Pas de filtre VmStateNames. L'inventaire remonte alors les VMs stopped, terminated et même les VMs en pending, Ansible essaie de les joindre, échoue, et pollue les logs. Filtrer dès l'API.
tags au lieu de vm_tags. Le nom tags est réservé par Ansible (warning explicite « Found variable using reserved name 'tags' »). Utiliser vm_tags (le plugin l'expose nativement).
Plugin global plutôt que local. Coller osc_vm.py dans /etc/ansible/plugins/inventory/ plutôt que dans le dossier du projet rend la mise à jour difficile et masque la dépendance du projet. Préférer inventory_plugins/ à la racine du projet.
Pas de cache. Sans cache = True, chaque ansible-playbook fait un nouvel appel ReadVms. Sur un parc de 100 VMs avec un pipeline CI qui re-lance plusieurs fois, ça mange du quota API et ralentit l'exécution.
Dépannage
Section intitulée « Dépannage »Les messages d'erreur d'un plugin d'inventaire sont rarement parlants : Ansible avale souvent l'exception d'origine et n'affiche qu'un symptôme distant. Le tableau ci-dessous relie les messages réellement rencontrés en lab à leur cause et à leur correction. Réflexe utile avant tout diagnostic : relancer avec ansible-inventory -vvv, qui affiche le chargement du plugin et les warnings normalement masqués.
| Symptôme | Cause probable | Action |
|---|---|---|
Le SDK Python OUTSCALE n'est pas installé | osc-sdk-python non dans le venv Ansible | pipx inject ansible osc-sdk-python |
mapping values are not allowed in this context (DOCUMENTATION) | Chaîne YAML non quotée avec [, ], : | Quoter les chaînes ambiguës ("PrivateIps[0]" plutôt que PrivateIps[0]) |
can only concatenate str (not "NoneType") to str | Profil OSC avec region_name (legacy oapi-cli) silencieusement rejeté par le SDK | Le plugin contourne via _load_profile_credentials, vérifier que la version du plugin est à jour |
Action close does not exists for python sdk | gw.close() interprété comme appel d'API | Utiliser with Gateway(...) as gw: (context manager) au lieu de try/finally + close() |
Found variable using reserved name 'tags' | Hostvar tags (réservé Ansible) exposée | Le plugin expose désormais vm_tags, mettre à jour vos playbooks |
Permission denied (publickey) alors que la clé est correcte | ansible_user: "outscale" dans compose: interprété comme variable (vide) puis fallback sur l'utilisateur courant | Quoter la constante en Jinja : ansible_user: "'outscale'" |
À retenir
Section intitulée « À retenir »osc-sdk-pythoncomme client officiel, gérer la divergenceregion_name/regioncôté plugin, lire manuellement~/.osc/config.json.with Gateway(...) as gw:comme context manager, évitergw.close()qui serait un appel API.- DOCUMENTATION YAML : quoter les chaînes contenant
[,],:pour éviter les ambiguïtés de parsing. hostvars_prefix: "osc_"systématique, évite les collisions Ansible.vm_tagsplutôt quetags(réservé Ansible).with Gateway(**creds)oùcredsvient de_load_profile_credentials(profile)qui normaliseregion_name→regionethttps: true→protocol: "https".- Cache
jsonfileactivé, coupe les appels API répétés. - Compte EIM scopé pour le pipeline,
ReadVms+ReadTagssuffisent. compose:est du Jinja : pour assigner une string littérale (ex.ansible_user), il faut quoter en interne :"'outscale'"(double-quote YAML, single-quote Jinja).entry_role: bastionrésout automatiquementansible_hostselon le rôle, IP publique pour le bastion, IP privée pour les autres. Évite un compose Jinja conditionnel.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Pilier Operational Excellence : L'inventaire dynamique replacé dans une chaîne d'exploitation complète, du tag à la métrique.
- Chapitre 3, Bastion SSH durci : Ce plugin en situation, avec le ciblage du bastion par le groupe dynamique
role_bastion. - Chapitre 4, Frontends Nginx HTTPS : Le même inventaire appliqué à trois frontends répartis sur trois sous-régions.