added grafana module

This commit is contained in:
Stanislav_Kopp 2025-07-10 11:28:59 +02:00
parent 878ecba717
commit 7c9e84971e
38 changed files with 583 additions and 0 deletions

4
grafana/README.md Normal file
View file

@ -0,0 +1,4 @@
# Modules for Grafana alerts and dashboards
## How to use
TODO

View file

@ -0,0 +1,3 @@
output "folder_uid" {
value = grafana_folder.this.uid
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,3 @@
resource "grafana_folder" "this" {
title = var.alert-folder
}

View file

@ -0,0 +1,6 @@
# Alert Rule Folder
variable "alert-folder" {
description = "folder name to group alerts"
type = string
}

View file

@ -0,0 +1,77 @@
resource "grafana_rule_group" "this" {
name = var.group_name
folder_uid = var.folder_uid
interval_seconds = 60
org_id = 1
disable_provenance = true
rule {
name = "test-alert"
for = "1m"
condition = "C"
no_data_state = "NoData"
exec_err_state = "Error"
is_paused = false
annotations = {
"description" = "Pod `{{ $labels.namespace }}/{{ $labels.pod }}` is crash looping"
"summary" = "Kubernetes pod crash looping (instance {{ $labels.instance }})"
}
data {
ref_id = "A"
query_type = ""
relative_time_range {
from = 600
to = 0
}
datasource_uid = var.datasource_uid
model = jsonencode({
intervalMs = 1000
maxDataPoints = 43200
refId = "A"
expr = "increase(kube_pod_container_status_restarts_total{}[1h]) > 3"
})
}
data {
ref_id = "C"
relative_time_range {
from = 0
to = 0
}
datasource_uid = var.datasource_uid
model = <<EOT
{
"conditions": [
{
"evaluator": {
"params": [0],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": ["C"]
},
"reducer": {
"params": [],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "__expr__"
},
"expression": "A",
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "C",
"type": "threshold"
}
EOT
}
}
}

View file

@ -0,0 +1,9 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,19 @@
variable "group_name" {
type = string
}
variable "folder_uid" {
type = string
}
variable "datasource_uid" {
type = string
}
variable "rule_name" {
type = string
}
variable "condition" {
type = string
}

46
grafana/alerts/locals.tf Normal file
View file

@ -0,0 +1,46 @@
locals {
#################################################################
# 1. Discover & decode alert YAML files from <root>/<alerts_dir>
#################################################################
alert_files = fileset(
"${path.root}/${var.alerts_dir}",
var.file_pattern
)
decoded_files = [
for f in local.alert_files :
yamldecode(file("${path.root}/${var.alerts_dir}/${f}"))
]
#################################################################
# 2. Flatten: each file may contain multiple groups
#################################################################
groups_raw = flatten([
for doc in local.decoded_files : try(doc.groups, [])
])
#################################################################
# 3. Merge defaults & convert camelCase snake_case
#################################################################
groups = [
for g in local.groups_raw : merge(g, {
uid = try(
g.uid,
trim(replace(replace(lower(g.name), " ", "-"), "_", "-"), "-")
)
folder_uid = try(try(g.folder_uid, g.folder), var.default_folder_uid)
interval = try(g.interval, "1m")
rules = [
for r in g.rules : merge(r, {
data = [
for d in r.data : merge(d, {
datasource_uid = try(d.datasourceUid, var.default_datasource_uid)
relative_time_range = try(d.relativeTimeRange, { from = 600, to = 0 })
})
]
})
]
})
]
}

51
grafana/alerts/main.tf Normal file
View file

@ -0,0 +1,51 @@
resource "grafana_rule_group" "this" {
for_each = {
for g in local.groups :
g.uid => g
}
name = each.value.name
folder_uid = each.value.folder_uid
interval_seconds = (
substr(each.value.interval, length(each.value.interval) - 1, 1) == "m"
? tonumber(substr(each.value.interval, 0, length(each.value.interval) - 1)) * 60
: tonumber(trim(each.value.interval, "s"))
)
disable_provenance = var.disable_provenance
dynamic "rule" {
for_each = each.value.rules
content {
uid = rule.value.uid
name = try(rule.value.title, rule.value.name)
condition = rule.value.condition
no_data_state = rule.value.noDataState
exec_err_state = rule.value.execErrState
is_paused = try(rule.value.isPaused, false)
for = try(rule.value.for, null)
labels = try(rule.value.labels, {})
dynamic "data" {
for_each = rule.value.data
content {
ref_id = data.value.refId
datasource_uid = data.value.datasource_uid
dynamic "relative_time_range" {
for_each = [data.value.relative_time_range]
content {
from = relative_time_range.value.from
to = relative_time_range.value.to
}
}
model = jsonencode(data.value.model)
}
}
}
}
}

4
grafana/alerts/output.tf Normal file
View file

@ -0,0 +1,4 @@
output "rule_group_ids" {
description = "Map: <folder_uid>.<name> → Grafana rule-group ID"
value = { for k, v in grafana_rule_group.this : k => v.id }
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,38 @@
variable "alerts_dir" {
type = string
default = "alerts"
description = "Relative path to the directory containing alert rule YAML files."
}
variable "file_pattern" {
type = string
default = "*.y{a,}ml"
description = "Glob pattern to match alert rule YAML files (e.g. *.yaml, *.yml)."
}
variable "default_datasource_uid" {
type = string
description = "UID of the Prometheus or Thanos datasource to use if not specified in the alert rule."
}
variable "default_receiver" {
type = string
description = "Name of the contact point (receiver) to use for notifications if not defined in alert rule."
}
variable "default_folder_uid" {
type = string
description = "UID of the Grafana folder to use for alert rules when not defined in the YAML."
}
variable "default_interval_seconds" {
type = number
default = 60
description = "Default evaluation interval (in seconds) for alert rule groups if not set in YAML."
}
variable "disable_provenance" {
type = bool
default = false
description = "If true, disables Grafana alert provisioning provenance (sets disable_provenance = true)."
}

View file

@ -0,0 +1,9 @@
resource "grafana_contact_point" "this" {
name = var.contact-point-name
googlechat {
url = var.google-chat-url
message = "{{ template \"google-chat-body-template\" . }}"
title = "{{ template \"google-chat-title-template\" . }}"
}
}

View file

@ -0,0 +1,8 @@
output "contact_name" {
value = grafana_contact_point.this.name
description = "UID of the created contact point"
}
output "contact_id" {
value = grafana_contact_point.this.id
description = "UID of the created contact point"
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,11 @@
# Grafana contact point
variable "google-chat-url" {
description = "google-chat-webhook url"
type = string
sensitive = true
}
variable "contact-point-name" {
description = "google-chat-contact-point-name"
type = string
}

View file

@ -0,0 +1,9 @@
resource "grafana_contact_point" "this" {
name = var.contact_point_name
disable_provenance = var.disable_provenance
webhook {
url = "${var.webhook_url}?apiKey=${var.opsgenie_api_key}"
http_method = var.http_method
}
}

View file

@ -0,0 +1,9 @@
output "contact_name" {
value = grafana_contact_point.this.name
description = "Receiver name of the webhook contact-point."
}
output "contact_id" {
value = grafana_contact_point.this.id
description = "Internal Grafana ID of the contact-point."
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,27 @@
variable "contact_point_name" {
description = "Name of the webhook contact point"
type = string
}
variable "webhook_url" {
description = "Base URL (e.g., https://api.eu.opsgenie.com/v2/heartbeats/<name>/ping)"
type = string
}
variable "opsgenie_api_key" {
description = "OpsGenie API key"
type = string
sensitive = true
}
variable "http_method" {
description = "HTTP method for webhook"
type = string
default = "POST"
}
variable "disable_provenance" {
description = "Disable provenance"
type = bool
default = false
}

View file

@ -0,0 +1,11 @@
resource "grafana_contact_point" "this" {
name = var.contact_point_name
disable_provenance = var.disable_provenance
opsgenie {
api_key = var.opsgenie_api_key
url = var.opsgenie_url
override_priority = var.override_priority
auto_close = var.auto_close
}
}

View file

@ -0,0 +1,8 @@
output "contact_name" {
value = grafana_contact_point.this.name
description = "UID of the created contact point"
}
output "contact_id" {
value = grafana_contact_point.this.id
description = "UID of the created contact point"
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,37 @@
########################################
# Required
########################################
variable "contact_point_name" {
description = "Name of the Opsgenie contact point (receiver) in Grafana"
type = string
}
variable "opsgenie_api_key" {
description = "Opsgenie API key"
type = string
sensitive = true
}
variable "disable_provenance" {
description = "Set to true to disable provenance on the contact-point"
type = bool
default = false
}
variable "opsgenie_url" {
description = "Base URL for the Opsgenie API (change for US vs EU)"
type = string
default = "https://api.eu.opsgenie.com/v2/alerts"
}
variable "override_priority" {
description = "Whether Opsgenie should override alert priority"
type = bool
default = true
}
variable "auto_close" {
description = "Whether to auto-close alerts in OpsGenie when they resolve in the Alertmanager"
type = bool
default = true
}

View file

@ -0,0 +1,11 @@
resource "grafana_data_source" "this" {
type = "prometheus"
name = var.datasource_name
url = var.datasource_url
basic_auth_enabled = true
basic_auth_username = var.datasource_username
secure_json_data_encoded = jsonencode({
basicAuthPassword = var.datasource_password
})
}

View file

@ -0,0 +1,3 @@
output "datasource_uid" {
value = grafana_data_source.this.uid
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,18 @@
# Grafana contact point
variable "datasource_url" {
type = string
}
variable "datasource_name" {
type = string
}
variable "datasource_username" {
type = string
sensitive = true
}
variable "datasource_password" {
type = string
sensitive = true
}

View file

@ -0,0 +1,7 @@
resource "grafana_message_template" "this" {
for_each = local.templates
name = each.key
template = each.value.content
disable_provenance = var.disable_provenance
}

View file

@ -0,0 +1,4 @@
output "template_ids" {
description = "Map of template-group name → Grafana UID"
value = { for k, v in grafana_message_template.this : k => v.id }
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,17 @@
locals {
# 1. Collect every matching file paths are relative to templates_dir
template_files = fileset(
"${path.root}/${var.templates_dir}",
var.file_pattern
)
# 2. Build a map: group-name { content = <file-text> }
# We strip the extension to get a neat group name.
templates = {
for rel_path in local.template_files :
trimsuffix(basename(rel_path), ".tmpl") =>
{
content = file("${path.root}/${var.templates_dir}/${rel_path}")
}
}
}

View file

@ -0,0 +1,17 @@
variable "templates_dir" {
description = "Relative path (from the root module) that holds all *.go or *.tmpl template files."
type = string
default = "templates"
}
variable "file_pattern" {
description = "Glob pattern for template files inside templates_dir."
type = string
default = "*.tmpl" # change to *.tmpl if you prefer
}
variable "disable_provenance" {
description = "Leave templates editable in Grafana UI."
type = bool
default = false
}

View file

@ -0,0 +1,18 @@
resource "grafana_notification_policy" "this" {
contact_point = var.default_contact_point_uid
group_by = var.group_by
dynamic "policy" {
for_each = var.folder_policies
content {
matcher {
label = "grafana_folder"
match = "="
value = policy.key # folder title, e.g. "Test-Alerts"
}
contact_point = policy.value
repeat_interval = "1h"
}
}
}

View file

@ -0,0 +1,4 @@
output "policy_id" {
description = "ID of the notification policy resource"
value = grafana_notification_policy.this.id
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
grafana = {
source = "grafana/grafana"
version = "= 3.25.4"
}
}
}

View file

@ -0,0 +1,31 @@
variable "group_by" {
description = "Labels to group alerts by before routing. Grafana default is [\"alertname\"]."
type = list(string)
default = ["alertname"]
}
variable "default_contact_point_uid" {
description = "Fallback contact-point UID for alerts that match no folder-specific policy."
type = string
}
variable "folder_policies" {
description = <<-EOT
Map of folder UID contact-point UID.
Each entry creates a matcher block:
policy {
matcher { label = "folder"; match = <folder_uid> }
contact_point = <contact_point_uid>
}
EOT
type = map(string)
}
variable "folder_label_key" {
description = "Label key used to match alert folders (e.g., 'folder' or 'namespace_uid')"
type = string
default = "folder"
}