Kubernetes mínimamente viable

La traducción del artículo ha sido preparada en la víspera del inicio del curso «Prácticas y herramientas de DevOps».

Kubernetes mínimamente viable

Si estás leyendo esto, probablemente hayas oído algo sobre Kubernetes (y si no, ¿cómo llegaste aquí?). Pero, ¿qué es realmente Kubernetes? Esto es “Orquestación de contenedores a nivel industrial”? Или «Sistema Operativo Nativo de la Nube»? Что вообще это значит?

La verdad es que no estoy 100% seguro. Pero creo que es interesante profundizar en sus entrañas y ver qué realmente sucede en Kubernetes bajo sus múltiples capas de abstracción. Así que, por curiosidad, veamos cómo se ve realmente un mínimo “clúster de Kubernetes”. (Esto será mucho más fácil que Kubernetes The Hard Way.)

Supongo que tienes conocimientos básicos de Kubernetes, Linux y contenedores. Todo lo que discutiremos aquí está destinado solo a exploración / estudio, ¡no ejecutes nada de esto en producción!

Reseña

Kubernetes contiene muchos componentes. Según Wikipedia, la arquitectura se ve de la siguiente manera:

Kubernetes mínimamente viable

Aquí se muestran al menos ocho componentes, pero la mayoría de ellos los ignoraremos. Quiero afirmar que lo mínimo que se puede razonablemente denominar Kubernetes consiste en tres componentes principales:

  • kubelet
  • kube-apiserver (que depende de etcd — su base de datos)
  • entorno de ejecución de contenedores (en este caso, Docker)

Veamos qué se dice sobre cada uno de ellos en la documentación (rus., ing.). Primero kubelet:

El agente que se ejecuta en cada nodo del clúster. Se asegura de que los contenedores estén en funcionamiento en el pod.

Suena bastante simple. ¿Qué pasa con entorno de ejecución de contenedores (container runtime)?

El entorno de ejecución de contenedores es un programa diseñado para ejecutar contenedores.

Muy informativo. Pero si estás familiarizado con Docker, deberías tener una idea general de lo que hace. (Los detalles sobre la separación de responsabilidades entre el entorno de ejecución de contenedores y kubelet son en realidad bastante sutiles y aquí no me profundizaré en ellos.)

Y API-server?

El servidor API es un componente del panel de control de Kubernetes que representa la API de Kubernetes. El servidor API es la parte del cliente del panel de control de Kubernetes

Cualquiera que haya hecho algo con Kubernetes alguna vez ha tenido que interactuar con la API ya sea directamente o a través de kubectl. Es el corazón de lo que hace que Kubernetes sea Kubernetes: el cerebro que convierte las montañas de YAML que todos conocemos y amamos (?), en una infraestructura funcional. Parece obvio que la API debe estar presente en nuestra configuración mínima.

Condiciones previas

  • Una máquina virtual o física de Linux con acceso root (uso Ubuntu 18.04 en una máquina virtual).
  • ¡Y eso es todo!

Instalación aburrida

En la máquina que vamos a utilizar, necesitamos instalar Docker. (No voy a entrar en detalles sobre cómo funciona Docker y los contenedores; si te interesa, hay artículos geniales). Simplemente instalémoslo usando apt:

$ sudo apt install docker.io
$ sudo systemctl start docker

Después de eso, necesitamos obtener los binarios de Kubernetes. De hecho, para iniciar nuestro "clúster", solo necesitamos kubelet, ya que para iniciar otros componentes del servidor podemos usar kubelet. Para interactuar con nuestro clúster una vez que esté funcionando, también utilizaremos kubectl.

$ curl -L https://dl.k8s.io/v1.18.5/kubernetes-server-linux-amd64.tar.gz > server.tar.gz
$ tar xzvf server.tar.gz
$ cp kubernetes/server/bin/kubelet .
$ cp kubernetes/server/bin/kubectl .
$ ./kubelet --version
Kubernetes v1.18.5

¿Qué pasará si simplemente ejecutamos kubelet?

$ ./kubelet
F0609 04:03:29.105194 4583 server.go:254] mkdir /var/lib/kubelet: permiso denegado

kubelet debe ejecutarse como root. Bastante lógico, ya que necesita gestionar todo el nodo. Echemos un vistazo a sus parámetros:

$ ./kubelet -h

$ ./kubelet -h | wc -l
284

¡Vaya, cuántas opciones! Afortunadamente, solo necesitaremos un par de ellas. Aquí hay uno de los parámetros que nos interesa:

--pod-manifest-path string

Ruta al directorio que contiene archivos para pods estáticos, o ruta a un archivo que describe pods estáticos. Los archivos que comienzan con puntos se ignoran. (OBSOLETO: este parámetro debe estar configurado en el archivo de configuración, que se pasa a Kubelet a través de la opción —config. Para más información, ver. kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file .)

Este parámetro nos permite ejecutar pods estáticos — pods que no son gestionados a través de la API de Kubernetes. Los pods estáticos se utilizan raramente, pero son muy útiles para levantar rápidamente un clúster, que es exactamente lo que necesitamos. Ignoraremos esta fuerte advertencia (de nuevo, ¡no lo ejecutes en producción!) y veremos si podemos iniciar un pod.

Primero crearemos un directorio para los pods estáticos y ejecutaremos kubelet:

$ mkdir pods
$ sudo ./kubelet --pod-manifest-path=pods

Luego, en otra terminal/ventana tmux/o en otro lugar, crearemos el manifiesto para el pod:

$ cat < pods/hello.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello
spec:
  containers:
  - image: busybox
    name: hello
    command: ["echo", "¡hola mundo!"]
EOF

kubelet empieza a escribir algunas advertencias y parece que no pasa nada. ¡Pero no es así! Veamos Docker:

$ sudo docker ps -a
CONTAINER ID        IMAGE                  COMMAND                 CREATED             STATUS                      PORTS               NAMES
8c8a35e26663        busybox                "echo 'hello world!'"   hace 36 segundos      Salió (0) hace 36 segundos                       k8s_hello_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_4
68f670c3c85f        k8s.gcr.io/pause:3.2   "pause"                hace 2 minutos       En ejecución hace 2 minutos                                    k8s_POD_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_0
$ sudo docker logs k8s_hello_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_4
hello world!

kubelet leí el manifiesto del pod y le di a Docker el comando para ejecutar un par de contenedores de acuerdo con nuestra especificación. (Si te interesa saber sobre el contenedor “pause”, esto es un truco de Kubernetes — más detalles en este blog.) Kubelet iniciará nuestro contenedor busybox con el comando especificado y lo reiniciará indefinidamente, hasta que el pod estático sea eliminado.

Felicítate. ¡Acabamos de idear una de las formas más enrevesadas de imprimir texto en la terminal!

Iniciando etcd

Nuestro objetivo final es ejecutar la API de Kubernetes, pero primero necesitamos ejecutar etcd. Vamos a iniciar un clúster mínimo de etcd, poniendo su configuración en el directorio pods (por ejemplo, pods/etcd.yaml):

apiVersion: v1
kind: Pod
metadata:
  name: etcd
  namespace: kube-system
spec:
  containers:
  - name: etcd
    command:
    - etcd
    - --data-dir=/var/lib/etcd
    image: k8s.gcr.io/etcd:3.4.3-0
    volumeMounts:
    - mountPath: /var/lib/etcd
      name: etcd-data
  hostNetwork: true
  volumes:
  - hostPath:
      path: /var/lib/etcd
      type: DirectoryOrCreate
    name: etcd-data

Si alguna vez has trabajado con Kubernetes, entonces estos archivos YAML deberían ser familiares para ti. Aquí hay que destacar solo dos puntos:

Montamos la carpeta del host /var/lib/etcd en el pod para que los datos de etcd se conserven después de un reinicio (si no se hace esto, el estado del clúster se borrará en cada reinicio del pod, lo que no sería bueno incluso para una instalación mínima de Kubernetes).

Establecimos hostNetwork: true. Este parámetro, como es de esperar, configura etcd para usar la red del host en lugar de la red interna del pod (esto facilitará al servidor API encontrar el clúster etcd).

Una verificación sencilla muestra que etcd realmente está en ejecución en localhost y guarda datos en disco:

$ curl localhost:2379/version
{"etcdserver":"3.4.3","etcdcluster":"3.4.0"}
$ sudo tree /var/lib/etcd/
/var/lib/etcd/
└── member
    ├── snap
    │   └── db
    └── wal
        ├── 0.tmp
        └── 0000000000000000-0000000000000000.wal

Iniciando el servidor API

Iniciar el servidor API de Kubernetes es aún más sencillo. El único parámetro que necesitas proporcionar es --etcd-servers, y hace lo que esperas:

apiVersion: v1
kind: Pod
metadata:
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - name: kube-apiserver
    command:
    - kube-apiserver
    - --etcd-servers=http://127.0.0.1:2379
    image: k8s.gcr.io/kube-apiserver:v1.18.5
  hostNetwork: true

Coloca este archivo YAML en el directorio pods, y el servidor API se iniciará. La verificación con curl muestra que el API de Kubernetes está escuchando en el puerto 8080 con acceso completamente abierto: ¡no se requiere autenticación!

$ curl localhost:8080/healthz
ok
$ curl localhost:8080/api/v1/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "selfLink": "/api/v1/pods",
    "resourceVersion": "59"
  },
  "items": []
}

(¡De nuevo, no ejecutes esto en producción! Me sorprendió un poco que la configuración predeterminada sea tan insegura. Pero supongo que esto se hace para facilitar el desarrollo y las pruebas.)

Y, una agradable sorpresa, ¡kubectl funciona de inmediato sin ninguna configuración adicional!

$ ./kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.5", GitCommit:"e6503f8d8f769ace2f338794c914a96fc335df0f", GitTreeState:"clean", BuildDate:"2020-06-26T03:47:41Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.5", GitCommit:"e6503f8d8f769ace2f338794c914a96fc335df0f", GitTreeState:"clean", BuildDate:"2020-06-26T03:39:24Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
$ ./kubectl get pod
No hay recursos encontrados en el espacio de nombres predeterminado.

Problema

Pero si profundizamos un poco más, parece que algo no va bien:

$ ./kubectl get pod -n kube-system
No hay recursos encontrados en el espacio de nombres kube-system.

¡Los pods estáticos que creamos han desaparecido! De hecho, nuestro nodo kubelet no se detecta en absoluto:

$ ./kubectl get nodes
No hay recursos encontrados en el espacio de nombres predeterminado.

¿Cuál es el problema? Si recuerdas, hace unos párrafos iniciamos kubelet con un conjunto de parámetros de línea de comandos extremadamente sencillo, por lo que kubelet no sabe cómo comunicarse con el servidor API y notificarle su estado. Al revisar la documentación, encontramos el flag correspondiente:

--kubeconfig string

La ruta al archivo kubeconfig, que indica cómo conectarse al servidor API. La presencia de --kubeconfig activa el modo servidor API, la ausencia de --kubeconfig activa el modo autónomo.

Todo este tiempo, sin saberlo, hemos estado ejecutando kubelet en "modo autónomo". (Si fuéramos pedantes, podríamos considerar el modo autónomo de kubelet como "Kubernetes mínimamente viable", pero eso sería muy aburrido). Para que la configuración "real" funcione, necesitamos pasar el archivo kubeconfig a kubelet para que sepa cómo comunicarse con el servidor API. Afortunadamente, esto es bastante sencillo (ya que no tenemos problemas con la autenticación o los certificados):

apiVersion: v1
kind: Config
clusters:
- cluster:
    server: http://127.0.0.1:8080
  name: mink8s
contexts:
- context:
    cluster: mink8s
  name: mink8s
current-context: mink8s

Guárdalo como kubeconfig.yaml, termina el proceso kubelet y reinicia con los parámetros necesarios:

$ sudo ./kubelet --pod-manifest-path=pods --kubeconfig=kubeconfig.yaml

(Por cierto, si intentas acceder a la API a través de curl cuando kubelet no está funcionando, descubrirás que aún está en funcionamiento. Kubelet no es el "padre" de sus pods, a diferencia de Docker, es más como un "demonio administrador". Los contenedores gestionados por kubelet seguirán funcionando hasta que kubelet decida detenerlos.)

En unos minutos kubectl debería mostrarnos pods y nodos, como esperábamos:

$ ./kubectl get pods -A
NAMESPACE     NAME                    READY   STATUS             RESTARTS   AGE
default       hello-mink8s            0/1     CrashLoopBackOff   261        21h
kube-system   etcd-mink8s             1/1     Running            0          21h
kube-system   kube-apiserver-mink8s   1/1     Running            0          21h
$ ./kubectl get nodes -owide
NAME     STATUS   ROLES    AGE   VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION       CONTAINER-RUNTIME
mink8s   Ready       21h   v1.18.5   10.70.10.228           Ubuntu 18.04.4 LTS   4.15.0-109-generic   docker://19.3.6

Esta vez, celebremos de verdad (sé que ya lo hice) — ¡tenemos un "cluster" Kubernetes mínimo que funciona con una API totalmente funcional!

Iniciando pod

Ahora veamos de lo que es capaz la API. Comencemos con un pod nginx:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx

Aquí obtendremos un error bastante interesante:

$ ./kubectl apply -f nginx.yaml
Error from server (Forbidden): error when creating "nginx.yaml": pods "nginx" is
forbidden: error looking up service account default/default: serviceaccount
"default" not found
$ ./kubectl get serviceaccounts
No resources found in default namespace.

Aquí vemos lo increíblemente incompleta que está nuestra entorno de Kubernetes — no tenemos cuentas de servicio. Intentemos de nuevo creando una cuenta de servicio manualmente y veamos qué sucede:

$ cat <<EOS | ./kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: default
EOS
serviceaccount/default created
$ ./kubectl apply -f nginx.yaml
Error from server (ServerTimeout): error when creating "nginx.yaml": No API
token found for service account "default", retry after the token is
automatically created and added to the service account

Incluso cuando creamos la cuenta de servicio manualmente, no se genera un token de autenticación. Al continuar experimentando con nuestro "clúster" minimalista, descubriremos que muchas de las cosas útiles que normalmente suceden automáticamente estarán ausentes. El servidor API de Kubernetes es bastante minimalista; la mayor parte de la configuración automática pesada ocurre en varios controladores y tareas en segundo plano que aún no se están ejecutando.

Podemos evitar este problema configurando la opción automountServiceAccountToken para la cuenta de servicio (ya que en realidad no la vamos a usar):

$ cat <<EOS | ./kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: default
automountServiceAccountToken: false
EOS
serviceaccount/default configured
$ ./kubectl apply -f nginx.yaml
pod/nginx created
$ ./kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
nginx   0/1     Pending   0          13m

¡Finalmente, el pod apareció! Pero en realidad no se iniciará, ya que no tenemos un programador (scheduler) — otro componente importante de Kubernetes. De nuevo, vemos que el API de Kubernetes es sorprendentemente "tonto"; cuando creas un pod en el API, lo registra, pero no intenta averiguar en qué nodo debe ejecutarse.

En realidad, no se necesita un programador para ejecutar un pod. Puedes agregar manualmente un nodo en el manifiesto en el parámetro nodeName:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
  nodeName: mink8s

(Reemplaza mink8s con el nombre del nodo.) Después de eliminar y aplicar, vemos que nginx se inició y está escuchando en la dirección IP interna:

$ .\/kubectl delete pod nginx
pod "nginx" eliminado
$ .\/kubectl apply -f nginx.yaml
pod/nginx creado
$ .\/kubectl get pods -owide
NOMBRE  LISTO  ESTADO     REINICIOS  EDAD  IP           NODO     NODO NOMINADO   PUERTAS DE DISPONIBILIDAD
nginx   1/1    Ejecutándose 0         30s   172.17.0.2   mink8s   <none>           <none>
$ curl -s 172.17.0.2 | head -4
<!DOCTYPE html>
<html>
<head>
<title>¡Bienvenido a nginx!</title>

Para asegurarnos de que la red entre los pods funcione correctamente, podemos ejecutar curl desde otro pod:

$ gato &lt;&lt;EOS | .\/kubectl apply -f -
apiversion: v1
kind: Pod
metadata:
  name: curl
spec:
  containers:
  - image: curlimages\/curl
    name: curl
    command: ["curl", "172.17.0.2"]
  nodeName: mink8s
EOS
pod\/curl creado
$ .\/kubectl logs curl | head -6
  % Total    % Recibido % Transf.  Velocidad Promedio   Tiempo    Tiempo     Tiempo  Actual
                                 Desc.  Cargado   Total   Gastado  Izquierda  Velocidad
<!DOCTYPE html>
<html>
<head>
<title>¡Bienvenido a nginx!</title>

Es bastante interesante explorar este entorno y ver qué funciona y qué no. He descubierto que ConfigMap y Secret funcionan como se espera, pero Service y Deployment no.

¡Éxito!

Esta publicación se está volviendo larga, así que voy a declarar la victoria y afirmar que esta es una configuración viable que se puede llamar “Kubernetes". Resumiendo: cuatro archivos binarios, cinco parámetros de línea de comandos y “solo” 45 líneas de YAML (no tan mal según los estándares de Kubernetes) y tenemos varias cosas en funcionamiento:

  • Los pods son gestionados a través de la API de Kubernetes estándar (con algunos trucos)
  • Se pueden cargar imágenes de contenedores públicas y gestionarlas
  • Los pods permanecen activos y se reinician automáticamente
  • La red entre los pods en un mismo nodo funciona bastante bien
  • ConfigMap, Secret y el montaje más básico de almacenamiento funcionan como se espera

Pero gran parte de lo que hace que Kubernetes sea realmente útil aún falta, por ejemplo:

  • El programador de pods
  • Autenticación / autorización
  • Múltiples nodos
  • Red de servicios
  • DNS interno del clúster
  • Controladores para cuentas de servicio, implementaciones, integración con proveedores de nube y la mayoría de las otras 'características' que Kubernetes ofrece

Entonces, ¿qué es lo que realmente hemos obtenido? La API de Kubernetes, por sí sola, es en realidad solo una plataforma para automatización de contenedores. No hace mucho: ese es el trabajo de varios controladores y operadores que utilizan la API, pero proporciona un entorno coherente para la automatización.

Descubre más sobre el curso en el seminario web gratuito.

Leer más:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster