Как встраивать Grafana-дашборды без головной боли
Мы регулярно получаем запросы от SaaS-команд: Grafana уже развёрнута, дашборды готовы, но встроить их в интерфейс не получается. iframe не грузится из-за CSP, авторизация требует логина, а браузер блокирует сторонние cookies. При этом каждый клиент должен видеть только свои данные — multi-tenant обязателен. Расскажем, как мы решаем эти задачи и получаем рабочее embedded-решение за несколько дней.
Какие проблемы решает встраивание Grafana?
Основная боль — безопасность. Анонимный iframe слишком прост, но для приватных данных не годится. Второй типичный сценарий — SameSite cookies: браузеры всё агрессивнее блокируют межсайтовые сессии. Третий — CSP: если в политике безопасности нет frame-src для вашего Grafana-домена, дашборд не отобразится. И наконец, mobile: стандартный интерфейс Grafana не адаптирован под телефоны.
Два подхода: anonymous vs embedded
Самый простой вариант — анонимный iframe. Grafana разрешает доступ без аутентификации, вы вставляете URL дашборда в iframe. Подходит для публичных панелей (мониторинг сайта, uptime). Но если данные приватные, нужна авторизация.
Второй вариант — встраивание с сервисным аккаунтом. Grafana 9.1+ поддерживает специальные токены, которые не требуют cookies. Вы генерируете URL на своём сервере с подписанным токеном и передаёте его в iframe. Пользователь даже не знает, что смотрит данные из Grafana.
| Критерий | Anonymous iframe | Embedded (service account) |
|---|---|---|
| Авторизация | Нет | Есть (токен) |
| Безопасность | Низкая | Высокая |
| Multi-tenant | Нет | Да (через vars) |
| Сложность настройки | 1–2 дня | 3–5 дней |
| Подходит для | Публичные данные | Приватные данные, SaaS |
Как настроить авторизацию через сервисные аккаунты?
Для продакшена используйте именно этот способ. Конфигурация Grafana:
[security]
allow_embedding = true
[auth.anonymous]
enabled = true
org_role = Viewer
hide_version = true
[cookie]
secure = true
samesite = none
Создайте сервисный аккаунт и токен:
curl -X POST http://grafana:3000/api/serviceaccounts \
-H "Content-Type: application/json" \
-u admin:admin \
-d '{"name":"embed-reader","role":"Viewer"}'
curl -X POST http://grafana:3000/api/serviceaccounts/1/tokens \
-H "Content-Type: application/json" \
-u admin:admin \
-d '{"name":"embed-token"}'
Токен храните на сервере и никогда не отдавайте клиенту.
Генерация signed URL на бэкенде
URL для iframe формируется на вашем сервере. Пример на TypeScript:
interface GrafanaEmbedOptions {
dashboardUid: string;
panelId?: number;
from?: string;
to?: string;
vars?: Record<string, string>;
theme?: 'light' | 'dark';
kiosk?: boolean;
}
class GrafanaEmbedService {
constructor(
private readonly baseUrl: string,
private readonly serviceAccountToken: string
) {}
buildEmbedUrl(options: GrafanaEmbedOptions): string {
const { dashboardUid, panelId, from = 'now-24h', to = 'now', vars = {}, theme = 'light', kiosk = true } = options;
const params = new URLSearchParams({ from, to, theme, ...(kiosk ? { kiosk: 'tv' } : {}) });
Object.entries(vars).forEach(([k, v]) => params.append(`var-${k}`, v));
const path = panelId ? `/d-solo/${dashboardUid}?panelId=${panelId}&` : `/d/${dashboardUid}?`;
return `${this.baseUrl}${path}${params.toString()}`;
}
}
Реализация на фронтенде
React‑компонент для iframe с лоадером и обработкой ошибок:
import { useState, useEffect } from 'react';
interface GrafanaPanelProps {
dashboardUid: string;
panelId: number;
vars?: Record<string, string>;
from?: string;
to?: string;
height?: number;
title?: string;
}
export function GrafanaPanel({ dashboardUid, panelId, vars, from = 'now-24h', to = 'now', height = 300, title }: GrafanaPanelProps) {
const [embedUrl, setEmbedUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/grafana/embed-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dashboardUid, panelId, vars, from, to }),
})
.then(r => r.json())
.then(({ url }) => setEmbedUrl(url))
.catch(() => setError('Не удалось получить URL дашборда'));
}, [dashboardUid, panelId, JSON.stringify(vars), from, to]);
if (error) return <div className="text-red-500">{error}</div>;
return (
<div className="relative rounded-lg overflow-hidden border bg-white" style={{ height }}>
{title && <div className="px-4 py-2 border-b text-sm font-medium text-gray-700">{title}</div>}
{loading && <div className="absolute inset-0 flex items-center justify-center bg-gray-50"><div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" /></div>}
{embedUrl && <iframe src={embedUrl} width="100%" height={title ? height - 40 : height} frameBorder="0" onLoad={() => setLoading(false)} title={title ?? `Grafana panel ${panelId}`} />}
</div>
);
}
API endpoint для генерации URL
Express‑пример с проверкой прав:
app.post('/api/grafana/embed-url', requireAuth, async (req, res) => {
const { dashboardUid, panelId, vars, from, to } = req.body;
const hasAccess = await checkDashboardAccess(req.user.id, dashboardUid);
if (!hasAccess) return res.status(403).json({ error: 'Forbidden' });
const url = grafanaService.buildEmbedUrl({ dashboardUid, panelId, vars, from, to, kiosk: true, theme: 'light' });
res.json({ url });
});
Multi-tenant через template variables
Если один дашборд используется для разных клиентов, передавайте tenant_id в качестве переменной шаблона. Добавьте в URL var-tenant_id=123. В источнике данных Grafana отфильтруйте строки, например, WHERE tenant_id = '${tenant_id}' AND $__timeFilter(time). Каждый пользователь увидит только свои данные. Для масштабирования используйте datasource с прокси-сервером, который подставляет tenant_id из токена.
Что делать, если CSP блокирует iframe?
Добавьте домен Grafana в директиву frame-src:
Content-Security-Policy: frame-src https://grafana.yourdomain.com
Также убедитесь, что SameSite cookie установлен на none (см. конфигурацию выше). Или, что надёжнее, используйте сервисные аккаунты — они не полагаются на cookies.
Что входит в работу по встраиванию?
- Аудит текущей инфраструктуры и требований к безопасности
- Настройка сервисного аккаунта и токенов в Grafana
- Разработка API для генерации signed URL с проверкой прав
- Интеграция React-компонента (или Vue/Angular) с обработкой загрузки и ошибок
- Настройка multi-tenant через template variables
- Оптимизация CSP и SameSite cookies
- Тестирование на десктопе, планшетах и мобильных устройствах
- Документация для команды
Процесс работы над встраиванием
| Этап | Длительность |
|---|---|
| Анализ инфраструктуры и требований | 1 день |
| Проектирование схемы (авторизация, multi‑tenant) | 1–2 дня |
| Реализация API и фронтенда | 2–3 дня |
| Тестирование в разных браузерах и мобильных | 1–2 дня |
| Документация и передача команде | 1 день |
Мы занимаемся встраиванием Grafana более пяти лет, реализовали более 50 проектов — от пары дашбордов до корпоративных multi‑tenant решений. В среднем экономия по сравнению с разработкой собственной панели составляет 60–80%. Гарантируем стабильную работу и соблюдение документации Grafana.
Получите консультацию инженера по вашему проекту. Закажите предварительную оценку — мы подберём оптимальную стратегию и назовём точные сроки.
Типичные ошибки при встраивании Grafana
- Токен сервисного аккаунта передаётся в URL клиента — это небезопасно. Генерируйте URL только на сервере.
- Не настроен
allow_embedding— iframe будет пустым. - CSP запрещает frame-src — добавьте домен Grafana.
- SameSite cookie установлен как Strict — авторизация сломается в iframe.
- Template variables не экранируются — возможна SQL-инъекция, если datasource использует строковые подстановки.







