Как встраивать 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 использует строковые подстановки.







