Manage Grafana with Terraform
Create folders, data sources, dashboards, alerts, teams, and service accounts from HCL.
File provisioning covers data sources, dashboards, and alerting, but not users, teams, service accounts, or permissions. Terraform covers all of them, works against any running Grafana (self-hosted or Grafana Cloud) through its API, and keeps a state file so it can detect and correct drift. This guide sets up the provider and manages the common resources.
Prerequisites and provider
You need Terraform or OpenTofu, a reachable Grafana, and a service account token with a role that can manage what you declare (see the authentication guide for creating one). Keep the token out of the code:
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = ">= 2.9.0" # pin to the version you have tested
}
}
}
variable "grafana_url" {
type = string
default = "http://localhost:3000"
}
variable "grafana_token" {
type = string
sensitive = true
}
provider "grafana" {
url = var.grafana_url
auth = var.grafana_token
}
Pass the token through the environment so it never lands in a file:
export TF_VAR_grafana_token='glsa_...'
terraform init
For Grafana Cloud, you can create the stack and its service account in the same configuration. The Grafana documentation shows a two-provider pattern: a cloud provider that authenticates with a Cloud access policy token to create the stack (grafana_cloud_stack) and a service account (grafana_cloud_stack_service_account with its token), and a second provider aliased to the stack that uses that token.
Folders and data sources
resource "grafana_folder" "apps" {
title = "Applications"
}
resource "grafana_data_source" "prometheus" {
type = "prometheus"
name = "Prometheus"
uid = "prometheus"
url = "http://prometheus:9090"
is_default = true
json_data_encoded = jsonencode({
timeInterval = "15s"
})
}
resource "grafana_data_source" "postgres" {
type = "postgres"
name = "Postgres"
uid = "postgres-main"
url = "postgres.internal:5432"
username = "grafana"
json_data_encoded = jsonencode({
database = "app"
sslmode = "require"
})
secure_json_data_encoded = jsonencode({
password = var.postgres_password
})
}
Setting uid explicitly keeps dashboards and alert rules portable across environments. Secrets passed through secure_json_data_encoded are stored encrypted by Grafana, but they are also written to Terraform state, so store the state in an encrypted, access-controlled backend.
Dashboards
resource "grafana_dashboard" "webapp" {
folder = grafana_folder.apps.uid
config_json = file("${path.module}/dashboards/webapp.json")
overwrite = true
}
Use fileset to load a whole directory:
resource "grafana_dashboard" "all" {
for_each = fileset("${path.module}/dashboards", "*.json")
folder = grafana_folder.apps.uid
config_json = file("${path.module}/dashboards/${each.value}")
}
The dashboard JSON is the same file you would use for file provisioning, so you can move between the two approaches. When you attach a dashboard to a folder, older provider versions referenced the folder’s id and newer ones prefer its uid; check the provider documentation for the version you pin.
Alerting
Terraform has resources for each part of alerting:
resource "grafana_contact_point" "slack" {
name = "team-slack"
slack {
url = var.slack_webhook_url
recipient = "#alerts"
}
}
resource "grafana_notification_policy" "default" {
group_by = ["alertname", "namespace"]
contact_point = grafana_contact_point.slack.name
group_wait = "30s"
group_interval = "5m"
repeat_interval = "4h"
}
Rule groups use grafana_rule_group with rule blocks whose data entries mirror the provisioning YAML (query, reduce, threshold). Mute timings and templates have grafana_mute_timing and grafana_message_template. Look up the exact attribute names in the provider documentation, since alerting resources have been revised across provider versions.
grafana_notification_policy manages the whole policy tree, as in file provisioning, so make sure only one tool owns it.
Teams, service accounts, and permissions
These are the resources files can’t express:
resource "grafana_team" "platform" {
name = "platform"
email = "platform@example.com"
}
resource "grafana_service_account" "ci" {
name = "ci-dashboards"
role = "Editor"
}
resource "grafana_service_account_token" "ci" {
name = "ci-token"
service_account_id = grafana_service_account.ci.id
}
resource "grafana_folder_permission" "apps" {
folder_uid = grafana_folder.apps.uid
permissions {
team_id = grafana_team.platform.id
permission = "Edit"
}
}
Treat the generated token as a secret output, and push it into your secret manager rather than printing it.
Workflow
terraform fmt -check
terraform validate
terraform plan -out tfplan
terraform apply tfplan
In CI, run plan on pull requests and apply on merge. The plan shows exactly which dashboards, data sources, and alerts will change.
Adopt what already exists
If someone built things in the UI, import them into state instead of recreating them:
terraform import grafana_folder.apps <folder-uid>
terraform import grafana_dashboard.webapp <dashboard-uid>
terraform import grafana_data_source.prometheus <data-source-uid>
Newer Terraform versions can also use import blocks. Import ID formats can differ between provider versions, so confirm them in the resource docs.
Terraform alongside file provisioning
Don’t manage the same resource with both. Pick a boundary and stick to it. A common split is file provisioning for data sources and dashboards (loaded at startup, no API token needed) and Terraform for teams, service accounts, and permissions. Another is Terraform for everything, with provisioning files limited to server settings.
Popular Guides
-
Operate Grafana as Code: The Big Picture What can live in files, what needs an API, and how the pieces fit together. -
Configure the Grafana Server with grafana.ini and Environment Variables Know where config files live, how overrides are ordered, and how to keep settings in Git. -
Provision Dashboards from JSON Files Load dashboards from disk, map folders from directories, and author them without losing changes. -
Provision Alert Rules, Contact Points, and Notification Policies Define the whole alerting pipeline in YAML so it is reviewed and versioned like code. -
Deploy Grafana on Kubernetes with Helm and Provisioning Values Run Grafana from a Helm chart where data sources, dashboards, and settings all come from values files. -
Reload, Verify, and Troubleshoot Provisioning from the Command Line Find out what Grafana loaded, reload without a restart, and fix the usual provisioning errors.