Kubernetes mínimo viable

La traducción del artículo se preparó la víspera del inicio del curso. "Prácticas y herramientas de DevOps".

Kubernetes mínimo viable

Si estás leyendo esto, probablemente hayas oído algo sobre Kubernetes (y si no, ¿cómo terminaste aquí?) Pero, ¿qué es exactamente Kubernetes? Este “Orquestación de contenedores de grado industrial”? O "Sistema operativo nativo de la nube"? ¿Qué significa esto siquiera?

Para ser honesto, no estoy 100% seguro. Pero creo que es interesante profundizar en lo interno y ver qué está pasando realmente en Kubernetes bajo sus muchas capas de abstracciones. Entonces, solo por diversión, echemos un vistazo a cómo se ve realmente un “clúster de Kubernetes” mínimo. (Esto será mucho más fácil que Kubernetes por las malas.)

Supongo que tienes conocimientos básicos de Kubernetes, Linux y contenedores. Todo lo que hablamos aquí es solo para fines de investigación/aprendizaje, ¡no pongas nada de eso en producción!

Descripción

Kubernetes contiene muchos componentes. De acuerdo a wikipedia, la arquitectura se ve así:

Kubernetes mínimo viable

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

  • kubelet
  • kube-apiserver (que depende de etcd - su base de datos)
  • tiempo de ejecución del contenedor (Docker en este caso)

Veamos qué dice la documentación sobre cada uno de ellos (ruso., Inglés.). En primer lugar kubelet:

Un agente que se ejecuta en cada nodo del clúster. Se asegura de que los contenedores se estén ejecutando en el módulo.

Suena bastante simple. Qué pasa tiempos de ejecución de contenedores (tiempo de ejecución del contenedor)?

Un tiempo de ejecución de contenedor es un programa diseñado para ejecutar contenedores.

Muy informativo. Pero si está familiarizado con Docker, debería tener una idea general de lo que hace. (Los detalles de la separación de responsabilidades entre el tiempo de ejecución del contenedor y el kubelet son en realidad bastante sutiles y no los abordaré aquí).

И servidor API?

API Server es el componente del panel de control de Kubernetes que expone la API de Kubernetes. El servidor API es el lado del cliente del panel de control de Kubernetes.

Cualquiera que haya hecho algo con Kubernetes ha tenido que interactuar con la API directamente o mediante kubectl. Este es el corazón de lo que hace a Kubernetes Kubernetes: el cerebro que convierte las montañas de YAML que todos conocemos y amamos (?) en infraestructura funcional. Parece obvio que la API debería estar presente en nuestra configuración mínima.

Prerrequisitos

  • Máquina física o virtual Linux con acceso root (estoy usando Ubuntu 18.04 en una máquina virtual).
  • ¡Y es todo!

Instalación aburrida

Necesitamos instalar Docker en la máquina que usaremos. (No voy a entrar en detalles sobre cómo funcionan Docker y los contenedores; si está interesado, hay maravillosos artículos). Instalémoslo con apt:

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

Después de eso, necesitamos obtener los binarios de Kubernetes. De hecho, para el lanzamiento inicial de nuestro “clúster” sólo necesitamos kubelet, ya que para ejecutar otros componentes del servidor podemos usar kubelet. Para interactuar con nuestro clúster después de que se esté ejecutando, también usaremos 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é pasa si simplemente corremos? kubelet?

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

kubelet debe ejecutarse como root. Es bastante lógico, ya que necesita gestionar todo el nodo. Veamos sus parámetros:

$ ./kubelet -h
<слишком много строк, чтобы разместить здесь>
$ ./kubelet -h | wc -l
284

¡Vaya, tantas opciones! Por suerte, sólo necesitamos un par de ellos. Aquí está uno de los parámetros que nos interesan:

--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. (OBSECUTIVO: esta opción debe establecerse en el archivo de configuración pasado a Kubelet a través de la opción --config. Para obtener más información, consulte kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file .)

Esta opción nos permite ejecutar vainas estáticas — pods que no se administran a través de la API de Kubernetes. Los pods estáticos rara vez se utilizan, pero son muy convenientes para generar rápidamente un clúster, y esto es exactamente lo que necesitamos. Ignoraremos esta gran advertencia (nuevamente, ¡no ejecute esto en producción!) y veremos si podemos hacer que el pod funcione.

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

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

Luego, en otra terminal/ventana tmux/lo que sea, crearemos un manifiesto de pod:

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

kubelet Empieza a escribir algunas advertencias y parece que no pasa nada. ¡Pero eso no es cierto! Veamos Docker:

$ sudo docker ps -a
CONTAINER ID        IMAGE                  COMMAND                 CREATED             STATUS                      PORTS               NAMES
8c8a35e26663        busybox                "echo 'hello world!'"   36 seconds ago      Exited (0) 36 seconds ago                       k8s_hello_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_4
68f670c3c85f        k8s.gcr.io/pause:3.2   "/pause"                2 minutes ago       Up 2 minutes                                    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 de lanzar un par de contenedores de acuerdo con nuestras especificaciones. (Si se pregunta acerca del contenedor de "pausa", es un truco de Kubernetes; consulte este blog.) Kubelet lanzará nuestro contenedor. busybox con el comando especificado y lo reiniciará indefinidamente hasta que se elimine el pod estático.

Felicitate a ti mismo. ¡Se nos ocurrió una de las formas más confusas de enviar texto al terminal!

Lanzar etc.

Nuestro objetivo final es ejecutar la API de Kubernetes, pero para hacerlo primero debemos ejecutar etcd. Comencemos un clúster etcd mínimo colocando 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 ha trabajado con Kubernetes, estos archivos YAML le resultarán familiares. Sólo hay dos puntos que vale la pena señalar aquí:

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

hemos instalado hostNetwork: true. Esta configuración, como era de esperar, configura etcd para usar la red host en lugar de la red interna del pod (esto facilitará que el servidor API encuentre el clúster etcd).

Una simple verificación muestra que etcd se está ejecutando en localhost y guardando datos en el 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

Ejecutar un servidor API de Kubernetes es aún más fácil. El único parámetro que debe pasarse es --etcd-servers, 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

Coloque este archivo YAML en el directorio podsy se iniciará el servidor API. Comprobando con curl muestra que la 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": []
}

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

Y, agradable sorpresa, ¡kubectl funciona desde el primer momento 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 resources found in default namespace.

problema

Pero si profundizas un poco más, algo parece estar yendo mal:

$ ./kubectl get pod -n kube-system
No resources found in kube-system namespace.

¡Los pods estáticos que creamos ya no están! De hecho, nuestro nodo kubelet no se descubre en absoluto:

$ ./kubectl get nodes
No resources found in default namespace.

¿Qué pasa? Si recuerdas hace unos párrafos, iniciamos kubelet con un conjunto extremadamente simple de parámetros de línea de comando, por lo que kubelet no sabe cómo contactar al servidor API y notificarle su estado. Después de estudiar la documentación, encontramos la bandera correspondiente:

--kubeconfig string

La ruta al archivo kubeconfig, que especifica cómo conectarse al servidor API. Disponibilidad --kubeconfig habilita el modo de servidor API, no --kubeconfig habilita el modo fuera de línea.

Todo este tiempo, sin saberlo, estuvimos ejecutando kubelet en "modo fuera de línea". (Si fuéramos pedantes, podríamos pensar en un kubelet independiente como "Kubernetes mínimo viable", pero eso sería muy aburrido). Para que la configuración "real" funcione, debemos pasar el archivo kubeconfig al kubelet para que sepa cómo comunicarse con el servidor API. Por suerte, es bastante sencillo (ya que no tenemos ningún problema de autenticación ni de certificado):

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

Guarda esto como kubeconfig.yaml, mata el proceso kubelet y reiniciar con los parámetros necesarios:

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

(Por cierto, si intenta acceder a la API a través de curl cuando kubelet no se está ejecutando, encontrará que todavía se está ejecutando. Kubelet no es un "padre" de sus pods como Docker, es más como un "control". daemon". Los contenedores administrados por un kubelet continuarán ejecutándose hasta que el kubelet los detenga).

Через несколько минут kubectl debería mostrarnos los pods y nodos como esperamos:

$ ./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    <none>   21h   v1.18.5   10.70.10.228   <none>        Ubuntu 18.04.4 LTS   4.15.0-109-generic   docker://19.3.6

Felicitémonos realmente esta vez (sé que ya me felicité): ¡tenemos un "clúster" mínimo de Kubernetes ejecutándose con una API completamente funcional!

Lanzamos bajo

Ahora veamos de qué es capaz la API. Comencemos con el pod nginx:

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

Aquí obtenemos 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 cuán lamentablemente incompleto es nuestro entorno Kubernetes: no tenemos cuentas para los servicios. Intentemos nuevamente creando manualmente una cuenta de servicio 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, el token de autenticación no se genera. A medida que sigamos experimentando con nuestro "clúster" minimalista, descubriremos que faltarán la mayoría de las cosas útiles que normalmente suceden automáticamente. El servidor API de Kubernetes es bastante minimalista, y la mayor parte del trabajo pesado y la configuración automática se realizan en varios controladores y trabajos en segundo plano que aún no se están ejecutando.

Podemos solucionar este problema configurando la opción automountServiceAccountToken para la cuenta de servicio (ya que no tendremos que usarla de todos modos):

$ 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

¡Por fin ha aparecido la vaina! Pero en realidad no arranca porque no tenemos planificador (programador) es otro componente importante de Kubernetes. Nuevamente, vemos que la API de Kubernetes es sorprendentemente "tonta": cuando creas un Pod en la API, lo registra, pero no intenta averiguar en qué nodo ejecutarlo.

De hecho, no necesita un programador para ejecutar un pod. Puede agregar manualmente un nodo al manifiesto en el parámetro nodeName:

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

(Reemplazar mink8s al nombre del nodo). Después de eliminar y aplicar, vemos que nginx se ha iniciado y está escuchando la dirección IP interna:

$ ./kubectl delete pod nginx
pod "nginx" deleted
$ ./kubectl apply -f nginx.yaml
pod/nginx created
$ ./kubectl get pods -owide
NAME    READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          30s   172.17.0.2   mink8s   <none>           <none>
$ curl -s 172.17.0.2 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Para asegurarnos de que la red entre pods esté funcionando correctamente, podemos ejecutar curl desde otro pod:

$ cat <<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 created
$ ./kubectl logs curl | head -6
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Es bastante interesante profundizar en este entorno y ver qué funciona y qué no. Descubrí que ConfigMap y Secret funcionan como se esperaba, pero Service and Deployment no.

Успех!

Esta publicación se está haciendo larga, así que voy a cantar victoria y decir que esta es una configuración viable que se puede llamar "Kubernetes". En resumen: cuatro binarios, cinco parámetros de línea de comando y "sólo" 45 líneas de YAML (no eso según los estándares de Kubernetes) y tenemos bastantes cosas funcionando:

  • Los pods se administran mediante la API normal de Kubernetes (con algunos trucos)
  • Puede cargar y administrar imágenes de contenedores públicos.
  • Los pods permanecen activos y se reinician automáticamente
  • La conexión en red entre pods dentro del mismo nodo funciona bastante bien
  • ConfigMap, el montaje de almacenamiento secreto y simple funciona como se esperaba

Pero todavía falta mucho de lo que hace que Kubernetes sea realmente útil, como por ejemplo:

  • Programador de pods
  • Autorización de autenticación
  • Múltiples nodos
  • Red de servicios
  • DNS interno agrupado
  • Controladores para cuentas de servicio, implementaciones, integración con proveedores de nube y la mayoría de las otras ventajas que ofrece Kubernetes.

Entonces, ¿qué obtuvimos realmente? La API de Kubernetes, que se ejecuta por sí sola, es en realidad solo una plataforma para automatización de contenedores. No hace mucho (es un trabajo para varios controladores y operadores que usan la API), pero proporciona un entorno consistente para la automatización.

Obtenga más información sobre el curso en el seminario web gratuito.

Lee mas:

Fuente: habr.com

Añadir un comentario