Información general
La aplicación de esta guía técnica es dependiente de la disponibilidad de la plataforma de EVENTOS. Se puede consultar la fecha de disponibilidad en la hoja de ruta
El objetivo de esta guía de integración es facilitar el uso de Apache Kafka como una plataforma de mensajería en arquitecturas basadas en eventos dentro de la Junta de Andalucía.
El artículo presenta los componentes que deben desarrollarse en las diferentes arquitecturas para que las aplicaciones puedan interactuar con Kafka de forma efectiva, asegurando un manejo adecuado de los mensajes, cumpliendo con la normativa y estándares definidos por la Oficina de Arquitectura y manteniendo las mejores prácticas en términos de consistencia, seguridad y escalabilidad.
Esta página del portal está dirigida a los equipos de desarrollo que van a hacer uso de la plataforma de eventos de la Agencia Digital de Andalucía.
Herramientas de desarrollo
Herramientas básicas para el desarrollo
Cada arquitectura tendrá en su guía de desarrollo las herramientas básicas que permiten a los desarrolladores implementar componentes para cada una de esas arquitecturas. Los componentes básicos que son necesarios para la integración de eventos tienen la ventaja que pueden instalarse onpremise o usando Docker en un entorno local ya que el consumo de estas herramientas es asumible por un equipo estándar.
Una vez consultadas las herramientas de la arquitectura sobre la que se van a implementar los eventos deben instalarse las siguientes herramientas propias de la arquitectura orientada a eventos:
- Docker: Se recomienda disponer de una instalación local de Docker que permita a los desarrolladores levantar imágenes de componentes en local. Este componente solo es obligatorio si se va a realizar la instalación de los componentes usando imagen Docker.
- Kafka: Para poder probar el flujo de los eventos en local es necesario disponer de una instalación de Kafka en local. Kafka es un componente gratuito y puede instalarse on-premise o desde una imagen Docker.
- Apicurio: Los mensajes que se escriben o leen de Kafka deben estar normalizados y con un esquema definido y registrado en Apicurio. Apicurio es un componente gratuito y puede instalarse on-premise o desde una imagen Docker.
Herramientas avanzadas para el desarrollo
Las siguientes herramientas no son obligatorias para desarrollar en local para la mayoría de los casos de uso, sin embargo pueden ser interesantes para algunas arquitecturas o para casos de uso más avanzados:
- Kafka Tool: Aplicación de escritorio que permite visualizar y gestionar topics, brokers, particiones y mensajes de forma gráfica.
- Conduktor: Herramienta avanzada para gestionar y depurar clústeres Kafka. Permite inspeccionar mensajes, gestionar configuraciones y verificar la salud de los consumidores.
Herramientas para pruebas y simulación
- AsyncAPI Studio: Permite simular mensajes Kafka y probar consumidores y productores basados en especificaciones asíncronas.
- Testcontainers: Biblioteca que permite ejecutar instancias de Kafka en contenedores Docker para pruebas de integración automatizadas.
Herramientas para la generación de documentación adicional
La documentación es clave para compartir las configuraciones, dependencias y parámetros utilizados en los clientes Kafka.
- AsyncAPI: Herramienta clave para documentar especificaciones asíncronas de productores y consumidores Kafka. Permite describir topics, esquemas de mensajes, headers y claves, asegurando una referencia estándar y clara para los equipos.
Diseño de eventos
El desarrollo de eventos dependerá de la arquitectura sobre la que se desarrolle, sin embargo existen una serie de conceptos comunes que deben tenerse en cuenta durante la fase de diseño de los eventos.
Estructura de un evento
Un evento se compone de los siguientes elementos:
Cabeceras o metadatos:
Proporcionan el contexto del mensaje y permiten a los sistemas procesar los mensajes de forma más óptima. Para el desarrollador no es un campo obligatorio aunque Kafka, en ciertas fases del proceso de envío de un mensaje a un topic, suele incluir algunas cabeceras en los mensajes para optimizar su uso. La existencia y uso de estas cabeceras especiales suele ser transparente para los desarrolladores.Key:
Identificador único del mensaje. Se puede considerar un tipo de metadato especial y si se hace un uso correcto de este identificador permite explotar capacidades adicionales de Kafka y optimizar su rendimiento. En la mayoría de los casos de uso no es un campo obligatorio. Solo es obligatorio cuando se quieren hacer, por ejemplo, operaciones de agrupación de los mensajes enviados a un topic.Mensaje:
Representa la información que compone el evento y permite notificar de un cambio de estado en un Sistema de Información. Este cambio de estado acabará propiciando una acción en el Sistema de Información notificado.Topic:
Es el canal donde se encuentra depositado el evento. Los productores depositan el evento en este canal y los consumidores están preguntando a ese canal continuamente para saber si hay nuevos eventos para consumir.
Roles que intervienen durante la ejecución de un evento
Cuando se genera un evento lo habitual es que exista un componente que genere el evento (productor) y otro reciba el evento (consumidor).
Productor de evento: Se considera productor al componente que escribe mensajes en Kafka. El productor puede ser un microservicio, un módulo de integración, un microfrontend o cualquier componente de cualquier tecnología o lenguaje de programación que acepte una integración con Kafka. En definitiva, un productor de eventos es cualquier elemento software que permita enviar un mensaje a un topic usando la instrucción SEND de Kafka.
A veces, según la tecnología o la arquitectura, el concepto productor en la bibliografía puede nombrarse en otros términos: Source en el contexto de Kafka Connect o Publisher en el contexto del patrón Pub/Sub.
Consumidor del evento:
Se considera consumidor al componente que lee mensajes en Kafka. Al igual que el productor el consumidor puede ser cualquier componente de cualquier tecnología o lenguaje de programación que acepte una integración con Kafka. En definitiva, un consumidor de eventos es cualquier elemento software que permita leer un mensaje de un topic usando la instrucción FETCH de Kafka.A veces, según la tecnología o la arquitectura, el concepto consumidor en la bibliografía puede nombrarse en otros términos: Sink en el contexto de Kafka Connect o Subscriber en el contexto del patrón Pub/Sub.
Un mismo componente software puede actuar con rol de productor y de consumidor al mismo tiempo. De hecho, este rol múltiple es habitual cuando se aplica patrones de diseño que incluyen la transformación de mensajes dentro de un mismo componente. En esos casos el componente recibe el mensaje (consumidor) lo transforma y lo reenvía a otro topic distinto (productor).
Un evento puede tener varios productores, si por ejemplo, las circunstancias del cambio de estado que genera el evento es compartido por varios componentes, y también puede tener varios consumidores, si por ejemplo, el cambio de estado debe notificarse a varios componentes simultáneamente.
Diseño de un mensaje
Una de las principales características de la arquitectura orientada a eventos es que el componente que actúa como productor del evento y el que actúa como consumidor no tienen que tener conocimiento el uno del otro y pueden actual de forma totalmente desacoplada.
El productor deja su mensaje en Kafka y le es indiferente si el mensaje lo lee algún consumidor o se queda en Kafka para siempre y el consumidor lee el mensaje de Kafka pero no sabe bajo que circunstancias ha llegado ese mensaje o que el mensaje ha sido puesto ahí por un componente externo a Kafka.
Este desacoplamiento trae muchos beneficios ya que optimiza y flexibiliza el flujo de los procesos, pero si no se gestiona correctamente la estructura del mensaje que se manda en el evento, puede generar problemas importantes en la comunicación.
Para evitar este tipo de problemas y garantizar que un consumidor pueda leer el mensaje enviado por un productor sin necesidad de que haya comunicación directa entre ellos se ha instalado dentro de la arquitectura el componente Apicurio.
Apicurio es un registro de esquema de datos que almacena la estructura de los mensajes para garantizar que la comunicación entre productor y consumidor pueda realizarse. Para ello, durante la fase de diseño debe definirse el formato del mensaje y registrarse en un fichero Avro. Este fichero Avro que generalmente es definido por el componente que actúa como productor se comparte entre todos los consumidores y se instala en Apicurio que hará de intermediario en la comunicación.
De esta forma, el productor al mandar un mensaje, antes de mandarlo verifica con Apicurio que el formato del mensaje es correcto. Sin la validación de Apicurio el evento no puede continuar. Una vez enviado el mensaje, el consumidor antes de procesarlo, valida el formato del mensaje en Apicurio. Una vez Apicurio ha validado el mensaje el consumidor puede leerlo y procesarlo.
Construcción de eventos
La construcción del evento dependerá de la arquitectura bajo la que se construyan los componentes que actúen con rol de productor o consumidor.
En las guías de desarrollo de cada una de las arquitecturas asociadas se incluye un anexo especificando como se hace la integración con la arquitectura de eventos.
Gestión de errores
En un sistema desacoplado como el que propone la arquitectura orientada a eventos, la gestión de errores debe hacerse en los dos extremos de la comunicación. La gestión de errores asociado a un evento debe ser una ponderación entre cuanto es de importante es que el mensaje se pierda y cuanto tiempo se quiere emplear en garantizar que el mensaje se procese correctamente.
Existen casos de uso como en envío de mensajes de logs o notificaciones donde lo que prima es la rapidez y la capacidad de enviar mensajes masivamente en un periodo muy corto de tiempo. En casos como ese puede que la pérdida de algún mensaje ocasional compense más que ralentizar el sistema añadiendo múltiples capas de procesamiento de errores y reintentos.
Sin embargo, existen casos de uso en el otro extremo. Por ejemplo, supongamos la presentación de un expediente administrativo por parte de un ciudadano donde no solamente es vital para el servicio que se está prestando un mecanismo de gestión de errores y reintentos bien diseñado sino que es crucial garantizar que el mensaje solo se procesa una vez ya que podría ser un problema para el ciudadano si el expediente se presentara varias veces.
Una vez determinado el tipo de caso de uso y lo completa que debe ser la gestión de errores, existen varios mecanismos que pueden implantarse tanto en el productor como en el consumidor.
Tipos de excepciones
Antes de pasar a explicar los distintos tipos de procesamiento de errores es importante comentar que Kafka separa los errores que pueden generarse durante el ciclo de vida de un evento en dos tipos:
Errores transitorios: Son aquellos que se producen por indisponibilidad temporal de algún componente del sistema. Supongamos que un productor no puede mandar un mensaje porque temporalmente el topic de Kafka se ha quedado sin partición líder que reciba el mensaje o un consumidor no puede procesar un mensaje porque un servicio con el que se conecta durante el procesamiento del mensaje no está disponible. En ambos casos, si se espera un tiempo prudencial y se reintenta el evento puede completarse sin problema al disponer los componentes que no están disponibles sus propios mecanismo de resiliencia.
Errores no transitorios: Son aquellos intrínsecos al mensaje y que no van a resolverse por mucho que se reintente. El caso más habitual de este tipo de errores se produce casi siempre en el consumidor y está relacionado con problemas con procesando el mensaje. Si el mensaje está mal formado y el consumidor no puede procesarlo deberá descartarse para evitar el colapso del sistema o procesarlo aparte.
Gestión de excepciones en el productor
Cuando al mandar un mensaje el produce recibe un error de Kafka el desarrollador puede aplicar varias estrategias para solventarlo:
La primera acción que se recomienda cuando se detecta que el envío de mensajes es inestable es revisar toda la configuración del productor y ajustarla. Muchas veces Kafka devuelve un error porque los tiempos que tiene definido para realizar las diferentes operaciones o el tamaño de los bloques a procesar no están ajustados. Si estos tiempos y tamaños no están debidamente ajustados da igual los reintentos que se hagan el envío de mensajes acabará fallando con un alto grado de probabilidad Se recomienda consultar las propiedades que pueden definirse para el productor en la documentación oficial de Kafka.
Para los errores de tipo transitorio se recomienda implementar en el productor una política de reintentos. La propiedad retries permite configurar cuantos reintentos hará el productor automáticamente al detectar un error en el envío de mensajes.
Puede también configurarse una política de reintentos manual a través de bloques de tipo try/catch o analizando el ack de respuesta que envía Kafka una vez procesado el mensaje.
- Los errores generados por Apicurio porque el mensaje no tiene el formato correcto no se pueden solventar con ningún mecanismo de resiliencia. En estos casos lo mejor es someter al productor a los suficientes test como para detectar cualquier problema en la generación o transformación de un mensaje antes de mandarlo a Kafka.
Si el productor se ve en la necesidad de descartar un mensaje porque al enviarlo se produce cualquier error de tipo no transitorio, se puede optar por implementar un patrón DLQ. Este patrón implementa un camino alternativo para los mensajes que no pueden enviarse correctamente por el flujo normal.
Para implementar este patrón se recomienda seguir los siguientes pasos:
- Crear un topic que actuará como dlq. Puede nombrarse dlq-[productorTopic]
- Cuando se produzca un error no transitorio, dentro del bloque catch de procesamiento invocar a un productor secundario.
- Este productor secundario estará configurado para enviar mensajes al topic dlq-[productorTopic] y enviar mensajes de tipo binario (value.serializer=org.apache.kafka.common.serialization.ByteArraySerializer). Eso indica a Kafka que este productor puede enviar cualquier tipo de mensajes por lo que Apicurio no analizará el mensaje y siempre lo dejará pasar.
- El mensaje al mandarse a la dlq no se perderá. Normalmente este tipo de mensajes no se pueden procesar por lo que una solución puede ser almacenarlos en una base de datos (por ejemplo MongoDB) a la espera de ser analizados manualmente por un técnico.
Gestión de excepciones en el consumidor
La gestión de errores en el consumidor es similar a la gestión de errores en el productor:
La primera acción que se recomienda cuando se detecta la lectura de mensajes es inestable es revisar toda la configuración del consumidor y ajustarla. Se recomienda consultar las propiedades que pueden definirse para el consumidor en la documentación oficial de Kafka.
Los consumidores tienen una propiedad llamada enable.auto.commit que permite activar o desactivar el commit automático. Cuando un consumidor ha finalizado de procesar un mensaje manda un commit a Kafka. Kafka al recibirlo considera que ese mensaje ya se ha procesado y se encarga de que otro consumidor del mismo grupo no lo lea y lo vuelva a procesar. Por defecto y lo recomendable, es dejar esta propiedad con su valor por defecto que es true. Es decir, el desarrollador no tiene que preocuparse de hacer el commit, este se hará automáticamente al ejecutarse la última línea del consumidor. Sin embargo, a veces, en casos de uso muy complejos y para poder afinar mejor el manejo de errores, se decide desactivar esta propiedad y dejar al desarrollador que ejecute el commit cuando considere que el mensaje ha finalizado de procesarse correctamente.
Si el consumidor termina de ejecutarse sin enviar un commit Kafka volverá a mandar el mensaje una y otra vez hasta que reciba un commit de ese grupo consumidor por lo que indirectamente, si se diseña bien, se está aplicando una política de reintentos.
No existe en los consumidores una propiedad única para configurar una política de reintentos automática. En el caso de los consumidores la configuración de esta política es más compleja y requerirá el balanceo de varias propiedades como retry.backoff.ms o fetch.max.wait.ms. Si se quiere implementar una política de reintentos acertada, en este caso se recomienda encarecidamente consultar la documentación oficial e interiorizar estas propiedades para llegar a la combinación que mejor se ajuste al caso de uso.
Los errores generados por Apicurio porque el mensaje no tiene el formato correcto no se pueden solventar con ningún mecanismo de resiliencia. En estos casos lo mejor es someter al consumidor a los suficientes test como para detectar cualquier problema durante el procesamiento de un mensaje.
Si el consumidor se ve en la necesidad de descartar un mensaje porque al procesarlo se produce cualquier error de tipo no transitorio, se puede optar por implementar un patrón DLQ. Este patrón implementa un camino alternativo para los mensajes que no pueden procesarse correctamente por el flujo normal.
Para implementar este patrón se recomienda seguir los siguientes pasos:
- Crear un topic que actuará como dlq. Puede nombrarse dlq-[consumidorTopic]
- Cuando se produzca un error no transitorio, dentro del bloque catch de procesamiento invocar a un productor secundario.
- Este productor secundario estará configurado para enviar mensajes al topic dlq-[consumidorTopic] y enviar mensajes de tipo binario (value.serializer=org.apache.kafka.common.serialization.ByteArraySerializer). Eso indica a Kafka que este productor puede enviar cualquier tipo de mensajes por lo que Apicurio no analizará el mensaje y siempre lo dejará pasar.
- El mensaje al mandarse a la dlq no se perderá. Normalmente este tipo de mensajes no se pueden procesar por lo que una solución puede ser almacenarlos en una base de datos (por ejemplo MongoDB) a la espera de ser analizados manualmente por un técnico.
Seguridad
En un entorno ya sea de desarrollo o producción, las decisiones de seguridad deben implementarse en Kafka y Apicurio directamente con ayuda de la Oficina de Interoperabilidad que son los responsables y administradores de estas herramientas.
Kafka
Para conectarse a Kafka se han definido varios mecanismos de seguridad que tienen que implementar los componentes que quieran comunicarse con kafka. Estos mecanismos son:
mTLS
ACL(Access Client List) asociado al CN (Common Name) de un certificado
mTLS
La comunicación con Kafka siempre estará securizada por mTLS. Para ello, la Oficina de Interoperabilidad como equipo responsable de la administración hará la configuración necesaria en Kafka para garantizar la seguridad mediante este mecanismo. Los pasos necesarios para que un componente configure la comunicación mTLS con Kafka son los siguientes:
- El equipo de desarrollo del componente debe crearse un certificado con identidad única que tenga un CN asociado.
- Debe abrirse una petición a la Oficina de Interoperabilidad pasándole la clave pública de su certificado para que lo incluya en la truststore de kafka y poder validar la comunicación mTLS.
- Deben configurarse los componentes (productores o consumidores) que quieran comunicarse con Kafka. Se deberán definir las siguientes propiedades: security.protocol, ssl.keystore.location, ssl.truststore.location, ssl.truststore.password, ssl.keystore.password y ssl.key.password.
- En la truststore se pondrá la clave pública del certificado vinculado a Kafka para poder establecer la comunicación mTLS.
- El keystore tiene el certificado del componente y lo usará Kafka para validar en su extremo la comunicación mTLS.
ACL asociado al CN de un certificado
Securizar las comunicaciones mediante mTLS no evita que un componente que tiene las propiedades de mTLS configurada pueda conectarse a cualquier topic. Es necesario también configurar la privacidad de los topic para asegurar que solo los componentes autorizados puedan acceder a ellos. El acceso a los topic se gestiona mediante un mecanismo llamado ACL o Access Client List. Este mecanismo permite a un administrador de Kafka configurar un topic para que solo los usuarios autorizados puedan acceder al sistema. ACL, para añadir más seguridad, permite restringir el acceso indicando que el nombre del usuario ACL tiene que coincidir con el CN o Common Name del certificado digital que se ha creado para este usuario. Existen otros mecanismos para vincular el nombre de usuario a Keycloak por ejemplo pero dado que la conexión no se hará con usuarios físicos sino sistemas por simplificar se ha decidido vincular el usuario al certificado digital del sistema.
Los pasos que hay que realizar para definir y configurar el ACL asociado a un topic y el usuario son los siguientes:
- Deberá abrirse una petición a la Oficina de Interoperabilidad indicando para que topic quiere definirse la ACL, el CN del certificado creado para el componente para la comunicación mTLS y los permisos que ese componente deben tener sobre el topic. El CN de ese certificado es el que se usará como usuario para el ACL.
- No es necesario añadir ninguna propiedad adicional en el componente ya que las dos únicas propiedades obligatorias son ssl.keystore.password y ssl.key.password y ya se definieron para la comunicación mTLS.
Apicurio
La comunicación con Apicurio también estará securizada por mTLS. La configuración de Apicurio para que solo admita comunicación por mTLS es responsabilidad de la oficina de interoperabilidad. Para configurar esta comunicación deben seguirse los siguientes pasos:
- Debe abrirse una petición a la Oficina de Interoperabilidad pasándole la clave pública de su certificado para que lo incluya en la truststore de kafka y poder validar la comunicación mTLS. El certificado puede ser el mismo que se ha usado para configurar mTLS en Kafka o crearse uno nuevo.
- Por parte de los componentes tiene que entenderse que la comunicación con Apicurio se hace a través de una api REST que el componente publica por lo que para securizar esta comunicación basa con securizar la comunicación HTTP con Apicurio para que sea mtLS. Componentes basados en tecnologías como Spring Boot y Quarkus tienen propiedades específicas para enviar automáticamente en una petición un certificado o tras tecnologías pueden que requieran de una configuración más manual.
Testing
Los componentes software ya actúen como productores o consumidores de eventos deben implementar un conjunto de pruebas que permitan validar la calidad del desarrollo.
Test unitarios
Tanto el productor como el consumidor de un evento tiene que tener sus correspondientes test unitarios. En un test unitario la conexión con Kafka o Apicurio puede sustituirse por mocks. Se recomienda consultar la documentación oficial para saber que tipo de mock puede utilizarse según la tecnología con la que se implementa el componente.
Test de integración
Tanto el productor como el consumidor de un evento tienen que tener sus correspondientes test de integración. Los test de integración se harán contra las instalaciones en DES o en PRE nunca sobre PRO.
Test de carga
Para poder afinar correctamente las propiedades y evitar que surjan errores durante el ciclo de vida del evento se recomienda realizar pruebas de carga sobre los productores y consumidores. De esta forma se puede testear el comportamiento de Kafka y Apicurio al recibir un volumen considerable de eventos y afinar la configuración tanto para el productor como para el consumidor. Las pruebas de carga se recomienda que se hagan sobre el entorno de PRE ya que su configuración será muy similar a la de PRO.
ANEXO. Fichero docker compose para desplegar Kafka y Apicurio en local
A continuación se propone el contenido de un fichero docker-compose.yml que puede utilizarse como base para una instalación local de Kafka y Apicurio que son los componentes básicos de la arquitectura orientada a eventos.
version: '3'
services:
broker:
image: confluentinc/cp-kafka:7.7.0
hostname: broker
container_name: broker
ports:
- "9092:9092"
- "9101:9101"
environment:
KAFKA_NODE_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_JMX_PORT: 9101
KAFKA_JMX_HOSTNAME: localhost
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@broker:29093'
KAFKA_LISTENERS: 'PLAINTEXT://broker:29092,CONTROLLER://broker:29093,PLAINTEXT_HOST://0.0.0.0:9092'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk'
apicurio-registry:
image: apicurio/apicurio-registry-mem:2.6.2.Final
depends_on:
- broker
environment:
QUARKUS_PROFILE: prod
KAFKA_BOOTSTRAP_SERVERS: broker:9092
REGISTRY_STORAGE: broker
MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_BOOTSTRAP_SERVERS: broker:9092
ports:
- "8990:8080"