Volver al blog

Cloud

Infraestructura Cloud con AWS: Lo que Hemos Aprendido

9 min lecturaEnviaIT Engineering

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) y eu-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:

  1. Nunca se salta staging. Cada commit en main se despliega automáticamente a staging. Producción requiere aprobación manual en GitHub.
  2. 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.
  3. 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.
  4. 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:

  1. No configurar límites en Lambda desde el principio. Una función recursiva sin reservedConcurrentExecutions puede escalar a miles de invocaciones en segundos y generar una factura inesperada. Ahora siempre definimos límites de concurrencia explícitos.

  2. 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.

  3. 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.

  4. 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.