Problema

Equipos pequeños que migran de Docker Compose a un clúster Kubernetes (k3s, EKS, GKE, etc.) suelen perder la agilidad que tenían al desarrollar localmente. En un flujo tradicional con Compose, cada ingeniero monta su código en un contenedor, reinicia al instante y ve los logs en la terminal. Cuando la aplicación pasa a Helm y ArgoCD, el proceso se vuelve más rígido: los pipelines generan imágenes versionadas, los charts se despliegan en entornos de staging y prod, y los desarrolladores ya no tienen una forma sencilla de lanzar una instancia aislada para pruebas rápidas.

El patrón que se repite es la falta de un “dev namespace” o de una estrategia que permita a cada ingeniero crear su propio pod, actualizar el código sin reconstruir la imagen y observar logs sin depender del pipeline completo. Sin una solución, el equipo termina bloqueado, esperando a que el equipo de infra abra namespaces o que el CI vuelva a compilar una nueva versión para cada pequeño cambio.

Causa

  1. Modelo de despliegue basado en imágenes inmutables
    Helm y ArgoCD están diseñados para trabajar con imágenes versionadas. Cuando el flujo de trabajo depende de rebuild‑push‑deploy, cualquier cambio de código requiere una nueva imagen, lo que rompe la rapidez del desarrollo local.

  2. Ausencia de namespaces de desarrollo
    Los clústers productivos suelen limitar la creación de namespaces a los equipos de infra. Sin un namespace aislado, los pods de cada ingeniero compiten por recursos y pueden interferir con los entornos de staging.

  3. Falta de configuración de volúmenes en Helm
    Los charts típicos no incluyen una opción para montar el código fuente desde el host del desarrollador. En Docker Compose esto se hace con volumes: - .:/app, pero en Helm se necesita parametrizar el hostPath o usar emptyDir con kubectl cp.

  4. CI/CD rígido
    ArgoCD sincroniza el estado del clúster con el repositorio Git. Si el repositorio solo contiene charts de producción, no hay un camino para que un ingeniero “haga commit” de cambios locales y los vea inmediatamente.

Solución

Diseñar un flujo de trabajo dev‑first que combine Helm, ArgoCD y un namespace dedicado para cada ingeniero. Los pasos clave son:

  1. Crear un namespace de desarrollo genérico
    Un namespace llamado dev o dev‑<username> se crea una sola vez por el equipo de infra. Dentro de él, los recursos pueden ser creados y destruidos libremente.

  2. Parametrizar el chart para modo desarrollo
    Añadir una sección values-dev.yaml que habilite:

    • Un hostPath que apunte a una ruta compartida (por ejemplo, un PVC NFS o un directorio montado en el nodo).
    • Un imagePullPolicy: IfNotPresent para que el pod use la última imagen local sin necesidad de re‑taggear.
    • Un restartPolicy: Always y command: ["sleep", "infinity"] para mantener el contenedor vivo mientras el código se sincroniza.
  3. Utilizar kubectl cp o rsync para sincronizar código
    En lugar de reconstruir la imagen, copiar los archivos al pod en tiempo real. Con kubectl cp se pueden actualizar archivos sin reiniciar el contenedor.

  4. ArgoCD en modo “auto‑sync” solo para producción
    Mantener ArgoCD deshabilitado en el namespace de desarrollo o usar la anotación argocd.argoproj.io/sync-options: Skip para evitar que sobrescriba los cambios locales.

  5. Makefile o script local para lanzar el pod
    Un objetivo make dev que:

    • Instala/actualiza el chart con helm upgrade --install usando values-dev.yaml.
    • Copia el código al pod.
    • Ejecuta kubectl port-forward para exponer puertos al host.
  6. Logs y debugging
    Con kubectl logs -f <pod> y kubectl exec -it <pod> -- /bin/sh el ingeniero tiene acceso inmediato al contenedor, igual que con docker compose up.

Ejemplo de values‑dev.yaml

image:
  repository: myregistry/app
  tag: latest
  pullPolicy: IfNotPresent

volumeMounts:
  - name: source-code
    mountPath: /app

volumes:
  - name: source-code
    hostPath:
      path: /mnt/dev-share/<username>
      type: Directory

command: ["sleep", "infinity"]

Script Makefile simplificado

DEV_NS=dev-$(USER)
CHART_PATH=charts/app
VALUES_DEV=values-dev.yaml

dev:
	@kubectl create namespace $(DEV_NS) --dry-run=client -o yaml | kubectl apply -f -
	helm upgrade --install app-dev $(CHART_PATH) \
		--namespace $(DEV_NS) \
		-f $(VALUES_DEV) \
		--set image.tag=latest \
		--set volumeMounts[0].name=source-code \
		--set volumes[0].hostPath.path=/mnt/dev-share/$(USER)
	@kubectl cp . $(DEV_NS)/app-dev-0:/app -c app
	@kubectl port-forward svc/app-dev 8080:80 -n $(DEV_NS) &

Cuándo aplicar esta solución

  • Equipos pequeños (1‑5 ingenieros) que necesitan iterar rápidamente sin esperar a que el CI genere una nueva imagen.
  • Entornos on‑prem o k3s donde se controla el nodo y se puede montar un directorio compartido (NFS, hostPath).
  • Proyectos en fase de prototipo donde la estabilidad del pipeline no es crítica y la velocidad de desarrollo es prioridad.

No es adecuada cuando:

  • El clúster está estrictamente aislado y no permite hostPath ni volúmenes compartidos.
  • La política de seguridad prohíbe el acceso directo a nodos o la ejecución de kubectl cp.
  • Se requiere trazabilidad completa de versiones de imagen en producción; en ese caso, el flujo de CI/CD debe seguir usando imágenes versionadas.

Código

# Crear namespace de desarrollo (solo una vez)
kubectl create namespace dev-jdoe

# Instalar chart en modo dev
helm upgrade --install myapp-dev ./charts/myapp \
  --namespace dev-jdoe \
  -f values-dev.yaml \
  --set image.tag=latest \
  --set volumeMounts[0].name=src \
  --set volumes[0].hostPath.path=/mnt/dev-share/jdoe

# Copiar código al pod (asume que el pod se llama myapp-dev-0)
kubectl cp . dev-jdoe/myapp-dev-0:/app -c myapp

# Exponer puerto localmente
kubectl port-forward svc/myapp-dev 8080:80 -n dev-jdoe &

Verificación

  1. Estado del pod

    kubectl get pods -n dev-jdoe -l app=myapp
    

    El pod debe estar en Running y el contenedor con sleep infinity.

  2. Acceso al código

    kubectl exec -n dev-jdoe myapp-dev-0 -- ls /app
    

    Debería listar los archivos que acabas de copiar.

  3. Logs en tiempo real

    kubectl logs -f -n dev-jdoe myapp-dev-0
    

    Verifica que la aplicación escribe en stdout como lo hacía en Docker Compose.

  4. Endpoint accesible
    Abre http://localhost:8080 en el navegador; la respuesta debe ser la misma que obtenías en el contenedor local.

Notas adicionales

  • Sincronización de código: kubectl cp copia todo el árbol de archivos cada vez. Para cambios frecuentes, considera montar un PVC NFS y usar rsync -avz dentro del pod; reduce el tiempo de copia y mantiene permisos.
  • Persistencia de datos: Si la aplicación escribe en disco, usa un emptyDir o un PVC dedicado al pod de desarrollo; de lo contrario, los datos se perderán al reiniciar el pod.
  • Política de recursos: Limita CPU y memoria en el chart de desarrollo (resources.limits) para evitar que un pod de prueba consuma todo el nodo.
  • ArgoCD: Añade la anotación argocd.argoproj.io/sync-options: Skip al metadata.annotations del Deployment en values-dev.yaml para que ArgoCD no intente sobrescribir los cambios locales.
  • Limpieza: Incluye un objetivo make dev-clean que elimina el namespace y los recursos asociados; evita que los nodos queden saturados con pods huérfanos.

Con este enfoque, cada ingeniero recupera la velocidad de los entornos Docker Compose mientras mantiene la consistencia de los charts y la visibilidad de ArgoCD para los entornos de staging y producción. La clave está en separar claramente los namespaces de desarrollo y producción y parametrizar el chart para que el mismo artefacto pueda servir a ambos mundos.