Laboratorio: https://labs.iximiuz.com/courses/kubernetes-the-very-hard-way-0cbfd997/introduction/welcome
Containerd es un runtime de contenedores estándar en la industria, utilizado para ejecutar contenedores en Kubernetes. Se trata de un runtime ligero y eficiente diseñado específicamente para este propósito.
Es un demonio que se ejecuta en cada worker node del clúster de Kubernetes. Sus principales responsabilidades son:
Guía oficial de instalación:
https://github.com/containerd/containerd/blob/main/docs/getting-started.md
OCI (Open Container Initiative) es un estándar abierto que define cómo deben ejecutarse los contenedores a bajo nivel. El OCI Runtime es el componente que realmente crea y ejecuta el contenedor en el sistema operativo, gestionando namespaces, cgroups y el sistema de archivos. El runtime más utilizado es runc.
El flujo de ejecución es el siguiente:
Kubernetes → Containerd → runc (OCI Runtime) → Contenedor
ctrctr es la CLI oficial de bajo nivel de Containerd, orientada principalmente a tareas de depuración e inspección interna. No ofrece compatibilidad con la sintaxis de Docker ni características avanzadas como soporte para Compose.
ctr images list
ctr containers list
⚠️ No está recomendado para uso en producción ni para usuarios finales.
nerdctlnerdctl es una CLI totalmente compatible con Docker construida sobre Containerd. Permite utilizar la misma sintaxis que docker sin necesidad de tener Docker instalado, e incluye soporte para nerdctl compose.
nerdctl run -it ubuntu bash
nerdctl compose up
Al igual que Containerd delega la ejecución de procesos de contenedor a los OCI Runtimes, delega la configuración de red a componentes especializados a través de la Container Network Interface (CNI).
CNI es una especificación y conjunto de librerías para configurar interfaces de red en contenedores Linux. Proporciona una forma estandarizada para que los runtimes de contenedores configuren la red, incluyendo:
El flujo completo quedaría así:
Kubernetes → Containerd → runc (OCI Runtime) → Contenedor
↘ CNI Plugin → Red del contenedor
Los plugins CNI más comunes en Kubernetes son Flannel, Calico y Cilium.
Kubelet es el agente principal del worker node que se ejecuta en cada worker node de un clúster de kubernetes. Este actúa como puente entre el control plane y los worker node.
Opera en un bucle continuo de "Watch-Loop" (bucle de vigilancia) donde constantemente consulta al control plane (API Server) para obtener las instrucciones más recientes sobre los Pods que deben ejecutarse en su nodo. Una vez que recibe estas instrucciones, se encarga de que el runtime de contenedores (Containerd) cree, inicie, detenga o elimine los contenedores correspondientes.
Guía oficial de instalación:
https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
Kubelet expone una API REST que permite a los usuarios interactúar con el nodo. Esta API se puede utilizar para obtener información sobre el nodo, los pods que se están ejecutando en el nodo, los contenedores que se están ejecutando en el nodo, etc. La API se puede utilizar para obtener información sobre el nodo, los pods que se están ejecutando en el nodo, los contenedores que se están ejecutando en el nodo, etc.
Configuración de kubelet para desactivar la autenticación y la autorización:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: true
webhook:
enabled: false
authorization:
mode: AlwaysAllow
Reinicia y verifica el estado de kubelet:
sudo systemctl restart kubelet
sudo systemctl status kubelet
curl -k https://localhost:10250/healthz
Los pods estáticos son gestionados directamente por kubelet en un nodo específico, en vez del servidor de la api de kubernetes.
A diferencia de los pods normales que se crean y se gestionan a través del control plane, los pods estáticos son definidos mediante fichero de manifiestos (yml) en un directorio que kubelet monitoriza.
¿Por qué usar pods estáticos?
Su uso principal es ejecutar los propios componentes delcontrol planedentro del clúster. Necesitas uncontrol planepara lanzar pods, pero necesitas pods para levantar elcontrol plane. Los pods estáticos rompen ese ciclo, ya que kubelet los gestiona directamente sin necesitar un API Server. También son útiles para probar la funcionalidad de kubelet de forma aislada.
Configura kubelet para que monitorice el directorio de pods estáticos y crea un fichero de configuración en el directorio de complementos de kubelet
/var/lib/kubelet/config.d/50-static-pods.conf
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
staticPodPath: /etc/kubernetes/manifests
Ejemplo de manifiesto:
apiVersion: v1
kind: Pod
metadata:
name: podinfo
spec:
hostNetwork: true
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
Nota: 💡 Observa
hostNetwork: trueen la especificación del pod.
Esto hace que el pod utilice el namespace de red del host en lugar de crear el suyo propio. El pod comparte la IP del host, por lo que el puerto 9898 es directamente accesible desde localhost.
Esta configuración es habitual en pods estáticos que necesitan acceso directo desde el host.
Importante: Por su propia naturaleza, los pods estáticos no pueden hacer referencia a otros objetos de la API de Kubernetes, como Secrets, ConfigMaps o ServiceAccounts.
Solo pueden utilizar recursos disponibles directamente en el nodo, como volúmenes hostPath o emptyDir.
La interfaz CRI es una API de gRPC que define un estandar para la comunicación entre kubelet y los runtimes de contenedores. Habilita kubelet para usar una amplia variedad de entornos de ejecución de contenedores, sin necesidad de recompilar kubelet cada vez que se quiere usar un nuevo runtime.
El CRI define un conjunto de operaciones que kubelet puede invocar en el runtime de contenedores, como:
Muchos entornos de ejecución de contenedores (como containerd y CRI-O) implementan la CRI de forma nativa, mientras que otros (como Docker) requieren una capa de adaptación para salvar la brecha entre el kubelet y el entorno de ejecución.
A diferencia de otras especificaciones mencionadas anteriormente (OCI, CNI), que son independientes de Kubernetes, el CRI forma parte del propio Kubernetes. Debido al predominio de Kubernetes en la orquestación de contenedores, muchos entornos de ejecución de contenedores implementan ahora el CRI de forma nativa para lograr una integración perfecta.
El flujo de trabajo de CRI es el siguiente:
Kubernetes → Kubelet → CRI → Containerd → runc (OCI Runtime) → Contenedor
Consiste en dos servicios principales:
Cuando kubelet necesita:
Un PodSandbox es un entorno compartido en el que se ejecutan los contenedores de un od, lo que cubre la brecha conceptual entre la abstracción de Pod de Kubernetes y el entorno de ejecución del contenedor. Cuando kubelet crea un pod, primero crea un pod sandbox llamando al método gRPC correspondiente. Una vez creado el pod sandbox, kubelet lanza los contenedores del pod en dicho entorno.
Lo que realmente implica un pod sandbox de pod y cómo se gestiona es un detalle de implementación del tiempo de ejecución del contenedor.
Los tiempos de ejecución de contenedores tradicionales como (Docker y containerd) que ejecutan contenedores como procesos aislados suelen crear un contenedor de pausa para configurar el sandbox. Esto es necesario, ya que los espacios de nombres de Linux requieren que exista un proceso en ejecución para mantenerlos activos. El contenedor de pausa es un contenedor ligero que ejecuta un único proceso, normalmente un comando sleep, que mantiene el contenedor en ejecución indefinidamente, conservando los espacios de nombres y manteniendo vivo el entorno de pruebas.
Por eso ves contenedores en pausa cuando visualizas la lista de contenedores en ejecución:
sudo ctr --namespace k8s.io container ls
ctr y nerdtctl ambas herramientas interactúan directamente con la api nativa de containerd.crictl esta herramienta usa CRI, la misma interfaz que usa kubelet.kubelet usa CRI para interactúar con el runtime de contenedores.A diferencia de ctr y nerdctl, crictl no necesita el parámetro --namespace, ya que utiliza CRI, que siempre opera en el contexto de Kubernetes.
kubectl get pods pero directamente desde el el CRI):sudo crictl pods
sudo crictl ps -a
sudo crictl images
# Primero obtenemos el ID del pod
POD_ID=$(sudo crictl pods -q --name podinfo-worker)
# Luego obtenemos los detalles del pod
sudo crictl inspectp $POD_ID
sudo crictl exec -it $POD_ID /bin/sh
sudo crictl logs $CONTAINER_ID
kubeletctl es una herramienta de línea de comandos que permite interactúar con el API de kubelet de forma directa.
Es útil para depurar problemas en el clúster, ya que permite ver información detallada sobre los pods, contenedores y nodos.
Nota: 💡 kubeletctl no es una herramienta oficial de Kubernetes, sino una herramienta de terceros que permite interactúar con el API de kubelet de forma directa. Es diferente de
kubectl, que interactúa con el API de Kubernetes.
Guía de instalación:
https://kubernetes.io/es/docs/tasks/tools/included/install-kubectl-linux/
etcd es un almacén de datos distribuido clave-valor que actúa como la copia principal del estado del clúster. Almacena la información de configuración, información del estado del clúster y metadatos de los objetos de Kubernetes.
Originalmente fue desarrollado por CoreOS para su sistema operativo de contenedores CoreOS Linux, ahora es mantenido por CNCF. Está diseñada para ser un servicio de alta disponibilidad, consistente y fiable. Todos estas cualidades son esenciales para un sistema que contiene la fuente de verdad** del clúster.
etcd implementa el algoritmo de consenso Raft para garantizar la coherencia entre varios nodos. El clúster puede seguir funcionando incluso si algunos nodos fallan, siempre y cuando la mayoría de los nodos (un quórum) sigan en buen estado.
Los clústeres de etcd suelen ejecutarse con un número impar de nodos (3, 5 o 7) para mantener un quórum adecuado.
La garantía de consistencia fuerte de etcd requiere que todas las operaciones sensibles a la consistencia (por ejemplo, cualquier operación de escritura) fluyan a través de un nodo líder, que se elige mediante el algoritmo de consenso Raft. En caso de fallo del líder, los nodos restantes eligen automáticamente un nuevo líder.
La consistencia fuerte es esencial para que Kubernetes garantice que todos los componentes vean el mismo estado del clúster. Sin esta garantía, Kubernetes podría programar pods en nodos que carecen de capacidad, o los controladores podrían tomar decisiones contradictorias basadas en datos obsoletos.
Solo el servidor de la API de Kubernetes se comunica directamente con etcd. Todos los demás componentes (como kubelet, el programador y los controladores) interactúan con el estado del clúster a través del servidor de la API, que actúa como puerta de enlace a etcd.
Nota: 💡 Este diseño protege a etcd del acceso directo por parte de los componentes del clúster y centraliza el control de la autenticación, la autorización, la admisión y la validación para garantizar que el estado del clúster se mantenga válido y coherente.
Guía de instalación:
https://etcd.io/docs/v3.6/install/
etcd ofrece interfaces de programación de aplicaciones (API) tanto HTTP como gRPC. Puedes probar la API HTTP con curl:
curl http://127.0.0.1:2379/health
Sin embargo etcd también ofrece una CLI llamada etcdctl que puede ser más cómoda de usar:
etcdctl endpoint health
Como almacén de clave-valor, etcd permite almacenar y recuperar datos en forma de pares clave-valor:
etcdctl put foo bar
Recupera datos:
etcdctl get foo
De forma predeterminada, etcdctl devuelve tanto la clave como el valor. Para recuperar solo el valor, utiliza el indicador --print-value-only:
etcdctl get foo --print-value-only
etcd admite la recuperación de claves basada en prefijos mediante el indicador --prefix, lo que permite organizar los datos en una estructura jerárquica (al igual que hace Kubernetes).
Crea algunas claves con una estructura similar a una ruta:
etcdctl put /foo bar
etcdctl put /foo/bar baz
etcdctl put /foo/bar/baz bat
Recupera claves con prefijo:
etcdctl get /foo --prefix
Protege etcd con TLS, es esencial cualquier cluster de producción: sin TLS, cualquier persona que pueda acceder a la red de etcd puede leer y modificar los datos almacenados.
Expondría datos críticos que cualquiera podría intercerptar en la red:
Sin embargo, TLS ofrece mucho más que simple cifrado. También permite autenticar a los clientes mediante la autenticación mTLS (TLS mutuo), lo que garantiza que solo los componentes autorizados puedan acceder a los datos de estado del clúster o modificarlos.
Crea el directorio para almacenar los certificados:
sudo mkdir -p /etc/etcd/pki
cd /etc/etcd/pki
Crea una entidad certificadora (CA) y genera un certificado para etcd:
sudo openssl genrsa -out ca.key 4096
sudo openssl req -x509 -new -nodes -key ca.key -out ca.crt -subj "/CN=etcd" -sha256 -days 3650
Genera el fichero de configuración para el certificado del servidor:
cat <<EOF | sudo tee server.cnf
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
CN = server
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
DNS.2 = $(hostname)
IP.1 = 127.0.0.1
IP.2 = ::1
IP.3 = $(ip -o -4 addr show | grep 'eth' | awk '{split($4,a,"/"); print a[1]}' | paste -sd,)
EOF
Genera el certificado del servidor:
sudo openssl genrsa -out server.key 2048
sudo openssl req -new -key server.key -out server.csr -config server.cnf
sudo openssl x509 -req -in server.csr -out server.crt \
-CA ca.crt -CAkey ca.key \
-days 365 -extfile server.cnf -extensions req_ext
Genera el certificado del cliente:
sudo openssl genrsa -out client.key 2048
sudo openssl req -new -key client.key -out client.csr -subj "/CN=etcd/O=etcd"
sudo openssl x509 -req -in client.csr -out client.crt \
-CA ca.crt -CAkey ca.key \
-days 365
Cambia el propietario a etcd:
sudo chown -R etcd:etcd .
Haz el certificado del cliente accesible a todos los usuarios:
sudo chmod 644 client.key
Nota: 💡 Esto garantiza que todos los usuarios (incluido el usuario del laboratorio) puedan autenticarse en etcd. En entornos de producción, esto NO es recomendable.
Configura etcd para que utilice TLS en las conexiones con el servidor y TLS mutuo para la autenticación de clientes:
cd ~
cat <<EOF | sudo tee -a /etc/default/etcd
ETCD_LISTEN_CLIENT_URLS=https://0.0.0.0:2379
ETCD_CLIENT_CERT_AUTH=true
ETCD_CERT_FILE=/etc/etcd/pki/server.crt
ETCD_KEY_FILE=/etc/etcd/pki/server.key
ETCD_TRUSTED_CA_FILE=/etc/etcd/pki/ca.crt
ETCD_NAME=$(hostname)
ETCD_ADVERTISE_CLIENT_URLS=https://$(hostname):2379
EOF
Desglose de variables:
Reinicia el servicio de etcd para que los cambios surtan efecto:
sudo systemctl daemon-reload
sudo systemctl restart etcd
Configura etcdctl para que utilice TLS:
cat <<EOF | tee -a "$HOME/.bashrc" "$HOME/.profile"
export ETCDCTL_CACERT=/etc/etcd/pki/ca.crt
export ETCDCTL_CERT=/etc/etcd/pki/client.crt
export ETCDCTL_KEY=/etc/etcd/pki/client.key
export ETCDCTL_ENDPOINTS=https://127.0.0.1:2379
EOF
Verifica la comunicación con etcd usando TLS:
bash --login -c "etcdctl endpoint health"
kube-apiserver actúa como la puerta de entrada al clúster de Kubernetes. Es el único componente que se comunica directamente con etcd y expone la API de Kubernetes al resto de componentes del clúster. Procesa operaciones REST, las valida y actúaliza los objetos correspondientes en etcd.
Desempeña varias funciones críticas:
Nota: 💡 kube-apiserver es el componente más crítico del clúster, ya que cualquier problema con él puede afectar a todo el clúster. Cada comando de kubectl que ejecutes se comunica con kube-apiserver, que a su vez se comunica con etcd para obtener y modificar el estado del clúster.
KUBE_VERSION=v1.34.0
Página oficial de descarga de binarios de Kubernetes:
curl -fsSLO "https://dl.k8s.io/${KUBE_VERSION?}/bin/linux/amd64/kube-apiserver"
sudo install -m 755 kube-apiserver /usr/local/bin
Descarga de la plantilla de systemd (labs.iximiuz):
sudo wget -O /etc/systemd/system/kube-apiserver.service https://labs.iximiuz.com/content/files/courses/kubernetes-the-very-hard-way-0cbfd997/03-control-plane/02-kube-apiserver/__static__/kube-apiserver.service?v=1773549427
Configur prerrequisitos:
sudo mkdir -p /etc/kubernetes/pki
cd /etc/kubernetes/pki
Generar certificados para kube-apiserver:
sudo openssl genrsa -out sa.key 2048
sudo openssl rsa -in sa.key -pubout -out sa.pub
cd ~
Configuración con los parametros necesarios que incluyen:
A diferencia de otros componentes de Kubernetes, kube-apiserver se configura principalmente mediante opciones de línea de comandos, en lugar de archivos de configuración.
Esto significa que tendrás que editar el archivo de unidad de systemd y añadir las opciones de línea de comandos necesarias.
/etc/systemd/system/kube-apiserver.service:
[Unit]
# ...
[Service]
# ...
ExecStart=/usr/local/bin/kube-apiserver \
--service-cluster-ip-range=10.96.0.0/12 \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--service-account-key-file=/etc/kubernetes/pki/sa.pub \
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key \
--etcd-cafile=/etc/etcd/pki/ca.crt \
--etcd-certfile=/etc/etcd/pki/client.crt \
--etcd-keyfile=/etc/etcd/pki/client.key \
--etcd-servers=https://127.0.0.1:2379
# ...
Reiniciar el servicio:
sudo systemctl daemon-reload
sudo systemctl restart kube-apiserver
Después de instalar kube-apiserver, configura la autenticación y autorización para que los componentes del clúster puedan autenticarse entre sí y los usuarios puedan autenticarse mediante tokens de cuentas de servicio.
kube-apiserver incluye los siguientes valores por defecto:
Sin embargo, esta configuración es inválida: API server no puede autorizar las autenticaciones anónimas con AlwaysAllow. API server automaticamente deshabilita las autenticaciones anónimas.
Para accerder al API server es necesario configurar un modo de autenticación diferente o modificar el modo de autorización.
kube-apiserver soporta los siguientes modos de autenticación: https://kubernetes.io/docs/reference/access-authn-authz/authentication/
Nota: 💡 En este laboratorio utilizaremos X509 client certificates para autenticarnos en kube-apiserver.
Los tokens estáticos pueden ser configurados mediante la creación de un fichero. Son fichero .csv y deben contener al menos 3 columnas:
Crea el token con el usuario admin:
echo "iximux,admin,admin,system:masters" | sudo tee /etc/kubernetes/tokens.csv
Nota: El grupo "system:masters" es un grupo especial en Kubernetes que tiene permisos de administrador sobre el clúster.
/etc/systemd/system/kube-apiserver.service:
[Unit]
# ...
[Service]
# ...
ExecStart=/usr/local/bin/kube-apiserver \
--service-cluster-ip-range=10.96.0.0/12 \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--service-account-key-file=/etc/kubernetes/pki/sa.pub \
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key \
--etcd-cafile=/etc/etcd/pki/ca.crt \
--etcd-certfile=/etc/etcd/pki/client.crt \
--etcd-keyfile=/etc/etcd/pki/client.key \
--etcd-servers=https://127.0.0.1:2379 \
--anonymous-auth=false \
--token-auth-file=/etc/kubernetes/tokens.csv
# ...
kubernetes soporta los siguientes modos de autorización: https://kubernetes.io/docs/reference/access-authn-authz/authorization/
Aun que se podria dejar el modo por defecto, esto no es realista para un entorno de producción.
En este laboratorio utilizaremos RBAC para autorizar las peticiones a kube-apiserver.
/etc/systemd/system/kube-apiserver.service:
[Unit]
# ...
[Service]
# ...
ExecStart=/usr/local/bin/kube-apiserver \
--service-cluster-ip-range=10.96.0.0/12 \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--service-account-key-file=/etc/kubernetes/pki/sa.pub \
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key \
--etcd-cafile=/etc/etcd/pki/ca.crt \
--etcd-certfile=/etc/etcd/pki/client.crt \
--etcd-keyfile=/etc/etcd/pki/client.key \
--etcd-servers=https://127.0.0.1:2379 \
--anonymous-auth=false \
--token-auth-file=/etc/kubernetes/tokens.csv \
--authorization-mode=RBAC
# ...
Reinicia el servicio:
sudo systemctl daemon-reload
sudo systemctl restart kube-apiserver
Verifica que kube-apiserver está funcionando correctamente:
bash --login -c "kubectl get nodes"
Comprueba que kube-apiserver ahora requiere autenticación probando tanto las solicitudes autenticadas como las no autenticadas.
Fail:
curl -f -k https://localhost:6443/api/v1/namespaces
Success:
curl -f -k -H "Authorization: Bearer iximiuz" https://localhost:6443/api/v1/namespaces
La API de kubernetes es una API REST que define la estructura y el comportamiento del servidor de la API. Lo que permite el la consulta y manipulación de los objetos del estado del clúster. Podemas interactúar con peticiones HTTP con herramientas como curl.
Los objetos de kubernetes son recursos que representan los componentes del clúster. Los objetos más comunes son:
Aunque estos términos suelen utilizarse indistintamente, tienen significados distintos:
- API de Kubernetes: la especificación general y el conjunto de puntos de conexión que definen cómo interactúan los clientes con el clúster.
- Servidor de API: el componente del
control planede Kubernetes que implementa la API de Kubernetes.- kube-apiserver: la implementación de referencia de la API de Kubernetes, que actúa como servidor de API en la mayoría de los clústeres de Kubernetes.
Consultas a la API de Kubernetes mediante curl:
curl -f -k https://127.0.0.1:6443/api/v1/namespaces \
-H "Authorization: Bearer iximiuz" \
| jq -r '.items[].metadata.name'
curl -f -k https://127.0.0.1:6443/api/v1/namespaces \
-H "Authorization: Bearer iximiuz" \
-X POST \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "test-curl"
}
}'
curl -f -k https://127.0.0.1:6443/api/v1/namespaces/test-curl \
-H "Authorization: Bearer iximiuz" \
| jq
curl -f -k https://127.0.0.1:6443/api/v1/namespaces/test-curl \
-H "Authorization: Bearer iximiuz" \
-X PATCH \
-H "Content-Type: application/merge-patch+json" \
-d '{"metadata": {"labels": {"foo": "bar"}}}'
curl -f -k https://127.0.0.1:6443/api/v1/namespaces/test-curl \
-H "Authorization: Bearer iximiuz" \
| jq '.metadata.labels'
curl -f -k https://127.0.0.1:6443/api/v1/namespaces/test-curl \
-H "Authorization: Bearer iximiuz" \
-X DELETE
Kubectl es la herramienta oficial de línea de comandos para interactúar con la API de Kubernetes. Permite gestionar los recursos del clúster de forma interactiva.
Guía oficial de instalación: https://kubernetes.io/es/docs/tasks/tools/included/install-kubectl-linux/
Es necesario configurar kubectl para que pueda autenticarse en kube-apiserver. Para ello, se debe crear un fichero kubeconfig que contenga la información necesaria para autenticarse en kube-apiserver.
Nota: 💡 Por defecto kubectl busca el fichero kubeconfig en el directorio $HOME/.kube/config. Si no existe, se debe crear.
Se puede especificar la ruta del fichero kubeconfig mediante la variable de entorno KUBECONFIG o mediante la opción --kubeconfig de kubectl.
Nota: 💡 Para crear el fichero kubeconfig se puede utilizar la herramienta kubeadm, que se instala junto con kubelet y kube-apiserver. O en su defecto, se puede configurar directamente con kubectl paso por paso.
kubectl config set-cluster default \
--insecure-skip-tls-verify \
--server=https://localhost:6443
Nota: ⚠️ En este caso no se utiliza TLS, pero en un entorno de producción se debe utilizar TLS.
kubectl config set-credentials iximiuz \
--token=iximiuz
kubectl config set-context default \
--cluster=default \
--user=iximiuz
kubectl config use-context default
TLS es un protocolo de seguridad que se utiliza para cifrar las comunicaciones entre el cliente y el servidor. En este caso, se utiliza para cifrar las comunicaciones entre kubectl y kube-apiserver.
Sin embargo, TLS ofrece mucho más que simple cifrado. También permite autenticar a los clientes mediante mTLS (TLS mutuo) y desempeña un papel fundamental en el RBAC: los certificados de cliente pueden incluir campos de organización que se corresponden con grupos de Kubernetes, lo que garantiza que solo los usuarios y componentes autorizados puedan acceder a los recursos del clúster.
Si kube-apiserver se inicializa sin TLS, este automáticamente genera certificados autofirmados para habilitar, son sencillos para https.
Directorio de certificados y claves:
ls -l /etc/kubernetes/pki
cd /etc/kubernetes/pki
Crea la Entidad Certificadora (CA) para firmar certificados:
sudo openssl genrsa -out ca.key 2048
sudo openssl req -x509 -new -nodes -key ca.key -out ca.crt -subj "/CN=kubernetes" -sha256 -days 3650
Nota: Aunque se podrían crear autoridades de certificación (CA) independientes para los certificados de servidor y de cliente, las herramientas habituales de implementación de Kubernetes (como kubeadm) utilizan una única CA.
Configura el fichero kube-apiserver:
cat <<EOF | sudo tee apiserver.cnf
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
CN = kube-apiserver
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
DNS.2 = kubernetes
DNS.3 = kubernetes.default
DNS.4 = kubernetes.default.svc
DNS.5 = kubernetes.default.svc.cluster.local
DNS.6 = control-plane
IP.1 = 127.0.0.1
IP.2 = ::1
IP.3 = 10.96.0.1
IP.4 = $(ip -o -4 addr show | grep 'eth' | awk '{split($4,a,"/"); print a[1]}' | paste -sd,)
EOF
Nota: 10.96.0.1 es la primera dirección IP del rango de direcciones IP del clúster de servicios.
Esta dirección IP se asigna automáticamente al servicio de Kubernetes en el espacio de nombres predeterminado.
Certificado de servidor:
sudo openssl genrsa -out apiserver.key 2048
sudo openssl req -new -key apiserver.key -out apiserver.csr -config apiserver.cnf
sudo openssl x509 -req -in apiserver.csr -out apiserver.crt \
-CA ca.crt -CAkey ca.key \
-days 365 -extfile apiserver.cnf -extensions req_ext
Nota: El campo «O=system:masters» (organización) del certificado se traduce a un grupo de Kubernetes. Los usuarios del grupo «system:masters» tienen privilegios de administrador del clúster a través de RBAC.
Esto muestra cómo se integran los certificados TLS con la autorización de Kubernetes.
Configuración actúalizada de systemd para kube-apiserver:
[Unit]
# ...
[Service]
# ...
ExecStart=/usr/local/bin/kube-apiserver \
--service-cluster-ip-range=10.96.0.0/12 \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--service-account-key-file=/etc/kubernetes/pki/sa.pub \
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key \
--etcd-cafile=/etc/etcd/pki/ca.crt \
--etcd-certfile=/etc/etcd/pki/client.crt \
--etcd-keyfile=/etc/etcd/pki/client.key \
--etcd-servers=https://127.0.0.1:2379 \
--anonymous-auth=false \
--token-auth-file=/etc/kubernetes/tokens.csv \
--authorization-mode=Node,RBAC \
--tls-cert-file=/etc/kubernetes/pki/apiserver.crt \
--tls-private-key-file=/etc/kubernetes/pki/apiserver.key \
--client-ca-file=/etc/kubernetes/pki/ca.crt
# ...
Reiniciar kube-apiserver:
sudo systemctl daemon-reload
sudo systemctl restart kube-apiserver
Configurar kubectl para que pueda autenticarse en kube-apiserver con TLS:
kubectl config set-cluster default \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--server=https://localhost:6443
kubectl config set-credentials default \
--client-certificate=/etc/kubernetes/pki/admin.crt \
--client-key=/etc/kubernetes/pki/admin.key \
--token=""
Verificar que kubectl se conecta con API server:
kubectl cluster-info
Verificar que API server proporciona certificados de clientes válidos:
curl -f https://127.0.0.1:6443/api/v1/namespaces \
--cacert /etc/kubernetes/pki/ca.crt \
--cert /etc/kubernetes/pki/admin.crt \
--key /etc/kubernetes/pki/admin.key
Kubeapi-server es la puerta de enlace a etcd y el unico componentent que puede leer y escribir en el estado persistente del clúster de manera directa.
Kubernetes almacena todos los datos bajo el prefijo /registry en etcd. Por lo tanto, si queremos ver los datos que almacena Kubernetes en etcd, debemos buscar en el prefijo /registry.
Estructuras de las claves en etcd:
etcdctl get --prefix /registry --keys-only
Nota: 💡 Notice the pattern: keys follow the structure
/registry/<resource-type>/[<namespace>/]<name>. This hierarchical layout mirrors the Kubernetes API structure you explored earlier.
ejemplo: listar los namespaces
etcdctl get --prefix /registry/namespaces --keys-only
Leer objetos de etcd:
etcdctl get /registry/namespaces/default --print-value-only
Verás una mezcla de datos binarios con algunas cadenas reconocibles repartidas por todo el texto.
Esto se debe a que Kubernetes codifica los objetos utilizando Protocol Buffers (protobuf) antes de almacenarlos en etcd. Protobuf es un formato de serialización binario compacto que resulta mucho más eficiente que JSON o YAML, pero a costa de la legibilidad.
Decodificar objetos con auger:
Para decodificar objetos de etcd, se puede utilizar la herramienta auger, que se instala junto con kubelet y kube-apiserver. O en su defecto, se puede configurar directamente con auger paso por paso.
AUGER_VERSION=1.0.3
curl -fsSLO "https://github.com/etcd-io/auger/releases/download/v${AUGER_VERSION?}/auger_${AUGER_VERSION?}_linux_amd64.tar.gz"
tar xzvof "auger_${AUGER_VERSION?}_linux_amd64.tar.gz"
sudo install -m 755 {auger,augerctl} /usr/local/bin
Ahora decodificar el objeto namespace:
etcdctl get /registry/namespaces/default --print-value-only | auger decode
Nota: 💡 Los datos de Protobuf se descodifican ahora en una representación YAML habitual del objeto Namespace, el mismo formato que se obtendría con el comando
kubectl get namespace default -o yaml
Otra forma:
etcdctl get /registry/namespaces/default --print-value-only | auger decode -o json | jq
Esto ilustra un concepto importante: kube-apiserver actúa como traductor entre la API legible para los humanos y el formato de almacenamiento binario de etcd. Cuando se crea un recurso mediante kubectl o la API REST, kube-apiserver lo valida, lo serializa a protobuf y lo escribe en etcd. Cuando se lee un recurso, el proceso se invierte.
Kube-scheduler es el encargado de asignar los pods a los nodos del clúster. Para ello, kube-scheduler observa los pods que se encuentran en estado "pending" y los asigna a los nodos que mejor se adapten a sus necesidades.
Nota: 💡 para saber determinar cual es el mejor nodo para un pod, kube-scheduler utiliza una serie de algoritmos que tienen en cuenta factores como los recursos disponibles en cada nodo, las afinidades y tolerancias de los pods, las políticas de planificación y otros factores.
Si creamos un pod antes de instalar kube-scheduler, el pod se quedará en estado "pending" hasta que se instale kube-scheduler. Una vez instalado kube-scheduler, el pod se asignará a un nodo y se pondrá en estado "running".
Creación nodo worker-01:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Node
metadata:
name: worker-01
status:
capacity:
cpu: "4"
memory: 8Gi
pods: "110"
allocatable:
cpu: "4"
memory: 8Gi
pods: "110"
conditions:
- type: Ready
status: "True"
reason: KubeletReady
message: Node is ready
EOF
Podemos asignarlo directamnete a un nodo de forma declarativa en el yaml del pod:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: podinfo-node-name
spec:
nodeName: worker-01
automountServiceAccountToken: false
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
EOF
Aunque este enfoque funciona cuando es necesario ubicar un Pod en un nodo específico, la asignación manual de Pods a nodos no es escalable, puede que no sea posible en todos los casos y, en gran medida, va en contra del objetivo de la orquestación de contenedores.
Sin embargo, existe un problema más fundamental en lo que respecta a la programación: los Pods son (en su mayoría) objetos inmutables. Una vez creado, la especificación de un Pod no se puede modificar, incluido el campo nodeName.
Esta inmutabilidad plantea una cuestión importante: si un Pod se crea sin un nodeName (sin programar) y los Pods no se pueden modificar tras su creación, ¿cómo lo asigna el programador a un nodo?
La respuesta reside en un recurso especial de Kubernetes llamado Binding. Se trata de un subrecurso interno del Pod que proporciona una ruta de API dedicada para asignar Pods a nodos. En lugar de requerir una actúalización completa del Pod, permite una operación específica en la subrecurso /binding del Pod, que establece el campo spec.nodeName del Pod.
Dado que kubectl no admite la creación directa de Binding (se trata de una operación interna), utilice la API de Kubernetes directamente para crear un objeto Binding para el Pod creado anteriormente:
curl -f -k https://127.0.0.1:6443/api/v1/namespaces/default/pods/podinfo-unscheduled/binding \
--cacert /etc/kubernetes/pki/ca.crt \
--cert /etc/kubernetes/pki/admin.crt \
--key /etc/kubernetes/pki/admin.key \
-X POST \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "v1",
"kind": "Binding",
"metadata": {
"name": "podinfo-unscheduled"
},
"target": {
"apiVersion": "v1",
"kind": "Node",
"name": "worker-01"
}
}'
Se puede instalar kube-scheduler de varias formas, pero la más común es utilizar kubeadm. Sin embargo, en este caso lo vamos a instalar manualmente.
KUBE_VERSION=v1.34.0
curl -fsSLO "https://dl.k8s.io/${KUBE_VERSION?}/bin/linux/amd64/kube-scheduler"
sudo install -m 755 kube-scheduler /usr/local/bin
sudo wget -O /etc/systemd/system/kube-scheduler.service https://labs.iximiuz.com/content/files/courses/kubernetes-the-very-hard-way-0cbfd997/03-control-plane/03-kube-scheduler/__static__/kube-scheduler.service?v=1773549428
Generar certificado de cliente para kube-scheduler:
(
cd /etc/kubernetes/pki
sudo openssl genrsa -out scheduler.key 2048
sudo openssl req -new -key scheduler.key -out scheduler.csr -subj "/CN=system:kube-scheduler"
sudo openssl x509 -req -in scheduler.csr -out scheduler.crt \
-CA ca.crt -CAkey ca.key \
-days 365
)
Kubeconfig file para kube-scheduler:
sudo kubectl config set-cluster default \
--kubeconfig=/etc/kubernetes/scheduler.conf \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true \
--server=https://127.0.0.1:6443
sudo kubectl config set-credentials default \
--kubeconfig=/etc/kubernetes/scheduler.conf \
--client-certificate=/etc/kubernetes/pki/scheduler.crt \
--client-key=/etc/kubernetes/pki/scheduler.key \
--embed-certs=true
sudo kubectl config set-context default \
--kubeconfig=/etc/kubernetes/scheduler.conf \
--cluster=default \
--user=default
sudo kubectl config use-context default \
--kubeconfig=/etc/kubernetes/scheduler.conf
Reiniciar kube-scheduler:
sudo systemctl daemon-reload
sudo systemctl restart kube-scheduler
Despues de instalar kube-scheduler, los pods pueden ser asignados de forma automática a los nodos del clúster.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: podinfo-unscheduled
spec:
automountServiceAccountToken: false
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
EOF
Kube controller manager es un demonio que gestiona varios controladores en un bucle contínuo para mantener el estado deseado del clúster.
Circuitos de control y reconciliación:
Controller es un proceso que continuamente observa el estado actúal del clúster y lo compara con el estado deseado. Si existe una discrepancia, el controlador toma medidas para corregirla. Este proceso se conoce como reconciliación.
Nota: 💡 Kubelet es, en esencia, un controlador para un único nodo. kube-controller-manager generaliza esta idea a todo el clúster.
Estado actúal vs estado deseado:
Esta es la filosofía fundamental de Kubernetes: tú declaras lo que quieres (el estado deseado) y los controladores trabajan continuamente para hacerlo realidad (el estado real).
El estado deseado se encuentra en el campo .spec de un recurso.
El estado real se refleja en el campo .status.
Los controladores detectan las diferencias y actúan para reducir la brecha.
Por ejemplo, el campo .spec.replicas de un ReplicaSet dice: «Quiero 3 Pods». El controlador del ReplicaSet cuenta los Pods reales que coinciden con el selector. Si solo hay 2, crea uno más. Si hay 4, elimina uno.
Esto es fundamentalmente diferente de los sistemas imperativos, en los que le dices al sistema qué hacer. En Kubernetes, le dices al sistema lo que quieres, y los controladores averiguan cómo llegar hasta allí.
Kube-controller-manager es un demonio que agrupa los controladores principales de kubernetes en un único binario.
Cada controlador es, desde el punto de vista lógico, un proceso independiente, pero todos se compilan juntos y se ejecutan como bucles concurrentes dentro de un mismo programa. Se trata únicamente de una cuestión de comodidad operativa: ejecutar docenas de binarios de controladores independientes sería una pesadilla a la hora de la implementación.
Todos los controladores se comunican con el clúster exclusivamente a través del servidor de la API. Los controladores nunca acceden directamente a etcd. Están atentos a los cambios, toman decisiones y envían los resultados a través de la API, del mismo modo que tú interactúas con el clúster utilizando kubectl.
kube-controller-manager incluye decena de controladores, estos son algunos de los mas importantes agrupados por funcion:
| Controlador | Descripción |
|---|---|
| ReplicaSet | Asegura que el número deseado de réplicas de Pods se esté ejecutando en todo momento |
| Deployment | Gestiona los ReplicaSets, maneja actúalizaciones graduales (rolling updates) y reversiones (rollbacks) |
| DaemonSet | Asegura que una copia de un Pod se ejecute en todos los nodos (o en los seleccionados) |
| StatefulSet | Gestiona cargas de trabajo con estado, proporcionando una identidad estable y operaciones ordenadas |
| Job / CronJob | Ejecuta tareas hasta su finalización (una sola vez o de forma programada) |
Controladores de infraestructura (se ejecutan de forma silenciosa en segundo plano):
| Controlador | Descripción |
|---|---|
| ServiceAccount | Crea automáticamente una cuenta de servicio por defecto en cada nuevo namespace |
| Namespace | Limpia todos los recursos cuando se elimina un namespace |
| Node Lifecycle | Detecta nodos inalcanzables y expulsa (evicts) sus pods |
| Garbage Collector | Elimina recursos huérfanos (por ejemplo, Pods cuyo ReplicaSet fue eliminado) |
Nota: 💡 Puedes ver toda la lista de controladores usando el comando:
--controllerscon el binario de kube-controller-manager.
Core vs cloud vs custom controllers:
No todos los controllers viven dentro de kube-controller-manager. Hay tres categorías principales:
Core controllers: Se ejecutan dentro de kube-controller-manager y gestionan los recursos principales de Kubernetes (Pods, Services, Deployments, etc.).
Cloud controllers: Se ejecutan fuera de kube-controller-manager y gestionan los recursos específicos de la nube (LoadBalancers, PersistentVolumes, etc.).
Custom controllers: Se ejecutan fuera de kube-controller-manager y gestionan recursos personalizados (CRDs).
Si no instalamos kube-controller-manager, los pods no se asignarán a los nodos y no se crearán los recursos necesarios para que el clúster funcione correctamente. Aun que declaremos 3 replicas de un pod, solo se creará una.
Se puede instalar kube-controller-manager de varias formas, pero la más común es utilizar kubeadm. Sin embargo, en este caso lo vamos a instalar manualmente.
KUBE_VERSION=v1.34.0
curl -fsSLO "https://dl.k8s.io/${KUBE_VERSION?}/bin/linux/amd64/kube-controller-manager"
sudo install -m 755 kube-controller-manager /usr/local/bin
sudo wget -O /etc/systemd/system/kube-controller-manager.service https://labs.iximiuz.com/content/files/courses/kubernetes-the-very-hard-way-0cbfd997/03-control-plane/04-kube-controller-manager/__static__/kube-controller-manager.service?v=1773549428
Generar certificado de cliente para kube-controller-manager:
(
cd /etc/kubernetes/pki
sudo openssl genrsa -out controller-manager.key 2048
sudo openssl req -new -key controller-manager.key -out controller-manager.csr -subj "/CN=system:kube-controller-manager"
sudo openssl x509 -req -in controller-manager.csr -out controller-manager.crt \
-CA ca.crt -CAkey ca.key \
-days 365
)
Kubeconfig file para kube-controller-manager:
sudo kubectl config set-cluster default \
--kubeconfig=/etc/kubernetes/controller-manager.conf \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true \
--server=https://127.0.0.1:6443
sudo kubectl config set-credentials default \
--kubeconfig=/etc/kubernetes/controller-manager.conf \
--client-certificate=/etc/kubernetes/pki/controller-manager.crt \
--client-key=/etc/kubernetes/pki/controller-manager.key \
--embed-certs=true
sudo kubectl config set-context default \
--kubeconfig=/etc/kubernetes/controller-manager.conf \
--cluster=default \
--user=default
sudo kubectl config use-context default \
--kubeconfig=/etc/kubernetes/controller-manager.conf
Reiniciar kube-controller-manager:
sudo systemctl daemon-reload
sudo systemctl restart kube-controller-manager
Hasta ahora el "worker node" y el control plane han estado funcionando de forma aislada. Ahora vamos a unirlos para formar un clúster.
Cuando un nodo se une al clúster ocurren tres cosas:
La diferencia es que, además de gestionar pods estáticos a nivel local, ahora recibe tareas del control plane y envía informes a través de la API.
El problema de confianza: ¿Cómo el "API server" confía en un nuevo "kubelet"?
En las lecciones anteriores hemos generado los certificados para los componenetes del control plane de forma manual. Para los "worker nodes" este trabajo no es escalable, ya que si tenemos 50 nodos, tendríamos que generar 50 certificados manualmente. Por lo tanto, se utiliza un sistema de bootstrapping para generar los certificados de forma automática.
TLS Bootstrapping
Este proceso permite que "kubelet" que use token temporal con bajos privilegios para solicitar un certificado de cliente válido. Una vez que el certificado es firmado, "kubelet" lo utilizará en futuras comunicaciones.
control plane:Antes de unir un nodo, debemos configurar algunas cosas:
Generar certificado:
cd /etc/kubernetes/pki
sudo openssl genrsa -out apiserver-kubelet-client.key 2048
sudo openssl req -new -key apiserver-kubelet-client.key -out apiserver-kubelet-client.csr -subj "/CN=kube-apiserver-kubelet-client/O=system:masters"
sudo openssl x509 -req -in apiserver-kubelet-client.csr -out apiserver-kubelet-client.crt \
-CA ca.crt -CAkey ca.key \
-days 365
/etc/systemd/system/kube-apiserver.service:
[[Unit]
# ...
[Service]
# ...
ExecStart=/usr/local/bin/kube-apiserver \
--service-cluster-ip-range=10.96.0.0/12 \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--service-account-key-file=/etc/kubernetes/pki/sa.pub \
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key \
--etcd-cafile=/etc/etcd/pki/ca.crt \
--etcd-certfile=/etc/etcd/pki/client.crt \
--etcd-keyfile=/etc/etcd/pki/client.key \
--etcd-servers=https://127.0.0.1:2379 \
--anonymous-auth=false \
--token-auth-file=/etc/kubernetes/tokens.csv \
--authorization-mode=Node,RBAC \
--tls-cert-file=/etc/kubernetes/pki/apiserver.crt \
--tls-private-key-file=/etc/kubernetes/pki/apiserver.key \
--client-ca-file=/etc/kubernetes/pki/ca.crt \
--kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt \
--kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key \
--enable-bootstrap-token-auth=true
# ...
Reiniciar kube-apiserver:
sudo systemctl daemon-reload
sudo systemctl restart kube-apiserver
Cómo ya hemos hablado de ellos anteriormente, este es un breve resumen: son tokens con vida-corta y privilegios limitados específicamente diseñados para registar nuevos nodos.
Un "bootstrap token" tiene un formato específico [token-id].[token-secret] (ejemplo: a1b2c3.d4e5f6). Es almacenado como un secreto en el namespace kube-system.
Generar bootstrap token:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: bootstrap-token-abcdef
namespace: kube-system
# Special type for bootstrap tokens
type: bootstrap.kubernetes.io/token
stringData:
token-id: abcdef
token-secret: 0123456789abcdef
usage-bootstrap-authentication: "true"
# Optional expiration
# expiration: 9999-12-31T23:59:59Z
# Optional description
description: "Bootstrap token for kubelets to authenticate to the API server"
EOF
Los bootstrap tokenns autentican a los kubelets como miembros del grupo `system:bootstrappers. Por defecto este grupo no tiene permisos para hacer nada, por lo tanto debemos tener que concederles los permisos necesarios.
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubelet-bootstrap
subjects:
- kind: Group
name: system:bootstrappers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: system:node-bootstrapper
apiGroup: rbac.authorization.k8s.io
EOF
El proceoso de TLS bootstrap comienza con la solicitud de firma de certificado (CSR) para el certificado de cliente de kubelet.
En este caso, una CSR no es simplemente una solicitud de firma de certificado habitual, sino un recurso CertificateSigningRequest creado mediante la API de Kubernetes.
Kubernetes cuenta con un mecanismo integrado para la firma de certificados que existe precisamente con este fin: proporcionar a los clientes (no solo a los kubelets) una forma de solicitar y obtener del clúster certificados firmados que puedan utilizar para autenticarse en el servidor de la API.
Sin embargo, solicitar un certificado no es suficiente, ya que debe ser aprobado. Puedes hacerlo manualmente utilizando el comando kubectl certificate approve, pero aprobar cada CSR manualmente es tedioso y propenso a errores.
Para solucionar esto, podemos configurar que se aprueben de forma automática usando un "CertificateSigningRequest controller". (via kube-controller-manager)
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: node-autoapprove-bootstrap
subjects:
- kind: Group
name: system:bootstrappers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: system:certificates.k8s.io:certificatesigningrequests:nodeclient
apiGroup: rbac.authorization.k8s.io
EOF
Una vez que el certificado ha sido firmado y recibido por el kubelet, este pasa a utilizar el nuevo certificado para la autenticación. Se registra como nodo en el servidor API utilizando el nuevo certificado.
En este punto, el proceso de arranque ha finalizado. El token de arranque ya no es necesario y puede eliminarse (o ser eliminado por el kube-controller-manager una vez que haya caducado).
Podrías detenerte aquí y todo funcionaría bien, hasta que dejara de hacerlo. Los certificados tienen una característica de seguridad inherente: caducan.
Para que los nodos sigan autenticándose después de que caduquen sus certificados, también necesitas una rotación automática de certificados. Hasta este momento, kubelet se autenticaba como una identidad en el grupo system:bootstrappers. Una vez completado el arranque, pasa a utilizar un certificado de cliente adecuado. El certificado emitido durante el arranque identifica a kubelet como system:node:<nodename>, lo que lo convierte en miembro del grupo system:nodes.
Kubernetes utiliza permisos RBAC explícitos, por lo que no concederá automáticamente a los nodos la capacidad de rotar sus propios certificados.
Se necesita conceder permisos:
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: node-autoapprove-certificate-rotation
subjects:
- kind: Group
name: system:nodes
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
apiGroup: rbac.authorization.k8s.io
EOF
Con el control plane listo para aceptar nuevos nodos, es hora de configurar el "worker". Kubelet necesita tres cosas para poder unirse al clúster:
El "worker node" necesita el certificado CA del clúster para establecer una conexión segura con el servidor API. Para ello, debemos copiar el certificado CA del clúster al "worker node".
sudo mkdir -p /etc/kubernetes/pki
sudo scp control-plane:/etc/kubernetes/pki/ca.crt /etc/kubernetes/pki/ca.crt
HAsta ahora kubelet API no hay necesitado TLS, esto esta bien en local. Para un entorno de producción se debe de configurar.
/var/lib/kubelet/config.d/70-authnz.conf:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false
webhook:
enabled: false
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: AlwaysAllow
sudo kubectl config set-cluster default \
--kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true \
--server=https://control-plane:6443
sudo kubectl config set-credentials default \
--kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
--token=abcdef.0123456789abcdef
sudo kubectl config set-context default \
--kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
--cluster=default \
--user=default
sudo kubectl config use-context default \
--kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf
Para unirse a un clúster, el proceso kubelet necesita dos parámetros adicionales:
--bootstrap-kubeconfig: ruta al archivo kubeconfig de arranque para la autenticación inicial--kubeconfig: ruta en la que kubelet escribirá su archivo kubeconfig permanente tras el arranque/etc/systemd/system/kubelet.service:
[Unit]
# ...
[Service]
# ...
ExecStart=/usr/local/bin/kubelet \
--config-dir /var/lib/kubelet/config.d/ \
--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
--kubeconfig=/etc/kubernetes/kubelet.conf
# ...
Reiniciar kubelet:
sudo systemctl daemon-reload
sudo systemctl restart kubelet
Tras reiniciar kubelet en la sección anterior, el proceso de incorporación debería iniciarse automáticamente sin necesidad de ninguna configuración adicional.
kubectl wait --for=create node worker
kubectl get nodes
Cuando kubelet se inició con --bootstrap-kubeconfig, puso en marcha el proceso de configuración inicial de TLS:
Puedes verificar el CSR que se creó y aprobó durante el proceso de configuración inicial:
kubectl get csr
NotReadySi comprobamos el estado de los nodos veremos que el nodo worker esta en estado NotReady.
kubectl get nodes
Kubelet realiza una serie de comprobaciones antes de marcar el nodo como «Ready». El kube-scheduler detecta esto y espera a que el nodo esté listo antes de programar cargas de trabajo en él. De lo contrario, las cargas de trabajo podrían programarse en un nodo que no está preparado para ejecutarlas de forma fiable, lo que provocaría todo tipo de fallos.
En este caso, la comprobación que falla es la de disponibilidad de red: el nodo aún no tiene configuración de CNI. Es lo esperado, ya que configurarás la red y el CNI en la siguiente lección.
Puedes verificarlo consultando el estado del nodo:
kubectl describe node worker
Nos va a mostrar algo como:
container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
Nota:
kubectl describees uno de los comandos más infravalorados de Kubernetes. Cuando necesites depurar un problema,kubectl describedebería ser uno de los primeros comandos que ejecutes.
/etc/cni/net.d/99-loopback.conf:
{
"cniVersion": "1.0.0",
"name": "lo",
"type": "loopback"
}
kubelet debería recuperarse automáticamente en cuanto containerd indique que la red está lista.
Puedes comprobarlo ejecutando:
kubectl get nodes
workloadAhora que el nodo está ready, las cargas de trabajo pueden ejecutarse realmente, y no solo programarse.
Crea un ReplicaSet para comprobar que el clúster está plenamente operativo:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: podinfo
spec:
replicas: 1
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
spec:
hostNetwork: true
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
EOF
Espera a que el ReplicaSet cree un Pod:
kubectl get replicaset podinfo
kubectl get pods -l app=podinfo -w
A continuación, comprueba el pod resultante:
kubectl get pods -l app=podinfo -o wide
Este es el ciclo de vida completo de Kubernetes en acción:
kubectl envió la especificación del ReplicaSet a kube-apiserverkube-apiserver la almacenó en etcdkube-controller-manager detectó que ningún Pod coincidía con el número de réplicas deseado y creó unokube-scheduler detectó el Pod sin programar y lo asignó al nodo de trabajokubelet en el nodo de trabajo recibió la especificación del Podkubelet ordenó a containerd que descargara la imagen e iniciara el contenedorEn la lección anterior, conectaste un nodo de trabajo al control plane e implementaste una carga de trabajo. El pod se ejecutó con hostNetwork: true, lo que significa que compartía el espacio de nombres de red y la dirección IP del host.
Eso fue un atajo. En un clúster real, los pods obtienen sus propias direcciones IP y necesitan comunicarse entre sí a través del clúster, a menudo ejecutándose en nodos completamente diferentes.
Kubernetes cuenta con un modelo de red específico que lo hace posible. Define el resultado (cómo se direccionan los pods y cómo se comunican), pero no prescribe el mecanismo. En su lugar, delega la implementación real a otras capas.
El modelo de redes de Kubernetes se basa en tres reglas fundamentales:
Sin estas garantías, cada aplicación tendría que saber qué puerto se le ha asignado, en qué host se está ejecutando y cómo llegar a otros servicios a través de capas de NAT.
El modelo de Kubernetes elimina todo eso. Las aplicaciones pueden vincularse a puertos conocidos, detectarse entre sí más fácilmente y comunicarse como si todas estuvieran en la misma red plana.
Esto es lo que hace que las redes de Kubernetes parezcan «transparentes» para las aplicaciones.
Kubernetes, por sí mismo, no configura las conexiones de red de los pods. En su lugar, delega esa tarea al entorno de ejecución de contenedores. En esta configuración, containerd utiliza complementos CNI (Container Network Interface) para configurar las interfaces de los pods, las direcciones IP y las rutas en cada nodo.
Pero un clúster de Kubernetes rara vez se ejecuta en un solo nodo (al menos no en producción). Si los pods de diferentes nodos necesitan comunicarse, algo también tiene que coordinar la asignación de subredes y la conectividad entre nodos en todo el clúster.
El modelo de Kubernetes en sí mismo no distingue entre redes «en el mismo nodo» y «entre nodos». Se trata de un detalle de implementación que varía según las diferentes soluciones de red.
Sin embargo, establecer esa distinción resulta útil en esta lección, ya que ayuda a diferenciar dos aspectos:
| Layer | Lo que solventa |
|---|---|
| Node-level setup | Configuración a nivel de nodo: Proporcionar a los pods interfaces, IPs y conectividad local en un único nodo |
| Cluster-level coordination | Coordinación a nivel de clúster: Hacer que la red de los pods funcione a través de múltiples nodos |
NotReadyEl entorno de pruebas comenzón con un clúster con dos nodos: worker-01 y worker-02. Ambos unidos al clúster a través del proceso TLS.
Estado del nodo:
kubectl get nodes
Estado NotReady, vamos a diagnosticar el problema:
kubectl describe node worker-01
En la sección Conditions, deberías ver algo como:
container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
Kubelet está en ejecución. El nodo está registrado. Sin embargo, el entorno de ejecución de contenedores (containerd) indica que la red no está lista porque no hay ninguna configuración de CNI.
CNI (Container Network Interface) es una especificación que define cómo los runtimes de contenedores configuran la red para los contenedores.
Ya te encontraste con CNI en la lección de containerd, donde instalaste los plugins CNI de referencia en el worker node.
Pero tener los plugins instalados no es suficiente. containerd también necesita saber qué plugins CNI utilizar y cómo configurarlos. Esto se hace a través de los archivos de configuración CNI almacenados en /etc/cni/net.d/.
Así es como containerd emplea CNI cuando se inicia un pod:
/etc/cni/net.d/En otras palabras, un "plugin" CNI no es un objeto de Kubernetes ni un daemon de ejecución continua. Es sencillamente un ejecutable (por convención en /opt/cni/bin/) al que el runtime llama pasándole una configuración en JSON y unas pocas variables de entorno que describen la operación.
El runtime invoca a CNI en el nodo donde se está creando el pod, y el plugin configura la red para dicho pod en ese nodo.
En este momento, el paso 3 falla porque /etc/cni/net.d/ está vacío. containerd reporta NetworkPluginNotReady a kubelet, el cual a su vez reporta el nodo como NotReady.
Para que el nodo pase a Ready, necesitamos configurar una CNI.
Empieza con el complemento bridge para mostrar cómo funciona la red de pods a nivel de nodo. Permite que los pods de un mismo nodo se comuniquen a través de un puente compartido.
El complemento bridge crea un puente de red virtual en el nodo. Conecta cada pod a ese puente mediante un par veth: un extremo permanece en el espacio de nombres de red del pod, mientras que el otro se conecta al puente.
Crea la configuración del puente `/etc/cni/net.d/10-bridge.conf`:
{
"cniVersion": "1.0.0",
"name": "bridge",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.244.0.0/24",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
| Campo | Propósito |
|---|---|
type: bridge |
Usa el binario del plugin CNI bridge desde /opt/cni/bin/ |
bridge: cni0 |
Nombre del dispositivo puente (bridge) de Linux a crear |
isGateway: true |
Asigna una IP al puente para que pueda actúar como puerta de enlace (gateway) para los pods |
ipMasq: true |
Habilita el enmascaramiento IP (SNAT) para que el tráfico que sale de la subred del pod alcance la red del nodo |
ipam.type: host-local |
Utiliza el plugin IPAM host-local para gestionar direcciones IP desde un pool local |
ipam.subnet: 10.244.0.0/24 |
La subred desde donde asignar IPs a los pods en este nodo |
ipam.routes |
Añade una ruta por defecto dentro de cada pod para que todo el tráfico no local pase a través de la puerta de enlace del puente (el puente obtiene una IP porque isGateway es true) |
nota: Cada nodo necesita su propia subred para que las direcciones IP de los pods sean únicas en todo el clúster. worker-1 utiliza
10.244.0.0/24, y más adelante configurarás worker-2 con10.244.1.0/24.
Se necesita también una configuración para loopbacK, para que puedan comunicarse con ellos mismos:
/etc/cni/net.d/99-loopback.conf:
{
"cniVersion": "1.0.0",
"name": "loopback",
"type": "loopback"
}
Containerd detecta automáticamente los cambios en la configuración de CNI. En unos segundos, el nodo debería estar Ready.
kubectl wait --for=condition=Ready node worker-1
kubectl get nodes
Con las CNI configuradas, los pods pueden comunicarse entre sí en el mismo nodo. Para verificarlo, crea dos pods en el mismo nodo y comprueba que pueden comunicarse entre sí.
podinfo-worker-1: ejecuta la aplicación web habitual de podinfoclient-worker-2: es un pequeño cliente basado en Curl que se comunicará con podinfo-worker-1 a través de la red de podskubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: podinfo-worker-1
spec:
nodeName: worker-1
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
---
apiVersion: v1
kind: Pod
metadata:
name: client-worker-1
spec:
nodeName: worker-1
containers:
- name: curl
image: ghcr.io/stefanprodan/podinfo:latest
command: ["sh", "-c", "sleep infinity"]
EOF
Nota: Con esta configuración hacemos el bypass de scheduler, ya que le indicamos explícitamente en qué nodo queremos que se ejecute el pod.
Nota: Ahora cada pod dispone de su propio espacio de nombres de red y su propia dirección IP, por lo que esta prueba pone a prueba la configuración de CNI que acabas de crear, en lugar de utilizar la red del nodo.
kubectl wait --for=condition=Ready pod podinfo-worker-1 client-worker-1
kubectl get pods -o wide
Ambos Pods deben tener direcciones IP de la subred 10.244.0.0/24. Comprueba que puedan comunicarse:
PODINFO_IP=$(kubectl get pod podinfo-worker-1 -o jsonpath='{.status.podIP}')
kubectl exec client-worker-1 -- curl -fsS "http://${PODINFO_IP}:9898/version"
Inspeccionamos la tabla de enrutamiento
ip route show
Deberías de ver algo como:
default via 172.16.0.1 dev eth0
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
172.16.0.0/24 dev eth0 proto kernel scope link src 172.16.0.3
Esto nos indica que worker-1 sabe que se puede acceder directamente a toda la subred 10.244.0.0/24 a través del puente cni0.
Por lo tanto, cuando client-worker-1 envía tráfico a podinfo-worker-1, el paquete permanece en el mismo nodo:
Esta es una forma de implementar el modelo de red de Kubernetes en un único nodo.
El worker-2 sigue estando con el estado not Ready porque aún no se ha configurado una CNI en ese nodo.
Configúralo con la misma configuración de puente + bucle de retorno, pero utilizando una subred diferente (10.244.1.0/24):
/etc/cni/net.d/10-bridge.conf:
{
"cniVersion": "1.0.0",
"name": "bridge",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.244.1.0/24",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
/etc/cni/net.d/99-loopback.conf
{
"cniVersion": "1.0.0",
"name": "lo",
"type": "loopback"
}
kubectl wait --for=condition=Ready node worker-2
kubectl get nodes
Ahora ambos nodos deberían estar Ready.
Crea un pod en worker-2 y comprueba que puede comunicarse con el pod en worker-1.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: podinfo-worker-2
spec:
nodeName: worker-2
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
EOF
kubectl wait --for=condition=Ready pod podinfo-worker-2
kubectl get pods -o wide
Fíjese en las direcciones IP: client-worker-1 y podinfo-worker-1 tienen IP de 10.244.0.0/24, mientras que podinfo-worker-2 tiene una de 10.244.1.0/24.
Ahora ejecute la comprobación de conectividad entre nodos desde client-worker-1:
PODINFO_IP=$(kubectl get pod podinfo-worker-2 -o jsonpath='{.status.podIP}')
kubectl exec client-worker-1 -- curl -fsS --max-time 5 "http://${PODINFO_IP}:9898/version"
Nota: Esto fallará (tiempo de espera agotado después de 5 segundos). El tráfico entre nodos aún no funciona.
Es neceario cambiar a worker-1, vuelva inspeccionar la table de enrutamiento:
ip route show
Seguirás viendo una ruta conectada para 10.244.0.0/24 via cni0, pero no hay ninguna ruta para 10.244.1.0/24.
Esa es la diferencia clave con respecto al caso del mismo nodo. worker-1 sabe cómo llegar a los Pods en su propio puente, pero no tiene idea de dónde enviar el tráfico para la subred del pod que reside en worker-2.
Cada nodo tiene su propio cni0 puente con su propia subred:
| Nodo | Puente | Subred |
|---|---|---|
| worker-1 | cni0 | 10.244.0.0/24 |
| worker-2 | cni0 | 10.244.1.0/24 |
Cuando client-worker-1 (10.244.0.x) intenta llegar a podinfo-worker-2 (10.244.1.x), el paquete sale del Pod, llega al cni0 puente en worker-1, y se reenvía a la puerta de enlace predeterminada del nodo.
Pero el nodo no sabe cómo enrutar 10.244.1.0/24 el tráfico. Esa subred solo existe en worker-2 el puente de , y nadie ha dicho worker-1 cómo llegar allí.
El complemento CNI del puente no tiene conocimiento de otros nodos ni de sus subredes. Es necesario que otro componente se encargue de ello.
Antes de buscar la solucióna adecuada, vamos a cercionarnos de que el problema es realmente el enrutamiento. Podemos demostrarlo añadiendo rutas manualmente.
En worker-1:
ip -4 addr show dev eth0
En worker-2:
ip -4 addr show dev eth0
Deberias de obtener las direcciones de las tarjetas de red, por ejemplo:
| Nodo | IP |
|---|---|
| worker-1 | 172.16.0.3 |
| worker-2 | 172.16.0.4 |
Ahora en worker-1, añade una ruta para que sepa cómo llegar a la subred de worker-2:
sudo ip route add 10.244.1.0/24 via 172.16.0.4
ip route show
Salida esperada:
default via 172.16.0.1 dev eth0
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 172.16.0.4 dev eth0
172.16.0.0/24 dev eth0 proto kernel scope link src 172.16.0.3
En worker-2, haz lo mismo para la subred de worker-1:
sudo ip route add 10.244.0.0/24 via 172.16.0.3
ip route show
Salida esperada:
default via 172.16.0.1 dev eth0
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1
10.244.0.0/24 via 172.16.0.3 dev eth0
172.16.0.0/24 dev eth0 proto kernel scope link src 172.16.0.4
Ahora vuelve a ejecutar la prueba de conectividad entre nodos desde control-plane:
PODINFO_IP=$(kubectl get pod podinfo-worker-2 -o jsonpath='{.status.podIP}')
kubectl exec client-worker-1 -- curl -fsS --max-time 5 "http://${PODINFO_IP}:9898/version"
Esta vez debería funcionar. El tráfico entre nodos ahora puede fluir porque cada nodo sabe cómo llegar a la subred del otro. Pero este método no es escalable, por lo tanto ncesitamos buscar la solucióna adecuada.
Ya hemos comprobado que con enrutamiento manual el tráfico funciona correctamente, ahora vamos a sustituir la configuración manuel por Flannel, que se encarga de ello automáticamente. Para ello, vamos a empezar de 0, eliminando todas las configuraciones de red anteriores y pods de prueba:
kubectl delete pod --all
for pod in podinfo-worker-1 client-worker-1 podinfo-worker-2; do
kubectl wait --for=delete pod $pod
done
En worker-1:
Eliminar la ruta manual:
sudo ip route del 10.244.1.0/24 via 172.16.0.4
Eliminar los archivos de configuración de CNI:
sudo rm -f /etc/cni/net.d/10-bridge.conf /etc/cni/net.d/99-loopback.conf
Eliminar la configuración de red e interfaz:
sudo ip link delete cni0 || true
sudo rm -rf /var/lib/cni/networks/bridge /var/lib/cni/networks/cbr0
En worker-2:
Eliminar la ruta manual:
sudo ip route del 10.244.1.0/24 via 172.16.0.3
Eliminar los archivos de configuración de CNI:
sudo rm -f /etc/cni/net.d/10-bridge.conf /etc/cni/net.d/99-loopback.conf
Eliminar la configuración de red e interfaz:
sudo ip link delete cni0 || true
sudo rm -rf /var/lib/cni/networks/bridge /var/lib/cni/networks/cbr0
Un complemento de red es la pieza que faltaba para que la red del clúster funcione entre todos los nodos.
A diferencia de la configuración CNI del puente que se crea manualmente en cada nodo, un complemento de red añade la coordinación a nivel de clúster que la configuración CNI a nivel de nodo por sí sola no proporciona. Coordina la asignación de subredes, el enrutamiento y la configuración de túneles en todo el clúster.
La forma en que lo logra depende completamente de la forma de implementación. Algunas distribuyen rutas directamente, por ejemplo con BGP, automatizando esencialmente lo que se hacía manualmente con ip route add. Otras utilizan las redes superpuestas como VXLAN, que funciona incluso cuando la red subyacente no permite el enrutamiento directo entre subredes de pods, lo que hace mas portátiles en diferentes infraestructuras.
Nota: Una red superpuesta crea una red virtual sobre la red física existente. Cuando un Pod
worker-1envía un paquete a otro Podworker-2, la red superpuesta
encapsula el paquete (lo envuelve en otro paquete) y lo envía a través de la red regular de nodo a nodo.
El nodo receptor lo desempaqueta y lo entrega al Pod de destino. De esta forma, los Pods pueden comunicarse como si estuvieran en la misma red, aunque se encuentren en máquinas diferentes.
VXLAN es un protocolo de encapsulación común utilizado por complementos de red como Flannel.
En muchos clústeres modernos, esa lógica de coordinación suele ejecutarse dentro de Kubernetes (por ejemplo, como DaemonSets).
Aquí lo harás de la manera difícil y lo ejecutarás como un servicio systemd en cada trabajador .
Los complementos de red suelen:
Implementar o gestionar la configuración CNI en cada nodo (sustituyendo cualquier configuración manual).
Configura una red superpuesta o configura rutas para que los Pods en diferentes nodos puedan comunicarse.
Administre IPAM (Administración de direcciones IP) en todo el clúster para garantizar que las direcciones IP de los pods sean únicas.
Nota: A día de hoy es raro encontrarlo en un entorno de producción. Pero es un buen complemento para entender como funcionan los complementos de red.
Flannel es un complemento de red simple y popular para Kubernetes que proporciona una red superpuesta para la comunicación entre pods.
Flannel al igual que muchas otros complementos de red consta de dos componentes:
En esta configuración flannelfunciona de la siguiente manera:
La división es la misma que has visto a lo largo de esta lección:
En la lección sobre kube-controller-manager , el administrador de controladores se configuró con las siguientes banderas:
--cluster-cidr=10.244.0.0/16--allocate-node-cidrs=truePuedes comprobarlo ahora:
kubectl get node worker-1 -o jsonpath='{.spec.podCIDR}{"\n"}'
kubectl get node worker-2 -o jsonpath='{.spec.podCIDR}{"\n"}'
Salida esperada:
worker-1: 10.244.1.0/24worker-2: 10.244.2.0/24Nota: kube-controller-manager utiliza /24subredes de nodos por defecto en configuraciones como esta.
Puedes cambiar el tamaño de la subred por nodo con el--node-cidr-mask-sizeindicador.
Estos metadatos no configuran la red de pods por sí solos. La configuración manual del puente funcionó anteriormente porque usted escribió la subred directamente en la configuración CNI local.
Lo que cambia aquí es que Flannel puede leer los CIDR de los Pods que Kubernetes asignó a cada nodo, usarlos para configurar la asignación de IP de los Pods y establecer el enrutamiento entre los nodos en consecuencia.
Flannel utiliza la API de Kubernetes para coordinar la asignación de subredes a los nodos, por lo que necesita acceso al servidor de la API. El proceso de configuración es el mismo que para los componentes del plano de control en lecciones anteriores. La diferencia radica en que deberá copiar el archivo kubeconfig a cada nodo trabajador.
Flannel obtiene su propio certificado e identidad ( system:flannel) para que su acceso a la API pueda limitarse únicamente a los permisos que necesita.
Generar un certificado y una clave para Flannel:
(
cd /etc/kubernetes/pki
sudo openssl genrsa -out flannel.key 2048
sudo openssl req -new -key flannel.key -out flannel.csr -subj "/CN=system:flannel"
sudo openssl x509 -req -in flannel.csr -out flannel.crt \
-CA ca.crt -CAkey ca.key \
-days 365
)
Crea un archivo kubeconfig para Flannel:
sudo kubectl config set-cluster default \
--kubeconfig=/etc/kubernetes/flannel.conf \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true \
--server=https://control-plane:6443
sudo kubectl config set-credentials default \
--kubeconfig=/etc/kubernetes/flannel.conf \
--client-certificate=/etc/kubernetes/pki/flannel.crt \
--client-key=/etc/kubernetes/pki/flannel.key \
--embed-certs=true
sudo kubectl config set-context default \
--kubeconfig=/etc/kubernetes/flannel.conf \
--cluster=default \
--user=default
sudo kubectl config use-context default \
--kubeconfig=/etc/kubernetes/flannel.conf
Configurar RBAC:
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: system:flannel
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:flannel
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:flannel
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: system:flannel
EOF
Esto prepara las piezas del lado del grupo que Flannel necesita:
worker-1Descargar e instalar Flannel:
FLANNEL_VERSION=v0.27.2
FLANNEL_PLUGIN_VERSION=v1.7.1-flannel2
curl -fsSLO "https://github.com/flannel-io/flannel/releases/download/${FLANNEL_VERSION?}/flannel-${FLANNEL_VERSION?}-linux-amd64.tar.gz"
curl -fsSLO "https://github.com/flannel-io/cni-plugin/releases/download/${FLANNEL_PLUGIN_VERSION?}/cni-plugin-flannel-linux-amd64-${FLANNEL_PLUGIN_VERSION?}.tgz"
tar xzvof "flannel-${FLANNEL_VERSION?}-linux-amd64.tar.gz"
tar xzvof "cni-plugin-flannel-linux-amd64-${FLANNEL_PLUGIN_VERSION?}.tgz"
sudo install -m 755 flanneld /usr/local/bin
sudo install -m 755 flannel-amd64 /opt/cni/bin/flannel
Descargar el fichero `systemd unit:
sudo wget -O /etc/systemd/system/flanneld.service https://labs.iximiuz.com/content/files/courses/kubernetes-the-very-hard-way-0cbfd997/04-cluster/02-network/__static__/flanneld.service?v=1774217657
| Bandera / Configuración | Objetivo |
|---|---|
| --kube-subnet-mgr | Lee las asignaciones de subredes de pods desde la API de Kubernetes (a través de Node .spec.podCIDR) en lugar de una instancia local de etcd. |
| --kube-api-url | El punto final del servidor API al que conectarse |
| --kubeconfig-file | Ruta al archivo kubeconfig con el certificado de cliente de Flannel. |
| --ip-masq | Configure el enmascaramiento de IP (SNAT) para el tráfico que sale de la red del pod del clúster. |
| --net-config-path | Ruta al archivo de configuración de red ( net-conf.json) |
| NODE_NAME=%H | Indica a Flannel en qué nodo se está ejecutando. Cuando Flannel se ejecuta como un Pod, obtiene esta información de la API descendente de Kubernetes. Como servicio systemd, %H(el nombre de host) la proporciona en su lugar. |
Configuración para que flannel use el rango conrrecto:
mkdir -p /etc/flannel
/etc/flannel/net-conf.json:
{
"Network": "10.244.0.0/16",
"EnableNFTables": false,
"Backend": {
"Type": "vxlan"
}
}
Nota: EnableNFTablesestá configurado false porque este entorno utiliza iptables. En los sistemas más recientes, Flannel utiliza por defecto nftables, lo que provocaría un conflicto.
Configuración del plugin Flannel CNI:
/etc/cni/net.d/10-flannel.conflist:
{
"name": "cbr0",
"cniVersion": "1.0.0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
| Campo | Objetivo |
|---|---|
| name: cbr0 | Nombre de red lógico, utilizado por CNI para el seguimiento del estado de IPAM. |
| type: flannel | El complemento Flannel CNI. Lee la asignación de subred del nodo desde flanneld y luego delega a un complemento puente en segundo plano. |
| delegate.hairpinMode | Permite que un Pod se comunique consigo mismo a través de su propia IP de servicio. |
| delegate.isDefaultGateway | Convierte el puente en la puerta de enlace predeterminada para los Pods (mismo rol que isGatewayen la configuración manual del puente). |
| type: portmap | Permite hostPortasignaciones para contenedores |
Internamente, el complemento Flannel CNI delega en el mismo complemento de puente que configuraste manualmente con anterioridad. La diferencia radica en que Flannel proporciona la subred automáticamente en función de la asignación CIDR del Pod del nodo.
Ya no necesitas una configuración aparte 99-loopback.conf. Al usar una lista de configuración, containerd configura automáticamente la interfaz de bucle invertido en el espacio de nombres de red del Pod.
Copia del archivo kubeconfig al worker:
sudo scp control-plane:/etc/kubernetes/flannel.conf /etc/kubernetes/flannel.conf
Reinicie el demonio systemd e inicie el servicio flanneld:
sudo systemctl daemon-reload
sudo systemctl enable --now flanneld
Recrear los Pods de prueba para la conectividad en el mismo nodo:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: podinfo-worker-1
spec:
nodeName: worker-1
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
---
apiVersion: v1
kind: Pod
metadata:
name: client-worker-1
spec:
nodeName: worker-1
containers:
- name: curl
image: ghcr.io/stefanprodan/podinfo:latest
command: ["sh", "-c", "sleep infinity"]
EOF
kubectl wait --for=condition=Ready pod podinfo-worker-1 client-worker-1
kubectl get pods -o wide
Verifica que puedan comunicarse:
PODINFO_IP=$(kubectl get pod podinfo-worker-1 -o jsonpath='{.status.podIP}')
kubectl exec client-worker-1 -- curl -fsS "http://${PODINFO_IP}:9898/version"
En worker-1:
ip route show
Salida esperada:
default via 172.16.0.1 dev eth0
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
172.16.0.0/24 dev eth0 proto kernel scope link src 172.16.0.3
Flannel recreó la cni0 interfaz utilizando el mismo complemento de puente que usaste anteriormente.
Todavía no hay interfaz Flannel porque solo hay un nodo de trabajo. La interfaz VXLAN (flannel) existe para tunelizar el tráfico de pods entre nodos: sin un segundo nodo, no hay a dónde tunelizar.
worker-2Repita los mismos pasos que realizó en worker-1:
Nota: Todos los archivos de configuración son idénticos en todos los trabajadores.
A diferencia de la configuración manual del puente, donde cada nodo necesitaba una subred diferente en su configuración CNI, Flannel lee automáticamente el CIDR del Pod asignado al nodo desde Kubernetes.
Recrear el Pod de prueba entre nodos en worker-2:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: podinfo-worker-2
spec:
nodeName: worker-2
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
EOF
kubectl wait --for=condition=Ready pod podinfo-worker-2
kubectl get pods -o wide
Comprobación de conectividad entre nodos:
PODINFO_IP=$(kubectl get pod podinfo-worker-2 -o jsonpath='{.status.podIP}')
kubectl exec client-worker-1 -- curl -fsS "http://${PODINFO_IP}:9898/version"
Esta vez funciona. La misma comprobación de conectividad que antes daba error ahora devuelve una respuesta.
El tráfico entre nodos también sigue funcionando, pero ahora la red a nivel de nodo se gestiona mediante la configuración CNI de Flannel en lugar del manual 10-bridge.conf que creaste anteriormente.
Flannel establece un túnel VXLAN entre los nodos: cuando client-worker-1 envía un paquete a 10.244.x.x en worker-2, Flannel lo encapsula y lo reenvía a través de la red del nodo. worker-2El agente Flannel de desencapsula el paquete y lo entrega al Pod de destino.
En worker-1:
ip route show
Deberías seguir viendo la subred del pod local en cni0, pero ahora también deberías ver una ruta que 10.244.1.0/24 apunta a flannel.1.
Eso nos da una visión completa:
10.244.0.0/24 se mantiene local en cni010.244.1.0/24 se envía a través de flannel.1Conclusiones clave:
El modelo de red de Kubernetes es un contrato: cada Pod obtiene su propia dirección IP, los Pods pueden comunicarse directamente sin NAT, y un Pod ve la misma IP que otros Pods usan para llegar a él. Kubernetes delega la implementación a otras capas.
CNI (Interfaz de red de contenedores) es un mecanismo a nivel de nodo para configurar la red de contenedores. Los entornos de ejecución de contenedores como containerd buscan la configuración de CNI en /etc/cni/net.d/e invocan los binarios de complemento correspondientes desde/opt/cni/bin/.
El complemento Bridge CNI crea un puente virtual en cada nodo, asignando direcciones IP a los Pods y habilitando la comunicación local entre Pods en ese nodo. Sin embargo, no tiene conocimiento de otros nodos ni de sus subredes.
Los complementos de red como Flannel añaden la coordinación a nivel de clúster necesaria para la interconexión de clústeres multinodo mediante la configuración de superposiciones (como VXLAN) o la distribución de rutas entre nodos. En esta lección, Flannel se ejecuta como un servicio systemd a nivel de host y funciona junto con el complemento Flannel CNI.
Sin un mecanismo que coordine el enrutamiento entre nodos , los Pods en diferentes nodos no pueden comunicarse entre sí, aunque ambos nodos estén conectados Readyy los Pods estén conectados Running. Las rutas estáticas manuales pueden funcionar como prueba de concepto, pero un complemento de red automatiza esto para todo el clúster.
Con la red configurada, los Pods ahora pueden comunicarse libremente en todo el clúster. Sin embargo, las direcciones IP de los Pods son efímeras: cada vez que se recrea un Pod, se le asigna una nueva dirección IP.
Las próximas lecciones abordarán este tema con kube-proxy y CoreDNS , que proporcionan abstracciones de red estables sobre la red de pods que acabas de crear.
Kube-proxy es un componente esencial de red que se ejecuta en cada nodo de un clúster de Kubernetes, gestionando las reglas de red para permitir la comunicación entre servicios y pods
En la lección anterior se configuró la red del clúster con Frannel. Ahora los pods pueden comunicarse entre sí idependientemente del nodo en el que estén. Sin embargo, las direcciones IP de los pods son efímeras: cada vez que se recrea un Pod, se le asigna una nueva dirección IP. Esto puede ser un problema para las aplicaciones que necesitan una dirección IP estable.
| Pod | IP | Node |
|---|---|---|
| backend-abc12 | 10.244.0.2 | worker-1 |
| backend-def34 | 10.244.1.2 | worker-2 |
| backend-ghi56 | 10.244.0.3 | worker-1 |
¿Qué IP debería usar el frontend? Las tres son direcciones válidas. ¿Cómo distribuye la carga el frontend entre ellas? ¿Qué sucede cuando un Pod falla y se inicia uno nuevo con una IP diferente?
Evidentemente, codificar las direcciones IP de los pods directamente en el código no es la solución.
Un Servicio es un recurso de kubernetes que proporciona una identidad de red estable para un conjunto de pods.
Cuando creas un servicio kubernetes le asigna una ClusterIP: una dirección IP virtual de un rango dedicado que nunca cambia durante la vida útil del servicio.
| Concepto | Dirección | Vida útil | Ejemplo |
|---|---|---|---|
| Pod IP | From the Pod CIDR | Tied to the Pod (ephemeral) | 10.244.0.2 |
| ClusterIP | From the service CIDR | Tied to the Service (stable) | 10.96.0.10 |
Un servicio utiliza un selector de etiquetas para encontrar sus pods objetivo. El control-plane de Kubernetes realiza un seguimiento continuo de qué pods coinciden con el selector y mantiene una lista de sus IP actúales en objetos llamados EndpointSlices .
Esto significa:
ClusterIP apunta a una de las direcciones IP de los Pods (que puede cambiar en cualquier momento).El entorno de pruebas inició un clúster con dos nodos de trabajo, Flannel instalado y ambos nodos Ready. La comunicación entre pods funciona, pero aún no hay ningún kube-proxy en ejecución.
Antes de instalar kube-proxy, probemos qué sucede sin él.
En `control-plane:
Crea un despliegue de con tres réplicas de podinfo:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 3
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
spec:
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
EOF
kubectl wait --for=condition=Ready pod -l app=podinfo
kubectl get pods -o wide
Observe que cada Pod tiene su propia dirección IP.
Ahora crea un servicio que tenga como objetivo estos Pods:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: podinfo
spec:
selector:
app: podinfo
ports:
- port: 80
targetPort: 9898
EOF
Comprueba el servicio:
kubectl get svc podinfo
El servicio existe y tiene asignada una ClusterIP . Kubernetes también creó objetos EndpointSlice que rastrean las IP de los Pods que coinciden con el selector del servicio:
kubectl get endpointslices -l kubernetes.io/service-name=podinfo
Deberías ver las direcciones IP de los tres Pods de podinfo listadas en EndpointSlice.
Todo parece correcto.
Intente acceder al servicio desde dentro del clúster:
CLUSTER_IP=$(kubectl get svc podinfo -o jsonpath='{.spec.clusterIP}')
kubectl exec deploy/podinfo -- curl -fsS --max-time 5 "http://${CLUSTER_IP}:80"
Nota: Esto fallará (tiempo de espera agotado después de 5 segundos). La IP del clúster es virtual y aún no tiene reglas de respaldo.
La ClusterIP es una dirección virtual que solo tiene sentido si existen reglas de red que enruten el tráfico desde esa IP a las IP reales de los Pods.
En este momento, no existen tales reglas. El paquete llega a la pila de red del nodo, nadie sabe adónde enviarlo y se descarta.
Kube-proxy es un componente de red que se ejecuta en cada nodo e implementa la abtracción de servicio programando las reglas de filtrado de paquetes del nodo.
Sigue el mismo patrón de controlador que has visto a lo largo de este curso:
El modo predeterminado de kube-proxy utiliza iptables para implementar estas reglas. Cuando un paquete llega a un nodo destinado a una ClusterIP, iptables lo identifica y reescribe la dirección de destino a una de las IP de Pod en la lista EndpointSlices. Esto ocurre completamente en la pila de red del kernel, sin la intervención de un proxy en el espacio de usuario.
| Modo | Mecanismo | Notas |
|---|---|---|
| iptables | Reglas de iptables en Linux | Predeterminado. Confiable, bien comprendido. Puede ralentizarse con miles de servicios. |
| ipvs | Servidor virtual IP (IPVS) de Linux | Mejor rendimiento a gran escala. Requiere módulos del kernel de IPVS. |
| nftables | Reglas de nftables de Linux | Nueva alternativa a iptables. Disponible desde Kubernetes 1.31. |
Los tres modos logran el mismo resultado: traducir el tráfico ClusterIP a tráfico Pod IP. Se diferencian en sus características de rendimiento y requisitos del kernel.
Esta lección utiliza el modo predeterminado de iptables.
Al igual que cualquier otro componente que se comunica con el servidor API, kube-proxy necesita autenticarse con dicho servidor.
Generar un certificado y una clave para kube-proxy:
(
cd /etc/kubernetes/pki
sudo openssl genrsa -out kube-proxy.key 2048
sudo openssl req -new -key kube-proxy.key -out kube-proxy.csr -subj "/CN=system:kube-proxy"
sudo openssl x509 -req -in kube-proxy.csr -out kube-proxy.crt \
-CA ca.crt -CAkey ca.key \
-days 365
)
Crea un archivo kubeconfig para kube-proxy:
sudo kubectl config set-cluster default \
--kubeconfig=/etc/kubernetes/kube-proxy.conf \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true \
--server=https://control-plane:6443
sudo kubectl config set-credentials default \
--kubeconfig=/etc/kubernetes/kube-proxy.conf \
--client-certificate=/etc/kubernetes/pki/kube-proxy.crt \
--client-key=/etc/kubernetes/pki/kube-proxy.key \
--embed-certs=true
sudo kubectl config set-context default \
--kubeconfig=/etc/kubernetes/kube-proxy.conf \
--cluster=default \
--user=default
sudo kubectl config use-context default \
--kubeconfig=/etc/kubernetes/kube-proxy.conf
KUBE_VERSION=v1.34.0
curl -fsSLO "https://dl.k8s.io/${KUBE_VERSION?}/bin/linux/amd64/kube-proxy"
sudo install -m 755 kube-proxy /usr/local/bin
Copia el archivo kubeconfig del plano de control:
sudo scp control-plane:/etc/kubernetes/kube-proxy.conf /etc/kubernetes/kube-proxy.conf
Kube-proxy se configura mediante un archivo KubeProxyConfiguration como mínimo se necesita:
Cree el archivo de configuración de kube-proxy:
sudo mkdir -p /etc/kubernetes/kube-proxy
/etc/kubernetes/kube-proxy/config.yaml:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
clusterCIDR: 10.244.0.0/16
clientConnection:
kubeconfig: /etc/kubernetes/kube-proxy.conf
Descarga el archivo de unidad systemd para kube-proxy:
sudo wget -O /etc/systemd/system/kube-proxy.service https://labs.iximiuz.com/content/files/courses/kubernetes-the-very-hard-way-0cbfd997/04-cluster/03-kube-proxy/__static__/kube-proxy.service?v=1774217659
Inciar kube-proxy:
sudo systemctl daemon-reload
sudo systemctl enable --now kube-proxy
En el control-plane:
Ahora intenta acceder al servicio de nuevo:
CLUSTER_IP=$(kubectl get svc podinfo -o jsonpath='{.spec.clusterIP}')
kubectl exec deploy/podinfo -- curl -fsS "http://${CLUSTER_IP}:80"
Funciona. La misma ClusterIP que antes daba error por tiempo de espera ahora devuelve una respuesta de uno de los Pods de podinfo.
Ejecútalo unas cuantas veces más:
for i in $(seq 1 5); do
kubectl exec deploy/podinfo -- curl -fsS "http://${CLUSTER_IP}:80" | jq -r '.hostname'
done
Puedes inspeccionar las reglas de iptables que creó kube-proxy:
sudo iptables -t nat -L KUBE-SERVICES -n | grep podinfo
Esto muestra la cadena de reglas para el servicio podinfo. Para cada servicio, kube-proxy crea una cadena de reglas de iptables que:
Conclusiones clave:
Las direcciones IP de los pods son efímeras : cambian con cada reinicio o reprogramación del pod. Las aplicaciones necesitan puntos finales estables para comunicarse de forma fiable, que es lo que proporcionan los servicios.
Los servicios asignan una ClusterIP estable a partir del CIDR del servicio. Kubernetes rastrea qué Pods coinciden con el selector de un Servicio y mantiene un EndpointSlice de sus IPs actúales.
Las ClusterIP son virtuales : ninguna interfaz de red contiene la dirección. Sin algo que programe reglas de enrutamiento, el tráfico hacia una ClusterIP no llega a ninguna parte.
kube-proxy supervisa los servicios y los EndpointSlices, y luego programa reglas de iptables en cada nodo para que el tráfico ClusterIP se reescriba (DNAT) a una IP de Pod real, con balanceo de carga aleatorio entre backends saludables.
kube-proxy es opcional : algunos complementos de red como Cilium pueden reemplazar completamente a kube-proxy, implementando el enrutamiento de servicios a través de eBPF u otros mecanismos. Pero kube-proxy sigue siendo el estándar predeterminado, bien probado.
| Layer | Handles | Implemented by |
|---|---|---|
| Pod network | Pod-to-Pod communication using Pod IPs | CNI + Flannel |
| Service network | Stable endpoints with load balancing using ClusterIPs | kube-proxy (iptables) |
En este momento si quiero acceder a un servicio, primero debes buscar su ClusterIP:
kubectl get svc SVC_NAME -o jsonpath='{.spec.clusterIP}'
Luego, codifica directamente esa IP o pásala a tu aplicación. Esto es frágil por la misma razón que codificar directamente las IP de los Pods era frágil: crea un acoplamiento estrecho y falla cuando las cosas cambian.
Nota: ClusterIP no cambia por si solo. Pero el servicio podría recrearse, lo que conllevaría la obtención de una nueva dirección IP.
Lo que realmente quieres en contactar con un servicio por su nombre:
curl http://podinfo:80
Esto es descubrimiento de servicios a través de DNS, y es la forma en que la mayoría de las aplicaciones de Kubernetes se encuentran entre sí.
Kubernetes define un esquema de nomenclatura predecible para los servicios:
<service-name>.<namespace>.svc.cluster.local
Por ejemplo, el servicio podinfo en el namespace default se puede resolver como podinfo.default.svc.cluster.local.
Kubernetes también configura dominios de búsqueda en cada Pod /etc/resolv.conf para que puedas usar nombres más cortos dentro del mismo espacio de nombres:
| Name form | When it works |
|---|---|
| podinfo | Mismo espacio de nombres |
| podinfo.default | Cualquier espacio de nombres (calificado con el espacio de nombres) |
| podinfo.default.svc | Qualificador de servicio explícito |
| podinfo.default.svc.cluster.local | Nombre de dominio completo (FQDN) |
Las cuatro se resuelven en la misma ClusterIP. La forma abreviada (podinfo) es la más común en la práctica.
Para que DNS funcione en un clúster de Kubernetes se necesitan dos componentes:
/etc/resolv.conf para que apunte al servidor DNS del clúster.kubelet gestiona la primera parte mediante dos opciones de configuración:
| Setting | Purpose | Example value |
|---|---|---|
| clusterDNS | IP address of the DNS server that kubelet writes into each Pod's /etc/resolv.conf | 10.96.0.10 |
| clusterDomain | The base domain for the cluster | cluster.local |
Cuando kubelet inicia un Pod, escribe /etc/resolv.conf con una nameserver entrada que apunta a la clusterDNS dirección y search entradas derivadas de la clusterDomain.
Hay que ejecutar los siguientes pasos tanto en worker-1 como en worker-2:
/var/lib/kubelet/config.d/60-dns.conf:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
clusterDNS:
- 172.16.0.2
clusterDomain: cluster.local
Reiniciar kubelet:
sudo systemctl daemon-reload
sudo systemctl restart kubelet
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 2
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
spec:
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:latest
ports:
- containerPort: 9898
---
apiVersion: v1
kind: Service
metadata:
name: podinfo
spec:
selector:
app: podinfo
ports:
- port: 80
targetPort: 9898
---
apiVersion: v1
kind: Pod
metadata:
name: client
spec:
containers:
- name: curl
image: ghcr.io/stefanprodan/podinfo:latest
command: ["sh", "-c", "sleep infinity"]
EOF
kubectl wait --for=condition=Available deployment podinfo
kubectl wait --for=condition=Ready pod client
El servicio tiene una ClusterIP y kube-proxy tiene reglas de iptables programadas para él. El acceso desde el Pod cliente mediante IP funciona:
CLUSTER_IP=$(kubectl get svc podinfo -o jsonpath='{.spec.clusterIP}')
kubectl exec client -- curl -fsS "http://${CLUSTER_IP}:80"
Ahora intenta acceder a él por su nombre:
kubectl exec client -- curl -fsS --max-time 5 "http://podinfo:80"
Nota: Esto fallará porque aún no hay ningún servidor DNS en ejecución en la máquina del plano de control.
Verifique la configuración DNS del Pod del cliente:
kubectl exec client -- cat /etc/resolv.conf
Salida esperada:
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 172.16.0.2
options ndots:5
Nota: kubelet configuró correctamente el DNS del Pod: apunta a 172.16.0.2(a la máquina control-plane) con los dominios de búsqueda correctos.
El problema es que aún no hay ningún servidor DNS en funcionamiento en la máquina control-plane. La consulta DNS se envía, el paquete llega a la máquina control-plane, pero nada está escuchando en el puerto 53.
CoreDNS es un servidor DNS flexible y extensible escrito en Go. Es un proyecto graduado de la CNCF y el servidor DNS predeterminado del clúster en Kubernetes.
CoreDNS supervisa la API de Kubernetes para obtener información sobre servicios y puntos finales, y luego responde a las consultas DNS según el estado actúal del clúster. Cuando un Pod pregunta "¿cuál es la IP de podinfo.default.svc.cluster.local?", CoreDNS busca el servicio correspondiente y devuelve su ClusterIP.
Al igual que cualquier otro componente que se comunica con el servidor API, CoreDNS necesita autenticarse.
Generar un certificado y una clave para CoreDNS:
(
cd /etc/kubernetes/pki
sudo openssl genrsa -out coredns.key 2048
sudo openssl req -new -key coredns.key -out coredns.csr -subj "/CN=system:coredns"
sudo openssl x509 -req -in coredns.csr -out coredns.crt \
-CA ca.crt -CAkey ca.key \
-days 365
)
Crea un archivo kubeconfig para CoreDNS:
sudo kubectl config set-cluster default \
--kubeconfig=/etc/kubernetes/coredns.conf \
--certificate-authority=/etc/kubernetes/pki/ca.crt \
--embed-certs=true \
--server=https://127.0.0.1:6443
sudo kubectl config set-credentials default \
--kubeconfig=/etc/kubernetes/coredns.conf \
--client-certificate=/etc/kubernetes/pki/coredns.crt \
--client-key=/etc/kubernetes/pki/coredns.key \
--embed-certs=true
sudo kubectl config set-context default \
--kubeconfig=/etc/kubernetes/coredns.conf \
--cluster=default \
--user=default
sudo kubectl config use-context default \
--kubeconfig=/etc/kubernetes/coredns.conf
Otorgue a CoreDNS los permisos que necesita para leer el estado del clúster:
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: system:coredns
rules:
- apiGroups: [""]
resources: ["endpoints", "services", "pods", "namespaces"]
verbs: ["list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:coredns
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: system:coredns
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:coredns
EOF
Descarga e instala CoreDNS:
COREDNS_VERSION=1.12.2
curl -fsSLO "https://github.com/coredns/coredns/releases/download/v${COREDNS_VERSION}/coredns_${COREDNS_VERSION}_linux_amd64.tgz"
Extrae e instala el binario:
tar xzf coredns_${COREDNS_VERSION}_linux_amd64.tgz
sudo install -m 755 coredns /usr/local/bin
Cree un usuario de sistema dedicado para que CoreDNS se ejecute como:
sudo adduser \
--system \
--group \
--disabled-login \
--disabled-password \
--home /var/lib/coredns \
coredns
Ahora que el coredns usuario existe, dejemos que lea el archivo kubeconfig:
sudo chown coredns:coredns /etc/kubernetes/coredns.conf
CoreDNS se configura mediante un fichero llamado Corefile.
sudo mkdir -p /etc/coredns
Fichero de configuración: /etc/coredns/corefile
.:53 {
bind eth0
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
kubeconfig /etc/kubernetes/coredns.conf
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
| Directive | Purpose |
|---|---|
| bind eth0 | Only listen on the node's network interface (not all interfaces) |
| errors | Log errors to stdout |
| health | Expose a health check endpoint |
| ready | Expose a readiness endpoint at :8181/ready |
| kubernetes cluster.local | Enable the Kubernetes plugin: resolve svc.cluster.local names by watching the API |
| kubeconfig /etc/kubernetes/coredns.conf | Authenticate to the API server using a kubeconfig file (since CoreDNS runs outside the |
| forward . /etc/resolv.conf | Forward non-cluster queries (e.g., google.com) to the node's upstream DNS |
| cache 30 | Cache DNS responses for 30 seconds |
| loop | Detect and stop forwarding loops |
| reload | Automatically reload the Corefile when it changes |
| loadbalance | Randomize the order of A records in responses (round-robin) |
Download the systemd unit file for CoreDNS:
sudo wget -O /etc/systemd/system/coredns.service https://labs.iximiuz.com/content/files/courses/kubernetes-the-very-hard-way-0cbfd997/04-cluster/04-coredns/__static__/coredns.service?v=1774217660
Reinicie el demonio systemd e inicie el servicio CoreDNS:
sudo systemctl daemon-reload
sudo systemctl enable --now coredns
Ahora intenta acceder de nuevo al servicio podinfo por su nombre desde el pod del cliente:
kubectl exec client -- curl -fsS --max-time 5 "http://podinfo:80"
Funciona. El Pod del cliente se resolvió podinfo a la ClusterIP del Servicio a través de CoreDNS, y kube-proxy enrutó el tráfico a uno de los Pods de backend.
Prueba las otras formas del nombre:
# Namespace-qualified
kubectl exec client -- curl -fsS --max-time 5 "http://podinfo.default:80"
# With svc prefix
kubectl exec client -- curl -fsS --max-time 5 "http://podinfo.default.svc:80"
# Fully qualified domain name
kubectl exec client -- curl -fsS --max-time 5 "http://podinfo.default.svc.cluster.local:80"
Las cuatro formas se resuelven en la misma ClusterIP. Las formas más cortas funcionan debido a los dominios de búsqueda /etc/resolv.conf configurados en ese kubelet.
Esto es lo que sucede cuando se ejecuta el Pod del cliente curl http://podinfo:80:
/etc/resolv.conf y agrega el primer dominio de búsqueda: .default.svc.cluster.local172.16.0.2 (la control-planeIP de la máquina desde clusterDNS)Cada componente de red que instalaste en este módulo desempeñó un papel: Flannel para la red de pods, kube-proxy para el enrutamiento de servicios y CoreDNS para la resolución de nombres.
Conclusiones clave:
Las IP de los pods son efímeras, las IP de los clústeres son estables, pero los nombres son prácticos : DNS es la capa final que hace que la comunicación del servicio sea natural. Los pods del cliente se conectan a podinfoen lugar de 10.96.23.42
kubelet configura el DNS de los pods: la clusterDNSconfiguración clusterDomainle indica a kubelet qué escribir en el DNS de cada pod /etc/resolv.conf. En este curso, clusterDNSapunta directamente a la control-planeIP de la máquina donde CoreDNS está escuchando. En las distribuciones estándar, normalmente apunta a una IP de clúster de servicios como 10.96.0.10
CoreDNS supervisa la API de Kubernetes para obtener servicios y puntos finales, y luego responde a las consultas DNS con las direcciones IP de clúster correctas. En este curso, se ejecuta en la control-planemáquina como un servicio systemd, autenticándose en el servidor API mediante un archivo kubeconfig con certificados de cliente.
La convención de nomenclatura <service>.<namespace>.svc.<cluster-domain> es predecible y jerárquica. Los dominios de búsqueda en /etc/resolv.conf los Pods utilizan nombres cortos como podinfo dentro del mismo espacio de nombres.