Table of Contents
Apache Camel es un framework de integración, que básicamente nos muestra un conjunto de herramientas que te va a ayudar a conectar un sistema con otro. Un ejemplo básico podría ser escuchar una cola AWS SQS y guardar un fichero en Google Cloud Storage Bucket por cada mensaje recibido.
Apache Camel tiene implementados los patrones más usados definidos en el libro «Enterprise Integration Patterns«, con el cual podríamos hacer una similitud con el libro de Design Patterns por «the Gang of Four» pero a nivel de integración de sistemas. Un libro que define una serie de patrones y blueprints para ayudarnos a diseñar mejor aplicaciones grandes basadas en componentes.
¿Por qué es necesario un framework de integración?
Claramente, Apache Camel (o cualquier otro framework de integración), es una solución a un problema – ya sea un esencial o accidental. En aplicaciones orientadas a microservicios, la comunicación entre ellas puede ser de muchos tipos, como se habla en el punto sobre «strategic design» de DDD, hay muchas maneras de comunicarse entre bounded contexts (microservicios), por poner un ejemplo, el mapping de tipo «Conformist»:
Nos dice que el servicio downstream (D) se conforma con la información que le llega y como le llega, no tiene otra opción dado que el poder reside en el servicio upstream (U).
Take it or leave it
Este tipo de comunicación es un ejemplo de que nos tendremos que adaptar ante cualquier comunicación (integración) que tengamos con dicho servicio, algunas veces tendremos cierto poder para poder definir ese canal de comunicación y otras no, en otras no habrá ni espacio para la definición porque ya la hubo en su día, proyectos legacy donde hay sistemas que llevan funcionando muchos años y son «intocables».
Running Camel
Camel puede ser ejecutado de múltiples maneras, desde standalone, como parte de springboot o quarkus, o incluso como parte de kubernetes (con Camel K), aunque no está limitado a estas, siempre se puede extender de manera más fácil a más sistemas.
Enterprise Integration Patterns (EIP)
Raro es que cuando busques información sobre Apache Camel no encuentres las siglas EIP, y es porque Apache Camel fue desarrollado como una implementación de los patrones descritos en el libro Enterprise Integration Patterns por Gregor Hohpe y Bobby Woolf. Comparable con el libro de Design Patterns de Gang of Four pero a nivel de integración de sistemas.
Por poner algunos ejemplos y saber de lo que estamos hablando, aquí una muestra de algunos EIP (encontraréis todos en EIP y en la documentación de Apache Camel):
Content Based Router
Nos ayudará a enrutar nuestro mensaje de entrada a diferentes destinos según el contenido del mismo.
from("direct:in")
.choice()
.when(header("type").isEqualTo("widget"))
.to("direct:widget")
.when(header("type").isEqualTo("gadget"))
.to("direct:gadget")
.otherwise()
.to("direct:other");
Filter
Con él filtraremos los mensajes que realmente nos interesan y descartar el resto.
from("direct:a")
.filter(simple("${header.foo} == 'bar'"))
.to("direct:b");
Enricher
Cuando el mensaje que recibimos no tiene todos los datos que necesitamos, con este patrón podremos ir a buscar la información que nos falta a otro origen de datos y agregarla al mensaje original.
from("direct:start")
.enrich("direct:resource", aggregationStrategy)
.to("direct:result");
Existen dos estrategias de Enrich en Camel, profundizaremos en ellas en próximos artículos.
¿Qué ofrece Camel?
No podemos reducir Apache Camel a una implementación de los EIP (aunque buena parte de su potencia viene de ello), sino que también proveé una amplia lista de Componentes (más de 300) con los que conectarnos, ya sea como origen o destino de datos, dado que al final, en Camel vamos a estar desarrollando Rutas donde conectaremos un Componente A con el Componente B, por ejemplo:
from("aws-sqs://queue-name")
to("aws-s3://bucket-name")
Puede ser que dos componentes que deben comunicarse entre ellos no hablen el mismo «lenguaje» (formato), es posible que la salida de uno sea XML mientras que la entrada del otro sea un JSON, para eso camel te permite configurar una serie de Data Format dentro de su runtime para poder convertir de un tipo a otro fácilmente.
Puede ser que dos componentes que deben comunicarse entre ellos no hablen el mismo «lenguaje» (formato), es posible que la salida de uno sea XML mientras que la entrada del otro sea un JSON, para eso camel te permite configurar una serie de Data Format dentro de su runtime para poder convertir de un tipo a otro fácilmente.
¿Cuándo usar Camel?
Esta pregunta lleva en mi cabeza desde que creé mi primera ruta en Apache Camel. Es fácil ver que cuando un proyecto tiene muchas integraciones, Apache Camel nos va a ayudar, nos va a gestionar mucha complejidad y encima unificará la manera en la que nos integramos, dado que para Camel todo es considerado un Message, independientemente del origen de datos.
El hecho que Camel normalice todos los datos de la misma manera nos va a simplificar mucho las integraciones con los demás sistemas. A nivel de ruta, el tipo de dato que viaja cuando lees un fichero en un bucket S3 o cuando lees un mensaje en una cola es el mismo, un Exchange con el Message dentro.
Dado que la decisión es muy subjetiva y va a depender mucho del proyecto y del equipo, voy a dejar aquí unos puntos a tener en cuenta que puede ayudar a tomar la decisión si usar o no Apache Camel:
- ¿Cuántos orígenes/destinos tiene tu aplicación?
- ¿Cuántos protocolos habla?
- ¿Qué tipo de comunicación (a nivel de Mapping Context) existe entre los diferentes bounded context? Si existe una comunicación fluida, donde se trabaja en equipo es posible poder conseguir unificar el protocol, formato y demás (aunque es algo utópico).
¿Infraestructura o negocio?
Esta otra pregunta surge una vez ya usas Apache Camel en tu aplicación, y es donde poner la lógica, si como parte de la ruta de Camel, o ¿en el destino de la ruta? ¿Debo utilizar el patrón Splitter mas el Content-Based Router dentro de la ruta de Camel para luego enviarlo a un endpoint u otro del destino, o creo un único endpoint en destino que se encargará de separarlo y hacer lo que tenga que hacer? ¿Debo enriquecer el mensaje antes de que llegue al destino o después?
Son preguntas difíciles de responder, pero si que trataría de seguir el concepto de Smart endpoints and dumb pipes, que viene a decir que la lógica debería estar mayoritariamente en el destino, evitar en la medida de lo posible meter lógica de negocio (reglas e invariantes que cubren la necesidad del usuario final) dentro de las rutas.
Endpoints
El concepto de Endpoint no es un concepto exclusivo de Apache Camel. Cuando hablamos de Endpoint, nos estamos refiriendo a un punto de conexión a un servicio, como por ejemplo podría ser un endpoint HTTP, un subscriber en un topic SNS, etc.
A nivel de Apache Camel, un Endpoint es una interfaz donde crearemos el Consumer o Producer (o ambos) desde donde empezar/finalizar una ruta.
Siguiendo con el primer ejemplo: aws-sqs://queue-name
es un Endpoint, y dado que lo estamos llamando desde el from()
, requiere que el Component aws-sqs
sea de tipo Consumer. Lo mismo pasa con el método .to()
, podríamos usar el mismo Endpoint siempre y cuando el componente sea también Producer. (El saber si un componente es Consumer o Producer está en la primera línea de la documentación del componente)
¿Por qué «Smart Endpoints and dumb pipes»?
Lo lógico sería pensar (y así se define en algunos artículos por internet), que las pipes lo único que deben hacer es redirigir el mensaje de salida de un punto hacía la entrada de otro. Un buen ejemplo de este tipo de pipes, son las de los sistemas Unix: ls | grep dir
, dónde el stdout del comando ls, se redirige al stdin de grep, la pipe no tiene más trabajo que ese, el resto lo hacen los endpoints.
Si seguimos con el ejemplo de Unix, es cierto que la pipe solo hace una cosa, pero el protocolo que siguen todos los endpoints es el mismo, stdout -> stdin. En una aplicación grande, es más que probable que nos encontremos varios protocolos en juego (sqs -> http, kafka -> ftp, y un largo etc.), pero esto es algo que solucionamos con los componentes que trae Camel, es algo que no afecta al concepto de dumb pipes.
Sin embargo, el formato de los datos en Unix mientras ejecutamos nuestros programas si pueden cambiar, puede ser que el stdout del origen de datos sea JSON mientras que la entrada del destino que buscamos sea un simple string (un id, por ejemplo). En Unix haríamos algo como curl http://product/list | jq .id | http://product/register
(pseudo-código), si bien es cierto que las pipes se limitan a mover el mensaje de un lado a otro, en la ruta completa hemos añadido un programa que transforma la salida de nuestro origen de datos y la adapta a la entrada de nuestro destino.
Es en este tipo de casos donde Camel nos va a ayudar sin romper este principio (siendo estrictos lo estamos rompiendo… pero no todo es blanco o negro).
Otra cosa muy distinta sería, siguiendo el ejemplo anterior, añadir en la ruta un check de que el producto tiene stock en almacén antes de registrarlo, esto es una invariante que bien podríamos meterlo previamente antes de registrar el producto, pero entonces estaríamos separando nuestra lógica de negocio para registrar un producto en dos servicios diferentes, con todos los problemas que ello conlleva.
En mi opinión, los endpoints entienden conceptos de negocio, hablan el lenguaje de negocio (ubiquitous language) – Invariantes, Reglas de negocio, flujos de negocio, etc. Mientras que a nivel de las dumb pipes, si veo bien poder tener cierta lógica de infraestructura como podría ser un filtrado de algún mensaje no deseado por el endpoint de destino, transformación de formato de mensajes (XML to JSON), normalizar mensajes que llegan desde varios sistemas (sqs, kafka, rabbitmq) quitándoles metadatos que incluye cada sistema y un largo etcétera que nos iremos encontrando, que como véis, muy relacionado con temas de infraestructura.
Conclusiones
Camel tiene una curva de aprendizaje media, no es difícil comenzar a desarrollar rutas robustas que puedan llegar a producción, pero sí será complejo dominar todo el sistema que hay montado, ya que siempre aparecen detalles que provocan un aprendizaje continuo (y te bloquean un poco en tu desarrollo).
Tampoco va a evitar que te tengas que leer la documentación de ningún componente, si por ejemplo estas usando aws-s3 (ya sea para origen o para destino), si leemos la implementación, veremos que está usando el SDK de AWS por lo que nada evitará tener que conocerlo, todas las propiedades de configuración las encontrarás en la documentación de Camel pero también en la de AWS. Comparable a querer usar Terraform sin saber AWS, GCP o Azure, o querer usar TailwindCSS sin saber CSS.
Este artículo es el inicio de una serie donde iremos entrando más en detalle, hasta llegar al punto de hablar de algunos componentes concretos, con ejemplos de uso y buenas prácticas.
Author
-
Software developer with over 16 years experience working as Fullstack Developer & Backend Developer.
Ver todas las entradas