Cuando sabes que tu aplicación va a ir a producción en Kubernetes desde el minuto cero, lo último que quieres es mantener un docker-compose.yml, un .aspire.app, un conjunto de manifiestos k8s/, y encima repetir la configuración en CI/CD. La tentación de crear “entornos de desarrollo paralelos” es alta, pero el coste a medio plazo también lo es.

En este artículo, te muestro cómo estructurar un entorno de desarrollo eficiente, realista y sin lock-in, comparando 4 enfoques comunes: Docker Compose, .NET Aspire, k3d y kind, desde la perspectiva de un equipo que desplegará sí o sí en Kubernetes.

Objetivo

  • Acelerar el desarrollo local.

  • Sin reinventar la infraestructura.

  • Sin definir entornos “alternativos” que no se parecen a producción.

  • Sin depender de soluciones propietarias o ligadas a un proveedor cloud.

El problema de duplicar entornos

“Lo levanto con Compose en local y ya en CI que use K8s.”

Error común. Lo que funciona en Compose suele tener diferencias sutiles: redes, volúmenes, healthchecks, secretos, puertos, etc.

“Uso Aspire para desarrollo, luego me paso a Helm y ya está.”

Otra trampa. Aspire abstrae muchas cosas útiles, pero introduce una capa adicional que no es la misma que usar manifiestos Kubernetes nativos.

Conclusión: Si sabes que vas a Kubernetes, no diseñes tu entorno local como si no fueras a ir.

Estrategia recomendada: Kubernetes como fuente de verdad

Define una sola vez tus servicios como manifiestos YAML para K8s:

  • deployment.yaml, service.yaml, configmap.yaml, etc.

  • Usa Helm, Kustomize o raw YAML, pero que sea compatible desde el primer día con producción.

Para desarrollo local, usa un clúster Kubernetes ligero:

Alternativa ¿Qué es? Ideal para
k3d K3s sobre Docker (ligero, rápido) Desarrollo diario
kind Kubernetes en Docker (más fiel al oficial) Validaciones CI/CD

¿Y qué pasa con Compose o Aspire?

Docker Compose

  • Es cómodo.

  • Pero no es Kubernetes.

  • Te obliga a mantener definiciones redundantes.

.NET Aspire

  • Genial si eres 100% .NET y quieres máxima productividad.

  • Pero no es Kubernetes-compatible.

  • Puedes usarlo como “shell de desarrollo”, pero vas a tener que escribir de nuevo los manifiestos de producción.

Flujo ideal de desarrollo (desde el día 0)

  • Desarrollas con el mismo manifiesto que desplegarás.

  • Puedes testear servicios, pods, configuración real, observabilidad…

  • Puedes hacer port forwarding (kubectl port-forward), logs, secretos…

Acelerar sin duplicar: tips prácticos

Necesidad Solución
Base de datos local persistente PersistentVolumeClaim con local-path-provisioner
Acceso desde navegador a servicios kubectl port-forward o Ingress con k3d
Rebuild rápido del microservicio skaffold dev o tilt
Debug paso a paso Visual Studio + kubectl port-forward al puerto del API
SQL de desarrollo pre-cargado initContainers en el Deployment del PostgreSQL

¿Y las herramientas de desarrollo?

Puedes usar herramientas como Tilt o Skaffold para acelerar los ciclos de desarrollo sin dejar de usar Kubernetes.

  • Tilt: recarga automática de código en el contenedor, integración con Helm/Kustomize.

  • Skaffold: build, deploy, y test en ciclo continuo.

Ambas trabajan sobre k3d/kind o cualquier clúster K8s.

¿Y si quiero simplificar el onboarding?

Crea un script o Makefile con estos pasos:

# Arrancar entorno local
make dev
# Internamente:
# - arranca k3d (si no existe)
# - aplica manifiestos
# - muestra servicios disponibles

También puedes publicar una imagen devcontainer con todo preconfigurado (Docker + kubectl + k3d + tilt).

Conclusión

Si ya sabes que tu app va a Kubernetes, elimina la idea de que necesitas un entorno local alternativo. Usa Kubernetes desde el principio y hazlo ligero, reproducible y portable con herramientas como k3d, kind, Tilt o Skaffold.

El mejor entorno de desarrollo no es el más cómodo: es el más parecido a producción.

Ejemplo completo con kind + Tilt + .NET

Cuando sabes que tu aplicación va a Kubernetes, no tiene sentido mantener docker-compose.yml, scripts alternativos ni setups exclusivos para desarrollo.

La clave está en usar Kubernetes también en local, pero sin complicar la vida a los desarrolladores.

Este artículo muestra un ejemplo real y completo para levantar localmente:

  • Un backend en .NET 8 Web API

  • Un frontend estático en Nginx

  • Una base de datos PostgreSQL

  • Todo orquestado en un clúster kind

  • Ciclo de vida gestionado por Tilt

  • Automatizado vía Makefile

🎯 Objetivo: cero duplicación entre desarrollo y producción.

¿Qué vamos a montar?
[frontend] ──────▶ HTTP :5000
    │
    ▼
[backend] ───────▶ HTTP :8000
    │
    ▼
[postgres] ──────▶ DB :5432

Cada componente será desplegado como un pod de Kubernetes, con sus Deployment y Service, directamente sobre un clúster local de kind.

Requisitos

Asegúrate de tener instalado:

Estructura del proyecto
microservices-kind-tilt-example/
├── Makefile
├── tiltfile
├── k8s/
│   ├── backend/
│   ├── frontend/
│   └── postgres/
├── src/
│   ├── backend/
│   └── frontend/
Paso 1: Automatización con Makefile
KIND_CLUSTER_NAME=dev-cluster

.PHONY: kind-start kind-delete dev

kind-start:
\tkind create cluster --name $(KIND_CLUSTER_NAME) --wait 60s

kind-delete:
\tkind delete cluster --name $(KIND_CLUSTER_NAME)

dev: kind-start
\ttilt up
Paso 2: Definir los manifiestos Kubernetes

PostgreSQL

# k8s/postgres/postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        env:
        - name: POSTGRES_DB
          value: "products"
        - name: POSTGRES_USER
          value: "user"
        - name: POSTGRES_PASSWORD
          value: "pass"
        ports:
        - containerPort: 5432

Backend (.NET 8)

# k8s/backend/backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: backend
        env:
        - name: ConnectionStrings__Default
          value: "Host=postgres;Database=products;Username=user;Password=pass"
        ports:
        - containerPort: 80
# k8s/backend/backend-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: backend
  ports:
  - port: 80
    targetPort: 80

Frontend (estático)

# k8s/frontend/frontend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: frontend
        ports:
        - containerPort: 80
# k8s/frontend/frontend-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  selector:
    app: frontend
  ports:
  - port: 80
    targetPort: 80
Paso 3: Dockerfiles

Backend (.NET 8 Web API)

# src/backend/Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/out

FROM base AS final
WORKDIR /app
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "ProductService.Api.dll"]

Frontend (Nginx con HTML)

# src/frontend/Dockerfile
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
Paso 4: Tiltfile para levantar todo
# tiltfile
k8s_yaml([
  "k8s/postgres/postgres-deployment.yaml",
  "k8s/postgres/postgres-service.yaml",
  "k8s/backend/backend-deployment.yaml",
  "k8s/backend/backend-service.yaml",
  "k8s/frontend/frontend-deployment.yaml",
  "k8s/frontend/frontend-service.yaml"
])

docker_build('backend', './src/backend')
docker_build('frontend', './src/frontend')

k8s_resource('backend', port_forwards=8000)
k8s_resource('frontend', port_forwards=5000)
Paso 5: Ejecutar
make dev
Resultado

Con este enfoque:

  • Usas Kubernetes desde el principio

  • No hay necesidad de docker-compose

  • Todo está preparado para pasar a producción sin reescribir nada

  • Tilt te ofrece recarga automática, logs y control visual

Una única fuente de verdad: tus manifiestos Kubernetes.

Depuración local + dependencias remotas (en kind)

Beneficios

  • VS Code usa su propio debugger.

  • No necesitas instalar nada especial en el contenedor.

  • Puedes hacer hot reload, breakpoints y todo lo habitual.

  • Solo el backend se ejecuta localmente, todo lo demás sigue en Kubernetes.

Paso 1: Ejecuta kind y Tilt normalmente
make dev

Esto desplegará PostgreSQL, el frontend y el backend dentro de Kubernetes.

Paso 2: Elimina el pod de backend para depurar localmente
kubectl delete deployment backend

Así liberas el puerto 80 que usaba el pod backend dentro del clúster, para que tu versión local pueda conectarse.

Paso 3: Haz port-forward de PostgreSQL al host
kubectl port-forward svc/postgres 5432:5432

Esto expone la base de datos del clúster en localhost:5432, permitiendo que tu backend local se conecte sin problemas.

Paso 4: Configura launch.json en VS Code
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": ".NET Core Launch (backend local)",
      "type": "coreclr",
      "request": "launch",
      "preLaunchTask": "build",
      "program": "${workspaceFolder}/bin/Debug/net8.0/ProductService.Api.dll",
      "args": [],
      "cwd": "${workspaceFolder}",
      "stopAtEntry": false,
      "serverReadyAction": {
        "action": "openExternally",
        "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
      },
      "env": {
        "ConnectionStrings__Default": "Host=localhost;Database=products;Username=user;Password=pass"
      },
      "launchBrowser": {
        "enabled": true,
        "args": "http://localhost:5000"
      }
    }
  ]
}

Asegúrate de que launchSettings.json esté configurado para escuchar en http://localhost:5000.

Paso 5: Levanta el backend desde VS Code

En la raíz del proyecto src/backend, pulsa F5 o selecciona el perfil «backend local» desde la vista de ejecución.

Ya puedes depurar línea por línea, ver el tráfico, modificar código, etc.

 (Opcional) Comunicar el frontend con el backend local

Si quieres que el frontend (que sigue en Kubernetes) hable con tu backend en localhost, necesitarás exponer tu backend en la red de Docker o de kind. Dos formas comunes:

Opción rápida: Ingresar tu IP local

En tu HTML o config del frontend:

const API_URL = "http://host.docker.internal:5000"; // Si Tilt está dentro de Docker

O expón el backend desde host a kind con:

kubectl port-forward svc/frontend 5000:80

Y redirige las llamadas desde el frontend al backend vía host expuesto.

Alternativa: usar Tilt para desarrollo mixto

Puedes indicar a Tilt que no construya ni despliegue el backend y que lo asuma como externo:

# tiltfile (modificado)
disable_yaml('k8s/backend/backend-deployment.yaml', 'k8s/backend/backend-service.yaml')

Así Tilt sigue levantando todo lo demás y tú controlas el backend desde tu IDE. Y lógicamente es la que deberías usar.

Resultado
  • PostgreSQL y frontend siguen en Kubernetes.

  • Backend se ejecuta desde VS Code con toda la potencia del debugger.

  • No duplicas infraestructura.

  • El comportamiento es idéntico al entorno productivo.

¿Cómo se integra en el ejemplo con kind + Tilt?

Vamos a dar un paso más, ya que si quieres que tu entorno de desarrollo se parezca de verdad a producción, usas un Ingress Controller, tambien puedes ponerlo en local.

En este caso vamos a usar Traefik.

¿Qué es Traefik?

Traefik es un ingress controller dinámico que:

  • Se despliega dentro de Kubernetes.

  • Detecta automáticamente tus servicios y rutas.

  • Expone tus aplicaciones al exterior mediante HTTP(S), con soporte para:

    • Enrutamiento por hostname o path.

    • TLS automático con Let’s Encrypt.

    • Middleware: autenticación, headers, rate limit, etc.

¿Por qué meter Traefik en desarrollo?

Porque ayuda a:

Ventaja ¿Por qué es útil en local?
Simula producción Puedes replicar cómo funciona tu ingress en cloud (AKS, EKS…)
Acceso uniforme Accedes a servicios con URLs como http://frontend.localhost, http://api.localhost
TLS opcional Puedes simular HTTPS en desarrollo con certificados locales
Middleware real Pruebas CORS, auth, redirecciones, etc., sin hacks
 Centraliza entrada Un solo punto de entrada para todos tus servicios
¿Cómo se integra en el ejemplo con kind + Tilt?

Paso 1: Instalar Traefik en el clúster kind

Vamos a usar Helm.

helm repo add traefik https://traefik.github.io/charts
helm repo update

helm install traefik traefik/traefik \
  --set service.type=NodePort \
  --set ports.web.nodePort=30080 \
  --set ports.websecure.nodePort=30443

Esto expone:

  • HTTP en localhost:30080

  • HTTPS en localhost:30443

Paso 2: Añadir IngressRoutes para frontend y backend

# k8s/ingress/frontend-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-ingress
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
  rules:
    - host: frontend.localhost
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend
                port:
                  number: 80
# k8s/ingress/backend-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: backend-ingress
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
  rules:
    - host: api.localhost
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: backend
                port:
                  number: 80

Paso 3: Modificar /etc/hosts para acceder con dominios locales

127.0.0.1 frontend.localhost api.localhost

Paso 4: Tilt + Traefik

k8s_yaml([
  # ... anteriores ...
  "k8s/ingress/frontend-ingress.yaml",
  "k8s/ingress/backend-ingress.yaml"
])

Ahora puedes acceder a:

Casos de uso que Traefik te permite probar en local

Escenario ¿Qué puedes simular con Traefik?
Enrutamiento por subdominio api.localhost, auth.localhost
Rutas específicas /api, /admin, /healthz
TLS y certificados HTTPS con certificados reales o de desarrollo
Redirecciones automáticas HTTP → HTTPS o paths
Middleware de seguridad Autenticación básica, CORS, cabeceras seguras

Si quieres que tu entorno de desarrollo se parezca de verdad a producción, usa Traefik desde el principio. Especialmente útil si:

  • Vas a desplegar con Ingress Controllers reales (Traefik, NGINX, Istio).

  • Necesitas probar CORS, rutas o dominios personalizados.

  • Quieres acceder fácilmente sin recordar puertos por servicio.

Proyecto 110 % funcional para experimentar sin trampas

Todo lo que has leído en este artículo está empaquetado en un ejemplo completo (la version final con Traefik), funcional y replicable que puedes clonar, ejecutar y extender. Aquí no hay simulaciones ni atajos: lo que montas en local es lo mismo que irá a producción, solo que más ligero.