From 7c9e84971e6ac01ff5ddc3027517456b4dfd3885 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Thu, 10 Jul 2025 11:28:59 +0200 Subject: [PATCH] added grafana module --- grafana/README.md | 4 + grafana/alert-folder/output.tf | 3 + grafana/alert-folder/providers.tf | 8 ++ grafana/alert-folder/rule-folder.tf | 3 + grafana/alert-folder/variables.tf | 6 ++ grafana/alert-group/alert-group.tf | 77 +++++++++++++++++++ grafana/alert-group/providers.tf | 9 +++ grafana/alert-group/variables.tf | 19 +++++ grafana/alerts/locals.tf | 46 +++++++++++ grafana/alerts/main.tf | 51 ++++++++++++ grafana/alerts/output.tf | 4 + grafana/alerts/providers.tf | 8 ++ grafana/alerts/variables.tf | 38 +++++++++ grafana/contact-point-gchat/main.tf | 9 +++ grafana/contact-point-gchat/output.tf | 8 ++ grafana/contact-point-gchat/providers.tf | 8 ++ grafana/contact-point-gchat/variables.tf | 11 +++ .../contact-point-opsgenie-heartbeat/main.tf | 9 +++ .../output.tf | 9 +++ .../providers.tf | 8 ++ .../variables.tf | 27 +++++++ grafana/contact-point-opsgenie/main.tf | 11 +++ grafana/contact-point-opsgenie/output.tf | 8 ++ grafana/contact-point-opsgenie/providers.tf | 8 ++ grafana/contact-point-opsgenie/variables.tf | 37 +++++++++ grafana/datasource/datasource.tf | 11 +++ grafana/datasource/output.tf | 3 + grafana/datasource/providers.tf | 8 ++ grafana/datasource/variables.tf | 18 +++++ grafana/message-template/message-template.tf | 7 ++ grafana/message-template/outputs.tf | 4 + grafana/message-template/providers.tf | 8 ++ grafana/message-template/templates.tf | 17 ++++ grafana/message-template/variables.tf | 17 ++++ grafana/notification-policy/main.tf | 18 +++++ grafana/notification-policy/output.tf | 4 + grafana/notification-policy/providers.tf | 8 ++ grafana/notification-policy/variables.tf | 31 ++++++++ 38 files changed, 583 insertions(+) create mode 100644 grafana/README.md create mode 100644 grafana/alert-folder/output.tf create mode 100644 grafana/alert-folder/providers.tf create mode 100644 grafana/alert-folder/rule-folder.tf create mode 100644 grafana/alert-folder/variables.tf create mode 100644 grafana/alert-group/alert-group.tf create mode 100644 grafana/alert-group/providers.tf create mode 100644 grafana/alert-group/variables.tf create mode 100644 grafana/alerts/locals.tf create mode 100644 grafana/alerts/main.tf create mode 100644 grafana/alerts/output.tf create mode 100644 grafana/alerts/providers.tf create mode 100644 grafana/alerts/variables.tf create mode 100644 grafana/contact-point-gchat/main.tf create mode 100644 grafana/contact-point-gchat/output.tf create mode 100644 grafana/contact-point-gchat/providers.tf create mode 100644 grafana/contact-point-gchat/variables.tf create mode 100644 grafana/contact-point-opsgenie-heartbeat/main.tf create mode 100644 grafana/contact-point-opsgenie-heartbeat/output.tf create mode 100644 grafana/contact-point-opsgenie-heartbeat/providers.tf create mode 100644 grafana/contact-point-opsgenie-heartbeat/variables.tf create mode 100644 grafana/contact-point-opsgenie/main.tf create mode 100644 grafana/contact-point-opsgenie/output.tf create mode 100644 grafana/contact-point-opsgenie/providers.tf create mode 100644 grafana/contact-point-opsgenie/variables.tf create mode 100644 grafana/datasource/datasource.tf create mode 100644 grafana/datasource/output.tf create mode 100644 grafana/datasource/providers.tf create mode 100644 grafana/datasource/variables.tf create mode 100644 grafana/message-template/message-template.tf create mode 100644 grafana/message-template/outputs.tf create mode 100644 grafana/message-template/providers.tf create mode 100644 grafana/message-template/templates.tf create mode 100644 grafana/message-template/variables.tf create mode 100644 grafana/notification-policy/main.tf create mode 100644 grafana/notification-policy/output.tf create mode 100644 grafana/notification-policy/providers.tf create mode 100644 grafana/notification-policy/variables.tf diff --git a/grafana/README.md b/grafana/README.md new file mode 100644 index 0000000..50ccb1c --- /dev/null +++ b/grafana/README.md @@ -0,0 +1,4 @@ +# Modules for Grafana alerts and dashboards + +## How to use +TODO diff --git a/grafana/alert-folder/output.tf b/grafana/alert-folder/output.tf new file mode 100644 index 0000000..c11089d --- /dev/null +++ b/grafana/alert-folder/output.tf @@ -0,0 +1,3 @@ +output "folder_uid" { + value = grafana_folder.this.uid +} diff --git a/grafana/alert-folder/providers.tf b/grafana/alert-folder/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/alert-folder/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/alert-folder/rule-folder.tf b/grafana/alert-folder/rule-folder.tf new file mode 100644 index 0000000..b952d07 --- /dev/null +++ b/grafana/alert-folder/rule-folder.tf @@ -0,0 +1,3 @@ +resource "grafana_folder" "this" { + title = var.alert-folder +} \ No newline at end of file diff --git a/grafana/alert-folder/variables.tf b/grafana/alert-folder/variables.tf new file mode 100644 index 0000000..4f45ad6 --- /dev/null +++ b/grafana/alert-folder/variables.tf @@ -0,0 +1,6 @@ +# Alert Rule Folder +variable "alert-folder" { + description = "folder name to group alerts" + type = string +} + diff --git a/grafana/alert-group/alert-group.tf b/grafana/alert-group/alert-group.tf new file mode 100644 index 0000000..a1149d5 --- /dev/null +++ b/grafana/alert-group/alert-group.tf @@ -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 = </ + ################################################################# + 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 }) + }) + ] + }) + ] + }) + ] +} diff --git a/grafana/alerts/main.tf b/grafana/alerts/main.tf new file mode 100644 index 0000000..fe0d541 --- /dev/null +++ b/grafana/alerts/main.tf @@ -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) + } + } + } + } +} diff --git a/grafana/alerts/output.tf b/grafana/alerts/output.tf new file mode 100644 index 0000000..af60ede --- /dev/null +++ b/grafana/alerts/output.tf @@ -0,0 +1,4 @@ +output "rule_group_ids" { + description = "Map: . → Grafana rule-group ID" + value = { for k, v in grafana_rule_group.this : k => v.id } +} diff --git a/grafana/alerts/providers.tf b/grafana/alerts/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/alerts/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/alerts/variables.tf b/grafana/alerts/variables.tf new file mode 100644 index 0000000..345bfda --- /dev/null +++ b/grafana/alerts/variables.tf @@ -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)." +} \ No newline at end of file diff --git a/grafana/contact-point-gchat/main.tf b/grafana/contact-point-gchat/main.tf new file mode 100644 index 0000000..1e27c62 --- /dev/null +++ b/grafana/contact-point-gchat/main.tf @@ -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\" . }}" + } +} \ No newline at end of file diff --git a/grafana/contact-point-gchat/output.tf b/grafana/contact-point-gchat/output.tf new file mode 100644 index 0000000..8600044 --- /dev/null +++ b/grafana/contact-point-gchat/output.tf @@ -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" +} diff --git a/grafana/contact-point-gchat/providers.tf b/grafana/contact-point-gchat/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/contact-point-gchat/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/contact-point-gchat/variables.tf b/grafana/contact-point-gchat/variables.tf new file mode 100644 index 0000000..0548cbe --- /dev/null +++ b/grafana/contact-point-gchat/variables.tf @@ -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 +} \ No newline at end of file diff --git a/grafana/contact-point-opsgenie-heartbeat/main.tf b/grafana/contact-point-opsgenie-heartbeat/main.tf new file mode 100644 index 0000000..e1c2704 --- /dev/null +++ b/grafana/contact-point-opsgenie-heartbeat/main.tf @@ -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 + } +} diff --git a/grafana/contact-point-opsgenie-heartbeat/output.tf b/grafana/contact-point-opsgenie-heartbeat/output.tf new file mode 100644 index 0000000..ff8ac1d --- /dev/null +++ b/grafana/contact-point-opsgenie-heartbeat/output.tf @@ -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." +} diff --git a/grafana/contact-point-opsgenie-heartbeat/providers.tf b/grafana/contact-point-opsgenie-heartbeat/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/contact-point-opsgenie-heartbeat/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/contact-point-opsgenie-heartbeat/variables.tf b/grafana/contact-point-opsgenie-heartbeat/variables.tf new file mode 100644 index 0000000..b3db1b1 --- /dev/null +++ b/grafana/contact-point-opsgenie-heartbeat/variables.tf @@ -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//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 +} diff --git a/grafana/contact-point-opsgenie/main.tf b/grafana/contact-point-opsgenie/main.tf new file mode 100644 index 0000000..5df9c08 --- /dev/null +++ b/grafana/contact-point-opsgenie/main.tf @@ -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 + } +} diff --git a/grafana/contact-point-opsgenie/output.tf b/grafana/contact-point-opsgenie/output.tf new file mode 100644 index 0000000..8600044 --- /dev/null +++ b/grafana/contact-point-opsgenie/output.tf @@ -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" +} diff --git a/grafana/contact-point-opsgenie/providers.tf b/grafana/contact-point-opsgenie/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/contact-point-opsgenie/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/contact-point-opsgenie/variables.tf b/grafana/contact-point-opsgenie/variables.tf new file mode 100644 index 0000000..9b638de --- /dev/null +++ b/grafana/contact-point-opsgenie/variables.tf @@ -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 +} diff --git a/grafana/datasource/datasource.tf b/grafana/datasource/datasource.tf new file mode 100644 index 0000000..710d31d --- /dev/null +++ b/grafana/datasource/datasource.tf @@ -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 + }) +} \ No newline at end of file diff --git a/grafana/datasource/output.tf b/grafana/datasource/output.tf new file mode 100644 index 0000000..f5f6b88 --- /dev/null +++ b/grafana/datasource/output.tf @@ -0,0 +1,3 @@ +output "datasource_uid" { + value = grafana_data_source.this.uid +} diff --git a/grafana/datasource/providers.tf b/grafana/datasource/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/datasource/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/datasource/variables.tf b/grafana/datasource/variables.tf new file mode 100644 index 0000000..a7a32fc --- /dev/null +++ b/grafana/datasource/variables.tf @@ -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 +} \ No newline at end of file diff --git a/grafana/message-template/message-template.tf b/grafana/message-template/message-template.tf new file mode 100644 index 0000000..a46be0a --- /dev/null +++ b/grafana/message-template/message-template.tf @@ -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 +} diff --git a/grafana/message-template/outputs.tf b/grafana/message-template/outputs.tf new file mode 100644 index 0000000..3ac36a7 --- /dev/null +++ b/grafana/message-template/outputs.tf @@ -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 } +} diff --git a/grafana/message-template/providers.tf b/grafana/message-template/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/message-template/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/message-template/templates.tf b/grafana/message-template/templates.tf new file mode 100644 index 0000000..6f841cc --- /dev/null +++ b/grafana/message-template/templates.tf @@ -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 = } + # 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}") + } + } +} diff --git a/grafana/message-template/variables.tf b/grafana/message-template/variables.tf new file mode 100644 index 0000000..5b241cd --- /dev/null +++ b/grafana/message-template/variables.tf @@ -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 +} \ No newline at end of file diff --git a/grafana/notification-policy/main.tf b/grafana/notification-policy/main.tf new file mode 100644 index 0000000..6ea9ebc --- /dev/null +++ b/grafana/notification-policy/main.tf @@ -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" + } + } +} diff --git a/grafana/notification-policy/output.tf b/grafana/notification-policy/output.tf new file mode 100644 index 0000000..73163df --- /dev/null +++ b/grafana/notification-policy/output.tf @@ -0,0 +1,4 @@ +output "policy_id" { + description = "ID of the notification policy resource" + value = grafana_notification_policy.this.id +} diff --git a/grafana/notification-policy/providers.tf b/grafana/notification-policy/providers.tf new file mode 100644 index 0000000..c5ab6b9 --- /dev/null +++ b/grafana/notification-policy/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "= 3.25.4" + } + } +} diff --git a/grafana/notification-policy/variables.tf b/grafana/notification-policy/variables.tf new file mode 100644 index 0000000..7f90446 --- /dev/null +++ b/grafana/notification-policy/variables.tf @@ -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 = } + contact_point = + } + 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" +} +