Actualiza tu dashboard cuando cambia un recurso Azure: Event Grid, caché y reindexación segura

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

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 recientes

Event 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:

  1. Validar el webhook antes de procesar su cuerpo.
  2. Filtrar únicamente los eventos que afectan al inventario.
  3. Invalidar la caché inmediatamente.
  4. 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.ResourceWriteSuccess
  • Microsoft.Resources.ResourceDeleteSuccess
  • Microsoft.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:

Terminal window
AZURE_EVENTGRID_WEBHOOK_SECRET=<secreto-largo-y-aleatorio>
EVENTGRID_BODY_MAX_BYTES=524288

El 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:

Terminal window
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}" true

En 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:

Terminal window
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 200 y validationResponse.
  • 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.

¡Suscríbete y recibe actualizaciones sobre tecnología, diseño, productividad, programación y mucho más!
0