Introducción y uso de las especificaciones de CloudEvents y AsyncAPI
Escrito por
picodotdev el .
programacion
planeta-codigo
Enlace permanente
Comentarios
En los sistemas distribuidos, la integración entre servicios no se limita a las APIs REST. La comunicación asíncrona mediante mensajes es igualmente habitual, y también requiere un contrato claro entre las partes. Para cubrir esta necesidad existen dos especificaciones complementarias. Una CloudEvents, que estandariza el formato del sobre de cualquier menssaje o event, y otra AsyncAPI, que describe la API completa de un servicio orientado a mensajes, de forma análoga a lo que hace OpenAPI para REST. En este artículo exploramos ambas especificaciones y las ponemos en práctica con una aplicación Spring Boot que produce y consume eventos Kafka encapsulados como CloudEvents, acompañada de su documento AsyncAPI correspondiente.
En una arquitectura de microservicios o sistemas distribuidos es necesario conocer la API de esos servicios para poder integrar otros servicios con ellos. En los servicios que exponene una API con REST la definición de las APIs se puede especificar con OpenAPI.
Pero es habitual como otra forma de integrar servicios hacerlo mediante mensajes de forma asíncrona. Aunque la comunicación sea asíncrona y desacoplada también existe un contrato o interfaz entre los servicios, este es el formato del mensaje y los datos que incluye.
Para la definición de las APIs de un servicio que produce o consume mensajes están CloudEvents y AsyncAPI. Ambas especificaciones son complementarias pero independientes. AsyncAPI describe la API (canales, operaciones, esquemas) mientras CloudEvents define el sobre del mensaje que viaja por esos canales.
Contenido del artículo
CloudEvents
CloudEvents simplemente estandaríza el formato del mensaje y define algunos campos a incluir como el payload de datos del mensaje y el tipo del formato de ese payload.
CloudEvents es una especificación para el evento en sí mismo, un formato de sobre estándar bajo la CNCF. Define un conjunto de atributos obligatorios y opcionales (id, source, specversion, type, más opcionales como subject, time, datacontenttype, etc.) que cualquier evento debería incluir, independientemente de si viaja por Kafka, HTTP, AMQP, NATS o Pub/Sub. El payload en sí sigue siendo específico del servicio, CloudEvents simplemente estandariza el envoltorio de metadatos y cómo se serializa sobre cada protocolo (modo binario vs. modo estructurado).
Esto permite tener un formato de mensajes común entre varios servicios, equipos, sistemas de mensajería o integraciones con terceras partes.
Modos de transporte
CloudEvents soporta diferentes protocolos de transporte y dos modos de funcionamiento:
- Modo estructurado: El evento completo, metadatos y payload, viaja en un único bloque serializado, normalmente JSON. En Kafka eso significa que todo va en el value del mensaje.
- Modo binario: Los atributos de CloudEvents se mapean a los headers del protocolo de transporte, y el value del mensaje contiene únicamente el payload de negocio.
Un ejemplo en modo estructurado.
|
|
Un ejemplo en modo binario. Estas son las cabeceras en el mensaje de Kafka.
|
|
El payload de Kafka.
|
|
Qué modo usar y por qué
El modo binario es el recomendado en la mayoría de casos productivos por varias razones prácticas:
- Rendimiento: los consumidores que solo necesitan enrutar o filtrar mensajes pueden leer los headers sin deserializar el payload completo. En un sistema con alto volumen de mensajes esto tiene impacto real.
- Compatibilidad: el payload de negocio queda aislado en el value sin que CloudEvents lo contamine, lo que facilita la convivencia con consumidores que usan y no usan CloudEvents.
- Procesamiento en infraestructura: herramientas intermedias como Kafka Streams, conectores de Kafka Connect o proxies HTTP pueden inspeccionar y enrutar por los metadatos sin tocar el payload.
El modo estructurado es más sencillo de implementar y depurar porque todo está en un sitio, pero acopla el formato CloudEvents al consumidor. Cualquier lector del mensaje necesita entender el sobre para extraer el payload.
La forma de usar un modo u otro es la siguiente. El serializador se encarga de mover los atributos a headers o al JSON según el modo, de forma transparente para el resto del código.
|
|
AsyncAPI
AsyncAPI es una especificación para describir una API orientada a eventos, como OpenAPI, pero para sistemas asíncronos. La especificación es un documento yaml que describe los canales de tu servicio (topics o colas), las operaciones que publica/suscribe, los esquemas de los mensajes, los bindings del servidor (clúster de Kafka, configuración del broker) y la seguridad.
A partir de ese documento al igual que con OpenAPI se puede generar documentación, stubs de código y teses de contrato. Los conceptos clave de AsyncAPI, siguiendo la estructura del documento de especificación:
- Info y servers: info contiene los metadatos de la API. Título, versión, descripción y licencia, igual que en OpenAPI. servers define los brokers con los que trabaja la aplicación: la URL de conexión, el protocolo (kafka, amqp, mqtt, ws…) y la configuración de seguridad.
- Channels: Un channel es el canal de comunicación, el topic de Kafka, la cola de RabbitMQ o el subject de NATS. Es el concepto central de AsyncAPI, define dónde viajan los mensajes. Cada channel tiene un nombre y puede tener parámetros dinámicos en el path, igual que las rutas REST.
- Operations: Una opración define qué hace tu aplicación con un channel, send (produce mensajes) o receive (los consume). A diferencia de OpenAPI donde un endpoint suele hacer una sola cosa, un mismo channel puede tener operaciones de envío y recepción en servicios distintos.
- Messages: Un message define la estructura del mensaje que viaja por el channel: sus headers, el content type y el payload. En el payload es donde encaja CloudEvents: los atributos ce_type, ce_source, etc. se pueden modelar como headers del mensaje en AsyncAPI.
- Components: El bloque components es el almacén de elementos reutilizables, exactamente igual que en OpenAPI. schemas para los modelos de datos, messages para los mensajes, securitySchemes para los mecanismos de autenticación y serverVariables para parametrizar los servers. Todo se referencia con $ref.
- Bindings: Los bindings son el concepto más específico de AsyncAPI y uno de los más potentes. Permiten añadir configuración propia del protocolo en cualquier nivel, server, channel, operation o message. En Kafka por ejemplo puedes definir el número de particiones, el factor de replicación o el groupId del consumidor, cosas que no tienen equivalente en un protocolo genérico.
En el ejemplo uso la siguiente definción de la API con AsyncAPI del productor y consumidor de la aplicación.
|
|
Línea de comandos
AsyncAPI ofrece una herramienta de linea de comandos con la que validar la especificación y generar artfactos al igual que se puede hacer con el documento de especificación OpenAPI. Por ejemplo, generar los clienteso documentación de la API.
|
|
Cuando tiene sentido adoptar CloudEvents y AsyncAPI
Uno de los escenarios donde más se nota la ausencia de AsyncAPI es cuando un evento evoluciona (campos nuevos, obsolescencia de campos, breaking changes). Tener el contrato escrito permite obtener las diferencias entre versiones y comunicar cambios.
Tiene sentido adoptarlas juntas cuando se dan una o varias de estas situaciones.
Múltiples equipos o servicios consumiendo los mismos eventos
Cuando un evento lo consumen tres servicios distintos mantenidos por equipos diferentes, CloudEvents garantiza que todos hablan el mismo idioma en cuanto al sobre del mensaje, y AsyncAPI documenta el contrato de forma que cualquier equipo puede integrarse sin preguntar. Sin estas especificaciones el contrato existe igualmente, pero está en la cabeza de alguien o en un Confluence desactualizado.
Heterogeneidad de sistemas de mensajería
Si hoy usas Kafka pero mañana puede aparecer RabbitMQ, un bus de eventos cloud como EventBridge o una integración con un tercero vía HTTP, CloudEvents te da portabilidad del formato del evento independientemente del transporte. AsyncAPI describe los bindings específicos de cada protocolo sin cambiar el resto del documento.
Integración con terceros o sistemas externos
Es el caso donde más valor aportan. Cuando expones eventos a un partner externo o consumes eventos de una plataforma SaaS, tener un documento AsyncAPI es el equivalente a publicar una API REST con OpenAPI. El otro equipo puede leerlo, validarlo y generar código sin necesidad de reuniones. CloudEvents añade la garantía de que el formato del mensaje es estándar y reconocible.
Equipos que ya usan OpenAPI para sus APIs REST
Si el equipo ya tiene cultura de API-first con OpenAPI, AsyncAPI es la extensión natural para los servicios asíncronos. El salto conceptual es pequeño y las herramientas son similares. CloudEvents complementa esto estandarizando lo que OpenAPI no cubre, el formato del mensaje en tránsito.
Cuándo no tiene tanto sentido
Si tienes un sistema pequeño con un único productor y un único consumidor en el mismo equipo y repositorio, el overhead de mantener un documento AsyncAPI y ajustarse a CloudEvents puede no compensar. El contrato en ese caso es el código compartido, un record de Kotlin o un DTO de Java con Jackson. Adoptar estas especificaciones tiene sentido cuando el coste de la descoordinación entre partes empieza a ser real, no como práctica preventiva en proyectos pequeños.
La regla práctica es que si el evento cruza una frontera de equipo, de sistema o de organización, las dos especificaciones juntas valen la inversión. Si el evento es interno a un servicio o a un equipo muy pequeño, probablemente es sobreingenieria.
Ejemplo usando CloudEvents y AsyncAPI
En este ejemplo de aplicación Java hay un servicio REST que recibe una petición de y genera un mensaje que es enviado a una cola de Kafka, el mensaje es encapsulado en un mensaje de CloudEvents usando el modo recomendado binario.
|
|
|
|
|
|
La configuración de Kafka para la aplicación de Spring Boot es la siguiente, junto con el archivo de construcción con las dependencias.
|
|
|
|
El archivo de Docker Compose para iniciar el contenedor de Kafka.
|
|
El consumidor del mensaje está en la misma aplicación que simplemente emite un mensaje en la salida del sistema.
|
|
|
|
El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:./gradlew run
