Configurar aplicaciones de Kubernetes con ConfigMaps y Secrets

KubernetesBeginner
Practicar Ahora

Introducción

Las imágenes de contenedor deben poder reutilizarse en distintos entornos. Kubernetes separa las configuraciones no sensibles en ConfigMaps y las credenciales en Secrets, y después inyecta cualquiera de estos objetos como variables de entorno o archivos.

Configurarás una aplicación utilizando ambos mecanismos, inspeccionarás el resultado de forma segura, actualizarás la configuración y observarás por qué las variables de entorno y los volúmenes montados se actualizan de manera diferente.

El objetivo no es solo crear dos objetos de la API. Seguirás cada valor desde el almacenamiento de Kubernetes hasta el contenedor en ejecución; después lo modificarás para observar cómo el mecanismo de entrega afecta al comportamiento de actualización.

Crear la configuración de la aplicación

En este paso, crea un espacio de nombres y un ConfigMap a partir de valores literales. Un ConfigMap almacena claves de texto bajo data; no las cifra.

Primero, ve al espacio de trabajo del laboratorio. El espacio de nombres mantiene juntos todos los objetos de este ejemplo y evita que se seleccione accidentalmente una configuración con el mismo nombre perteneciente a otro laboratorio.

cd /home/labex/project/application-config
kubectl create namespace practice-config
kubectl create configmap app-settings -n practice-config --from-literal=APP_MODE=production --from-literal=LOG_LEVEL=info
kubectl get configmap app-settings -n practice-config -o yaml

Cada --from-literal=KEY=VALUE añade una entrada. La salida YAML muestra las claves almacenadas y resulta adecuada para configuraciones no sensibles.

Busca APP_MODE y LOG_LEVEL bajo data:. Kubernetes almacena los valores deseados, pero todavía no hay ningún consumidor; crear un ConfigMap por sí solo no modifica ningún Pod.

La parte relevante de la salida es deliberadamente texto sin formato:

data:
  APP_MODE: production
  LOG_LEVEL: info
kind: ConfigMap

Campos como uid, resourceVersion y creationTimestamp son generados por Kubernetes y serán diferentes. En este paso, lee únicamente data, ya que contiene la configuración que proporcionaste.

Crear un Secret de forma segura

En este paso, crea un Secret sin colocar su valor en un archivo YAML. Los datos de un Secret se codifican en base64 en la API; no quedan cifrados simplemente porque el tipo de objeto sea Secret. El control de acceso basado en roles (RBAC) y el cifrado en reposo siguen siendo importantes.

generic indica que se trata de un Secret común de clave y valor, en lugar de un Secret especializado para TLS o para extraer imágenes. Las comillas alrededor de la contraseña evitan que el intérprete de comandos procese los signos de puntuación si el valor contiene caracteres especiales.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=appuser --from-literal=password='practice-pass'
kubectl get secret app-credentials -n practice-config
kubectl get secret app-credentials -n practice-config -o jsonpath='{.data.username}' | base64 --decode; echo

La última canalización decodifica únicamente el nombre de usuario como demostración. Evita mostrar contraseñas en las salidas operativas habituales.

El carácter de tubería envía el resultado codificado de JSONPath a base64 --decode. El punto y coma inicia un echo final, que añade un salto de línea después del valor decodificado para que el siguiente indicador del intérprete siga siendo legible. Ver appuser demuestra cómo funciona la codificación; también deja claro por qué base64 nunca debe considerarse un límite de seguridad.

El comando intermedio muestra metadatos sin revelar ninguno de los valores:

NAME              TYPE     DATA   AGE
app-credentials   Opaque   2      ...
appuser

DATA=2 significa que el Secret contiene dos claves; no indica un número de bytes. Opaque es el tipo de API utilizado para un Secret común de clave y valor.

Inyectar la configuración en un Pod

En este paso, utiliza los valores del ConfigMap como variables de entorno y monta el Secret como archivos de solo lectura.

Antes de crear el Pod, separa las dos vías de entrega en el manifiesto:

  • envFrom convierte cada clave del ConfigMap en una variable de entorno del contenedor.
  • volumes conecta el Secret con el Pod, mientras que volumeMounts determina dónde aparece ese volumen dentro de este contenedor concreto.

El bucle del Pod no realiza ninguna tarea de aplicación; simplemente mantiene el contenedor en ejecución para que puedas inspeccionar ambas vías.

cd /home/labex/project/application-config
cat <<'EOF' > configured-app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: configured-app
  namespace: practice-config
spec:
  containers:
    - name: app
      image: busybox:1.36
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "while true; do sleep 30; done"]
      envFrom:
        - configMapRef:
            name: app-settings
      volumeMounts:
        - name: credentials
          mountPath: /etc/app-credentials
          readOnly: true
  volumes:
    - name: credentials
      secret:
        secretName: app-credentials
EOF

Aplica el manifiesto y espera a que el Pod esté listo:

kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s

Ahora inspecciona cada vía de entrega desde dentro del contenedor. printenv lee el entorno del proceso, mientras que ls muestra los archivos proyectados por el volumen del Secret:

kubectl exec configured-app -n practice-config -- printenv APP_MODE LOG_LEVEL
kubectl exec configured-app -n practice-config -- ls -l /etc/app-credentials

envFrom importa todas las claves del ConfigMap. El volumen del Secret crea un archivo por cada clave y se marca como de solo lectura dentro del contenedor.

La salida esperada del entorno es production, seguida de info. El directorio debe contener archivos llamados username y password; los nombres de archivo corresponden a las claves del Secret.

En el contenedor real, el listado será similar a este:

production
info
total 0
lrwxrwxrwx ... password -> ..data/password
lrwxrwxrwx ... username -> ..data/username

Las entradas son enlaces simbólicos administrados por Kubernetes. Esta indirección permite que kubelet cambie los datos montados a una versión nueva sin reescribir un archivo mientras la aplicación lo está leyendo. La aplicación puede seguir abriendo las rutas conocidas /etc/app-credentials/username y /etc/app-credentials/password.

Actualizar los datos montados del Secret

En este paso, actualiza el Secret y observa el archivo montado. kubectl create ... --dry-run=client -o yaml | kubectl apply -f - genera localmente el YAML deseado y lo envía directamente a apply; - significa entrada estándar.

Este patrón resulta útil para actualizar un objeto generado de forma imperativa sin eliminarlo. El lado izquierdo imprime un manifiesto completo del Secret y el derecho envía ese manifiesto al servidor de la API.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=serviceuser --from-literal=password='rotated-pass' --dry-run=client -o yaml | kubectl apply -f -

Como el Secret se creó originalmente con kubectl create, la primera actualización puede incluir esta advertencia antes de la línea de éxito:

Warning: resource secrets/app-credentials is missing the kubectl.kubernetes.io/last-applied-configuration annotation ...
secret/app-credentials configured

La advertencia indica que apply no dispone de una instantánea declarativa anterior con la que comparar. Kubernetes añade automáticamente la anotación durante esta actualización, por lo que configured confirma que la operación se realizó correctamente; no es necesario eliminar y volver a crear el Secret.

Kubelet actualiza de forma asíncrona el contenido proyectado del volumen del Secret. Repite la comprobación hasta que aparezca el nuevo nombre de usuario:

current_username=""
for attempt in $(seq 1 24); do
  current_username=$(kubectl exec configured-app -n practice-config -- cat /etc/app-credentials/username)
  if [ "$current_username" = "serviceuser" ]; then
    break
  fi
  sleep 5
done
echo "$current_username"
test "$current_username" = "serviceuser"

El bucle reintenta durante un máximo de dos minutos porque la proyección del volumen es eventualmente consistente. En el entorno probado, el objeto de la API cambió de inmediato, pero el archivo montado tardó más de un minuto en actualizarse.

$(...) es una sustitución de comandos: el intérprete ejecuta seq 1 24 y utiliza sus números como valores del bucle. En cada iteración, guarda el contenido actual del archivo en current_username; break detiene el bucle en la primera coincidencia y sleep 5 evita consultar continuamente la API. El test final devuelve un error si el valor nunca cambia, de modo que el comando no puede continuar silenciosamente con datos obsoletos. La salida correcta es serviceuser.

Actualizar la configuración del entorno

En este paso, actualiza LOG_LEVEL y compáralo con el valor del Pod en ejecución. Las variables de entorno quedan fijadas cuando se inicia un contenedor, por lo que cambiar el ConfigMap no modifica el entorno del proceso actual.

El parche de combinación cambia únicamente una entrada dentro de data. Leer de inmediato el proceso en ejecución demuestra una diferencia importante con respecto a un volumen montado:

kubectl patch configmap app-settings -n practice-config --type=merge -p '{"data":{"LOG_LEVEL":"debug"}}'
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

El Pod todavía muestra info. Vuelve a crearlo a partir del manifiesto guardado para que consuma el nuevo valor:

configmap/app-settings patched
info

Este resultado es esperado y no indica que el parche haya fallado: el ConfigMap contiene debug, pero el proceso que ya estaba en ejecución conserva el entorno creado durante el arranque.

cd /home/labex/project/application-config
kubectl delete pod configured-app -n practice-config --wait=true
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

El Pod reemplazado muestra debug. Esta diferencia explica por qué las cargas de trabajo de producción suelen activar un nuevo despliegue después de cambiar una configuración basada en variables de entorno.

pod "configured-app" deleted
pod/configured-app created
pod/configured-app condition met
debug

El manifiesto sigue haciendo referencia al ConfigMap por su nombre; no se copió ningún valor en configured-app.yaml. Al volver a crear el Pod, kubelet resuelve de nuevo esa referencia e inyecta los datos más recientes.

Resumen

Separaste la configuración de la aplicación y las credenciales de la imagen, consumiste los datos del ConfigMap como variables de entorno, montaste los datos del Secret como archivos, rotaste un Secret proyectado y volviste a crear un Pod para actualizar la configuración basada en variables de entorno.