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)
- Auditar el estado actual. Ejecutar los scripts de detección en todos los namespaces relevantes. Guardar el resultado.
- Hacer backup. Exportar todos los ConfigMaps y Secrets a YAML antes de tocar nada.
- 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)
- Definir estándar de etiquetado. Acordar con los equipos qué etiquetas son obligatorias (
app,owner,version,managed-by). - Añadir owner references a los manifiestos existentes que no las tengan.
- Desplegar CronJobs de detección. Empezar solo con detección (sin borrado), para generar visibilidad.
- Integrar limpieza en CI/CD para los pipelines con versionado explícito.
Medio plazo (1-3 meses)
- Adoptar GitOps con pruning habilitado en ArgoCD o Flux.
- Implementar OPA Gatekeeper para forzar etiquetado en admisión.
- Configurar alertas en Prometheus sobre métricas de recursos huérfanos.
- 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.