Microservicios con Okteto stack

En este blog ya he escrito algunas entradas sobre la plataforma de Okteto (http://okteto.com) y cómo utilizarla para desplegar pequeños proyectos (de forma gratuita) en Kubernetes de una forma simple.

Básicamente Okteto son dos productos diferentes pero relacionados:

  • una herramienta a "instalar" en tu Kubernetes que permite al desarrollador sincronizar su proyecto en un pod del cluster así como poder depurar la aplicación directamente en el mismo (entre otras cosas)

  • un servicio kubernetes con diferentes planes de precios incluida una capa gratuita con suficientes recursos como para desplegar servicios básicos en real (ideal en mi opinión para el aprendizaje de k8s)

En estos post he descrito cómo puedes desplegar desde un static site de fotos (sí, soy culpable, pero a cambio aprendí el concepto de volumenes y cómo definirlos y gestionarlos en k8s), una aplicación Grails (o SpringBoot) etc. todos ellos usando el descriptor propio de k8s (que es bastante verbose) .Así mismo, en todos estos ejemplos siempre ha sido desplegar una aplicación simple y en algunos casos usar una instancia Postgre desplegada mediante el interface gráfico.

En este post voy a contar otra de las funcionalidades con las que cuenta este servicio, okteto stack, y que es propia de él, es decir, no es válida para otro cluster k8s que no sea Okteto, al menos hasta donde yo se.

Okteto stack es muy parecido a un docker-compose (de hecho salvo algunas particularidades podrías usar un docker-compose) donde podemos definir las características principales de nuestro(s) servicio(s) e incluso generar la imagen al estilo de este. La ventaja que tiene es que nos permite desplegar en el cluster de kubernetes servicios sin necesidad de la verbosidad de este (ficheros de cientos de líneas de kubernetes se convierten en unas pocas con Okteto stack).

Objetivo

Para verlo en su conjunto vamos a hacer el ejercicio de desplegar 2 microservicios en el mismo namespace de tal forma que:

  • uno de ellos, API, va a realizar la labor de hacer de ApiGateway, enrutando las llamadas al otro servicio.

  • el otro servicio, Customer-Service, va a persistir entidades en una base de datos "bajo su control"

Vamos a usar lo menos posible características propias de Kubernetes, aunque se podría usar. La idea es no añadir más complejidad en este ejemplo

NOTE

He creado un repositorio donde puedes bajarte el código e intentar desplegarlo en tu cluster. Simplemente descargarlo de https://gitlab.com/jorge-aguilera/okteto-stack y sigue las instrucciones del README

INFO

Para poder ejecutar y desplegar este ejemplo vas a necesitar una cuenta en Okteto así como la herramienta de consola okteto-cli

Customer Service

Customer service es un microservicio destinado a guardar y devolver entidades de Customer para lo que usará una base de datos PostgreSQL.

En un proyecto típico de Docker tendriamos un docker-compose similar a:

okteto-stack.yml
services:

  dbcustomers:
    image: okteto.dev/dbcustomers
    build:
      context: .
      dockerfile: DockerDatabase
      args:
        - DATABASE_NAME
        - DATABASE_USERNAME
        - DATABASE_PASSWORD
    ports:
      - 5432
    volumes:
      - data_customers:/var/lib/postgresql/data/


  customer-service:
    image: okteto.dev/customer-service
    build:
      context: .
      dockerfile: DockerApp
    ports:
      - 8080
    environment:
      - DATABASE_HOST
      - DATABASE_NAME
      - DATABASE_USERNAME
      - DATABASE_PASSWORD
    depends_on:
      dbcustomers:
        condition: service_healthy

volumes:
  data_customers:

y lo podríamos desplegar mediante docker-compose build && docker-compose up -d

Como puedes observar el build requiere de dos ficheros de docker: DockerDatabase y DockerApp

DockerDatabase
FROM postgres:latest

ARG DATABASE_NAME
ARG DATABASE_USERNAME
ARG DATABASE_PASSWORD

ENV POSTGRES_HOST_AUTH_METHOD=trust
ENV POSTGRES_PASSWORD=${DATABASE_PASSWORD}
ENV POSTGRES_DB=${DATABASE_NAME}
ENV POSTGRES_USER=${DATABASE_USERNAME}

ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 5432
CMD ["postgres"]
DockerApp
FROM gradle:7.2.0-jdk11 AS build
COPY . /home/gradle
RUN gradle build -x check

FROM openjdk:16-alpine
WORKDIR /home/app
COPY --from=build /home/gradle/build/docker/layers/libs /home/app/libs
COPY --from=build /home/gradle/build/docker/layers/resources /home/app/resources
COPY --from=build /home/gradle/build/docker/layers/application.jar /home/app/application.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/home/app/application.jar"]
INFO

Obviamente todo esto puede ser más simplificado, por ejemplo no necesitarias construir una imagen para Postgre y podrías usar la standard. Así mismo tampoco necesitarías el DockerApp si construyes y subes la imagen a tu repo mediante las herramientas que uses habitualmente.

Si en el directorio del servicio customer ejecutamos:

okteto deploy stack

y todo va bien, habrás desplegado un stack customers donde se está ejecutando dos pods: un postgresql y un servicio

API

Api sigue el mismo principio pero su fichero es más simple puesto que no necesita base de datos. Sin embargo su okteto-stack.yml ya no es compatible con docker-compose porque vamos a incluir una funcionalidad propia de esta plataforma:

okteto-stack.yml
services:
  api:
    image: okteto.dev/api
    build:
      context: .
      dockerfile: DockerApp
    ports:
      - 8080

endpoints:
  - path: /
    service: api
    port: 8080

Si en el proyecto api ejecutamos:

okteto stack deploy

veremos que aparece un nuevo stack api`en nuestro namespace con un sólo pod el cual además ofrece un endpoint abierto a internet en `https://api-TUNAMESPACE.cloud.okteto.net/

Probando

Si hemos conseguido desplegar los dos stacks ahora podriamos ejecutar peticiones REST a API el cual las enrutará a Customer (yo utilizo httpie pero puedes usar curl, postman o la herramienta que uses habitualmente)

Si no hay ningún error el primer comando habrá creado un customer con el name igual a test y la segunda llamada nos devolverá una lista de un sólo elemento

Actualizando

Una vez validado que nuestros dos stacks se encuentran funcionando y hablan entre sí, vamos a realizar un cambio en uno de ellos y redesplegarlo sin necesidad de actualizar el otro.

Por ejemplo, vamos a editar CustomerController.java en el proyecto de customer-service y vamos a cambiar la línea 27

return customerRepository.save(customerEntity);

por

return customerEntity;

(Simplemente vamos a hacer que ya no se puedan añadir más customers pero sin devolver un error)

Para redesplegar la actualización ejecutaremos desde el proyecto de customer-service:

okteto stack deploy --build

Lo cual va a volver a generar y subir la imagen actualizada para después redesplegar el servicio. Una vez desplegado podremos repetir los pasos de intentar añadir un nuevo customer

y deberíamos ver que aunque el post para crear el customer "otro" no ha dado error en realidad no ha sido guardado y el get nos sigue devolviendo una lista con un sólo elemento.

Lo interesante de este cambio es ver que API no ha sido afectado y seguía ejecutándose.

Microservicios

Si abrimos la configuración de API (application.yml) podemos ver que este está enrutando las llamadas a un servicio del que sólo sabe el nombre:

micronaut:
  application:
    name: api
  http:
    services:
      customers:
        url: http://customer-service:8080

Es decir, API hará de proxy hacia un host que responda a customer-service el cual corresponde con el del stack customer. En una solución más compleja se podría usar sistemas de descubrimiento de servicios pero para este ejemplo es suficiente.

Otra de las ventajas de esta aproximación es que podemos agrupar los servicios por dependencias (por ejemplo customer-service y su base de datos) pudiendo actualizar un stack sin tener que actuar en los otros.

Conclusión

En un petproject en el que estoy trabajando he empezado a usar esta funcionalidad y me está siendo muy valiosa para poder tener separados los diferentes servicios a la vez que defino en cada uno las dependencias. Así por ejemplo tengo unos stacks de infraestructura (Kafka y Databases) y otros para cada servicio

Kafka

name: kafka

services:

  kafdrop:
    image: obsidiandynamics/kafdrop:3.28.0-SNAPSHOT
    ports:
      - 9000
    environment:
      - KAFKA_BROKERCONNECT=kafka:9092
      - JVM_OPTS=-Xms16M -Xmx48M -Xss180K -XX:-TieredCompilation -XX:+UseStringDeduplication -noverify

  zookeeper:
    image: docker.io/bitnami/zookeeper:3-debian-10
    ports:
      - 2181
    volumes:
      - data_zookeeper:/bitnami
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes

  kafka:
    image: docker.io/bitnami/kafka:2-debian-10
    ports:
      - 9092
    volumes:
      - data_kafka:/bitnami
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - ALLOW_PLAINTEXT_LISTENER=yes
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092

volumes:
  data_zookeeper:
    driver_opts:
      size: 1Gi
  data_kafka:
    driver_opts:
      size: 2Gi

Databases

name: databases

services:

  dbcustomer:
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
    image: okteto.dev/dbcustomer
    build:
      context: .
      dockerfile: DockerfileCustomer
      args:
        - USERNAME=$USERNAME
        - PASSWORD=$PASSWORD
    ports:
      - 5432
    volumes:
      - data_customer:/var/lib/postgresql/data/

  dbcore:
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
    image: okteto.dev/dbcore
    build:
      context: .
      dockerfile: DockerfileCore
      args:
        - USERNAME=$USERNAME
        - PASSWORD=$PASSWORD
    ports:
      - 5432
    volumes:
      - data_core:/var/lib/postgresql/data/


volumes:
  data_customer:
    driver_opts:
      size: 1Gi
  data_core:
    driver_opts:
      size: 1Gi
Follow comments at Telegram group Or subscribe to the Channel Telegram channel

2019 - 2021 | Mixed with Bootstrap | Baked with JBake v2.6.7