Kubernetes – Apuntes del CKA

Bueno, aquí dejo algunos apuntes (ojo, ésto no es una guía ni una biblia; son sólo eso, apuntes) tras repasarme los cursos «Kubernetes Quick Start» y «Cloud Native Certified Kubernetes Administrator (CKA)» de LinuxAcademy.com

Cómo instalar Docker + Kubernetes from scratch en CentOS

Usando Flannel para la gestión de la red.

Dejo un copy&paste de las instrucciones originales de LinuxAcademy.

    #The first thing that we are going to do is use SSH to log in to all machines. Once we have logged in, we need to elevate privileges using sudo.
     sudo su  

    #Disable SELinux.
     setenforce 0
     sed -i --follow-symlinks 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/sysconfig/selinux

    #Enable the br_netfilter module for cluster communication.
     modprobe br_netfilter
     echo '1' > /proc/sys/net/bridge/bridge-nf-call-iptables

    #Disable swap to prevent memory allocation issues.
     swapoff -a
     vim /etc/fstab.orig  ->  Comment out the swap line

    #Install the Docker prerequisites.
     yum install -y yum-utils device-mapper-persistent-data lvm2

    #Add the Docker repo and install Docker.
     yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
     yum install -y docker-ce

    #Configure the Docker Cgroup Driver to systemd, enable and start Docker
     sed -i '/^ExecStart/ s/$/ --exec-opt native.cgroupdriver=systemd/' /usr/lib/systemd/system/docker.service 
     systemctl daemon-reload
     systemctl enable docker --now

    #Add the Kubernetes repo.
     cat < /etc/yum.repos.d/kubernetes.repo
     [kubernetes]
     name=Kubernetes
     baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
     enabled=1
     gpgcheck=0
     repo_gpgcheck=0
     gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg
             https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
     EOF

    #Install Kubernetes.
     yum install -y kubelet kubeadm kubectl

    #Enable Kubernetes. The kubelet service will not start until you run kubeadm init.
     systemctl enable kubelet

*Note: Complete the following section on the MASTER ONLY!

    #Initialize the cluster using the IP range for Flannel
     kubeadm init --pod-network-cidr=10.244.0.0/16

    #Copy the kubeadmin join command. Exit sudo and run the following:
     mkdir -p $HOME/.kube
     sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
     sudo chown $(id -u):$(id -g) $HOME/.kube/config

    #Deploy Flannel.
     kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

    #Check the cluster state.
     kubectl get pods --all-namespaces

*Note: Complete the following steps on the NODES ONLY!

    #Run the join command that you copied earlier (this command needs to be run as sudo), then check your nodes from the master.
     kubectl get nodes

Nota:
Al final k8s es una capa encima de docker que usa la API de docker para manipularlo. Puedes correr un docker ps en las instancias para ver exactamente qué está pasando por debajo de k8s (en docker)

La arquitectura del cluster

Master nodes

  • API Server: expone la API de k8s.
  • Scheduler: asigna tu aplicación a un worker.
  • Controller Manager: mantiene el cluster (node failures, número deseado de Pods, etc.)
  • etcd: mantiene la base de datos con la configuración del cluster.
kubectl get componentstatus

Worker nodes (responsables de correr y monitorizar las aplicaciones):

  • kubelet: corre y gestiona los containers.
  • kube-proxy: mantiene el listado de endpoints y actualiza iptables para balancear el tráfico entre componentes de una aplicación.
  • container runtime: docker, rkt, containerd.

Los componentes no hablan entre sí directamente, si no que siempre pasan por el API Server.
kubelet es el único componente que corre como servicio en el sistema, y no como Pod.

API

Un objeto de k8s tendrá una sección «spec» con la declaración del estado que queremos, y otra sección «status» con el estado real. k8s se asegurará de que el estado hace match con la especificación. Ésto podemos verlo con el siguiente comando:

kubectl get deployment DEPLOYMENT-NAME -n NAMESPACE -o yaml

En el yaml original que define el objeto, no tendremos la sección «status» obviamente, únicamente el «spec» y en función del tipo de objeto declarado (deployment, Pod, etc.) también un «spec» para el container.

Podemos filtrar objectos de k8s con «–field-selector» tal que así:

kubectl get pods --field-selector status.phase=Running,metadata.namespace!=default

Los objetos de k8s pueden filtrarse también mediante etiquetas (o labels) y usar estas labels para aplicar acciones en grupo

kubectl get pods --show-labels
kubectl get pods -L LABEL-NAME

KUBECTL Basic commands

# Listar todos los nodos del cluster
kubectl get nodes

Un namespace vendría a ser un «cluster virtual» o una separación virtual entre entornos. K8s crea por defecto los namespaces default, kube-public y kube-system.

# Listar todos los namespaces
kubectl get namespaces 

# Listar todos los Pods para el namespace "mynamespace", dándo la máxima información
kubectl get pods -n mynamespace -o wide

# Listar todos los Pods para todos los namespaces
kubectl get pods --all-namespaces

# Lista todos los Pods en el namespace "kube-system" filtrando y ordenando el resultado
kubectl get pods -o custom-columns=POD:metadata.name,NODE:spec.nodeName --sort-by spec.NodeName -n kube-system

Los ficheros yaml de definición de objetos de k8s, pueden tener un atributo «kind» que indica el tipo de recurso de k8s que describe (un Pod, servicio, replica-set, etc.) con lo que no haría falta especificarlo durante el «create». Ésto además, permite tener todos los yaml para los diferentes servicios y deployments en un folder, y directamente pasarle ese folder en el momento del create para que kubectl se encargue de crear todos los recursos definidos

# Crear un objecto a partir de un fichero de definición
kubectl create -f pod-file.yaml

# Crear todos los objectos definidos en los diferentes yamls dentro de un directorio
kubectl create -f /path/to/yamls

# Updatea un objeto tras modificar su definición en el yaml file
kubectl apply -f pod-file.yaml
# Igual que en docker, acceso terminal a un Pod
kubectl exec -it POD-NAME /bin/bash

# Ejecutar un comando en un Pod, i.e. cat /path/to/file
kubectl exec -t POD-NAME -- COMMAND

Pods

Tanto el Pod como sus containers tienen la misma IP (comparten el espacio de IPs).
Los containers comparten el espacio de puertos del Pod, es decir, cuando corren un programa escuchando en el 80 localhost, automáticamente el puerto se expone en el Pod (IP-pod:80).
Los containers de un mismo Pod, pueden comunicarse entre ellos vía localhost (si el container A expone el puerto 80 y el B el 3306, el A puede comunicarse con el B símplemente haciendo localhost:3306).

apiVersion: v1
kind: Pod
metadata:
  name: busybox
spec:
  containers:
  - image: busybox:1.28.4
    command:
      - sleep
      - "3600"
    name: busybox

Networking

La gestión de direcciones IP y del networking se hace via plugins de red. K8s necesita pues, de una capa por encima, CNI (Container Network Interface), la cual simplifica las comunicaciones entre containers en un cluster, gestionando las IPs, encapsulando paquetes y mappings en el espacio de usuario. Es necesario hacerle saber a kubelet que vamos a usar un CNI. Estas instrucciones variarán en función del plugin que usemos.

Flannel CNI

  • Flannel es bastante sencillo de usar y no necesita post-configuración
  • Es un binario (flanneld) corriendo directamente en el master y los nodos (que no en los containers) que además modifica IPtables para permitir routing y la comunicación entre Pods y nodos
  • Ejecutando un «ip route» en el master, veremos las rutas que ha establecido flannel para el master y los nodos
  • Lanzando un «kubectl pods –all-namespaces -o wide» en el master vemos que todos los Pods principales, corren en el espacio de IPs del host, aunque el Pod de los DNS corre dentro del espacio de IPs configurado en flannel (el espacio de IPs de los Pods del usuario)
  • Todo se guarda en etcd

DNS

  • CoreDNS es ahora el plugin de DNS por defecto en Kubernetes
  • Se configura en background cuando seteamos el networking (no hay que hacer nada)
  • Los Pods hacen búsquedas relativas a su propio namespace (basta con poner «db» en lugar de «db.namespace»)
  • Todos los servicios en un cluster tienen un registro DNS
  • El cluster tiene un Pod DNS y kubelets se encargará que los containers usen el servicio de DNS del cluster (el /etc/resolv.conf de los containers tendrá la entrada correspondiente)

Replica Sets

  • Es un conjunto de Pods que tienen las mismas etiquetas (labels).
  • En la definición del replica set se especifica cuantos Pods vamos a gestionar (réplicas).
  • Las Replica Sets no cubren las necesidades de HA y escalado así que habitualmente usaremos «Deployents» para gestionar las Replica Sets.
kubectl describe rs/RS-NAME

# Info sobre ese Pod incluyendo un "Controlled By" que indicará el replicaset que lo está gestionando
kubectl describe pod POD-NAME 

kubectl scale rs/RS-NAME --replicas=4
kubectl delete rs/RS-NAME

Services

Los servicios exponen al exterior, símplificando encontrar los componentes de nuestras aplicaciones, incluso cuando los componentes se mueven o levantan nuevas réplicas. Proporcionan HA y LB entre Pods replicados proporcionando una única IP virtual, la cual se distribuye y asigna automáticamente.

Para ello, en la especificación del servicio, se usa el «selector» para elegir los Pods que tengan determinado «label», además de configurar el puerto que expone el servicio. Si escalamos, el servicio se ajustará automáticamente pues tendrá más/menos Pods con ese label.

Cuando un servicio se crea, el API Server notifica a todos los agentes «kube-proxy» en los workers para que estén actualizados. Los kube-proxy entonces, setean algunas reglas en iptables para permitir el acceso al servicio. Además del servicio se crea un endpoint, que no es más que una caché con las IPs de los Pods para ese servicio.

# Crear un servicio via kubectl para el deployment nginx
kubectl expose deployment nginx --port 80 --type NodePort

# Mostrar los endpoints (que están por detrás) de un servicio
kubectl describe service SERVICE-NAME -n NAMESPACE

# Listar los endpoints
kubectl get endpoints

Hay varios tipos de servicios además del habitual «NodePort«, como el «ClusterIP» que se crea automáticamente con el cluster y gesiona el enrutado intra-cluster permitiendo que si un Pod se mueve, el resto sepa dónde está y pueda comunicarse con él.

También está el tipo «LoadBalancer«, que en este caso tendrá una EXTERNAL-IP que permitirá el acceso desde fuera del cluster, y balanceará las peticiones entre los nodos que tienen Pods que forman parte del servicio. Es decir, por defecto no balancea entre Pods si no entre nodos. Podemos habilitar el «pod-awareness» pero ésto añadirá algo de latencia:

kubectl annotate services SERVICE-NAME externalTrafficPolicy=Local

Además de los servicios, tenemos «ingress rules» (kind: Ingress) que operando en la capa de aplicación, permite enviar peticiones a un servicio o a otro en función del dominio de destino.

Deployments

La forma de deployar aplicaciones en k8s es via «deployments». Los Deployments gestionan «Replica Sets» por nosotros, definiendo una estrategia para los updates (p.ej. RollingUpdateStrategy)

kubectl get replicasets

Con el Deployment creado, podemos crear un servicio que gestione los Pods con esas etiquetas, igual que haríamos únicamente con Replica Sets. Ésto nos permitiría por ejemplo, cambiar la imagen de docker de los containers de los Pods de los replica sets, por una nueva versión, sin downtime, siguiendo la estrategia definida para los updates en el manifiesto del deployment:

kubectl set image deployments/deployment-name nginx=darealmc/nginx-k8s:v2

O si modificamos el yaml del deployment, podemos aplicar (y registrar en el history del deployment) los cambios vía:

kubectl apply -f /path/to/file.yaml --record

Cuando se hace el RollingUpdate, el deployment en realidad crea un nuevo replica set y después elimina los Pods del replica set anterior. Ésto significa que se mantiene un histórico de versiones y además permite volver a versiones anteriores.

# Ver el histórico de rollouts
kubectl rollout history deployment DEPLOYMENT-NAME

# Volver a la versión anterior del deployment
kubectl rollout undo deployments DEPLOYMENT-NAME

# Watch (-w) RollingUpdate estado del deployment hasta que se completa el update 
kubectl rollout status -w deployment DEPLOYMENT-NAME

Finalmente, también podemos símplemente escalar el número de réplicas del deployment

# Escalar el número de réplicas del deployment
kubectl scale deployment DEPLOYMENT-NAME --replicas=10

Pasar parámetros de configuración a tu aplicación

Via variables de entorno, usando configMap o secrets

kubectl create configmap CONFIGMAP-NAME --from-literal=key1=value1 --from-literal=key2=value2

Posteriormente usados en el container

apiVersion: v1
kind: Pod
metadata:
  name: configmap-pod
spec:
  containers:
  - name: app-container
    image: busybox:1.28
    command: ['sh', '-c', "echo $(MY_VAR) && sleep 3600"]
    env:
    - name: MY_VAR
      valueFrom:
        configMapKeyRef:
          name: CONFIGMAP-NAME
          key: key1

Es best practice no pasar secretos vía variables de entorno, pues las aplicaciones pueden hacer un volcado de esta y otra información, incluso en forma de logs.

También podemos directamente montar un volúmen con el configMap para exponer un fichero con los secrets en el container. Éstos volúmenes usan sistemas de fichero en memoria (tmpfs), con lo que los secrets no se escriben en el disco físico. Cada Pod tiene un volúmen para secrets atachado automáticamente por defecto, que monta el secret con el certificado, el namespace y el token para poder usar su Service Account.

# Atención a la sección "Mounts" del Pod
kubectl describe pods POD-NAME

Storage

Kubernetes ofrece una capa de abstracción entre la aplicación y el storage, llamada PersistentVolumes, o símplemente «pv» (kind: PersistentVolume).

kubectl get pv

Un PersistentVolumeClaim (kind: PersistentVolumeClaim) permitiría a un developer usar ese claim en el momento de definir el Pod, sin que el developer tenga que preocuparse de los detalles del volumen en sí.

kubectl get pvc

Mediante StorageClass (kind: StorageClass) se pueden definir otros tipos de storage (según el provider), por ejemplo un SSD, para poder especificarlos en la definición del pvc mediante «storageClassName».

Seguridad

Se recomienda:
– Securizar las comunicaciones intra-cluster usando HTTPS.
– Habilitar HA replicando el master.

Cuando se accede a la API (ya sea un serviceaccount o un usuario normal) para consultar o modificar el estado del cluster (i.e. crear un Pod) se sigue este camino:
– Autenticación para identificar a quien hace la petición (via http header o certificado). Retorna el username, userid y los grupos a los que pertenece.
– Autorización para validar si el usuario puede realizar la operación sobre el recurso.
– Admisión para crear, modificar o eliminar objetos (no para consultarlos). Pasa por todos los plugins que tienen permiso para modificar el recurso.
– Validación del recurso
– Acceso a etcd para guardar el nuevo estado

roles: definen qué se puede hacer.
role bindings: definen quién puede hacerlo.

Los «role» y «role bindings», para recursos a nivel de namespace. También hay «cluster role» y «cluster role bindings», para recursos a nivel de cluster (i.e. listar los servicios de un namespace).

ServiceAccounts

Los containers que corren en un Pod, pueden usar un «Service Account» para comunicarse con el API Server del cluster.

kubectl get serviceaccounts

Podemos ver en cada Pod, su fichero con el token para autenticarse con la API:

cat /var/run/secrets/kubernetes.io/serviceaccount/token

Cuando se crea un serviceaccount, a su vez se crea un secret que mantiene la clave pública del API Server así como un token json firmado

# Crear un nuevo serviceaccount llamado "jenkins"
kubectl create serviceaccount jenkins

# Ver el objeto creado para obtener el secret name
kubectl get sa jenkins -o yaml

# Ver el secret que usará para acceder al API Server
kubectl get secret SECRET-NAME

Con ésto, podríamos ir al server de Jenkins, añadirle el plugin de kubernetes-cli y el token, y con eso el server de Jenkins podría controlar los Pods que usaran ese ServiceAccount.
Si en la definición del Pod no especificamos el serviceAccount, se usará el default.

[Más]

Políticas de red

Por defecto los Pods están abiertos y accesibles. Podemos gestionar las políticas de red mediante un plugin externo, como canal, añadiéndo una política de red deny-all para todas las conexiones de entrada (ingress).

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress

Mediante otro objeto del tipo «NetworkPolicy» pero con un selector appropiado (para que aplique únicamente a los Pods con una etiqueta determinada) podemos definir una ingress rule que sí permita la comunicación hacia un puerto determinado desde unos Pods determinados, con otro label selector (o con un «namespaceSelector» o incluso con rango de IPs).

Exactamente igual para las conexiones de salida (egress).

Configurando un docker registry privado

# Crear un secret para poder acceder al registry privado
kubectl create secret docker-registry SECRET-NAME --docker-server=REGISTRY-URL --docker-username=USER --docker-password='PASS' --docker-email=EMAIL

# Modificar el serviceaccount (aka "sa") para por defecto usar el nuevo registry
kubectl patch sa default -p '{"imagePullSecrets": [{"name": "SECRET-NAME"}]}'

Security Contexts

Los Security Contexts permiten asegurar los containers para especificar qué procesos pueden hacer qué cosas. Por ejemplo:

1) Ejecución como root:
En la definición del container, dentro del Pod, podemos especificar un securityContext para forzar al container ejecutar el proceso principal como un ususario específico, entre otras opciones.

2) Acceso a funciones de Kernel:
También podemos especificar, dentro del securityContext, las «capabilities» que queremos añadir o quitar al container para que pueda ejecutar ciertas funciones a nivel de kernel.

3) Además, el securityContext permite especificar opciones como «readOnlyRootFilesystem: true», muy útil junto con un volumen con la opción «readOnly: false» para asegurar la escritura únicamente en el volúmen evitando problemas en el sistema de ficheros raíz

Logs y Monitoring

El Metrics Server descubre todos los nodos y Pods (y sus containers) del cluster y recoge métricas de CPU y memoria de todos ellos, para poder consultarlo desde kubectl o con llamadas a la API del servidor de métricas.

# Clonamos el repo e instalamos el server de métricas
git clone https://github.com/linuxacademy/metrics-server
kubectl apply -f ~/metrics-server/deploy/1.8+/

# Top de todos los Pods
kubectl top pods --all-namespaces

# Top de los containers de un Pod concreto
kubectl top pods POD-NAME --containers

Por otra parte, en kubernetes podemos especificar checkeos en la definición de un container, para asegurar:

  • livenessProbe: que está «vivo». En caso contrario reinicia el container.
  • readinessProbe: que está listo para operar. En caso contrario lo saca del endpoint.
apiVersion: v1
kind: Pod
metadata:
  name: liveness
spec:
  containers:
  - image: linuxacademycontent/kubeserve
    name: kubeserve
    livenessProbe:
      httpGet:
        path: /
        port: 80

Logs

Los containers guardan los logs (stdout) en /var/log/containers a nivel de nodo.
Puedes ver los logs (stdout y stderr) de un container determinado via kubectl y sus diversas opciones de filtrado.

# Tail -f de los logs de un container de un Pod
kubectl logs POD-NAME -f -c CONTAINER-NAME

Chuleta

# Ver todos los recursos de un namespace
kubectl get all -n NAMESPACE

# Ver el yaml del objeto de k8s. Muy útil para ver todos los detalles y detectar errores
kubectl get pod POD-NAME -o yaml -n NAMESPACE

# Editar el objecto directamente (útil para pods, aunque no todos los campos son editables)
kubectl edit pod POD-NAME

# Para cambiar un campo no editable, se debe recrear el objeto. Si no tenemos la definición podemos exportarla
kubectl get pod POD-NAME -o yaml --export > POD-NAME.yaml
kubectl delete pod POD-NAME
kubectl create -f POD-NAME.yaml

# O Updatear un objeto de k8s via patch (más útil para RS, deployments...)
kubectl patch pod POD-NAME --patch '{"spec": {"template": {"spec": {"containers": [{"name": "patch-demo-ctr-2","image": "redis"}]}}}}'
# Exponemos el puerto nodeport para redirigir las peticiones a los Pods 
kubectl port-forward DEPLOYNAME nodeport:podport &

# Abrimos otra terminal y para hacer curl a ese nodo, nodeport y testear
curl https://localhost:NODEPORT -k
# Levantamos un busybox para testear via curl otros pods
kubectl run busybox --image=busybox --rm -it --restart=Never -- sh
# Añadimos una label para usar posteriormente en el servicio
kubectl label deployments DEPLOYMENT-NAME app=nginx

Recursos útiles

Testeando el cluster

kubetest es una herramienta para tests end-to-end que merece la pena mirarse.
Gran lista de comprobaciones y sus comandos en The pod of Minerva (Install, config and validate > End-to-end tests)

+ https://linuxacademy.com/cp/modules/view/id/371

Links

Deja una respuesta

Tu dirección de correo electrónico no será publicada.