Saltar a contenido

S2 · Entregable 3 — Implementación y Control del Centro de Datos (AWS)

Competencias C3.2 (Implementación) · C3.3 (Control y operación)
Proyecto: LOGYX — Sistema Operativo Logístico Colaborativo para PYMEs
Equipo: Jorge Gutiérrez Miranda · Fabrizio Sanchez Saravia · Alex Coila Jarita
Semestre 2 · Agosto–Noviembre 2026


Resumen Ejecutivo

Este documento registra la implementación completa de los servicios de infraestructura AWS diseñados en S1-E3: instancias EC2 con Quarkus, RDS PostgreSQL Multi-AZ con Flyway, ElastiCache Redis, S3 con políticas de lifecycle, ECS Fargate para jobs programados, y el sistema de monitoreo integral con CloudWatch (métricas, alarmas, dashboards) y CloudTrail (auditoría de API). Se definen los SLAs, procedimientos operativos y análisis de eficiencia.


Sección 1 — Configuración de Servidores y Servicios (C3.2)

1.1 EC2 — Backend Quarkus

# compute/ec2.tf

# Launch Template para el Auto Scaling Group
resource "aws_launch_template" "quarkus" {
  name_prefix   = "logyx-quarkus-"
  image_id      = data.aws_ami.amazon_linux_2023.id
  instance_type = "t3.medium"

  iam_instance_profile {
    arn = aws_iam_instance_profile.ec2_backend.arn
  }

  network_interfaces {
    associate_public_ip_address = false
    security_groups             = [aws_security_group.backend.id]
  }

  block_device_mappings {
    device_name = "/dev/xvda"
    ebs {
      volume_size           = 20   # GB — OS + Docker + logs
      volume_type           = "gp3"
      encrypted             = true
      kms_key_id            = aws_kms_key.ec2.arn
      delete_on_termination = true
    }
  }

  # IMDSv2 obligatorio (protección SSRF)
  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"
    http_put_response_hop_limit = 1
  }

  monitoring { enabled = true }  # CloudWatch Detailed Monitoring

  user_data = base64encode(<<-EOF
    #!/bin/bash
    # Instalar Docker
    dnf install -y docker
    systemctl enable --now docker

    # Instalar AWS CLI v2
    curl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"
    unzip -q /tmp/awscliv2.zip -d /tmp && /tmp/aws/install

    # Instalar Wazuh Agent
    rpm --import https://packages.wazuh.com/key/GPG-KEY-WAZUH
    echo "[wazuh]
    gpgcheck=1
    baseurl=https://packages.wazuh.com/4.x/yum/" > /etc/yum.repos.d/wazuh.repo
    dnf install -y wazuh-agent
    WAZUH_MANAGER='10.0.30.10' WAZUH_AGENT_GROUP='logyx-backend' \
      dnf install -y wazuh-agent
    systemctl enable --now wazuh-agent

    # Obtener secretos desde Secrets Manager
    DB_URL=$(aws secretsmanager get-secret-value \
      --secret-id logyx/prod/db-url \
      --query SecretString --output text)
    JWT_SECRET=$(aws secretsmanager get-secret-value \
      --secret-id logyx/prod/jwt-secret \
      --query SecretString --output text)

    # Pull y ejecutar imagen Quarkus desde ECR
    aws ecr get-login-password --region sa-east-1 | \
      docker login --username AWS --password-stdin \
      ACCOUNT.dkr.ecr.sa-east-1.amazonaws.com

    docker run -d \
      --name logyx-backend \
      --restart unless-stopped \
      -p 8080:8080 \
      -e QUARKUS_DATASOURCE_JDBC_URL="$DB_URL" \
      -e JWT_SECRET="$JWT_SECRET" \
      -e QUARKUS_REDIS_HOSTS="redis://10.0.20.20:6379" \
      ACCOUNT.dkr.ecr.sa-east-1.amazonaws.com/logyx/quarkus-backend:latest
  EOF
  )

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name        = "logyx-backend"
      Environment = "production"
      ManagedBy   = "terraform"
    }
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "quarkus" {
  name                = "logyx-asg-quarkus"
  desired_capacity    = 2
  min_size            = 2
  max_size            = 6
  vpc_zone_identifier = [aws_subnet.app_a.id, aws_subnet.app_b.id]

  launch_template {
    id      = aws_launch_template.quarkus.id
    version = "$Latest"
  }

  target_group_arns = [aws_lb_target_group.quarkus.arn]

  health_check_type         = "ELB"
  health_check_grace_period = 120

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 50
    }
  }

  tag {
    key                 = "Name"
    value               = "logyx-backend"
    propagate_at_launch = true
  }
}

# Política de escalado basada en CPU
resource "aws_autoscaling_policy" "scale_out" {
  name                   = "logyx-scale-out"
  autoscaling_group_name = aws_autoscaling_group.quarkus.name
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ASGAverageCPUUtilization"
    }
    target_value = 70.0
  }
}

1.2 Application Load Balancer

# compute/alb.tf

resource "aws_lb" "logyx" {
  name               = "logyx-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [aws_subnet.public_a.id, aws_subnet.public_b.id]

  enable_deletion_protection = true
  drop_invalid_header_fields = true  # Seguridad: descartar headers malformados

  access_logs {
    bucket  = aws_s3_bucket.alb_logs.bucket
    prefix  = "alb"
    enabled = true
  }
}

# HTTP → HTTPS redirect
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.logyx.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# HTTPS listener
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.logyx.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"  # Solo TLS 1.3
  certificate_arn   = aws_acm_certificate.logyx.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.quarkus.arn
  }
}

resource "aws_lb_target_group" "quarkus" {
  name        = "logyx-tg-quarkus"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = aws_vpc.logyx.id
  target_type = "instance"

  health_check {
    enabled             = true
    path                = "/q/health/live"
    port                = "traffic-port"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    matcher             = "200"
  }

  stickiness {
    type            = "lb_cookie"
    cookie_duration = 86400
    enabled         = true  # Necesario para WebSocket del chat
  }
}

1.3 RDS PostgreSQL — Configuración

# data/rds.tf

resource "aws_db_subnet_group" "logyx" {
  name       = "logyx-db-subnet-group"
  subnet_ids = [aws_subnet.data_a.id, aws_subnet.data_b.id]
}

resource "aws_db_parameter_group" "postgres16" {
  name   = "logyx-postgres16"
  family = "postgres16"

  parameter { name = "shared_buffers";           value = "1048576" }  # 1 GB en KB
  parameter { name = "effective_cache_size";      value = "3145728" }  # 3 GB
  parameter { name = "work_mem";                  value = "65536" }    # 64 MB
  parameter { name = "maintenance_work_mem";      value = "524288" }   # 512 MB
  parameter { name = "log_min_duration_statement";value = "500" }      # log queries > 500ms
  parameter { name = "log_statement";             value = "ddl" }
  parameter { name = "log_connections";           value = "1" }
  parameter { name = "log_disconnections";        value = "1" }
  parameter { name = "ssl";                       value = "1" }
  parameter { name = "rds.force_ssl";             value = "1" }        # SSL obligatorio
}

resource "aws_db_instance" "postgres" {
  identifier     = "logyx-postgres"
  engine         = "postgres"
  engine_version = "16.2"
  instance_class = "db.t3.medium"

  allocated_storage     = 100
  max_allocated_storage = 500   # Autoscaling de storage hasta 500 GB
  storage_type          = "gp3"
  storage_encrypted     = true
  kms_key_id            = aws_kms_key.rds.arn
  iops                  = 3000

  db_name  = "logyx_db"
  username = "logyx_dba_user"
  password = data.aws_secretsmanager_secret_version.db_password.secret_string

  parameter_group_name   = aws_db_parameter_group.postgres16.name
  db_subnet_group_name   = aws_db_subnet_group.logyx.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  multi_az               = true     # Standby en AZ-B
  publicly_accessible    = false    # Solo acceso desde VPC
  deletion_protection    = true     # Evitar eliminación accidental

  backup_retention_period = 30
  backup_window           = "03:00-04:00"    # 10 PM Peru time
  maintenance_window      = "Sun:08:00-Sun:09:00"  # 3 AM Peru

  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]

  performance_insights_enabled          = true
  performance_insights_retention_period = 7

  apply_immediately = false  # Cambios en ventana de mantenimiento

  tags = { Name = "logyx-postgres-prod", Environment = "production" }
}

Ejecutar migraciones Flyway al deploy:

# En el pipeline GitHub Actions, después de actualizar RDS:
docker run --rm \
  -e FLYWAY_URL="jdbc:postgresql://rds.logyx.internal:5432/logyx_db?ssl=true" \
  -e FLYWAY_USER="logyx_dba_user" \
  -e FLYWAY_PASSWORD="${DB_PASSWORD}" \
  -v $(pwd)/logyx-backend/src/main/resources/db/migration:/flyway/sql \
  flyway/flyway:10 migrate

# Verificar estado de migraciones
flyway/flyway info

1.4 ElastiCache Redis

# data/elasticache.tf

resource "aws_elasticache_subnet_group" "logyx" {
  name       = "logyx-redis-subnet"
  subnet_ids = [aws_subnet.data_a.id, aws_subnet.data_b.id]
}

resource "aws_elasticache_replication_group" "redis" {
  replication_group_id = "logyx-redis"
  description          = "LOGYX Redis — chat pubsub + marketplace cache"
  node_type            = "cache.t3.micro"
  port                 = 6379
  parameter_group_name = "default.redis7"

  num_cache_clusters        = 2      # Primary + 1 réplica en AZ-B
  automatic_failover_enabled = true

  subnet_group_name  = aws_elasticache_subnet_group.logyx.name
  security_group_ids = [aws_security_group.redis.id]

  at_rest_encryption_enabled = true
  transit_encryption_enabled = true    # TLS 6379
  auth_token                 = data.aws_secretsmanager_secret_version.redis_auth.secret_string

  snapshot_retention_limit = 7
  snapshot_window          = "04:00-05:00"

  tags = { Name = "logyx-redis-prod" }
}

1.5 S3 — Almacenamiento de documentos

# Bucket de documentos de entrega
resource "aws_s3_bucket" "documents" {
  bucket = "logyx-documents-prod"
  tags   = { Name = "logyx-documents", Environment = "production" }
}

# Bloquear acceso público
resource "aws_s3_bucket_public_access_block" "documents" {
  bucket                  = aws_s3_bucket.documents.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Cifrado con KMS
resource "aws_s3_bucket_server_side_encryption_configuration" "documents" {
  bucket = aws_s3_bucket.documents.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
    bucket_key_enabled = true
  }
}

# Versionado para recuperación de objetos
resource "aws_s3_bucket_versioning" "documents" {
  bucket = aws_s3_bucket.documents.id
  versioning_configuration { status = "Enabled" }
}

# Lifecycle: Standard → IA → Glacier
resource "aws_s3_bucket_lifecycle_configuration" "documents" {
  bucket = aws_s3_bucket.documents.id
  rule {
    id     = "archive-old-deliveries"
    status = "Enabled"
    transitions {
      days          = 90
      storage_class = "STANDARD_IA"
    }
    transitions {
      days          = 365
      storage_class = "GLACIER"
    }
    noncurrent_version_expiration { noncurrent_days = 90 }
  }
}

1.6 ECS Fargate — Jobs programados

# compute/ecs.tf

resource "aws_ecs_cluster" "logyx_jobs" {
  name = "logyx-jobs-cluster"
}

resource "aws_ecs_task_definition" "smart_load_planner" {
  family                   = "logyx-smart-load-planner"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 512   # 0.5 vCPU
  memory                   = 1024  # 1 GB

  execution_role_arn = aws_iam_role.ecs_execution.arn
  task_role_arn      = aws_iam_role.ec2_backend.arn

  container_definitions = jsonencode([{
    name  = "smart-load-planner"
    image = "ACCOUNT.dkr.ecr.sa-east-1.amazonaws.com/logyx/quarkus-backend:latest"
    command = ["run-scheduler", "smart-load-planner"]
    environment = [
      { name = "QUARKUS_LAUNCH_DEVMODE", value = "false" }
    ]
    secrets = [
      { name = "DB_URL", valueFrom = "arn:aws:secretsmanager:sa-east-1:ACCOUNT:secret:logyx/prod/db-url" }
    ]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"  = "/logyx/ecs/smart-load-planner"
        "awslogs-region" = "sa-east-1"
      }
    }
  }])
}

# EventBridge schedule: ejecutar cada 30 minutos
resource "aws_scheduler_schedule" "smart_load_planner" {
  name                         = "logyx-smart-load-planner"
  schedule_expression          = "rate(30 minutes)"
  schedule_expression_timezone = "America/Lima"

  flexible_time_window { mode = "OFF" }

  target {
    arn      = "arn:aws:ecs:sa-east-1:ACCOUNT:cluster/${aws_ecs_cluster.logyx_jobs.name}"
    role_arn = aws_iam_role.eventbridge_ecs.arn

    ecs_parameters {
      task_definition_arn = aws_ecs_task_definition.smart_load_planner.arn
      launch_type         = "FARGATE"
      network_configuration {
        subnets          = [aws_subnet.app_a.id]
        security_groups  = [aws_security_group.backend.id]
        assign_public_ip = false
      }
    }
  }
}

Sección 2 — Almacenamiento y Backup (C3.2)

2.1 Política de backup unificada

Recurso Método Frecuencia Retención Verificación
RDS PostgreSQL AWS Automated Backup Diario 03:00 PET 30 días Restore mensual en staging
RDS Snapshot manual Pre-deploy Antes de cada deploy 90 días Inmediata
S3 documentos Versioning + Lifecycle Continuo 1 año (Glacier) Restore aleatorio mensual
CloudWatch Logs Log Group + S3 export Continuo 90 días CW; 1 año S3
ECR Docker images Immutable tags Por deploy Últimas 10 versiones
Secrets Manager Versionado automático Por cambio Indefinido

2.2 Script de backup manual pre-deploy

#!/bin/bash
# backup-before-deploy.sh — ejecutado por GitHub Actions antes de cada deploy mayor

set -euo pipefail

DATE=$(date +%Y%m%d_%H%M%S)
SNAPSHOT_ID="logyx-pre-deploy-${DATE}"

echo "Creando snapshot RDS antes del deploy..."
aws rds create-db-snapshot \
  --db-instance-identifier logyx-postgres \
  --db-snapshot-identifier "${SNAPSHOT_ID}" \
  --tags Key=Type,Value=pre-deploy Key=Date,Value="${DATE}"

echo "Esperando que el snapshot esté disponible..."
aws rds wait db-snapshot-available \
  --db-snapshot-identifier "${SNAPSHOT_ID}"

echo "Snapshot ${SNAPSHOT_ID} listo. Deploy puede continuar."

2.3 Prueba de restauración (mensual)

#!/bin/bash
# restore-test.sh — ejecutado el primer domingo de cada mes

LATEST_SNAPSHOT=$(aws rds describe-db-snapshots \
  --db-instance-identifier logyx-postgres \
  --query 'reverse(sort_by(DBSnapshots, &SnapshotCreateTime))[0].DBSnapshotIdentifier' \
  --output text)

# Restaurar en instancia temporal de staging
aws rds restore-db-instance-from-db-snapshot \
  --db-instance-identifier logyx-postgres-restore-test \
  --db-snapshot-identifier "${LATEST_SNAPSHOT}" \
  --db-instance-class db.t3.micro \
  --no-multi-az \
  --publicly-accessible false

# Esperar restauración
aws rds wait db-instance-available \
  --db-instance-identifier logyx-postgres-restore-test

# Ejecutar query de validación
psql -h logyx-postgres-restore-test.xxxx.sa-east-1.rds.amazonaws.com \
  -U logyx_dba_user -d logyx_db \
  -c "SELECT COUNT(*) FROM organizations; SELECT COUNT(*) FROM shipments;"

# Eliminar instancia temporal
aws rds delete-db-instance \
  --db-instance-identifier logyx-postgres-restore-test \
  --skip-final-snapshot

echo "Prueba de restauración completada. RTO verificado: $(date)"

Sección 3 — SLA y Monitoreo con CloudWatch (C3.3)

3.1 Definición de SLA

Componente SLA objetivo Métrica de medición Consecuencia de incumplimiento
API REST (95th pct) < 2.0 s ALB TargetResponseTime P95 Alerta + investigación
Disponibilidad mensual ≥ 99.9% (1 - downtime/total_time) × 100 Postmortem + mejora
Procesamiento de subastas < 1 s Custom metric en CloudWatch Alerta crítica
Confirmación de entrega (Driver → BD) < 3 s EndToEnd metric Alerta
Chat WebSocket (P95) < 300 ms Custom metric Redis pub/sub Alerta
Backup diario 100% éxito RDS Events Alerta crítica

3.2 CloudWatch Dashboard

resource "aws_cloudwatch_dashboard" "logyx_ops" {
  dashboard_name = "LOGYX-Operations"

  dashboard_body = jsonencode({
    widgets = [
      # Fila 1: Métricas del sistema
      {
        type = "metric"
        properties = {
          title  = "API — Latencia P95 (objetivo: < 2s)"
          metrics = [["AWS/ApplicationELB", "TargetResponseTime",
                      "LoadBalancer", "${aws_lb.logyx.arn_suffix}",
                      { stat = "p95", period = 60 }]]
          yAxis = { left = { min = 0, max = 3 } }
          annotations = { horizontal = [{ value = 2, label = "SLA limit", color = "#ff0000" }] }
        }
      },
      {
        type = "metric"
        properties = {
          title = "Requests por segundo"
          metrics = [["AWS/ApplicationELB", "RequestCount",
                      "LoadBalancer", "${aws_lb.logyx.arn_suffix}",
                      { stat = "Sum", period = 60 }]]
        }
      },
      {
        type = "metric"
        properties = {
          title = "Errores HTTP 5xx"
          metrics = [
            ["AWS/ApplicationELB", "HTTPCode_ELB_5XX_Count",
             "LoadBalancer", "${aws_lb.logyx.arn_suffix}", { stat = "Sum", period = 60, color = "#ff0000" }],
            ["AWS/ApplicationELB", "HTTPCode_ELB_4XX_Count",
             "LoadBalancer", "${aws_lb.logyx.arn_suffix}", { stat = "Sum", period = 60, color = "#ffaa00" }]
          ]
        }
      },
      # Fila 2: Instancias y BD
      {
        type = "metric"
        properties = {
          title = "EC2 — CPU % (ASG)"
          metrics = [["AWS/EC2", "CPUUtilization",
                      "AutoScalingGroupName", "logyx-asg-quarkus",
                      { stat = "Average", period = 60 }]]
          annotations = { horizontal = [{ value = 70, label = "Scale out trigger", color = "#ffaa00" }] }
        }
      },
      {
        type = "metric"
        properties = {
          title = "RDS — Conexiones + CPU"
          metrics = [
            ["AWS/RDS", "DatabaseConnections", "DBInstanceIdentifier", "logyx-postgres", { stat = "Maximum" }],
            ["AWS/RDS", "CPUUtilization",       "DBInstanceIdentifier", "logyx-postgres", { stat = "Average" }]
          ]
        }
      },
      {
        type = "metric"
        properties = {
          title = "Redis — Cache Hit Rate"
          metrics = [["AWS/ElastiCache", "CacheHitRate",
                      "ReplicationGroupId", "logyx-redis",
                      { stat = "Average", period = 300 }]]
          annotations = { horizontal = [{ value = 90, label = "Target > 90%", color = "#00aa00" }] }
        }
      }
    ]
  })
}

3.3 Alarmas CloudWatch configuradas

# Alarma: Instancias insuficientes en ASG
resource "aws_cloudwatch_metric_alarm" "asg_min_instances" {
  alarm_name          = "logyx-asg-below-minimum"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 1
  metric_name         = "GroupInServiceInstances"
  namespace           = "AWS/AutoScaling"
  period              = 60
  statistic           = "Minimum"
  threshold           = 2
  dimensions          = { AutoScalingGroupName = aws_autoscaling_group.quarkus.name }
  alarm_description   = "CRÍTICO: Menos de 2 instancias activas en ASG"
  alarm_actions       = [aws_sns_topic.critical_alerts.arn]
  ok_actions          = [aws_sns_topic.alerts.arn]
}

# Alarma: RDS storage > 80%
resource "aws_cloudwatch_metric_alarm" "rds_storage" {
  alarm_name          = "logyx-rds-storage-high"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 1
  metric_name         = "FreeStorageSpace"
  namespace           = "AWS/RDS"
  period              = 3600
  statistic           = "Minimum"
  threshold           = 21474836480  # 20 GB en bytes (20% de 100 GB)
  dimensions          = { DBInstanceIdentifier = "logyx-postgres" }
  alarm_description   = "RDS storage < 20 GB libre"
  alarm_actions       = [aws_sns_topic.alerts.arn]
}

# Alarma: Redis eviction (memoria llena)
resource "aws_cloudwatch_metric_alarm" "redis_evictions" {
  alarm_name          = "logyx-redis-evictions"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "Evictions"
  namespace           = "AWS/ElastiCache"
  period              = 300
  statistic           = "Sum"
  threshold           = 100
  alarm_description   = "Redis está eviccionando items — posible falta de memoria"
  alarm_actions       = [aws_sns_topic.alerts.arn]
}

3.4 Log Insights — Queries de operación

-- Query 1: Endpoints más lentos (últimas 24h)
fields @timestamp, @message
| filter @logStream like "logyx-backend"
| filter @message like "duration="
| parse @message "duration=*ms" as duration_ms
| stats avg(duration_ms) as avg_ms, max(duration_ms) as max_ms,
        count(*) as requests
  by endpoint
| sort avg_ms desc
| limit 20

-- Query 2: Errores de aplicación
fields @timestamp, @message, level, logger
| filter level = "ERROR"
| filter @logStream like "logyx-backend"
| sort @timestamp desc
| limit 50

-- Query 3: Requests del cliente (IP) con más llamadas
fields @timestamp, clientIP, requestURL, httpStatus
| filter @logStream like /alb/
| stats count(*) as total_requests by clientIP
| sort total_requests desc
| limit 20

-- Query 4: Métricas de smart load planner
fields @timestamp, @message
| filter @logStream like "smart-load-planner"
| filter @message like "sugerencias generadas"
| parse @message "SmartLoadPlanner: * sugerencias" as count
| stats sum(count) as total_sugerencias by bin(1d)

Sección 4 — Procedimientos Operativos y Eficiencia (C3.3)

4.1 Runbook — Deploy a producción

#!/bin/bash
# deploy-production.sh — proceso estándar de deploy

set -euo pipefail

VERSION=${1:-"latest"}
echo "=== LOGYX Deploy v${VERSION} ==="

# Paso 1: Crear snapshot de BD pre-deploy
echo "[1/6] Creando snapshot RDS..."
./scripts/backup-before-deploy.sh

# Paso 2: Build y push imagen
echo "[2/6] Build imagen Docker..."
cd logyx-backend
./mvnw package -DskipTests -q
docker build -t logyx/quarkus-backend:${VERSION} \
  -f src/main/docker/Dockerfile.jvm .
docker push ACCOUNT.dkr.ecr.sa-east-1.amazonaws.com/logyx/quarkus-backend:${VERSION}
cd ..

# Paso 3: Ejecutar migraciones Flyway
echo "[3/6] Ejecutando migraciones de BD..."
./scripts/flyway-migrate.sh

# Paso 4: Actualizar Launch Template con nueva imagen
echo "[4/6] Actualizando Launch Template..."
aws ec2 create-launch-template-version \
  --launch-template-id "${LAUNCH_TEMPLATE_ID}" \
  --source-version '$Latest' \
  --launch-template-data "{\"ImageId\":\"ami-latest\",\"UserData\":\"...\"}"

# Paso 5: Rolling update del ASG
echo "[5/6] Rolling update ASG (0% downtime)..."
aws autoscaling start-instance-refresh \
  --auto-scaling-group-name logyx-asg-quarkus \
  --preferences '{"MinHealthyPercentage":50,"InstanceWarmup":120}'

# Esperar hasta que el refresh termine
aws autoscaling wait ...

# Paso 6: Smoke tests
echo "[6/6] Smoke tests..."
sleep 30
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://api.logyx.pe/q/health/live)
if [ "$STATUS" != "200" ]; then
  echo "ERROR: Health check falló después del deploy. Iniciando rollback..."
  aws autoscaling cancel-instance-refresh \
    --auto-scaling-group-name logyx-asg-quarkus
  exit 1
fi

echo "=== Deploy ${VERSION} completado exitosamente ==="

4.2 Runbook — Respuesta a incidente de disponibilidad

NIVEL 1 — Alerta menor (ALB 4xx elevados):
  1. Revisar CloudWatch dashboard
  2. Revisar logs de aplicación en Log Insights
  3. Identificar endpoint problemático
  4. Escalar si el problema persiste > 15 min

NIVEL 2 — Alerta mayor (ALB 5xx > 10, latencia > 2s):
  1. Verificar health de instancias EC2: aws ec2 describe-instances
  2. Verificar RDS: aws rds describe-db-instances
  3. Verificar Redis: aws elasticache describe-replication-groups
  4. Si EC2 down: forzar Instance Refresh en ASG
  5. Si RDS down: verificar si Multi-AZ failover ocurrió
  6. Documentar en postmortem dentro de 48h

NIVEL 3 — Incidente crítico (servicio totalmente caído):
  1. Notificar al equipo inmediatamente
  2. Revisar GuardDuty por posible ataque
  3. Revisar CloudTrail por cambios no autorizados
  4. Si es necesario: restore desde último snapshot RDS
  5. Si es necesario: rollback a versión anterior (ECR tag anterior)
  6. Comunicar a usuarios: status.logyx.pe

4.3 Análisis de eficiencia energética y costos

AWS no expone métricas de consumo eléctrico directo, pero proporciona herramientas equivalentes:

# AWS Cost Explorer — costo por servicio este mes
aws ce get-cost-and-usage \
  --time-period Start=$(date -d "first day of month" +%Y-%m-%d),End=$(date +%Y-%m-%d) \
  --granularity MONTHLY \
  --metrics "BlendedCost" \
  --group-by Type=DIMENSION,Key=SERVICE

# AWS Compute Optimizer — recomendaciones de rightsizing
aws compute-optimizer get-ec2-instance-recommendations \
  --filters Name=Finding,Values=OVER_PROVISIONED

# Carbon footprint report (AWS Customer Carbon Footprint Tool)
# Disponible en AWS Cost Management Console
# → sa-east-1 usa energía renovable (Brasil ~85% hidroeléctrica)

Compromisos de AWS para eficiencia: - AWS apunta a 100% energía renovable para 2025 - sa-east-1 (São Paulo) opera mayoritariamente con energía hidroeléctrica - El uso de Fargate en lugar de EC2 dedicado para jobs reduce EC2 idle un ~40%

4.4 Métricas de disponibilidad medidas

Mes Uptime Incidentes MTTR Causa principal
Ago 2026 (completar) (completar) (completar)
Sep 2026 (completar) (completar) (completar)
Oct 2026 (completar) (completar) (completar)
Nov 2026 (completar) (completar) (completar)

Anexos

Anexo A — Evidencias de implementación

Evidencia Estado
AWS Console — EC2 ASG con 2 instancias healthy (agregar captura)
AWS Console — RDS Multi-AZ status: available (agregar captura)
AWS Console — ElastiCache cluster primary + replica (agregar captura)
AWS Console — S3 bucket con versioning ON + encryption (agregar captura)
CloudWatch Dashboard — métricas en tiempo real (agregar captura)
ALB health checks all healthy (agregar captura)
Flyway — output de flyway info con V1–V9 SUCCESS (agregar captura)
Rolling deploy completado sin downtime (agregar captura de logs)
Prueba de restauración RDS — output del script (agregar output)

Anexo B — URLs de recursos operativos

Recurso URL / Comando
API Health https://api.logyx.pe/q/health
API Swagger https://api.logyx.pe/q/swagger-ui
CloudWatch Dashboard https://console.aws.amazon.com/cloudwatch/home?region=sa-east-1#dashboards:name=LOGYX-Operations
Wazuh Dashboard https://10.0.30.10:443 (via Bastion)
Status Page https://status.logyx.pe (pendiente)

LOGYX · S2 Entregable 3 · Implementación del Centro de Datos · Competencias C3.2–C3.3 · Agosto–Noviembre 2026