De recomendaciones de Azure Advisor a acciones en tu dashboard
Daniel J. Saldaña- 9 de septiembre de 2026
- Puntuación de feedback

Un dashboard de infraestructura no aporta mucho si se limita a replicar métricas. El salto de valor llega cuando convierte una señal de Azure en una decisión concreta: qué recurso está afectado, cuál es el impacto, quién puede actuar y dónde está la documentación para resolverlo.
Ese es precisamente el caso de uso que encaja en Goliat Dashboard: reunir señales de costes, seguridad, rendimiento y disponibilidad para que un equipo pueda priorizarlas sin saltar continuamente entre herramientas. En este post desarrollo una implementación de referencia para llevar las recomendaciones de Azure Advisor a una aplicación con Node.js y React.
El resultado sigue un principio sencillo: el navegador sólo habla con nuestra API; la API valida el acceso del usuario, consulta Azure Resource Manager y devuelve un DTO pequeño, paginado y preparado para la interfaz. Ni los secretos ni los tokens de Azure salen del servidor.
Qué devuelve Azure Advisor
Azure Advisor analiza la configuración y la telemetría de los recursos y agrupa sus recomendaciones en las categorías Cost, Security, Performance, HighAvailability y OperationalExcellence. Para cada recomendación podemos aprovechar, entre otros, la categoría, el impacto (High, Medium o Low), el recurso afectado, la descripción corta, el beneficio potencial y el enlace de ayuda.
La operación Recommendations - List de la API REST devuelve recomendaciones cacheadas y puede incluir un nextLink. Esto cambia el diseño: no debemos asumir que una sola respuesta contiene toda la suscripción ni que cada visita al panel recalcula Advisor.
Arquitectura mínima y segura
El flujo recomendado es el siguiente:
- React solicita
GET /api/advisor/recommendationsa nuestro backend con la sesión normal de la aplicación. - El backend comprueba que el usuario puede consultar la suscripción solicitada. El
subscriptionIdenviado por el navegador nunca basta como autorización. - Un servicio de servidor obtiene un token para Azure Resource Manager mediante una identidad administrada en producción.
- El servicio sigue todos los
nextLink, normaliza la respuesta de Advisor y guarda una instantánea breve en caché. - La interfaz recibe sólo los campos que necesita y los presenta con filtros, prioridad y paginación.
Para leer las recomendaciones, una identidad necesita permisos de lectura en el alcance adecuado y acceso a los recursos asociados. Reader permite consultarlas; si se define un rol personalizado, la acción mínima es Microsoft.Advisor/recommendations/read. Consulta los permisos oficiales de Azure Advisor antes de asignar permisos más amplios.
Dependencias e identidad
Instala la librería de identidad en el servicio Node.js:
npm install @azure/identityEn producción, asigna una Managed Identity a la aplicación y concede únicamente los permisos necesarios. En desarrollo, DefaultAzureCredential permite autenticarse con Azure CLI, Visual Studio Code u otras credenciales locales. La elección explícita de ManagedIdentityCredential en producción evita depender de secretos de cliente dentro del despliegue.
El código de este artículo es una implementación de referencia para el patrón de Goliat Dashboard; no expone credenciales ni debe ejecutarse desde React.
Servicio Node.js: consultar, paginar y normalizar
El siguiente servicio usa la API REST actual de Azure Resource Manager. Elegir REST aquí nos deja controlar de forma explícita la versión de API y el nextLink; también podrías usar el cliente oficial @azure/arm-advisor, que expone un iterador paginado.
import { DefaultAzureCredential, ManagedIdentityCredential, type TokenCredential,} from '@azure/identity';
const ARM_SCOPE = 'https://management.azure.com/.default';const ADVISOR_API_VERSION = '2025-01-01';
type AdvisorProperties = { category?: string; impact?: 'High' | 'Medium' | 'Low'; impactedValue?: string; label?: string; potentialBenefits?: string; lastUpdated?: string; learnMoreLink?: string; shortDescription?: { problem?: string; solution?: string }; resourceMetadata?: { resourceId?: string };};
type AdvisorResource = { id?: string; name?: string; properties?: AdvisorProperties;};
type AdvisorPage = { value: AdvisorResource[]; nextLink?: string;};
export type AdvisorRecommendation = { id: string; category: string; impact: 'High' | 'Medium' | 'Low' | 'Unknown'; label: string; resourceId: string | null; problem: string; solution: string; potentialBenefits: string | null; lastUpdated: string | null; learnMoreLink: string | null;};
function createCredential(): TokenCredential { return process.env.NODE_ENV === 'production' ? new ManagedIdentityCredential() : new DefaultAzureCredential();}
function toRecommendation(item: AdvisorResource): AdvisorRecommendation { const properties = item.properties ?? {};
return { id: item.name ?? item.id ?? 'unknown', category: properties.category ?? 'Unknown', impact: properties.impact ?? 'Unknown', label: properties.label ?? properties.shortDescription?.problem ?? 'Recomendación de Azure', resourceId: properties.resourceMetadata?.resourceId ?? properties.impactedValue ?? null, problem: properties.shortDescription?.problem ?? '', solution: properties.shortDescription?.solution ?? '', potentialBenefits: properties.potentialBenefits ?? null, lastUpdated: properties.lastUpdated ?? null, learnMoreLink: properties.learnMoreLink ?? null, };}
export async function listAdvisorRecommendations(subscriptionId: string) { const token = await createCredential().getToken(ARM_SCOPE);
if (!token) { throw new Error('No se pudo obtener un token para Azure Resource Manager'); }
const firstPage = new URL( `https://management.azure.com/subscriptions/${encodeURIComponent(subscriptionId)}/providers/Microsoft.Advisor/recommendations`, ); firstPage.searchParams.set('api-version', ADVISOR_API_VERSION); firstPage.searchParams.set('$top', '100');
let nextUrl: string | undefined = firstPage.toString(); const recommendations: AdvisorRecommendation[] = [];
while (nextUrl) { const response = await fetch(nextUrl, { headers: { Authorization: `Bearer ${token.token}`, Accept: 'application/json', }, });
if (!response.ok) { throw new Error(`Azure Advisor respondió con HTTP ${response.status}`); }
const page = (await response.json()) as AdvisorPage; recommendations.push(...page.value.map(toRecommendation)); nextUrl = page.nextLink; }
const impactOrder = { High: 0, Medium: 1, Low: 2, Unknown: 3 }; return recommendations.sort((a, b) => impactOrder[a.impact] - impactOrder[b.impact]);}El DTO anterior es deliberadamente pequeño. Evita entregar al navegador el objeto ARM completo, que puede cambiar con el tiempo y contiene más metadatos de los que la UI necesita. Conserva el identificador completo del recurso sólo si va a utilizarse para enlazarlo con inventario, etiquetas o permisos propios.
Endpoint: autorización antes de consultar Azure
Aquí está el patrón del controlador. requireSession, acl y cache representan los servicios de autenticación, control de acceso y caché de tu aplicación; adapta sus nombres a tu stack.
app.get('/api/advisor/recommendations', requireSession, async (req, res, next) => { try { const subscriptionId = typeof req.query.subscriptionId === 'string' ? req.query.subscriptionId : null;
if (!subscriptionId) { return res.status(400).json({ error: 'subscriptionId es obligatorio' }); }
// Nunca conviertas el subscriptionId de la URL en un permiso implícito. const canRead = await acl.canReadSubscription(req.user.id, subscriptionId); if (!canRead) { return res.status(403).json({ error: 'No tienes acceso a esta suscripción' }); }
const snapshot = await cache.remember( `advisor:v1:${subscriptionId}`, 5 * 60, () => listAdvisorRecommendations(subscriptionId), );
const category = typeof req.query.category === 'string' ? req.query.category : null; const filtered = category ? snapshot.filter((item) => item.category === category) : snapshot;
const page = Math.max(1, Number(req.query.page) || 1); const pageSize = Math.min(50, Math.max(1, Number(req.query.pageSize) || 10)); const start = (page - 1) * pageSize;
return res.json({ items: filtered.slice(start, start + pageSize), total: filtered.length, page, pageSize, cachedAt: new Date().toISOString(), }); } catch (error) { next(error); }});Una caché de pocos minutos reduce latencia y evita peticiones redundantes. En una plataforma multiusuario, incluye en la clave de caché el ámbito que pueda modificar la visibilidad de los datos. Además, registra de forma auditable quién consultó cada suscripción, sin escribir tokens ni secretos en los logs.
React: una vista accionable, no una lista interminable
Este componente consume el endpoint propio, reinicia la página al cambiar el filtro y cancela una petición anterior cuando cambian las dependencias. El diseño deja espacio para añadir acciones internas —por ejemplo, crear una incidencia o enlazar un runbook— sin otorgar permisos de escritura sobre Azure al navegador.
import { useEffect, useState } from 'react';
type Item = { id: string; category: string; impact: 'High' | 'Medium' | 'Low' | 'Unknown'; label: string; resourceId: string | null; problem: string; solution: string; potentialBenefits: string | null; learnMoreLink: string | null;};
type Response = { items: Item[]; total: number; page: number; pageSize: number;};
export function AdvisorRecommendations({ subscriptionId }: { subscriptionId: string }) { const [data, setData] = useState<Response | null>(null); const [category, setCategory] = useState(''); const [page, setPage] = useState(1); const [error, setError] = useState<string | null>(null);
useEffect(() => { const controller = new AbortController(); const query = new URLSearchParams({ subscriptionId, page: String(page), pageSize: '10', });
if (category) query.set('category', category);
setError(null); fetch(`/api/advisor/recommendations?${query}`, { signal: controller.signal }) .then(async (response) => { if (!response.ok) throw new Error('No se pudieron cargar las recomendaciones'); return (await response.json()) as Response; }) .then(setData) .catch((reason: unknown) => { if ((reason as Error).name !== 'AbortError') setError('No se pudieron cargar las recomendaciones'); });
return () => controller.abort(); }, [subscriptionId, category, page]);
if (error) return <p role="alert">{error}</p>; if (!data) return <p>Cargando recomendaciones…</p>;
const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize));
return ( <section aria-label="Recomendaciones de Azure Advisor"> <label> Categoría <select value={category} onChange={(event) => { setCategory(event.target.value); setPage(1); }} > <option value="">Todas</option> <option value="Cost">Coste</option> <option value="Security">Seguridad</option> <option value="Performance">Rendimiento</option> <option value="HighAvailability">Alta disponibilidad</option> <option value="OperationalExcellence">Excelencia operativa</option> </select> </label>
<p>{data.total} recomendaciones</p>
<ul> {data.items.map((item) => ( <li key={item.id}> <strong>{item.impact} · {item.label}</strong> <p>{item.problem}</p> <p>{item.solution}</p> {item.resourceId && <code>{item.resourceId}</code>} {item.learnMoreLink && ( <a href={item.learnMoreLink} target="_blank" rel="noreferrer"> Ver documentación de Azure </a> )} </li> ))} </ul>
<button disabled={page === 1} onClick={() => setPage((value) => value - 1)}> Anterior </button> <span> Página {page} de {totalPages} </span> <button disabled={page >= totalPages} onClick={() => setPage((value) => value + 1)}> Siguiente </button> </section> );}En una UI de producción, representaría el impacto con color y texto —nunca sólo con color—, mostraría el momento de la última sincronización y enlazaría cada resultado con el recurso y el runbook correspondiente. La recomendación no debe aplicar cambios automáticamente: es una señal para que el equipo revise, apruebe y ejecute la acción mediante un flujo controlado.
Refrescar recomendaciones sin bloquear la interfaz
La ruta GET de Advisor lee datos cacheados. Si necesitas pedir una nueva generación, programa un trabajo de servidor que llame a generateRecommendations; es una operación asíncrona que responde 202, entrega un Location y debe consultarse respetando Retry-After antes de volver a listar. No la ejecutes cuando un usuario abre el panel ni la expongas directamente como botón del navegador.
Para iniciar ese proceso se necesita el permiso adicional Microsoft.Advisor/generateRecommendations/action. Incluso después de implementar una mejora, Advisor puede tardar en reflejarla; por ello es útil mostrar la hora de actualización y evitar prometer que el estado cambiará al instante. Los detalles del ciclo están documentados en Generate recommendations y Get generate status.
Checklist antes de llevarlo a producción
- Usa Managed Identity en producción y limita RBAC al alcance necesario.
- Mantén la llamada a Azure Resource Manager y los tokens exclusivamente en el backend.
- Autoriza cada suscripción contra la identidad de usuario antes de leerla.
- Sigue
nextLink, devuelve un DTO estable y pagina el resultado para la UI. - Añade caché, límites de tasa y trazabilidad; registra metadatos operativos, no secretos.
- Trata la generación como un trabajo asíncrono y conserva aprobación humana para cualquier cambio.
Con este patrón, Azure Advisor deja de ser una pestaña más del portal y se convierte en una fuente de trabajo priorizada dentro del flujo de DevOps y FinOps. La integración es pequeña, pero permite conectar cada recomendación con contexto, responsables y una acción verificable.


