ConfigMaps y Secrets: Gestión de parámetros de modelos

Lectura
20 min~12 min lectura

Introducción: Separando Configuración del Código en el Mundo ML

En el desarrollo tradicional de modelos de Machine Learning, es común encontrar parámetros, rutas de archivos, credenciales y configuraciones de hiperparámetros incrustados directamente en el código fuente o en scripts auxiliares. Este enfoque, aunque funcional en un entorno local, se convierte en una pesadilla operativa al llevar el modelo a producción en un orquestador como Kubernetes. Imagina tener que reconstruir y redespelgar la imagen de tu contenedor Docker cada vez que necesitas cambiar el umbral de clasificación de tu modelo, la dirección de un servidor de características o una clave de API. Este proceso es lento, propenso a errores y rompe los principios de la entrega continua.

Aquí es donde entran en juego dos objetos fundamentales de Kubernetes: ConfigMaps y Secrets. Estos recursos nos permiten desacoplar la configuración y la información sensible de la definición de la aplicación (el Pod o Deployment), inyectándola de manera dinámica en los contenedores en tiempo de ejecución. Para un ingeniero de ML, esto significa poder gestionar el entorno del modelo (parámetros, endpoints de servicios, configuraciones de logging) y sus secretos (tokens, claves de acceso a bases de datos, credenciales de cloud) de forma centralizada, segura y declarativa, sin tocar una sola línea del código del modelo empaquetado en Docker.

Dominar el uso de ConfigMaps y Secrets es lo que separa un despliegue básico de uno profesional, resiliente y preparado para escalar. Permite crear una sola imagen de contenedor genérica para tu modelo que puede ser configurada para múltiples entornos (desarrollo, staging, producción) simplemente cambiando los objetos de configuración que se montan en ella, un patrón esencial para las prácticas modernas de MLOps.

Concepto Clave: ConfigMaps y Secrets Desmitificados

Piensa en un ConfigMap como el "archivo de configuración inteligente" de tu aplicación en Kubernetes. No es más que un diccionario clave-valor (o un conjunto de archivos de configuración) que Kubernetes almacena en su base de datos interna (etcd). Puedes llenar este diccionario con cualquier dato no sensible que tu aplicación necesite para configurarse: URLs de servicios externos, nombres de buckets de almacenamiento, banderas de funcionalidad (feature flags), hiperparámetros del modelo (como la tasa de aprendizaje o el número de estimadores), o incluso archivos de configuración completos en formato JSON, YAML o XML. La magia está en que este ConfigMap vive fuera de tu contenedor, y Kubernetes puede inyectar sus datos en el contenedor como variables de entorno o como archivos en un directorio específico del sistema de archivos del Pod.

Por otro lado, un Secret es conceptualmente idéntico a un ConfigMap: también es un objeto clave-valor. La diferencia crucial es que Kubernetes trata los Secrets con más cuidado. Están diseñados para contener información sensible como contraseñas, tokens OAuth, claves SSH o claves de API. Kubernetes se esfuerza por no escribir datos de Secrets en el disco (aunque puede hacerlo bajo petición) y, cuando lo hace, intenta mantenerlos encriptados. En la práctica, los valores dentro de un Secret están codificados en base64, lo que no es encriptación, pero añade una capa de ofuscación que evita que se lean de forma casual. Es fundamental entender que los Secrets están destinados a la *conveniencia*, no a la *seguridad absoluta* en clusters no configurados específicamente para ello. Su principal ventaja es separar la información sensible del código y de las definiciones de los Pods.

Tip Crítico: Nunca confíes en los Secrets de Kubernetes para almacenar datos altamente sensibles (como claves maestras de cifrado) sin habilitar el cifrado en reposo para etcd y gestionar los controles de acceso RBAC de forma estricta. Considera soluciones externas como Vault, AWS Secrets Manager o Azure Key Vault para esos casos, usando los Secrets de Kubernetes solo para almacenar las credenciales de acceso a esos servicios más seguros.

Cómo Funciona en la Práctica: Un Flujo Paso a Paso

El flujo de trabajo típico para integrar ConfigMaps y Secrets en el despliegue de un modelo de ML sigue una serie de pasos lógicos y repetibles. Primero, debes identificar qué partes de tu aplicación son configurables y cuáles son sensibles. Para un servicio de inferencia de un modelo, los parámetros configurables podrían incluir el puerto en el que escucha el servidor, la URL del endpoint de un servicio de monitoreo, el nombre del modelo a cargar desde un almacenamiento, o los hiperparámetros de post-procesamiento (como un umbral de confianza). La información sensible podría ser una clave de acceso a un bucket de S3 donde está guardado el modelo serializado, o un token de API para enviar métricas a una plataforma externa.

Una vez identificados, el siguiente paso es crear los objetos de Kubernetes. Esto se hace de forma declarativa, usualmente mediante archivos YAML. Creas un archivo config-modelo.yaml que define un ConfigMap con claves como MODEL_THRESHOLD y FEATURE_SERVICE_URL. De forma similar, creas un archivo secret-modelo.yaml que define un Secret, donde los valores (como tu clave de S3) deben ser previamente codificados en base64. Luego, aplicas estos archivos al cluster con kubectl apply -f config-modelo.yaml.

El paso final es modificar el Deployment o el Pod de tu modelo para consumir estos objetos. En el spec del contenedor, defines variables de entorno que obtienen su valor desde el ConfigMap o el Secret usando valueFrom. Alternativamente, puedes montar todo el ConfigMap o Secret como un volumen, creando un archivo en el sistema de archivos del contenedor por cada clave-valor. Cuando Kubernetes programa una nueva instancia (Pod) de tu modelo, leerá las definiciones, obtendrá los datos actuales del ConfigMap y del Secret, y los inyectará en el contenedor antes de que se inicie. Tu aplicación, al arrancar, leerá estas variables de entorno o archivos y se configurará dinámicamente.

Código en Acción: Despliegue de un Clasificador Configurable

Vamos a construir un ejemplo completo. Supongamos un servicio de inferencia para un modelo de clasificación de texto. El código de la aplicación (app.py) está diseñado para leer su configuración desde variables de entorno.

1. Código de la Aplicación Python (app.py)

import os
import pickle
import numpy as np
from flask import Flask, request, jsonify
import boto3
from botocore.exceptions import ClientError
import logging

app = Flask(__name__)

# Cargar configuración desde variables de entorno (inyectadas por Kubernetes)
MODEL_FILE_PATH = os.getenv('MODEL_FILE_PATH', 'model.pkl')
MODEL_CONFIDENCE_THRESHOLD = float(os.getenv('MODEL_THRESHOLD', '0.7'))
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
# Credenciales sensibles desde Secrets
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
S3_BUCKET = os.getenv('S3_BUCKET')

# Configurar logging
logging.basicConfig(level=getattr(logging, LOG_LEVEL))
logger = logging.getLogger(__name__)

def load_model():
    """Carga el modelo desde S3 usando credenciales de Secrets."""
    try:
        s3_client = boto3.client(
            's3',
            aws_access_key_id=AWS_ACCESS_KEY_ID,
            aws_secret_access_key=AWS_SECRET_ACCESS_KEY
        )
        # Descargar el modelo desde S3
        s3_client.download_file(S3_BUCKET, MODEL_FILE_PATH, '/tmp/model.pkl')
        with open('/tmp/model.pkl', 'rb') as f:
            model = pickle.load(f)
        logger.info(f"Modelo cargado desde s3://{S3_BUCKET}/{MODEL_FILE_PATH}")
        return model
    except ClientError as e:
        logger.error(f"Error al cargar el modelo desde S3: {e}")
        raise

model = load_model()

@app.route('/predict', methods=['POST'])
def predict():
    data = request.json
    features = np.array(data['features']).reshape(1, -1)
    prediction_proba = model.predict_proba(features)[0]
    
    # Aplicar umbral configurable
    predicted_class = 1 if prediction_proba[1] >= MODEL_CONFIDENCE_THRESHOLD else 0
    confidence = float(prediction_proba[predicted_class])
    
    response = {
        'prediction': predicted_class,
        'confidence': confidence,
        'threshold_used': MODEL_CONFIDENCE_THRESHOLD
    }
    logger.debug(f"Solicitud de predicción procesada: {response}")
    return jsonify(response)

if __name__ == '__main__':
    port = int(os.getenv('PORT', 8080))
    app.run(host='0.0.0.0', port=port)

2. ConfigMap y Secret en YAML

Ahora definimos los objetos de Kubernetes que proporcionarán los valores a las variables de entorno. Primero, codificamos en base64 nuestra clave secreta de AWS (ejemplo: echo -n 'tu-clave-super-secreta' | base64).

# configmap-modelo-clasificador.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ml-model-config
data:
  # Datos de configuración no sensibles
  MODEL_THRESHOLD: "0.75"
  LOG_LEVEL: "INFO"
  PORT: "8080"
  MODEL_FILE_PATH: "models/production/classifier_v2.pkl"
  FEATURE_SERVICE_URL: "http://feature-service.prod.svc.cluster.local:8500"
---
# secret-modelo-clasificador.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ml-model-secrets
type: Opaque
data:
  # Valores codificados en base64
  AWS_ACCESS_KEY_ID: QUtJQUpLM0VaVFlST1VHQUZOT0c=
  AWS_SECRET_ACCESS_KEY: dTdJei9FUmZQMmN6S3RrbHhWTEZSMUNyN2pWa2pVNXVGd2pJYw==
  S3_BUCKET: bXktbWwtbW9kZWxzLXByb2Q=

3. Deployment que Consume ConfigMap y Secret

Finalmente, creamos un Deployment que monta estos objetos en el contenedor como variables de entorno.

# deployment-modelo-clasificador.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: text-classifier-deployment
  labels:
    app: text-classifier
spec:
  replicas: 2
  selector:
    matchLabels:
      app: text-classifier
  template:
    metadata:
      labels:
        app: text-classifier
    spec:
      containers:
      - name: classifier-app
        image: mi-registry/text-classifier:latest
        ports:
        - containerPort: 8080
        env:
          # Variables de entorno desde ConfigMap
          - name: MODEL_THRESHOLD
            valueFrom:
              configMapKeyRef:
                name: ml-model-config
                key: MODEL_THRESHOLD
          - name: LOG_LEVEL
            valueFrom:
              configMapKeyRef:
                name: ml-model-config
                key: LOG_LEVEL
          - name: PORT
            valueFrom:
              configMapKeyRef:
                name: ml-model-config
                key: PORT
          - name: MODEL_FILE_PATH
            valueFrom:
              configMapKeyRef:
                name: ml-model-config
                key: MODEL_FILE_PATH
          # Variables de entorno desde Secret
          - name: AWS_ACCESS_KEY_ID
            valueFrom:
              secretKeyRef:
                name: ml-model-secrets
                key: AWS_ACCESS_KEY_ID
          - name: AWS_SECRET_ACCESS_KEY
            valueFrom:
              secretKeyRef:
                name: ml-model-secrets
                key: AWS_SECRET_ACCESS_KEY
          - name: S3_BUCKET
            valueFrom:
              secretKeyRef:
                name: ml-model-secrets
                key: S3_BUCKET
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Para desplegar todo el sistema, ejecutarías: kubectl apply -f configmap-modelo-clasificador.yaml -f secret-modelo-clasificador.yaml -f deployment-modelo-clasificador.yaml. Cada Pod creado tendrá sus variables de entorno pobladas con los valores actuales de los objetos ConfigMap y Secret. Si actualizas el ConfigMap (por ejemplo, cambias MODEL_THRESHOLD a 0.8), los Pods existentes NO se actualizarán automáticamente. Necesitarías reiniciarlos (por ejemplo, con un rollout restart del deployment) para que capten los nuevos valores, a menos que uses una herramienta como Reloader o hayas implementado un mecanismo de recarga en caliente en tu aplicación.

Errores Comunes y Cómo Evitarlos

Al trabajar con ConfigMaps y Secrets, varios errores recurrentes pueden causar dolores de cabeza en producción. Conocerlos de antemano te ahorrará horas de debugging.

1. Codificar/Decodificar manualmente en base64 de forma incorrecta: Un error clásico al crear Secrets es no codificar correctamente en base64, o hacerlo añadiendo un salto de línea final. Usa echo -n para evitar el salto de línea, o mejor aún, deja que kubectl lo haga por ti con kubectl create secret generic mi-secreto --from-literal=clave=valor y luego obtén el YAML con kubectl get secret mi-secreto -o yaml.

2. Asumir que los cambios en ConfigMaps/Secrets se propagan automáticamente a los Pods en ejecución: Kubernetes no reinicia automáticamente los Pods cuando se actualiza un ConfigMap o Secret al que hacen referencia. El contenedor seguirá viendo los valores antiguos. Para forzar una recarga, debes realizar un kubectl rollout restart deployment/nombre-deployment. Una alternativa es usar volúmenes montados, donde las actualizaciones eventualmente se propagan (aunque el timing no es instantáneo), o usar operadores como Stakater Reloader que monitorean cambios.

3. Almacenar datos verdaderamente sensibles en Secrets sin cifrado adicional: Como se mencionó, los Secrets por defecto no están encriptados en etcd. Cualquiera con acceso de lectura a etcd o a la API de Kubernetes podría recuperarlos. Para entornos de producción, siempre habilita el cifrado en reposo para los Secrets y aplica políticas RBAC estrictas que limiten el acceso a los objetos Secret.

4. Referencias incorrectas en el spec del Pod (nombres o claves que no existen): Si en tu Deployment especificas configMapKeyRef.name: config-inexistente o key: CLAVE_INEXISTENTE, el Pod NO fallará en el despliegue inmediatamente. El contenedor se creará, pero la variable de entorno no se establecerá (o se establecerá como vacía). Esto puede llevar a fallos silenciosos en tu aplicación. Siempre verifica que los objetos referenciados existen y tienen las claves correctas usando kubectl get configmap <nombre> -o yaml.

5. Exponer accidentalmente Secrets en logs o mensajes de error: Tu aplicación debe ser cuidadosa de no imprimir variables de entorno que contengan secretos en los logs, trazas o respuestas de error. Asegúrate de que tu configuración de logging (también manejable por ConfigMap) esté en un nivel apropiado y de filtrar datos sensibles en los manejadores de errores.

Checklist de Dominio

Para verificar que has comprendido y puedes aplicar efectivamente los conceptos de esta lección, asegúrate de poder realizar o comprender cada uno de los siguientes puntos:

  • Diferenciar claramente el uso de un ConfigMap (datos no sensibles) de un Secret (datos sensibles) y justificar cuándo usar cada uno.
  • Crear un ConfigMap y un Secret desde la línea de comandos usando kubectl create y desde un archivo YAML declarativo.
  • Configurar un Deployment para inyectar:
    • Una variable de entorno específica desde una clave de ConfigMap.
    • Una variable de entorno específica desde una clave de Secret.
    • Todo un ConfigMap como un volumen, creando múltiples archivos en el sistema del contenedor.
  • Explicar el ciclo de vida de la actualización: saber que al modificar un ConfigMap/Secret, los Pods existentes no se actualizan automáticamente y conocer al menos una estrategia para forzar la recarga de la configuración (ej: restart del deployment).
  • Listar y aplicar al menos dos medidas de seguridad básicas para trabajar con Secrets (cifrado en reposo, RBAC estricto, evitar codificación manual).
  • Depurar un problema donde un Pod no está recibiendo la configuración esperada, verificando la existencia del objeto, las claves y las referencias en el spec del Pod.
  • Diseñar la configuración para un modelo de ML típico, identificando correctamente qué parámetros irían a un ConfigMap (hiperparámetros, URLs) y cuáles a un Secret (claves de API, contraseñas de BD).
  • Comprender la limitación de tamaño (1 MiB para ConfigMaps y Secrets) y planificar para configuraciones muy grandes (usar volúmenes persistentes o servicios de configuración externos).
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión