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)
http https://api-TUNAMESPACE.cloud.okteto.net/api name=test
http https://api-TUNAMESPACE.cloud.okteto.net/api
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
http https://api-TUNAMESPACE.cloud.okteto.net/api name=otro
http https://api-TUNAMESPACE.cloud.okteto.net/api
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
Top comments (0)