Hace tres años, nuestro proceso de despliegue consistía en conectarse por SSH a un servidor, hacer git pull y rezar. Funcionaba — hasta que dejó de funcionar. Un viernes a las 22:00, un deploy manual rompió producción y tardamos cuatro horas en restaurar el servicio. Ese fue el punto de inflexión.
Hoy, todos nuestros proyectos corren sobre AWS con infraestructura como código, pipelines automatizados y rollbacks en menos de 60 segundos. Este artículo documenta lo que hemos aprendido en el camino.
Por qué AWS (y no otro proveedor)
Antes de entrar en detalle, la pregunta obvia: ¿por qué AWS y no GCP o Azure? La respuesta honesta es que no hay una respuesta universal. Para nosotros, AWS fue la elección correcta por:
- Amplitud de servicios. AWS tiene más de 200 servicios. Para cada necesidad que hemos tenido — desde colas de mensajes hasta machine learning — hay una solución nativa.
- Madurez del ecosistema. La documentación, los ejemplos y la comunidad son incomparables. Cuando tienes un problema a las 3am, hay un post en Stack Overflow o un hilo en re:Post que lo resuelve.
- Presencia en Europa. Las regiones
eu-west-1(Irlanda) yeu-south-2(España) nos permiten cumplir con los requisitos de residencia de datos del RGPD sin compromisos.
Si tu equipo conoce otro proveedor, probablemente sea la mejor opción para vosotros. El mejor cloud es el que tu equipo domina.
Los servicios que usamos en cada proyecto
No todos los proyectos necesitan los mismos servicios. Después de más de 20 proyectos en producción, hemos identificado patrones claros según el tipo de aplicación.
Para aplicaciones web (SaaS, dashboards, portales)
| Servicio | Función | Por qué lo elegimos | |----------|---------|---------------------| | ECS Fargate | Contenedores sin gestión de servidores | Escala automáticamente, sin parchear instancias EC2 | | RDS PostgreSQL | Base de datos relacional | Multi-AZ, backups automáticos, réplicas de lectura | | ElastiCache Redis | Caché y sesiones | Latencia sub-milisegundo, pub/sub para WebSockets | | CloudFront + S3 | CDN y assets estáticos | Edge locations en todo el mundo, invalidación rápida | | ALB | Load balancer | Health checks, routing basado en path, TLS termination |
Un ejemplo real: para una plataforma de gestión de eventos que construimos, el stack completo se ve así:
Usuario → CloudFront (CDN) → ALB → ECS Fargate (3 tareas)
↓
RDS PostgreSQL (Multi-AZ)
ElastiCache Redis
S3 (uploads de usuarios)
El coste mensual de este stack para ~5.000 usuarios activos diarios: aproximadamente 280 EUR/mes. Mucho menos de lo que costaría un equipo de sysadmins gestionando servidores bare-metal.
Para APIs y microservicios
Cuando el proyecto tiene una API con tráfico variable o necesita escalar componentes de forma independiente:
- API Gateway + Lambda para endpoints con tráfico impredecible. Pagas solo por invocación.
- ECS Fargate para servicios que necesitan conexiones persistentes (WebSockets) o tiempos de ejecución largos.
- EventBridge como bus de eventos para comunicación asíncrona entre servicios.
- SQS para colas de trabajo con retry automático y dead-letter queues.
// Ejemplo: Lambda handler para procesar pagos
// Invocado por EventBridge cuando un pedido se confirma
import { EventBridgeEvent } from 'aws-lambda';
import { processPayment } from './services/payment';
import { notifyUser } from './services/notification';
export const handler = async (
event: EventBridgeEvent<'OrderConfirmed', OrderPayload>
) => {
const { orderId, amount, userId } = event.detail;
try {
const result = await processPayment({ orderId, amount });
// Emitir evento de pago completado
await eventBridge.putEvents({
Entries: [{
Source: 'payments',
DetailType: 'PaymentCompleted',
Detail: JSON.stringify({ orderId, transactionId: result.id }),
}],
});
} catch (error) {
// El evento va a DLQ tras 3 reintentos automáticos
console.error(`Payment failed for order ${orderId}`, error);
throw error;
}
};
Para sitios estáticos y JAMstack
La opción más simple y económica:
- S3 para hosting de archivos estáticos
- CloudFront como CDN con certificado TLS gratuito vía ACM
- Route 53 para DNS con health checks y failover automático
Coste típico: menos de 5 EUR/mes para sitios con decenas de miles de visitas.
Infraestructura como Código: Terraform vs CDK
Este es un tema donde las opiniones son fuertes. Hemos usado ambos en producción y esta es nuestra posición actual.
Terraform
Lo usamos para infraestructura compartida y multi-cuenta: networking (VPCs, subnets, peering), cuentas de AWS Organizations, políticas IAM globales, y cualquier recurso que no esté ligado a una aplicación específica.
# Ejemplo: VPC con subnets públicas y privadas
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "production"
cidr = "10.0.0.0/16"
azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true # Ahorro: un solo NAT en dev/staging
enable_dns_hostnames = true
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
Ventajas de Terraform: Es agnóstico del proveedor, el plan de ejecución te muestra exactamente qué va a cambiar antes de aplicar, y el state file es predecible.
AWS CDK
Lo usamos para recursos ligados a una aplicación concreta: definiciones de ECS tasks, Lambdas, tablas DynamoDB, colas SQS. La ventaja principal es que puedes usar TypeScript — el mismo lenguaje que tu aplicación — con autocompletado y type safety.
// Ejemplo: Servicio ECS con CDK
const service = new ecs.FargateService(this, 'ApiService', {
cluster,
taskDefinition,
desiredCount: 2,
assignPublicIp: false,
circuitBreaker: { rollback: true }, // Rollback automático si falla
capacityProviderStrategies: [
{
capacityProvider: 'FARGATE_SPOT',
weight: 2, // 66% en Spot para ahorro
},
{
capacityProvider: 'FARGATE',
weight: 1, // 33% en On-Demand para estabilidad
},
],
});
// Auto-scaling basado en CPU
const scaling = service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 10,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: Duration.seconds(60),
scaleOutCooldown: Duration.seconds(30),
});
Nuestra regla general: Terraform para la base (redes, cuentas, DNS), CDK para los servicios de la aplicación. No mezclarlos dentro del mismo stack.
CI/CD: El pipeline que nunca falla (casi)
Cada proyecto tiene un pipeline que se ejecuta en GitHub Actions. La estructura típica:
# .github/workflows/deploy.yml (simplificado)
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm run test -- --coverage
- run: npm run build
deploy-staging:
needs: test
runs-on: ubuntu-latest
environment: staging
steps:
- name: Build & push Docker image
run: |
docker build -t $ECR_REPO:${{ github.sha }} .
docker push $ECR_REPO:${{ github.sha }}
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster staging \
--service api \
--force-new-deployment
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requiere aprobación manual
steps:
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster production \
--service api \
--force-new-deployment
Puntos clave de nuestro pipeline:
- Nunca se salta staging. Cada commit en
mainse despliega automáticamente a staging. Producción requiere aprobación manual en GitHub. - Docker image tagging por SHA. Cada imagen se etiqueta con el SHA del commit. Esto hace que los rollbacks sean triviales: basta desplegar la imagen del commit anterior.
- Health checks obligatorios. ECS no enruta tráfico a una tarea nueva hasta que su health check responde 200. Si la tarea falla, ECS la mata y mantiene la versión anterior.
- Circuit breaker activado. Si el despliegue falla repetidamente, ECS hace rollback automático sin intervención manual.
Optimización de costes: lo que realmente importa
AWS puede ser carísimo si no prestas atención. Estas son las prácticas que nos han ahorrado miles de euros al año:
1. Fargate Spot para workloads no críticos
Las tareas de Fargate Spot cuestan hasta un 70% menos que las estándar. Las usamos para:
- Workers de procesamiento de colas
- Tareas de staging y desarrollo
- Batch processing nocturno
La contrapartida es que AWS puede interrumpir la tarea con 30 segundos de aviso. Para workers que procesan mensajes de SQS, esto es aceptable: el mensaje vuelve a la cola y otro worker lo procesa.
2. Reserved Instances para bases de datos
RDS es generalmente el componente más caro del stack. Un Reserved Instance de 1 año ahorra un 40% sobre el precio on-demand. Para bases de datos de producción que sabes que van a existir al menos un año, siempre merece la pena.
3. S3 Intelligent-Tiering
Para buckets con datos de acceso variable (logs, backups, uploads de usuarios), S3 Intelligent-Tiering mueve automáticamente los objetos entre tiers según su patrón de acceso. Activarlo es una línea de configuración y el ahorro es significativo en buckets grandes.
4. AWS Cost Explorer + alertas
Configuramos alertas de coste en cada cuenta:
- Alerta al 50% del presupuesto mensual: notificación informativa
- Alerta al 80%: revisión obligatoria del equipo
- Alerta al 100%: escalación inmediata
Más de una vez hemos detectado un Lambda en bucle infinito o un ECS service con demasiadas réplicas gracias a estas alertas.
Monitorización: CloudWatch y más allá
La monitorización no es opcional. Si no puedes ver qué pasa en tu sistema, no puedes operarlo. Este es nuestro stack de observabilidad:
CloudWatch como base
Para cada servicio configuramos:
- Métricas custom de negocio (pedidos procesados, pagos completados, errores de validación)
- Alarmas que disparan notificaciones a Slack vía SNS
- Dashboards por servicio y uno general del sistema
- Log Insights para queries ad-hoc sobre los logs
// Ejemplo: métrica custom en la aplicación
import { CloudWatch } from '@aws-sdk/client-cloudwatch';
const cw = new CloudWatch({});
async function trackOrderProcessed(duration: number, success: boolean) {
await cw.putMetricData({
Namespace: 'MyApp/Orders',
MetricData: [
{
MetricName: 'OrderProcessingTime',
Value: duration,
Unit: 'Milliseconds',
Dimensions: [
{ Name: 'Environment', Value: process.env.ENV },
{ Name: 'Status', Value: success ? 'success' : 'failure' },
],
},
],
});
}
Alarmas que importan
No todas las alarmas son iguales. Hemos aprendido a clasificarlas:
- P1 (Despierta a alguien): Tasa de errores 5xx > 5% durante 5 minutos, base de datos inaccesible, latencia P99 > 5 segundos
- P2 (Revisar en horario laboral): CPU > 80% durante 15 minutos, disco > 75%, errores 4xx inusuales
- P3 (Informativa): Costes por encima del forecast, deploys completados, certificados que expiran en 30 días
La fatiga de alarmas es real. Si tu equipo ignora las alarmas porque hay demasiadas, tienes un problema mayor que cualquier incidencia técnica.
Tracing distribuido
Para sistemas con múltiples servicios, usamos AWS X-Ray para trazar peticiones de extremo a extremo. Cuando un usuario reporta que "la app va lenta", podemos ver exactamente qué servicio, qué query o qué llamada externa está causando la latencia.
Errores que hemos cometido (para que tú no los cometas)
Ser transparentes sobre los errores es más útil que presumir de aciertos. Aquí van los nuestros:
-
No configurar límites en Lambda desde el principio. Una función recursiva sin
reservedConcurrentExecutionspuede escalar a miles de invocaciones en segundos y generar una factura inesperada. Ahora siempre definimos límites de concurrencia explícitos. -
Subestimar los costes de transferencia de datos. La transferencia entre regiones y hacia internet no es gratuita. En un proyecto con mucho tráfico de vídeo, descubrimos que el 60% de la factura era data transfer. La solución fue usar CloudFront agresivamente para cachear contenido.
-
No usar ambientes separados desde el día uno. En los primeros proyectos, teníamos staging y producción en la misma cuenta de AWS. Un error en un script de limpieza de staging borró datos de producción. Ahora usamos AWS Organizations con cuentas separadas para cada ambiente.
-
Ignorar los security groups por defecto. El security group por defecto de una VPC permite todo el tráfico saliente y todo el tráfico entre miembros del grupo. Más de una vez dejamos servicios accesibles internamente que no deberían haberlo sido. La regla ahora: security groups explícitos para cada servicio, deny-all por defecto.
Checklist de infraestructura para nuevos proyectos
Cada vez que arrancamos un proyecto nuevo, revisamos esta lista:
- [ ] Cuenta AWS dedicada en Organizations
- [ ] VPC con subnets públicas y privadas en al menos 2 AZs
- [ ] Terraform para networking, CDK para servicios de aplicación
- [ ] Pipeline CI/CD con test, staging y producción
- [ ] Health checks y circuit breaker en ECS
- [ ] Alarmas de CloudWatch para P1, P2 y P3
- [ ] Alertas de coste al 50%, 80% y 100% del presupuesto
- [ ] Backups automáticos de RDS con retención de 7 días (mínimo)
- [ ] Secrets en AWS Secrets Manager (nunca en variables de entorno)
- [ ] Logs centralizados con retención definida
El camino no termina aquí
La infraestructura cloud no es un proyecto que se completa — es una práctica que evoluciona. Cada trimestre revisamos nuestros stacks, actualizamos versiones, optimizamos costes y evaluamos nuevos servicios.
Lo más importante que hemos aprendido: la mejor infraestructura es la que tu equipo entiende y puede operar. No sirve de nada tener un setup sofisticado si solo una persona sabe cómo funciona.
¿Necesitas migrar a cloud o mejorar tu infraestructura actual? Hablemos sobre cómo podemos ayudarte a diseñar un stack que se adapte a tu proyecto.