Skip to content

ADR-006: Migración de infraestructura — AWS → Azure

Date: 2026-08-03 Status: Rechazada (2026-08-07) — se sigue en AWS. Ver "Actualización" abajo.

Actualización (2026-08-07): la migración se frena, seguimos en AWS

Decisión de Carlos, 2026-08-07: no se migra. Staging y Producción se quedan en AWS.

El motivo no es técnico. Este ADR nunca argumentó que AWS estuviera mal elegido — existía porque el cliente había pedido el cambio de proveedor, y el pedido dejó de estar vigente. Con la condición de negocio caída, no queda ninguna razón para mover un Staging que ya está aplicado, sirviendo tráfico y con deploys automáticos andando desde el 2026-07-28.

Qué se deja de hacer, todo lo que este ADR planificaba: el cutover de DNS de Staging, la VPN Site-to-Site contra el FortiGate de CIS Latam, la construcción de Producción en Azure, y las cuatro decisiones abiertas del final (ancho de banda del enlace, redundancia del FortiGate, suscripción de Azure, datos del túnel). Ninguna hay que resolver ya.

Qué se recupera al no migrar — vale decirlo, porque el ADR lo había aceptado como costo:

  • La resiliencia de la base de datos. Aurora replica el storage 6 veces en 3 zonas; Flexible Server con HA replica a una standby. Migrar era aceptar esa regresión a cambio del cambio de proveedor. Sin el cambio de proveedor, no hay nada que aceptar.
  • El borde público 100% cloud-native. El diseño en Azure ponía al FortiGate on-premise de CIS Latam como puerta de entrada del sistema: si ese appliance, su enlace de internet o el túnel VPN caían, la app quedaba inalcanzable con Azure perfectamente sano. Ese punto único de falla, que en AWS no existe, ya no se introduce.
  • La ventana de doble costo y el ciclo de aprendizaje de un proveedor que nadie del equipo operó todavía.

El Terraform de Azure no se borra, pero tampoco viene a main. Los ~2.600 líneas de infra/terraform-azure/ (bootstrap, network, compute, database, redis, keyvault, identity, monitoring, vpn) quedan en el branch feature/migracion-azure, sin mergear. Si el pedido vuelve, el trabajo está hecho y este ADR sigue siendo el mapeo de servicios válido — habrá que revisar versiones de provider y poco más. Traerlo a main sería mantener indefinidamente infraestructura que nadie aplica.

Lo que sí queda como trabajo real, y es lo que motivó frenar acá: cerrar Staging en AWS con el sistema al día. Ver doc/environments/staging.md.


Lo que sigue es el ADR original, tal como se escribió el 2026-08-03. Se conserva completo y sin retoques: si el pedido de migrar vuelve, este es el análisis que evita rehacerlo desde cero.

Context

Requisito del cliente (CIS Latam): la infraestructura de Soterex tiene que migrar de AWS a Azure, como reemplazo total (no multi-cloud). Esto reabre una decisión ya tomada y aplicada — doc/architecture/ADR-003-database.md (Aurora), doc/architecture/aws-best-practices.md, y el review de consenso doc/architecture/infra-consensus.md (devops+security, 2026-07-25) — no por una razón técnica nueva, sino por una condición de negocio externa al proyecto. Este ADR no argumenta que AWS estuviera mal elegido; documenta el mapeo de servicios y las decisiones nuevas que requiere el mismo diseño en Azure.

Estado real de cada ambiente (esto determina el alcance del corte, ver más abajo):

AmbienteEstado en AWSQué implica para la migración
StagingAplicado y en uso real — servicios ECS corriendo, deploys automáticos funcionando (.github/workflows/deploy-staging.yml), datos de prueba de E1-E4 cargados, URL pública soterex.stag.cislatam.netEs el único ambiente que necesita migración real (levantar en paralelo + cutover de DNS)
ProducciónNunca se aplicó (doc/environments/production.md: "No provisionado todavía") — ni credenciales de Soterex de prod, ni Firebase de prodNo hay nada que migrar — se construye directo en Azure, de cero. Todo el Terraform de environments/production/ en AWS queda descartado sin haberse usado nunca

Esto simplifica mucho el alcance real: la única pieza delicada es cortar Staging sin perder continuidad; Producción es simplemente "construir en Azure" sin la carga de migrar nada.

Lo que no cambia: el frontend/docs (Vue 3 SPA + VitePress) se deployan a Firebase Hosting, que es independiente del proveedor de cloud del backend — esta migración no lo toca.

Restricción dura, no negociable: todo lo que el consenso devops+security de infra-consensus.md exigió sobre AWS (WAF frente al ALB, migraciones automáticas en el pipeline con corte si fallan, alarmas activas — no solo logs pasivos, scanning de secretos y dependencias en CI, IAM/identidad federada sin secretos de larga vida) tiene que preservarse 1:1 en Azure. Este ADR no repite ese review desde cero — traduce cada requisito ya aprobado a su control equivalente en Azure y lo deja explícito en la tabla de mapeo.

Decision

Migrar a Azure Container Apps (ACA) para compute, con el resto de los servicios mapeados 1:1 a su equivalente gestionado más directo en Azure (ver tabla de mapeo). Se descarta AKS como opción de compute (ver Alternativas). Autenticación de despliegue vía OIDC federado de GitHub Actions a Microsoft Entra ID (equivalente exacto al patrón OIDC ya usado con AWS — sin credenciales de larga vida en secrets).

Mapeo de servicios AWS → Azure

FunciónAWS (actual)Azure (propuesto)Notas
Compute (web + queue worker)ECS Fargate (2 servicios, autoscaling por CPU)Azure Container Apps (2 apps: web, queue), autoscaling por CPU/HTTP concurrency (KEDA)Ver sección "Compute" abajo — es la decisión con más superficie de alternativas
Job programado (envío periódico a Soterex)EventBridge Scheduler → ecs:RunTask (módulo scheduler)ACA Jobs con trigger tipo Schedule (cron nativo)Simplifica: no hace falta un módulo de scheduler aparte — es una feature nativa de Container Apps Jobs, un recurso menos que mantener
Migraciones en el pipelineECS RunTask puntual con php artisan migrate --force, corta el deploy si fallaACA Jobs de tipo Manual/Schedule con override de comando, mismo patrón: se dispara desde el workflow, se espera el resultado, se corta el deploy si el exit code ≠ 0Mismo principio exigido por el consenso (hallazgo #5 de infra-consensus.md), otro backend
Load balancer + TLSApplication Load Balancer (ALB), públicoIngress de Container Apps en modo INTERNO (no público — ver fila WAF, cambia el borde de la red)Deja de ser el borde público del sistema — ahora el borde es el FortiGate on-premise de CIS Latam
WAFAWS WAFv2 (rate-based rule + Managed Common Rule Set + IP Reputation List), asociado al ALBFortiGate on-premise, existente en CIS Latam — pasa a ser el borde público real (IP/DNS de entrada), filtra antes de reenviar el tráfico a Azure por un túnel privadoCambio de arquitectura, no solo de proveedor — ver "Conectividad on-prem" abajo y "Riesgos". Confirmado por vos: FortiGate (no FortiWeb), appliance físico/virtual en el datacenter de CIS Latam, no en Azure
Conectividad on-prem ↔ AzureNo existía — todo el tráfico entraba directo a AWS, sin dependencia de red del clienteAzure VPN Gateway (Site-to-Site, IPsec) entre la VNet de Soterex y el FortiGate de CIS LatamComponente nuevo, no tiene equivalente en el diseño AWS — ver "Decisiones abiertas"
Base de datosAurora PostgreSQL Serverless v2 (auto-scaling ACUs, 6 réplicas de storage en 3 AZs)Azure Database for PostgreSQL – Flexible Server, SKU Burstable/General Purpose + auto-grow de storage, zone-redundant HA en producciónVer "Pérdida de capacidad" abajo — no es un mapeo 1:1 perfecto
Cache/queue (Redis)ElastiCache for Redis (replication group)Azure Cache for Redis (tier Basic en staging, Standard/Premium con réplica en producción)Mapeo directo, mismo modelo de tiers por ambiente que ya existe (num_cache_clusters=1 en staging)
Registro de imágenesECRAzure Container Registry (ACR)Mapeo directo
SecretsAWS Secrets Manager (app_key, database_url, firebase_credentials, credenciales Soterex)Azure Key Vault, referenciado desde Container Apps vía secret referencesMapeo directo
Logs + alarmasCloudWatch Logs + CloudWatch Alarms + SNS (email)Azure Monitor (Log Analytics workspace, mismas queries vía KQL) + Alertas de Azure Monitor + Action Group (email)Mapeo directo, mismo alcance de alarmas (5xx, hosts no saludables → réplicas no listas, CPU de compute, CPU/conexiones de DB)
Identidad de deploy (CI/CD)IAM Role + GitHub OIDC provider (module github-oidc)Federated Identity Credential sobre una User-Assigned Managed Identity / App Registration en Entra ID, con subject acotado a repo:raxardev/soterex:environment:staging (mismo patrón que ya se usa con AWS — y el mismo gotcha ya resuelto: usar el claim environment, no ref:refs/heads, ver commits Fix OIDC en el historial)Mismo modelo de seguridad, sin secretos de larga vida en GitHub
Terraform stateS3 + DynamoDB (lock)Azure Storage Account (blob) + lease-based locking nativo del backend azurermMapeo directo
RedVPC con subnets públicas (solo LB)/privadas (compute, DB, cache), NAT GatewayVNet con el mismo esquema de subnets públicas/privadas + NAT Gateway de AzureMapeo directo, mismo principio de "nada con IP pública salvo el borde" (aws-best-practices.md, sección 6)
Contenedor arranca como root (nginx+supervisord)Riesgo aceptado documentado (infra-consensus.md, hallazgo #8)Se mantiene igual — no es específico de AWS, es de la imagen DockerSin cambios

Compute: por qué Azure Container Apps y no AKS

Se evaluaron 3 opciones:

1. Azure Container Apps (ACA) — elegido.

  • Pros: modelo serverless-de-contenedores, igual que ECS Fargate — no hay nodos ni cluster que administrar, autoscaling nativo (incluye scale-to-zero, que ECS Fargate no ofrece), Jobs nativos con trigger de cron (reemplaza el módulo scheduler completo), ingress HTTP con TLS managed incorporado, curva de aprendizaje mínima para un equipo que nunca operó Kubernetes.
  • Contras: menos control fino que AKS sobre networking interno (sidecars custom, service mesh) — no aplica hoy: esta app no necesita nada de eso.

2. Azure Kubernetes Service (AKS) — descartado.

  • Pros: máximo control, ecosistema más grande, portabilidad total.
  • Contras: exactamente el mismo argumento por el que el equipo eligió ECS Fargate en vez de EKS originalmente (nunca documentado como ADR propio porque no hizo falta — Fargate fue la elección obvia): un cluster de Kubernetes es infraestructura que alguien tiene que operar (upgrades de versión, node pools, RBAC interno) para un equipo de 2 desarrolladores Laravel/Vue sin experiencia previa en K8s. Sería repetir el mismo error que el criterio general de arquitectura de este equipo ya evita explícitamente ("un monolito bien estructurado" > complejidad operativa no justificada por la escala actual).

3. Azure Container Instances (ACI) — descartado rápido.

  • Pros: el más simple de los tres.
  • Contras: sin modelo de "servicio" real (no autoscaling por carga, no rolling deployments, no health checks de aplicación) — es para contenedores efímeros/batch, no para un servicio web persistente con dos procesos (web + queue) que necesitan permanecer arriba y escalar.

Rationale

  • El criterio rector de este equipo ya está escrito (architect SKILL.md, sección "Common Architecture Decisions"): "Monolito vs Microservicios: default a un monolito modular bien estructurado. Microservicios [y, por extensión, K8s crudo] se justifican cuando hay necesidad real de cadencias de deploy independientes o escala radicalmente distinta por servicio." Nada de eso cambió con el pedido de migrar de cloud — el pedido es "mismo sistema, otro proveedor", no "rediseñar la arquitectura".
  • ACA es, servicio por servicio, la traducción más directa de lo que ya existe (Fargate→ACA, EventBridge Scheduler→ACA Jobs, que de hecho simplifica el Terraform actual al eliminar un módulo entero).
  • Mantener el mismo nivel de garantías de seguridad que ya se exigió en AWS (WAF, alarmas activas, identidad federada sin secretos) no es opcional — es la restricción dura de este ADR, no una aspiración.

Alternatives Considered

  • AKS: descartado, ver arriba — complejidad operativa no justificada por el tamaño del equipo ni por ningún requisito de escala real hoy.
  • ACI: descartado, ver arriba — no cubre el modelo de servicio persistente que necesita la app.
  • Multi-cloud (mantener AWS Staging, sumar Azure): descartado por el usuario explícitamente — el pedido es reemplazo total, no coexistencia. Se documenta acá para que quede registrado que se consideró y por qué no aplica: hubiera evitado el riesgo de cutover, a costa de mantener el doble de infraestructura y de complejidad de CI/CD indefinidamente, sin que el cliente lo haya pedido.
  • Azure Database for PostgreSQL – Cosmos DB for PostgreSQL (Citus, distribuido): descartado — es para volúmenes que necesitan sharding horizontal; este proyecto no tiene ese volumen ni lo va a tener en el horizonte de este ADR. Flexible Server es el default correcto (mismo criterio que ya usó ADR-003 para elegir Aurora sobre alternativas más exóticas).
  • WAF gestionado de Azure (Front Door Premium o Application Gateway v2 con WAF Policy): era la propuesta original de este ADR — descartada porque el cliente ya tiene FortiGate on-premise en CIS Latam y lo quiere usar como el borde/WAF del sistema, en vez de sumar un WAF más del lado de Azure. Queda como alternativa de respaldo si en la revisión de security el FortiGate no resulta suficiente por sí solo (ver "Decisiones abiertas").

Consequences

  • Positivo: el modelo serverless-de-contenedores se mantiene igual que en AWS — no se suma complejidad operativa nueva. El módulo scheduler desaparece (ACA Jobs lo cubre nativo), Terraform queda más simple, no más complejo. La identidad de CI/CD sigue sin secretos de larga vida (mismo nivel de seguridad).
  • Negativo — pérdida real de capacidad, no cosmética: Aurora replica storage 6 veces en 3 AZs de forma nativa y separa compute de storage; Azure Database for PostgreSQL Flexible Server con HA zone-redundant replica a una réplica standby (no 6 copias), y el failover, aunque automático, no tiene el mismo mecanismo interno que Aurora. Para el volumen actual de este proyecto (MVP1, un país) la diferencia no es bloqueante, pero hay que decírselo al cliente explícitamente — no es un mapeo neutro, es una regresión de resiliencia aceptada a cambio del cambio de proveedor pedido, igual que ADR-003 ya aceptó conscientemente el costo de Aurora sobre RDS estándar por la razón inversa.
  • Negativo — cambio de arquitectura, no solo de proveedor: el borde público del sistema deja de ser 100% cloud-native (ALB/WAFv2, redundante dentro de la misma región AWS) y pasa a depender de un appliance físico/virtual en el datacenter de CIS Latam más un túnel VPN Site-to-Site hacia Azure. Esto introduce una dependencia de red que no existía en el diseño AWS ni en la propuesta original de este ADR (Front Door/App Gateway) — ver "Riesgos" para el detalle de qué se pierde en resiliencia.
  • Negativo (transición, no permanente): durante la ventana de cutover de Staging hay dos infraestructuras vivas en paralelo (costo doble temporal) — ver plan de migración.
  • Neutral: Firebase Hosting (frontend/docs) no cambia — sigue siendo el mismo proveedor, independiente de este ADR.

Plan de migración

Producción: no aplica migración — se construye directo en Azure. Como nunca se aplicó Terraform de AWS en producción (doc/environments/production.md), no hay nada que cortar ni sincronizar: se escribe el Terraform de Azure para producción y se aplica una sola vez, cuando el resto de los bloqueantes de producción (credenciales Soterex, Firebase de prod) estén resueltos — sin relación con este ADR.

Staging: cutover con DNS, no big-bang.

  1. Levantar la infraestructura de Azure para Staging en paralelo (Terraform nuevo, ambiente staging), con AWS Staging siguiendo activo y sirviendo tráfico real todo este tiempo. 1.b. Coordinar con el equipo de red de CIS Latam el túnel Site-to-Site (IPsec) entre el Azure VPN Gateway y su FortiGate, y probarlo de punta a punta (conectividad + reglas de filtrado del FortiGate hacia el rango privado de la VNet) antes de mover ningún tráfico real — es la pieza que más puede demorar el cronograma porque depende de un equipo externo a este proyecto, no solo de nosotros.
  2. Deploy del backend a Azure Container Apps (Staging, ingress interno — no público), apuntado a una base de datos nueva (Flexible Server) — no hay migración de datos con continuidad de servicio que preservar: los datos actuales de Staging son de prueba (E1-E4, sin integración real con Soterex todavía, ver doc/environments/production.md — M5 sigue bloqueado por falta de credenciales). Se recrean con los seeders existentes, no se migra el dato real.
  3. Verificar el ambiente nuevo end-to-end (mismo checklist manual que ya se usó para M1-M4, ver doc/qa/) contra la URL temporal de Azure, sin tocar el DNS todavía.
  4. Cutover: cambiar el DNS de soterex.stag.cislatam.net / api.soterex.stag.cislatam.net — el destino ahora es el FortiGate (su IP/regla pública configurada para reenviar a Azure por el túnel), no un endpoint de Azure directo. TTL bajo puesto con anticipación (ej. 24-48hs antes) para que el corte sea rápido.
  5. Ventana de observación (alarmas de Azure Monitor activas desde el paso 2) antes de decomisionar AWS Staging.
  6. Decomisionar AWS Staging (terraform destroy del ambiente staging de AWS) — requiere tu OK explícito antes de ejecutarse, es destructivo e irreversible.

Riesgos

RiesgoMitigación
Ventana de DNS cutover con caché de resolvers (usuarios internos probando Staging)TTL bajo con anticipación, comunicar la ventana al equipo que usa Staging para QA
Azure Database for PostgreSQL Flexible Server no replica storage con el mismo mecanismo que AuroraAceptado explícitamente arriba (Consequences) — revisitar si el volumen de producción lo justifica más adelante
Doble costo durante la ventana de paralelo en StagingVentana acotada (días, no semanas) — Staging es el único ambiente que necesita esta duplicación
Nadie en el equipo operó Azure todavía (a diferencia de AWS, donde ya hay varios fix: de aprendizaje en el historial — OIDC, Security Groups, tags)Esperable un ciclo de aprendizaje similar al que ya pasó con AWS (ver commits Fix OIDC..., Fix crítico: dominio... de la implementación AWS) — no es un riesgo nuevo, es el mismo costo de la primera vez, ahora en otro proveedor
Nuevo — punto único de falla en CIS Latam: si el FortiGate, su enlace de internet, o el túnel VPN caen, el sistema entero queda inalcanzable — aunque Azure esté perfectamente sano. Este riesgo no existía en el diseño AWS (borde 100% cloud-native, redundante) ni en la propuesta original de este ADR (Front Door/App Gateway)Preguntar explícitamente si el FortiGate/enlace de CIS Latam tiene redundancia (segundo appliance/segundo ISP) — si no la tiene, dejarlo documentado como riesgo aceptado por el cliente, no nuestro de resolver
Nuevo — latencia y capacidad: todo el tráfico (incluido el de usuarios en Guatemala) rutea primero al datacenter de CIS Latam antes de llegar a Azure — suma un salto, y el enlace de internet de CIS Latam pasa a ser el techo de throughput de la app, no el de AzureMedir latencia real en la prueba de Staging (paso 3 del plan) antes del cutover de Producción; si es inaceptable, es un dato para la decisión abierta #1 de abajo
Nuevo — ¿el FortiGate tiene el módulo de Web Application Firewall activo? FortiGate es un firewall de red (NGFW); el WAF de aplicación (reglas OWASP, protección L7) es un security profile específico que hay que confirmar que esté licenciado y habilitado — si solo se usa como firewall de red, el sistema queda con menos protección L7 que el WAFv2 actual de AWSConfirmar con el equipo de infra de CIS Latam qué perfiles de seguridad tiene activos el FortiGate — ver "Decisiones abiertas"
Nuevo — dependencia de un equipo externo: el túnel VPN y las reglas del FortiGate las configura/mantiene el equipo de IT de CIS Latam, no este proyecto — cualquier cambio de red futuro (nuevo dominio, nueva regla) depende de su disponibilidadCoordinar un canal directo con ese equipo antes de empezar la implementación, no en el medio del cutover

Decisiones abiertas (necesitan tu input antes de que devops escriba Terraform)

  1. ✅ Resuelto: WAF = FortiGate on-premise de CIS Latam (no Front Door/Application Gateway). Quedan 3 sub-preguntas técnicas para el equipo de red de CIS Latam, necesarias antes de diseñar la VPN:
    • ¿El FortiGate tiene el security profile de Web Application Firewall activo y licenciado, o solo firewall de red/IPS genérico? (ver fila nueva en "Riesgos")
    • ¿Tiene redundancia (HA activo/pasivo, segundo enlace de ISP) o es un punto único de falla?
    • ¿Hay capacidad/ancho de banda disponible en el enlace de CIS Latam para cursar todo el tráfico de la app (hoy y proyectado), o hay que dimensionarlo?
  2. Fecha/ventana del cutover de Staging — para coordinar el TTL bajo del DNS y la ventana de trabajo conjunta con el equipo de red de CIS Latam (el túnel VPN no lo armamos solos).
  3. ¿Alguien del equipo ya tiene o necesita provisionar una suscripción de Azure con owner/billing admin? Es un prerequisito antes de que devops pueda escribir el primer terraform apply (equivalente al bootstrap que ya existe en infra/terraform/bootstrap/ para AWS).
  4. Datos del FortiGate para el túnel Site-to-Site: IP pública, versión de FortiOS, y si prefieren IKEv1 o IKEv2 — lo necesita devops para configurar el Azure VPN Gateway del lado correspondiente.

Next Steps

  • devops: una vez resueltas las 3 decisiones abiertas, escribir los módulos de Terraform en infra/terraform-azure/ (o reemplazar infra/terraform/ — a decidir si convive temporalmente con el de AWS durante el cutover de Staging o se reemplaza directo) usando el provider azurerm, replicando la estructura modular actual (network, compute, database, cache, secrets, monitoring, identity) 1:1 con la tabla de mapeo de este ADR.
  • security: revisar el Terraform de Azure con el mismo checklist que ya se aplicó en infra-consensus.md antes de aplicar en Staging — no asumir que "es el mismo diseño" alcanza sin una segunda revisión real, hay servicios (Key Vault, Managed Identity, NSGs) con matices de configuración propios de Azure que no existen en AWS.
  • backend-dev: sin cambios de código esperados — DB_CONNECTION=pgsql sigue siendo wire-compatible contra Flexible Server igual que lo es contra Aurora; los cambios son de variables de entorno/infra, no de aplicación.

Documentación viva — se actualiza junto con el código, no es un anexo aparte.