Introducción a docker y Kubernetes
Kubernetes y Docker son dos tecnologías que van unidas de la mano. No podemos hablar de una sin hablar de la otra. Empecemos por el principio, empecemos por Docker.
¿Que es Docker? Docker es una herramienta que permite portar aplicaciones junto con todas sus dependencias y conseguir que funcionen en cualquier sistema que tenga Docker instalado, con total independencia del sistema operativo que esté corriendo esa máquina por debajo. Esto es muy interesante para distintas tareas. Permite a los sysadmin hacer despliegues de sistemas con configuraciones complejas en muy poco tiempo. O a los developers centrar sus esfuerzos en desarrollar las aplicaciones sin preocuparse por instalar servicios ni configurarlos.
¿Que es una imagen de docker? Una imagen de docker es un sistema de ficheros minimo que contiene todo lo necesario para arrancar una aplicación. Es decir, contiene un sistema operativo, una aplicación y todas sus dependencias resueltas (librerias, herramientas y ficheros de configuración). Las imágenes de docker estan formadas por varias capas de solo lectura (RO). La capa del sistema operativo (FROM) es la de mas bajo nivel. Sobre la capa FROM se instalan paquetes y librerías en una o varias capas de RUN. Y sobre las capas de RUN se monta otra capa de comando (CMD) con el comando que debe ejedcutarse para arrancar la aplicación. Estas imágenes de docker se pueden descargar desde un repositorio público (DockerHub). En el DockerHub se pueden encontrar imágenes prefabricadas de apache, nginx, postgresql, mysql, oracle, mongodb, ubuntu, centos, php, golang, python, java, jenkins, mediawiki, prestashop, yocto, openvpn y un largo sinfín de servicios. También puedes hacer tu propia imagen a mano usando Dockerfiles y posteriormente publicarla en DockerHub. O incluso hacerte tu propio DockerHub privado. Hay mucha información en Internet sobre Dockerfiles, no voy a profundizar mas en este documento.
¿Que diferencia hay entre una distro de Linux y una imagen de docker? A diferencia de una distro de Linux, una imagen de docker contiene solo lo esencial para ejecutar una aplicación dada. Las imágenes no tienen estados, son de solo lectura, lo cual significa que no se puede cambiar su contenido. Se usan como input para crear un contenedor de docker.
¿Que es un contenedor de docker? Un contenedor es una instancia de una imagen de docker. El contenedor es una capa adicional de lectura/escritura (RW) situada sobre las n capas RO de la imagen. Es la capa donde se ejecuta todo (se crean log, se instalan o borran ficheros, se generan resultados, etc). Los cambios en el contenedor son temporales, se pierden tan pronto muere el contenedor. Podemos hacer un simil con los procesos del sistema operativo y los binarios ejecutables: un proceso es una instancia de un binario ejecutable. El proceso es equivalente al contenedor, de la misma manera que la imagen de docker es equivalente al binario ejecutable.
Pongamos un ejemplo para aclarar las cosas. Tenemos una imagen de docker llamada nginx. Al correr con docker un contenedor de esta imagen, arranca una distro minimalista de Linux sobre la que se levanta un servicio de nginx que expone el puerto 80 para escuchar conexiones. Podemos entrar al contenedor, editar el fichero de configuración de nginx y cambiar el puerto expuesto al puerto 8080. Cuando muera este contenedor y levantemos un segundo contenedor con esa misma imagen de nginx, el nuevo contenedor volverá a levantar nginx escuchando en el puerto 80. Ninguno de los cambios aplicados en el primer contenedor estará presente en el segundo.
¿Que diferencia existe entre docker y una maquina virtual (vmware o virtualbox)? Bien, por encima del hardware tenemos siempre corriendo a nuestro querido sistema operativo Linux. Cuando usamos maquinas virtuales (virtualbox o vmware), por encima de Linux corre una capa de software llamada monitor de máquinas virtuales (VMM). Y sobre el VMM corren los distintos sistemas operativos virtualizados. Cada uno de ellos necesita una instalación completa del sistema operativo, reservando una cantidad ingente de recursos, tanto de disco duro como de memoria RAM. En cambio, cuando usamos docker, por encima de Linux corre el Docker Engine, un proceso mas del sistema operativo sobre el cual se pueden ejecutar directamente los contenedores. Lo cual hace que los recursos consumidos (tanto de disco duro como de memoria RAM) sean muy inferiores. Y por tanto docker aproveche mucho mejor los recursos que una máquina virtual. Al ser mucho mas libiano que una maquina virtual podemos correr muchos mas contenedores que máquinas virtuales, mejorando la utilización de los recursos hardware.
"Everything in Google runs in a container". Fueron las palabras citadas allá por Mayo de 2014 por un tal Joe Beda, por aquel entonces ingeniero de software senior en Google, en una conferencia donde explicó que Google corría 2 billones de contenedores cada semana. Imagino que 6 años mas tarde esa cantidad se habra incrementado significativamente. Imaginaros el trabajo DESCOMUNAL que puede suponer administrar todos esos contenedores. ¿Como garantizo que todos esos contenedores estan funcionando? ¿Como garantizo que ninguno esta colgado consumiendo recursos innecesarios? ¿Como reinicio los contenedores que han muerto? Es imposible hacer todo ese trabajo a mano.
Para dar respuesta a todas estas cuestiones, Google diseñó Kubernetes, un sistema de código libre para realizar el despliegue y el manejo de todos esos contenedores de forma automática. Kubernetes actúa como supervisor de todos los contenedores en ejecución. Si un contenedor cae, Kubernetes elimina sus recursos y levanta uno nuevo. Si un contenedor tiene fugas de memoria y empieza a consumir muchos recursos de memoria RAM, Kubernetes lo mata y crea uno nuevo. Si un contenedor consume mucha CPU, Kubernetes lo mata y crea uno nuevo. Si queremos actualizar un subconjunto de los contenedores que estan dando un servicio en producción a una versión mas nueva, Kubernetes es capaz de actualizar los contenedores sin que el servicio en ejecución se caiga en ningún momento (sin downtime). También nos permite deshacer la actualización (un rollback) en caso de que la actualización tenga algún error. Estos son solo algunos ejemplos de lo que Kubernetes puede hacer por nosotros. Kubernetes soporta diferentes entornos para la ejecución de los contenedores. Pero entre todos ellos destaca Docker, el runtime para contenedores mas popular hoy en día. Y es por eso que hemos hablado de Docker en la introducción.
Arquitectura de Docker
Un PC donde tengamos instalado docker se llama Docker Host. Está compuesto por tres componentes importantes: el Docker Engine, el Docker Client y un API REST. El Docker Engine es el servicio sobre el que corren los contenedores de docker. El Docker Client (la herramienta docker) se comunica con el Docker Engine a traves del API REST. Cuando ejecutamos docker en la linea de comandos (cliente) en realidad nos estamos conectando al API REST del Docker Engine para enviarle nuestras peticiones. Y el Docker Engine utiliza la misma API REST para contestar al cliente.
Arquitectura de Kubernetes
A muy grosso modo, un cluster de Kubernetes se divide en dos partes: un Master y un conjunto de n Nodos.
En el Master reside el cerebro de Kubernetes, es la parte que realiza todo el procesamiento y da las ordenes a los Nodos. En el Master hay un API REST que recibe todas las ordenes desde los clientes. El API acepta requests en formato JSON, por lo que podemos manipular directamente esta API con cualquier lenguaje de programación, enviando las REQUEST en el formato JSON esperado para que el Master actúe en consecuencia. Existe una herramienta llamada kubectl que implementa todos los comandos del API y los convierte en mensajes JSON que envía al Master para hacernos la vida un poco mas facil.
Los Nodos son máquinas (virtuales o físicas) donde corren los contenedores. Por tanto, en los Nodos debemos tener instalado docker (deben ser Docker Host).
A traves del API REST (usando kubectl o una aplicación que nosotros mismos podemos desarrollar) yo le puedo decir al Master: "ok, quiero un nuevo contenedor de nginx corriendo". Y el Master se encarga de escanear los Nodos disponibles en el cluster y elegir el mejor calificado para alojar el nuevo contenedor. Desde el Nodo elegido se descarga la imagen de ngix y se arranca el contenedor.
Instalar un Docker Host
Para instalar docker en tu PC, sigue estos pasos:
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - $ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" $ sudo apt-get update $ sudo apt-get install -y docker-ce
Por defecto, ejecutar el cliente de docker requiere privilegios de root, por lo que debes usar sudo para ejecutarlo. Si quieres evitar tener que escribir sudo cada vez que ejecutes el comando docker, puedes añadir tu usuario al grupo "docker" que se ha creado automáticamente durante la instalación.
$ sudo usermod -aG docker ${YOUR_USERNAME}
Si todo ha ido bien, el demonio de docker debería estar instalado, listo para funcionar y para reiniciarse cada vez que reinicies el Docker Host. Comprueba su estado haciendo esto:
$ systemctl status docker
La instalación ha instalado también la herramienta docker que actúa como cliente para enviar comandos al daemon a traves del API REST. Compruebalo haciendo esto:
$ docker run hello-world
La salida debería mostrar algo así como "Hello from Docker".
Instalar el cliente kubectl
Para instalar la herramienta kubectl sigue estos pasos:
$ curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl $ chmod +x ./kubectl $ mv ./kubectl /usr/local/bin/kubectl
Asegurate de que la versión descargada es reciente:
$ kubectl version --client
Instalar un cluster de Kubernetes
Como cluster de Kubernetes voy a usar minikube. Minikube es un servicio que nos permite simular un cluster muy pequeño de Kubernetes de manera local, compuesto por un Master y un solo Nodo. Con este cluster voy a tener todas las bases necesarias para aprender a usar Kubernetes. El Master va a ser nuestra maquina host y el Nodo también será nuestra máquina host. Con esto es suficiente para empezar a explorar Kubernetes y aprender muchisimas utilidades que son muy útiles.
Como el Nodo de nuestro cluster va a usar Docker, necesitamos instalar docker en el mismo PC en el que voy a instalar minikube. Es un paso que ya hemos hecho antes.
A continuación sigue estos pasos para instalar minikube:
$ sudo -s # apt-get install conntrack # curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 # chmod +x minikube # mv minikube /usr/local/bin # minikube start --vm-driver=none
El ultimo comando se toma su tiempo, paciencia.
NOTA: a instalación desde mi usuario me dio problemas. Para poder trabajar con mi usuario con normalidad, una vez finalizada la instalación desde el usuario root, tuve que hacer estos pasos a mano:
# USER=tu-usuario # mv /root/.kube /home/${USER} # mv /root/.minikube /home/${USER} # chown -R ${USER}.${USER} /home/${USER}/.kube # chown -R ${USER}.${USER} /home/${USER}/.minikube # sed -i "s|/root|/home/${USER}|g" /home/${USER}/.kube/config # sed -i "s|/root|/home/${USER}|g" /home/${USER}/.minikube/machines/minikube/config.json # su - ${USER}
Para verificar que todo ha ido bien, ejecuta este comando. La respuesta esperada es esta:
$ kubectl get pods No resources found in default namespace. $ kubectl cluster-info Kubernetes master is running at https://192.168.1.133:8443 KubeDNS is running at https://192.168.1.133:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy $ minikube status host: Running kubelet: Running apiserver: Running kubeconfig: Configured
Parece que vamos bien!
Los objetos de Kubernetes
En Kubernetes creamos objetos para indicarle al Master cual es el estado deseado de nuestro cluster. Esos objetos son entidades persistentes dentro del cluster. Una vez que has creado un objeto, el sistema se pone a trabajar para asegurar que el objeto existe dentro del cluster. Y hará todos los cambios necesarios en el cluster para asegurar que se cumpla ese nuevo estado. Para manipular objetos, es decir, crearlos, modificarlos o borrarlos, usamos la API de Kubernetes, normalmente a traves del cliente kubectl. Kubernetes tiene un ecosistema de objetos realmente extenso que puedes consultar con este comando:
$ kubectl api-resources NAME SHORTNAMES APIGROUP NAMESPACED KIND configmaps cm true ConfigMap endpoints ep true Endpoints limitranges limits true LimitRange namespaces ns false Namespace persistentvolumeclaims pvc true PersistentVolumeClaim persistentvolumes pv false PersistentVolume pods po true Pod resourcequotas quota true ResourceQuota secrets true Secret serviceaccounts sa true ServiceAccount services svc true Service deployments deploy apps true Deployment replicasets rs apps true ReplicaSet ingresses ing extensions true Ingress clusterrolebindings rbac.authorization.k8s.io false ClusterRoleBinding clusterroles rbac.authorization.k8s.io false ClusterRole rolebindings rbac.authorization.k8s.io true RoleBinding roles rbac.authorization.k8s.io true Role ...
En la salida del comando anterior veremos todos los objetos disponibles del API REST con los que podemos interactuar.
Para crear un objeto en Kubernetes debes especificar su estructura interna en un archivo Yaml. Dentro del archivo Yaml vas a definir campos comunes a todos ellos como el apiVersion (la versión del API REST de Kubernetes que estás usando para crear este objeto), kind (la clase del objeto) o metadata (metadatos para identificar unívocamente al objeto como su nombre o una etiqueta y opcionalmente su Namespace). Después cada objeto tiene una serie de campos específicos de ese objeto que iremos viendo después.
Podemos usar el cliente kubectl para convertir un fichero Yaml a JSON y enviarle al Master del cluster la petición para que cree el objeto y despliegue en el cluster todos los cambios necesarios:
$ kubectl apply -f /path/to/file.yaml $ kubectl apply -f https://www.foo.com/path/to/file.yaml
Fijate que tenemos dos variantes. Bien podemos usar un fichero yaml local creado por nosotros o bien podemos apuntar a un yaml alojado en Internet. Esto nos dá una gran flexibilidad a la hora de trabajar. Para borrar todos los objetos creados por un fichero Yaml en el Master y aplicar el nuevo estado en el cluster, podemos usar estos comandos:
$ kubectl delete -f /path/to/file.yaml $ kubectl delete -f https://www.foo.com/path/to/file.yaml
Veamos ahora algunos de los objetos de Kubernetes en mas detalle:
Namespaces
Los Namespaces básicamente son una separación lógica del cluster dentro del propio cluster. Podemos tener n Namespaces dentro del cluster. Si creamos dos Namespaces distintos, podemos tener objetos diferentes de Kubernetes creados en cada Namespace y entre ellos no se van a ver.
Son utiles para muchas cosas dependiendo del uso que decidamos darles. Se pueden crear Namespaces para desarrollo, producción e integración. O se puede crear un Namespace distinto para cada aplicación que despleguemos dentro del cluster. Tambien se pueden limitar recursos a nivel de Namespaces.
$ cat namespace.yaml apiVersion: v1 kind: Namespace metadata: name: ns-dev labels: name: ns-dev
En este yaml hemos definido un objeto de la clase Namespace llamado ns-dev al que le hemos asignado una etiqueta llamada name con valor ns-dev.
La siguiente es una lista de comandos útiles para para gestionar namespaces.
$ kubectl create namespace <ns-name> $ kubectl delete namespace <ns-name> $ kubectl describe namespace <ns-name> $ kubectl get namespaces $ kubectl get namespaces -l name=development $ kubectl get namespaces --show-labels $ kubectl get ns
Pods
Los Pods son la unidad básica de ejecución dentro de un cluster de Kubernetes. Cada Pod tiene una IP única dentro del cluster, un espacio de memoria independiente y un hostname. El Pod es básicamente un wrapper sobre un pequeño conjunto de contenedores intimamente relacionados. Todos esos contenedores comparten la IP del Pod, su espacio de memoria y hostname.
$ cat pod.yml apiVersion: v1 kind: Pod metadata: name: podtest spec: containers: - name: cont1 image: nginx:alpine
En este yaml hemos definido un objeto de la clase Pod llamado podtest que define un contenedor llamado "cont1" que usará la imagen de nginx:alpine para funcionar.
La siguiente es una lista de comandos útiles para gestionar Pods:
$ kubectl delete pod <pod-name> $ kubectl describe pod <pod-name> $ kubectl get pod <pod-name> -o yaml $ kubectl exec -ti <pod-name> -- sh $ kubectl get po $ kubectl get pods $ kubectl get pods --watch
ReplicaSets
Los Pods por si mismo tienen varios problemas. El primero es que si muere alguno de los contenedores que lo componen, nadie se encarga de reiniciarlo. El segundo es que si quiero 1000 replicas de mi Pod, tengo que editar y crear a mano 1000 copias de mi Pod dentro del fichero pod.yaml. No parece muy practico. ¿Como resolver estos problemas?
Utilizando un ReplicaSet, otro objeto de Kubernetes que está en un nivel de abstracción mas alto que los Pods. ¿Por que? Pues porque el ReplicaSet crea Pods. Yo le digo al objeto ReplicaSet: "me vas a crear N replicas de este Pod". Y el ReplicaSet se encargará de crear esas N replicas. Si alguna de esas replicas muere, el ReplicaSet se dará cuenta de ello y creará otro Pod de las mismas características para garantizar que el numero total de Pods es igual a N.
El ReplicaSet utiliza un label que nosotros definimos para indicar cual es el Pod que debe tomar para replicar.
Cada Pod creado y unido a un ReplicaSet RS1 utiliza una referencia interna para apuntar a su ReplicaSet padre (RS1). De forma que si creamos un segundo objeto ReplicaSet RS2, éste solo podrá tomar Pods huerfanos, que todavía no tengan un ReplicaSet padre asignado.
$ cat rs.yaml apiVersion: apps/v1 kind: ReplicaSet metadata: name: rs-test labels: app: rs-test spec: replicas: 5 selector: matchLabels: app: pod-label template: metadata: labels: app: pod-label spec: containers: - name: cont1 image: nginx:1.17.10
En este caso hemos creado un objeto de la clase ReplicaSet llamado rs-test. Indicamos que nos interesan 5 replicas y que debe usar Pods que hagan match con la etiqueta app con valor pod-label. Despues en la template del contenedor le ponemos la misma etiqueta app con valor pod-label para que el ReplicaSet haga match con ese Pod. Finalmente definimos el contenedor que correrá una imagen de nginx en su versión 1.17.10
¿Para que quiero yo tener tantas replicas de un Pod? Una razón para tener los Pods replicados es para alta disponibilidad: en caso que uno de ellos muera, todavía voy a tener 4 Pods sirviendo la pagina index.html. Si mueren 3 Pods, sigo teniendo dos mas disponibles para servir la pagina index.html. Obviamente el ReplicaSet de va a encargar de recrear los Pods cuando mueran. Otra razón para tener los Pods replicados es que cuantos mas Pods tengamos, voy a ser capaces de manejar mas carga. Por ejemplo, si un Pod es capaz de de manejar 100 peticiones/sec, entonces 5 Pods serán capaces de manejar 500 peticiones/sec. En esencia esta la idea de escalar horizontalmente, para ser capaces de manejar mas tráfico.
La siguiente es una lista de comandos útiles para gestionar ReplicaSet:
$ kubectl get replicaset $ kubectl get rs $ kubectl describe rs <rs-name> $ kubectl get rs <rs-name> -o yaml $ kubectl get rs --show-labels
Deployments
El ReplicaSet también tiene problemas. Imagina que por alguna razón quiero actualizar la imagen de mis contenedores y usar nginx:1.18.0 en lugar de nginx:1.17.10. Si aplico los cambios en el yaml no se van a actualizar. ¿Por que? Pues por la sencilla razón de que el ReplicaSet solo esta pendiente del número de replicas que hay en funcionamiento que cumplan las condiciones del selector (labels). Si hay cambios a nivel de template del Pod, al aplicar los cambios, el ReplicaSet no se va a enterar. Por tanto, no cambiará NADA. Esto se traduce en que un ReplicaSet no puede actualizar los Pods, para cambiar por ejemplo la imagen de los contenedores.
Para solucionar este problema se crea un nuevo objeto de Kubernetes: el Deployment. Al crear un Deployment tengo que especificar un template para mi ReplicaSet. Es decir, al crear un Deployment, éste va a crear un ReplicaSet. Y en el template de mi ReplicaSet yo le digo el template de los Pod (imagen) y cuantos quiero. Lo que significa que también se van a crear Pods, debido al ReplicaSet que se crea.
¿Que pasa si quiero actualizar el contenedor? El Deployment se da cuenta que debe actualizar el ReplicaSet RS. Y para ello crea un segundo ReplicaSet RS2 guiado por dos parámetros: MaxUnavailable y MaxSurge. El parámetro MaxUnavailable significa el % de Pods que voy a permitir que mueran. Un 25% significa que como máximo voy a permitir que un 25% de los Pods de RS estén muertos. El MaxSurge significa el % por encima del 100% que voy a permitir que esten vivos. Un 25% significa que como maximo tendre 125% de los Pods de RS estén vivos. Entonces el Deployment irá matando Pods de RS (hasta que queden 0 Pods en RS) y creando Pods en RS2, hasta tener N Pods en ejecución en RS2. Recordemos que los Pods de RS2 ya corren la imagen nueva de nginx:1.18.0
$ cat dep.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deployment-test labels: app: front spec: replicas: 5 selector: matchLabels: app: front template: metadata: labels: app: front spec: containers: - name: nginx image: nginx:1.18.0
Aqui creamos un objeto de la clase Deployment. En sus metadatos definimos su nombre "deployment-test" y una etiqueta app con valor front. A continuación en el campo "spec" empieza a describir el objeto ReplicaSet, que ya conocemos. Tenemos el numero de replicas deseadas (5 replicas) y el selector, es decir, cual va a ser el label que el ReplicaSet va a utilizar para encontrar a sus Pod. Despues viene el campo "template", que define básicamente el Pod que voy a crear. Obviamente, el Pod tiene que cumplir con el selector del ReplicaSet. Por tanto, voy a ponerle una etiqueta (label) igual a la que hemos definido en el selector (app=front). Ya por último, dentro del campo "spec" especificamos a los contenedores que forman parte del Pod. Vemos que sera un solo contenedor llamado nginx que usará la imagen nginx:alpine.
La siguiente es una lista de comandos útiles para gestionar ReplicaSet:
$ kubectl get deployment $ kubectl delete deployment <deploy-name> $ kubectl describe deployment <deploy-name>
Services
Sabemos que cada Pod corriendo dentro del cluster tiene una dirección IP diferente. Entonces, si dentro de un objeto ReplicaSet tenemos varios Pods replicados, y cada uno tiene su propia dirección IP. ¿como accedemos a todos los Pods que están corriendo en un Deployment desde un solo punto?
Para resolver este problema, en Kubernetes existe un objeto que se llama Service. Básicamente un Service es un objeto aislado, independiente, que tiene una IP fija única dentro del cluster que Kubernetes se encarga de garantizar durante todo el tiempo de vida de ese objeto.
El Service define un selector que básicamente es una etiqueta que tienen que cumplir todos los Pods que controle este objeto Service. El Service mantiene una lista de endpoints actualizada en tiempo real. Si un Pod con la etiqueta del selector se crea, su IP se añade a la lista de endpoints del Service. Si un Pod con la etiqueta del selector se destruye, su IP se elimina de la lista de endpoints del Service.
Yo como usuario le podré hacer solicitudes a la IP del objeto Service. Y el objeto Service se encargará de reenviar la petición a la IP de uno de los Pod que mantenga en su lista de endpoints. Dos peticiones diferentes no tienen por que ser atendidas por el mismo Pod. Dicho de otro modo, el Service va a estar actuando como balanceador de carga sobre los Pods que esté monitorizando. Por otro lado, al disponer de varios Pods replicados, tenemos alta disponibilidad del servicio.
Hay varios tipos de objeto Service. Los dos mas importantes son ClusterIP y NodePort. El Service de tipo ClusterIP asigna al objeto Service una IP interna del cluster que es accesible por otros Pods del cluster. Pero esta IP no es accesible desde Internet. En cambio, el Service el tipo NodePort, además de asignar al objeto Service una IP interna del cluster, expone un puerto del Nodo del cluster hacia el exterior, de forma que a través de ese puerto podamos consumir nuestro servicio desde fuera del cluster, usando la IP del Nodo.
$ cat svc.yaml apiVersion: v1 kind: Service metadata: name: my-service labels: app: front spec: type: NodePort selector: app: front ports: - protocol: TCP port: 8080 targetPort: 80
Aquí creamos un objeto de la clase Service llamado my-service del tipo ClusterIP. El selector indica que este objeto elegirá a los Pod que definan una etiqueta app con valor front para añadirlos a su lista de endpoints. Las conexiones a este Pod por el puerto 8080 se redirigiran al puerto 80. Para que sea el Pod escuchando en el puerto 80 (corriendo nginx) el que atienda la petición.
La siguiente es una lista de comandos útiles para gestionar Services:
$ kubectl get svc $ kubectl get svc -l app=front $ kubectl get svc -n <ns-name> $ kubectl describe svc <svc-name> $ kubectl describe endpoints <svc-name>
Manos a la obra con un ejemplo práctico
En este ejemplo voy a poner en práctica todos los conceptos que hemos explicado durante este articulo. Voy a crear un objeto Deployment con 5 replicas de contenedores que van a correr una imagen de nginx. Después voy a crear un objeto Service de tipo NodePort para acceder al servicio nginx desde cualquier IP de la LAN donde tengas conectado el PC donde está corriendo tu Nodo del cluster. Después haremos algos experimentos para ver como se actualiza un Deployment y comprobaremos que funcione el balanceador de carga.
El yaml de este servicio lo he dejado alojado en mi cuenta personal de GitHub. Puedes verlo aquí: https://raw.githubusercontent.com/aicastell/k8s-resources/master/service/blog.yaml. Si abres el fichero en tu browser veras la definición de dos objetos de Kubernetes: el Deployment y el Service de tipo NodePort. La definición de ambos objetos es muy parecida a la que hemos hecho en secciones anteriores. Fijate que se pueden definir varios objetos de Kubernetes en un mismo fichero de yaml separando cada objeto por tres guiones "---". Vamos a aplicar este fichero yaml al vuelo, sin necesidad de descargarlo desde Internet en un archivo local:
$ kubectl apply -f https://github.com/aicastell/k8s-resources/blob/master/service/blog.yaml
Vamos a consultar los Pods disponibles:
$ kubectl get pods NAME READY STATUS RESTARTS AGE my-deployment-7768665d6b-7rn7q 1/1 Running 0 52s my-deployment-7768665d6b-9xrvb 1/1 Running 0 52s my-deployment-7768665d6b-s6d8l 1/1 Running 0 52s my-deployment-7768665d6b-wkm24 1/1 Running 0 52s my-deployment-7768665d6b-xxf54 1/1 Running 0 52s
Vemos que hay 5 Pods corriendo, tal y como esperábamos. Vamos a eliminar uno de esos Pods a ver que ocurre. Eliminemos el llamado my-deployment-7768665d6b-7rn7q:
$ kubectl delete pod my-deployment-7768665d6b-7rn7q pod "my-deployment-7768665d6b-7rn7q" deleted
Seguidamente vuelvo a consultar los Pods disponibles:
$ kubectl get pods NAME READY STATUS RESTARTS AGE my-deployment-7768665d6b-9xrvb 1/1 Running 0 2m44s my-deployment-7768665d6b-c8p9l 1/1 Running 0 15s my-deployment-7768665d6b-s6d8l 1/1 Running 0 2m44s my-deployment-7768665d6b-wkm24 1/1 Running 0 2m44s my-deployment-7768665d6b-xxf54 1/1 Running 0 2m44s
Vemos como de nuevo tenemos 5 Pods disponibles. El ReplicaSet ha actuado generando un nuevo Pod hace exactamente 15 segundos.
Veamos ahora los Service disponibles:
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 none 443/TCP 17h my-service NodePort 10.97.76.248 none 8080:30498/TCP 4m12s
Vemos que tenemos dos objetos de tipo Service: kubernetes y my-service. El Service llamado "kubernetes" de tipo ClusterIP es creado por el propio Kubernetes cuando se arranca el cluster, pero no voy a entrar en detalles. El Service llamado "my-service" de tipo NodePort es el que hemos creado antes. Vemos como tiene una IP interna del cluster 10.97.76.248 y vemos que está exponiendo el puerto TCP 30498 del nodo. Desde cualquier IP de nuestra LAN podemos validar si el servicio contesta, haciendo un GET por http a la IP de nuestro Nodo y al puerto 30498, de esta manera:
$ curl 192.168.1.133:30498 Welcome to nginx! If you see this page, the nginx web server is successfully installed and working. Further configuration is required. For online documentation and support please refer to nginx.org. Commercial support is available at nginx.com. Thank you for using nginx.
Y aquí tenemos la respuesta de nuestro servicio nginx. ¿No te parece maravilloso?
Pero voy a ir mas lejos. Vamos ahora a hacer una actualización en todos los Pods de nuestro Deployment. Vamos a modificar el fichero index.html que sirven los contenedores. Para ello descarga el fichero blog.yaml en el PC donde estas corriendo Kubernetes:
$ wget https://raw.githubusercontent.com/aicastell/k8s-resources/master/service/blog.yaml
Después edita el fichero y borra el comentario (borra el caracter #).
Graba los cambios, sal del fichero y aplica la nueva configuración:
$ kubectl apply -f blog.yaml deployment.apps/my-deployment configured service/my-service unchanged
En la respuesta que nos da kubectl vemos que el objeto Deployment ha sido configurado y el Service no ha sufrido modificaciones. Si listamos ahora los Pods en ejecución:
$ kubectl get pods NAME READY STATUS RESTARTS AGE my-deployment-86cb867556-4spks 1/1 Running 0 34s my-deployment-86cb867556-rvm79 1/1 Running 0 40s my-deployment-86cb867556-s596q 1/1 Running 0 40s my-deployment-86cb867556-zc6s7 1/1 Running 0 35s my-deployment-86cb867556-zdgsp 1/1 Running 0 40s
Vemos que todos los Pods son nuevos, todos los nombres han cambiado. ¿Como ha ocurrido esto? El objeto Deployment ha creado un nuevo ReplicaSet, y ha ido matando los Pods viejos y creando los Pods nuevos hasta disponer del número de 5 Pods en el ReplicaSet nuevo (el numero deseado) y 0 Pods en el ReplicaSet viejo.
¿Que pasa ahora si accedemos varias veces a nuestro servicio?
$ curl 192.168.1.133:30498 Hello from my-deployment-86cb867556-rvm79 $ curl 192.168.1.133:30498 Hello from my-deployment-86cb867556-zdgsp $ curl 192.168.1.133:30498 Hello from my-deployment-86cb867556-4spks $ curl 192.168.1.133:30498 Hello from my-deployment-86cb867556-4spks $ curl 192.168.1.133:30498 Hello from my-deployment-86cb867556-s596q
Vemos como nuestros Pods están contestando con un index.html diferente. Por tanto la actualización del contenedor se ha realizado correctamente. Pero también vemos como cada petición GET la atiende un Pod diferente. Lo cual indica que estamos balanceando carga. ¿No os parece impresionante? :)
Cierre del articulo
He dejado muchos objetos de Kubernetes sin comentar. Temas como limitar recursos, control de acceso a usuarios, volumenes, y otras muchas cosas. Pero Kubernetes es una herramienta muy extensa y explicar todos estos conceptos en un solo post haría esta entrada interminable.
Con los temas explicados creo que os habréis hecho una buena idea de lo que és Kubernetes, como funciona y para qué os puede ser útil. Este puede ser un buen punto de partida para que os animéis a experimentar con esta herramienta. Espero que os haya gustado el articulo y nos leemos en una próxima ocasión! :)