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.
¿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
.
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
-
Abre automáticamente la interfaz de Tilt (
http://localhost:10350
) -
Accede al frontend en: http://localhost:5000
-
Accede al backend en: http://localhost:8000
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:
-
Frontend: http://frontend.localhost:30080
-
Backend: http://api.localhost:30080
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.