Evolución de la validación de red en Hostinger (Parte 2)

Evolución de la validación de red en Hostinger (Parte 2)

En nuestra publicación anterior , discutimos cómo Hostinger comenzó a usar la validación de red antes de lanzarse. Al implementar una validación de red para nuestra red central, mantuvimos un control completo del funcionamiento de la red a escala. 

Entre otras cosas, la publicación resume el uso de Suzieq para validar aspectos clave de la red. Esta vez, entraremos en más detalles sobre cómo Hostinger usa Suzieq para realizar la validación de la red y una descripción más detallada de Batfish durante la evaluación. 

Para darte algunos números, tenemos 9 centros de datos (DC) en todo el mundo y más próximamente. Cada centro de datos es diferente en términos de tamaño: puede abarcar desde un par de racks hasta decenas de racks por centro de datos. El uso de la automatización además de eso no genera ninguna diferencia considerable, a pesar de la rapidez con la que se introducen los cambios en la producción. Para el cliente final, el uso de los servicios proporcionados por una empresa que continuamente contribuye y realiza la validación de la red se suma a la construcción de la base de la confianza y la confiabilidad de los productos de Hostinger.

Suzieq

Sondeo en ejecución continua Vs. Snapshot

Una de las primeras decisiones que tuvimos que tomar con cualquier herramienta que usamos para realizar la validación de la red es si ejecutar el sondeo en modo independiente o en modo de ejecución continua.

Un sondeo en funcionamiento continuo tiene un costo de ingeniería más alto, sin importar la herramienta, aunque es el enfoque correcto. En este enfoque, el sondeador debe estar ejecutándose todo el tiempo y debe estar altamente disponible, es decir, el sondeador debe recuperarse de las fallas… 

Ejecutar el sondeo en modo “Snapshot” es trivial desde una perspectiva de mantenibilidad. Se puede ejecutar de forma independiente en cualquier entorno: en una máquina local (estación de trabajo) o en CI/CD sin necesidad de tener en mente ningún servicio en ejecución. En nuestro caso, sondeamos los datos una vez y luego ejecutamos las pruebas de Python. En Hostinger, tenemos implementaciones repartidas en muchas regiones geográficas: Asia, Europa, EE. UU. y tenemos varios países en desarrollo en cada una de estas regiones. Usamos Jenkins para nuestra canalización de CI/CD. Para asegurarnos de que ejecutamos las mismas pruebas en todas las regiones, lanzamos varios esclavos Jenkins. Si hubiéramos utilizado un sondeo en funcionamiento continuo, el costo de ingeniería habría sido más alto de configurar y mantener.  

Un ejemplo de ejecución de sq-poller (que se ejecuta en un bucle para cada DC o región).

for DC in "${DATACENTERS[@]}"
do
  python generate_hosts_for_suzieq.py --datacenter "$DC"
  ../bin/sq-poller --devices-file "hosts-$DC.yml" \
    --ignore-known-hosts \
    --run-once gather \
    --exclude-services devconfig
  ../bin/sq-poller --input-dir ./sqpoller-output
  python -m pytest -s -v --no-header "test_$DC.py" || exit 5
done

Quizás te estés preguntando si esta combinación de comandos es necesaria.

generate_hosts_for_suzieq.py sirve como contenedor para generar hosts a partir del inventario de Ansible, pero con más azúcar adentro, como omitir hosts específicos, configurando ansible_host dinámicamente (debido a que nuestra red OOB está altamente disponible, significa que tenemos varias puertas para acceder a ella). 

El archivo generado es similar a:

- namespace: xml
  hosts:
    - url: ssh://root@xml-oob.example.org:2232 keyfile=~/.ssh/id_rsa
    - url: ssh://root@xml-oob.example.org:2223 keyfile=~/.ssh/id_rsa

¿Por qué agrupar run-once y sq-poller? Ya hay un problema abierto que va a resolver este problema. Eventualmente, solo requiere agregar una única opción de Snapshot, y eso es todo.

Flujo de trabajo para validar cambios

Cada nueva solicitud de extracción (PR) crea un entorno virtual de Python (Pyenv) nuevo y limpio e inicia las pruebas. Lo mismo sucede cuando se fusiona PR. 

El flujo de trabajo simplificado fue: 

  1. Hacer cambios.
  2. Confirmar cambios, crear relaciones públicas en Github.
  3. Sondear y ejecutar pruebas de Pytest con Suzieq ( /tests/run-tests.sh <region|all>).
  4. Requerimos que las pruebas sean verdes antes de que se permita fusionarse. 
  5. Fusionar PR.
  6. Repetir todos nuestros controladores de dominio uno por uno: implementar y volver a ejecutar Pytests posteriores a la implementación.

Algo como:

stage('Run pre-flight production tests') {
  when {
    expression {
      env.BRANCH_NAME != 'master' && !(env.DEPLOY_INFO ==~ /skip-suzieq/)
    }
  }
  parallel {
    stage('EU') {
      steps {
        sh './tests/prepare-tests-env.sh && ./tests/run-tests.sh ${EU_DC}'
      }
    }
    stage('Asia') {
      agent {
        label 'deploy-sg'
      }
    }

Manejo de falsos positivos

Cada prueba tiene la posibilidad de un falso positivo, es decir, la prueba revela un problema que no es real. Esto es cierto si se trata de una prueba para detectar una enfermedad o una prueba para verificar un cambio. En Hostinger, asumimos que se producirán falsos positivos, y eso es normal. Entonces, ¿cómo los manejamos y cuándo? 

En nuestro entorno, los falsos positivos ocurren principalmente debido a tiempos de espera, errores de conexión durante la fase de raspado (sondeo) o al arrancar un nuevo dispositivo. En tal caso, volvemos a ejecutar las pruebas hasta que se solucione (verde en la canalización de Jenkins). Pero si tenemos una falla permanente (probablemente una real), las pruebas siempre permanecen en rojo. Esto significa que el RP no se fusiona y los cambios no se implementan. 

Sin embargo, en el caso de un falso positivo, usamos una etiqueta de confirmación de Git Deploy-Info: skip-suzieq para decirle a las canalizaciones de Jenkins que ignoren las pruebas. 

Agregar nuevas pruebas

Primero probamos las pruebas nuevas o modificadas localmente antes de que lleguen al repositorio de Git. Para agregar una prueba útil, debe probarse varias veces a menos que sea realmente trivial. Por ejemplo:

def bgp_sessions_are_up(self):
    # Test if all BGP sessions are UP
    assert (
        get_sqobject("bgp")().get(namespace=self.namespace, state="NotEstd").empty
    )

Pero si estamos hablando de algo como:

def uniq_asn_per_fabric(self):
    # Test if we have a unique ASN per fabric
    asns = {}
    for spine in self.spines.keys():
        for asn in (
            get_sqobject("bgp")()
            .get(hostname=[spine], query_str="afi == 'ipv4' and safi == 'unicast'")
            .peerAsn
        ):
            if asn == 65030:
                continue
            if asn not in asns:
                asns[asn] = 1
            else:
                asns[asn] += 1
    assert len(asns) > 0
    for asn in asns:
        assert asns[asn] == len(self.spines.keys())

Esto debe revisarse cuidadosamente. Aquí verificamos si tenemos un número AS único por DC. La omisión de 65030 se usa para enrutar las instancias de host para anunciar algunos servicios anycast como DNS, balanceadores de carga, etc. Este es el fragmento de salida de las pruebas (resumen):

test_phx.py::test_bgp_sessions_are_up PASSED
test_phx.py::test_loopback_ipv4_is_uniq_per_device PASSED
test_phx.py::test_loopback_ipv6_is_uniq_per_device PASSED
test_phx.py::test_uniq_asn_per_fabric PASSED
test_phx.py::test_upstream_ports_are_in_correct_state PASSED
test_phx.py::test_evpn_fabric_links PASSED
test_phx.py::test_default_route_ipv4_from_upstreams PASSED
test_phx.py::test_ipv4_host_routes_received_from_hosts PASSED
test_phx.py::test_ipv6_host_routes_received_from_hosts PASSED
test_phx.py::test_evpn_fabric_bgp_sessions PASSED
test_phx.py::test_vlan100_assigned_interfaces PASSED
test_phx.py::test_evpn_fabric_arp PASSED
test_phx.py::test_no_failed_interface PASSED
test_phx.py::test_no_failed_bgp PASSED
test_phx.py::test_no_active_critical_alerts_firing PASSED
test_imm.py::test_bgp_sessions_are_up PASSED
test_imm.py::test_loopback_ipv4_is_uniq_per_device PASSED
test_imm.py::test_loopback_ipv6_is_uniq_per_device PASSED
test_imm.py::test_uniq_asn_per_fabric FAILED
test_imm.py::test_upstream_ports_are_in_correct_state PASSED
test_imm.py::test_default_route_ipv4_from_upstreams PASSED
test_imm.py::test_ipv4_host_routes_received_from_hosts PASSED
test_imm.py::test_ipv6_host_routes_received_from_hosts PASSED
test_imm.py::test_no_failed_bgp PASSED
test_imm.py::test_no_active_critical_alerts_firing PASSED

Aquí, nos damos cuenta de que esta prueba DC  test_imm.py::test_uniq_asn_per_fabric falló. Dado que usamos ASN derivado automáticamente por conmutador (sin números de AS estáticos en el inventario de Ansible), podría ocurrir una carrera que podría tener ASN duplicado, lo cual es malo. 

O algo como:

def loopback_ipv6_is_uniq_per_device(self):
    # Test if we don't have duplicate IPv6 loopback address
    addresses = get_sqobject("address")().unique(
        namespace=[self.namespace],
        columns=["ip6AddressList"],
        count=True,
        type="loopback",
    )
    addresses = addresses[addresses.ip6AddressList != "::1/128"]
    assert (addresses.numRows == 1).all()

Para verificar si no tenemos una dirección de loopback IPv6 duplicada por dispositivo para el mismo centro de datos. Esta regla es válida y fue probada al menos un par de veces. La mayoría de las veces sucede cuando iniciamos un nuevo conmutador y el archivo de host de Ansible se copia/pega. 

Principalmente, se agregan nuevas pruebas cuando ocurre una falla, y se deben tomar algunas acciones para detectarlas rápidamente o mitigarlas con anticipación en el futuro. Por ejemplo, si cambiamos del diseño solo L3 al diseño EVPN, podríamos sorprendernos cuando el agotamiento de ARP/ND golpee la pared, o las rutas L3 caigan de varios miles a solo unas pocas. 

Batfish

Ya hemos evaluado Batfish dos veces. El primero fue una especie de descripción general y un ensayo para ver sus oportunidades para nosotros. La primera impresión fue algo así como “¿Qué pasa con mi configuración?” Porque, en ese momento, Batfish no admitía alguna sintaxis de configuración para FRR. Cumulus Linux y muchos otros proyectos masivos utilizan FRR. Se convierte en la mejor suite de enrutamiento de código abierto de facto. Y es por eso que Batfish también incluye a FRR como proveedor. Solo FRR como modelo necesita más cambios antes de ser utilizado en producción (al menos en nuestro entorno). 

Más tarde, hace uno o dos meses, comenzamos a investigar el producto nuevamente para ver qué se podía hacer realmente. Desde la perspectiva operativa, es un producto realmente genial porque permite al operador construir el modelo de red analizando los archivos de configuración. Además de eso, puedes crear Snapshots, hacer algunos cambios y ver cómo se comporta tu red. Por ejemplo, desactiva un enlace o un par BGP y predice los cambios antes de que se publiquen. 

También comenzamos a considerar a Batfish como un proyecto de código abierto para impulsar los cambios a la comunidad. Un par de ejemplos de modelos de comportamiento que faltan para nuestros casos:

https://github.com/batfish/batfish/pull/7671/commits/4fa895fd675ae60a257f1e6e10d27348ed21d4a0

https://github.com/batfish/batfish/pull/7694/commits/115a81770e8a78471d28a6a0b209eef7bc34df88

https://github.com/batfish/batfish/pull/7670/commits/10ec5a03c15c48fd46890be4da394170fa6eb03a

https://github.com/batfish/batfish/pull/7666/commits/f440c5202dd8f338661e8b6bd9711067ba8652b6

https://github.com/batfish/batfish/pull/7666/commits/974c92535ecb5eedfe8fd57fc4295e59f2d4639d

https://github.com/batfish/batfish/pull/7710/commits/a2c368ae1b0a3477ba5b5e5e8f8ebe88e4bf2342

Pero faltan muchos más. Somos grandes admiradores de IPv6, pero desafortunadamente, IPv6 no está bien (¿todavía?) cubierto en el modelo FRR en Batfish. 

Esta no es la primera vez que nos perdimos la compatibilidad con IPv6 y, suponemos, no es la última. Esperando y esperando que Batfish obtenga pronto el soporte de IPv6. 

Algunas observaciones sobre las mejores prácticas en las pruebas

Diríamos que las pruebas segregadas sirven para evitar arrojar espaguetis a la pared en un principio. Escribe pruebas fáciles y comprensibles. Si ves que dos pruebas dependen una de la otra, es mejor dividirlas en pruebas separadas. 

Algunas pruebas pueden superponerse y, si una falla, la otra también falla. Pero eso es bueno porque dos pruebas fallidas pueden decir más de una, incluso si prueban una funcionalidad similar. 

Para confirmar que las pruebas son útiles, debes ejecutarlas y usarlas a diario. De lo contrario, no vemos mucho sentido en tenerlas.

Si puedes adivinar lo que puede suceder en el futuro, cubrir esto en las pruebas es una buena idea a menos que sea demasiado ruidoso. 

Como siempre, el Principio de Pareto es la mejor respuesta a si vale la pena y cuánto valor cubren las pruebas. Si cubre al menos el 20% de las piezas críticas con pruebas, lo más probable es que tu red esté en buenas condiciones. 

No vale la pena automatizar y probar todas las cosas que se te ocurren. Es solo un impuesto adicional sin ningún motivo. Tienes que pensar en la capacidad de mantenimiento de esas pruebas con tu equipo y decidir. 

Lo que nos hace felices es que Suzieq es genial por defecto y no hay necesidad de escribir pruebas muy sofisticadas en Python. CLI es realmente impresionante y trivial incluso para empezar. Si necesitas algo excepcional, siempre puedes escribir a la lógica en Python, que también es amigable. Envuelto con la biblioteca de pandas, puedes manipular los datos de tu red tanto como quieras, muy flexible.

Author
El autor

Carlos Mora

Carlos es un profesional del marketing digital, eCommerce y de los constructores de sitios web. Ama ayudar a crecer a empresas en línea a través de sus tips. En su tiempo libre, seguramente está cantando o practicando artes marciales.