Cómo limpiar ConfigMaps y Secrets huérfanos en Kubernetes

Kubernetes Housekeeping: Cómo Limpiar ConfigMaps y Secrets Huérfanos

El mantenimiento de un cluster Kubernetes en producción va mucho más allá de desplegar aplicaciones y escalar pods. Con el tiempo, uno de los problemas más silenciosos que acumula cualquier cluster en uso real es el de los recursos huérfanos: ConfigMaps y Secrets que en algún momento sirvieron para algo, pero que hoy llevan semanas o meses sin ser referenciados por ningún workload activo.

Kubernetes no elimina automáticamente ConfigMaps ni Secrets cuando se borra el Deployment, StatefulSet o Job que los utilizaba. El resultado es predecible: acumulación progresiva de basura en etcd, superficie de ataque innecesaria con credenciales obsoletas todavía accesibles, y un ruido operativo que complica cualquier diagnóstico.

Este artículo cubre las causas habituales de este problema, métodos de detección —tanto manuales como automatizados—, estrategias de prevención y, sobre todo, cómo ejecutar la limpieza de forma segura en entornos reales.


Por qué se acumulan ConfigMaps y Secrets huérfanos

Antes de limpiar, conviene entender cómo se llega a esta situación. No es un caso de negligencia puntual: es el resultado natural de varios patrones de trabajo habituales.

Actualizaciones de aplicaciones sin limpieza del historial

Cada vez que se actualiza la configuración de una aplicación —cambio de variables de entorno, rotación de credenciales, nueva URL de base de datos— es habitual crear un nuevo ConfigMap o Secret con nombre versionado. El problema: el anterior no se borra.

Despliegues eliminados que dejan su configuración atrás

Cuando un equipo hace kubectl delete deployment myapp, el Deployment desaparece, pero los ConfigMaps y Secrets asociados permanecen. Kubernetes no establece esa relación de ciclo de vida salvo que se configure explícitamente.

Rollouts fallidos

Un rollout que falla a mitad puede dejar una versión de ConfigMap preparada para una versión de la aplicación que nunca llegó a arrancar. Esa configuración queda flotando en el namespace indefinidamente.

Flujos de trabajo en desarrollo

Los entornos de desarrollo y staging son especialmente propensos: pruebas rápidas, experimentos, recursos creados a mano sin etiquetas ni propietario declarado. Muy pocos equipos tienen disciplina para limpiar detrás de ellos.

Pipelines CI/CD con nombres únicos por hash

Herramientas como Helm y Kustomize pueden generar ConfigMaps con sufijos de hash basados en el contenido (myapp-config-7d4f9c). Cada actualización genera un nuevo recurso. Sin un mecanismo de pruning activo, los anteriores se acumulan indefinidamente.


El impacto real de no hacer housekeeping

No es solo una cuestión de orden. Los recursos huérfanos tienen consecuencias concretas:

Seguridad degradada. Un Secret huérfano puede contener credenciales antiguas, claves de API o certificados que ya no deberían ser accesibles para ningún proceso. Siguen siendo válidos en muchos casos, y siguen ocupando espacio en etcd —que no está cifrado por defecto en todos los clusters.

Consumo de etcd. Cada ConfigMap y Secret ocupa espacio en la base de datos de estado del cluster. etcd tiene límites de tamaño (el valor por defecto es 2 GB), y alcanzarlo en producción es una emergencia operativa real. Los recursos huérfanos aceleran ese límite sin aportar ningún valor.

Ruido operativo. Un namespace con cien ConfigMaps activos y doscientos huérfanos es un namespace difícil de operar. Localizar la configuración correcta, depurar un problema de arranque, revisar qué credenciales están en uso… todo se complica.

Coste de cognitive load para el equipo. No es trivial: los equipos que trabajan en namespaces desordenados cometen más errores, tardan más en diagnosticar incidencias y tienen más resistencia a hacer cambios.


Detección: encontrar los recursos huérfanos

Detección manual con kubectl y jq

El enfoque básico consiste en cruzar la lista de ConfigMaps y Secrets de un namespace contra las referencias activas en los Pods en ejecución.

Script para ConfigMaps:

#!/bin/bash
# detect-orphaned-configmaps.sh
# Identifica ConfigMaps no referenciados por ningún Pod activo

NAMESPACE=${1:-default}

echo "Comprobando ConfigMaps huérfanos en namespace: $NAMESPACE"
echo "---"

# Obtener todos los ConfigMaps del namespace
CONFIGMAPS=$(kubectl get configmaps -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}')

for cm in $CONFIGMAPS; do
    # Omitir kube-root-ca.crt — gestionado por el sistema
    if [[ "$cm" == "kube-root-ca.crt" ]]; then
        continue
    fi

    # Comprobar si algún Pod referencia este ConfigMap
    REFERENCED=$(kubectl get pods -n $NAMESPACE -o json | \
        jq -r --arg cm "$cm" '.items[] |
        select(
            (.spec.volumes[]?.configMap.name == $cm) or
            (.spec.containers[].env[]?.valueFrom.configMapKeyRef.name == $cm) or
            (.spec.containers[].envFrom[]?.configMapRef.name == $cm)
        ) | .metadata.name' | head -1)

    if [[ -z "$REFERENCED" ]]; then
        echo "Huérfano: $cm"
    fi
done

Script para Secrets:

#!/bin/bash
# detect-orphaned-secrets.sh

NAMESPACE=${1:-default}

echo "Comprobando Secrets huérfanos en namespace: $NAMESPACE"
echo "---"

SECRETS=$(kubectl get secrets -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}')

for secret in $SECRETS; do
    # Omitir tokens de service account y Secrets del sistema
    SECRET_TYPE=$(kubectl get secret $secret -n $NAMESPACE -o jsonpath='{.type}')
    if [[ "$SECRET_TYPE" == "kubernetes.io/service-account-token" ]]; then
        continue
    fi

    # Comprobar si algún Pod referencia este Secret
    REFERENCED=$(kubectl get pods -n $NAMESPACE -o json | \
        jq -r --arg secret "$secret" '.items[] |
        select(
            (.spec.volumes[]?.secret.secretName == $secret) or
            (.spec.containers[].env[]?.valueFrom.secretKeyRef.name == $secret) or
            (.spec.containers[].envFrom[]?.secretRef.name == $secret) or
            (.spec.imagePullSecrets[]?.name == $secret)
        ) | .metadata.name' | head -1)

    if [[ -z "$REFERENCED" ]]; then
        echo "Huérfano: $secret"
    fi
done

Importante: Estos scripts comprueban únicamente Pods en ejecución. Un ConfigMap puede estar referenciado en un Deployment cuyo Pod está temporalmente caído, en un CronJob que no ha disparado recientemente, o en un template de Job. Antes de borrar cualquier recurso, es imprescindible cruzar también contra Deployments, StatefulSets, DaemonSets, CronJobs y Jobs.

Herramientas automatizadas

Para clusters con muchos namespaces o workflows de auditoría recurrente, los scripts manuales no escalan. Hay herramientas específicas para esta tarea.

Kor

Kor es una herramienta de código abierto diseñada específicamente para detectar recursos sin usar en Kubernetes. Tiene soporte nativo para ConfigMaps, Secrets, ServiceAccounts, Services y varios recursos más.

# Instalación en macOS
brew install kor

# Escanear ConfigMaps y Secrets sin usar en un namespace
kor all --namespace production --output json

# Solo ConfigMaps
kor configmap --namespace production

# Solo Secrets, excluyendo namespaces de sistema
kor secret --namespace production --exclude-namespaces kube-system,kube-public

La salida en formato JSON es especialmente útil para integrar el análisis en pipelines de CI/CD o dashboards de monitorización.

Popeye

Popeye genera informes completos de salud del cluster, incluyendo recursos huérfanos, configuraciones incorrectas, y buenas prácticas no seguidas.

# Instalación
brew install derailed/popeye/popeye

# Escaneo completo con salida en JSON
popeye --output json --save

# Escaneo centrado en un namespace
popeye --namespace production

Popeye es especialmente valioso para una primera auditoría de un cluster heredado, porque da una visión global del estado de salud en múltiples dimensiones, no solo recursos huérfanos.


Prevención: evitar que se acumulen en primer lugar

Detectar y limpiar es necesario, pero el objetivo real es que los recursos tengan un ciclo de vida gestionado desde el principio. Hay varias estrategias complementarias.

Owner References: limpieza automática en cascada

El mecanismo más potente de Kubernetes para gestión de ciclo de vida es ownerReferences. Si un ConfigMap tiene como propietario un Deployment, cuando ese Deployment se borra, el garbage collector de Kubernetes elimina automáticamente el ConfigMap.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: production
  ownerReferences:
    - apiVersion: apps/v1
      kind: Deployment
      name: myapp
      uid: d9607e19-f88f-11e6-a518-42010a800195
      controller: true
      blockOwnerDeletion: true
data:
  app.properties: |
    database.url=postgres://db:5432

Las herramientas de gestión de paquetes como Helm y Kustomize gestionan esto automáticamente para los recursos que crean. Si estás aplicando manifiestos directamente con kubectl apply, es responsabilidad del equipo añadir estas referencias.

Etiquetado consistente: visibilidad y control

Las etiquetas son la base de cualquier operación de gestión masiva en Kubernetes. Sin etiquetas consistentes, no hay forma de hacer consultas selectivas ni de atribuir un recurso a su propietario.

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-gateway-config-v2
  labels:
    app: api-gateway
    component: configuration
    version: v2
    managed-by: argocd
    owner: platform-team
data:
  config.yaml: |
    # configuración aquí

Con etiquetas bien definidas, las operaciones de limpieza se simplifican enormemente:

# Todos los ConfigMaps de una aplicación
kubectl get configmaps -l app=api-gateway

# Eliminar versiones antiguas
kubectl delete configmaps -l app=api-gateway,version=v1

Conviene definir un estándar de etiquetado a nivel de organización y hacerlo cumplir con políticas de admisión (ver sección de OPA Gatekeeper más adelante).

GitOps con pruning habilitado

Las herramientas GitOps como ArgoCD mantienen el estado del cluster sincronizado con el repositorio Git. Cuando se habilita el pruning, cualquier recurso que desaparezca del repositorio también se elimina del cluster automáticamente.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
spec:
  syncPolicy:
    automated:
      prune: true      # Eliminar recursos que no estén en Git
      selfHeal: true

Esta es probablemente la estrategia de prevención más eficaz para equipos que trabajan con GitOps: el cluster siempre refleja exactamente lo que está en Git, sin residuos.

Kustomize configMapGenerator con pruning

Kustomize tiene un generador nativo de ConfigMaps que añade un sufijo de hash basado en el contenido. Esto garantiza que cualquier cambio en la configuración genere un nuevo ConfigMap (y que los Pods hagan rolling update automáticamente). Combinado con --prune, las versiones antiguas se eliminan al aplicar la nueva.

kubectl apply --prune -k ./overlays/production \
  -l app=myapp

Este patrón es especialmente útil en pipelines CI/CD donde la configuración cambia frecuentemente.

Resource Quotas como mecanismo de presión

Establecer cuotas de recursos por namespace crea una presión natural para que los equipos mantengan el orden:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: config-quota
  namespace: production
spec:
  hard:
    configmaps: "50"
    secrets: "50"

Cuando un equipo alcanza el límite, se ve obligado a revisar qué hay en el namespace y eliminar lo que ya no sirve. No es la solución más elegante, pero funciona como red de seguridad.


Ejecución de la limpieza: de lo manual a lo automatizado

Limpieza manual puntual

Para una primera limpieza de un cluster heredado, el proceso recomendado es:

# 1. Generar listado de candidatos a eliminar
./detect-orphaned-configmaps.sh production > orphaned-cms.txt
cat orphaned-cms.txt

# 2. Revisar el listado manualmente antes de actuar

# 3. Eliminar los confirmados
for cm in $(cat orphaned-cms.txt | grep "Huérfano:" | awk '{print $2}'); do
    kubectl delete configmap $cm -n production
done

La revisión manual es imprescindible en la primera ejecución. Los scripts de detección basados en Pods activos pueden tener falsos positivos para recursos referenciados por workloads caídos temporalmente.

Backup antes de cualquier borrado

Antes de eliminar nada en producción, siempre:

# Backup de todos los ConfigMaps del namespace
kubectl get configmap -n production -o yaml > cm-backup-$(date +%Y%m%d).yaml

# Solo eliminar ConfigMaps con más de 30 días de antigüedad
kubectl get configmap -n production -o json | \
  jq -r --arg date "$(date -d '30 days ago' -u +%Y-%m-%dT%H:%M:%SZ)" \
  '.items[] | select(.metadata.creationTimestamp < $date) | .metadata.name' | \
  while read cm; do
    echo "Candidato a eliminar: $cm (creado: $(kubectl get cm $cm -n production -o jsonpath='{.metadata.creationTimestamp}'))"
    # Descomentar para eliminar realmente:
    # kubectl delete configmap $cm -n production
  done

El filtro por antigüedad es una capa de seguridad adicional: un ConfigMap reciente tiene más probabilidades de estar en uso aunque no aparezca referenciado en ningún Pod activo en ese momento.

CronJob para limpieza periódica automatizada

Una vez que el proceso manual está validado, automatizarlo con un CronJob es el paso natural:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: configmap-cleanup
  namespace: kube-system
spec:
  schedule: "0 2 * * 0"  # Domingos a las 2:00 AM
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: cleanup-sa
          containers:
          - name: cleanup
            image: bitnami/kubectl:latest
            command:
            - /bin/bash
            - -c
            - |
              echo "Iniciando limpieza de ConfigMaps..."

              for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
                echo "Revisando namespace: $ns"

                # ConfigMaps referenciados por workloads activos
                REFERENCED_CMS=$(kubectl get deploy,sts,ds -n $ns -o json | \
                  jq -r '.items[].spec.template.spec |
                  [.volumes[]?.configMap.name,
                   .containers[].env[]?.valueFrom.configMapKeyRef.name,
                   .containers[].envFrom[]?.configMapRef.name] |
                  .[] | select(. != null)' | sort -u)

                ALL_CMS=$(kubectl get cm -n $ns -o jsonpath='{.items[*].metadata.name}')

                for cm in $ALL_CMS; do
                  if [[ "$cm" == "kube-root-ca.crt" ]]; then
                    continue
                  fi

                  if ! echo "$REFERENCED_CMS" | grep -q "^$cm$"; then
                    echo "Eliminando ConfigMap huérfano: $cm en namespace: $ns"
                    kubectl delete cm $cm -n $ns
                  fi
                done
              done
          restartPolicy: OnFailure

El CronJob necesita los permisos adecuados para listar y borrar recursos en todos los namespaces:

# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: cleanup-sa
  namespace: kube-system
---
# ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cleanup-role
rules:
- apiGroups: [""]
  resources: ["configmaps", "secrets", "namespaces"]
  verbs: ["get", "list", "delete"]
- apiGroups: ["apps"]
  resources: ["deployments", "statefulsets", "daemonsets"]
  verbs: ["get", "list"]
---
# ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cleanup-binding
subjects:
- kind: ServiceAccount
  name: cleanup-sa
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: cleanup-role
  apiGroup: rbac.authorization.k8s.io

Integración en CI/CD

Para pipelines que despliegan con versionado explícito (etiquetas Git, versiones semánticas), la limpieza se puede integrar directamente como paso post-deploy:

# Ejemplo en GitLab CI
cleanup_old_configs:
  stage: post-deploy
  image: bitnami/kubectl:latest
  script:
    - |
      # Eliminar ConfigMaps de versiones antiguas tras despliegue exitoso
      kubectl delete configmap -n production \
        -l app=myapp,version!=${CI_COMMIT_TAG}

    - |
      # Conservar solo las últimas 3 versiones por timestamp
      kubectl get configmap -n production \
        -l app=myapp \
        --sort-by=.metadata.creationTimestamp \
        -o name | head -n -3 | xargs -r kubectl delete -n production
  only:
    - tags
  when: on_success

Este enfoque es muy limpio: cada despliegue exitoso elimina las versiones anteriores, manteniendo un historial mínimo pero suficiente para un rollback rápido.


Patrones avanzados para clusters en producción

OPA Gatekeeper: forzar etiquetado desde admisión

La forma más efectiva de garantizar que todos los ConfigMaps tengan etiquetas de ciclo de vida es hacerlo obligatorio en el momento de su creación, a nivel de admission webhook. OPA Gatekeeper permite definir estas políticas como código:

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: configmaprequiredlabels
spec:
  crd:
    spec:
      names:
        kind: ConfigMapRequiredLabels
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package configmaprequiredlabels

        violation[{"msg": msg}] {
          input.review.kind.kind == "ConfigMap"
          not input.review.object.metadata.labels["app"]
          msg := "Los ConfigMaps deben tener la etiqueta 'app' para gestión de ciclo de vida"
        }

        violation[{"msg": msg}] {
          input.review.kind.kind == "ConfigMap"
          not input.review.object.metadata.labels["owner"]
          msg := "Los ConfigMaps deben tener la etiqueta 'owner' para gestión de ciclo de vida"
        }

Con esta política activa, ningún ConfigMap sin las etiquetas app y owner puede crearse en el cluster. Esto resuelve el problema en origen.

Monitorización con Prometheus

Para clusters grandes, la visibilidad continua del estado de recursos huérfanos es fundamental. Un exporter personalizado puede exponer métricas para Prometheus:

apiVersion: v1
kind: ConfigMap
metadata:
  name: orphan-detection-exporter
data:
  script.sh: |
    #!/bin/bash
    # Exponer métricas para scraping de Prometheus
    while true; do
      echo "# HELP k8s_orphaned_configmaps Número de ConfigMaps huérfanos"
      echo "# TYPE k8s_orphaned_configmaps gauge"

      for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
        count=$(./detect-orphaned-configmaps.sh $ns | grep -c "Huérfano:")
        echo "k8s_orphaned_configmaps{namespace=\"$ns\"} $count"
      done

      sleep 300  # Actualizar cada 5 minutos
    done

Con las métricas disponibles en Prometheus, se puede configurar una alerta que notifique cuando el número de ConfigMaps huérfanos supera un umbral:

groups:
- name: kubernetes-housekeeping
  rules:
  - alert: ConfigMapsHuerfanosExcesivos
    expr: k8s_orphaned_configmaps > 20
    for: 24h
    labels:
      severity: warning
    annotations:
      summary: "Alto número de ConfigMaps huérfanos en {{ $labels.namespace }}"
      description: "El namespace {{ $labels.namespace }} tiene {{ $value }} ConfigMaps huérfanos"

Políticas de limpieza en flota multi-cluster

En entornos con múltiples clusters, gestionar las políticas de limpieza de forma centralizada es un requisito. Crossplane permite definir estas políticas como recursos declarativos que se aplican a toda la flota:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: cluster-cleanup-policy
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1
    kind: ClusterCleanupPolicy
  resources:
    - name: cleanup-cronjob
      base:
        apiVersion: kubernetes.crossplane.io/v1alpha1
        kind: Object
        spec:
          forProvider:
            manifest:
              apiVersion: batch/v1
              kind: CronJob

Este patrón es especialmente valioso cuando el equipo de plataforma necesita garantizar que todos los clusters de la organización siguen las mismas políticas de mantenimiento, sin depender de que cada equipo recuerde configurarlo manualmente.


Plan de implementación

Acciones inmediatas (hoy)

  1. Auditar el estado actual. Ejecutar los scripts de detección en todos los namespaces relevantes. Guardar el resultado.
  2. Hacer backup. Exportar todos los ConfigMaps y Secrets a YAML antes de tocar nada.
  3. Eliminar los obvios. Recursos con más de 90 días de antigüedad sin ninguna referencia en ningún workload son candidatos seguros. Eliminar con revisión manual previa.

Corto plazo (1-4 semanas)

  1. Definir estándar de etiquetado. Acordar con los equipos qué etiquetas son obligatorias (app, owner, version, managed-by).
  2. Añadir owner references a los manifiestos existentes que no las tengan.
  3. Desplegar CronJobs de detección. Empezar solo con detección (sin borrado), para generar visibilidad.
  4. Integrar limpieza en CI/CD para los pipelines con versionado explícito.

Medio plazo (1-3 meses)

  1. Adoptar GitOps con pruning habilitado en ArgoCD o Flux.
  2. Implementar OPA Gatekeeper para forzar etiquetado en admisión.
  3. Configurar alertas en Prometheus sobre métricas de recursos huérfanos.
  4. Establecer quotas por namespace como mecanismo de presión secundario.

Ongoing

  • Revisar semanalmente los informes de Popeye o Kor en los namespaces clave.
  • Incluir revisión de recursos huérfanos en la planificación de sprints del equipo de plataforma.
  • Incorporar la gestión de ciclo de vida de ConfigMaps y Secrets en el onboarding de nuevos desarrolladores.

Conclusión

Los ConfigMaps y Secrets huérfanos son uno de esos problemas de Kubernetes que parecen menores hasta que no lo son: un cluster con años de historial puede tener miles de recursos obsoletos, consumiendo etcd, exponiendo credenciales que deberían estar rotadas, y generando confusión operativa en cada incidencia.

La buena noticia es que el problema tiene solución clara. No se trata de una operación de limpieza masiva única —eso solo resuelve el síntoma— sino de un enfoque por capas que combina:

  • Prevención estructural mediante owner references, etiquetado consistente y GitOps con pruning.
  • Detección continua con herramientas como Kor, Popeye o un exporter Prometheus personalizado.
  • Limpieza controlada con scripts auditados, backups previos, y criterios de antigüedad antes de borrar.

Un cluster bien mantenido no es el que nunca acumula residuos —es imposible evitarlo en entornos dinámicos— sino el que tiene mecanismos para detectarlos, contenerlos y eliminarlos de forma sistemática y segura.

La gestión del ciclo de vida de los recursos de configuración es una práctica de ingeniería de plataforma, no una tarea de limpieza puntual. Cuanto antes se integre en los procesos del equipo, menos deuda técnica operativa se acumula.