Azure y Contenedores: Reducir Costos al Máximo
DevOps

Azure y Contenedores: Reducir Costos al Máximo

Reduce la factura de Azure con contenedores: Container Apps vs AKS vs ACI, ACR optimizado, spot instances, auto-scaling real y las trampas de costos que nadie documenta.

Por Omar Flores
#devops #docker #azure #optimization #performance #best-practices #ci-cd #senior

Azure es como un hotel cinco estrellas con servicio a la habitación disponible las 24 horas. Si llamas y pides algo, te lo traen. La comodidad es real. El problema aparece al final del mes cuando llega la cuenta y descubres que pediste agua mineral de $15 cuando el agua del grifo era gratuita, que dejaste el aire acondicionado encendido toda la semana aunque solo estuviste dos días, y que el minibar se cobró automáticamente por abrirlo aunque no hayas tomado nada.

Azure funciona exactamente igual. Los servicios están ahí, son buenos, y es fácil usarlos sin entender exactamente cuánto cuestan. La factura llega 30 días después. Y para entonces, el costo ya se acumuló.

Este post es el mapa de los cargos que nadie documenta, con las decisiones concretas que reducen la factura de Azure para stacks basados en contenedores.

Por qué Azure cobra más de lo que esperas

Antes de optimizar, hay que entender el modelo de precios. Azure no cobra por lo que usas — cobra por lo que tienes aprovisionado, por lo que transfiere datos, y por servicios auxiliares que se activan automáticamente.

Las tres fuentes de costo más comunes en stacks con contenedores:

Ancho de banda de salida. Las transferencias de datos que salen de Azure a internet tienen costo. Los primeros 100GB al mes son gratuitos. Después, dependiendo de la región, entre $0.08 y $0.12 por GB. Un servicio que envía 500GB al mes de respuestas API está pagando entre $32 y $48 solo en transferencia — sin contar el cómputo.

Almacenamiento de logs. Azure Monitor y Log Analytics cobran por GB ingestado. La configuración por defecto en AKS y Container Apps activa diagnósticos verbosos. He visto proyectos pequeños con $80/mes solo en logs porque nadie ajustó la retención ni el nivel de verbosidad.

Recursos idle. Un AKS cluster con dos nodos de Standard_D2s_v3 corriendo durante el fin de semana sin tráfico cuesta igual que durante la semana. Los nodos no saben que no hay usuarios — siguen corriendo y cobrando.

# Ver el costo estimado de recursos actuales en tu suscripción
az consumption usage list \
  --start-date 2026-03-01 \
  --end-date 2026-03-09 \
  --query "[].{name:instanceName, cost:pretaxCost, currency:currency}" \
  --output table

# Recursos que llevan más de 7 días sin actividad significativa
az monitor metrics list \
  --resource /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ContainerService/managedClusters/{cluster} \
  --metric "node_cpu_usage_percentage" \
  --interval PT1H \
  --start-time 2026-03-02T00:00:00Z \
  --end-time 2026-03-09T00:00:00Z \
  --output table

La decisión más importante: elegir el servicio correcto

Azure tiene cuatro formas de correr contenedores y cada una tiene un perfil de costo radicalmente diferente. Elegir mal aquí determina si pagas $15/mes o $300/mes por la misma carga de trabajo.

Azure Container Instances (ACI)

Pago por segundo de ejecución. No hay nodos que mantener. Ideal para tareas batch, jobs programados, y cargas de trabajo esporádicas.

Precio: ~$0.0000015/vCPU-segundo + $0.00000015/GB-segundo
Un contenedor con 1 vCPU y 2GB corriendo 8 horas al día:
- vCPU: 1 × 0.0000015 × 28800s = $0.043/día
- RAM:  2 × 0.00000015 × 28800s = $0.0086/día
Total: ~$1.55/mes

El mismo contenedor corriendo 24/7:
- Total: ~$4.64/mes

ACI es la opción más barata para cargas de trabajo que no necesitan estar disponibles continuamente. Un job que procesa datos una vez al día, un scraper que corre cada hora, un servicio de reportes que solo se activa bajo demanda — todos son candidatos perfectos para ACI.

Lo que ACI no puede hacer: no tiene load balancer integrado, no escala automáticamente bajo carga, no tiene persistencia de estado entre ejecuciones. Es un contenedor que arranca, corre, y termina.

Azure Container Apps

La opción de sweet spot para la mayoría de aplicaciones web. Escala desde cero hasta N réplicas según carga, cobra por uso real, y abstrae completamente la gestión de nodos.

Precio:
- vCPU: $0.000024/vCPU-segundo activo
- RAM:  $0.000003/GB-segundo activo
- Escala a cero: $0 cuando no hay tráfico

Una API con 0.5 vCPU y 1GB, activa 12 horas/día (tráfico real):
- vCPU: 0.5 × 0.000024 × 43200s = $0.518/mes
- RAM:  1 × 0.000003 × 43200s = $0.129/mes
Total: ~$0.65/mes + requests ($0.000004 por cada 1000 requests)

Container Apps escala a cero — cuando no hay tráfico, no hay costo de cómputo. Para aplicaciones con tráfico diurno y nulo nocturno, esto puede reducir el costo de cómputo a la mitad sin ningún cambio en el código.

La limitación: el cold start cuando escala desde cero puede tardar 2-5 segundos. Para APIs públicas con SLA estricto, esto puede no ser aceptable. La solución es configurar un mínimo de una réplica:

az containerapp update \
  --name mi-api \
  --resource-group mi-rg \
  --min-replicas 1 \
  --max-replicas 10

Con --min-replicas 1 siempre hay una instancia activa. Pierdes el beneficio del scale-to-zero pero mantienes el auto-scaling hacia arriba.

AKS (Azure Kubernetes Service)

AKS es el servicio más poderoso y el más caro de operar correctamente. El control plane de Kubernetes es gratuito en Azure — pagas los nodos worker.

Nodo Standard_D2s_v3: 2 vCPU, 8GB RAM — $0.096/hora = $70/mes
Cluster mínimo viable (2 nodos para HA): $140/mes

Cluster de producción típico (3 nodos Standard_D4s_v3):
- 3 × $0.192/hora = $0.576/hora = $420/mes

AKS solo tiene sentido si genuinamente necesitas las capacidades de Kubernetes: múltiples equipos compartiendo el mismo cluster, workloads complejos con dependencias entre servicios, o requisitos de compliance que exigen control total sobre la infraestructura.

Para la mayoría de startups y proyectos medianos, Container Apps ofrece 80% de las capacidades de Kubernetes al 10% del costo operativo.

Cuándo usar cada opción:

ACI           → Jobs, tareas batch, cargas esporádicas
Container Apps → APIs, frontends, microservicios con tráfico variable
AKS           → Plataformas multi-tenant, equipos grandes, compliance estricto

Azure Container Registry: el costo invisible

ACR cobra por storage de imágenes y por transferencia. Lo que la mayoría de equipos no configura: la limpieza automática de imágenes antiguas.

ACR Basic:   $0.167/día (~$5/mes) + $0.003/GB almacenado
ACR Standard: $0.667/día (~$20/mes) + $0.003/GB almacenado
ACR Premium:  $1.667/día (~$50/mes) + $0.003/GB almacenado

El tier Basic es suficiente para la mayoría de equipos pequeños y medianos. Standard añade geo-replicación y webhooks. Premium añade redes privadas y zona de disponibilidad.

El costo oculto es el storage acumulado. Sin una política de limpieza, cada build añade una imagen al registry. Con 20 builds al día durante 6 meses, tienes 3,600 imágenes. Si cada una pesa 200MB son 720GB — $2.16/día solo en storage.

Política de retención automática

ACR tiene una política de retención nativa que elimina imágenes sin tag después de N días:

# Activar política de retención — eliminar imágenes sin tag después de 7 días
az acr config retention update \
  --registry mi-registry \
  --type UntaggedManifests \
  --days 7 \
  --status enabled

# Ver el estado actual de la política
az acr config retention show \
  --registry mi-registry

Para imágenes con tag, la herramienta acr purge tiene más control:

# Eliminar imágenes del repositorio mi-api con más de 30 días
# excepto las últimas 5 — ejecutar como tarea programada en ACR
az acr task create \
  --registry mi-registry \
  --name purge-old-images \
  --cmd "acr purge \
    --filter 'mi-api:.*' \
    --ago 30d \
    --keep 5 \
    --untagged" \
  --schedule "0 1 * * *" \
  --context /dev/null

# Ejecutar manualmente si necesitas limpieza inmediata
az acr run \
  --registry mi-registry \
  --cmd "acr purge --filter 'mi-api:.*' --ago 30d --keep 5 --untagged" \
  /dev/null

Esta tarea corre cada día a la 1am y elimina todas las imágenes del repositorio mi-api que tienen más de 30 días, manteniendo siempre las últimas 5. En un registry activo, puede liberar 200-500GB al mes.

Imágenes mínimas reducen el costo de storage y transferencia

Cada push de una imagen de 800MB a ACR y cada pull al Container App cuesta en transferencia. Una imagen de 12MB hace esas operaciones 66 veces más rápido y barato.

# Comparar tamaño antes y después de optimizar
docker images mi-api --format "table {{.Tag}}\t{{.Size}}"

# Build con target específico para asegurar que se usa la etapa de producción
docker build --target production -t mi-api:latest .
docker push mi-registry.azurecr.io/mi-api:latest

La transferencia entre ACR y Container Apps en la misma región es gratuita. Entre regiones distintas tiene costo. Si tu ACR está en eastus y tu Container App en westus2, cada pull paga transferencia inter-regional.

# Verificar que ACR y Container App están en la misma región
az acr show --name mi-registry --query location -o tsv
az containerapp show --name mi-api --resource-group mi-rg --query location -o tsv

Spot instances y nodos preemptibles en AKS

Si usas AKS, los spot node pools pueden reducir el costo de nodos entre 60% y 90%. Azure puede recuperar los nodos spot con 30 segundos de aviso, pero para cargas de trabajo tolerantes a interrupciones — workers de procesamiento de datos, jobs de ML, sistemas de caché regenerable — son la opción más barata disponible.

# Añadir un spot node pool a un cluster AKS existente
az aks nodepool add \
  --resource-group mi-rg \
  --cluster-name mi-cluster \
  --name spotnodes \
  --priority Spot \
  --eviction-policy Delete \
  --spot-max-price -1 \           # -1 = pagar el precio de mercado actual
  --node-count 2 \
  --node-vm-size Standard_D4s_v3 \
  --labels "nodepool=spot" \
  --node-taints "kubernetes.azure.com/scalesetpriority=spot:NoSchedule"

El taint NoSchedule garantiza que solo los pods explícitamente diseñados para spot se programen en estos nodos. En el deployment de Kubernetes, añades la toleración:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker-procesamiento
spec:
  replicas: 3
  template:
    spec:
      tolerations:
        - key: "kubernetes.azure.com/scalesetpriority"
          operator: "Equal"
          value: "spot"
          effect: "NoSchedule"
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: "nodepool"
                    operator: In
                    values: ["spot"]
      containers:
        - name: worker
          image: mi-registry.azurecr.io/worker:latest
          # Los workers deben ser stateless y capaces de reiniciarse limpiamente
Nodo Standard_D4s_v3 regular:  $0.192/hora = $140/mes
Nodo Standard_D4s_v3 spot:     ~$0.02-0.06/hora = $14-43/mes (70-90% menos)

Para el API principal — donde la disponibilidad es crítica — mantén nodos regulares. Para todo lo que puede reiniciarse sin impacto al usuario, los spot son dinero ahorrado.

Auto-scaling: pagar por lo que se usa, no por lo que podría necesitarse

El anti-patrón más caro en Azure: aprovisionar para el pico y pagar por ese pico las 24 horas. Una aplicación con tráfico de 100 req/s en hora punta y 2 req/s a las 3am no necesita los mismos recursos todo el día.

Container Apps: KEDA y auto-scaling por métricas reales

Container Apps usa KEDA internamente. Puedes escalar por HTTP requests, CPU, memoria, o mensajes en una cola de Azure Service Bus:

# Escalar basado en peticiones HTTP concurrentes
az containerapp update \
  --name mi-api \
  --resource-group mi-rg \
  --min-replicas 1 \
  --max-replicas 20 \
  --scale-rule-name http-scaling \
  --scale-rule-type http \
  --scale-rule-http-concurrency 50   # Una réplica por cada 50 peticiones concurrentes

Con esta configuración, bajo carga normal (5 peticiones concurrentes) corre 1 réplica. En hora punta (500 peticiones concurrentes) escala automáticamente a 10 réplicas. A las 3am con 0 tráfico, escala a 0 si tienes --min-replicas 0.

# Escalar por cola de mensajes — ideal para workers
az containerapp update \
  --name mi-worker \
  --resource-group mi-rg \
  --min-replicas 0 \
  --max-replicas 10 \
  --scale-rule-name queue-scaling \
  --scale-rule-type azure-queue \
  --scale-rule-metadata "queueName=mi-cola" "queueLength=10" \
  --scale-rule-auth "connection=mi-servicebus-connection"

Este worker arranca cuando hay mensajes en la cola y se detiene completamente cuando la cola está vacía. Costo: cero cuando no hay trabajo.

AKS: Cluster Autoscaler + Horizontal Pod Autoscaler

En AKS, la combinación de HPA (escala pods) y Cluster Autoscaler (escala nodos) permite pagar exactamente por la carga actual.

# HPA — escalar pods basado en CPU
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mi-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: mi-api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60    # Escalar cuando CPU promedio supere 60%
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 75
# Activar Cluster Autoscaler — añade o elimina nodos según los pods pendientes
az aks nodepool update \
  --resource-group mi-rg \
  --cluster-name mi-cluster \
  --name nodepool1 \
  --enable-cluster-autoscaler \
  --min-count 2 \
  --max-count 10

Con ambos activos: cuando el tráfico aumenta, HPA crea más pods. Si no hay nodos disponibles para programarlos, Cluster Autoscaler añade nodos. Cuando el tráfico baja, HPA elimina pods y Cluster Autoscaler elimina nodos vacíos después de 10 minutos (configurable con --scale-down-delay-after-delete).

El ahorro real depende del patrón de tráfico. Para aplicaciones con picos diurnos y valles nocturnos, el ahorro puede ser del 40-60% del costo de nodos.

Reducir el costo de logs y monitoreo

Los logs son el costo invisible que más sorprende a los equipos cuando llega la primera factura real.

Log Analytics:
- Ingesta: $2.76/GB (primeros 5GB gratuitos al mes)
- Retención: primeros 31 días gratuitos, luego $0.12/GB/mes
- Exportación: $0.10/GB

Una aplicación que genera 10GB de logs al mes:
- Ingesta: (10 - 5) × $2.76 = $13.80/mes
- Si los guardas 90 días: (10 GB × 2 meses adicionales) × $0.12 = $2.40/mes
Total: ~$16/mes solo en logs de una app

Ajustar nivel de verbosidad por ambiente

El error más común: el mismo nivel de logging en desarrollo y producción. En desarrollo quieres todo. En producción, solo lo que indica un problema.

// En Go — nivel de log configurable por variable de entorno
import "log/slog"

func setupLogger() *slog.Logger {
    level := slog.LevelInfo   // Default: solo Info, Warn, Error
    if os.Getenv("LOG_LEVEL") == "debug" {
        level = slog.LevelDebug
    }
    return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: level,
    }))
}
# Container App — LOG_LEVEL=info en producción
env:
  - name: LOG_LEVEL
    value: info   # No debug en producción — reduce volumen de logs 5-10x

Sampling de logs para tráfico alto

Para aplicaciones con alto volumen de requests, logear cada petición exitosa es redundante. Si el 99% de tus peticiones responden 200 en menos de 100ms, logear cada una solo añade ruido y costo.

// Logear solo peticiones lentas, errores, y una muestra del tráfico normal
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{ResponseWriter: w, status: 200}

        next.ServeHTTP(rw, r)

        duration := time.Since(start)
        status := rw.status

        // Siempre logear errores y peticiones lentas
        shouldLog := status >= 400 || duration > 500*time.Millisecond

        // Logear 1% del tráfico exitoso y rápido como muestra
        if !shouldLog && rand.Intn(100) == 0 {
            shouldLog = true
        }

        if shouldLog {
            slog.Info("request",
                "method", r.Method,
                "path", r.URL.Path,
                "status", status,
                "duration_ms", duration.Milliseconds(),
            )
        }
    })
}

Con este patrón, una API con 1 millón de requests exitosos al día genera logs para ~10,000 de ellos en lugar de 1,000,000. El 99% del tráfico normal no produce logs. Los errores y la latencia alta siempre se registran.

Política de retención corta en Log Analytics

La retención por defecto en Log Analytics es 30 días. Para la mayoría de equipos, 7-14 días es suficiente para debugging de incidentes recientes.

# Reducir retención a 7 días para reducir costos de storage
az monitor log-analytics workspace update \
  --resource-group mi-rg \
  --workspace-name mi-workspace \
  --retention-time 7

# Para tablas específicas — retención granular por tipo de log
az monitor log-analytics workspace table update \
  --resource-group mi-rg \
  --workspace-name mi-workspace \
  --name ContainerLog \
  --retention-time 7

# Logs de auditoría y seguridad: retención más larga
az monitor log-analytics workspace table update \
  --resource-group mi-rg \
  --workspace-name mi-workspace \
  --name AzureActivity \
  --retention-time 90

Reserved Instances: el descuento que pocos aprovechan

Si tienes cargas de trabajo que corren continuamente — un AKS con nodos siempre activos, una base de datos Azure SQL o Cosmos DB, Container Apps con min-replicas >= 1 — las Reserved Instances ofrecen descuentos del 30-60% a cambio de un compromiso de 1 o 3 años.

Standard_D4s_v3 (4 vCPU, 16GB):
- Pay-as-you-go: $0.192/hora = $140/mes
- 1 año Reserved:  $0.118/hora = $86/mes  (38% descuento)
- 3 años Reserved: $0.074/hora = $54/mes  (61% descuento)

Para un cluster AKS con 3 nodos Standard_D4s_v3:
- Pay-as-you-go: $420/mes
- 1 año Reserved: $258/mes    (ahorro: $162/mes, $1,944/año)
- 3 años Reserved: $162/mes   (ahorro: $258/mes, $9,288 en 3 años)

La decisión es simple: si un recurso lleva más de 6 meses funcionando y no tienes planes de eliminarlo, la Reserved Instance ya se pagó sola comparado con pay-as-you-go.

# Ver recomendaciones de reservas basadas en tu uso actual
az consumption reservation recommendation list \
  --scope /subscriptions/{subscription-id} \
  --look-back-period Last30Days \
  --reservation-term P1Y \
  --query "[].{resource:skuName, savings:netSavings, currency:currency}" \
  --output table

Azure genera automáticamente recomendaciones basadas en tu historial de uso. Si un recurso aparece en esa lista, estás pagando de más.

Detector de recursos huérfanos

Los recursos que nadie usa pero que siguen costando son la mayor fuente de desperdicio en Azure. Discos no conectados, IPs públicas sin asignar, snapshots de meses atrás, load balancers sin backends.

# Discos no conectados (managed disks sin VM asociada)
az disk list \
  --query "[?diskState=='Unattached'].{name:name, size:diskSizeGb, rg:resourceGroup, cost:'revisar'}" \
  --output table

# IPs públicas no asociadas a ningún recurso
az network public-ip list \
  --query "[?ipConfiguration==null].{name:name, rg:resourceGroup, sku:sku.name}" \
  --output table

# Snapshots con más de 90 días
az snapshot list \
  --query "[?timeCreated<'2025-12-09'].{name:name, size:diskSizeGb, created:timeCreated}" \
  --output table

# Load balancers sin backends configurados
az network lb list \
  --query "[?backendAddressPools[0].backendIPConfigurations==null].{name:name, rg:resourceGroup}" \
  --output table

Ejecutar estas cuatro consultas una vez al mes y eliminar lo que encuentran puede ahorrar entre $20 y $200 al mes dependiendo del tamaño del entorno. Un disco Premium_LRS de 256GB no conectado cuesta $35/mes. Una IP pública cuesta $3.60/mes. A escala de una empresa con decenas de proyectos, esto se convierte en miles de dólares al año.

Azure Policy para prevenir el desperdicio

Prevenir es más barato que limpiar. Una Azure Policy que prohíbe crear recursos sin etiquetas obligatorias fuerza a los equipos a identificar a qué proyecto y ambiente pertenece cada recurso:

{
  "mode": "Indexed",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "tags['environment']",
          "exists": "false"
        },
        {
          "field": "tags['project']",
          "exists": "false"
        }
      ]
    },
    "then": {
      "effect": "deny"
    }
  }
}
# Aplicar la policy a un resource group
az policy assignment create \
  --name "require-tags" \
  --scope /subscriptions/{sub}/resourceGroups/mi-rg \
  --policy "require-environment-project-tags"

Con tags obligatorios de environment y project, los reportes de costos en Azure Cost Management muestran exactamente cuánto cuesta cada proyecto y ambiente. El desperdicio se vuelve visible.

El stack mínimo de producción en Azure

Poniendo todo junto, un stack de producción real — API + frontend + base de datos — optimizado para costo mínimo en Azure:

# Resource group con tags obligatorios
az group create \
  --name mi-proyecto-prod \
  --location eastus \
  --tags environment=production project=mi-proyecto

# ACR Basic — suficiente para equipos pequeños
az acr create \
  --resource-group mi-proyecto-prod \
  --name miregistryprod \
  --sku Basic \
  --admin-enabled false   # Usar identidades administradas, no credenciales de admin

# Container Apps Environment — infraestructura compartida para todos los Container Apps
az containerapp env create \
  --name mi-entorno-prod \
  --resource-group mi-proyecto-prod \
  --location eastus

# API — escala a 0 fuera del horario de tráfico
az containerapp create \
  --name mi-api \
  --resource-group mi-proyecto-prod \
  --environment mi-entorno-prod \
  --image miregistryprod.azurecr.io/mi-api:latest \
  --cpu 0.5 \
  --memory 1.0Gi \
  --min-replicas 0 \
  --max-replicas 10 \
  --ingress external \
  --target-port 8080 \
  --registry-server miregistryprod.azurecr.io

# Frontend — puede ir a Cloudflare Pages o Blob Storage + CDN para costo cero
# Si necesita SSR en Container Apps:
az containerapp create \
  --name mi-frontend \
  --resource-group mi-proyecto-prod \
  --environment mi-entorno-prod \
  --image miregistryprod.azurecr.io/mi-frontend:latest \
  --cpu 0.25 \
  --memory 0.5Gi \
  --min-replicas 0 \
  --max-replicas 5 \
  --ingress external \
  --target-port 80
Costo estimado de este stack con tráfico moderado (12h activo/día):
- Container Apps API (0.5 vCPU activo 12h):    ~$0.52/mes
- Container Apps Frontend (0.25 vCPU 12h):     ~$0.26/mes
- ACR Basic:                                    ~$5/mes
- Azure Database for PostgreSQL Flexible (B1ms): ~$12/mes
- Transferencia de salida (primeros 100GB):     $0
Total: ~$18/mes

Si el frontend es completamente estático: despliégalo en Cloudflare Pages y el costo baja a cero. La API queda en Container Apps con scale-to-zero y pagas solo por el tráfico real.

Los números que cambian la conversación

Un equipo que migró de un AKS de 3 nodos (Standard_D4s_v3) a Container Apps con scale-to-zero:

Antes (AKS 3 nodos, pay-as-you-go):
- Nodos:       3 × $140/mes = $420/mes
- ACR Standard: $20/mes
- Log Analytics: $45/mes (logs verbosos)
- Discos, IPs:   $25/mes
Total: $510/mes

Después (Container Apps + ACR Basic + logs reducidos):
- Container Apps (tráfico real 10h/día): $8/mes
- ACR Basic con limpieza automática:     $5/mes
- Log Analytics con sampling:            $6/mes
Total: $19/mes

Ahorro: $491/mes — $5,892 al año

El código no cambió. Las capacidades no cambiaron. Solo cambió el servicio de Azure que ejecuta los contenedores y la configuración de logging.

Azure cobra lo que tiene aprovisionado, no lo que necesitas. La diferencia entre una factura de $500 y una de $20 no está en el código — está en entender qué estás pagando y por qué.