Contratos, direccionamiento y API para microservicios

ID de región

REGION_ID es un código abreviado que Google asigna en función de la región que eliges cuando creas la app. El código no corresponde a un país ni a una provincia, aunque algunos ID de región puedan parecer similares a los códigos de país y provincia que se suelen usar. En el caso de las apps creadas después de febrero de 2020, REGION_ID.r se incluye en las URL de App Engine. En el caso de las apps existentes creadas antes de esta fecha, el ID de región es opcional en la URL.

Obtén más información acerca de los ID de región.

En App Engine, los microservicios suelen llamarse entre sí a través de las API de RESTful basadas en HTTP. También es posible invocar microservicios en segundo plano con la lista de tareas en cola, y se aplican los principios de diseño de API descritos aquí. Es importante seguir ciertos patrones para garantizar que la aplicación basada en microservicios sea estable y segura, y tenga un buen rendimiento.

Utilización de contratos sólidos

Uno de los aspectos más importantes de las aplicaciones basadas en microservicios es la capacidad de implementar microservicios que sean completamente independientes entre sí. Para lograr esa independencia, cada microservicio debe proporcionar un contrato con versiones bien definido a sus clientes, que son otros microservicios. Ningún servicio debe poner fin a estos contratos con versiones hasta tanto no se sepa que no hay microservicios basados en cierto contrato con versiones. Recuerda que puede ser necesario revertir otros microservicios a una versión anterior del código que requiera un contrato previo; por eso, es importante tener en cuenta esa posibilidad en tus políticas de baja.

Lograr una cultura en torno a contratos con versiones sólidos es probablemente el aspecto organizativo más desafiante de una aplicación estable basada en microservicios. Los equipos de desarrollo deben comprender qué implica un cambio rotundo frente a uno no rotundo. Tienen que saber cuándo se requiere una actualización general. Deben comprender cómo y cuándo se puede poner fin a un contrato antiguo. Los equipos deben implementar técnicas apropiadas de comunicación, que incluyan avisos de baja, para garantizar que se conozcan los cambios en los contratos de microservicios. Si bien lo anterior puede sonar abrumador, incorporar estas prácticas en tu cultura de desarrollo producirá grandes mejoras en cuanto a velocidad y calidad a lo largo del tiempo.

Direccionamiento a microservicios

En el trabajo con distintas versiones de código y servicios se admite el direccionamiento específico a cada una de ellas. Como resultado, puedes implementar versiones de código nuevas a la par de otras existentes y probar código nuevo antes de convertirlo en la versión predeterminada en uso.

Cada proyecto de App Engine tiene un servicio predeterminado, y cada servicio tiene una versión de código predeterminada. Para direccionar el servicio predeterminado de la versión predeterminada de un proyecto, usa la siguiente URL:
https://PROJECT_ID.REGION_ID.r.appspot.com

Si implementas un servicio llamado user-service, puedes acceder a la versión en uso predeterminada de ese servicio mediante la siguiente URL:

https://user-service-dot-my-app.REGION_ID.r.appspot.com

Si implementas una versión de código secundaria no predeterminada con el nombre banana en el servicio user-service, puedes acceder directamente a ella con la siguiente URL:

https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com

Ten en cuenta que si implementas una versión de código secundaria no predeterminada con el nombrecherry en el servicio default, puedes acceder a ella con la siguiente URL:

https://cherry-dot-my-app.REGION_ID.r.appspot.com

App Engine aplica la regla según la cual los nombres de las versiones de código en el servicio predeterminado no pueden entrar en conflicto con los nombres de los servicios.

El direccionamiento a versiones de código específicas solo debería utilizarse en pruebas de humo y para facilitar las pruebas A/B, la puesta al día y la reversión. El código de tu cliente solo debería dirigirse a la versión predeterminada en uso del servicio predeterminado o de uno específico:


https://PROJECT_ID.REGION_ID.r.appspot.com

https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com

Este estilo de direccionamiento permite que los microservicios implementen versiones nuevas de sus servicios, que pueden incluir la corrección de errores, sin necesidad de incorporar cambios en los clientes.

Usa versiones de API

Cada API de microservicio debería tener una versión principal de API en la URL, como se ilustra a continuación:

/user-service/v1/

Esta versión principal de API identifica con claridad en los registros qué versión de API del microservicio se está llamando. Más importante aún, la versión principal de API genera diferentes URL, de modo que se pueden entregar las nuevas versiones principales de API junto con las antiguas:

/user-service/v1/
/user-service/v2/

No es necesario incluir la versión secundaria de API en la URL, ya que por definición esas versiones no producen cambios rotundos. De hecho, hacerlo daría lugar a una proliferación de URL y causaría incertidumbre sobre la capacidad de un cliente para pasar a una nueva versión secundaria de API.

Ten en cuenta que, en este artículo, se supone que se trabaja con un entorno de integración y entrega continuas, en el que la rama principal siempre se implementa en App Engine. En este artículo, se usan dos conceptos distintos de versión:

  • Versión de código, que tiene correspondencia directa con una versión de servicio de App Engine y representa una etiqueta de confirmación particular de la rama principal

  • Versión de API, que tiene correspondencia directa con una URL de API y representa la forma de los argumentos de solicitud y del documento de respuesta, y el comportamiento de la API

En este artículo, también se supone que, con una sola implementación de código, se implementarán versiones de una API antiguas y nuevas en una versión de código común. Por ejemplo, tu rama principal implementada podría implementar /user-service/v1/ y /user-service/v2/. Cuando se lanzan nuevas versiones secundarias y de parche, este enfoque te permite dividir el tráfico entre dos versiones de código, independientemente de las versiones de API que el código implementa en realidad.

Tu organización puede elegir desarrollar tu /user-service/v1/ y /user-service/v2/ en ramas de código diferente; es decir, ninguna implementación de código implementará ambas al mismo tiempo. Este modelo también es viable en App Engine, pero para dividir el tráfico tendrías que trasladar la versión de API principal al nombre del servicio. Por ejemplo, tus clientes usarían las URL siguientes:

http://user-service-v1.my-app.REGION_ID.r.appspot.com/user-service/v1/
http://user-service-v2.my-app.REGION_IDappspot.com/user-service/v2/

La versión de API principal se traslada al nombre del servicio, como user-service-v1 y user-service-v2. (Las partes /v1/ y /v2/ de la ruta son redundantes en este modelo y pueden quitarse, aunque podrían ser útiles en el análisis de registros). Este modelo requiere un poco más de trabajo porque es probable que exija realizar actualizaciones en las secuencias de comandos de tus implementaciones para implementar servicios nuevos en cambios de versiones de API principales. Además, ten en cuenta la cantidad máxima de servicios permitidos por aplicación de App Engine.

Cambios rotundos frente a no rotundos

Es importante entender la diferencia entre los cambios rotundos y los no rotundos. Los primeros suelen ser sustractivos, es decir que eliminan parte del documento de respuesta o de la solicitud. El cambio de forma del documento o del nombre de las claves puede producir un cambio rotundo. Los argumentos obligatorios nuevos siempre son cambios rotundos. También pueden producirse este tipo de cambios si se modifica el comportamiento del microservicio.

Los cambios no rotundos suelen ser aditivos. Un argumento opcional nuevo en una solicitud o una sección adicional nueva en el documento de respuesta son cambios no rotundos. Para lograr cambios no rotundos, la elección de la serialización en línea es esencial. Muchas serializaciones son compatibles con cambios no rotundos, como JSON, Protocol Buffers o Thrift. Cuando se deserializan, estas serializaciones ignoran de manera silenciosa la información adicional inesperada. En los lenguajes dinámicos, la información adicional simplemente aparece en el objeto deserializado.

Observa la siguiente definición de JSON para el servicio /user-service/v1/:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com"
}

El siguiente cambio rotundo requerirá que se modifique la versión del servicio a /user-service/v2/:

{
  "userId": "UID-123",
  "name": "Jake Cole",  # combined fields
  "email": "jcole@example.com"  # key change
}

Sin embargo, el cambio no rotundo a continuación no requiere una versión nueva:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com",
  "company": "Acme Corp."  # new key
}

Cómo implementar nuevas versiones secundarias de API con cambios no rotundos

Cuando se implementa una nueva versión secundaria de API, App Engine permite que la nueva versión de código se distribuya junto con la antigua. Aunque en esta plataforma puedes dirigirte específicamente a cualquiera de las versiones implementadas, solo una es la versión predeterminada en uso; recuerda que hay una versión predeterminada en uso para cada servicio. En este ejemplo, hay una versión de código anterior con el nombre apple, que es la versión predeterminada en uso, y la versión de código nueva con el nombre banana se implementa en paralelo. Ten en cuenta que las URL del microservicio son las mismas para ambas, /user-service/v1/, ya que lo que se implementa es un cambio no rotundo de API secundaria.

App Engine proporciona mecanismos a fin de migrar el tráfico desde apple hasta banana automáticamente. Para ello, se debe marcar la versión de código nueva banana como la versión en uso predeterminada. Una vez que se configura la versión en uso predeterminada nueva, todas las solicitudes nuevas se enrutarán a banana en lugar de a apple. Así es como se avanza hacia una nueva versión de código que implementa una versión de API secundaria o de parche nueva sin causar ningún impacto en los microservicios del cliente.

Si se produce un error, es posible revertir el proceso: se debe configurar la versión antigua, apple en nuestro ejemplo, como la versión en uso predeterminada. Las solicitudes nuevas se enrutarán hacia la versión de código anterior en lugar de a banana. Ten en cuenta que se permite que se completen las solicitudes en curso.

App Engine también brinda la capacidad de dirigir solo un porcentaje del tráfico a tu versión de código nueva. Este proceso se conoce como lanzamiento canary y el mecanismo se denomina división del tráfico en App Engine. Puedes dirigir el 1%, el 10%, el 50% o cualquier porcentaje del tráfico que desees a tus versiones de código nuevas, y ajustar ese valor a lo largo del tiempo. Por ejemplo, podrías implementar tu nueva versión de código a lo largo de 15 minutos, incrementar el tráfico de a poco y prestar atención a situaciones que pudieran indicar la necesidad de ejecutar una reversión. Este mecanismo también te permite hacer pruebas A/B entre dos versiones de código: divide el tráfico al 50% y compara las características de tasa de error y rendimiento de ambas versiones de código para confirmar las mejoras esperadas.

En la siguiente imagen, se muestra la configuración de la división de tráfico en la consola de Google Cloud:

Configuración de la división del tráfico en la consola de Google Cloud

Cómo implementar nuevas versiones principales de API con cambios rotundos

Cuando implementas versiones principales de API rotundas, el proceso de puesta al día y reversión es el mismo que en el caso de versiones secundarias de API no rotundas. Sin embargo, es probable que no tengas que dividir el tráfico o realizar pruebas A/B, ya que la versión de API rotunda es una URL de lanzamiento reciente, como /user-service/v2/. Naturalmente, si cambiaste la implementación subyacente de tu versión de API principal anterior, tal vez quieras recurrir a la división del tráfico para probar que esa versión aún funcione según lo esperado.

Cuando implementes una versión de API principal nueva, debes recordar que tal vez las antiguas sigan en uso. Por ejemplo, es posible que /user-service/v1/ aún esté en uso cuando se lance /user-service/v2/. Este es un elemento esencial del trabajo con versiones de código independientes. Solo deberías poder dar de baja versiones de API principales anteriores tras verificar que ningún otro microservicio las necesite, incluidos aquellos que puedan requerir que se los revierta a una versión de código anterior.

Como ejemplo concreto, imagina que tienes un microservicio, llamado web-app, que depende de otro microservicio, llamado user-service. Imagina que user-service necesita modificar alguna implementación subyacente que imposibilitará la compatibilidad con la versión de API principal anterior que web-app usa actualmente, como contraer firstName y lastName en un único campo llamado name. Es decir, user-service necesita dar de baja una versión de API principal anterior.

Para realizar ese cambio, se deben hacer tres implementaciones por separado:

  • Primero, user-service debe implementar /user-service/v2/ sin perder la compatibilidad con /user-service/v1/. Esta implementación tal vez requiera la escritura de código temporal para permitir la compatibilidad con versiones anteriores, una consecuencia común en las aplicaciones basadas en microservicios

  • Luego, web-app debe implementar código actualizado que modifique su dependencia de /user-service/v1/ a /user-service/v2/

  • Por último, luego de verificar que web-app ya no requiere /user-service/v1/ y que web-app no necesita reversiones, el equipo de user-service puede implementar código que quite el extremo /user-service/v1/ anterior y cualquier código temporal necesario para su compatibilidad.

Si bien toda esta actividad puede parecer ardua, es un proceso esencial para las aplicaciones basadas en microservicios que permite trabajar con ciclos de versiones de desarrollo independientes. Este proceso parece implicar un alto grado de dependencia, pero es importante destacar que cada paso puede llevarse a cabo según cronogramas independientes, y la puesta al día y la reversión se producen dentro del alcance de un microservicio. Solo es fijo el orden de los pasos, que pueden efectuarse a lo largo de muchas horas, días o incluso semanas.

Próximos pasos