Cerrábamos el módulo 2 con una lista de deudas y la primera de todas era esta: la URL de la API va incrustada dentro de la imagen de tienda-web. Cada vez que queremos desplegar la tienda en rutas-norte-pre en lugar de en rutas-norte-pro hay que construir una imagen distinta, con el mismo código y un solo carácter cambiado en un fichero de configuración. Eso rompe la promesa más básica del contenedor: construir una vez, desplegar en cualquier sitio. El objeto que lo arregla es el ConfigMap, un almacén de configuración no confidencial que vive en el clúster, se versiona en Git junto al resto de manifiestos y se inyecta en el contenedor en el momento del arranque. En esta lección construirás el ConfigMap de tienda-web con su nginx.conf real y lo montarás como un fichero dentro del contenedor, que es la forma de consumo más potente y la que da menos sorpresas. La inyección como variables de entorno la verás a fondo en Variables de Entorno; aquí la mencionaremos solo para situarla.
Contenido
- Por qué se separa la configuración del código
- Anatomía del objeto ConfigMap
- Las cuatro formas de crear un ConfigMap
- Las tres formas de consumirlo (y cuál veremos aquí)
- Montar un ConfigMap como volumen
- Seleccionar claves concretas con
itemsydefaultMode subPath: el uso y sus dos peligros- El caso central: el
nginx.confdetienda-web - Actualizar un ConfigMap y la propagación a los volúmenes
- ConfigMaps inmutables
- El límite de 1 MiB y qué hacer si se supera
- ConfigMaps por entorno en Rutas Norte
- Por qué se separa la configuración del código
La metodología de los doce factores (The Twelve-Factor App), escrita en 2011 para aplicaciones en la nube, dedica su tercer factor exactamente a este problema y su enunciado es una sola frase:
Guarda la configuración en el entorno. La configuración es todo aquello que puede variar entre despliegues; el código es lo que no varía.
La prueba práctica que propone es tajante y merece la pena aplicarla a Rutas Norte: ¿podrías hacer público el repositorio de código ahora mismo, sin filtrar ninguna credencial? Hoy la respuesta es no, porque la contraseña de postgres-reservas está escrita en un manifiesto del repositorio. Esa parte la salda la lección de Secrets. Pero hay una segunda prueba, más silenciosa: ¿es exactamente la misma imagen la que corre en rutas-norte-dev, en rutas-norte-pre y en rutas-norte-pro? Hoy tampoco.
Esto no es purismo. Tiene consecuencias muy concretas:
| Con la configuración dentro de la imagen | Con la configuración fuera de la imagen |
|---|---|
Una imagen por entorno: tienda-web:2.5.0-dev, -pre, -pro |
Una sola imagen tienda-web:2.5.0 para los tres entornos |
Lo que se prueba en pre no es lo que sale a pro |
Lo validado en pre es el artefacto exacto que se promociona |
| Cambiar un tiempo de espera exige reconstruir y volver a pasar el pipeline (minutos u horas) | Cambiar un tiempo de espera es editar un objeto del clúster (segundos) |
| La configuración de producción vive en el registro de imágenes, ilegible | La configuración vive en YAML, revisable en una pull request |
| Un rollback de configuración obliga a rebobinar la imagen | Un rollback de configuración es independiente del código |
Los dos casos concretos que arrastra Rutas Norte:
- La URL de la API en
tienda-web. La SPA necesita saber a qué dirección llamar:http://api-reservasdentro del clúster,https://api.rutasnorte.exampledesde el navegador del cliente. Hoy está incrustada en el JavaScript compilado dentro de la imagen. - El
nginx.confdetienda-web. La configuración del servidor —compresión, cabeceras de seguridad, la regla de fallback aindex.htmlque toda SPA necesita, el proxy haciaapi-reservas— está horneada en la imagen. Ajustar la caché de los ficheros estáticos requiere reconstruirla.
Ambos se resuelven hoy, en esta lección.
- Anatomía del objeto ConfigMap
Un ConfigMap es uno de los objetos más simples de Kubernetes. No tiene spec ni status: solo metadata y datos.
apiVersion: v1 # ConfigMap vive en el grupo core, sin prefijo
kind: ConfigMap
metadata:
name: tienda-web-config
namespace: rutas-norte-dev
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
data: # pares clave -> valor de TEXTO (UTF-8)
API_URL: "http://api-reservas:8080"
NIVEL_LOG: "debug"
nginx.conf: | # el valor puede ser un fichero entero
server {
listen 80;
}
binaryData: # pares clave -> valor BINARIO (base64)
favicon.ico: AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAA...Los puntos que definen el objeto:
apiVersion: v1: pertenece al grupo core, como Pod, Service o Namespace. No lleva grupo delante.- No tiene
spec. Un ConfigMap no describe un estado deseado que un controlador tenga que reconciliar; es un contenedor de datos pasivo. Por eso tampoco tienestatus. datacontiene texto válido en UTF-8. Las claves son nombres de fichero potenciales, así que deben limitarse a caracteres alfanuméricos,-,_y..binaryDatacontiene datos que no son texto, codificados en base64 al escribirlos en el manifiesto. Kubernetes los descodifica al montarlos. Sirve para un certificado en formato DER, un.ico, una fuente, un pequeño.tar.gzde plantillas.- Una misma clave no puede aparecer en
datay enbinaryData: elapplyfalla. - Tiene namespace. Un pod solo puede montar ConfigMaps de su propio namespace. No existe forma nativa de referenciar uno de otro namespace, y esa restricción es la que hace natural el patrón de "un ConfigMap por entorno" que veremos en el apartado 12.
Un detalle importante sobre las claves multilínea. El | de YAML es un bloque literal: conserva los saltos de línea tal cual. Es lo que quieres para un fichero de configuración. Su primo > (bloque plegado) convierte los saltos de línea simples en espacios, y arruinaría un nginx.conf. Para ficheros, usa siempre |.
- Las cuatro formas de crear un ConfigMap
3.1. Manifiesto YAML
Es la forma canónica y la única que se versiona bien en Git. Ya la has visto arriba. Se aplica como cualquier otro objeto:
3.2. --from-literal
Para valores cortos, sobre todo cuando estás explorando o generando el YAML con --dry-run.
kubectl create configmap tienda-web-config \
--from-literal=API_URL=http://api-reservas:8080 \
--from-literal=NIVEL_LOG=debug \
-n rutas-norte-devComprobamos qué ha creado exactamente:
apiVersion: v1
data:
API_URL: http://api-reservas:8080
NIVEL_LOG: debug
kind: ConfigMap
metadata:
creationTimestamp: "2026-08-05T09:14:22Z"
name: tienda-web-config
namespace: rutas-norte-dev
resourceVersion: "184213"
uid: 6f2b1c4a-3d9e-4a71-9f0c-2b8e5d417a63Fíjate en que no lleva ninguna etiqueta. Los objetos creados con kubectl create no siguen el esquema de etiquetado de Rutas Norte, y eso los deja fuera de todas las consultas con -l app.kubernetes.io/part-of=rutas-norte. Por eso el uso recomendado del comando imperativo es como generador de YAML, no como forma de crear objetos:
kubectl create configmap tienda-web-config \
--from-literal=API_URL=http://api-reservas:8080 \
--dry-run=client -o yaml > k8s/base/tienda-web/configmap.yamlDespués se editan las etiquetas a mano y se aplica el fichero. Es el mismo flujo --dry-run=client -o yaml del módulo 1, ahora aplicado a la configuración.
3.3. --from-file con un fichero
Aquí está el caso interesante. Tienes el nginx.conf real en tu repositorio y quieres meterlo en un ConfigMap sin copiarlo a mano dentro de un YAML (con el riesgo de estropear la indentación).
kubectl create configmap tienda-web-nginx \
--from-file=k8s/base/tienda-web/nginx.conf \
--dry-run=client -o yaml -n rutas-norte-devapiVersion: v1
data:
nginx.conf: |
server {
listen 80;
server_name www.rutasnorte.example;
...
}
kind: ConfigMap
metadata:
creationTimestamp: null
name: tienda-web-nginx
namespace: rutas-norte-devLa clave es el nombre base del fichero (nginx.conf), no la ruta completa. Si quieres que la clave se llame de otra forma, se antepone con un =:
kubectl create configmap tienda-web-nginx \
--from-file=default.conf=k8s/base/tienda-web/nginx-produccion.conf \
--dry-run=client -o yaml -n rutas-norte-devLa clave será default.conf aunque el fichero del disco se llame nginx-produccion.conf. Este truco es imprescindible cuando el nombre que espera la aplicación no coincide con el que te resulta cómodo en el repositorio.
3.4. --from-file con un directorio
Si apuntas a un directorio, cada fichero de dentro se convierte en una clave. No es recursivo: los subdirectorios se ignoran.
kubectl create configmap tienda-web-nginx \
--from-file=k8s/base/tienda-web/conf.d/ \
--dry-run=client -o yaml -n rutas-norte-devapiVersion: v1
data:
compresion.conf: |
gzip on;
gzip_types text/css application/javascript application/json;
seguridad.conf: |
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
spa.conf: |
location / {
try_files $uri $uri/ /index.html;
}
kind: ConfigMap
metadata:
creationTimestamp: null
name: tienda-web-nginxTres ficheros, tres claves. Cuando montemos este ConfigMap en /etc/nginx/conf.d aparecerán los tres ficheros con esos mismos nombres, que es justo lo que nginx espera.
3.5. --from-env-file
Esta es la que más confunde. Toma un fichero con formato CLAVE=valor y crea una clave por línea, no una clave con el fichero entero.
kubectl create configmap tienda-web-config \
--from-env-file=k8s/entornos/dev/tienda-web.env \
--dry-run=client -o yaml -n rutas-norte-devapiVersion: v1
data:
API_URL: http://api-reservas:8080
NIVEL_LOG: debug
TIEMPO_ESPERA_MS: "5000"
kind: ConfigMap
metadata:
creationTimestamp: null
name: tienda-web-configCompara con lo que habría hecho --from-file sobre el mismo fichero: una sola clave llamada tienda-web.env cuyo valor sería el texto completo. La diferencia es enorme y es la fuente de un error clásico.
| Opción | Sobre app.env produce |
Uso típico |
|---|---|---|
--from-file=app.env |
1 clave: app.env → contenido entero |
Montar el fichero tal cual |
--from-env-file=app.env |
N claves: una por línea | Alimentar variables de entorno con envFrom |
Reglas del formato de --from-env-file: las líneas vacías y las que empiezan por # se ignoran; no se admiten comillas alrededor del valor (se guardarían como parte del valor) ni continuaciones de línea; y las claves deben ser nombres válidos de variable de entorno.
3.6. Tabla resumen
| Forma | Comando | Cuándo usarla |
|---|---|---|
| Manifiesto | kubectl apply -f cm.yaml |
Siempre en producción. Es lo que se versiona |
| Literal | --from-literal=K=V |
Pruebas rápidas y generación de YAML |
| Fichero | --from-file=ruta/f.conf |
Ficheros de configuración largos |
| Directorio | --from-file=ruta/dir/ |
Un conjunto de ficheros que van juntos |
| Env file | --from-env-file=app.env |
Muchas variables simples de una vez |
- Las tres formas de consumirlo (y cuál veremos aquí)
flowchart LR
CM["ConfigMap<br/>tienda-web-config"] --> A["Variables de entorno<br/>env / envFrom"]
CM --> B["Fichero en un volumen<br/>volumeMounts"]
CM --> C["Argumentos de línea de comandos<br/>args con $(VAR)"]
A --> P["Contenedor"]
B --> P
C --> P
| Forma | Ventaja | Inconveniente | Dónde se estudia |
|---|---|---|---|
| Variable de entorno | Simple, universal | No se refresca sin reiniciar el pod | 03-03 |
| Fichero en volumen | Se actualiza en caliente, admite ficheros enteros | La app debe leer del disco | Esta lección |
Argumento (args) |
Explícito en el manifiesto | Fijo desde el arranque | 03-03 |
Esta lección se centra en la segunda porque es la que resuelve el caso del nginx.conf —un fichero de 40 líneas no cabe cómodamente en una variable de entorno— y porque es la única que permite cambiar la configuración sin recrear pods.
- Montar un ConfigMap como volumen
El mecanismo tiene siempre dos mitades que deben cuadrar: un volumen a nivel de pod que declara de dónde salen los datos, y un volumeMount a nivel de contenedor que declara dónde aparecen. El puente entre ambos es el campo name.
apiVersion: apps/v1
kind: Deployment
metadata:
name: tienda-web
namespace: rutas-norte-dev
spec:
replicas: 3
selector:
matchLabels:
app: tienda-web # solo app y entorno en el selector
entorno: dev
template:
metadata:
labels:
app: tienda-web
app.kubernetes.io/name: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: nginx
image: nginx:1.27.1-alpine # etiqueta inmutable, nunca latest
ports:
- containerPort: 80
volumeMounts:
- name: config-nginx # (2) debe coincidir con volumes[].name
mountPath: /etc/nginx/conf.d # dónde aparece dentro del contenedor
readOnly: true # buena práctica: la app no debe escribir
volumes:
- name: config-nginx # (1) nombre del volumen dentro del pod
configMap:
name: tienda-web-nginx # ConfigMap del MISMO namespaceQué ocurre exactamente cuando arranca ese pod:
- El kubelet lee el pod y ve un volumen de tipo
configMap. - Pide a la API el ConfigMap
tienda-web-nginxdel namespacerutas-norte-dev. Si no existe, el pod se queda enContainerCreatinghasta que aparezca, con un eventoFailedMount. No falla para siempre: en cuanto crees el ConfigMap, el pod arranca. - Crea en el nodo un directorio y escribe dentro un fichero por cada clave del ConfigMap.
- Monta ese directorio en
/etc/nginx/conf.ddentro del contenedor.
Y aquí está el punto que sorprende a todo el mundo la primera vez: el montaje sustituye el contenido del directorio del contenedor. Lo que la imagen nginx traía en /etc/nginx/conf.d (el fichero default.conf por defecto) deja de verse. No se borra de la imagen; simplemente queda oculto bajo el punto de montaje, igual que en Linux al montar un disco sobre un directorio que ya tenía ficheros.
Vamos a verlo:
total 12
drwxrwxrwt 3 root root 120 Aug 5 09:41 .
drwxr-xr-x 3 root root 4096 Aug 5 09:41 ..
drwxr-xr-x 2 root root 80 Aug 5 09:41 ..2026_08_05_09_41_18.1042318874
lrwxrwxrwx 1 root root 32 Aug 5 09:41 ..data -> ..2026_08_05_09_41_18.1042318874
lrwxrwxrwx 1 root root 17 Aug 5 09:41 compresion.conf -> ..data/compresion.conf
lrwxrwxrwx 1 root root 17 Aug 5 09:41 seguridad.conf -> ..data/seguridad.conf
lrwxrwxrwx 1 root root 11 Aug 5 09:41 spa.conf -> ..data/spa.confEsta salida contiene toda la magia del mecanismo, así que conviene leerla con calma:
- Los ficheros que ve la aplicación (
compresion.conf, etc.) son enlaces simbólicos a..data/. ..dataes a su vez un enlace simbólico a un directorio con marca de tiempo.- El directorio real contiene los ficheros de verdad.
¿Por qué tanta indirección? Porque permite una actualización atómica. Cuando el ConfigMap cambia, el kubelet escribe un directorio nuevo con marca de tiempo nueva y después mueve el enlace ..data de golpe. La aplicación nunca ve un estado a medias con la mitad de los ficheros viejos y la mitad nuevos. Es un detalle de implementación, pero explica dos comportamientos que verás más adelante: por qué la actualización funciona y por qué con subPath no funciona.
- Seleccionar claves concretas con
items y defaultMode
items y defaultModePor defecto se proyectan todas las claves. A menudo no es lo que quieres: un mismo ConfigMap puede tener claves destinadas a variables de entorno (API_URL) y claves que son ficheros (nginx.conf), y montarlas todas juntas llenaría /etc/nginx/conf.d de basura que nginx intentaría interpretar.
Con items eliges qué claves se proyectan y con qué nombre de fichero:
volumes:
- name: config-nginx
configMap:
name: tienda-web-config
defaultMode: 0444 # permisos por defecto: solo lectura
items:
- key: nginx.conf # clave del ConfigMap
path: default.conf # nombre del fichero resultante
mode: 0400 # permisos SOLO de este fichero
- key: mapa-mime.types
path: mime.typesDetalles que importan:
pathes relativo almountPath. ConmountPath: /etc/nginx/conf.dypath: default.conf, el fichero acaba en/etc/nginx/conf.d/default.conf.pathadmite subdirectorios:path: extra/cabeceras.confcrea el subdirectorio. Lo que no admite es empezar por/ni contener...- Si usas
items, solo se proyecta lo que listes. Las claves no mencionadas desaparecen del volumen. - Si una clave listada en
itemsno existe en el ConfigMap, el pod no arranca. Es un comportamiento deseable: falla pronto y con un mensaje claro en lugar de arrancar nginx sin configuración. defaultModese escribe en octal y hay que ponerlo con el0delante (0444). Si escribes444en decimal, YAML lo interpreta como el número 444, que en octal es0674: permisos absurdos. Es un error real y frecuente.- El
modede unitemconcreto prevalece sobredefaultMode.
Una advertencia sobre los permisos: si el contenedor corre con un usuario no root (lo habitual y lo recomendable, como verás en Contextos de Seguridad), un defaultMode: 0400 significa "solo lectura para el propietario", y el propietario del fichero es root. El proceso no podría leerlo. Para ese caso, 0444 es la elección segura.
subPath: el uso y sus dos peligros
subPath: el uso y sus dos peligrosVuelve al problema del apartado 5: montar un volumen sobre un directorio oculta lo que ya había. ¿Y si solo quieres añadir o sustituir un único fichero dentro de un directorio que debe conservar el resto de su contenido?
El caso canónico en Rutas Norte es /etc/nginx/nginx.conf. Ese fichero está en /etc/nginx/, un directorio que también contiene mime.types, fastcgi_params, el directorio conf.d... Si montamos el volumen en /etc/nginx/, nginx se queda sin todo lo demás y no arranca.
La solución es subPath:
volumeMounts:
- name: config-nginx
mountPath: /etc/nginx/nginx.conf # ruta a un FICHERO, no a un directorio
subPath: nginx.conf # qué clave del volumen se monta ahí
readOnly: trueCon esto, solo el fichero /etc/nginx/nginx.conf queda sustituido; el resto de /etc/nginx/ sigue intacto.
Todo en su sitio. Ahora los dos peligros, que hay que conocer antes de usarlo:
Peligro 1: con subPath la actualización NO se propaga. Cuando montas con subPath, el kubelet copia el fichero directamente en lugar de crear la cadena de enlaces simbólicos del apartado 5. Al no haber enlace ..data que mover, el fichero queda congelado en el valor que tenía al arrancar el pod. Puedes editar el ConfigMap cien veces: el contenedor seguirá viendo el original hasta que lo recrees. Es, con diferencia, la causa número uno de "he cambiado la configuración y no pasa nada".
Peligro 2: es fácil equivocarse de sentido. mountPath debe apuntar al fichero de destino y subPath a la clave de origen. Si los intercambias, o si mountPath apunta a un directorio existente, obtendrás un directorio donde esperabas un fichero y la aplicación fallará con un mensaje confuso.
Sin subPath |
Con subPath |
|---|---|
mountPath es un directorio |
mountPath es un fichero |
| Oculta el contenido previo del directorio | Conserva el resto del directorio |
Ficheros como enlaces simbólicos a ..data |
Fichero copiado directamente |
| Se actualiza en caliente | No se actualiza nunca |
| Preferible siempre que sea posible | Solo si no hay alternativa |
La recomendación práctica: evita subPath si puedes. En el caso de nginx tienes una alternativa mejor, que es la que usaremos: montar en /etc/nginx/conf.d/, el directorio que la propia imagen reserva para configuración añadida, sin tocar /etc/nginx/nginx.conf. Ese directorio está pensado para ser sustituido entero.
- El caso central: el
nginx.conf de tienda-web
nginx.conf de tienda-webVamos a resolver la deuda del módulo 2 con todo lo anterior. Creamos el fichero k8s/base/tienda-web/configmap-nginx.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: tienda-web-nginx
namespace: rutas-norte-dev
labels:
app: tienda-web
app.kubernetes.io/name: tienda-web
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: rutas-norte
entorno: dev
data:
default.conf: |
server {
listen 80;
server_name www.rutasnorte.example;
root /usr/share/nginx/html;
index index.html;
# Cabeceras de seguridad para el portal de venta de billetes
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Compresion de los estaticos de la SPA
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# Los estaticos con hash en el nombre se cachean un anyo
location ~* \.(js|css|woff2|png|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Regla imprescindible en una SPA: cualquier ruta desconocida
# devuelve index.html para que el enrutador del navegador la resuelva
location / {
try_files $uri $uri/ /index.html;
}
# Sonda de vida para el modulo 7
location = /healthz {
access_log off;
return 200 "ok\n";
}
}Y modificamos el Deployment de tienda-web que ya teníamos:
apiVersion: apps/v1
kind: Deployment
metadata:
name: tienda-web
namespace: rutas-norte-dev
labels:
app: tienda-web
app.kubernetes.io/name: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 3
selector:
matchLabels:
app: tienda-web
entorno: dev
template:
metadata:
labels:
app: tienda-web
app.kubernetes.io/name: tienda-web
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: nginx
image: nginx:1.27.1-alpine
ports:
- name: http
containerPort: 80
volumeMounts:
- name: config-nginx
mountPath: /etc/nginx/conf.d # directorio entero: sin subPath
readOnly: true
resources: # los afinaremos en 03-04
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumes:
- name: config-nginx
configMap:
name: tienda-web-nginx
defaultMode: 0444Aplicamos y comprobamos:
kubectl apply -f k8s/base/tienda-web/configmap-nginx.yaml
kubectl apply -f k8s/base/tienda-web/deployment.yaml
kubectl rollout status deploy/tienda-web -n rutas-norte-devconfigmap/tienda-web-nginx created
deployment.apps/tienda-web configured
Waiting for deployment "tienda-web" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "tienda-web" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "tienda-web" successfully rolled outVerificamos que nginx está leyendo nuestra configuración y no la de la imagen:
kubectl exec -n rutas-norte-dev deploy/tienda-web -- cat /etc/nginx/conf.d/default.conf | head -5
kubectl exec -n rutas-norte-dev deploy/tienda-web -- nginx -T 2>/dev/null | grep -c "try_files"server {
listen 80;
server_name www.rutasnorte.example;
root /usr/share/nginx/html;
index index.html;
1nginx -T vuelca la configuración efectiva tras resolver todos los include. Que aparezca nuestro try_files confirma que la regla de la SPA está activa. Y la prueba definitiva, desde un pod efímero:
kubectl run prueba --rm -it --restart=Never --image=curlimages/curl:8.10.1 \
-n rutas-norte-dev -- curl -si http://tienda-web/healthzHTTP/1.1 200 OK
Server: nginx/1.27.1
Content-Type: text/plain
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
okLas tres cabeceras de seguridad están ahí. La configuración de nginx ya no vive en la imagen: vive en un YAML revisable en una pull request.
- Actualizar un ConfigMap y la propagación a los volúmenes
Ahora el pago del esfuerzo. Añadimos una cabecera más sin reconstruir la imagen y sin reiniciar nada.
# Editamos el YAML en el repositorio y aplicamos
kubectl apply -f k8s/base/tienda-web/configmap-nginx.yamlEsperamos y miramos dentro del contenedor:
kubectl exec -n rutas-norte-dev deploy/tienda-web -- grep Permissions-Policy /etc/nginx/conf.d/default.confEl fichero ha cambiado dentro de un contenedor que no se ha reiniciado. Los tres pods siguen teniendo RESTARTS 0.
Sobre los tiempos y las condiciones:
- La propagación no es instantánea. El kubelet refresca los volúmenes en su ciclo de sincronización, gobernado por
--sync-frequency(por defecto 1 minuto), más el tiempo de caché de su cliente de la API (configMapAndSecretChangeDetectionStrategy, por defectoWatch). En la práctica cuenta con hasta un par de minutos, no con segundos. - Se propaga solo si el volumen no usa
subPath(apartado 7). - Se propaga solo si el ConfigMap no es inmutable (apartado 10).
- Y, sobre todo: que el fichero cambie no significa que la aplicación se entere. nginx leyó su configuración al arrancar y la tiene en memoria.
Este último punto es fundamental. Hay tres tipos de aplicación:
| Tipo de aplicación | Comportamiento al cambiar el fichero | Qué hacer |
|---|---|---|
| Relee el fichero en cada uso | Se aplica sola | Nada |
| Recarga con una señal (nginx, HAProxy) | Ignora el cambio | Enviar la señal o reiniciar |
| Lee solo al arrancar (la mayoría) | Ignora el cambio | Reiniciar los pods |
Para nginx podemos enviarle la señal de recarga, que no corta ninguna conexión en curso:
Para el caso general, la solución limpia es forzar un despliegue nuevo:
Esto sustituye los pods con la estrategia RollingUpdate del módulo 2, sin corte de servicio. Existe una técnica más elegante y automática —guardar un hash del ConfigMap en una anotación del template, de modo que cambiar la configuración dispare el despliegue por sí solo— que verás en Variables de Entorno, donde es todavía más necesaria.
- ConfigMaps inmutables
Desde Kubernetes 1.21, un ConfigMap puede declararse inmutable:
apiVersion: v1
kind: ConfigMap
metadata:
name: tienda-web-nginx-v3
namespace: rutas-norte-pro
data:
default.conf: |
server { listen 80; }
immutable: true # a partir de aqui, ni data ni binaryData se pueden tocarQué gana:
- Rendimiento y escala. Este es el motivo principal. Cada kubelet mantiene un watch abierto contra el apiserver por cada ConfigMap que monta. En un clúster grande eso son miles de conexiones vigilando cambios. Si el objeto es inmutable, el kubelet cierra el watch: no hay nada que vigilar. La carga sobre el apiserver baja de forma notable.
- Protección contra el cambio accidental. Nadie puede modificar por error la configuración de producción de un tirón.
- Determinismo. Un pod que arranque hoy y otro que arranque dentro de un mes con el mismo ConfigMap tendrán exactamente la misma configuración. Con un ConfigMap mutable, dos réplicas del mismo Deployment podrían tener contenidos distintos si el ConfigMap cambió entre sus arranques.
Qué se pierde: la actualización en caliente del apartado 9. Y hay dos reglas duras:
- La inmutabilidad no se puede revertir. Una vez
immutable: true, no se puede volver afalse. Solo queda borrar el objeto y recrearlo. metadatasí se puede cambiar (etiquetas, anotaciones). Lo congelado esdataybinaryData.
Intentar modificarlo produce un error muy claro:
The ConfigMap "tienda-web-nginx-v3" is invalid: data: Forbidden: field is immutable when `immutable` is setEl patrón que acompaña a la inmutabilidad es versionar el nombre: tienda-web-nginx-v3, -v4, etc. El Deployment apunta a la versión concreta, y cambiar de configuración significa crear un ConfigMap nuevo y actualizar la referencia en el Deployment, lo que dispara un despliegue con historial y rollback. Es exactamente lo que hacen los generadores de Kustomize añadiendo un sufijo con el hash del contenido.
Recomendación para Rutas Norte: mutable en dev (iterar rápido con nginx -s reload), inmutable con nombre versionado en pro (determinismo y rollback).
- El límite de 1 MiB y qué hacer si se supera
Un ConfigMap no puede superar 1 MiB, contando la suma de data y binaryData más los metadatos. El límite no es caprichoso: los ConfigMaps se guardan en etcd, que impone por defecto un tamaño máximo de 1,5 MiB por valor, y etcd es el corazón del clúster.
Al pasarte, el error es inmediato:
O bien, cuando la validación la hace el propio objeto:
Ten presente además que base64 infla un 33 %: un .ico de 800 KiB ocupa unos 1,07 MiB en binaryData y no cabe.
Qué hacer si te acercas al límite:
| Situación | Solución |
|---|---|
| Muchos ficheros que van juntos | Dividir en varios ConfigMaps y montar varios volúmenes |
| Un solo fichero enorme (dataset, catálogo) | No es configuración. Va en un volumen persistente (módulo 5) o se descarga al arrancar |
| Binarios grandes (fuentes, imágenes) | Deben ir en la imagen del contenedor |
| Plantillas comprimidas | .tar.gz en binaryData y descompresión con un init container (06-04) |
La regla de olfato: si te acercas al megabyte, casi seguro que estás metiendo en un ConfigMap algo que no es configuración. En Rutas Norte esto aplicaría al catálogo completo de rutas y paradas, que son datos de negocio y viven en postgres-reservas, no en un ConfigMap.
- ConfigMaps por entorno en Rutas Norte
Recuerda la restricción del apartado 2: un ConfigMap tiene namespace y un pod solo puede montar los de su propio namespace. Como los entornos de Rutas Norte son namespaces, esto encaja de forma natural: un ConfigMap por entorno, con el mismo nombre en cada uno.
flowchart TB
subgraph dev["rutas-norte-dev"]
CMD["ConfigMap tienda-web-config<br/>API_URL: http://api-reservas:8080<br/>NIVEL_LOG: debug"]
DD["Deployment tienda-web<br/>configMap: tienda-web-config"]
CMD --- DD
end
subgraph pre["rutas-norte-pre"]
CMP["ConfigMap tienda-web-config<br/>API_URL: https://api-pre.rutasnorte.example<br/>NIVEL_LOG: info"]
DP["Deployment tienda-web<br/>configMap: tienda-web-config"]
CMP --- DP
end
subgraph pro["rutas-norte-pro"]
CMR["ConfigMap tienda-web-config<br/>API_URL: https://api.rutasnorte.example<br/>NIVEL_LOG: warn"]
DR["Deployment tienda-web<br/>configMap: tienda-web-config"]
CMR --- DR
end
IMG["Imagen unica<br/>tienda-web:2.5.0"] --> DD
IMG --> DP
IMG --> DR
El nombre del ConfigMap es idéntico en los tres namespaces. Eso permite que el Deployment sea literalmente el mismo fichero en k8s/base/, y que lo único que cambie entre entornos sea el ConfigMap de k8s/entornos/<entorno>/. La estructura del repositorio queda así:
k8s/
├── base/
│ ├── tienda-web/
│ │ ├── deployment.yaml # identico para los tres entornos
│ │ ├── service.yaml
│ │ └── configmap-nginx.yaml # nginx.conf comun
│ └── api-reservas/
│ ├── deployment.yaml
│ └── service.yaml
└── entornos/
├── dev/
│ ├── namespace.yaml
│ ├── tienda-web-config.yaml # API_URL de dev
│ └── api-reservas-config.yaml
├── pre/
│ └── ...
└── pro/
└── ...Y este es el contenido concreto de la configuración de Rutas Norte por entorno. Toma esta tabla como referencia; en 03-03 la ampliaremos al catálogo completo de todos los componentes.
| Clave | dev |
pre |
pro |
|---|---|---|---|
API_URL |
http://api-reservas:8080 |
https://api-pre.rutasnorte.example |
https://api.rutasnorte.example |
NIVEL_LOG |
debug |
info |
warn |
TIEMPO_ESPERA_MS |
5000 |
3000 |
2000 |
REDIS_HOST |
redis-cache |
redis-cache |
redis-cache |
REDIS_TTL_SEGUNDOS |
30 |
120 |
300 |
MAX_PLAZAS_POR_RESERVA |
9 |
9 |
9 |
MODO_MANTENIMIENTO |
false |
false |
false |
Observa dos decisiones de diseño:
REDIS_TTL_SEGUNDOSsube al acercarse a producción. En desarrollo interesa una caché corta para ver los cambios; en producción, con los picos de tráfico de puentes y vacaciones, interesa una caché larga que descargue apostgres-reservas.MODO_MANTENIMIENTOes un interruptor operativo. Ponerlo atruey recargar nginx muestra la página de "estamos actualizando" sin desplegar nada. Es exactamente el tipo de palanca que justifica separar configuración de código.
Y una cosa que no está en esta tabla y nunca debe estarlo: la contraseña de postgres-reservas. Un ConfigMap no ofrece ninguna protección: cualquiera que pueda listar ConfigMaps en el namespace lee su contenido en claro. Esa deuda la salda la lección siguiente.
Errores Comunes y Consejos
| Error | Síntoma | Solución |
|---|---|---|
| ConfigMap en otro namespace | Pod en ContainerCreating, evento FailedMount |
Un pod solo monta ConfigMaps de su namespace. Créalo en el correcto |
| ConfigMap inexistente | configmap "x" not found en los eventos |
kubectl get cm -n <ns> y revisa el nombre |
| Montar sobre un directorio con contenido | La aplicación no encuentra sus ficheros | Monta en un subdirectorio previsto (conf.d) o usa subPath |
Esperar actualización con subPath |
"He cambiado el ConfigMap y no pasa nada" | subPath no propaga. Quítalo o haz rollout restart |
| Esperar que la app recargue sola | El fichero cambia pero el comportamiento no | Envía la señal de recarga o reinicia los pods |
defaultMode: 644 sin el cero |
Permisos extraños (--w----r-T) |
Es octal: escribe 0644 |
0400 con usuario no root |
Permission denied al leer |
Usa 0444 o ajusta fsGroup |
Usar > en vez de ` |
` en YAML | El fichero llega con las líneas pegadas |
Confundir --from-file y --from-env-file |
Una clave con todo el .env dentro |
Revisa la tabla del apartado 3.5 |
| Meter secretos en un ConfigMap | Credencial legible por cualquiera | Usa Secrets y RBAC |
Objetos creados con kubectl create |
No aparecen en las consultas con -l |
Genera el YAML con --dry-run=client -o yaml y añade etiquetas |
| Superar 1 MiB | Too long: must have at most 1048576 bytes |
Divide, o admite que no es configuración |
Cinco consejos que ahorran horas:
- Etiqueta los ConfigMaps con el mismo esquema que el resto.
kubectl get cm -l app=tienda-web -Adebe funcionar. - Comprueba siempre desde dentro del contenedor, no desde el YAML:
kubectl exec ... -- cat <ruta>. Es la única verdad. - Documenta cada clave con un comentario en el YAML. El manifiesto es la documentación de la configuración.
- Un ConfigMap por propósito, no uno gigante por aplicación. Separa el que se monta como fichero del que alimenta variables de entorno.
- En producción, inmutable y versionado. El determinismo compensa la comodidad.
Ejercicios
Ejercicio 1: De la imagen al ConfigMap
api-reservas necesita un fichero /app/config/reglas-tarifas.json con las reglas de descuento, que hoy está dentro de su imagen.
- Crea en tu máquina el fichero
reglas-tarifas.jsoncon este contenido:
{
"descuento_ida_vuelta": 0.12,
"descuento_joven": 0.20,
"edad_maxima_joven": 26,
"recargo_puente": 0.15
}- Genera con
kubectl(sin escribir YAML a mano) el manifiesto de un ConfigMap llamadoapi-reservas-tarifasenrutas-norte-deva partir de ese fichero, y guárdalo enk8s/entornos/dev/. - Añádele las etiquetas del esquema de Rutas Norte y aplícalo.
- Escribe el fragmento de
volumesyvolumeMountsque lo monta en/app/configde forma que solo se pueda leer. - Verifica desde dentro del pod que el fichero existe y tiene el contenido correcto.
Ejercicio 2: subPath y la trampa de la actualización
- Despliega un pod de
tienda-webque monte su configuración consubPathsobre/etc/nginx/nginx.conf. - Comprueba que
/etc/nginx/conservamime.typesy el directorioconf.d. - Modifica el ConfigMap y espera tres minutos. Comprueba si el fichero ha cambiado dentro del contenedor.
- Repite el experimento montando el mismo ConfigMap sin
subPathen/etc/nginx/conf.d/. ¿Se propaga ahora? - Explica en dos líneas la diferencia y qué se ve con
ls -laen cada caso.
Ejercicio 3: Configuración de los tres entornos
- Crea el ConfigMap
tienda-web-configen los tres namespaces con los valores de la tabla del apartado 12. - Demuestra con un solo comando que existen tres objetos con ese nombre y que sus valores de
NIVEL_LOGson distintos. - Haz inmutable únicamente el de
rutas-norte-proe intenta modificarlo. Copia el error. - Actualiza el
REDIS_TTL_SEGUNDOSde producción sabiendo que el ConfigMap es inmutable. Describe la secuencia completa de pasos. - Responde: si mañana hay que activar el modo mantenimiento en producción a las 3 de la madrugada, ¿qué comando exacto ejecutarías?
Soluciones
Solución 1
mkdir -p k8s/entornos/dev
cat > /tmp/reglas-tarifas.json <<'EOF'
{
"descuento_ida_vuelta": 0.12,
"descuento_joven": 0.20,
"edad_maxima_joven": 26,
"recargo_puente": 0.15
}
EOF
kubectl create configmap api-reservas-tarifas \
--from-file=/tmp/reglas-tarifas.json \
-n rutas-norte-dev \
--dry-run=client -o yaml > k8s/entornos/dev/api-reservas-tarifas.yamlEl YAML generado, ya con las etiquetas añadidas a mano:
apiVersion: v1
kind: ConfigMap
metadata:
name: api-reservas-tarifas
namespace: rutas-norte-dev
labels:
app: api-reservas
app.kubernetes.io/name: api-reservas
app.kubernetes.io/component: backend
app.kubernetes.io/part-of: rutas-norte
entorno: dev
data:
reglas-tarifas.json: |
{
"descuento_ida_vuelta": 0.12,
"descuento_joven": 0.20,
"edad_maxima_joven": 26,
"recargo_puente": 0.15
}El fragmento del Deployment:
volumeMounts:
- name: tarifas
mountPath: /app/config
readOnly: true
volumes:
- name: tarifas
configMap:
name: api-reservas-tarifas
defaultMode: 0444Y la verificación:
{
"descuento_ida_vuelta": 0.12,
"descuento_joven": 0.20,
"edad_maxima_joven": 26,
"recargo_puente": 0.15
}Un apunte: la clave del ConfigMap es reglas-tarifas.json porque --from-file toma el nombre base del fichero, y el mountPath es el directorio /app/config. El resultado es /app/config/reglas-tarifas.json, la ruta pedida.
Solución 2
Con subPath:
volumeMounts:
- name: config-nginx
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: trueEl directorio conserva todo. Tras modificar el ConfigMap y esperar:
kubectl apply -f configmap-nginx.yaml
sleep 180
kubectl exec -n rutas-norte-dev deploy/tienda-web -- grep -c "worker_connections 2048" /etc/nginx/nginx.confNo ha cambiado. El fichero sigue con el valor del arranque.
Sin subPath, montando en /etc/nginx/conf.d/:
kubectl apply -f configmap-nginx.yaml
sleep 90
kubectl exec -n rutas-norte-dev deploy/tienda-web -- grep -c "Permissions-Policy" /etc/nginx/conf.d/default.confAhora sí. La diferencia se ve con ls -la:
# SIN subPath: enlaces simbolicos, la actualizacion mueve ..data de forma atomica
lrwxrwxrwx 1 root root 15 Aug 5 10:41 default.conf -> ..data/default.conf
lrwxrwxrwx 1 root root 32 Aug 5 10:41 ..data -> ..2026_08_05_10_41_02.418822910
# CON subPath: fichero real copiado al arrancar, sin enlace que actualizar
-rw-r--r-- 1 root root 648 Aug 5 09:12 nginx.confSin subPath, el kubelet actualiza escribiendo un directorio nuevo y moviendo el enlace ..data. Con subPath no hay enlace: el fichero se copió una vez y ahí se queda hasta que se recree el pod.
Solución 3
for E in dev pre pro; do
kubectl create namespace rutas-norte-$E --dry-run=client -o yaml | kubectl apply -f -
done
kubectl create configmap tienda-web-config -n rutas-norte-dev \
--from-literal=API_URL=http://api-reservas:8080 \
--from-literal=NIVEL_LOG=debug \
--from-literal=TIEMPO_ESPERA_MS=5000 \
--from-literal=REDIS_TTL_SEGUNDOS=30
kubectl create configmap tienda-web-config -n rutas-norte-pre \
--from-literal=API_URL=https://api-pre.rutasnorte.example \
--from-literal=NIVEL_LOG=info \
--from-literal=TIEMPO_ESPERA_MS=3000 \
--from-literal=REDIS_TTL_SEGUNDOS=120
kubectl create configmap tienda-web-config -n rutas-norte-pro \
--from-literal=API_URL=https://api.rutasnorte.example \
--from-literal=NIVEL_LOG=warn \
--from-literal=TIEMPO_ESPERA_MS=2000 \
--from-literal=REDIS_TTL_SEGUNDOS=300La comprobación en un comando:
kubectl get cm tienda-web-config -A -o custom-columns=\
NS:.metadata.namespace,LOG:.data.NIVEL_LOG,TTL:.data.REDIS_TTL_SEGUNDOS,API:.data.API_URLNS LOG TTL API
rutas-norte-dev debug 30 http://api-reservas:8080
rutas-norte-pre info 120 https://api-pre.rutasnorte.example
rutas-norte-pro warn 300 https://api.rutasnorte.exampleTres objetos con el mismo nombre y contenidos distintos, exactamente lo que permite que el Deployment sea idéntico.
Ahora la inmutabilidad de producción:
kubectl patch configmap tienda-web-config -n rutas-norte-pro \
--type merge -p '{"immutable": true}'
kubectl patch configmap tienda-web-config -n rutas-norte-pro \
--type merge -p '{"data": {"NIVEL_LOG": "info"}}'configmap/tienda-web-config patched
Error from server: ConfigMap "tienda-web-config" is invalid: data: Forbidden: field is immutable when `immutable` is setPara actualizar REDIS_TTL_SEGUNDOS en producción siendo inmutable, la secuencia correcta es:
- Crear un ConfigMap nuevo con nombre versionado,
tienda-web-config-v2, con el valor nuevo yimmutable: true. - Aplicarlo.
- Editar el Deployment de
rutas-norte-propara que apunte atienda-web-config-v2, conkubernetes.io/change-causedocumentando el motivo. kubectl rollout statuspara seguir el despliegue progresivo del módulo 2.- Verificar y conservar el
-v1unos días: si hay que hacerkubectl rollout undo, el Deployment volverá a referenciarlo y debe existir. - Borrar el
-v1pasada la ventana de rollback.
Fíjate en la ventaja: la configuración de producción ahora tiene historial y rollback, igual que el código.
Para el modo mantenimiento a las 3 de la madrugada, con el ConfigMap mutable de nginx:
kubectl patch configmap tienda-web-nginx -n rutas-norte-pro \
--type merge -p '{"data": {"mantenimiento.conf": "return 503;"}}'
kubectl exec -n rutas-norte-pro deploy/tienda-web -- nginx -s reloadSin reconstruir imagen, sin desplegar, sin cortar conexiones. Ese es exactamente el valor de tener la configuración fuera del código.
Conclusión
Has saldado la primera de las deudas del módulo 2. Sabes por qué el tercer factor de los doce factores exige que la configuración viva fuera del código, y has visto el coste concreto de no hacerlo: una imagen por entorno, lo probado en pre distinto de lo desplegado en pro, y un cambio de tiempo de espera convertido en un pipeline completo. Conoces la anatomía del ConfigMap —sin spec ni status, con data para texto y binaryData para lo demás, con namespace y con las claves como nombres de fichero potenciales— y sus cuatro formas de creación, sabiendo distinguir la trampa entre --from-file y --from-env-file.
Dominas el consumo como volumen, que es el que da más juego: la pareja volumes/volumeMounts, el hecho de que el montaje oculta el contenido previo del directorio, la cadena de enlaces simbólicos hacia ..data que hace atómica la actualización, la selección de claves con items, los permisos con defaultMode en octal y sus interacciones con los usuarios no root, y los dos peligros de subPath, empezando por el más caro: con subPath la configuración no se propaga nunca. Sabes que la propagación tarda hasta un par de minutos y que cambiar el fichero no equivale a que la aplicación se entere, con las tres estrategias según el tipo de aplicación. Y conoces los ConfigMaps inmutables, lo que ganan en carga sobre el apiserver y determinismo, el patrón de nombre versionado que los acompaña, y el límite de 1 MiB con la señal que emite: si te acercas, no es configuración.
Sobre el terreno, tienda-web ya sirve su nginx.conf desde un ConfigMap montado en /etc/nginx/conf.d, con cabeceras de seguridad, compresión, caché de estáticos, la regla try_files de la SPA y un endpoint /healthz que la lección de sondas aprovechará. Los tres entornos tienen su propio ConfigMap con el mismo nombre, lo que ha permitido que el Deployment sea un único fichero en k8s/base/.
Queda la deuda gorda. Todo lo que hemos guardado hoy es público: cualquiera con permiso de lectura en el namespace ve el contenido íntegro de un ConfigMap. La contraseña de postgres-reservas —la base de datos que guarda el nombre, el DNI, el teléfono y el correo de cada cliente de Rutas Norte— sigue en claro en un manifiesto de Git. La lección siguiente, Secrets, la saca de ahí: verás en qué se diferencia realmente un Secret de un ConfigMap, por qué base64 no es cifrado y lo demostrarás descodificando uno en tres segundos, los tipos de Secret y sus usos, el cifrado en reposo en etcd que por defecto no está activado, cómo se descargan imágenes privadas de registry.rutasnorte.example y qué soluciones reales usa el ecosistema cuando el Secret nativo no basta.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
