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

ex-Blade Network Team

Il existe de nom­breuses res­sources pour l’au­to­ma­ti­sa­tion du ré­seau avec An­sible. La plu­part d’entre elles n’ex­posent que les pre­mières étapes ou se li­mitent à un champ d’ap­pli­ca­tion ré­duit. Elles donnent peu d’in­di­ca­tions sur la ma­nière d’al­ler plus loin. Les en­vi­ron­ne­ments ré­seau réels peuvent être vastes, po­ly­va­lents, hé­té­ro­gènes et rem­plis d’ex­cep­tions. L’ab­sence d’exemples concrets de dé­ploie­ments An­sible, contrai­re­ment à Pup­pet et SaltStack, conduit de nom­breuses équipes à éla­bo­rer des so­lu­tions fra­giles et in­complètes.

Nous avons pu­blié sous une li­cence libre notre ten­ta­tive pour pal­lier ce manque :

  • Je­ri­kan, un ou­til pour construire des fi­chiers de confi­gu­ra­tion à par­tir d’une source de vé­rité unique et de mo­dèles Jinja2, ainsi que son in­té­gra­tion dans GitLab ;
  • un play­book An­sible pour dé­ployer ces fi­chiers de confi­gu­ra­tion sur les équi­pe­ments réseau ;
  • les don­nées de confi­gu­ra­tion et les mo­dèles pour nos centres de don­nées, au­jourd’hui fer­més, à San Fran­cisco et en Co­rée du Sud, cou­vrant de nom­breux four­nis­seurs (Fa­ce­book Wedge 100, Dell S4048 et S6010, Ju­ni­per QFX 5110, Ju­ni­per QFX 10002, Cisco ASR 9001, Cisco Ca­ta­lyst 2960, ser­veurs de console Open­gear et ser­veurs Li­nux) et de nom­breuses fonc­tion­na­li­tés (ap­pro­vi­sion­ne­ment, rou­tage BGP vers l’hôte, rou­tage In­ter­net, ré­seau d’ad­mi­nis­tra­tion sé­paré, confi­gu­ra­tion DNS, in­té­gra­tion avec NetBox et les IRR).

Voici une démo pour l’ajout d’un nou­veau peering:

Ce tra­vail est l’œuvre col­lec­tive de Cé­dric Has­coët, Jean-Chris­tophe Le­gatte, Loïc Pail­has, Sé­bas­tien Hur­tel, Tcha­del Icard et Vincent Ber­nat. Nous sommes l’équipe ré­seau de Blade, une so­ciété fran­çaise ex­ploi­tant Sha­dow, un pro­duit de PC dans les nuages. En mai 2021, notre so­ciété a été ra­che­tée par Oc­tave Klaba et l’in­fra­struc­ture est en cours de trans­fert vers OVHcloud, sau­vant ainsi Sha­dow en tant que pro­duit, mais ren­dant notre équipe su­perflue.

Notre ré­seau comp­tait en­vi­ron 800 équi­pe­ments, ré­par­tis sur 10 centres de don­nées avec plus de 2,5 Tbps de bande pas­sante dis­po­nible en sor­tie. Le ma­té­riel pu­blié est donc un exemple sub­stan­tiel de ges­tion d’un ré­seau de taille moyenne à l’aide d’An­sible. Nous avons laissé de côté la confi­gu­ra­tion de nos centres de don­nées plus an­ciens pour rendre le ré­sul­tat fi­nal plus li­sible, tout en conser­vant suf­fi­sam­ment de sub­stance pour ne pas en faire un exemple trivial.

Jerikan#

Le pre­mier com­po­sant est Je­ri­kan. En en­trée, il prend une liste d’équi­pe­ments, des don­nées de confi­gu­ra­tion, des mo­dèles et des scripts de va­li­da­tion. Il gé­nère un en­semble de fi­chiers de confi­gu­ra­tion pour chaque ap­pa­reil. An­sible pour­rait ef­fec­tuer cette tâche, mais il pré­sente les li­mi­ta­tions sui­vantes concer­nant la com­pi­la­tion des modèles :

  • il est lent ;
  • les er­reurs sont dif­fi­ciles à corriger1 ;
  • la hié­rar­chie de re­cherche d’une va­riable est rigide.
Entrées et sorties de Jerikan
Entrées et sorties de Jerikan

Si vous vou­lez suivre les exemples, il vous suf­fit d’avoir ins­tallé Do­cker et Do­cker Com­pose. Clo­nez le dé­pôt et vous êtes prêt !

Source de vérité#

Nous uti­li­sons des fi­chiers YAML, ver­sion­nés avec Git, comme source de vé­rité unique au lieu d’uti­li­ser une base de don­nées, comme NetBox, ou un mé­lange de base de don­nées et de fi­chiers texte. Cela offre de nom­breux avantages :

  • cha­cun peut uti­li­ser son édi­teur de texte préféré ;
  • l’équipe pré­pare les chan­ge­ments dans des branches ;
  • l’équipe éva­lue les chan­ge­ments en uti­li­sant des de­mandes de fusion ;
  • les de­mandes de fu­sion pré­sentent l’im­pact sur les fi­chiers de confi­gu­ra­tion générés ;
  • le re­tour à un état an­té­rieur est facile ;
  • c’est rapide.

Mise à jour (11.2021)

Pour un ar­gu­men­taire plus dé­taillé sur le choix de Git par rap­port à NetBox, li­sez « Git comme source de vé­rité pour l’au­to­ma­ti­sa­tion du réseau »

Le pre­mier fi­chier est devices.​yaml. Il contient la liste des équi­pe­ments. Le deuxième fi­chier est classifier.​yaml. Il dé­fi­nit un en­semble de clés-va­leurs pour chaque équi­pe­ment. Il est uti­lisé dans les mo­dèles et pour re­cher­cher les don­nées as­so­cié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’équi­pe­ment est com­paré à une liste d’ex­pres­sions ré­gu­lières et chaque cor­res­pon­dance contri­bue au ré­sul­tat. Pour to1-p1.​sk1.​blade-group.​net, le sous-en­semble sui­vant de classifier.​yaml dé­fi­nit ses va­riables :

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 troi­sième fi­chier est searchpaths.py. Il dé­crit les ré­per­toires à exa­mi­ner pour trou­ver une va­riable. Une fonc­tion Py­thon four­nit une liste de che­mins dans data/ se­lon les va­riables as­so­ciées à l’équi­pe­ment. Voici une ver­sion sim­plifié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é­fi­ni­tion, les don­nées pour to1-p1.​sk1.​blade-group.​net sont re­cher­chées dans les che­mins 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 va­riables sont li­mi­tées à un es­pace de noms à spé­ci­fier lors d’une re­cherche. Nous uti­li­sons les suivants :

  • system pour les comptes, DNS, ser­veurs syslog ;
  • topology pour les ports, in­ter­faces, adresses IP, sous-réseaux ;
  • bgp pour la confi­gu­ra­tion de BGP ;
  • build pour les mo­dèles et les scripts de va­lidation ;
  • apps pour les va­riables d’ap­plication.

Lors de la re­cherche d’une va­riable dans un es­pace de noms donné, Je­ri­kan re­cherche un fi­chier YAML nommé d’après l’es­pace de noms dans chaque ré­per­toire re­tourné par la fonc­tion de re­cherche. Par exemple, si nous re­cher­chons une va­riable pour to1-p1.​sk1.​blade-group.​net dans l’es­pace de noms bgp, les fi­chiers YAML sui­vants sont trai­té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 re­cherche s’ar­rête à la pre­mière cor­res­pondance.

Le fi­chier schema.​yaml nous per­met d’ou­tre­pas­ser ce der­nier com­por­te­ment en fu­sion­nant dans cer­tains cas les dic­tion­naires et les ta­bleaux cor­res­pon­dants3. Voici un ex­trait de ce fi­chier pour l’es­pace de noms topology :

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

La der­nière fonc­tion­na­lité de la source de vé­rité est la pos­si­bi­lité d’uti­li­ser des mo­dèles Jinja2 pour les clefs et les va­leurs 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

Cher­cher netbox dans l’es­pace de noms system pour to1-p2.​ussfo03.​blade-group.​net pro­duit le ré­sul­tat 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 fonc­tionne aussi pour des don­né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 re­tourne le ré­sul­tat 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 pla­cer des don­nées dans la source de vé­rité, nous uti­li­sons les règles suivantes :

  1. Ne pas se répéter.
  2. Pré­fé­rer l’em­pla­ce­ment le plus spé­ci­fique sans en­freindre la pre­mière règle.
  3. Uti­li­ser avec par­ci­mo­nie les mo­dèles, prin­ci­pa­le­ment pour res­pec­ter les règles pré­cédentes.
  4. Li­mi­ter le mo­dèle de don­nées à ce qui est né­ces­saire pour votre cas d’uti­lisation.

La pre­mière règle est as­sez im­por­tante. Par exemple, lorsque vous spé­ci­fiez des adresses IP pour une liai­son point à point, ne spé­ci­fiez qu’un côté et dé­dui­sez l’autre va­leur via les mo­dèles. La der­nière règle si­gni­fie qu’il est in­utile de re­pro­duire un mo­dèle BGP YANG pour spé­ci­fier les pairs et les po­li­tiques 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 mo­dèles à com­pi­ler pour chaque équi­pe­ment est sto­ckée dans la source de vé­rité, sous l’es­pace 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 mo­dèles uti­lisent Jinja2. C’est le même mo­teur que pour An­sible. Je­ri­kan four­nit quelques filtres per­son­na­li­sés mais réuti­lise éga­le­ment cer­tains des filtres d’An­sible4, no­tam­ment ipaddr. Voici un ex­trait de templates/​junos/​base.j2 pour confi­gu­rer les ser­veurs DNS et NTP sur les équi­pe­ments Juniper :

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

Voici le mo­dèle équi­valent 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 fonc­tions utiles sont fournies :

  • devices() ren­voie la liste des équi­pe­ments cor­res­pon­dant à un en­semble de condi­tions. Par exemple, devices("location==​ussfo03", "groups==​tor-bgp") ren­voie la liste des rou­teurs de San Fran­cisco ap­par­te­nant au groupe tor-bgp. Vous pou­vez éga­le­ment omettre l’opé­ra­teur si vous sou­hai­tez que la va­leur spé­ci­fiée soit égale à celle de l’équi­pe­ment en cours. Par exemple, devices("location") ren­voie les équi­pe­ments si­tués dans le lieu actuel.
  • lookup() ef­fec­tue une re­cherche par clé. Elle prend l’es­pace de noms, la clé, et op­tion­nel­le­ment, un nom d’équi­pe­ment. S’il n’est pas fourni, la re­cherche s’ef­fec­tue sur l’équi­pe­ment actuel.
  • scope() ren­voie les va­riables as­so­ciées à l’équi­pe­ment indiqué.

Voici com­ment dé­fi­nir des ses­sions iBGP entre des rou­teurs si­tué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 dis­po­sons éga­le­ment d’une base de don­nées clé-va­leur pour en­re­gis­trer les in­for­ma­tions à réuti­li­ser dans un autre mo­dèle ou équi­pe­ment. Ceci est très utile pour construire au­to­ma­ti­que­ment des en­re­gis­tre­ment DNS. Tout d’abord, on « cap­ture » l’adresse IP in­sé­rée dans un mo­dè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éuti­li­sons les va­leurs sto­ckées pour construire les en­re­gis­tre­ments DNS en ité­rant sur les ré­sul­tats de la fonc­tion 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 mo­dèles sont com­pi­lés lo­ca­le­ment avec ./run-jerikan build. L’ar­gu­ment --limit res­treint les équi­pe­ments pour les­quels gé­né­rer des confi­gu­ra­tions. La construc­tion ne se fait pas en pa­ral­lèle car un mo­dèle peut dé­pendre des don­nées col­lec­tées par un autre mo­dèle. Ac­tuel­le­ment, il faut 1 mi­nute pour construire en­vi­ron 3000 fi­chiers cou­vrant plus de 800 ap­pareils.

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 er­reur se pro­duit, une trace dé­taillée est af­fi­chée. Elle in­clut le nom du mo­dèle, le nu­méro de ligne et la va­leur de toutes les va­riables vi­sibles. C’est un gain de temps consi­dé­rable par rap­port à 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 dis­po­sons pas de règles gé­né­rales pour l’écri­ture des mo­dèles. Comme pour la source de vé­rité, il n’est pas né­ces­saire de créer des mo­dèles gé­né­riques ca­pables de pro­duire n’im­porte quelle confi­gu­ra­tion BGP. Il y a un équi­libre à trou­ver entre la li­si­bi­lité et le fait d’évi­ter la du­pli­ca­tion. Les mo­dèles peuvent de­ve­nir ef­frayants et com­plexes : par­fois, il est pré­fé­rable d’écrire un filtre ou une fonc­tion dans jerikan/​jinja.py. Maî­tri­ser Jinja2 est un très bon in­ves­tis­se­ment. Pre­nez le temps de par­cou­rir nos mo­dèles, car cer­tains d’entre eux pré­sentent des fonc­tion­na­li­tés in­té­ressantes.

Validation#

Op­tio­nel­le­ment, chaque fi­chier de confi­gu­ra­tion peut être va­lidé par un script dans le ré­per­toire checks/. Je­ri­kan uti­lise la va­riable checks dans l’es­pace de noms build pour connaître les tests à ap­pliquer :

$ ./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-des­sus, checks/​junoser est exé­cuté s’il y a une mo­di­fi­ca­tion du fi­chier gé­néré config.​txt. Il pro­duit éga­le­ment une ver­sion trans­for­mée du fi­chier de confi­gu­ra­tion qui est plus fa­cile à in­ter­fa­cer avec l’uti­li­taire diff. Ju­no­ser vé­ri­fie un fi­chier de confi­gu­ra­tion Ju­nos en uti­li­sant la dé­fi­ni­tion du schéma XML pour Net­conf6. En cas d’er­reur, Je­ri­kan 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 sui­vante consiste à com­pi­ler les mo­dèles en uti­li­sant un sys­tème d’in­té­gra­tion conti­nue. Comme nous uti­li­sons GitLab, Je­ri­kan contient un fi­chier .gitlab-ci.​yml. Lorsque nous de­vons ef­fec­tuer un chan­ge­ment, nous créons une branche dé­diée et une de­mande de fu­sion. GitLab com­pile les mo­dèles en uti­li­sant le même en­vi­ron­ne­ment que ce­lui que nous uti­li­sons sur nos or­di­na­teurs por­tables 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’ap­prou­ver une fu­sion, un autre membre de l’équipe étu­die les chan­ge­ments au ni­veau du code mais aussi les dif­fé­rences au ni­veau des fi­chiers 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 Je­ri­kan a construit les fi­chiers de confi­gu­ra­tion, An­sible prend le re­lais. Il est éga­le­ment li­vré sous la forme d’une image Do­cker pour évi­ter la main­te­nance d’un en­vi­ron­ne­ment vir­tuel Py­thon et s’as­su­rer que nous uti­li­sons tous les mêmes versions.

Inventaire#

Je­ri­kan a gé­néré un fi­chier d’in­ven­taire. Il contient tous les équi­pe­ments gé­rés, des va­riables dé­fi­nies pour cha­cun d’eux et les groupes conver­tis 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 équi­pe­ments dont la confi­gu­ra­tion doit cor­res­pondre à la confi­gu­ra­tion gé­né­rée. Quo­ti­dien­ne­ment et sans sur­veillance, An­sible de­vrait être ca­pable de pous­ser les confi­gu­ra­tions vers ce groupe. L’ob­jec­tif à moyen terme est de cou­vrir tous les équi­pements.

none est un équi­pe­ment spé­cial pour les tâches non liées à un hôte spé­ci­fique. Cela in­clut la syn­chro­ni­sa­tion de NetBox, des ob­jets IRR et du DNS, la mise à jour de la RPKI et la construc­tion des fi­chiers de géo­lo­ca­lisation.

Playbook#

Nous uti­li­sons un unique play­book pour tous les équi­pe­ments. Il est dé­crit dans le fi­chier ansible/​playbooks/​site.​yaml. En voici une ver­sion rac­courcie :

- 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 équi­pe­ment Ju­nos exé­cute le rôle blade.​junos. Une fois qu’un rôle a été exé­cuté, l’équi­pe­ment est ajouté au groupe done et les autres rôles sont ignorés.

Le play­book peut être exé­cuté avec les fi­chiers de confi­gu­ra­tion gé­né­rés par GitLab en uti­li­sant la com­mande ./run-ansible-gitlab. Elle uti­lise Do­cker pour ex­po­ser la com­mande ansible-playbook et elle ac­cepte les mêmes ar­gu­ments. Pour si­mu­ler un dé­ploie­ment de la confi­gu­ra­tion sur les équi­pe­ments du centre de don­né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é­tec­ter si un chan­ge­ment est né­cessaire,
  • --diff doit four­nir une vi­sua­li­sa­tion des chan­ge­ments prévus,
  • --check et --diff ne doivent rien af­fi­cher s’il n’y a rien à changer,
  • écrire un mo­dule sur me­sure pour nos be­soins est une so­lu­tion valable,
  • toute la confi­gu­ra­tion de l’équi­pe­ment est gérée7,
  • les se­crets doivent être sto­ckés dans Vault,
  • les mo­dèles doivent être évi­tés car nous avons Je­ri­kan pour cela,
  • les tâches doivent être réuti­li­sées et non du­pliquées8.

Nous évi­tons d’uti­li­ser des col­lec­tions d’An­sible Ga­laxy, à l’ex­cep­tion de celles per­met­tant de se connec­ter et d’in­ter­agir avec les équi­pe­ments construc­teur, comme la col­lec­tion cisco.​iosxr. La qua­lité des col­lec­tions d’An­sible Ga­laxy est as­sez aléa­toire et cela re­pré­sente un ef­fort de main­te­nance sup­plé­men­taire. Nous pré­fé­rons écrire des rôles adap­tés à nos be­soins. Les col­lec­tions que nous uti­li­sons sont dans ci/​ansible/​ansible-galaxy.​yaml. Nous uti­li­sons Mi­to­gen pour ac­cé­lé­rer consi­dé­ra­ble­ment les exé­cu­tions d’An­sible sur les hôtes Linux.

Nous dis­po­sons éga­le­ment de quelques play­books à des fins opé­ra­tion­nelles : mise à ni­veau d’une ver­sion, iso­la­tion d’un rou­teur In­ter­net, etc. Nous avions éga­le­ment en tête d’ajou­ter une étape de va­li­da­tion dans les rôles : est-ce que toutes les ses­sions BGP sont opé­ra­tion­nelles ? Cela au­rait per­mis de va­li­der un dé­ploie­ment et de re­ve­nir ra­pi­de­ment en ar­rière en cas de problèmes.

Ac­tuel­le­ment, nos play­books sont exé­cu­tés à par­tir de nos or­di­na­teurs por­tables. Pour as­su­rer le suivi des exé­cu­tions, nous uti­li­sons ARA. Un es­sai heb­do­ma­daire sur les équi­pe­ments du groupe in-sync four­nit éga­le­ment un ta­bleau de bord sur les hôtes sur les­quels nous de­vons faire conver­ger la confi­guration.

Données de configuration et modèles#

Nous li­vrons Je­ri­kan avec des don­nées et des mo­dèles cor­res­pon­dant à la confi­gu­ra­tion de nos centres de don­nées USSFO03 et SK1. Ils n’existent plus mais, nous vous le pro­met­tons, tout ceci était uti­lisé en pro­duc­tion à 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 trou­ve­rez la confi­gu­ra­tion pour :

Nos rou­teurs In­ter­net
Cer­tains fonc­tionnent sur Ju­nos, comme edge2.ussfo03, les autres sur IOS XR, comme edge1.sk1. Les fonc­tion­na­li­tés im­plé­men­tées sont si­mi­laires dans les deux cas et nous pou­vons échan­ger l’un contre l’autre. Il y a la confi­gu­ra­tion BGP pour les tran­sits, les pee­rings et les IX ainsi que les po­li­tiques BGP as­so­ciées. PeeringDB est in­ter­rogé pour ob­te­nir le nombre maxi­mum de pré­fixes à ac­cep­ter. bgpq3 et un IRRd conte­neu­risé aident à fil­trer les routes re­çues. Un pare-feu est éga­le­ment pré­sent. IPv4 et IPv6 sont tous deux configurés.
Notre fa­brique BGP
BGP est uti­lisé à l’in­té­rieur du centre de don­nées9 et s’étend jusqu’aux hôtes phy­siques. La confi­gu­ra­tion est au­to­ma­ti­que­ment dé­ri­vée de l’em­pla­ce­ment du dis­po­si­tif et du nu­méro de port10. Les switchs en haut de chaque rack uti­lisent des ses­sions BGP pas­sives pour les ports vers les ser­veurs. Ils ex­posent éga­le­ment un ré­seau spé­ci­fique pour per­mettre de dé­mar­rer en uti­li­sant DHCP et PXE. Ils font éga­le­ment of­fice de ser­veur DHCP. La concep­tion est mul­ti­ven­deur : cer­tains équi­pe­ments fonc­tionnent sous Cu­mu­lus Li­nux, comme to1-p1.ussfo03, tan­dis que d’autres fonc­tionnent sous Ju­nos, comme to1-p2.ussfo03.
Notre ré­seau hors bande
Nous uti­li­sons des com­mu­ta­teurs Cisco Ca­ta­lyst 2960 pour construire un ré­seau hors bande L2. Pour as­su­rer la re­don­dance et éco­no­mi­ser quelques sous sur le câ­blage, nous construi­sons des pe­tites boucles et ex­ploi­tons le pro­to­cole STP. Re­gar­dez ob1-p1.ussfo03. Le ré­seau est connecté de ma­nière re­don­dante à des ser­veurs d’ac­cès. Nous uti­li­sons éga­le­ment des équi­pe­ments OpenGear pour les ac­cès console. Re­gar­dez con1-n1.ussfo03.
Nos pas­se­relles d’ac­cès
Ces ser­veurs Li­nux ont plu­sieurs ob­jec­tifs : pas­se­relles SSH, connexion de se­cours, ac­cès di­rect au ré­seau hors bande, ins­tal­la­tion au­to­ma­ti­sée des équi­pe­ments ré­seau11, ac­cès In­ter­net pour les flux ad­mi­nis­tra­tifs, cen­tra­li­sa­tion des ser­veurs de console à l’aide de Conser­ver et mise à dis­po­si­tion d’une API pour l’au­to­con­fi­gu­ra­tion des ses­sions BGP pour les ser­veurs phy­siques. Ce sont les pre­miers ser­veurs ins­tal­lés dans un nou­veau centre de don­nées et ils sont uti­li­sés pour mettre en place tout le reste. Re­gar­dez à la fois les fi­chiers gé­né­rés et les tâches An­sible as­sociées.

Mise à jour (10.2021)

J’ai donné une pré­sen­ta­tion de Je­ri­kan et An­sible du­rant le FRnOG #34. Elle re­prend es­sen­tiel­le­ment le contenu de cet article.

Mise à jour (10.2026)

Cet ar­ticle a été briè­ve­ment dis­cuté sur Ha­cker News.