Nuestra experiencia trabajando con datos en un clúster de Kubernetes etcd directamente (sin la API de K8s)

Cada vez más, los clientes nos solicitan proporcionar acceso al clúster de Kubernetes para poder acceder a los servicios dentro del clúster: para poder conectarse directamente a alguna base de datos o servicio, para enlazar una aplicación local con aplicaciones dentro del clúster...

Nuestra experiencia trabajando con datos en un clúster de Kubernetes etcd directamente (sin la API de K8s)

Por ejemplo, surge la necesidad de conectarse desde su máquina local al servicio memcached.staging.svc.cluster.local. Ofrecemos esta capacidad mediante una VPN dentro del clúster, a la que se conecta el cliente. Para ello, anunciamos subredes de pods, servicios y enviamos el DNS del clúster al cliente. De esta manera, cuando el cliente intenta conectarse al servicio memcached.staging.svc.cluster.local, la solicitud se envía al DNS del clúster y como respuesta recibe la dirección de dicho servicio de la red de servicios del clúster o la dirección del pod.

Configuramos los clústeres de K8s utilizando kubeadm, donde por defecto la subred de servicios es 192.168.0.0/16, y la red de pods es 10.244.0.0/16. Normalmente, todo funciona bien, pero hay algunos aspectos:

  • La subred 192.168.*.* se utiliza con frecuencia en las redes de oficinas de los clientes, y aún más a menudo en las redes domésticas de los desarrolladores. Y entonces tenemos conflictos: los routers domésticos operan en esta subred y la VPN empuja estas subredes desde el clúster al cliente.
  • Tenemos varios clústeres (clústeres de producción, etapa y/o varios clústeres de desarrollo). Así, en todos ellos, por defecto, habrá subredes idénticas para pods y servicios, lo que crea grandes dificultades para trabajar simultáneamente con servicios en varios clústeres.

Ya hace un tiempo adoptamos la práctica de utilizar diferentes subredes para los servicios y pods dentro de un mismo proyecto — en general, para que todos los clústeres tuvieran redes diferentes. Sin embargo, hay una gran cantidad de clústeres en funcionamiento que no nos gustaría reinventar desde cero, ya que en ellos están en ejecución muchos servicios, aplicaciones con estado, etc.

Y entonces nos planteamos la pregunta: ¿cómo cambiar la subred en un clúster existente?

Búsqueda de soluciones

La práctica más común es recrear cargas de trabajo dejarán de funcionar! los servicios con tipo ClusterIP. Como opción, pueden aconsejar y lo siguiente:

El siguiente proceso presenta un problema: después de que todo está configurado, los pods vienen con la antigua IP como servidor DNS en /etc/resolv.conf.
Como aún no he encontrado la solución, tuve que reiniciar todo el clúster con kubeadm reset e inicializarlo nuevamente.

Pero no a todos les conviene esto... Aquí hay introducciones más detalladas para nuestro caso:

  • Se utiliza Flannel;
  • Existen clústeres tanto en la nube como en hardware;
  • Nos gustaría evitar el redepliegue de todos los servicios en el clúster;
  • Hay una necesidad de hacer todo con la menor cantidad de problemas posible;
  • La versión de Kubernetes es 1.16.6 (de hecho, las siguientes acciones serán similares para otras versiones);
  • La tarea principal consiste en reemplazar la subred de servicio en el clúster desplegado con kubeadm 192.168.0.0/16, por 172.24.0.0/16.

Y ha coincidido que desde hace tiempo nos interesaba ver qué y cómo se almacena en Kubernetes en etcd, qué se puede hacer con ello... Así que pensamos: "¿Por qué no actualizar simplemente los datos en etcd, reemplazando las direcciones IP antiguas (subred) por nuevas??»

Buscando herramientas listas para trabajar con los datos en etcd, no encontramos nada que resolviera completamente la tarea planteada. (Por cierto, si conoces alguna utilidad para trabajar con datos directamente en etcd, apreciaríamos los enlaces.) Sin embargo, un buen punto de partida fue etcdhelper de OpenShift (¡gracias a sus autores!).

Esta utilidad puede conectarse a etcd usando certificados y leer datos de allí con los comandos Muestra el contenido del directorio. Si no se proporciona una ruta, se muestra el contenido del directorio actual., get, dump.

Añadiendo etcdhelper

La siguiente idea es evidente: "¿Qué impide agregar a esta utilidad la capacidad de escribir datos en etcd?"

Se concretó en una versión modificada de etcdhelper con dos nuevas funciones changeServiceCIDR y changePodCIDR. Su código se puede ver aquí.

¿Qué hacen las nuevas funciones? El algoritmo changeServiceCIDR:

  • crea un deserializador;
  • compila una expresión regular para reemplazar CIDR;
  • recorre todos los servicios con tipo ClusterIP en el clúster:
    • decodifica el valor de etcd en un objeto Go;
    • con la expresión regular reemplaza los primeros dos bytes de la dirección;
    • asigna al servicio una dirección IP de la nueva subred;
    • crea un serializador, transforma el objeto Go en protobuf, y escribe los nuevos datos en etcd.

La función changePodCIDR es esencialmente análoga changeServiceCIDR — solo que en lugar de editar la especificación de servicios lo hacemos para el nodo y cambiamos .spec.PodCIDR por la nueva subred.

Práctica

Cambio de serviceCIDR

El plan para implementar la tarea planteada es muy simple, pero implica un tiempo de inactividad durante la recreación de todos los pods en el clúster. Después de describir los pasos principales, también compartiremos ideas sobre cómo se podría minimizar este tiempo de inactividad en teoría.

Acciones preparatorias:

  • instalación del software necesario y compilación de etcdhelper parcheado;
  • respaldo de etcd y /etc/kubernetes.

Un breve plan de acción para cambiar el serviceCIDR:

  • cambio de manifiestos del apiserver y del controller-manager;
  • renovación de certificados;
  • modificación de los servicios ClusterIP en etcd;
  • reinicio de todos los pods en el clúster.

A continuación, se presenta la secuencia completa de acciones en detalle.

1. Instalamos etcd-client para realizar un volcado de datos:

apt install etcd-client

2. Compilamos etcdhelper:

  • Instalamos golang:
    GOPATH=\/root\/golang
    mkdir -p $GOPATH\/local
    curl -sSL https:\/\/dl.google.com\/go\/go1.14.1.linux-amd64.tar.gz | tar -xzvC $GOPATH\/local
    echo "export GOPATH="$GOPATH"" >> ~\/.bashrc
    echo 'export GOROOT="$GOPATH\/local\/go"' >> ~\/.bashrc
    echo 'export PATH="$PATH:$GOPATH\/local\/go\/bin"' >> ~\/.bashrc
  • Guardamos el archivo etcdhelper.go, descargamos las dependencias, compilamos:
    wget https:\/\/raw.githubusercontent.com\/flant\/examples\/master\/2020\/04-etcdhelper\/etcdhelper.go
    go get go.etcd.io\/etcd\/clientv3 k8s.io\/kubectl\/pkg\/scheme k8s.io\/apimachinery\/pkg\/runtime
    go build -o etcdhelper etcdhelper.go

3. Hacemos una copia de seguridad de etcd:

backup_dir=\/root\/backup
mkdir ${backup_dir}
cp -rL \/etc\/kubernetes ${backup_dir}
ETCDCTL_API=3 etcdctl --cacert=\/etc\/kubernetes\/pki\/etcd\/ca.crt --key=\/etc\/kubernetes\/pki\/etcd\/server.key --cert=\/etc\/kubernetes\/pki\/etcd\/server.crt --endpoints https:\/\/192.168.199.100:2379 snapshot save ${backup_dir}\/etcd.snapshot

4. Cambiamos la subred del servicio en los manifiestos del plano de control de Kubernetes. En los archivos /etc/kubernetes/manifests/kube-apiserver.yaml y /etc/kubernetes/manifests/kube-controller-manager.yaml modificamos el parámetro --service-cluster-ip-range por la nueva subred: 172.24.0.0/16 en lugar de 192.168.0.0/16.

5. Dado que estamos cambiando la subred del servicio para la cual kubeadm emite certificados para el apiserver (entre otros), es necesario renovarlos:

  1. Veamos a qué dominios y direcciones IP se ha emitido el certificado actual:
    openssl x509 -noout -ext subjectAltName <\/etc\/kubernetes\/pki\/apiserver.crt
    X509v3 Subject Alternative Name:
        DNS:dev-1-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:apiserver, IP Address:192.168.0.1, IP Address:10.0.0.163, IP Address:192.168.199.100
  2. Prepararemos la configuración mínima para kubeadm:
    cat kubeadm-config.yaml
    apiVersion: kubeadm.k8s.io\/v1beta1
    kind: ClusterConfiguration
    networking:
      podSubnet: "10.244.0.0\/16"
      serviceSubnet: "172.24.0.0\/16"
    apiServer:
      certSANs:
      - "192.168.199.100" # IP de la nodo maestro
  3. Eliminamos los antiguos crt y key, ya que sin esto no se emitirá el nuevo certificado:
    rm \/etc\/kubernetes\/pki\/apiserver.{key,crt}
  4. Renovamos los certificados para el API-server:
    kubeadm init phase certs apiserver --config=kubeadm-config.yaml
  5. Verificamos que se emitió el certificado para la nueva subred:
    openssl x509 -noout -ext subjectAltName <\/etc\/kubernetes\/pki\/apiserver.crt
    X509v3 Subject Alternative Name:
        DNS:kube-2-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:172.24.0.1, IP Address:10.0.0.163, IP Address:192.168.199.100
  6. Después de renovar el certificado del API-server, reiniciaremos su contenedor:
    docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
  7. Regeneramos la configuración para admin.conf:
    kubeadm alpha certs renew admin.conf
  8. Editamos los datos en etcd:
    .\/etcdhelper -cacert \/etc\/kubernetes\/pki\/etcd\/ca.crt -cert \/etc\/kubernetes\/pki\/etcd\/server.crt -key \/etc\/kubernetes\/pki\/etcd\/server.key -endpoint https:\/\/127.0.0.1:2379 change-service-cidr 172.24.0.0\/16 

    ¡Atención! En este momento, la resolución de dominios deja de funcionar en el clúster, ya que en los pod existentes se /etc/resolv.conf ha configurado la dirección antigua de CoreDNS (kube-dns), y kube-proxy cambió las reglas de iptables de la antigua subred a la nueva. A continuación, el artículo describe posibles formas de minimizar el tiempo de inactividad.

  9. Corrijamos los ConfigMaps en el espacio de nombres kube-system:
    kubectl -n kube-system edit cm kubelet-config-1.16

    — aquí cambiaremos clusterDNS por la nueva dirección IP del servicio kube-dns: kubectl -n kube-system get svc kube-dns.

    kubectl -n kube-system edit cm kubeadm-config

    — corregiremos data.ClusterConfiguration.networking.serviceSubnet por la nueva subred.

  10. Dado que la dirección de kube-dns ha cambiado, es necesario actualizar la configuración de kubelet en todos los nodos:
    kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
  11. Solo queda reiniciar todos los pod en el clúster:
    kubectl get pods --no-headers=true --all-namespaces |sed -r 's/(S+)s+(S+).*/kubectl --namespace 1 delete pod 2/e'

Minimización del tiempo de inactividad

Pensamientos sobre cómo minimizar el tiempo de inactividad:

  1. Después de los cambios en los manifiestos del control plane, crea un nuevo servicio kube-dns, por ejemplo, con el nombre kube-dns-tmp y una nueva dirección 172.24.0.10.
  2. Haz if en etcdhelper, que no modificará el servicio kube-dns.
  3. Reemplaza en todos los kubelets la dirección ClusterDNS por la nueva, mientras que el antiguo servicio seguirá funcionando al mismo tiempo que el nuevo.
  4. Espera a que los pod con las aplicaciones se actualicen, ya sea por su cuenta o a una hora acordada.
  5. Elimina el servicio kube-dns-tmp y cambia serviceSubnetCIDR para el servicio kube-dns.

Este plan permitirá minimizar el tiempo de inactividad a aproximadamente un minuto, durante el tiempo de eliminación del servicio kube-dns-tmp y cambio de la subred para el servicio kube-dns.

Modificación de podNetwork

Además, decidimos ver cómo modificar podNetwork usando el etcdhelper que resultó. La secuencia de acciones es la siguiente:

  • corregimos las configuraciones en kube-system;
  • corregimos el manifiesto de kube-controller-manager;
  • cambiamos podCIDR directamente en etcd;
  • reiniciamos todos los nodos del clúster.

Ahora más de estos procedimientos:

1. Modificamos los ConfigMaps en el espacio de nombres kube-system:

kubectl -n kube-system edit cm kubeadm-config

— corregimos data.ClusterConfiguration.networking.podSubnet por la nueva subred 10.55.0.0/16.

kubectl -n kube-system edit cm kube-proxy

— corregimos data.config.conf.clusterCIDR: 10.55.0.0/16.

2. Modificamos el manifiesto del controller-manager:

vim /etc/kubernetes/manifests/kube-controller-manager.yaml

— corregimos --cluster-cidr=10.55.0.0/16.

3. Observamos los valores actuales .spec.podCIDR, .spec.podCIDRs, .InternalIP, .status.addresses para todos los nodos del clúster:

kubectl get no -o json | jq '[.items[] | {"name": .metadata.name, "podCIDR": .spec.podCIDR, "podCIDRs": .spec.podCIDRs, "InternalIP": (.status.addresses[] | select(.type == "InternalIP") | .address)}]'

[
  {
    "name": "kube-2-master",
    "podCIDR": "10.244.0.0/24",
    "podCIDRs": [
      "10.244.0.0/24"
    ],
    "InternalIP": "192.168.199.2"
  },
  {
    "name": "kube-2-master",
    "podCIDR": "10.244.0.0/24",
    "podCIDRs": [
      "10.244.0.0/24"
    ],
    "InternalIP": "10.0.1.239"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.244.1.0/24",
    "podCIDRs": [
      "10.244.1.0/24"
    ],
    "InternalIP": "192.168.199.222"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.244.1.0/24",
    "podCIDRs": [
      "10.244.1.0/24"
    ],
    "InternalIP": "10.0.4.73"
  }
]

4. Cambiemos el podCIDR, haciendo modificaciones directamente en etcd:

./etcdhelper -cacert /etc/kubernetes/pki/etcd/ca.crt -cert /etc/kubernetes/pki/etcd/server.crt -key /etc/kubernetes/pki/etcd/server.key -endpoint https://127.0.0.1:2379 change-pod-cidr 10.55.0.0/16

5. Verifiquemos que el podCIDR haya cambiado realmente:

kubectl get no -o json | jq '[.items[] | {"name": .metadata.name, "podCIDR": .spec.podCIDR, "podCIDRs": .spec.podCIDRs, "InternalIP": (.status.addresses[] | select(.type == "InternalIP") | .address)}]'

[
  {
    "name": "kube-2-master",
    "podCIDR": "10.55.0.0/24",
    "podCIDRs": [
      "10.55.0.0/24"
    ],
    "InternalIP": "192.168.199.2"
  },
  {
    "name": "kube-2-master",
    "podCIDR": "10.55.0.0/24",
    "podCIDRs": [
      "10.55.0.0/24"
    ],
    "InternalIP": "10.0.1.239"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.55.1.0/24",
    "podCIDRs": [
      "10.55.1.0/24"
    ],
    "InternalIP": "192.168.199.222"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.55.1.0/24",
    "podCIDRs": [
      "10.55.1.0/24"
    ],
    "InternalIP": "10.0.4.73"
  }
]

6. Reiniciemos uno a uno todos los nodos del clúster.

7. Si al menos un nodo tiene el antiguo podCIDR, el kube-controller-manager no podrá iniciarse y los pods en el clúster no serán programados.

De hecho, se puede cambiar el podCIDR de una manera más simple (por ejemplo, así). Pero queríamos aprender a trabajar con etcd directamente, porque hay casos en los que editar objetos de Kubernetes en etcd es la única opción posible. (Por ejemplo, no se puede simplemente sin tiempo de inactividad cambiar el campo spec.clusterIP.)

Summary

Este artículo aborda la posibilidad de trabajar con datos en etcd directamente, es decir, eludiendo la API de Kubernetes. A veces, este enfoque permite hacer 'trucos inteligentes'. Las operaciones mencionadas en el texto las hemos probado en clústeres K8s reales. Sin embargo, su estado de preparación para un uso generalizado es PoC (prueba de concepto). Por lo tanto, si desea utilizar una versión modificada de la herramienta etcdhelper en sus clústeres, hágalo bajo su propio riesgo.

P.D.

También puedes leer en nuestro blog:

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