---
name: azure-cloud
description: >
  Microsoft Azure: Azure Container Apps y AKS para contenedores, Azure SQL y
  Cosmos DB para datos, Azure Functions serverless, Service Bus para mensajería
  async, Azure AD (Entra ID) para autenticación y Key Vault para secretos.
  Cargar cuando se despliegue en Azure, se configure infraestructura con Terraform
  o Bicep, se integre Azure AD o se diseñen soluciones con servicios Azure.
version: "1.0.0"
herramientasPermitidas: [Read, Bash, Grep]
evolvable: true  # default para skill estandar
exclusiones:
  - "No cargar para despliegues en AWS o GCP — para AWS cargar `cloud-aws`, para GCP cargar `gcp-cloud`."
  - "No cargar para configurar Azure DevOps pipelines o GitHub Actions con self-hosted runners en Azure — para CI/CD pipelines cargar el skill de git-workflow y usar la documentación de Azure DevOps directamente."
  - "No cargar para análisis de costos Azure o rightsizing de instancias — para optimización de costos cloud usar Azure Cost Management directamente o el skill de arquitectura cloud."
  - "No cargar para diseño de estrategias de disaster recovery o backup multi-región — este skill cubre despliegue en una región; para DR multi-región consultar la documentación de Azure Site Recovery."
---
# Azure Cloud — Microsoft Azure

Skill para desplegar y operar sistemas en Microsoft Azure. Cubre Container Apps,
AKS, Azure SQL, Service Bus, Azure AD (Entra ID) y Key Vault. Para patrones AWS
ver `Skill("cloud-aws")`. Para GCP ver `Skill("gcp-cloud")`.

---

## Cuándo NO cargar

- La tarea es desplegar en AWS: cargar `cloud-aws`.
- La tarea es desplegar en GCP: cargar `gcp-cloud`.
- La tarea es configurar Azure DevOps pipelines o GitHub Actions: usar la documentación de Azure DevOps directamente.
- La tarea es análisis de costos o rightsizing: usar Azure Cost Management directamente.

## Cuándo cargar este skill

- Al desplegar servicios en Azure Container Apps o AKS
- Al conectar a Azure SQL o Cosmos DB desde Python o Node
- Al configurar Service Bus para mensajería async entre servicios
- Al implementar autenticación con Azure AD (Entra ID) en APIs
- Al gestionar secretos con Key Vault
- Al escribir Bicep o Terraform para infraestructura Azure
- Al configurar Managed Identity para acceso sin credenciales explícitas

---

## 1. Azure Container Apps — contenedores serverless con Bicep

```bicep
// container-app.bicep
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  name: 'mi-api'
  location: location
  identity: {
    type: 'SystemAssigned'  // Managed Identity para acceder a Key Vault sin credenciales
  }
  properties: {
    managedEnvironmentId: environment.id
    configuration: {
      ingress: {
        external: true
        targetPort: 8000
        transport: 'http'
      }
      secrets: [
        {
          name: 'db-connection-string'
          keyVaultUrl: dbSecretUri
          identity: 'system'  // Usa la Managed Identity para leer el secreto
        }
      ]
    }
    template: {
      scale: {
        minReplicas: 1
        maxReplicas: 10
        rules: [
          {
            name: 'http-scaling'
            http: {
              metadata: { concurrentRequests: '50' }
            }
          }
        ]
      }
      containers: [
        {
          name: 'api'
          image: 'miregistry.azurecr.io/mi-api:latest'
          resources: {
            cpu: json('0.5')
            memory: '1Gi'
          }
          env: [
            { name: 'DATABASE_URL', secretRef: 'db-connection-string' }
            { name: 'AZURE_TENANT_ID', value: tenantId }
          ]
        }
      ]
    }
  }
}
```

**Puntos clave de Container Apps:**
- `identity.type: 'SystemAssigned'` habilita Managed Identity automáticamente
- Los secretos se leen de Key Vault usando la Managed Identity — sin credenciales en código
- `minReplicas: 1` para APIs REST que no toleran cold starts

---

## 2. Azure Service Bus — mensajería async

```python
from azure.servicebus.aio import ServiceBusClient, ServiceBusMessage
from azure.identity.aio import DefaultAzureCredential
import json
import uuid

# DefaultAzureCredential usa Managed Identity en producción
# y credenciales del desarrollador localmente (az login)
credential = DefaultAzureCredential()

NAMESPACE = "mi-namespace"

async def enviar_mensaje(cola: str, datos: dict) -> None:
    async with ServiceBusClient(
        fully_qualified_namespace=f"{NAMESPACE}.servicebus.windows.net",
        credential=credential,
    ) as client:
        sender = client.get_queue_sender(queue_name=cola)
        async with sender:
            await sender.send_messages(
                ServiceBusMessage(
                    json.dumps(datos, ensure_ascii=False),
                    content_type="application/json",
                    message_id=str(uuid.uuid4()),  # Deduplicación de mensajes
                    subject=datos.get("tipo_evento", "evento"),
                )
            )

async def recibir_mensajes(cola: str, max_mensajes: int = 10) -> None:
    async with ServiceBusClient(
        fully_qualified_namespace=f"{NAMESPACE}.servicebus.windows.net",
        credential=credential,
    ) as client:
        receiver = client.get_queue_receiver(queue_name=cola)
        async with receiver:
            mensajes = await receiver.receive_messages(max_message_count=max_mensajes)
            for mensaje in mensajes:
                try:
                    datos = json.loads(str(mensaje))
                    await procesar_evento(datos)
                    await receiver.complete_message(mensaje)
                except Exception:
                    await receiver.abandon_message(mensaje)  # Reencola
```

---

## 3. Azure AD (Entra ID) — autenticación en APIs FastAPI

```python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer
from jose import jwt, JWTError
import httpx

TENANT_ID = "tu-tenant-id"
CLIENT_ID = "tu-client-id"

# JWKS endpoint de Azure AD para validar firmas
JWKS_URL = f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys"

oauth2_scheme = OAuth2AuthorizationCodeBearer(
    authorizationUrl=f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize",
    tokenUrl=f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
)

async def obtener_jwks() -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.get(JWKS_URL)
        return resp.json()

async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    try:
        jwks = await obtener_jwks()
        payload = jwt.decode(
            token,
            jwks,
            algorithms=["RS256"],
            audience=CLIENT_ID,
            issuer=f"https://login.microsoftonline.com/{TENANT_ID}/v2.0",
        )
        return payload
    except JWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token inválido o expirado",
            headers={"WWW-Authenticate": "Bearer"},
        )
```

---

## 4. Key Vault — gestión de secretos

```python
from azure.keyvault.secrets.aio import SecretClient
from azure.identity.aio import DefaultAzureCredential

KEY_VAULT_URL = "https://mi-vault.vault.azure.net/"

async def get_secret(nombre: str) -> str:
    """Obtiene un secreto de Key Vault usando Managed Identity."""
    credential = DefaultAzureCredential()
    client = SecretClient(vault_url=KEY_VAULT_URL, credential=credential)
    try:
        secret = await client.get_secret(nombre)
        return secret.value
    finally:
        await client.close()
        await credential.close()

# Patrón recomendado: cachear secrets al inicio para evitar latencia
# en cada request. Usar Azure App Configuration para config dinámica.
```

---

## 5. Managed Identity — principio de mínimo privilegio

```bash
# Otorgar acceso a Key Vault para la Managed Identity del Container App
az keyvault set-policy \
    --name mi-vault \
    --object-id $(az containerapp identity show \
        --name mi-api \
        --resource-group mi-rg \
        --query principalId -o tsv) \
    --secret-permissions get list

# Para Azure SQL: crear usuario en la base de datos vinculado a la Managed Identity
# (ejecutar en la base de datos como administrador)
# CREATE USER [mi-api] FROM EXTERNAL PROVIDER;
# ALTER ROLE db_datareader ADD MEMBER [mi-api];
# ALTER ROLE db_datawriter ADD MEMBER [mi-api];
```

---

## 6. Reglas obligatorias

| Regla | Justificación | Verificación |
|-------|--------------|--------------|
| Managed Identity en Container Apps y AKS | Los service principals con secretos rotan y se filtran | Verificar `identity.type: 'SystemAssigned'` en Bicep/Terraform |
| `DefaultAzureCredential` en el código | Funciona con Managed Identity en producción y `az login` en local sin cambios | Buscar `DefaultAzureCredential` en lugar de cadenas de conexión |
| Key Vault para todos los secretos | Las connection strings en variables de entorno aparecen en logs y dashboards | Verificar que no hay `SecretKeyRef` con valores hardcodeados |
| Diagnostic settings en todos los recursos | Sin logs en Log Analytics no hay observabilidad ni alertas | Revisar que cada recurso tiene `diagnosticSettings` en Bicep |
| Tags en recursos | Sin tags no se puede analizar costo por servicio ni equipo | `tags: { env: 'prod', equipo: 'backend' }` en cada recurso |

---

## 7. Anti-patrones

| MAL | BIEN |
|-----|------|
| Connection string en variable de entorno de Container App | Secreto en Key Vault + referencia `secretRef` |
| Service principal con client_secret en código | Managed Identity + `DefaultAzureCredential` |
| `Owner` o `Contributor` para el identity de CI/CD | Roles específicos: `AcrPush`, `Storage Blob Data Contributor` |
| `az login --service-principal` con credenciales en código | Workload Identity Federation para GitHub Actions |
| Puerto 1433 (Azure SQL) abierto a internet en NSG | Private Endpoint o acceso solo desde la VNET |
| Secretos rotados manualmente | Key Vault + rotación automática activada |

---

## 8. Checklist de verificación

- [ ] Container Apps y AKS usan Managed Identity (`SystemAssigned` o `UserAssigned`)
- [ ] Todos los secretos están en Key Vault, no en variables de entorno planas
- [ ] El código usa `DefaultAzureCredential` (no strings de conexión con contraseña)
- [ ] Cada recurso tiene tags `env`, `equipo` y `gestion`
- [ ] Diagnostic settings apuntan a un Log Analytics Workspace
- [ ] Network Security Groups no tienen puertos innecesarios abiertos a internet
- [ ] CI/CD usa Workload Identity Federation (no service principal con secret)
- [ ] Azure SQL o Cosmos DB tienen Private Endpoint configurado

---

## 9. Referencias

| Tema | Recurso |
|------|---------|
| AKS: clusters, Workload Identity, HPA, configuración segura | [recursos/aks.md](recursos/aks.md) |
| Azure Container Apps documentación oficial | [learn.microsoft.com/azure/container-apps](https://learn.microsoft.com/azure/container-apps) |
| DefaultAzureCredential — flujo de autenticación | [learn.microsoft.com/azure/developer/python/sdk/authentication-overview](https://learn.microsoft.com/azure/developer/python/sdk/authentication-overview) |
| Key Vault — rotación automática de secretos | [learn.microsoft.com/azure/key-vault/secrets/tutorial-rotation](https://learn.microsoft.com/azure/key-vault/secrets/tutorial-rotation) |
| Workload Identity Federation para CI/CD | [learn.microsoft.com/azure/active-directory/workload-identities/workload-identity-federation](https://learn.microsoft.com/azure/active-directory/workload-identities/workload-identity-federation) |

---

## Gotchas / Errores comunes no obvios

**`DefaultAzureCredential` funciona localmente con `az login` pero falla en el Container App con `CredentialUnavailableError` porque la Managed Identity no tiene acceso al recurso aunque aparezca asignada**: el `az containerapp identity show` confirma que la Managed Identity existe y tiene `principalId`, pero `DefaultAzureCredential` falla al intentar obtener un token de Key Vault con `403 Forbidden`. Causa: asignar la Managed Identity al Container App es diferente a otorgarle permisos en el recurso destino. La identidad existe, pero Key Vault no tiene una política de acceso para ese `principalId`. Fix: ejecutar `az keyvault set-policy --name <vault> --object-id <principalId> --secret-permissions get list` o usar RBAC de Azure con `az role assignment create --role "Key Vault Secrets User" --assignee <principalId> --scope <vault-resource-id>`. Verificar con `az keyvault secret show --vault-name <vault> --name <secret>` usando las credenciales de la identidad.

**Service Bus con `abandon_message()` en el handler de errores reencola el mensaje inmediatamente sin backoff, causando que un mensaje con procesamiento fallido se procese cientos de veces por segundo saturando la cola**: un handler que falla por un error transitorio de red hace `await receiver.abandon_message(mensaje)`, y el mensaje vuelve al frente de la cola inmediatamente. Con 10 consumidores procesando el mismo mensaje fallido, se generan 100+ llamadas por segundo al servicio fallido. Causa: Service Bus Basic/Standard tier no tiene dead letter queue automática con backoff exponencial por defecto; `abandon` simplemente reincrementar el `delivery_count`. Fix: verificar `mensaje.delivery_count` antes de procesar; si supera el umbral (ej: 5), llamar `dead_letter_message(mensaje, reason="MaxDeliveryCountExceeded")` en lugar de `abandon`. Configurar el `max_delivery_count` en la cola (por defecto 10) y verificar periódicamente la dead letter queue.

**La validación de tokens Azure AD con `jwt.decode()` y el JWKS endpoint falla 1-2 veces por día porque Azure AD rota las claves de firma sin previo aviso y el caché de JWKS en memoria tiene las claves anteriores**: el primer request después de la rotación de claves devuelve `JWTError: Signature verification failed` hasta que se llama de nuevo a `obtener_jwks()`. Los usuarios reciben 401 intermitentes durante los minutos que tardan los workers en refrescar su caché de JWKS. Causa: Azure AD rota las claves de firma periódicamente como parte de su política de seguridad; las aplicaciones deben manejar la rotación sin downtime. Fix: implementar caché de JWKS con TTL de 24 horas, pero con refresh automático cuando la verificación de firma falla con un kid desconocido: `try: verify_with_cached_jwks() except UnknownKid: refresh_jwks_cache(); verify_with_fresh_jwks()`. No hacer el refresh en cada request fallido para evitar loops.

**Bicep con `identity.type: 'SystemAssigned'` no propaga automáticamente la Managed Identity a los secretos referenciados en `configuration.secrets` si el Container App y el Key Vault están en resource groups diferentes**: el Bicep despliega sin error, pero el Container App no puede leer los secretos de Key Vault y el contenedor falla al arrancar con `SecretNotFound`. Causa: Bicep crea la Managed Identity al momento del deployment, pero la política de acceso en Key Vault (en otro resource group) no se crea automáticamente; debe ser un recurso separado en el Bicep o un paso post-deployment. Fix: agregar explícitamente el recurso de política en el Bicep del Key Vault: `resource kvPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { name: '${keyVaultName}/add' properties: { accessPolicies: [{ objectId: containerApp.identity.principalId ... }] } }`. Usar `dependsOn: [containerApp]` para garantizar el orden de creación.
