A serverless Azure Function (PowerShell, Flex Consumption) that runs on a daily cron schedule, exports all dashboard JSON from an Azure Managed Grafana instance, and commits them to a GitHub repository as an incremental backup.
- Timer trigger fires daily at midnight UTC (
0 0 0 * * *). - The function queries the Grafana API for every dashboard.
- Each dashboard's JSON is exported with
idnulled out so it can be directly imported into a fresh Grafana instance viaPOST /api/dashboards/db. - The file tree in the target GitHub branch is compared (by Git blob SHA) with the new exports.
- If any dashboards were added, modified, or removed, a single batch commit is created on the target branch. The commit message follows this format:
chore(backup): Backup on 2026-03-19 Added: My New Dashboard (uid123) Modified: Server Metrics (uid456) Removed: dashboards/OldFolder/uid789.json - If nothing changed, no commit is made.
Dashboards are stored mirroring the Grafana folder structure:
dashboards/
├── General/
│ └── abc123.json
├── Infrastructure/
│ └── def456.json
└── Application/
└── ghi789.json
Each file is named by the dashboard's Grafana UID. The dashboard title, panels, and all configuration are inside the JSON.
| Requirement | Details |
|---|---|
| Azure Function App | Flex Consumption plan, PowerShell 7.4 runtime, North Central US (or your region) |
| Grafana Service Account Token | Create a Service Account in your Azure Managed Grafana instance with Viewer role, then generate a token. See Grafana Service Accounts docs. |
| GitHub PAT | Classic PAT with repo scope, or a fine-grained PAT with Contents: Read and write permission on the target repository. |
| GitHub repository | Private repo with a branch named grafana_export already created. The branch can be empty (initial commit only) or have existing content. |
Configure these as Application Settings on the Function App (or in local.settings.json for local dev):
| Variable | Required | Description |
|---|---|---|
GRAFANA_URL |
Yes | Azure Managed Grafana endpoint, e.g. https://myinstance.cuse.grafana.azure.com |
GRAFANA_API_KEY |
Yes | Grafana Service Account token (starts with glsa_) |
GITHUB_REPO |
Yes | Target repo in owner/repo format |
GITHUB_PAT |
Yes | GitHub Personal Access Token |
GITHUB_BRANCH |
No | Branch to commit to (default: grafana_export) |
GITHUB_COMMIT_AUTHOR_NAME |
No | Git author name (default: Grafana Backup Bot) |
GITHUB_COMMIT_AUTHOR_EMAIL |
No | Git author email (default: grafana-backup-bot@users.noreply.github.com) |
- Install the Azure Functions VS Code extension.
- Copy
local.settings.json.exampletolocal.settings.jsonand fill in your values (for local testing only). - Right-click the workspace root in VS Code → Deploy to Function App… → select your Flex Consumption Function App.
- After deployment, add the environment variables above as Application Settings in the Azure portal (or via Azure CLI):
az functionapp config appsettings set \ --name <function-app-name> \ --resource-group <rg-name> \ --settings \ GRAFANA_URL="https://..." \ GRAFANA_API_KEY="glsa_..." \ GITHUB_REPO="owner/repo" \ GITHUB_PAT="ghp_..."
# Install Azure Functions Core Tools (if not already)
npm install -g azure-functions-core-tools@4 --unsafe-perm true
# Copy and fill in settings
Copy-Item local.settings.json.example local.settings.json
# Edit local.settings.json with real values
# Start the function locally
func startThe timer trigger won't fire immediately when running locally. To test on demand, send an admin request:
Invoke-RestMethod -Uri "http://localhost:7071/admin/functions/BackupGrafanaDashboards" `
-Method Post -Body '{}' -ContentType "application/json"Each JSON file under dashboards/ is a complete Grafana dashboard model (with id: null) that can be imported directly:
$token = "glsa_your_new_instance_token"
$grafanaUrl = "https://new-instance.grafana.azure.com"
Get-ChildItem -Path ./dashboards -Recurse -Filter *.json | ForEach-Object {
$dashboard = Get-Content $_.FullName -Raw | ConvertFrom-Json
$folderName = $_.Directory.Name
$body = @{
dashboard = $dashboard
overwrite = $true
message = "Restored from backup"
} | ConvertTo-Json -Depth 100
Invoke-RestMethod -Uri "$grafanaUrl/api/dashboards/db" `
-Headers @{ "Authorization" = "Bearer $token"; "Content-Type" = "application/json" } `
-Method Post -Body $body
}Note: This does not recreate Grafana folders automatically. Create the target folders in the new instance first, or extend the script to call
POST /api/foldersbefore importing.