Downward API
Un ConfigMap y un Secret sirven para lo que tú sabes de antemano. Pero hay datos que tu aplicación necesita y que nadie puede escribir en un manifiesto, porque no existen hasta que el Pod arranca: en qué nodo va a ejecutarse, qué IP le ha dado el CNI, cómo se llama la réplica concreta que está ejecutando ese código.
La Downward API es el mecanismo para bajar esos datos al contenedor. No es un objeto nuevo: es una variante más del valueFrom que ya usaste con el ConfigMap y el Secret.
En esta lección vas a montar la api de la tienda con dos réplicas y a darle tres cosas que solo el clúster conoce: su identidad, sus propios límites de recursos y sus etiquetas. Y al final vas a comprobar tú mismo cuál de esas vías se refresca en caliente y cuál no, que es la diferencia que más disgustos da. Trabaja desde la pestaña dev-machine.
Paso 1: La identidad del Pod
Crea deployment-api.yaml:
cat << 'EOF' > deployment-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/iximiuz/labs/nginx:alpine
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
EOF
El YAML, explicado con preguntas y respuestas
¿Por qué un Deployment y no un Pod suelto?
Porque con una sola réplica esto no se ve. Un Deployment reparte dos Pods con nombres distintos y, muy probablemente, en nodos distintos: es ahí donde POD_NAME deja de ser un adorno y empieza a responder a la pregunta de qué réplica ha escrito una línea de log.
¿Qué campos puedo pedir con fieldRef?
Los del propio Pod: metadata.name, metadata.namespace, metadata.uid, spec.nodeName, spec.serviceAccountName, status.podIP y status.hostIP. No es la lista completa del objeto, es la lista de lo que el kubelet sabe con certeza en el momento de crear el contenedor.
¿No podría escribir el nombre a mano y ahorrarme esto?
En un Pod suelto, sí. En un Deployment no existe: el nombre lo compone el ReplicaSet con un sufijo aleatorio, y no lo conoces hasta que el Pod existe. Ese es exactamente el hueco que la Downward API rellena.
Aplícalo y mira las dos réplicas:
kubectl apply -f deployment-api.yaml
kubectl get pods -l app=api -o wide
Ahora pregúntale a cada una quién es. Fíjate en las comillas simples: quien tiene que expandir $POD_NAME es la shell de dentro del contenedor, no la tuya.
for p in $(kubectl get pods -l app=api -o name); do
kubectl exec $p -- sh -c 'echo "$POD_NAME va en $NODE_NAME con IP $POD_IP"'
done
Paso 2: Sus propios límites
Ahora la parte que más veces salva una aplicación de un OOMKilled. Añade al contenedor un bloque resources y tres variables más. El contenedor completo queda así:
containers:
- name: api
image: ghcr.io/iximiuz/labs/nginx:alpine
resources:
requests:
memory: 128Mi
cpu: 250m
limits:
memory: 256Mi
cpu: 500m
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: MEMORIA_MAXIMA_MB
valueFrom:
resourceFieldRef:
containerName: api
resource: limits.memory
divisor: 1Mi
- name: MEMORIA_SOLICITADA_MB
valueFrom:
resourceFieldRef:
containerName: api
resource: requests.memory
divisor: 1Mi
- name: CPU_MAXIMA_MILI
valueFrom:
resourceFieldRef:
containerName: api
resource: limits.cpu
divisor: 1m
El YAML, explicado con preguntas y respuestas
¿Qué hace el divisor?
Decide en qué unidad llega el número. La Downward API entrega bytes y núcleos, y casi ningún runtime quiere eso: con divisor: 1Mi los 256Mi del límite llegan como 256, y con divisor: 1m los 500m de CPU llegan como 500. El resultado siempre se redondea hacia arriba al entero siguiente.
¿Para qué querría mi propio límite de memoria?
Para dimensionarse. Una JVM que no sabe cuánta memoria tiene reserva su heap según lo que ve del nodo, no según su cgroup, y acaba pidiendo más de lo que el limit le permite: el resultado es un OOMKilled que no se parece a un error de configuración. Lo mismo le pasa a un runtime de Node con --max-old-space-size. Leer el límite de aquí en vez de escribirlo a mano en dos sitios es lo que evita que el manifiesto y la aplicación se contradigan.
¿Y si el contenedor no declara limits?
Entonces no falla: te devuelve lo que el nodo puede dar. Es el espejismo de esta sección, porque el número llega, la aplicación lo cree y se dimensiona para una máquina entera que no es suya.
¿containerName es obligatorio?
En una variable de entorno no, se asume el contenedor que la declara. Pero escríbelo: en un Pod con sidecar te permite pedir los recursos de otro contenedor, y sin él nunca sabes cuál estás leyendo.
Aplícalo. Como cambia el template, el Deployment hace un rollout: los Pods anteriores desaparecen y se crean otros dos.
kubectl apply -f deployment-api.yaml
kubectl rollout status deploy/api
kubectl exec deploy/api -- sh -c 'echo "$MEMORIA_SOLICITADA_MB MB pedidos, $MEMORIA_MAXIMA_MB MB de techo, $CPU_MAXIMA_MILI milicores"'
Paso 3: Las etiquetas, por las dos vías
Faltan las etiquetas, y son un caso aparte: no caben en una variable de entorno tal cual, porque son un mapa que puede cambiar. Para eso existe la variante en volumen.
Vas a montar las etiquetas como fichero y, además, exponer una de ellas como variable. Sirven para comparar dentro de un momento.
Primero, añade la etiqueta version al template del Pod. Ojo a dónde: en template.metadata.labels, no en selector.matchLabels.
selector:
matchLabels:
app: api # el selector se queda como está
template:
metadata:
labels:
app: api
version: "1" # <- nueva
Después, una variable más en la lista de env:
- name: VERSION_ENV
valueFrom:
fieldRef:
fieldPath: metadata.labels['version']
Y el volumen, con su volumeMounts en el contenedor y su volumes a nivel de Pod, el patrón en dos mitades de la lección anterior:
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
readOnly: true
volumes:
- name: podinfo
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: annotations
fieldRef:
fieldPath: metadata.annotations
El YAML, explicado con preguntas y respuestas
¿Por qué las etiquetas van en volumen y el nombre no?
Por lo mismo que un ConfigMap se puede montar: el volumen puede refrescarse y una variable de entorno no. El nombre del Pod no cambia nunca mientras el Pod existe, así que una variable le vale. Las etiquetas y las anotaciones sí cambian en vida del Pod, y por eso tienen esta forma.
Entonces, ¿por qué también VERSION_ENV?
Para el experimento del paso siguiente. En un manifiesto real tendrías una de las dos, no las dos.
¿Qué pinta tiene el fichero labels?
Una línea por etiqueta, con el formato clave="valor". Verás también la pod-template-hash que pone el ReplicaSet, que no la escribiste tú.
¿Y si quiero solo una etiqueta concreta en el volumen?
Ahí sí puedes usar metadata.labels['version'] como fieldPath, y montarla como un fichero de una línea. metadata.labels a secas, el mapa entero, solo vale en volumen: en una variable de entorno la API lo rechaza.
Aplícalo. Es el tercer rollout de la lección, y no es casualidad: cada cambio de configuración del contenedor significa Pods nuevos.
kubectl apply -f deployment-api.yaml
kubectl rollout status deploy/api
kubectl exec deploy/api -- cat /etc/podinfo/labels
Paso 4: Cuál de las dos se entera
Aquí está el motivo real de la lección. Coge una de las réplicas y cámbiale la etiqueta version en caliente:
POD=$(kubectl get pods -l app=api -o jsonpath='{.items[0].metadata.name}')
kubectl label pod $POD version=2 --overwrite
El Pod sigue vivo: version no está en el selector del Deployment, así que el ReplicaSet no se da por aludido y nadie recrea nada. Ahora pregunta lo mismo por las dos vías:
kubectl exec $POD -- cat /etc/podinfo/labels
kubectl exec $POD -- sh -c 'echo $VERSION_ENV'
Puede que la primera vez el fichero todavía diga version="1". No está roto: el kubelet refresca estos volúmenes en su ciclo de sincronización, que suele resolverse en unos segundos pero puede estirarse hasta un minuto. Repite el cat hasta que cambie.
Cuando el fichero cambie, la variable seguirá diciendo 1. Y va a seguir diciéndolo hasta que el contenedor se recree, por mucho que la etiqueta ya sea otra.
Por qué esto importa más de lo que parece
Lo que acabas de comprobar con una etiqueta vale igual para los recursos. MEMORIA_MAXIMA_MB se resolvió al crear el contenedor, así que si el VPA redimensiona el Pod en caliente, tu aplicación sigue funcionando con el número que leyó al arrancar y ya no se corresponde con su cgroup.
La regla práctica es corta: lo que leas de una variable de entorno es lo que había al arrancar el contenedor. Si necesitas un dato que puede cambiar mientras el Pod está en marcha, o lo montas como fichero y lo relees, o asumes que hace falta un reinicio.
Resumen
- La Downward API baja al contenedor lo que solo el clúster sabe: identidad, nodo, IP y recursos. Es un
valueFrommás, no un objeto nuevo. fieldRefda campos del Pod;resourceFieldRefda sus requests y limits, condivisorpara elegir la unidad.- Sin
limits,resourceFieldRefdevuelve lo que puede dar el nodo. El número llega igual, y es mentira para tu contenedor. - Las etiquetas y anotaciones se montan como volumen, y ahí sí se refrescan. Las variables de entorno se congelan al crear el contenedor.
- Por eso una aplicación que se dimensiona sola lee su límite al arrancar, y por eso un redimensionado en caliente le pasa desapercibido.
- Previous lesson
- Challenge: registro privado
- Next lesson
- Almacenamiento persistente