viernes, 24 de abril de 2020

Montar tu propio cluster de kubernetes con minikube

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! :)

miércoles, 22 de abril de 2020

Mi primera Transpirenaica


Retrocedo en el tiempo hasta el 20 de Agosto de 2017, hace ya mas de dos años. Un día inolvidable para el que escribe estas palabras. Aún en la plaza de Obradoiro tras haber completado en bicicleta los 1000Km que separan mi ciudad natal, Castellón de la Plana, de Santiago de Compostela en 8 días, la emoción de conseguir tal hazaña me animó a publicar en redes sociales un ofrecimiento para que fuera alguno de mis amigos quien me planteara el siguiente reto en bicicleta.

“Estamos en Santiago de Compostela. Va por todos vosotros! Reto conseguido, y a pensar en locuras nuevas. ¡Se aceptan propuestas!.”


Entre todas las felicitaciones que aún me emociono al leer, una que especialmente despertó mi curiosidad. Y sin ser del todo consciente en ese momento, estaba plantando la semilla del que iba a ser el siguiente gran reto en bicicleta:


“Una Transpirenaica... Un entrenamiento para ti.”


Sin tener demasiado claro de que se trataba la Transpirenaica me puse a buscar información. Enseguida me di cuenta que la estupidez humana no tiene limites, LoL. Una distancia similar pero con el doble de desnivel positivo subido para llegar a Santiago. La dificultad superaba con creces la aventura que acababa de terminar. Ni tiempo tuve de disfrutar mi llegada a Santiago, ya me había metido en la cabeza otra locura de mayor envergadura...




Y de esta manera es como comienza la narración de la que se ha convertido en la mayor de mis aventuras: la publicación de mi primer libro! Ya a la venta disponible en tiendas online tanto en CasaDelLibro como en Amazon.

Estamos pasando unas semanas de confinamiento muy duras por toda la situación generada por la covid-19. La solidaridad y el compromiso con esta sociedad nos han mantenido confinados todo este tiempo, sin salir de casa, sin poder practicar deportes al aire libre (#YoMeQuedoEnCasa). Los ciclistas, acostumbrados a pasar muchas horas pedaleando al aire libre, estamos sufriendo especialmente esta falta de actividad física durante tantos días.

Por eso me dirijo especialmente a vosotros, a los que os gusta salir en bici como a mi y estáis sufriendo este confinamiento. Animaros a leer el libro y durante unas horas volveréis a sentir la libertad de rodar en bicicleta por esas carreteras... Tras su lectura, quien sabe, igual os pica el gusanillo y os animáis a planificar esta misma ruta. Quizá estés un paso mas mas cerca de convertirte en el siguiente ciclista que completa una Transpirenaica en solitario.

A los tres primeros que me expliquéis en los comentarios cual ha sido vuestra mayor aventura en bici os enviaré de manera gratuita un ejemplar del libro con una dedicatoria personalizada. Podéis pasarme vuestro nombre y dirección de entrega enviando un email a la dirección miprimeratranspirenaica@gmail.com.

Por último quiero pediros un favor: darle difusión a la obra, compartiéndola con vuestros contactos, especialmente con aquellos a los que les guste la bicicleta tanto como a mi! Gracias! Y mucha fuerza para salir de esta cuanto antes!



Visitas:

Seguidores