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

  1. Por qué se separa la configuración del código
  2. Anatomía del objeto ConfigMap
  3. Las cuatro formas de crear un ConfigMap
  4. Las tres formas de consumirlo (y cuál veremos aquí)
  5. Montar un ConfigMap como volumen
  6. Seleccionar claves concretas con items y defaultMode
  7. subPath: el uso y sus dos peligros
  8. El caso central: el nginx.conf de tienda-web
  9. Actualizar un ConfigMap y la propagación a los volúmenes
  10. ConfigMaps inmutables
  11. El límite de 1 MiB y qué hacer si se supera
  12. ConfigMaps por entorno en Rutas Norte

  1. 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:

  1. La URL de la API en tienda-web. La SPA necesita saber a qué dirección llamar: http://api-reservas dentro del clúster, https://api.rutasnorte.example desde el navegador del cliente. Hoy está incrustada en el JavaScript compilado dentro de la imagen.
  2. El nginx.conf de tienda-web. La configuración del servidor —compresión, cabeceras de seguridad, la regla de fallback a index.html que toda SPA necesita, el proxy hacia api-reservas— está horneada en la imagen. Ajustar la caché de los ficheros estáticos requiere reconstruirla.

Ambos se resuelven hoy, en esta lección.

  1. 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 tiene status.
  • data contiene texto válido en UTF-8. Las claves son nombres de fichero potenciales, así que deben limitarse a caracteres alfanuméricos, -, _ y ..
  • binaryData contiene 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.gz de plantillas.
  • Una misma clave no puede aparecer en data y en binaryData: el apply falla.
  • 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 |.

  1. 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:

kubectl apply -f k8s/base/tienda-web/configmap.yaml
configmap/tienda-web-config created

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-dev
configmap/tienda-web-config created

Comprobamos qué ha creado exactamente:

kubectl get configmap tienda-web-config -n rutas-norte-dev -o yaml
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-2b8e5d417a63

Fí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.yaml

Despué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-dev
apiVersion: v1
data:
  nginx.conf: |
    server {
      listen 80;
      server_name www.rutasnorte.example;
      ...
    }
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: tienda-web-nginx
  namespace: rutas-norte-dev

La 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-dev

La 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.

ls k8s/base/tienda-web/conf.d/
compresion.conf  seguridad.conf  spa.conf
kubectl create configmap tienda-web-nginx \
  --from-file=k8s/base/tienda-web/conf.d/ \
  --dry-run=client -o yaml -n rutas-norte-dev
apiVersion: 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-nginx

Tres 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.

cat k8s/entornos/dev/tienda-web.env
API_URL=http://api-reservas:8080
NIVEL_LOG=debug
TIEMPO_ESPERA_MS=5000
kubectl create configmap tienda-web-config \
  --from-env-file=k8s/entornos/dev/tienda-web.env \
  --dry-run=client -o yaml -n rutas-norte-dev
apiVersion: v1
data:
  API_URL: http://api-reservas:8080
  NIVEL_LOG: debug
  TIEMPO_ESPERA_MS: "5000"
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: tienda-web-config

Compara 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

  1. 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.

  1. 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 namespace

Qué ocurre exactamente cuando arranca ese pod:

  1. El kubelet lee el pod y ve un volumen de tipo configMap.
  2. Pide a la API el ConfigMap tienda-web-nginx del namespace rutas-norte-dev. Si no existe, el pod se queda en ContainerCreating hasta que aparezca, con un evento FailedMount. No falla para siempre: en cuanto crees el ConfigMap, el pod arranca.
  3. Crea en el nodo un directorio y escribe dentro un fichero por cada clave del ConfigMap.
  4. Monta ese directorio en /etc/nginx/conf.d dentro 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:

kubectl exec -n rutas-norte-dev deploy/tienda-web -- ls -la /etc/nginx/conf.d
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.conf

Esta 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/.
  • ..data es 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.

  1. Seleccionar claves concretas con items y defaultMode

Por 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.types

Detalles que importan:

  • path es relativo al mountPath. Con mountPath: /etc/nginx/conf.d y path: default.conf, el fichero acaba en /etc/nginx/conf.d/default.conf.
  • path admite subdirectorios: path: extra/cabeceras.conf crea 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 items no 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.
  • defaultMode se escribe en octal y hay que ponerlo con el 0 delante (0444). Si escribes 444 en decimal, YAML lo interpreta como el número 444, que en octal es 0674: permisos absurdos. Es un error real y frecuente.
  • El mode de un item concreto prevalece sobre defaultMode.

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.

  1. subPath: el uso y sus dos peligros

Vuelve 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: true

Con esto, solo el fichero /etc/nginx/nginx.conf queda sustituido; el resto de /etc/nginx/ sigue intacto.

kubectl exec -n rutas-norte-dev deploy/tienda-web -- ls /etc/nginx/
conf.d
fastcgi.conf
fastcgi_params
mime.types
modules
nginx.conf
scgi_params
uwsgi_params

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.

  1. El caso central: el nginx.conf de tienda-web

Vamos 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: 0444

Aplicamos 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-dev
configmap/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 out

Verificamos 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;
1

nginx -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/healthz
HTTP/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

ok

Las 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.

  1. 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.yaml
configmap/tienda-web-nginx configured

Esperamos y miramos dentro del contenedor:

kubectl exec -n rutas-norte-dev deploy/tienda-web -- grep Permissions-Policy /etc/nginx/conf.d/default.conf
add_header Permissions-Policy "geolocation=(), microphone=()" always;

El 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 defecto Watch). 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:

kubectl exec -n rutas-norte-dev deploy/tienda-web -- nginx -s reload
2026/08/05 10:22:41 [notice] 47#47: signal process started

Para el caso general, la solución limpia es forzar un despliegue nuevo:

kubectl rollout restart deploy/tienda-web -n rutas-norte-dev

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.

  1. 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 tocar

Qué gana:

  1. 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.
  2. Protección contra el cambio accidental. Nadie puede modificar por error la configuración de producción de un tirón.
  3. 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 a false. Solo queda borrar el objeto y recrearlo.
  • metadata sí se puede cambiar (etiquetas, anotaciones). Lo congelado es data y binaryData.

Intentar modificarlo produce un error muy claro:

kubectl apply -f configmap-nginx-v3.yaml
The ConfigMap "tienda-web-nginx-v3" is invalid: data: Forbidden: field is immutable when `immutable` is set

El 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).

  1. 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:

Error from server (RequestEntityTooLarge): Request entity too large: limit is 3145728

O bien, cuando la validación la hace el propio objeto:

The ConfigMap "catalogo-rutas" is invalid: []: Too long: must have at most 1048576 bytes

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.

  1. 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_SEGUNDOS sube 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 a postgres-reservas.
  • MODO_MANTENIMIENTO es un interruptor operativo. Ponerlo a true y 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:

  1. Etiqueta los ConfigMaps con el mismo esquema que el resto. kubectl get cm -l app=tienda-web -A debe funcionar.
  2. Comprueba siempre desde dentro del contenedor, no desde el YAML: kubectl exec ... -- cat <ruta>. Es la única verdad.
  3. Documenta cada clave con un comentario en el YAML. El manifiesto es la documentación de la configuración.
  4. Un ConfigMap por propósito, no uno gigante por aplicación. Separa el que se monta como fichero del que alimenta variables de entorno.
  5. 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.

  1. Crea en tu máquina el fichero reglas-tarifas.json con este contenido:
{
  "descuento_ida_vuelta": 0.12,
  "descuento_joven": 0.20,
  "edad_maxima_joven": 26,
  "recargo_puente": 0.15
}
  1. Genera con kubectl (sin escribir YAML a mano) el manifiesto de un ConfigMap llamado api-reservas-tarifas en rutas-norte-dev a partir de ese fichero, y guárdalo en k8s/entornos/dev/.
  2. Añádele las etiquetas del esquema de Rutas Norte y aplícalo.
  3. Escribe el fragmento de volumes y volumeMounts que lo monta en /app/config de forma que solo se pueda leer.
  4. Verifica desde dentro del pod que el fichero existe y tiene el contenido correcto.

Ejercicio 2: subPath y la trampa de la actualización

  1. Despliega un pod de tienda-web que monte su configuración con subPath sobre /etc/nginx/nginx.conf.
  2. Comprueba que /etc/nginx/ conserva mime.types y el directorio conf.d.
  3. Modifica el ConfigMap y espera tres minutos. Comprueba si el fichero ha cambiado dentro del contenedor.
  4. Repite el experimento montando el mismo ConfigMap sin subPath en /etc/nginx/conf.d/. ¿Se propaga ahora?
  5. Explica en dos líneas la diferencia y qué se ve con ls -la en cada caso.

Ejercicio 3: Configuración de los tres entornos

  1. Crea el ConfigMap tienda-web-config en los tres namespaces con los valores de la tabla del apartado 12.
  2. Demuestra con un solo comando que existen tres objetos con ese nombre y que sus valores de NIVEL_LOG son distintos.
  3. Haz inmutable únicamente el de rutas-norte-pro e intenta modificarlo. Copia el error.
  4. Actualiza el REDIS_TTL_SEGUNDOS de producción sabiendo que el ConfigMap es inmutable. Describe la secuencia completa de pasos.
  5. 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.yaml

El 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
    }
kubectl apply -f k8s/entornos/dev/api-reservas-tarifas.yaml
configmap/api-reservas-tarifas created

El fragmento del Deployment:

          volumeMounts:
            - name: tarifas
              mountPath: /app/config
              readOnly: true
      volumes:
        - name: tarifas
          configMap:
            name: api-reservas-tarifas
            defaultMode: 0444

Y la verificación:

kubectl exec -n rutas-norte-dev deploy/api-reservas -- cat /app/config/reglas-tarifas.json
{
  "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: true
kubectl exec -n rutas-norte-dev deploy/tienda-web -- ls /etc/nginx/
conf.d
fastcgi_params
mime.types
modules
nginx.conf
scgi_params
uwsgi_params

El 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.conf
configmap/tienda-web-nginx configured
0

No 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.conf
configmap/tienda-web-nginx configured
1

Ahora 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.conf

Sin 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=300

La 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_URL
NS                 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.example

Tres 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 set

Para actualizar REDIS_TTL_SEGUNDOS en producción siendo inmutable, la secuencia correcta es:

  1. Crear un ConfigMap nuevo con nombre versionado, tienda-web-config-v2, con el valor nuevo y immutable: true.
  2. Aplicarlo.
  3. Editar el Deployment de rutas-norte-pro para que apunte a tienda-web-config-v2, con kubernetes.io/change-cause documentando el motivo.
  4. kubectl rollout status para seguir el despliegue progresivo del módulo 2.
  5. Verificar y conservar el -v1 unos días: si hay que hacer kubectl rollout undo, el Deployment volverá a referenciarlo y debe existir.
  6. Borrar el -v1 pasada 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 reload

Sin 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

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados