Jerikan+Ansible : un système de gestion de configuration pour le réseau

ex-Blade Network Team

Il existe de nombreuses ressources pour l’automatisation du réseau avec Ansible. La plupart d’entre elles n’exposent que les premières étapes ou se limitent à un champ d’application réduit. Elles donnent peu d’indications sur la manière d’aller plus loin. Les environnements réseau réels peuvent être vastes, polyvalents, hétérogènes et remplis d’exceptions. L’absence d’exemples concrets de déploiements Ansible, contrairement à Puppet et SaltStack, conduit de nombreuses équipes à élaborer des solutions fragiles et incomplètes.

Nous avons publié sous une licence libre notre tentative pour pallier à ce manque :

  • Jerikan, un outil pour construire des fichiers de configuration à partir d’une source de vérité unique et de modèles Jinja2, ainsi que son intégration dans GitLab,
  • un playbook Ansible pour déployer ces fichiers de configuration sur les équipements réseau,
  • les données de configuration et les modèles pour nos centres de données, aujourd’hui fermés, à San Francisco et en Corée du Sud, couvrant de nombreux fournisseurs (Facebook Wedge 100, Dell S4048 et S6010, Juniper QFX 5110, Juniper QFX 10002, Cisco ASR 9001, Cisco Catalyst 2960, serveurs de console Opengear et serveurs Linux) et de nombreuses fonctionnalités (approvisionnement, routage BGP vers l’hôte, routage Internet, réseau d’administration séparé, configuration DNS, intégration avec NetBox et les IRR).

Voici une démo pour l’ajout d’un nouveau peering:

Ce travail est l’œuvre collective de Cédric Hascoët, Jean-Christophe Legatte, Loïc Pailhas, Sébastien Hurtel, Tchadel Icard et Vincent Bernat. Nous sommes l’équipe réseau de Blade, une société française exploitant Shadow, un produit de PC dans les nuages. En mai 2021, notre société a été rachetée par Octave Klaba et l’infrastructure est en cours de transfert vers OVHcloud, sauvant ainsi Shadow en tant que produit, mais rendant notre équipe superflue.

Notre réseau comptait environ 800 équipements, répartis sur 10 centres de données avec plus de 2,5 Tbps de bande passante disponible en sortie. Le matériel publié est donc un exemple substantiel de gestion d’un réseau de taille moyenne à l’aide d’Ansible. Nous avons laissé de côté la configuration de nos centres de données plus anciens pour rendre le résultat final plus lisible, tout en conservant suffisamment de substance pour ne pas en faire un exemple trivial.

Jerikan#

Le premier composant est Jerikan. En entrée, il prend une liste d’équipements, des données de configuration, des modèles et des scripts de validation. Il génère un ensemble de fichiers de configuration pour chaque appareil. Ansible pourrait effectuer cette tâche, mais il présente les limitations suivantes concernant la compilation des modèles :

  • il est lent,
  • les erreurs sont difficiles à corriger1,
  • la hiérarchie de recherche d’une variable est rigide.
Entrées et sorties de Jerikan
Entrées et sorties de Jerikan

Si vous voulez suivre les exemples, il vous suffit d’avoir installé Docker et Docker Compose. Clonez le dépôt et vous êtes prêt !

Source de vérité#

Nous utilisons des fichiers YAML, versionnés avec Git, comme source de vérité unique au lieu d’utiliser une base de données, comme NetBox, ou un mélange de base de données et de fichiers texte. Cela offre de nombreux avantages :

  • chacun peut utiliser son éditeur de texte préféré,
  • l’équipe prépare les changements dans des branches,
  • l’équipe évalue les changements en utilisant des demandes de fusion,
  • les demandes de fusion présentent l’impact sur les fichiers de configuration générés,
  • le retour à un état antérieur est facile,
  • c’est rapide.

Mise à jour (11.2021)

Pour un argumentaire plus détaillé sur le choix de Git par rapport à NetBox, lisez « Git comme source de vérité pour l’automatisation du réseau »

Le premier fichier est devices.yaml. Il contient la liste des équipements. Le deuxième fichier est classifier.yaml. Il définit un ensemble de clés-valeurs pour chaque équipement. Il est utilisé dans les modèles et pour rechercher les données associées à un appareil.

$ ./run-jerikan scope to1-p1.sk1.blade-group.net
continent: apac
environment: prod
groups:
- tor
- tor-bgp
- tor-bgp-compute
host: to1-p1.sk1
location: sk1
member: '1'
model: dell-s4048
os: cumulus
pod: '1'
shorthost: to1-p1

Le nom de l’équipement est comparé à une liste d’expressions régulières et chaque correspondance contribue au résultat. Pour to1-p1.sk1.blade-group.net, le sous-ensemble suivant de classifier.yaml définit ses variables :

matchers:
  - '^(([^.]*)\..*)\.blade-group\.net':
      environment: prod
      host: '\1'
      shorthost: '\2'
  - '\.(sk1)\.':
      location: '\1'
      continent: apac
  - '^to([12])-[as]?p(\d+)\.':
      member: '\1'
      pod: '\2'
  - '^to[12]-p\d+\.':
      groups:
        - tor
        - tor-bgp
        - tor-bgp-compute
  - '^to[12]-(p|ap)\d+\.sk1\.':
      os: cumulus
      model: dell-s4048

Le troisième fichier est searchpaths.py. Il décrit les répertoires à examiner pour trouver une variable. Une fonction Python fournit une liste de chemins dans data/ selon les variables associées à l’équipement. Voici une version simplifiée2 :

def searchpaths(scope):
    paths = [
        "host/{scope[location]}/{scope[shorthost]}",
        "location/{scope[location]}",
        "os/{scope[os]}-{scope[model]}",
        "os/{scope[os]}",
        'common'
    ]
    for idx in range(len(paths)):
        try:
            paths[idx] = paths[idx].format(scope=scope)
        except KeyError:
            paths[idx] = None
    return [path for path in paths if path]

Avec cette définition, les données pour to1-p1.sk1.blade-group.net sont recherchées dans les chemins suivants :

$ ./run-jerikan scope to1-p1.sk1.blade-group.net
[…]
Search paths:
  host/sk1/to1-p1
  location/sk1
  os/cumulus-dell-s4048
  os/cumulus
  common

Les variables sont limitées à un espace de noms à spécifier lors d’une recherche. Nous utilisons les suivants :

  • system pour les comptes, DNS, serveurs syslog, …
  • topology pour les ports, interfaces, adresses IP, sous-réseaux, …
  • bgp pour la configuration de BGP
  • build pour les modèles et les scripts de validation
  • apps pour les variables d’application

Lors de la recherche d’une variable dans un espace de noms donné, Jerikan recherche un fichier YAML nommé d’après l’espace de noms dans chaque répertoire retournés par la fonction de recherche. Par exemple, si nous recherchons une variable pour to1-p1.sk1.blade-group.net dans l’espace de noms bgp, les fichiers YAML suivants sont traités : host/sk1/to1-p1/bgp.yaml, location/sk1/bgp.yaml, os/cumulus-dell-s4048/bgp.yaml, os/cumulus/bgp.yaml et common/bgp.yaml. La recherche s’arrête à la première correspondance.

Le fichier schema.yaml nous permet d’outrepasser ce dernier comportement en fusionnant dans certains cas les dictionnaires et les tableaux correspondants3. Voici un extrait de ce fichier pour l’espace de noms topology :

system:
  users:
    merge: hash
  sampling:
    merge: hash
  ansible-vars:
    merge: hash
  netbox:
    merge: hash

La dernière fonctionnalité de la source de vérité est la possibilité d’utiliser des modèles Jinja2 pour les clefs et les valeurs en les préfixant par « ~ » :

# Dans data/os/junos/system.yaml
netbox:
  manufacturer: Juniper
  model: "~{{ model|upper }}"

# Dans data/groups/tor-bgp-compute/system.yaml
netbox:
  role: net_tor_gpu_switch

Chercher netbox dans l’espace de noms system pour to1-p2.ussfo03.blade-group.net produit le résultat suivant :

$ ./run-jerikan scope to1-p2.ussfo03.blade-group.net
continent: us
environment: prod
groups:
- tor
- tor-bgp
- tor-bgp-compute
host: to1-p2.ussfo03
location: ussfo03
member: '1'
model: qfx5110-48s
os: junos
pod: '2'
shorthost: to1-p2
[…]
Search paths:
[…]
  groups/tor-bgp-compute
[…]
  os/junos
  common
$ ./run-jerikan lookup to1-p2.ussfo03.blade-group.net system netbox
manufacturer: Juniper
model: QFX5110-48S
role: net_tor_gpu_switch

Cela fonctionne aussi pour des données structurées :

# Dans groups/adm-gateway/topology.yaml
interface-rescue:
  address: "~{{ lookup('topology', 'addresses').rescue }}"
  up:
    - "~ip route add default via {{ lookup('topology', 'addresses').rescue|ipaddr('first_usable') }} table rescue"
    - "~ip rule add from {{ lookup('topology', 'addresses').rescue|ipaddr('address') }} table rescue priority 10"

# Dans groups/adm-gateway-sk1/topology.yaml
interfaces:
  ens1f0: "~{{ lookup('topology', 'interface-rescue') }}"

Cela retourne le résultat suivant :

$ ./run-jerikan lookup gateway1.sk1.blade-group.net topology interfaces
[…]
ens1f0:
  address: 121.78.242.10/29
  up:
  - ip route add default via 121.78.242.9 table rescue
  - ip rule add from 121.78.242.10 table rescue priority 10

Pour placer des données dans la source de vérité, nous utilisons les règles suivantes :

  1. Ne pas se répéter.
  2. Préférer l’emplacement le plus spécifique sans enfreindre la première règle.
  3. Utiliser avec parcimonie les modèles, principalement pour respecter les règles précédentes.
  4. Limiter le modèle de données à ce qui est nécessaire pour votre cas d’utilisation.

La première règle est assez importante. Par exemple, lorsque vous spécifiez des adresses IP pour une liaison point à point, ne spécifiez qu’un côté et déduisez l’autre valeur via les modèles. La dernière règle signifie qu’il est inutile de reproduire un modèle BGP YANG pour spécifier les pairs et les politiques BGP :

peers:
  transit:
    cogent:
      asn: 174
      remote:
        - 38.140.30.233
        - 2001:550:2:B::1F9:1
      specific-import:
        - name: ATT-US
          as-path: ".*7018$"
          lp-delta: 50
  ix-sfmix:
    rs-sfmix:
      monitored: true
      asn: 63055
      remote:
        - 206.197.187.253
        - 206.197.187.254
        - 2001:504:30::ba06:3055:1
        - 2001:504:30::ba06:3055:2
    blizzard:
      asn: 57976
      remote:
        - 206.197.187.42
        - 2001:504:30::ba05:7976:1
      irr: AS-BLIZZARD

Modèles#

La liste des modèles à compiler pour chaque équipement est stockée dans la source de vérité, sous l’espace de noms build :

$ ./run-jerikan lookup edge1.ussfo03.blade-group.net build templates
data.yaml: data.j2
config.txt: junos/main.j2
config-base.txt: junos/base.j2
config-irr.txt: junos/irr.j2
$ ./run-jerikan lookup to1-p1.ussfo03.blade-group.net build templates
data.yaml: data.j2
config.txt: cumulus/main.j2
frr.conf: cumulus/frr.j2
interfaces.conf: cumulus/interfaces.j2
ports.conf: cumulus/ports.j2
dhcpd.conf: cumulus/dhcp.j2
default-isc-dhcp: cumulus/default-isc-dhcp.j2
authorized_keys: cumulus/authorized-keys.j2
motd: linux/motd.j2
acl.rules: cumulus/acl.j2
rsyslog.conf: cumulus/rsyslog.conf.j2

Les modèles utilisent Jinja2. C’est le même moteur que pour Ansible. Jerikan fournit quelques filtres personnalisés mais réutilise également certains des filtres d’Ansible4, notamment ipaddr. Voici un extrait de templates/junos/base.j2 pour configurer les serveurs DNS et NTP sur les équipements Juniper :

system {
  ntp {
{% for ntp in lookup("system", "ntp") %}
    server {{ ntp }};
{% endfor %}
  }
  name-server {
{% for dns in lookup("system", "dns") %}
    {{ dns }};
{% endfor %}
  }
}

Voici le modèle équivalent pour Cisco IOS-XR :

{% for dns in lookup('system', 'dns') %}
domain vrf VRF-MANAGEMENT name-server {{ dns }}
{% endfor %}
!
{% for syslog in lookup('system', 'syslog') %}
logging {{ syslog }} vrf VRF-MANAGEMENT
{% endfor %}
!

Trois fonctions utiles sont fournies :

  • devices() renvoie la liste des équipements correspondant à un ensemble de conditions. Par exemple, devices("location==ussfo03", "groups==tor-bgp") renvoie la liste des routeurs de San Francisco appartenant au groupe tor-bgp. Vous pouvez également omettre l’opérateur si vous souhaitez que la valeur spécifiée soit égale à celle de l’équipement en cours. Par exemple, devices("location") renvoie les équipements situés dans le lieu actuel.
  • lookup() effectue une recherche par clé. Elle prend l’espace de noms, la clé, et optionellement, un nom d’équipement. S’il n’est pas fourni, la recherche s’effectue sur l’équipement actuel.
  • scope() renvoie les variables associées à l’équipement indiqué.

Voici comment définir des sessions iBGP entre des routeurs situés dans le même centre de données :

{% for neighbor in devices("location", "groups==edge") if neighbor != device %}
  {% for address in lookup("topology", "addresses", neighbor).loopback|tolist %}
protocols bgp group IPV{{ address|ipv }}-EDGES-IBGP {
  neighbor {{ address }} {
    description "IPv{{ address|ipv }}: iBGP to {{ neighbor }}";
  }
}
  {% endfor %}
{% endfor %}

Nous disposons également d’une base de données clé-valeur pour enregistrer les informations à réutiliser dans un autre modèle ou équipement. Ceci est très utile pour construire automatiquement des enregistrement DNS. Tout d’abord, on « capture » l’adresse IP insérée dans un modèle avec le filtre store() :

interface Loopback0
 description 'Loopback:'
 {% for address in lookup('topology', 'addresses').loopback|tolist %}
 ipv{{ address|ipv }} address {{ address|store('addresses', 'Loopback0')|ipaddr('cidr') }}
 {% endfor %}
!

Puis, nous réutilisons les valeurs stockées pour construire les enregistrements DNS en itérant sur les résultats de la fonction store()5 :

{% for device, ip, interface in store('addresses') %}
  {% set interface = interface|replace('/', '-')|replace('.', '-')|replace(':', '-') %}
  {% set name = '{}.{}'.format(interface|lower, device) %}
{{ name }}. IN {{ 'A' if ip|ipv4 else 'AAAA' }} {{ ip|ipaddr('address') }}
{% endfor %}

Les modèles sont compilés localement avec ./run-jerikan build. L’argument --limit restreint les équipements pour lesquels générer des configurations. La construction ne se fait pas en parallèle car un modèle peut dépendre des données collectées par un autre modèle. Actuellement, il faut 1 minute pour construire environ 3000 fichiers couvrant plus de 800 appareils.

Sortie de Jerikan lors de la compilation des
modèles
Sortie de Jerikan après la génération des fichiers de configuration pour six équipements

Lorsqu’une erreur se produit, une trace détaillée est affichée. Elle inclut le nom du modèle, le numéro de ligne et la valeur de toutes les variables visibles. C’est un gain de temps considérable par rapport à Ansible !

templates/opengear/config.j2:15: in top-level template code
    config.interfaces.{{ interface }}.netmask {{ infos.adddress | ipaddr("netmask") }}
        continent  = 'us'
        device     = 'con1-ag2.ussfo03.blade-group.net'
        environment = 'prod'
        host       = 'con1-ag2.ussfo03'
        infos      = {'address': '172.30.24.19/21'}
        interface  = 'wan'
        location   = 'ussfo03'
        loop       = <LoopContext 1/2>
        member     = '2'
        model      = 'cm7132-2-dac'
        os         = 'opengear'
        shorthost  = 'con1-ag2'
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = JerkianUndefined, query = 'netmask', version = False, alias = 'ipaddr'

[…]
        # Check if value is a list and parse each element
        if isinstance(value, (list, tuple, types.GeneratorType)):
            _ret = [ipaddr(element, str(query), version) for element in value]
            return [item for item in _ret if item]

>       elif not value or value is True:
E       jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'adddress'

Nous ne disposons pas de règles générales pour l’écriture des modèles. Comme pour la source de vérité, il n’est pas nécessaire de créer des modèles génériques capables de produire n’importe quelle configuration BGP. Il y a un équilibre à trouver entre la lisibilité et le fait d’éviter la duplication. Les modèles peuvent devenir effrayants et complexes : parfois, il est préférable d’écrire un filtre ou une fonction dans jerikan/jinja.py. Maîtriser Jinja2 est un très bon investissement. Prenez le temps de parcourir nos modèles, car certains d’entre eux présentent des fonctionnalités intéressantes.

Validation#

Optionellement, chaque fichier de configuration peut être validé par un script dans le répertoire checks/. Jerikan utilise la variable checks dans l’espace de noms build pour connaître les tests à appliquer :

$ ./run-jerikan lookup edge1.ussfo03.blade-group.net build checks
- description: Juniper configuration file syntax check
  script: checks/junoser
  cache:
    input: config.txt
    output: config-set.txt
- description: check YAML data
  script: checks/data.yaml
  cache: data.yaml

Dans l’exemple ci-dessus, checks/junoser est exécuté s’il y a une modification du fichier généré config.txt. Il produit également une version transformée du fichier de configuration qui est plus facile à interfacer avec l’utilitaire diff. Junoser vérifie un fichier de configuration Junos en utilisant la définition du schéma XML pour Netconf6. En cas d’erreur, Jerikan affiche :

jerikan/build.py:127: RuntimeError
-------------- Captured syntax check with Junoser call --------------
P: checks/junoser edge2.ussfo03.blade-group.net
C: /app/jerikan
O:
E: Invalid syntax:  set system syslog archive size 10m files 10 word-readable
S: 1

Intégration continue avec GitLab#

L’étape suivante consiste à compiler les modèles en utilisant un système d’intégration continue. Comme nous utilisons GitLab, Jerikan contient avec un fichier .gitlab-ci.yml. Lorsque nous devons effectuer un changement, nous créons une branche dédiée et une demande de fusion. GitLab compile les modèles en utilisant le même environnement que celui que nous utilisons sur nos ordinateurs portables et les stocke dans un artefact.

Demande de fusion dans
GitLab
Demande de fusion pour ajouter un nouveau port sur USSFO03. Les modèles sont compilés avec succès mais un autre membre de l'équipe doit approuver le changement avant de pouvoir l'intégrer.

Avant d’approuver une fusion, un autre membre de l’équipe étudie les changements au niveau du code mais aussi les différences au niveau des fichiers générés :

Différences sur les fichiers
générés
Le changement inclut la configuration d'un port sur un équipement Juniper, l'ajout des enregistrements DNS et les mises à jour de NetBox pour les nouvelles adresses IP.

Ansible#

Une fois que Jerikan a construit les fichiers de configuration, Ansible prend le relais. Il est également livré sous la forme d’une image Docker pour éviter la maintenance d’un environnement virtuel Python et s’assurer que nous utilisons tous les mêmes versions.

Inventaire#

Jerikan a généré un fichier d’inventaire. Il contient tous les équipements gérés, des variables définies pour chacun d’eux et les groupes convertis en groupes Ansible :

ob1-n1.sk1.blade-group.net ansible_host=172.29.15.12 ansible_user=blade ansible_connection=network_cli ansible_network_os=ios
ob2-n1.sk1.blade-group.net ansible_host=172.29.15.13 ansible_user=blade ansible_connection=network_cli ansible_network_os=ios
ob1-n1.ussfo03.blade-group.net ansible_host=172.29.15.12 ansible_user=blade ansible_connection=network_cli ansible_network_os=ios
none ansible_connection=local

[oob]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net

[os-ios]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net

[model-c2960s]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net

[location-sk1]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net

[location-ussfo03]
ob1-n1.ussfo03.blade-group.net

[in-sync]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net
none

in-sync est un groupe spécial pour les équipements dont la configuration doit correspondre à la configuration générée. Quotidiennement et sans surveillance, Ansible devrait être capable de pousser les configurations vers ce groupe. L’objectif à moyen terme est de couvrir tous les équipements.

none est un équipement spécial pour les tâches non liées à un hôte spécifique. Cela inclut la synchronisation de NetBox, des objets IRR et du DNS, la mise à jour de la RPKI et la construction des fichiers de géolocalisation.

Playbook#

Nous utilisons un unique playbook pour tous les équipements. Il est décrit dans le fichier ansible/playbooks/site.yaml. En voici une version raccourcie :

- hosts: adm-gateway:!done
  strategy: mitogen_linear
  roles:
    - blade.linux
    - blade.adm-gateway
    - done

- hosts: os-linux:!done
  strategy: mitogen_linear
  roles:
    - blade.linux
    - done

- hosts: os-junos:!done
  gather_facts: false
  roles:
    - blade.junos
    - done

- hosts: os-opengear:!done
  gather_facts: false
  roles:
    - blade.opengear
    - done

- hosts: none:!done
  gather_facts: false
  roles:
    - blade.none
    - done

Un hôte n’exécute qu’un seul des rôles. Par exemple, un équipement Junos exécute le rôle blade.junos. Une fois qu’un rôle a été exécuté, l’équipement est ajouté au groupe done et les autres rôles sont ignorés.

Le playbook peut être exécuté avec les fichiers de configuration générés par GitLab en utilisant la commande ./run-ansible-gitlab. Elle utilise Docker pour exposer la commande ansible-playbook et elle accepte les mêmes arguments. Pour simuler un déploiement de la configuration sur les équipements du centre de données SK1, nous utilisons :

$ ./run-ansible-gitlab playbooks/site.yaml --limit='edge:&location-sk1' --diff --check
[…]
PLAY RECAP *************************************************************
edge1.sk1.blade-group.net  : ok=6    changed=0    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0
edge2.sk1.blade-group.net  : ok=5    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

Nous avons quelques règles pour écrire les rôles :

  • --check doit détecter si un changement est nécessaire,
  • --diff doit fournir une visualisation des changements prévus,
  • --check et --diff ne doivent rien afficher s’il n’y a rien à changer,
  • écrire un module sur mesure pour nos besoins est une solution valable,
  • toute la configuration de l’équipement est gérée7,
  • les secrets doivent être stockés dans Vault,
  • les modèles doivent être évités car nous avons Jerikan pour cela,
  • les tâches doivent être réutilisées et non dupliquées8.

Nous évitons d’utiliser des collections d’Ansible Galaxy, à l’exception de celles permettant de se connecter et d’interagir avec les équipements constructeur, comme la collection cisco.iosxr. La qualité des collections d’Ansible Galaxy est assez aléatoire et cela représente un effort de maintenance supplémentaire. Nous préférons écrire des rôles adaptés à nos besoins. Les collections que nous utilisons sont dans ci/ansible/ansible-galaxy.yaml. Nous utilisons Mitogen pour accélérer considérablement les exécutions d’Ansible sur les hôtes Linux.

Nous disposons également de quelques playbooks à des fins opérationnelles : mise à niveau d’une version, isolation d’un routeur Internet, etc. Nous avions également en tête d’ajouter une étape de validation dans les rôles : est-ce que toutes les sessions BGP sont opérationnelles ? Cela aurait permis de valider un déploiement et de revenir rapidement en arrière en cas de problèmes.

Actuellement, nos playbooks sont exécutés à partir de nos ordinateurs portables. Pour assurer le suivi des exécutions, nous utilisons ARA. Un essai hebdomadaire sur les équipements du groupe in-sync fournit également un tableau de bord sur les hôtes sur lesquels nous devons faire converger la configuration.

Données de configuration et modèles#

Nous livrons Jerikan avec des données et des modèles correspondant à la configuration de nos centres de données USSFO03 et SK1. Ils n’existent plus mais, nous vous le promettons, tout ceci était utilisé en production à une époque ! 😢

Architecture réseau pour les centres de données
Blade
La dernière itération de notre infrastructure réseau pour SK1, USSFO03 et les futurs centres de données. Le réseau de production utilise BGPttH en utilisant une topologie Clos. Le réseau hors bande utilise une conception L2 simple, avec le protocole STP, ainsi qu'un ensemble de serveurs de console.

Vous trouverez la configuration pour :

Nos routeurs Internet
Certains fonctionnent sur Junos, comme edge2.ussfo03, les autres sur IOS-XR, comme edge1.sk1. Les fonctionnalités implémentées sont similaires dans les deux cas et nous pouvons échanger l’un contre l’autre. Il y a la configuration BGP pour les transits, les peerings et les IX ainsi que les politiques BGP associées. PeeringDB est interrogé pour obtenir le nombre maximum de préfixes à accepter. bgpq3 et un IRRd conteneurisé aident à filtrer les routes reçues. Un pare-feu est également présent. IPv4 et IPv6 sont tous deux configurés.
Notre fabrique BGP
BGP est utilisé à l’intérieur du centre de données9 et s’étend jusqu’aux les hôtes physiques. La configuration est automatiquement dérivée de l’emplacement du dispositif et du numéro de port10. Les switchs en haut de chaque rack utilisent des sessions BGP passives pour les ports vers les serveurs. Ils exposent également un réseau spécifique pour permettre de démarrer en utilisant DHCP et PXE. Ils font également office de serveur DHCP. La conception est multivendeur : certains équipements fonctionnent sous Cumulus Linux, comme to1-p1.ussfo03, tandis que d’autres fonctionnent sous Junos, comme to1-p2.ussfo03.
Notre réseau hors bande
Nous utilisons des commutateurs Cisco Catalyst 2960 pour construire un réseau hors bande L2. Pour assurer la redondance et économiser quelques sous sur le câblage, nous construisons des petites boucles et exploitons le protocole STP. Regardez ob1-p1.ussfo03. Le réseau est connecté de manière redondante à des serveurs d’accès. Nous utilisons également des équipements OpenGear pour les accès console. Regardez con1-n1.ussfo03.
Nos passerelles d’accès
Ces serveurs Linux ont plusieurs objectifs : passerelles SSH, connexion de secours, accès direct au réseau hors bande, installation automatisée des équipements réseau11, accès Internet pour les flux administratifs, centralisation des serveurs de console à l’aide de Conserver et mise à disposition d’une API pour l’autoconfiguration des sessions BGP pour les serveurs physiques. Ce sont les premiers serveurs installés dans un nouveau centre de données et ils sont utilisés pour mettre en place tout le reste. Regardez à la fois les fichiers générés et les tâches Ansible associées.

Mise à jour (10.2021)

J’ai donné une présentation de Jerikan et Ansible durant le FRnOG #34. Elle reprend essentiellement le contenu de cet article.


  1. Ansible ne fournit même pas un numéro de ligne lorsqu’il y a une erreur dans un modèle. Il faut parfois trouver le problème par bissection.

    $ ansible --version
    ansible 2.10.8
    […]
    $ cat test.j2
    Hello {{ name }}!
    $ ansible all -i localhost, \
    >  --connection=local \
    >  -m template \
    >  -a "src=test.j2 dest=test.txt"
    localhost | FAILED! => {
        "changed": false,
        "msg": "AnsibleUndefinedVariable: 'name' is undefined"
    }
    
    ↩︎
  2. Ce sont les mêmes concepts que dans Hiera, le système clés-valeurs hiérarchique de Puppet. Au départ, nous utilisions Jerakia, un système similaire indépendant qui expose une interface HTTP REST. Cependant, le surcoût en temps d’une recherche est trop important pour notre utilisation. Jerikan implémente la même fonctionnalité dans une fonction Python↩︎

  3. Ce fichier pourrait également être utilisé pour définir un schéma validant la source de vérité et détecter les erreurs plus tôt. Il pourrait aussi indiquer des valeurs par défaut pour éviter de déléguer cette tâche aux modèles. Yamale pourrait être une première brique pour cette fonctionnalité. ↩︎

  4. La liste des filtres est placée en dur dans jerikan/jinja.py. C’est une conséquence du fait que nous ne maintenons pas Jerikan comme un logiciel indépendant. ↩︎

  5. C’est un peu confus. Nous avons un filtre store() et une fonction store(). Dans Jinja2, les filtres et les fonctions vivent dans deux espaces de noms distincts. ↩︎

  6. Nous utilisons une version modifiée pour pouvoir valider nos configurations et exposer un service HTTP pour réduire le temps de validation. ↩︎

  7. Il y a une tendance dans l’automatisation des réseaux à se limiter à un sous-ensemble de configuration, par exemple en ayant un playbook pour créer une nouvelle session BGP. Nous pensons que c’est une mauvaise approche : avec le temps, votre configuration ne correspondra pas à l’état attendu, notamment les changements faits à la main ne seront pas détectés. ↩︎

  8. Voir ansible/roles/blade.linux/tasks/firewall.yaml et ansible/roles/blade.linux/tasks/interfaces.yaml. Ils sont destinés à être appelés lorsque cela est nécessaire, en utilisant import_role↩︎

  9. Nous avons également quelques centres de données qui utilisent BGP EVPN VXLAN avec des équipements Juniper. Comme ils sont encore en production aujourd’hui, nous n’avons pas inclus cette fonctionnalité mais nous pourrions la publier à l’avenir. ↩︎

  10. Rétrospectivement, ce n’est peut-être pas une bonne idée, sauf si vous êtes à peu près sûr que tout est immuable. Ce n’était pas notre cas. Nous pensons maintenant qu’il aurait été plus judicieux d’attribuer un préfixe à chaque appareil et de le consigner dans la source de vérité. ↩︎

  11. Les équipements n’utilisant pas Linux sont mis à jour et configurés sans interaction de notre part. Les périphériques Cumulus Linux sont automatiquement mis à jour lors de leur première installation mais la configuration finale doit être poussée en utilisant Ansible : nous ne voulions pas dupliquer le processus de configuration en utilisant un autre outil. ↩︎