Actualiza tu dashboard cuando cambia un recurso Azure: Event Grid, caché y reindexación segura
Daniel J. Saldaña- 26 de septiembre de 2026
- Puntuación de feedback

Un dashboard de infraestructura que consulta Azure cada pocos minutos tiene un intervalo incómodo: puede enseñar un recurso que ya no existe, no mostrar uno recién creado o conservar unas tags que acabamos de cambiar.
En Goliat Dashboard quería mantener el inventario en caché para no pedir a Azure Resource Manager los mismos datos en cada navegación, pero sin aceptar que el usuario tuviese que esperar al siguiente polling. Azure Event Grid encaja muy bien como señal de cambio.
La idea de este post es pequeña y reutilizable:
Azure modifica un recurso ↓Event Grid llama a nuestro webhook ↓El backend invalida el inventario cacheado ↓Un trabajo interno reindexa una sola vez ↓El dashboard vuelve a mostrar datos recientesEvent Grid no es la fuente de verdad del inventario. Sólo avisa de que debemos volver a consultar Azure de forma controlada desde el servidor.
El problema: una ráfaga no debe convertirse en una tormenta
Un despliegue de Terraform puede crear, modificar o borrar muchos recursos en pocos segundos. Lanzar una reindexación completa por cada evento sería tan ineficiente como consultar Azure constantemente desde el navegador.
El patrón de Goliat tiene cuatro pasos:
- Validar el webhook antes de procesar su cuerpo.
- Filtrar únicamente los eventos que afectan al inventario.
- Invalidar la caché inmediatamente.
- Agrupar la reindexación durante una ventana breve.
Así el dashboard deja de servir datos viejos, pero tampoco se satura cuando una operación genera decenas de notificaciones.
El contrato del endpoint
La ruta puede ser pública porque Event Grid no tiene una sesión de usuario de tu aplicación, pero nunca debe quedar abierta:
POST /api/public/integrations/azure/eventgrid| Entrada | Respuesta | Efecto |
|---|---|---|
| Evento de validación válido | 200 | Devuelve el código que Azure espera |
| Alta, modificación o borrado de recurso | 202 | Invalida y programa un refresco |
| Evento válido que no nos interesa | 202 | Se ignora sin reindexar |
| JSON inválido | 400 | Rechaza el payload |
| Secreto ausente o incorrecto | 401 | No procesa el evento |
| Cuerpo demasiado grande | 413 | Protege memoria y proceso |
Con el esquema Event Grid, Azure necesita recibir el valor de validationCode dentro de validationResponse y un estado 200 OK para crear la suscripción. Un 202 no completa el handshake. Puedes comprobar el flujo en la documentación de validación de webhooks.
Filtra antes de llegar a tu API
Empieza por un grupo de recursos y suscríbete a los eventos que de verdad afectan al inventario:
Microsoft.Resources.ResourceWriteSuccessMicrosoft.Resources.ResourceDeleteSuccessMicrosoft.Resources.ResourceActionSuccess
Los grupos de recursos emiten estos eventos de Azure Resource Manager; la lista completa está en Azure resource group como origen de Event Grid.
El código mantiene un segundo filtro. Es importante: la configuración de Azure reduce tráfico y coste; la validación en la API protege el comportamiento si alguien modifica esa configuración más adelante.
type EventGridEvent = { eventType?: string; subject?: string; data?: Record<string, unknown>;};
function isResourceMutationEvent(event: EventGridEvent): boolean { const eventType = String(event.eventType ?? '');
return ( eventType === 'Microsoft.Resources.ResourceWriteSuccess' || eventType === 'Microsoft.Resources.ResourceDeleteSuccess' || eventType === 'Microsoft.Resources.ResourceActionSuccess' || eventType.endsWith('.ResourceCreatedOrUpdatedSuccess') || eventType.endsWith('.ResourceDeletedSuccess') );}No uses subject para construir una URL, ejecutar un comando o elegir una ruta interna. En esta integración todos los eventos aceptados provocan la misma acción fija: refrescar el ámbito de inventario.
Autenticar sin exponer el secreto
En Goliat, Event Grid entrega una cabecera estática que sólo conoce Azure y el servidor:
AZURE_EVENTGRID_WEBHOOK_SECRET=<secreto-largo-y-aleatorio>EVENTGRID_BODY_MAX_BYTES=524288El secreto se almacena en variables de servidor o en Key Vault. No se publica en React, no se registra y no se envía mediante la URL.
import { timingSafeEqual } from 'node:crypto';
const SECRET_HEADER = 'X-Goliat-EventGrid-Secret';
function safeEqual(left: string, right: string): boolean { const a = Buffer.from(left); const b = Buffer.from(right);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);}
function isAuthorized(request: Request, expectedSecret?: string): boolean { if (!expectedSecret) return false;
const provided = request.headers.get(SECRET_HEADER);
return !!provided && safeEqual(provided, expectedSecret);}
function validationCode(event: EventGridEvent): string | null { return typeof event.data?.validationCode === 'string' ? event.data.validationCode : null;}Una cabecera no reemplaza a Microsoft Entra ID en escenarios de máxima exigencia, pero es un punto de partida claro y válido para un webhook. Event Grid también puede usar entrega protegida con Entra ID cuando tu plataforma está preparada para validar tokens. Aquí tienes la guía oficial.
Limita el cuerpo antes de parsear JSON
Comprobar Content-Length ayuda, pero una petición puede no incluir esa cabecera. El endpoint debe contar bytes mientras lee el stream.
class BodyTooLargeError extends Error {}
function rejectIfBodyExceeds(request: Request, maxBytes: number) { const value = request.headers.get('Content-Length'); const declared = value ? Number.parseInt(value, 10) : NaN;
if (Number.isFinite(declared) && declared > maxBytes) { throw new BodyTooLargeError('Payload demasiado grande'); }}
async function readBoundedText(request: Request, maxBytes: number): Promise<string> { if (!request.body) return '';
const reader = request.body.getReader(); const decoder = new TextDecoder(); let bytes = 0; let text = '';
while (true) { const { done, value } = await reader.read(); if (done) break;
bytes += value.byteLength; if (bytes > maxBytes) { await reader.cancel(); throw new BodyTooLargeError('Payload demasiado grande'); }
text += decoder.decode(value, { stream: true }); }
return text + decoder.decode();}Esta protección es útil en cualquier webhook externo, no sólo con Event Grid. Primero se comprueba el secreto; después se limita la lectura; sólo entonces se intenta convertir el contenido a JSON.
El controlador: validar, invalidar y responder rápido
El siguiente ejemplo está reducido del endpoint de Goliat. Sustituye invalidateCacheScope y scheduleResourceReindex por Redis, una tabla de trabajos, una cola o la abstracción que ya tengas.
export async function POST(request: Request): Promise<Response> { if (!isAuthorized(request, process.env.AZURE_EVENTGRID_WEBHOOK_SECRET)) { return Response.json({ error: 'No autorizado' }, { status: 401 }); }
try { rejectIfBodyExceeds(request, 512 * 1024); const raw = await readBoundedText(request, 512 * 1024); const body = raw ? JSON.parse(raw) : []; const events: EventGridEvent[] = Array.isArray(body) ? body : [body];
const validation = events.find((event) => event.eventType === 'Microsoft.EventGrid.SubscriptionValidationEvent');
if (validation) { const code = validationCode(validation);
return code ? Response.json({ validationResponse: code }) : Response.json({ error: 'validationCode ausente' }, { status: 400 }); }
const resourceEvents = events.filter(isResourceMutationEvent);
if (resourceEvents.length === 0) { return Response.json( { accepted: true, ignored: events.length }, { status: 202, } ); }
await invalidateCacheScope('cloud:resources'); const reindexScheduled = await scheduleResourceReindex();
return Response.json( { accepted: true, events: resourceEvents.length, reindexScheduled, }, { status: 202 } ); } catch (error) { if (error instanceof BodyTooLargeError) { return Response.json({ error: error.message }, { status: 413 }); }
return Response.json({ error: 'Payload JSON inválido' }, { status: 400 }); }}La reindexación no debe ejecutarse antes de responder. Event Grid recibe un 202 pronto y el trabajo costoso ocurre de manera controlada fuera del ciclo de entrega.
Agrupar eventos con debounce
Una ventana de dos minutos evita que diez eventos de Terraform activen diez inventarios completos. Con Redis, NX garantiza que sólo la primera petición reserva el trabajo.
const DEBOUNCE_SECONDS = 120;
async function scheduleResourceReindex(): Promise<boolean> { const key = 'debounce:azure-eventgrid:resources'; const accepted = await redis.set(key, '1', { NX: true, EX: DEBOUNCE_SECONDS, });
if (accepted !== 'OK') return false;
await queue.enqueue('cloud-resource-index', { scope: 'resources', });
return true;}En un servidor Node persistente puedes retrasar el trabajo con un temporizador. En un entorno serverless, usa una cola, Azure Service Bus, Storage Queue o un trabajo duradero: el proceso puede terminar después de devolver el 202.
La caché debe ser consciente de carreras. Goliat incrementa una generación al invalidar un ámbito para impedir que una petición lenta, iniciada antes del evento, sobrescriba con datos antiguos la caché recién invalidada.
Crear la suscripción
Publica el endpoint con HTTPS y crea la suscripción. Este ejemplo usa Azure CLI y configura la cabecera estática como propiedad de entrega:
az eventgrid event-subscription create \\ --name dashboard-resource-cache \\ --source-resource-id "/subscriptions/\${SUBSCRIPTION_ID}/resourceGroups/\${RESOURCE_GROUP}" \\ --endpoint "\${WEBHOOK_URL}" \\ --endpoint-type webhook \\ --event-delivery-schema eventgridschema \\ --included-event-types \\ Microsoft.Resources.ResourceWriteSuccess \\ Microsoft.Resources.ResourceDeleteSuccess \\ Microsoft.Resources.ResourceActionSuccess \\ --delivery-attribute-mapping \\ X-Goliat-EventGrid-Secret static "\${EVENTGRID_SECRET}" trueEn el portal, crea una Event Subscription, selecciona Web Hook, limita los tipos de evento y añade la cabecera en Delivery properties. Event Grid permite definir cabeceras estáticas de entrega para webhooks; consulta Custom delivery properties.
Prueba antes de conectar Azure
Puedes probar la validación desde terminal:
curl --fail-with-body \\ --request POST "\${WEBHOOK_URL}" \\ --header 'Content-Type: application/json' \\ --header "X-Goliat-EventGrid-Secret: \${EVENTGRID_SECRET}" \\ --data '[ { "eventType": "Microsoft.EventGrid.SubscriptionValidationEvent", "data": { "validationCode": "local-validation-code" } } ]'La respuesta debe incluir:
{ "validationResponse": "local-validation-code" }Después cambia el tipo a Microsoft.Resources.ResourceWriteSuccess y comprueba que devuelve 202, invalida el ámbito correcto y programa un único trabajo.
Checklist de producción
- El webhook usa HTTPS y un certificado de confianza.
- El secreto sólo existe en el servidor y llega por cabecera.
- El endpoint rechaza cuerpos excesivos antes de parsear JSON.
- Azure y la API filtran los tipos de evento.
- La respuesta de validación devuelve
200yvalidationResponse. - La entrega es idempotente: Event Grid puede reintentar.
- La caché se invalida antes de reindexar.
- El debounce es distribuido si hay varias réplicas.
- Ninguna cabecera pública permite invocar el trabajo interno.
- Los logs no contienen secretos ni cuerpos completos.
Con esta pieza, el dashboard conserva la velocidad de una caché sin obligar al equipo a esperar al siguiente intervalo de polling. Event Grid señala el cambio; tu backend decide cómo reaccionar; la interfaz sólo recibe un inventario reciente y autorizado.


