Observability (Managed Grafana)¶
This runbook is for the platform team. It covers the optional observability tier: an Azure Managed Grafana instance per environment that reads Log Analytics and shows the health of Fabric pipelines. Workload teams read Monitoring.
What it deploys¶
The tier is a separate Terraform root (infra/deploy/observability) with its own state (observability.tfstate), so it has
its own lifecycle. The only dependency is the Log Analytics workspace of the spoke, which must exist first.
flowchart LR
fp["Fabric pipelines"] --> lh["pipeline_logs Lakehouse"]
lh --> law["Log Analytics workspace"]
law --> g["Managed Grafana (KQL dashboards)"]
law --> alert["Scheduled query rule: pipeline failures"]
law -->|"data export rule (optional)"| st["Storage account: cool, then archive, then delete"]
| Resource | Notes |
|---|---|
| Resource group | rg-<workload>-observability-<env>-<region>-001 |
| Managed Grafana | grafana-<workload>-<env>-<region>, SKU Standard, size X1 by default, major version 12, system-assigned identity, deterministic outbound IPs on, API keys off, public access on by default, zone redundancy off by default |
| Role assignments for the Grafana identity | Monitoring Reader and Log Analytics Reader on each connected Log Analytics workspace |
| Role assignments for groups | Grafana Admin, Grafana Editor and Grafana Viewer for the groups in grafana_rbac |
| Private endpoint (optional) | In the spoke private endpoint subnet. Private DNS is set by policy, so the Terraform ignores changes to the DNS zone group. |
| Diagnostic setting | Grafana audit logs and metrics to the first Log Analytics workspace |
| Alert rule and action group | alert-pipeline-failure-grafana-... and ag-pipeline-alerts-grafana-... |
| Historical log storage (optional) | A storage account with a data export rule and a lifecycle policy |
Design decisions¶
| Decision | Reason |
|---|---|
| Dedicated resource group and state | Isolates observability from workload resources and allows an independent lifecycle |
| System-assigned managed identity | Grafana reads Log Analytics without credentials, over the Azure backbone |
| Deterministic outbound IPs | Egress addresses are fixed, so they can be identified in logs and allowed in firewalls |
| Entra ID only, no API keys | All access is by Entra ID with MFA through Conditional Access. Automation uses a service principal or managed identity. |
| Separate groups for Grafana | Different blast radius: Grafana access is not Fabric access |
| Public by default, private endpoint optional | Development uses public access with Entra ID and MFA. Production can restrict to the VNet. |
| Dashboards as code | JSON templates in the configuration repository, provisioned with the Grafana Terraform provider |
Prerequisites¶
Security groups¶
Create three Entra ID security groups per workload before deploying. They are separate from the Fabric workspace groups.
| Group (naming pattern) | Grafana role | Intended members |
|---|---|---|
sg-fabric-<workload>-grafana-admins |
Admin | Platform team engineers |
sg-fabric-<workload>-grafana-editors |
Editor | Data engineers and analysts of the workload team |
sg-fabric-<workload>-grafana-viewers |
Viewer | Business stakeholders, management |
Creating groups needs the Groups Administrator or Global Administrator role in Entra ID.
WORKLOAD="<workload>"
for role in admins editors viewers; do
az ad group create \
--display-name "sg-fabric-${WORKLOAD}-grafana-${role}" \
--mail-nickname "sg-fabric-${WORKLOAD}-grafana-${role}" \
--security-enabled true --query id -o tsv
done
Add members with az ad group member add --group <name> --member-id <object id>. Put the object IDs in the variables:
grafana_rbac = {
admins = ["<admins group object id>", "<deploying service principal object id>"]
editors = ["<editors group object id>"]
viewers = ["<viewers group object id>"]
}
The deploying service principal must be Grafana Admin
The dashboards are provisioned through the Grafana API. That needs the Grafana Admin role on the instance, which is a
separate assignment from the Contributor rights that create the instance. Put the object ID of the deploying service
principal in grafana_rbac.admins.
Other¶
- The Log Analytics workspace IDs to connect (
log_analytics_workspace_ids). The first one is used for diagnostics, the alert rule, the data export rule and the dashboard template variable. - Optionally the subnet and private DNS zone for a private endpoint.
Variables¶
| Variable | Purpose and default |
|---|---|
grafana_config |
sku (Standard), sku_size (X1), api_key_enabled (false), deterministic_outbound_ip_enabled (true), public_network_access (true), zone_redundancy (false), grafana_major_version ("12") |
grafana_private_endpoint |
enabled (false), subnet_id, private_dns_zone_id |
grafana_rbac |
Lists of object IDs for admins, editors, viewers |
alert_rules |
pipeline_failure_enabled (false), pipeline_failure_interval (300 seconds), pipeline_failure_lookback (3600 seconds), notification_contact_emails |
dashboard_definitions |
enabled (false) |
dashboard_path |
Directory with the dashboards. Set by the pipeline through the environment variable TF_VAR_dashboard_path. |
historical_log_storage |
See Historical log retention |
Allowed regions and environments are the same as for the other tiers (Platform configuration).
Deployment¶
The pipeline has a run option to deploy the observability tier. The pipeline definitions are not part of this solution's source, so the exact option name depends on your pipeline.
Two phases on the first deployment. The Grafana provider needs the URL of an instance that does not exist yet.
| Phase | What happens |
|---|---|
| First run | The Grafana instance, role assignments, diagnostics and alert rule are created. Dashboard resources fail because there is no endpoint yet. This is expected. |
| Second run | The pipeline reads the grafana_endpoint output, gets a token for the Azure Managed Grafana application (GRAFANA_URL and GRAFANA_AUTH environment variables) and the dashboards are provisioned |
After that, every run is a single phase. Data engineers add dashboards in the configuration repository and set
dashboard_definitions.enabled = true.
The dashboards directory has one sub-folder per Grafana folder, and one .json file per dashboard. The module discovers all
*/*.json files, so no per-file configuration is needed. The file is a Terraform template with one variable:
${LAW_RESOURCE_ID}. Grafana template variables must be escaped ($${...}).
Security controls¶
| Control | Implementation |
|---|---|
| Authentication | Entra ID SSO, no local accounts |
| MFA | Conditional Access policies |
| API keys | Disabled by default. If enabled, rotation is manual. |
| Audit logging | Diagnostic settings to Log Analytics (audit logs, all logs, metrics) |
| Outbound IPs | Deterministic |
| TLS | HTTPS only |
| DNS label scope | TenantReuse, which gives a predictable DNS name |
| Private DNS | Managed by policy |
Historical log retention¶
Logs can be tiered for cheaper long-term retention. It is off by default (historical_log_storage.enabled = false).
| Tier | Duration | Storage | Query |
|---|---|---|---|
| Hot | 0 to 30 days | Log Analytics workspace | Interactive KQL |
| Cool | 31 to 90 days | Blob storage, cool | Log search jobs or restore |
| Archive | 91 to 365 days | Blob storage, archive | Rehydrate first (takes hours) |
| Setting | Default |
|---|---|
export_tables |
["FabricPipelineRuns_CL"] |
archive_after_days |
90 |
delete_after_days |
365 |
law_resource_group_name |
Resource group of the Log Analytics workspace, needed for the export rule |
The storage account is Standard, LRS, StorageV2, access tier Cool, with blob and container soft delete of 7 days. A
lifecycle rule moves blobs to archive and deletes them after the configured days. Prerequisites: the custom table must exist
in Log Analytics before the export rule can be created, and export rules only support DCR-based custom tables (suffix _CL).
Deploy historical storage after the data engineers have set up the log ingestion pipeline.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| "No data" in all panels | The FabricPipelineRuns table does not exist yet |
The data engineering pipeline has not filled Log Analytics yet |
| "No data" in some panels | Time range too narrow, or no runs of that status | Widen the time range, check that pipelines ran |
| "Insufficient permissions" on the data source | Role assignments of the Grafana identity have not propagated | Wait 10 minutes after deployment |
| Cannot sign in (403) | User is not in a Grafana group | Add the user to a group |
| Cannot sign in (timeout) in private mode | VPN or ExpressRoute is not connected, or DNS does not return a private IP | Connect and check DNS |
| "Datasource not found" | The Azure Monitor data source is not configured | Check that the Grafana identity has Monitoring Reader on the workspace |
| Alert does not fire | Rule disabled, or no email configured | Check pipeline_failure_enabled and notification_contact_emails |
Deployment fails on grafana_major_version |
The Standard SKU needs a supported major version | Set grafana_major_version = "12" |
Terraform wants to change private_dns_zone_group |
Policy set the DNS zone group after creation | Expected. Keep the ignore_changes lifecycle block. |
Known limitations¶
- No folder-level or dashboard-level permissions in Azure Managed Grafana. Options if you need separation: one instance per team (each with its own groups and Log Analytics connections), accept a shared instance, or run your own Grafana Enterprise (more cost and operations).
- No automated rotation of Grafana API keys, if you enable them.
- Pipeline failure alerts send email only when
notification_contact_emailsis filled. - Only the first Log Analytics workspace is used for the alert rule and the dashboard variable.