En el post anterior ya incluimos el concepto de service, en este post explicaremos realmente lo que es.

Hasta ahora habíamos estado trabajando con objetos de tipo POD, si hacemos un poco de memoría o volvieramos a probar veriamos como no realmente no podíamos acceder realmente desde nuestro equipo a ningún contenedor a no ser que hicieramos algún mapeo externo de puertos o similar, pues bien para esto son los objetos de tipo de Service, estos objetos entre otras cosas nos permiten exponer hacía afuera los nodos del cluster que nosotros le indiquemos.

Conceptualmente puede ser algún similar a un nginx o por ejemplo si pensamos en cloud podría ser como un API Gateway de AWS (ojo que digo conceptualmente para que entendamos un poco cual es su función). Realmente el objetivo de un objeto service es proveer una dirección o forma estable de acceder a nuestros nodos del cluster, ya que cada vez que se levanten nuestros nodos pueden tener una dirección IP diferente, es más, usar un objeto service realmente no implica necesariamente que expongamos nuestros nodos, podemos simplemente crear una IP interna del cluster para acceder a un conjunto de nodos desde otros nodos, ya depende realmente de nuestras necesidades.

Veamos un ejemplo de configuración de un objeto de tipo service. Para ello lo más fácil es hacer lo mismo que hicimos cuando veiamos los pods, ejecutaremos un comando con el flag dry-run para que no haga nada.

kubectl expose pod/mario-test --port 8080 --type=NodePort --dry-run=client -o yaml > mario-service.yaml

Ya hablaremos sobre los tipos por el momento solo ejecutaremos el comando y abrimos el fichero que nos crea

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: mario-game
    run: mario-test
    tier: games
  name: mario-test
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: mario-game
    run: mario-test
    tier: games
  type: NodePort
status:
  loadBalancer: {}

Vemos como ahora la propiedad kind cambia y ahora es Service, pero hablemos un poco más en específico de lo que estamos viendo.

Primero de todo tenemos metadata, que como información relevante tiene:

  • labels: Conjunto de clave valor que en un principio es meramente informativo y que podemos incluir los que queramos. Esto según el caso puede servir como identificador, por ejemplo, en mi caso tiene los mismos labels que mi pod, en vuestro caso puede contener otros si habéis puesto algo distinto en los labels. Podéis hacer la prueba eliminando el pod actual, modificando los labels del fichero pod que estéis usando, creando el pod de nuevo y ejecutando el comando dry-run de nuevo para ver como cambia
  • name: El nombre con el que identificaremos el servicio

A continuación tenemos spec que al igual que con los pods es donde esta todo lo importante, es decir, lo que queremos crear realmente o que configuración debe llevar, veamoslo un poco en detalle:

  • selector: Empezamos por este porque nos ayuda a saber donde apunta realmente el service. Si nos fijamos en su contenido debe tener lo mismo que los labels que tiene nuestro pod. Esto es porque realmente esta propiedad es la que se usa para filtrar y encontrar los pods en cuestión. Pensemos que realmente todo esto esta pensado para tener varias instancias del mismo pod, entonces como las identifica el service? Hace uso de los labels que le hemos puesto al pod y con eso filtra o busca los contenedores en cuestión.
  • type: Como tal es posible crear 3 tipos de servicio.
    • Tipo ClusterIP: Nos proporcionaría una ip fija interna del cluster para todas las instancias de un mismo pod para que el resto de pods puedan hacer uso de ellos
    • Tipo NodePort: Expone hacia el exterior del cluster el conjunto de pods seleccionados.
    • Tipo LoadBalancer: Se usa realmente con kubernetes en producción en algún proveedor cloud que son capaces de hacer uso de esta configuración para integrarlo con sus balanceadores.
  • ports: Parte muy importante, en este punto definimos los puertos por los que se puede acceder a nuestro contenedor a través del servicio (bueno y también el protocolo como veis):
    • port: Puerto interno del cluster por el que el resto de pods pueden acceder a la aplicación que se despliegue en el pod (suele coincidir con targetPort)
    • targetPort: Puerto que expone el contenedor o pod de su servicio, este seria al puerto que redireccionará automáticamente el service cuando se accede desde fuera del cluster al pod (suele coincidir con port)
    • nodePort (opcional): Aunque no aparezca ahora mismo tenemos esta opción también que es el puerto que expondra el servicio para acceder al targetPort seleccionado y se usará cuando pongamos el type=NodePort como os podéis imaginar. Tiene un rango específico -> 30000-32767. A no ser que lo queramos tener identificado, podemos dejar que el servicio decida de manera random uno dentro de ese rango.

Estas serían las configuraciones principales o básicas sobre un Service, como siempre recomiendo ver la documentación para ver los detalles y más configuraciones que nos puedieran interesar -> Kubernetes Service

Veamos ahora como funciona todo esto, pero antes de continuar comentar que lo que se explica aquí puede dar algún problema o ser distinto según la configuración con la que tengáis levantado minikube o vuestro servicio de kubernetes que estéis usando, en mi caso el ejemplo para el blog lo estoy haciendo en un mac con Docker Desktop por lo que es necesario hacer algún extra para que funcione el Service.

Primero de todo creemos el servicio:

kubectl create -f mario-service.yaml

Y una vez creado veamos que tenemos

kubectl get svc

Captura-de-pantalla-2021-01-12-a-las-21.09.15

Como vemos tenemos dos servicios el de kubernetes que es el de por defecto, (de tipo ClusterIP y es el servicio que comunica con el server master de kubernetes) y el que acabamos de crear mario-test de tipo NodePort, es decir, que "técnicamente" deberiamos poder acceder desde nuestra máquina al contenedor en cuestión al que apunta usando el puerto que ha seleccionado kubernetes automáticamente, en mi caso el 32142

Si intentamos acceder a través del navegador con localhost:32142 deberíamos poder ver el fork de Mario

Captura-de-pantalla-2021-01-14-a-las-20.09.01

He comentado antes el "técnicamente" porque es posible (como es mi caso con el entorno de ejemplo) que aún asi no podamos acceder, ahora veremos porque.

Kubernetes como tal no está pensado para funcionar en un portatil o en el equipo típico de sobremesa, es decir, si estas usando la aplicación "Docker Desktop" para jugar con kubernetes (en lugar de simplemente tener el Docker Engine) esto no te va a funcionar, es decir, si no has creado una máquina virtual para esto o no estas usando un servidor real de kubernetes seguirás sin poder acceder. Para poder acceder tendremos que hacer uso de un port-forward. REPITO ESTO NO SE TIENE QUE HACER EN UN ENTORNO EN PRODUCCIÓN A NO SER QUE SEPAS LO QUE HACES, LO HACEMOS POR NECESIDAD EN NUESTRO ENTORNO DE PRUEBAS PARA ACCEDER AL CONTENEDOR

Para este caso no es necesario que tengamos un service creado ni nada (pongo esta opción solo para que podamos acceder a nuestro contenedor, no tiene que ver con nuestro uso de services). Lo que haremos será ejecutar

kubectl port-forward mario-test 8000:8080

Con este comando simplemente hacemos una redirección de nuestro puerto local 8000 al puerto 8080 del pod mario-test. Al ejecutar este comando deberíamos ver algo similar a esto

Captura-de-pantalla-2021-01-14-a-las-20.13.32

Y si entramos con un navegador a localhost:8000 veremos como nos aparece la aplicación y en la consola (donde debe permanecer en ejecución el port-forward) esta gestionando conexiones del puerto 8000

Captura-de-pantalla-2021-01-14-a-las-20.15.05

Tras el inciso para mostrar como acceder en el caso de que no tengas un entorno similar al de producción de kubernetes, continuamos un poco hablando de los tipos de Service

En el ejemplo anterior hemos visto como sería un service de tipo NodePort, he querido mostrar este ejemplo porque es similar al de ClusterIP pero nos permite comprobar el acceso sin complicarnos creando más pods ni nada así.
Si pensamos en el tipo ClusterIP, nos quedaremos con que nos permite acceder a un conjunto de pods usando una misma dirección IP o nombre de host desde dentro del propio cluster para que puedan tener comunicación entre sí, sobre todo cuando tenemos intención de crear más de una instancia de un mismo pod (que sería para lo que usamos kubernetes realmente creo yo jejejeje). Hago incapié en esto del hostname, como tal el nombre del service es también el hostname que podremos usar para acceder al conjunto de pods dentro del cluster.

Si, por ejemplo, eliminamos el servicio actual

kubectl delete svc mario-test

Cambiamos el fichero del service indicandole que es de tipo ClusterIP

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: mario-game
    run: mario-test
    tier: games
  name: mario-service
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: mario-game
    run: mario-test
    tier: games
  type: ClusterIP <-- cambiamos esta linea
status:
  loadBalancer: {}

Y ejecutamos de nuevo el create

kubectl create -f mario-service.yaml

Veremos como haciendo un get de los servicios tenemos otro de tipo ClusterIP. Como detalle fijarse en que ahora en puertos solo nos aparece uno, es decir, solo nos aparece el puerto interno del cluster, es decir, al que redireccionariamos si usáramos el tipo NodePort.

Captura-de-pantalla-2021-01-14-a-las-20.26.34

No me extiendo en esta parte porque no tiene mucho más, si quisierais probarlo solo es necesario crear otro pod, acceder al contenedor y lanzar una petición, como ejemplo rápido

Ejecutamos y nos adjuntamos a la consola

kubectl run --rm -i --tty alpine-pod --image=alpine --restart=Never -- sh

Con esto le indicamos que queremos un pod con el nombre alpine-pod, que use la imagen de docker de alpine. Los extras como el rm que se elimine al terminar la ejecución y el último relevante es el sh para acceder a la consola.

Si todo funciona os aparecera un promt para ejecutar comandos

Captura-de-pantalla-2021-01-14-a-las-20.47.25

Ejecutamos

wget mario-service:8080 

Y tenemos que se ha bajado el index.html de nuestro contenedor

Captura-de-pantalla-2021-01-14-a-las-20.48.05

(para salir con ejecutar exit vale ;) y veremos como nos aviso de que elimina el pod )

Es una prueba muy rápida pero creo que es suficiente para entender el funcionamiento de un service de tipo ClusterIP, aquí tenemos que plantearnos que en un entorno en producción lo más probable es que tengamos mas de un contenedor del mismo tipo de POD.

Nos faltaría por ver el tipo loadBalancer pero como esta relacionado más con entornos cloud lo veremos más adelante, sin mucho más se termina este post nos vemos en el siguiente un abrazooooor