From f5ca31d62cbc52465df3f48e01bb7751fa68cdec Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Wed, 16 Jul 2025 08:50:43 +0200 Subject: [PATCH 01/34] Readme --- grafana/README.md | 70 +++- grafana/examples/alerts/alerts.yaml | 339 ++++++++++++++++++ .../templates/google-chat-body-template.tmpl | 26 ++ .../templates/google-chat-title-template.tmpl | 36 ++ 4 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 grafana/examples/alerts/alerts.yaml create mode 100644 grafana/examples/templates/google-chat-body-template.tmpl create mode 100644 grafana/examples/templates/google-chat-title-template.tmpl diff --git a/grafana/README.md b/grafana/README.md index 50ccb1c..3a7fb24 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -1,4 +1,72 @@ # Modules for Grafana alerts and dashboards -## How to use +## Alerting + +Please check documentation about Grafana alerting [here](https://itdoc.schwarz/x/X11nf) and [official documentation](https://grafana.com/docs/grafana/latest/alerting/) for deeper look. + +The Terraform modules are separated per resource type, check README in each module directory for spefic examples. +Below is example for alerts using β€ž**Prometheus/Thanos**β€œ datasorce and sending notification to β€ž**Google Chat**β€œ. + + + +```hcl title="main.tf" + # Datasource +module "datasource" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/datasource?ref=main" + datasource_name = "Thanos - Myteam" + datasource_url = var.datasource_url + datasource_username = var.datasource_username + datasource_password = var.datasource_password +} + +# Alert Receiver / Contact Point +module "google-chat-contact-point" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/contact-point-gchat?ref=main" + google-chat-url = var.google_chat_url + contact-point-name = "gchat" +} + + +# Alert Rule Folders +module "alert-folder" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/alert-folder?ref=main" + alert-folder = "Alerts" +} + + +# Template for messages +module "message-templates" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/message-template?ref=main" + templates_dir = "templates" + disable_provenance = true +} + +# Notification policies +module "notification-policy" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/notification-policy?ref=main" + + default_contact_point_uid = module.google-chat-contact-point.contact_name + group_by = ["alertname"] + + folder_policies = { + "Alerts" = module.google-chat-contact-point.contact_name + } +} + +# Alert definition +module "alerting" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/alerts?ref=main" + + alerts_dir = "alerts" + default_datasource_uid = module.datasource.datasource_uid + default_receiver = module.google-chat-contact-point.contact_name + default_folder_uid = module.alert-folder.folder_uid + default_interval_seconds = 60 + disable_provenance = true +} +``` +With this configuration you need to place your notification templates into `templates` folder and alert definitions in YAML format to `alerts` folder in same directoty where `main.tf`is located. +You can example for both in [examples](./examples/) folder. + +## Dashboard TODO diff --git a/grafana/examples/alerts/alerts.yaml b/grafana/examples/alerts/alerts.yaml new file mode 100644 index 0000000..95844dd --- /dev/null +++ b/grafana/examples/alerts/alerts.yaml @@ -0,0 +1,339 @@ +apiVersion: 1 +groups: + - orgId: 1 + name: "alerts" + interval: 1m + rules: + - uid: KubernetesPodCrashLooping + title: Kubernetes Pod CrashLooping + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + model: + editorMode: code + expr: increase(kube_pod_container_status_restarts_total{}[1h]) > 3 + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + 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 + noDataState: OK + execErrState: Error + for: 5m + annotations: + description: Container {{ $labels.container }} in pod {{ $labels.namespace }}/{{ $labels.pod }} is restarting too frequently + summary: Pod {{ $labels.pod }} is crash looping + isPaused: false + + - uid: KubernetesPodPending + title: Kubernetes Pod Pending + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + model: + editorMode: code + expr: kube_pod_status_phase{phase="Pending"} > 0 + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + 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 + noDataState: OK + execErrState: Error + for: 10m + isPaused: false + annotations: + description: Pod {{ $labels.pod }} is in Pending state + summary: Pod {{ $labels.pod }} is pending + + - uid: ContainerCPUUsageHigh + title: Container CPU Usage High + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + model: + editorMode: code + expr: rate(container_cpu_usage_seconds_total{image!=""}[5m]) > 0.9 + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + 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 + noDataState: OK + execErrState: Error + for: 5m + isPaused: false + annotations: + summary: "High CPU usage for container {{ $labels.container }}" + description: "Container {{ $labels.container }} in pod {{ $labels.pod }} is using >90% CPU for 5 minutes." + + - uid: ContainerMemoryUsageHigh + title: Container Memory Usage High + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + model: + editorMode: code + expr: container_memory_usage_bytes{} + / + container_spec_memory_limit_bytes{} + > 0.9 + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + 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 + noDataState: OK + execErrState: Error + for: 5m + isPaused: false + annotations: + summary: "High memory usage for container {{ $labels.container }}" + description: "Container {{ $labels.container }} in pod {{ $labels.pod }} is using >90% of its memory limit." + + - uid: PVCStorageAlmostFull + title: PVC Storage Almost Full + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + model: + editorMode: code + expr: kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes > 0.9 + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + - refId: C + datasourceUid: __expr__ + model: + 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 + noDataState: OK + execErrState: Error + for: 5m + isPaused: false + annotations: + summary: "PVC almost full" + description: "PersistentVolumeClaim {{ $labels.persistentvolumeclaim }} in namespace {{ $labels.namespace }} is >90% full." + +# Loki Alerts + - uid: loki-alert + title: Loki-Alert + condition: C + data: + - refId: A + queryType: range + relativeTimeRange: + from: 600 + to: 0 + #datasourceUid: berm4y85oiwaoc + #model: + # datasource: + # type: loki + # uid: berm4y85oiwaoc + editorMode: code + expr: count_over_time({job="controlling"} |= "error while querying DB" [10m]) + hide: false + instant: true + intervalMs: 1000 + maxDataPoints: 43200 + queryType: range + refId: A + - refId: reducer + queryType: expression + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0 + - 0 + type: gt + operator: + type: and + query: + params: [] + reducer: + params: [] + type: avg + type: query + datasource: + name: Expression + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + reducer: last + refId: reducer + type: reduce + - refId: C + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0 + type: gt + operator: + type: and + query: + params: + - C + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: reducer + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + noDataState: OK + execErrState: Error + for: 5m + isPaused: false diff --git a/grafana/examples/templates/google-chat-body-template.tmpl b/grafana/examples/templates/google-chat-body-template.tmpl new file mode 100644 index 0000000..9c02a8f --- /dev/null +++ b/grafana/examples/templates/google-chat-body-template.tmpl @@ -0,0 +1,26 @@ +{{ define "google-chat-body-template" -}} +{{- $alerts := .Alerts }} +{{- if not $alerts }}{{ $alerts = . }}{{ end }} + +{{- range $alerts }} +🚨 *{{ index .Labels "alertname" }}* ({{ .Status }}) + +{{- with index .Labels "pod" }} +πŸ›’οΈ Pod: `{{ . }}` +{{- end }} +{{- with index .Labels "container" }} +πŸ“¦ Container: `{{ . }}` +{{- end }} +{{- with index .Labels "stage" }} +πŸ§ͺ Stage: `{{ . }}` +{{- end }} +{{- with index .Labels "cluster" }} +🌐 K8s cluster: `{{ . }}` +{{- end }} + +πŸ“ {{ with index .Annotations "summary" }}{{ . }}{{ else }}n/a{{ end }} +πŸ“„ {{ with index .Annotations "description" }}{{ . }}{{ else }}n/a{{ end }} + +{{ end -}} +{{ end }} + diff --git a/grafana/examples/templates/google-chat-title-template.tmpl b/grafana/examples/templates/google-chat-title-template.tmpl new file mode 100644 index 0000000..5bbe8bc --- /dev/null +++ b/grafana/examples/templates/google-chat-title-template.tmpl @@ -0,0 +1,36 @@ +{{ define "google-chat-title-template" -}} +{{- if eq .Status "firing" -}} +πŸ”₯ Firing: +{{- else if eq .Status "resolved" -}} +βœ… Resolved: +{{- else -}} +⚠️ Alert Status: {{ .Status }}: +{{- end }} + +{{- $alerts := .Alerts }} +{{- if not $alerts }}{{ $alerts = . }}{{ end }} + +{{- $a1 := "" }}{{ $a2 := "" }}{{ $a3 := "" }}{{ $a4 := "" }}{{ $a5 := "" }} +{{- $a6 := "" }}{{ $a7 := "" }}{{ $a8 := "" }}{{ $a9 := "" }}{{ $a10 := "" }} +{{- $sep := " " }} + +{{- range $alerts }} + {{- $name := index .Labels "alertname" }} + {{- if and (ne $name $a1) (ne $name $a2) (ne $name $a3) (ne $name $a4) (ne $name $a5) + (ne $name $a6) (ne $name $a7) (ne $name $a8) (ne $name $a9) (ne $name $a10) }} + {{- printf "%s%s" $sep $name }} + {{- $sep = ", " }} + {{- if eq $a1 "" }}{{ $a1 = $name }} + {{- else if eq $a2 "" }}{{ $a2 = $name }} + {{- else if eq $a3 "" }}{{ $a3 = $name }} + {{- else if eq $a4 "" }}{{ $a4 = $name }} + {{- else if eq $a5 "" }}{{ $a5 = $name }} + {{- else if eq $a6 "" }}{{ $a6 = $name }} + {{- else if eq $a7 "" }}{{ $a7 = $name }} + {{- else if eq $a8 "" }}{{ $a8 = $name }} + {{- else if eq $a9 "" }}{{ $a9 = $name }} + {{- else if eq $a10 "" }}{{ $a10 = $name }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} From 7b1947acd47c35c3bc7e04f0c4519b8bb1b0d8b4 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Wed, 16 Jul 2025 08:58:51 +0200 Subject: [PATCH 02/34] vars --- grafana/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/grafana/README.md b/grafana/README.md index 3a7fb24..b129b6a 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -67,6 +67,17 @@ module "alerting" { ``` With this configuration you need to place your notification templates into `templates` folder and alert definitions in YAML format to `alerts` folder in same directoty where `main.tf`is located. You can example for both in [examples](./examples/) folder. +For this example you need export your secret variables e.g. with lookup in Secret Manager. + +```sh +export TF_VAR_grafana_url="" +export TF_VAR_grafana_username="admin" +export TF_VAR_grafana_password="xxxxxxx" +export TF_VAR_google_chat_url="https://chat.googleapis.com/v1/spaces/xxxxx" +export TF_VAR_datasource_url="https://xxxxx.stackit.cloud/instances/xxxxxx" +export TF_VAR_datasource_username="stackit9_xxxxx" +export TF_VAR_datasource_password="xxxxxx" +``` ## Dashboard TODO From ae79eda74581e53e0669ef61026b97fd77194545 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Wed, 30 Jul 2025 21:49:33 +0200 Subject: [PATCH 03/34] renamed google-chat to gchat --- grafana/README.md | 10 +++++----- grafana/contact-point-gchat/main.tf | 6 +++--- grafana/contact-point-gchat/variables.tf | 6 +++--- .../examples/templates/google-chat-body-template.tmpl | 2 +- .../examples/templates/google-chat-title-template.tmpl | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/grafana/README.md b/grafana/README.md index b129b6a..b9f995b 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -20,9 +20,9 @@ module "datasource" { } # Alert Receiver / Contact Point -module "google-chat-contact-point" { +module "gchat-contact-point" { source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/contact-point-gchat?ref=main" - google-chat-url = var.google_chat_url + gchat-url = var.google_chat_url contact-point-name = "gchat" } @@ -45,11 +45,11 @@ module "message-templates" { module "notification-policy" { source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/notification-policy?ref=main" - default_contact_point_uid = module.google-chat-contact-point.contact_name + default_contact_point_uid = module.gchat-contact-point.contact_name group_by = ["alertname"] folder_policies = { - "Alerts" = module.google-chat-contact-point.contact_name + "Alerts" = module.gchat-contact-point.contact_name } } @@ -59,7 +59,7 @@ module "alerting" { alerts_dir = "alerts" default_datasource_uid = module.datasource.datasource_uid - default_receiver = module.google-chat-contact-point.contact_name + default_receiver = module.gchat-contact-point.contact_name default_folder_uid = module.alert-folder.folder_uid default_interval_seconds = 60 disable_provenance = true diff --git a/grafana/contact-point-gchat/main.tf b/grafana/contact-point-gchat/main.tf index 1e27c62..eb21706 100644 --- a/grafana/contact-point-gchat/main.tf +++ b/grafana/contact-point-gchat/main.tf @@ -2,8 +2,8 @@ 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\" . }}" + url = var.gchat-url + message = "{{ template \"gchat-body-template\" . }}" + title = "{{ template \"gchat-title-template\" . }}" } } \ No newline at end of file diff --git a/grafana/contact-point-gchat/variables.tf b/grafana/contact-point-gchat/variables.tf index 0548cbe..9b9c48c 100644 --- a/grafana/contact-point-gchat/variables.tf +++ b/grafana/contact-point-gchat/variables.tf @@ -1,11 +1,11 @@ # Grafana contact point -variable "google-chat-url" { - description = "google-chat-webhook url" +variable "gchat-url" { + description = "gchat-webhook url" type = string sensitive = true } variable "contact-point-name" { - description = "google-chat-contact-point-name" + description = "gchat-contact-point-name" type = string } \ No newline at end of file diff --git a/grafana/examples/templates/google-chat-body-template.tmpl b/grafana/examples/templates/google-chat-body-template.tmpl index 9c02a8f..15385a3 100644 --- a/grafana/examples/templates/google-chat-body-template.tmpl +++ b/grafana/examples/templates/google-chat-body-template.tmpl @@ -1,4 +1,4 @@ -{{ define "google-chat-body-template" -}} +{{ define "gchat-body-template" -}} {{- $alerts := .Alerts }} {{- if not $alerts }}{{ $alerts = . }}{{ end }} diff --git a/grafana/examples/templates/google-chat-title-template.tmpl b/grafana/examples/templates/google-chat-title-template.tmpl index 5bbe8bc..45aa607 100644 --- a/grafana/examples/templates/google-chat-title-template.tmpl +++ b/grafana/examples/templates/google-chat-title-template.tmpl @@ -1,4 +1,4 @@ -{{ define "google-chat-title-template" -}} +{{ define "gchat-title-template" -}} {{- if eq .Status "firing" -}} πŸ”₯ Firing: {{- else if eq .Status "resolved" -}} From 7413af8b2bdf5edfa349ae34428bdaa3bb0c4c08 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Tue, 5 Aug 2025 16:40:48 +0200 Subject: [PATCH 04/34] updated grafana modules --- grafana/README.md | 10 ++--- grafana/Untitled111.md | 1 + grafana/alerts/locals.tf | 37 +++++++++-------- grafana/alerts/main.tf | 10 ++--- grafana/alerts/output.tf | 7 ++++ grafana/alerts/variables.tf | 22 ++++------ grafana/contact-point-gchat/main.tf | 10 ++--- grafana/contact-point-gchat/output.tf | 14 +++++-- grafana/contact-point-gchat/templates.tf | 24 +++++++++++ grafana/contact-point-gchat/variables.tf | 37 +++++++++++++---- grafana/datasource/datasource.tf | 18 +++++--- grafana/datasource/output.tf | 5 ++- grafana/datasource/variables.tf | 41 +++++++++++++------ .../templates/google-chat-body-template.tmpl | 2 +- .../templates/google-chat-title-template.tmpl | 2 +- 15 files changed, 159 insertions(+), 81 deletions(-) create mode 100644 grafana/Untitled111.md create mode 100644 grafana/contact-point-gchat/templates.tf diff --git a/grafana/README.md b/grafana/README.md index b9f995b..b129b6a 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -20,9 +20,9 @@ module "datasource" { } # Alert Receiver / Contact Point -module "gchat-contact-point" { +module "google-chat-contact-point" { source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/contact-point-gchat?ref=main" - gchat-url = var.google_chat_url + google-chat-url = var.google_chat_url contact-point-name = "gchat" } @@ -45,11 +45,11 @@ module "message-templates" { module "notification-policy" { source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/notification-policy?ref=main" - default_contact_point_uid = module.gchat-contact-point.contact_name + default_contact_point_uid = module.google-chat-contact-point.contact_name group_by = ["alertname"] folder_policies = { - "Alerts" = module.gchat-contact-point.contact_name + "Alerts" = module.google-chat-contact-point.contact_name } } @@ -59,7 +59,7 @@ module "alerting" { alerts_dir = "alerts" default_datasource_uid = module.datasource.datasource_uid - default_receiver = module.gchat-contact-point.contact_name + default_receiver = module.google-chat-contact-point.contact_name default_folder_uid = module.alert-folder.folder_uid default_interval_seconds = 60 disable_provenance = true diff --git a/grafana/Untitled111.md b/grafana/Untitled111.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/grafana/Untitled111.md @@ -0,0 +1 @@ + diff --git a/grafana/alerts/locals.tf b/grafana/alerts/locals.tf index e685468..322654b 100644 --- a/grafana/alerts/locals.tf +++ b/grafana/alerts/locals.tf @@ -1,10 +1,8 @@ locals { - ################################################################# - # 1. Discover & decode alert YAML files from / - ################################################################# + # 1. Find and decode YAML files alert_files = fileset( "${path.root}/${var.alerts_dir}", - var.file_pattern + "**/*.yaml" ) decoded_files = [ @@ -12,31 +10,36 @@ locals { yamldecode(file("${path.root}/${var.alerts_dir}/${f}")) ] - ################################################################# - # 2. Flatten: each file may contain multiple groups - ################################################################# + # 2. Flatten: each YAML may define multiple groups groups_raw = flatten([ for doc in local.decoded_files : try(doc.groups, []) ]) - ################################################################# - # 3. Merge defaults & convert camelCase β†’ snake_case - ################################################################# + # 3. Inject defaults (datasource, folder, interval) and sanitize models 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") + uid = try(g.uid, lower(replace(g.name, " ", "-"))) + folder_uid = var.folder_uid + interval = try(g.interval, "${var.default_interval_seconds}s") 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) + # Preserve __expr__ for expressions, otherwise inject default UID + datasource_uid = ( + try(d.datasourceUid, "") == "__expr__" + ? "__expr__" + : var.datasource_uid + ) + relative_time_range = try(d.relativeTimeRange, { from = 600, to = 0 }) + + # Sanitize model: remove keys Grafana ignores to prevent plan drift + model = { + for k, v in d.model : k => v + if !(k == "queryType" || k == "query_type") + } }) ] }) diff --git a/grafana/alerts/main.tf b/grafana/alerts/main.tf index 5a1a078..837664f 100644 --- a/grafana/alerts/main.tf +++ b/grafana/alerts/main.tf @@ -1,8 +1,5 @@ resource "grafana_rule_group" "this" { - for_each = { - for g in local.groups : - g.uid => g - } + for_each = { for g in local.groups : g.uid => g } name = each.value.name folder_uid = each.value.folder_uid @@ -21,14 +18,15 @@ resource "grafana_rule_group" "this" { uid = rule.value.uid name = try(rule.value.title, rule.value.name) condition = rule.value.condition + for = try(rule.value.for, null) 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, {}) + # Merge team labels with platform-injected receiver annotations = try(rule.value.annotations, {}) + labels = try(rule.value.labels, {}) dynamic "data" { for_each = rule.value.data diff --git a/grafana/alerts/output.tf b/grafana/alerts/output.tf index af60ede..facf109 100644 --- a/grafana/alerts/output.tf +++ b/grafana/alerts/output.tf @@ -2,3 +2,10 @@ output "rule_group_ids" { description = "Map: . β†’ Grafana rule-group ID" value = { for k, v in grafana_rule_group.this : k => v.id } } +output "debug_alert_files" { + value = local.alert_files +} +output "debug_groups" { + description = "Parsed groups from YAML" + value = local.groups +} \ No newline at end of file diff --git a/grafana/alerts/variables.tf b/grafana/alerts/variables.tf index 345bfda..34d8f20 100644 --- a/grafana/alerts/variables.tf +++ b/grafana/alerts/variables.tf @@ -4,35 +4,29 @@ variable "alerts_dir" { description = "Relative path to the directory containing alert rule YAML files." } -variable "file_pattern" { +variable "datasource_uid" { + description = "Grafana datasource UID to apply to all alerts in this module" type = string - default = "*.y{a,}ml" - description = "Glob pattern to match alert rule YAML files (e.g. *.yaml, *.yml)." } -variable "default_datasource_uid" { +variable "folder_uid" { + description = "Grafana folder UID where alerts will be placed" type = string - description = "UID of the Prometheus or Thanos datasource to use if not specified in the alert rule." } -variable "default_receiver" { +variable "receiver" { + description = "Contact point name to associate with alerts" 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" { + description = "Default evaluation interval (in seconds)" type = number default = 60 - description = "Default evaluation interval (in seconds) for alert rule groups if not set in YAML." } variable "disable_provenance" { + description = "Disable provenance flag for imported alerts" 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 index eb21706..bed73bf 100644 --- a/grafana/contact-point-gchat/main.tf +++ b/grafana/contact-point-gchat/main.tf @@ -1,9 +1,9 @@ resource "grafana_contact_point" "this" { - name = var.contact-point-name + name = var.contact_point_name googlechat { - url = var.gchat-url - message = "{{ template \"gchat-body-template\" . }}" - title = "{{ template \"gchat-title-template\" . }}" + url = var.gchat_url + message = "{{ template \"${var.template_prefix}gchat-body-template\" . }}" + title = "{{ template \"${var.template_prefix}gchat-title-template\" . }}" } -} \ No newline at end of file +} diff --git a/grafana/contact-point-gchat/output.tf b/grafana/contact-point-gchat/output.tf index 8600044..6f049c6 100644 --- a/grafana/contact-point-gchat/output.tf +++ b/grafana/contact-point-gchat/output.tf @@ -1,8 +1,14 @@ -output "contact_name" { +output "contact_point_name" { value = grafana_contact_point.this.name - description = "UID of the created contact point" + description = "Name of the created contact point" } -output "contact_id" { + +output "contact_point_id" { value = grafana_contact_point.this.id - description = "UID of the created contact point" + description = "ID of the created contact point" +} + +output "template_names" { + value = [for t in grafana_message_template.templates : t.name] + description = "List of message template names created by this module" } diff --git a/grafana/contact-point-gchat/templates.tf b/grafana/contact-point-gchat/templates.tf new file mode 100644 index 0000000..f547011 --- /dev/null +++ b/grafana/contact-point-gchat/templates.tf @@ -0,0 +1,24 @@ +locals { + # Collect template files relative to root + template_files = fileset( + "${path.root}/${var.templates_dir}", + var.file_pattern + ) + + # Map filename (without extension) to template content + templates = { + for rel_path in local.template_files : + trimsuffix(basename(rel_path), ".tmpl") => { + content = file("${path.root}/${var.templates_dir}/${rel_path}") + } + } +} + +resource "grafana_message_template" "templates" { + for_each = local.templates + + name = "${var.template_prefix}${each.key}" + template = each.value.content + disable_provenance = var.disable_provenance +} + diff --git a/grafana/contact-point-gchat/variables.tf b/grafana/contact-point-gchat/variables.tf index 9b9c48c..156bc39 100644 --- a/grafana/contact-point-gchat/variables.tf +++ b/grafana/contact-point-gchat/variables.tf @@ -1,11 +1,32 @@ -# Grafana contact point -variable "gchat-url" { - description = "gchat-webhook url" - type = string - sensitive = true +variable "contact_point_name" { + description = "Name of the Grafana contact point" + type = string } -variable "contact-point-name" { - description = "gchat-contact-point-name" - type = string +variable "gchat_url" { + description = "Google Chat webhook URL" + type = string +} + +variable "templates_dir" { + description = "Path to directory containing template files (.tmpl)" + type = string +} + +variable "file_pattern" { + description = "Pattern to match template files" + type = string + default = "*.tmpl" +} + +variable "disable_provenance" { + description = "Disable provenance for message templates" + type = bool + default = false +} + +variable "template_prefix" { + description = "Optional prefix for template names to avoid collisions" + type = string + default = "" } \ No newline at end of file diff --git a/grafana/datasource/datasource.tf b/grafana/datasource/datasource.tf index 710d31d..b943c87 100644 --- a/grafana/datasource/datasource.tf +++ b/grafana/datasource/datasource.tf @@ -1,11 +1,17 @@ resource "grafana_data_source" "this" { - type = "prometheus" - name = var.datasource_name - url = var.datasource_url + for_each = var.datasources + + name = each.key + type = each.value.type + url = var.datasource_urls[each.value.url_key] + is_default = coalesce(each.value.is_default, false) + basic_auth_enabled = true - basic_auth_username = var.datasource_username + basic_auth_username = var.datasource_users[each.value.user_key] secure_json_data_encoded = jsonencode({ - basicAuthPassword = var.datasource_password + basicAuthPassword = var.datasource_passwords[each.value.pass_key] }) -} \ No newline at end of file + + json_data_encoded = each.value.json_data != null ? jsonencode(each.value.json_data) : null +} diff --git a/grafana/datasource/output.tf b/grafana/datasource/output.tf index f5f6b88..38d026f 100644 --- a/grafana/datasource/output.tf +++ b/grafana/datasource/output.tf @@ -1,3 +1,4 @@ -output "datasource_uid" { - value = grafana_data_source.this.uid +output "datasource_uids" { + description = "UIDs of created Grafana datasources" + value = { for k, v in grafana_data_source.this : k => v.uid } } diff --git a/grafana/datasource/variables.tf b/grafana/datasource/variables.tf index a7a32fc..5602fd8 100644 --- a/grafana/datasource/variables.tf +++ b/grafana/datasource/variables.tf @@ -1,18 +1,35 @@ -# Grafana contact point -variable "datasource_url" { - type = string +# Define datasources (non-sensitive metadata only) +variable "datasources" { + description = < Date: Tue, 5 Aug 2025 19:55:24 +0200 Subject: [PATCH 05/34] updated README --- grafana/README.md | 261 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 215 insertions(+), 46 deletions(-) diff --git a/grafana/README.md b/grafana/README.md index b129b6a..e0e9fe5 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -1,4 +1,6 @@ # Modules for Grafana alerts and dashboards + + ## Alerting @@ -7,77 +9,244 @@ Please check documentation about Grafana alerting [here](https://itdoc.schwarz/x The Terraform modules are separated per resource type, check README in each module directory for spefic examples. Below is example for alerts using β€ž**Prometheus/Thanos**β€œ datasorce and sending notification to β€ž**Google Chat**β€œ. +## Authentication +Set Grafana credentials as Terraform variables: -```hcl title="main.tf" - # Datasource +```bash +export TF_VAR_grafana_url="https://grafana.example.com" +export TF_VAR_grafana_username="admin" +export TF_VAR_grafana_password="super-secret" +``` + +These credentials are used by all modules to authenticate with the Grafana API. + +--- + +## Directory Structure + +Organize alerts, templates, and Terraform code as follows: + +``` +. +β”œβ”€β”€ alerts/ +β”‚ β”œβ”€β”€ infra/ +β”‚ β”‚ β”œβ”€β”€ loki/ +β”‚ β”‚ β”‚ └── alert-loki.yaml +β”‚ β”‚ └── thanos/ +β”‚ β”‚ └── alert-thanos.yaml +β”‚ β”œβ”€β”€ oncall/ +β”‚ β”‚ └── alert-oncall.yaml +β”‚ └── heartbeats/ +β”‚ └── alert-heartbeat.yaml +β”œβ”€β”€ templates/ +β”‚ └── myteam/ +β”‚ └── gchat-message.tmpl +└── main.tf +``` + +- **Alerts**: YAML files defining rule groups (`apiVersion: 1, groups: [...]`). +- **Templates**: Notification templates for Google Chat contact points. +- **Terraform code**: References modules and binds everything together. + +--- + +## Defining Secrets + +Datasource URLs and credentials should be stored in Terraform variables, not hardcoded. + +**Example: Environment Variables** + +```bash +export TF_VAR_thanos_coin_prd_url="https://thanos.example.com" +export TF_VAR_thanos_coin_prd_user="reader" +export TF_VAR_thanos_coin_prd_pass="password" + +export TF_VAR_loki_coin_prd_url="https://loki.example.com" +export TF_VAR_loki_coin_prd_user="reader" +export TF_VAR_loki_coin_prd_pass="password" + +export TF_VAR_opsgenie_api_key="xxxxxx" +``` + +--- + +## Module Usage + +### Datasources + +Define multiple datasources (Prometheus, Loki, etc.) with unique keys for URL/username/password: + +```hcl module "datasource" { source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/datasource?ref=main" - datasource_name = "Thanos - Myteam" - datasource_url = var.datasource_url - datasource_username = var.datasource_username - datasource_password = var.datasource_password -} -# Alert Receiver / Contact Point -module "google-chat-contact-point" { + datasources = { + Thanos-Common-Infra-PRD = { + type = "prometheus" + url_key = "thanos_coin_prd" + user_key = "thanos_coin_prd" + pass_key = "thanos_coin_prd" + is_default = true + } + Loki-Common-Infra-PRD = { + type = "loki" + url_key = "loki_coin_prd" + user_key = "loki_coin_prd" + pass_key = "loki_coin_prd" + } + } + + datasource_urls = { + thanos_coin_prd = var.thanos_coin_prd_url + loki_coin_prd = var.loki_coin_prd_url + } + + datasource_users = { + thanos_coin_prd = var.thanos_coin_prd_user + loki_coin_prd = var.loki_coin_prd_user + } + + datasource_passwords = { + thanos_coin_prd = var.thanos_coin_prd_pass + loki_coin_prd = var.loki_coin_prd_pass + } +} +``` + +### Contact Points + +**Google Chat** + +Each Google Chat space is configured as a contact point: + +```hcl +module "gchat-contact-point-coin" { source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/contact-point-gchat?ref=main" - google-chat-url = var.google_chat_url - contact-point-name = "gchat" + gchat_url = var.gchat_url_coin + contact_point_name = "gchat-coin" + templates_dir = "templates/coin" + template_prefix = "coin-" + disable_provenance = true } +``` +**OpsGenie** -# Alert Rule Folders +OpsGenie contact points use API keys: + +```hcl +module "opsgenie-contact-point" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/contact-point-opsgenie?ref=main" + contact_point_name = "opsgenie-dev" + opsgenie_api_key = var.opsgenie_api_key +} +``` + +### Alert Folders + +Organize alerts in Grafana folders for logical separation: + +```hcl module "alert-folder" { - source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/alert-folder?ref=main" - alert-folder = "Alerts" + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/alert-folder?ref=main" + alert-folder = "Common-Infra-Alerts" } +``` +### Notification Policies -# Template for messages -module "message-templates" { - source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/message-template?ref=main" - templates_dir = "templates" - disable_provenance = true -} +Map folders to contact points (e.g., send β€œCommon-Infra-Alerts” to Google Chat): -# Notification policies +```hcl module "notification-policy" { source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/notification-policy?ref=main" - - default_contact_point_uid = module.google-chat-contact-point.contact_name + default_contact_point_uid = module.gchat-contact-point-coin.contact_point_name group_by = ["alertname"] folder_policies = { - "Alerts" = module.google-chat-contact-point.contact_name + "Common-Infra-Alerts" = module.gchat-contact-point-coin.contact_point_name + "Common-Infra-OnCall-Alerts" = module.opsgenie-contact-point.contact_name } } +``` -# Alert definition -module "alerting" { - source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/alerts?ref=main" +### Alert Definitions - alerts_dir = "alerts" - default_datasource_uid = module.datasource.datasource_uid - default_receiver = module.google-chat-contact-point.contact_name - default_folder_uid = module.alert-folder.folder_uid - default_interval_seconds = 60 - disable_provenance = true +Alert rules are defined in YAML and applied via the module: + +```hcl +module "alerting-coin" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//grafana/alerts?ref=main" + alerts_dir = "alerts/common-infra/thanos" + datasource_uid = module.datasource.datasource_uids["Thanos-Common-Infra-PRD"] + folder_uid = module.alert-folder.folder_uid + receiver = module.gchat-contact-point-coin.contact_point_name + disable_provenance = true } ``` -With this configuration you need to place your notification templates into `templates` folder and alert definitions in YAML format to `alerts` folder in same directoty where `main.tf`is located. -You can example for both in [examples](./examples/) folder. -For this example you need export your secret variables e.g. with lookup in Secret Manager. -```sh -export TF_VAR_grafana_url="" -export TF_VAR_grafana_username="admin" -export TF_VAR_grafana_password="xxxxxxx" -export TF_VAR_google_chat_url="https://chat.googleapis.com/v1/spaces/xxxxx" -export TF_VAR_datasource_url="https://xxxxx.stackit.cloud/instances/xxxxxx" -export TF_VAR_datasource_username="stackit9_xxxxx" -export TF_VAR_datasource_password="xxxxxx" +--- + +## Alert YAML Format + +Alerts are defined in YAML Grafana format. The easiest way to get example from scratch is to define alert in Grafana UI and then export it using β€žExport rulesβ€œ button. +However, make sure to remove some fields which are not needed and provided by Terraform module logic automatically: + +- `datasourceUid` (defined with `alerts` module) +- `notification_settings` ( defined with `notification policy` module) + +Each file must have `apiVersion: 1` and define groups: + +```yaml +apiVersion: 1 +groups: + - name: infra-alerts + interval: 1m + rules: + - uid: pod-restart-alert + title: "Pod Restart Count High" + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + model: + expr: increase(kube_pod_container_status_restarts_total{}[5m]) > 3 + instant: true + refId: A + - refId: C + datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: [0] + type: gt + operator: + type: and + query: + params: [C] + reducer: + type: last + type: query + expression: A + type: threshold + noDataState: OK + execErrState: Error + for: 5m + annotations: + description: "Pod is restarting too often" ``` -## Dashboard -TODO +--- + + +## Updating Alerts or Contact Points + +- Add new YAML files under `alerts/` for additional rules. +- Add new modules in `main.tf` for new datasources or contact points. +- Run `terraform apply` to sync changes to Grafana. + + +You can examples for alerts and templates in [examples](./examples/) folder. From 4de6f8a4dfcf0cd6d65ebc7402de6cdec0bdaa76 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Tue, 5 Aug 2025 19:58:09 +0200 Subject: [PATCH 06/34] up README --- grafana/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana/README.md b/grafana/README.md index e0e9fe5..856b2d9 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -7,7 +7,7 @@ Please check documentation about Grafana alerting [here](https://itdoc.schwarz/x/X11nf) and [official documentation](https://grafana.com/docs/grafana/latest/alerting/) for deeper look. The Terraform modules are separated per resource type, check README in each module directory for spefic examples. -Below is example for alerts using β€ž**Prometheus/Thanos**β€œ datasorce and sending notification to β€ž**Google Chat**β€œ. +Below is example for alerts using β€ž**Prometheus/Thanos**β€œ datasource and sending notification to β€ž**Google Chat**β€œ. ## Authentication @@ -30,7 +30,7 @@ Organize alerts, templates, and Terraform code as follows: ``` . β”œβ”€β”€ alerts/ -β”‚ β”œβ”€β”€ infra/ +β”‚ β”œβ”€β”€ common-infra/ β”‚ β”‚ β”œβ”€β”€ loki/ β”‚ β”‚ β”‚ └── alert-loki.yaml β”‚ β”‚ └── thanos/ From 06b2db44bb4b7169d9fba597e5e05f19c3049dd5 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Thu, 14 Aug 2025 15:50:38 +0200 Subject: [PATCH 07/34] change version constraint for ske module --- ske-cluster/providers.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ske-cluster/providers.tf b/ske-cluster/providers.tf index 67a3dc9..4d06b01 100644 --- a/ske-cluster/providers.tf +++ b/ske-cluster/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.50.0" + version = ">= 0.50.0" } } -} \ No newline at end of file +} From 77a36b4bb0c5074b31003fe232b1ae4a5a7999be Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Tue, 19 Aug 2025 15:38:33 +0200 Subject: [PATCH 08/34] STACKITCIN-299 Add support for derivedFields to Grafana TF module --- grafana/Untitled111.md | 1 - grafana/datasource/datasource.tf | 66 ++++++++++++++++++++++++++++++-- grafana/datasource/variables.tf | 29 ++++++++++++-- 3 files changed, 88 insertions(+), 8 deletions(-) delete mode 100644 grafana/Untitled111.md diff --git a/grafana/Untitled111.md b/grafana/Untitled111.md deleted file mode 100644 index 8b13789..0000000 --- a/grafana/Untitled111.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/grafana/datasource/datasource.tf b/grafana/datasource/datasource.tf index b943c87..cb953e1 100644 --- a/grafana/datasource/datasource.tf +++ b/grafana/datasource/datasource.tf @@ -1,9 +1,13 @@ +# main.tf + +# Step 1: Create all Grafana data sources with their basic configuration. +# This resource establishes the fundamental properties of each data source. resource "grafana_data_source" "this" { for_each = var.datasources - name = each.key - type = each.value.type - url = var.datasource_urls[each.value.url_key] + name = each.key + type = each.value.type + url = var.datasource_urls[each.value.url_key] is_default = coalesce(each.value.is_default, false) basic_auth_enabled = true @@ -13,5 +17,61 @@ resource "grafana_data_source" "this" { basicAuthPassword = var.datasource_passwords[each.value.pass_key] }) + # Encodes initial, non-dependent JSON data. + # Configurations that depend on other datasource UIDs will be handled separately. json_data_encoded = each.value.json_data != null ? jsonencode(each.value.json_data) : null } + +# Step 2: Apply Loki-specific 'derivedFields' configuration. +# This resource targets Loki data sources that need to link to another data source (like Tempo). +# It runs after the initial data sources are created to resolve the UIDs. +resource "grafana_data_source_config" "loki_derived_fields" { + # Filter for datasources that are of type 'loki' and have 'derived_fields' defined. + for_each = { + for k, v in var.datasources : k => v + if v.type == "loki" && v.derived_fields != null + } + + # The UID of the Loki data source to configure. + uid = grafana_data_source.this[each.key].uid + + # Construct the json_data with the derivedFields. + json_data_encoded = jsonencode({ + derivedFields = [ + for field in each.value.derived_fields : { + # The UID of the target data source (e.g., Tempo). + datasourceUid = grafana_data_source.this[field.target_datasource_name].uid + matcherRegex = field.matcher_regex + name = field.name + url = field.url + } + ] + }) +} + +# Step 3: Apply Tempo-specific 'tracesToLogsV2' configuration. +# This resource targets Tempo data sources that need to link back to a logging data source (like Loki). +resource "grafana_data_source_config" "tempo_traces_to_logs" { + # Filter for datasources that are of type 'tempo' and have 'traces_to_logs' defined. + for_each = { + for k, v in var.datasources : k => v + if v.type == "tempo" && v.traces_to_logs != null + } + + # The UID of the Tempo data source to configure. + uid = grafana_data_source.this[each.key].uid + + # Construct the json_data with the tracesToLogsV2 settings. + json_data_encoded = jsonencode({ + tracesToLogsV2 = { + # The UID of the target data source (e.g., Loki). + datasourceUid = grafana_data_source.this[each.value.traces_to_logs.target_datasource_name].uid + query = each.value.traces_to_logs.query + customQuery = coalesce(each.value.traces_to_logs.custom_query, true) + filterBySpanID = coalesce(each.value.traces_to_logs.filter_by_span_id, false) + filterByTraceID = coalesce(each.value.traces_to_logs.filter_by_trace_id, false) + spanStartTimeShift = each.value.traces_to_logs.span_start_time_shift + spanEndTimeShift = each.value.traces_to_logs.span_end_time_shift + } + }) +} diff --git a/grafana/datasource/variables.tf b/grafana/datasource/variables.tf index 5602fd8..cbf19db 100644 --- a/grafana/datasource/variables.tf +++ b/grafana/datasource/variables.tf @@ -1,17 +1,38 @@ +# variables.tf + # Define datasources (non-sensitive metadata only) variable "datasources" { description = < Date: Tue, 19 Aug 2025 15:50:46 +0200 Subject: [PATCH 09/34] STACKITCIN-299 Hotfix add lifecycle --- grafana/datasource/datasource.tf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/grafana/datasource/datasource.tf b/grafana/datasource/datasource.tf index cb953e1..a2eec52 100644 --- a/grafana/datasource/datasource.tf +++ b/grafana/datasource/datasource.tf @@ -20,6 +20,13 @@ resource "grafana_data_source" "this" { # Encodes initial, non-dependent JSON data. # Configurations that depend on other datasource UIDs will be handled separately. json_data_encoded = each.value.json_data != null ? jsonencode(each.value.json_data) : null + + # ignore changes made by the _config resource + lifecycle { + ignore_changes = [ + json_data_encoded, + ] + } } # Step 2: Apply Loki-specific 'derivedFields' configuration. From 0839a8adc44edfd9e23688cf56ffff887234446a Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Fri, 8 Aug 2025 16:21:49 +0200 Subject: [PATCH 10/34] feature(iac): add dns module --- dns/README.md | 47 +++++++++++++++++ dns/dns.tf | 34 ++++++++++++ dns/outputs.tf | 46 +++++++++++++++++ dns/providers.tf | 9 ++++ dns/variables.tf | 132 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+) create mode 100644 dns/README.md create mode 100644 dns/dns.tf create mode 100644 dns/outputs.tf create mode 100644 dns/providers.tf create mode 100644 dns/variables.tf diff --git a/dns/README.md b/dns/README.md new file mode 100644 index 0000000..9b072e8 --- /dev/null +++ b/dns/README.md @@ -0,0 +1,47 @@ +# Terraform module to create DNS zone + +## Example for main.tf + +```tf +locals { + stackit_project_id = "fb06b3bf-70b6-45bf-b1a4-e84708b26f92" + region = "eu01" + env = "dev" +} + +module "dns" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns" + project_id = "my-stackit-project-id" + + zone_name = "example-zone" + dns_name = "example.com" + + contact_email = "admin@example.com" + description = "Main DNS zone - managed via Terraform" + default_ttl = 3600 + + record_name = "www.example.com" + record_type = "A" + records = ["192.0.29.1"] + ttl = 3600 + comment = "My example records - managed by Terraform" +} +``` + +## Usage Options + +### Use an Existing DNS Zone + +#### If you already have a DNS zone created in STACKIT, simply provide the `zone_id`: + +```hcl +module "dns" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns" + project_id = "your-project-id" + + zone_id = "preexisting-zone-id" + record_name = "www.example.com" + record_type = "A" + records = ["192.0.29.1"] +} +``` diff --git a/dns/dns.tf b/dns/dns.tf new file mode 100644 index 0000000..3101bbf --- /dev/null +++ b/dns/dns.tf @@ -0,0 +1,34 @@ +resource "stackit_dns_zone" "this" { + count = var.zone_id == null ? 1 : 0 + project_id = var.stackit_project_id + name = var.dns_zone_name + dns_name = var.dns_zone_dns_name + + # Optional attributes + acl = var.dns_zone_acl + active = var.dns_zone_active + contact_email = var.dns_zone_contact_email + default_ttl = var.dns_zone_default_ttl + description = var.dns_zone_description + expire_time = var.dns_zone_expire_time + is_reverse_zone = var.dns_zone_is_reverse_zone + negative_cache = var.dns_zone_negative_cache + primaries = var.dns_zone_primaries + refresh_time = var.dns_zone_refresh_time + retry_time = var.dns_zone_retry_time + type = var.dns_zone_type +} + +resource "stackit_dns_record_set" "this" { + project_id = var.stackit_project_id + name = var.dns_record_set_name + records = var.dns_record_set_records + type = var.dns_record_set_type + #zone_id = var.dns_record_set_zone_id + zone_id = var.zone_id != null ? var.zone_id : stackit_dns_zone.this[0].zone_id + + # Optional + active = var.dns_record_set_active + comment = var.dns_record_set_comment + ttl = var.dns_record_set_ttl +} diff --git a/dns/outputs.tf b/dns/outputs.tf new file mode 100644 index 0000000..947a9d3 --- /dev/null +++ b/dns/outputs.tf @@ -0,0 +1,46 @@ +# === DNS Zone Outputs === +output "zone_id" { + value = var.zone_id != null ? var.zone_id : stackit_dns_zone.this[0].zone_id + description = "The DNS zone ID used or created" +} + +output "zone_primary_name_server" { + value = stackit_dns_zone.this.primary_name_server + description = "Primary name server" +} + +output "zone_state" { + value = stackit_dns_zone.this.state + description = "State of the DNS zone" +} + +output "zone_visibility" { + value = stackit_dns_zone.this.visibility + description = "Zone visibility" +} + +output "zone_record_count" { + value = stackit_dns_zone.this.record_count + description = "Number of DNS records in the zone" +} + +# === DNS Record Set Outputs === +output "record_set_fqdn" { + value = stackit_dns_record_set.this.fqdn + description = "Fully qualified domain name of the record" +} + +output "record_set_id" { + value = stackit_dns_record_set.this.record_set_id + description = "ID of the record set" +} + +output "record_set_state" { + value = stackit_dns_record_set.this.state + description = "State of the record set" +} + +output "record_set_error" { + value = stackit_dns_record_set.this.error + description = "Error during record creation (if any)" +} diff --git a/dns/providers.tf b/dns/providers.tf new file mode 100644 index 0000000..232c583 --- /dev/null +++ b/dns/providers.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.59.0" + + } + } +} diff --git a/dns/variables.tf b/dns/variables.tf new file mode 100644 index 0000000..f254546 --- /dev/null +++ b/dns/variables.tf @@ -0,0 +1,132 @@ +variable "stackit_project_id" { + description = "STACKIT project ID to which the dns zone is associated." + type = string +} + +variable "zone_id" { + description = "ID of an existing DNS zone. If provided, no new zone will be created." + type = string + default = null +} + +# === DNS Zone variables === +variable "dns_zone_name" { + description = "Descriptive name of the DNS zone." + type = string +} + +variable "dns_zone_dns_name" { + description = "The actual zone name, e.g. example.com" + type = string +} + +variable "dns_zone_acl" { + description = "Access control list, e.g. 0.0.0.0/0,::/0" + type = string + default = null +} + +variable "dns_zone_active" { + description = "Whether the zone is active. Defaults to true" + type = bool + default = true +} + +variable "dns_zone_contact_email" { + description = "Contact email for the zone." + type = string + default = null +} + +variable "dns_zone_default_ttl" { + description = "The default value of the TTL for new resource records in the DNS zone. Time in seconds. Defaults to 3600" + type = number + default = 3600 +} + +variable "dns_zone_description" { + description = "An additional descriptive free text for the DNS zone." + type = string + default = null +} + +variable "dns_zone_expire_time" { + description = "If the secondary DNS server cannot perform a serial check in this interval, it must assume that its copy of the zone is obsolete and discard it. Time in seconds. Defaults to 1209600" + type = number + default = 1209600 +} + +variable "dns_zone_is_reverse_zone" { + description = "Indicates whether a zone is a reverse DNS zone or a forward zone. Both have a different set of allowed resource record types. Defaults to false" + type = bool + default = false +} + +variable "dns_zone_negative_cache" { + description = "The response about the non-existence of a resource record is cached for this interval. Time in seconds. Defaults to 60" + type = number + default = 60 +} + +variable "dns_zone_primaries" { + description = "List of primary nameservers (for secondary zone)" + type = list(string) + default = [] +} + +variable "dns_zone_refresh_time" { + description = "This is the amount of time a secondary DNS server will wait before checking with the primary for a new serial if a zone refresh fails. Time in seconds. Defaults to 3600" + type = number + default = 3600 +} + +variable "dns_zone_retry_time" { + description = "This is the amount of time a secondary DNS server waits before retrying a zone update if the update fails. Time in seconds. Defaults to 600" + type = number + default = 600 +} + +variable "dns_zone_type" { + description = "Zone type (primary or secondary). Defaults to primary" + type = string + default = "primary" +} + +# === DNS Record Set variables === +variable "dns_record_set_name" { + description = "Name of the DNS record (e.g. 'www.example.com')" + type = string +} + +variable "dns_record_set_records" { + description = "List of DNS record values" + type = list(string) +} + +variable "dns_record_set_type" { + description = "Record type (e.g. 'A', 'CNAME', etc.)" + type = string +} + +variable "dns_record_set_zone_id" { + description = "The zone ID for the DNS record" + type = string +} + +variable "dns_record_set_ttl" { + description = "Time to live (TTL) for the record" + type = number + default = 3600 +} + +variable "dns_record_set_active" { + description = "Whether the record set is active" + type = bool + default = true +} + +variable "dns_record_set_comment" { + description = "Optional comment for the record" + type = string + default = null +} From 0fc20b8741c54aee78ed15d8a5e8b96fe82cdefb Mon Sep 17 00:00:00 2001 From: "florian.heuer" Date: Fri, 8 Aug 2025 15:56:51 +0000 Subject: [PATCH 11/34] Update dns/README.md --- dns/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dns/README.md b/dns/README.md index 9b072e8..aa775e8 100644 --- a/dns/README.md +++ b/dns/README.md @@ -1,4 +1,4 @@ -# Terraform module to create DNS zone +# Terraform module to create DNS zone or record ## Example for main.tf From fcbd49b302f2164ba6244a855023935b92e7a17a Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Fri, 22 Aug 2025 20:04:34 +0200 Subject: [PATCH 12/34] feature(iac): enable dns module to create one or many zones with none or many records --- dns/README.md | 177 ++++++++++++++++++++++++++++++++++++++--------- dns/dns.tf | 116 +++++++++++++++++++++++-------- dns/outputs.tf | 51 +++----------- dns/providers.tf | 3 +- dns/variables.tf | 164 +++++++++++-------------------------------- 5 files changed, 282 insertions(+), 229 deletions(-) diff --git a/dns/README.md b/dns/README.md index aa775e8..eb9ad15 100644 --- a/dns/README.md +++ b/dns/README.md @@ -1,47 +1,160 @@ -# Terraform module to create DNS zone or record +# Terraform STACKIT DNS Zone and Record Set Module -## Example for main.tf +This module allows you to declaratively manage DNS zones and their associated record sets in STACKIT. -```tf -locals { - stackit_project_id = "fb06b3bf-70b6-45bf-b1a4-e84708b26f92" - region = "eu01" - env = "dev" +It supports: + +- Creating one or more new DNS zones. +- Using pre-existing DNS zones by providing a `zone_id`. +- Creating one or more record sets within any managed zone. + +## Usage Example + +Here is an example of how to use the module to create one or many new zones and use another pre-existing one. + +```terraform +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.59.0" + } + } + backend "s3" { + endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" + } + bucket = "test-bucket" + key = "state/test/dns" + region = "eu01" + skip_region_validation = true + skip_metadata_api_check = true + skip_credentials_validation = true + skip_requesting_account_id = true + skip_s3_checksum = true + use_path_style = true + } +} + +provider "stackit" { + # Configure your provider credentials, i.e.: + default_region = local.region + enable_beta_resources = true + service_account_key_path = "sa_key.json" } module "dns" { - source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns" - project_id = "my-stackit-project-id" + source = "./path/to/your/module" # Or a Git URL git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - zone_name = "example-zone" - dns_name = "example.com" + zones = { + # EXAMPLE 1: Create multiple zones with multiple A records + "primary_domain" = { + name = "first domain" + dns_name = "test-b.stackit.rocks" + record_sets = { + "qa-test" = { + name = "qa-test" + type = "A" + ttl = 3600 + records = ["192.0.1.1", "192.0.1.2"] + } + "beta-test" = { + name = "beta-test" + type = "A" + ttl = 3600 + records = ["192.0.2.10", "192.0.2.20", "192.0.2.30"] + } + } + }, + "secondary_domain" = { + name = "second domain" + dns_name = "test-a.stackit.rocks" + record_sets = { + "alpha-records" = { + name = "alpha-test" + type = "A" + ttl = 3600 + records = ["192.10.2.10", "192.10.2.20", "192.10.2.30"] + } + } + }, - contact_email = "admin@example.com" - description = "Main DNS zone - managed via Terraform" - default_ttl = 3600 + # EXAMPLE 2: Use a pre-existing zone and add a new TXT and A record to it + "existing_domain" = { + zone_id = "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz" + record_sets = { + "spf_txt" = { + name = "@" + type = "TXT" + records = ["v=spf1 mx -all"] + ttl = 7200 + comment = "this is a test txt record" + }, + "spf_cname" = { + name = "exampledomain" + type = "A" + ttl = 3600 + records = ["192.0.29.1"] + } + } + }, - record_name = "www.example.com" - record_type = "A" - records = ["192.0.29.1"] - ttl = 3600 - comment = "My example records - managed by Terraform" + # EXAMPLE 3: Create a new zone with no initial records + "empty_domain" = { + name = "My Empty Domain" + dns_name = "empty-example.com" + } + } } ``` -## Usage Options +## Inputs -### Use an Existing DNS Zone +| Variable Name | Description | Type | Required | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| `project_id` | STACKIT project ID to which the DNS resources are associated. | `string` | Yes | +| `zones` | A map of DNS zones to be created or managed. Each zone contains a `name`, `dns_name`, and `record_sets` map. | `map` | Yes | +| `record_sets` | A map of DNS record sets to create within this zone. Each record set contains a `name`, `type`, `records`, `ttl`, `comment`, and `active` attribute. | `map` | Optional | -#### If you already have a DNS zone created in STACKIT, simply provide the `zone_id`: +### Values for zones -```hcl -module "dns" { - source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/dns" - project_id = "your-project-id" +| Key | Description | Type | Required | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ----------------------------------- | +| `zone_id` | The ID of an existing DNS zone to manage. If provided, the module will use the existing zone. If not provided, a new zone will be created. | `string` | Optional | +| `name` | The descriptive name of the DNS zone. | `string` | Optional | +| `dns_name` | The DNS name of the DNS zone. | `string` | Optional | +| `contact_email` | The contact email for the DNS zone. | `string` | Optional | +| `description` | A description of the DNS zone. | `string` | Optional | +| `acl` | The Access Control List (ACL) for the DNS zone. | `string` | Optional | +| `active` | Whether the DNS zone is active or not. | `bool` | Optional (currently non-functional) | +| `default_ttl` | The default Time-to-Live (TTL) for records in the DNS zone. | `number` | Optional | +| `expire_time` | The expiration time for the DNS zone. | `number` | Optional | +| `is_reverse_zone` | Whether the DNS zone is a reverse zone or not. | `bool` | Optional | +| `negative_cache` | The negative cache duration for the DNS zone. | `number` | Optional | +| `primaries` | A list of primary name servers for the DNS zone. | `list(string)` | Optional | +| `refresh_time` | The refresh time for the DNS zone. | `number` | Optional | +| `retry_time` | The retry time for the DNS zone. | `number` | Optional | +| `type` | The type of the DNS zone. Defaults to "primary" if not provided. | `string` | Optional | - zone_id = "preexisting-zone-id" - record_name = "www.example.com" - record_type = "A" - records = ["192.0.29.1"] -} -``` +### Values for record_sets + +| Key | Description | Type | Required | +| --------- | ------------------------------------------------------------- | -------------- | ----------------------------------- | +| `name` | The name of the DNS record set. | `string` | Yes | +| `type` | The type of the DNS record set. | `string` | Yes | +| `records` | A list of DNS records for the record set. | `list(string)` | Yes | +| `ttl` | The Time-to-Live (TTL) for the DNS records in the record set. | `number` | Optional | +| `comment` | A comment for the DNS record set. | `string` | Optional | +| `active` | Whether the DNS record set is active or not. | `bool` | Optional (currently non-functional) | + +## Outputs + +| Name | Description | +| ------------- | ------------------------------------------------------------------------------------------ | +| `zones` | A map of all managed DNS zone objects, including those created and those referenced by ID. | +| `record_sets` | A map of all created DNS record set objects. | + +## Notes + +Setting a zone or record to inactive by using `active = false` is currently not possible due to a bug in the provider. It is active by default. diff --git a/dns/dns.tf b/dns/dns.tf index 3101bbf..47168b6 100644 --- a/dns/dns.tf +++ b/dns/dns.tf @@ -1,34 +1,94 @@ -resource "stackit_dns_zone" "this" { - count = var.zone_id == null ? 1 : 0 - project_id = var.stackit_project_id - name = var.dns_zone_name - dns_name = var.dns_zone_dns_name +# main.tf - # Optional attributes - acl = var.dns_zone_acl - active = var.dns_zone_active - contact_email = var.dns_zone_contact_email - default_ttl = var.dns_zone_default_ttl - description = var.dns_zone_description - expire_time = var.dns_zone_expire_time - is_reverse_zone = var.dns_zone_is_reverse_zone - negative_cache = var.dns_zone_negative_cache - primaries = var.dns_zone_primaries - refresh_time = var.dns_zone_refresh_time - retry_time = var.dns_zone_retry_time - type = var.dns_zone_type +# -------------------------------------------------------------------------------------------------- +# LOCAL VARIABLES +# -------------------------------------------------------------------------------------------------- + +locals { + # Create a map of zones to be created (where zone_id is not specified) + zones_to_create = { for k, v in var.zones : k => v if try(v.zone_id, null) == null } + + # Create a map of zones to be referenced via data source (where zone_id is specified) + zones_to_read = { for k, v in var.zones : k => v if try(v.zone_id, null) != null } + + # Merge the created resources and data sources into a single, unified map. + # This allows record sets to reference a zone regardless of whether it was created or read. + all_zones = merge( + { + for k, zone in stackit_dns_zone.this : k => zone + }, + { + for k, zone in data.stackit_dns_zone.this : k => zone + } + ) + + # Flatten the nested record_sets structure into a single list, making it easy to iterate with for_each. + # Each item in the list retains a reference to its parent zone key. + flat_record_sets = flatten([ + for zone_key, zone_config in var.zones : [ + for record_key, record_config in try(zone_config.record_sets, {}) : { + zone_key = zone_key + record_key = record_key + name = record_config.name + type = record_config.type + records = record_config.records + ttl = try(record_config.ttl, null) + comment = try(record_config.comment, null) + active = try(record_config.active, null) + } + ] + ]) } +# -------------------------------------------------------------------------------------------------- +# DNS ZONE RESOURCES (CREATE OR READ) +# -------------------------------------------------------------------------------------------------- + +# Create new DNS zones for configurations that do not have a zone_id +resource "stackit_dns_zone" "this" { + for_each = local.zones_to_create + + project_id = var.project_id + name = each.value.name + dns_name = each.value.dns_name + contact_email = try(each.value.contact_email, null) + description = try(each.value.description, null) + acl = try(each.value.acl, null) + active = try(each.value.active, null) + default_ttl = try(each.value.default_ttl, null) + expire_time = try(each.value.expire_time, null) + is_reverse_zone = try(each.value.is_reverse_zone, null) + negative_cache = try(each.value.negative_cache, null) + primaries = try(each.value.primaries, null) + refresh_time = try(each.value.refresh_time, null) + retry_time = try(each.value.retry_time, null) + type = try(each.value.type, "primary") +} + +# Read existing DNS zones for configurations that provide a zone_id +data "stackit_dns_zone" "this" { + for_each = local.zones_to_read + + project_id = var.project_id + zone_id = each.value.zone_id +} + +# -------------------------------------------------------------------------------------------------- +# DNS RECORD SET RESOURCES +# -------------------------------------------------------------------------------------------------- + resource "stackit_dns_record_set" "this" { - project_id = var.stackit_project_id - name = var.dns_record_set_name - records = var.dns_record_set_records - type = var.dns_record_set_type - #zone_id = var.dns_record_set_zone_id - zone_id = var.zone_id != null ? var.zone_id : stackit_dns_zone.this[0].zone_id + # The key is a unique combination of the zone and record keys for a stable address. + for_each = { for record in local.flat_record_sets : "${record.zone_key}.${record.record_key}" => record } - # Optional - active = var.dns_record_set_active - comment = var.dns_record_set_comment - ttl = var.dns_record_set_ttl + project_id = var.project_id + # Look up the correct zone_id from the unified 'all_zones' map + zone_id = local.all_zones[each.value.zone_key].zone_id + + name = each.value.name + type = each.value.type + records = each.value.records + ttl = each.value.ttl + comment = each.value.comment + active = each.value.active } diff --git a/dns/outputs.tf b/dns/outputs.tf index 947a9d3..3352a17 100644 --- a/dns/outputs.tf +++ b/dns/outputs.tf @@ -1,46 +1,11 @@ -# === DNS Zone Outputs === -output "zone_id" { - value = var.zone_id != null ? var.zone_id : stackit_dns_zone.this[0].zone_id - description = "The DNS zone ID used or created" +# outputs.tf + +output "zones" { + description = "A map of all managed DNS zone objects, including those created and those referenced by ID." + value = local.all_zones } -output "zone_primary_name_server" { - value = stackit_dns_zone.this.primary_name_server - description = "Primary name server" -} - -output "zone_state" { - value = stackit_dns_zone.this.state - description = "State of the DNS zone" -} - -output "zone_visibility" { - value = stackit_dns_zone.this.visibility - description = "Zone visibility" -} - -output "zone_record_count" { - value = stackit_dns_zone.this.record_count - description = "Number of DNS records in the zone" -} - -# === DNS Record Set Outputs === -output "record_set_fqdn" { - value = stackit_dns_record_set.this.fqdn - description = "Fully qualified domain name of the record" -} - -output "record_set_id" { - value = stackit_dns_record_set.this.record_set_id - description = "ID of the record set" -} - -output "record_set_state" { - value = stackit_dns_record_set.this.state - description = "State of the record set" -} - -output "record_set_error" { - value = stackit_dns_record_set.this.error - description = "Error during record creation (if any)" +output "record_sets" { + description = "A map of all created DNS record set objects." + value = stackit_dns_record_set.this } diff --git a/dns/providers.tf b/dns/providers.tf index 232c583..96e3e65 100644 --- a/dns/providers.tf +++ b/dns/providers.tf @@ -2,8 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.59.0" - + version = "~> 0.61.0" } } } diff --git a/dns/variables.tf b/dns/variables.tf index f254546..7fc7b55 100644 --- a/dns/variables.tf +++ b/dns/variables.tf @@ -1,132 +1,48 @@ -variable "stackit_project_id" { - description = "STACKIT project ID to which the dns zone is associated." +# variables.tf + +variable "project_id" { type = string + description = "STACKIT project ID to which the DNS resources are associated." } -variable "zone_id" { - description = "ID of an existing DNS zone. If provided, no new zone will be created." - type = string - default = null -} +variable "zones" { + type = map(object({ + # If zone_id is provided, the module will use the existing zone. + # Otherwise, a new zone will be created using the attributes below. + zone_id = optional(string) -# === DNS Zone variables === -variable "dns_zone_name" { - description = "Descriptive name of the DNS zone." - type = string -} + # Required attributes for new zones + name = optional(string) + dns_name = optional(string) -variable "dns_zone_dns_name" { - description = "The actual zone name, e.g. example.com" - type = string -} + # Optional attributes for new zones + contact_email = optional(string) + description = optional(string) + acl = optional(string) + active = optional(bool) + default_ttl = optional(number) + expire_time = optional(number) + is_reverse_zone = optional(bool) + negative_cache = optional(number) + primaries = optional(list(string)) + refresh_time = optional(number) + retry_time = optional(number) + type = optional(string, "primary") -variable "dns_zone_acl" { - description = "Access control list, e.g. 0.0.0.0/0,::/0" - type = string - default = null -} + # A map of DNS record sets to create within this zone. + # The key is a logical name for the record set (e.g., "www", "mx_record"). + record_sets = optional(map(object({ + # Required record set attributes + name = string + type = string + records = list(string) -variable "dns_zone_active" { - description = "Whether the zone is active. Defaults to true" - type = bool - default = true -} - -variable "dns_zone_contact_email" { - description = "Contact email for the zone." - type = string - default = null -} - -variable "dns_zone_default_ttl" { - description = "The default value of the TTL for new resource records in the DNS zone. Time in seconds. Defaults to 3600" - type = number - default = 3600 -} - -variable "dns_zone_description" { - description = "An additional descriptive free text for the DNS zone." - type = string - default = null -} - -variable "dns_zone_expire_time" { - description = "If the secondary DNS server cannot perform a serial check in this interval, it must assume that its copy of the zone is obsolete and discard it. Time in seconds. Defaults to 1209600" - type = number - default = 1209600 -} - -variable "dns_zone_is_reverse_zone" { - description = "Indicates whether a zone is a reverse DNS zone or a forward zone. Both have a different set of allowed resource record types. Defaults to false" - type = bool - default = false -} - -variable "dns_zone_negative_cache" { - description = "The response about the non-existence of a resource record is cached for this interval. Time in seconds. Defaults to 60" - type = number - default = 60 -} - -variable "dns_zone_primaries" { - description = "List of primary nameservers (for secondary zone)" - type = list(string) - default = [] -} - -variable "dns_zone_refresh_time" { - description = "This is the amount of time a secondary DNS server will wait before checking with the primary for a new serial if a zone refresh fails. Time in seconds. Defaults to 3600" - type = number - default = 3600 -} - -variable "dns_zone_retry_time" { - description = "This is the amount of time a secondary DNS server waits before retrying a zone update if the update fails. Time in seconds. Defaults to 600" - type = number - default = 600 -} - -variable "dns_zone_type" { - description = "Zone type (primary or secondary). Defaults to primary" - type = string - default = "primary" -} - -# === DNS Record Set variables === -variable "dns_record_set_name" { - description = "Name of the DNS record (e.g. 'www.example.com')" - type = string -} - -variable "dns_record_set_records" { - description = "List of DNS record values" - type = list(string) -} - -variable "dns_record_set_type" { - description = "Record type (e.g. 'A', 'CNAME', etc.)" - type = string -} - -variable "dns_record_set_zone_id" { - description = "The zone ID for the DNS record" - type = string -} - -variable "dns_record_set_ttl" { - description = "Time to live (TTL) for the record" - type = number - default = 3600 -} - -variable "dns_record_set_active" { - description = "Whether the record set is active" - type = bool - default = true -} - -variable "dns_record_set_comment" { - description = "Optional comment for the record" - type = string - default = null + # Optional record set attributes + ttl = optional(number) + comment = optional(string) + active = optional(bool, true) + })), {}) + })) + description = "A map of DNS zones to manage. The key is a logical name for the zone." + default = {} } From 61761c438a7bf188565fe055aefbf0258964ba48 Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Fri, 8 Aug 2025 18:00:08 +0200 Subject: [PATCH 13/34] feature(iac): add service-account module --- service-account/README.md | 26 +++++++++++++++ service-account/outputs.tf | 20 ++++++++++++ service-account/providers.tf | 8 +++++ service-account/service-account.tf | 22 +++++++++++++ service-account/variables.tf | 52 ++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 service-account/README.md create mode 100644 service-account/outputs.tf create mode 100644 service-account/providers.tf create mode 100644 service-account/service-account.tf create mode 100644 service-account/variables.tf diff --git a/service-account/README.md b/service-account/README.md new file mode 100644 index 0000000..d26d450 --- /dev/null +++ b/service-account/README.md @@ -0,0 +1,26 @@ +# Terraform module to create STACKIT Service Account + +## Example for main.tf + +# Service Account Terraform Module + +This module creates a STACKIT service account, optionally creates a key, and optionally attaches it to a server. + +## Usage + +```hcl +module "service_account" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/service-account" + name = "my-service-account" + project_id = "your-project-id" + + create_key = true + ttl_days = 90 + rotate_when_changed = { + rotated_at = timestamp() + } + + attach_to_server = true + server_id = "your-server-id" +} +``` diff --git a/service-account/outputs.tf b/service-account/outputs.tf new file mode 100644 index 0000000..3587015 --- /dev/null +++ b/service-account/outputs.tf @@ -0,0 +1,20 @@ +output "service_account_email" { + description = "The email of the service account" + value = stackit_service_account.this.email +} + +output "service_account_id" { + description = "Internal ID of the service account" + value = stackit_service_account.this.id +} + +output "service_account_key_id" { + description = "ID of the created key" + value = try(stackit_service_account_key.this[0].key_id, null) +} + +output "service_account_key_json" { + description = "Sensitive JSON key output" + value = try(stackit_service_account_key.this[0].json, null) + sensitive = true +} diff --git a/service-account/providers.tf b/service-account/providers.tf new file mode 100644 index 0000000..28dfab1 --- /dev/null +++ b/service-account/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.59.0" + } + } +} diff --git a/service-account/service-account.tf b/service-account/service-account.tf new file mode 100644 index 0000000..c7fdbeb --- /dev/null +++ b/service-account/service-account.tf @@ -0,0 +1,22 @@ +resource "stackit_service_account" "this" { + name = var.service_account_name + project_id = var.stackit_project_id +} + +resource "stackit_service_account_key" "this" { + count = var.service_account_create_key ? 1 : 0 + + project_id = var.stackit_project_id + service_account_email = stackit_service_account.this.email + public_key = var.service_account_public_key + rotate_when_changed = var.service_account_rotate_when_changed + ttl_days = var.service_account_ttl_days +} + +resource "stackit_service_account_attachment" "this" { + count = var.attach_to_server ? 1 : 0 + + project_id = var.stackit_project_id + server_id = var.server_id + service_account_email = stackit_service_account.this.email +} diff --git a/service-account/variables.tf b/service-account/variables.tf new file mode 100644 index 0000000..1ff754c --- /dev/null +++ b/service-account/variables.tf @@ -0,0 +1,52 @@ +variable "stackit_project_id" { + description = "STACKIT project ID" + type = string +} + +# === Service Account variables === + + +variable "service_account_name" { + description = "Name of the service account" + type = string +} + +# === Service Account Key variables === + +variable "service_account_create_key" { + description = "Whether to create a service account key" + type = bool + default = false +} + +variable "service_account_public_key" { + description = "Optional: Specifies the public_key (RSA2048 key-pair). If not provided, a certificate from STACKIT will be used to generate a private_key." + type = string + default = null +} + +variable "service_account_rotate_when_changed" { + description = "Map to force key rotation when changed" + type = map(string) + default = {} +} + +variable "service_account_ttl_days" { + description = "Key validity duration in days. Defaults to 90" + type = number + default = 90 +} + +# === Server Service Account Attach variables === + +variable "attach_to_server" { + description = "Whether to attach the service account to a server" + type = bool + default = false +} + +variable "server_id" { + description = "Server ID for attachment" + type = string + default = "" +} From 6446a9d123ae5a401c382d486b4548469f96d6ac Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Fri, 22 Aug 2025 22:27:03 +0200 Subject: [PATCH 14/34] feature(iac): fix service-account module --- service-account/outputs.tf | 5 +++++ service-account/service-account.tf | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/service-account/outputs.tf b/service-account/outputs.tf index 3587015..753c387 100644 --- a/service-account/outputs.tf +++ b/service-account/outputs.tf @@ -1,3 +1,8 @@ +output "service_account_name" { + description = "The name of the service account" + value = var.service_account_name +} + output "service_account_email" { description = "The email of the service account" value = stackit_service_account.this.email diff --git a/service-account/service-account.tf b/service-account/service-account.tf index c7fdbeb..7bbcc0f 100644 --- a/service-account/service-account.tf +++ b/service-account/service-account.tf @@ -3,6 +3,10 @@ resource "stackit_service_account" "this" { project_id = var.stackit_project_id } +resource "time_rotating" "this" { + rotation_days = 3 +} + resource "stackit_service_account_key" "this" { count = var.service_account_create_key ? 1 : 0 @@ -13,7 +17,7 @@ resource "stackit_service_account_key" "this" { ttl_days = var.service_account_ttl_days } -resource "stackit_service_account_attachment" "this" { +resource "stackit_server_service_account_attach" "this" { count = var.attach_to_server ? 1 : 0 project_id = var.stackit_project_id From 2bf17ea21ed10a7fee93a80b2b5dc63fa8c52e8b Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Fri, 22 Aug 2025 22:27:23 +0200 Subject: [PATCH 15/34] feature(iac): improve readme --- service-account/README.md | 67 +++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/service-account/README.md b/service-account/README.md index d26d450..5e0b070 100644 --- a/service-account/README.md +++ b/service-account/README.md @@ -1,26 +1,59 @@ -# Terraform module to create STACKIT Service Account +# Terraform Module: STACKIT Service Account -## Example for main.tf +This module is designed to create a STACKIT service account, optionally generate a key, and optionally attach it to a server. It is useful for managing service accounts and their associated keys in a secure and repeatable manner. -# Service Account Terraform Module +The purpose of this module is to simplify the creation and management of service accounts in STACKIT, while providing flexibility to generate keys and attach them to servers. It also allows for secure storage of keys using a secrets manager. -This module creates a STACKIT service account, optionally creates a key, and optionally attaches it to a server. +## Example Usage -## Usage +```terraform +module "service-account" { + source = "./service-account" # Or a Git URL "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/service-account" + stackit_project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + service_account_name = "my-service-account" + service_account_create_key = true +} -```hcl -module "service_account" { - source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/service-account" - name = "my-service-account" - project_id = "your-project-id" +# Save json created to secrets manager +variable "secret_manager_username" { + description = "username of the secrets manger to store credentials" + type = string + sensitive = true +} - create_key = true - ttl_days = 90 - rotate_when_changed = { - rotated_at = timestamp() +variable "secret_manager_password" { + description = "password of the secrets manger to store credentials" + type = string + sensitive = true +} + +module "service_account_key" { + source = "./create-secret" # Or a Git URL "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public//terraform-modules/create-secret" + secret_manager_instance_id = local.secret_manager_instance_id + secret_manager_username = var.secret_manager_username + secret_manager_password = var.secret_manager_password + secrets_path = "service-accounts/${module.service-account.service_account_name}" + secret_data = { + key_json = module.service-account.service_account_key_json } - - attach_to_server = true - server_id = "your-server-id" } ``` + +## Inputs + +| Key | Description | Type | Required | Default | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | -------- | ------- | +| `service_account_name` | Name of the service account | `string` | yes | +| `service_account_create_key` | Whether to create a service account key | `bool` | no | `false` | +| `service_account_public_key` | Optional: Specifies the public_key (RSA2048 key-pair). If not provided, a certificate from STACKIT will be used to generate a private_key. | `string` | no | `null` | +| `service_account_rotate_when_changed` | Map to force key rotation when changed | `map(string)` | no | `{}` | +| `service_account_ttl_days` | Key validity duration in days. Defaults to 90 | `number` | no | `90` | +| `attach_to_server` | Whether to attach the service account to a server | `bool` | no | `false` | +| `server_id` | Server ID for attachment | `string` | no | `""` | + +## Notes + +- When creating a key, it is recommended to save it securely using a secrets manager. In the example usage we illustrated how to do that using the `create-secret` module. +- The module does not handle key rotation automatically. You can use the service_account_rotate_when_changed input to force key rotation when certain attributes change. +- The module does not handle server attachment automatically. You can use the attach_to_server and server_id inputs to attach the service account to a server. +- The module does not handle deletion of service accounts or keys. It is recommended to manage these resources using appropriate Terraform lifecycle configurations or external tools. From ae40c80195641fcd1e4c8fae056adc1c3a17d0ea Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Tue, 26 Aug 2025 18:33:19 +0200 Subject: [PATCH 16/34] STACKITCIN-311 Adjust Grafana TF module to allow Postgres datasources --- grafana/datasource/datasource.tf | 107 ++++++++++++++++++++----------- grafana/datasource/variables.tf | 44 ++++++------- 2 files changed, 91 insertions(+), 60 deletions(-) diff --git a/grafana/datasource/datasource.tf b/grafana/datasource/datasource.tf index a2eec52..2fc1eab 100644 --- a/grafana/datasource/datasource.tf +++ b/grafana/datasource/datasource.tf @@ -1,52 +1,81 @@ -# main.tf - -# Step 1: Create all Grafana data sources with their basic configuration. -# This resource establishes the fundamental properties of each data source. +# Step 1: Create the basic "shell" of each datasource. resource "grafana_data_source" "this" { for_each = var.datasources - name = each.key - type = each.value.type - url = var.datasource_urls[each.value.url_key] + name = each.key + type = each.value.type + url = var.datasource_urls[each.value.url_key] is_default = coalesce(each.value.is_default, false) - basic_auth_enabled = true - basic_auth_username = var.datasource_users[each.value.user_key] + # For HTTP Basic Auth (Loki, Prometheus, etc.) + # FIX: Changed 'user_key' to 'basic_auth_user_key' to match your variables.tf + basic_auth_enabled = each.value.basic_auth_user_key != null + basic_auth_username = each.value.basic_auth_user_key != null ? var.datasource_users[each.value.basic_auth_user_key] : null - secure_json_data_encoded = jsonencode({ - basicAuthPassword = var.datasource_passwords[each.value.pass_key] - }) + # For database usernames (like Postgres) + # This sets the username initially. + username = each.value.db_user_key != null ? var.datasource_users[each.value.db_user_key] : null - # Encodes initial, non-dependent JSON data. - # Configurations that depend on other datasource UIDs will be handled separately. - json_data_encoded = each.value.json_data != null ? jsonencode(each.value.json_data) : null - - # ignore changes made by the _config resource + # CRITICAL FIX: This resource must ignore attributes that are + # managed by the other 'config' resources below. lifecycle { ignore_changes = [ json_data_encoded, + secure_json_data_encoded, + # Also ignore username, as it can be managed/reported back differently by the API. + username, ] } } -# Step 2: Apply Loki-specific 'derivedFields' configuration. -# This resource targets Loki data sources that need to link to another data source (like Tempo). -# It runs after the initial data sources are created to resolve the UIDs. -resource "grafana_data_source_config" "loki_derived_fields" { - # Filter for datasources that are of type 'loki' and have 'derived_fields' defined. +# Step 2: Apply the main json_data for datasources like PostgreSQL. +resource "grafana_data_source_config" "json_data_main" { for_each = { for k, v in var.datasources : k => v - if v.type == "loki" && v.derived_fields != null + if v.json_data != null && v.derived_fields == null && v.traces_to_logs == null } - # The UID of the Loki data source to configure. uid = grafana_data_source.this[each.key].uid - # Construct the json_data with the derivedFields. + json_data_encoded = jsonencode(each.value.json_data) + + # This config must ignore the password, which is managed by the 'passwords' resource. + lifecycle { + ignore_changes = [secure_json_data_encoded] + } +} + +# Step 3: Apply passwords to all datasources that require one. +resource "grafana_data_source_config" "passwords" { + for_each = { + for k, v in var.datasources : k => v if v.pass_key != null + } + + uid = grafana_data_source.this[each.key].uid + + secure_json_data_encoded = jsonencode( + each.value.type == "grafana-postgresql-datasource" ? { + password = var.datasource_passwords[each.value.pass_key] + } : { + basicAuthPassword = var.datasource_passwords[each.value.pass_key] + } + ) + + # This config must ignore the main json_data, which is managed elsewhere. + lifecycle { + ignore_changes = [json_data_encoded] + } +} + +# Step 4: Apply Loki-specific 'derivedFields' configuration. +resource "grafana_data_source_config" "loki_derived_fields" { + for_each = { + for k, v in var.datasources : k => v if v.type == "loki" && v.derived_fields != null + } + uid = grafana_data_source.this[each.key].uid json_data_encoded = jsonencode({ derivedFields = [ for field in each.value.derived_fields : { - # The UID of the target data source (e.g., Tempo). datasourceUid = grafana_data_source.this[field.target_datasource_name].uid matcherRegex = field.matcher_regex name = field.name @@ -54,24 +83,21 @@ resource "grafana_data_source_config" "loki_derived_fields" { } ] }) + + # This config must ignore the password, which is managed elsewhere. + lifecycle { + ignore_changes = [secure_json_data_encoded] + } } -# Step 3: Apply Tempo-specific 'tracesToLogsV2' configuration. -# This resource targets Tempo data sources that need to link back to a logging data source (like Loki). +# Step 5: Apply Tempo-specific 'tracesToLogsV2' configuration. resource "grafana_data_source_config" "tempo_traces_to_logs" { - # Filter for datasources that are of type 'tempo' and have 'traces_to_logs' defined. for_each = { - for k, v in var.datasources : k => v - if v.type == "tempo" && v.traces_to_logs != null + for k, v in var.datasources : k => v if v.type == "tempo" && v.traces_to_logs != null } - - # The UID of the Tempo data source to configure. - uid = grafana_data_source.this[each.key].uid - - # Construct the json_data with the tracesToLogsV2 settings. + uid = grafana_data_source.this[each.key].uid json_data_encoded = jsonencode({ tracesToLogsV2 = { - # The UID of the target data source (e.g., Loki). datasourceUid = grafana_data_source.this[each.value.traces_to_logs.target_datasource_name].uid query = each.value.traces_to_logs.query customQuery = coalesce(each.value.traces_to_logs.custom_query, true) @@ -81,4 +107,9 @@ resource "grafana_data_source_config" "tempo_traces_to_logs" { spanEndTimeShift = each.value.traces_to_logs.span_end_time_shift } }) -} + + # This config must ignore the password, which is managed elsewhere. + lifecycle { + ignore_changes = [secure_json_data_encoded] + } +} \ No newline at end of file diff --git a/grafana/datasource/variables.tf b/grafana/datasource/variables.tf index cbf19db..94ebfa8 100644 --- a/grafana/datasource/variables.tf +++ b/grafana/datasource/variables.tf @@ -1,32 +1,33 @@ -# variables.tf - -# Define datasources (non-sensitive metadata only) variable "datasources" { description = < Date: Tue, 26 Aug 2025 18:38:36 +0200 Subject: [PATCH 17/34] Fixes --- grafana/README.md | 18 +++++++++--------- grafana/datasource/datasource.tf | 13 ++++++------- grafana/datasource/variables.tf | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/grafana/README.md b/grafana/README.md index 856b2d9..d0482a4 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -83,17 +83,17 @@ module "datasource" { datasources = { Thanos-Common-Infra-PRD = { - type = "prometheus" - url_key = "thanos_coin_prd" - user_key = "thanos_coin_prd" - pass_key = "thanos_coin_prd" - is_default = true + type = "prometheus" + url_key = "thanos_coin_prd" + basic_auth_user_key = "thanos_coin_prd" + pass_key = "thanos_coin_prd" + is_default = true } Loki-Common-Infra-PRD = { - type = "loki" - url_key = "loki_coin_prd" - user_key = "loki_coin_prd" - pass_key = "loki_coin_prd" + type = "loki" + url_key = "loki_coin_prd" + basic_auth_user_key = "loki_coin_prd" + pass_key = "loki_coin_prd" } } diff --git a/grafana/datasource/datasource.tf b/grafana/datasource/datasource.tf index 2fc1eab..34a22fe 100644 --- a/grafana/datasource/datasource.tf +++ b/grafana/datasource/datasource.tf @@ -1,4 +1,4 @@ -# Step 1: Create the basic "shell" of each datasource. +# Create the basic "shell" of each datasource. resource "grafana_data_source" "this" { for_each = var.datasources @@ -8,7 +8,6 @@ resource "grafana_data_source" "this" { is_default = coalesce(each.value.is_default, false) # For HTTP Basic Auth (Loki, Prometheus, etc.) - # FIX: Changed 'user_key' to 'basic_auth_user_key' to match your variables.tf basic_auth_enabled = each.value.basic_auth_user_key != null basic_auth_username = each.value.basic_auth_user_key != null ? var.datasource_users[each.value.basic_auth_user_key] : null @@ -16,7 +15,7 @@ resource "grafana_data_source" "this" { # This sets the username initially. username = each.value.db_user_key != null ? var.datasource_users[each.value.db_user_key] : null - # CRITICAL FIX: This resource must ignore attributes that are + # This resource must ignore attributes that are # managed by the other 'config' resources below. lifecycle { ignore_changes = [ @@ -28,7 +27,7 @@ resource "grafana_data_source" "this" { } } -# Step 2: Apply the main json_data for datasources like PostgreSQL. +# Apply the main json_data for datasources like PostgreSQL. resource "grafana_data_source_config" "json_data_main" { for_each = { for k, v in var.datasources : k => v @@ -45,7 +44,7 @@ resource "grafana_data_source_config" "json_data_main" { } } -# Step 3: Apply passwords to all datasources that require one. +# Apply passwords to all datasources that require one. resource "grafana_data_source_config" "passwords" { for_each = { for k, v in var.datasources : k => v if v.pass_key != null @@ -67,7 +66,7 @@ resource "grafana_data_source_config" "passwords" { } } -# Step 4: Apply Loki-specific 'derivedFields' configuration. +# Apply Loki-specific 'derivedFields' configuration. resource "grafana_data_source_config" "loki_derived_fields" { for_each = { for k, v in var.datasources : k => v if v.type == "loki" && v.derived_fields != null @@ -90,7 +89,7 @@ resource "grafana_data_source_config" "loki_derived_fields" { } } -# Step 5: Apply Tempo-specific 'tracesToLogsV2' configuration. +# Apply Tempo-specific 'tracesToLogsV2' configuration. resource "grafana_data_source_config" "tempo_traces_to_logs" { for_each = { for k, v in var.datasources : k => v if v.type == "tempo" && v.traces_to_logs != null diff --git a/grafana/datasource/variables.tf b/grafana/datasource/variables.tf index 94ebfa8..b505e8d 100644 --- a/grafana/datasource/variables.tf +++ b/grafana/datasource/variables.tf @@ -15,7 +15,7 @@ EOT # Key to look up a basic auth username basic_auth_user_key = optional(string) - # ADDED BACK: Non-sensitive JSON data for Postgres, etc. + # Non-sensitive JSON data for Postgres, etc. json_data = optional(map(any)) # Linking Attributes (for Loki/Tempo) From d5a0c28c11b1379833acdb7520cc7c4dc9bb4d45 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Wed, 27 Aug 2025 09:44:37 +0200 Subject: [PATCH 18/34] format --- grafana/datasource/datasource.tf | 4 ++-- grafana/datasource/variables.tf | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/grafana/datasource/datasource.tf b/grafana/datasource/datasource.tf index 34a22fe..dd31ba8 100644 --- a/grafana/datasource/datasource.tf +++ b/grafana/datasource/datasource.tf @@ -71,7 +71,7 @@ resource "grafana_data_source_config" "loki_derived_fields" { for_each = { for k, v in var.datasources : k => v if v.type == "loki" && v.derived_fields != null } - uid = grafana_data_source.this[each.key].uid + uid = grafana_data_source.this[each.key].uid json_data_encoded = jsonencode({ derivedFields = [ for field in each.value.derived_fields : { @@ -94,7 +94,7 @@ resource "grafana_data_source_config" "tempo_traces_to_logs" { for_each = { for k, v in var.datasources : k => v if v.type == "tempo" && v.traces_to_logs != null } - uid = grafana_data_source.this[each.key].uid + uid = grafana_data_source.this[each.key].uid json_data_encoded = jsonencode({ tracesToLogsV2 = { datasourceUid = grafana_data_source.this[each.value.traces_to_logs.target_datasource_name].uid diff --git a/grafana/datasource/variables.tf b/grafana/datasource/variables.tf index b505e8d..a349c98 100644 --- a/grafana/datasource/variables.tf +++ b/grafana/datasource/variables.tf @@ -5,18 +5,18 @@ Each datasource specifies type, keys to lookup URL/user/password, and optional configurations for linking data sources. EOT type = map(object({ - type = string - url_key = string - pass_key = optional(string) - is_default = optional(bool) + type = string + url_key = string + pass_key = optional(string) + is_default = optional(bool) # Key to look up a database username - db_user_key = optional(string) + db_user_key = optional(string) # Key to look up a basic auth username - basic_auth_user_key = optional(string) + basic_auth_user_key = optional(string) # Non-sensitive JSON data for Postgres, etc. - json_data = optional(map(any)) + json_data = optional(map(any)) # Linking Attributes (for Loki/Tempo) derived_fields = optional(list(object({ From 9604dfb6f4a21d410494d902f192b46e2948c588 Mon Sep 17 00:00:00 2001 From: Michael Messmer Date: Tue, 2 Sep 2025 16:56:31 +0200 Subject: [PATCH 19/34] add k8s min version --- ske-cluster/README.md | 1 + ske-cluster/providers.tf | 2 +- ske-cluster/ske-cluster.tf | 8 +------- ske-cluster/variables.tf | 9 ++++----- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/ske-cluster/README.md b/ske-cluster/README.md index 1f8c5a1..46c065b 100644 --- a/ske-cluster/README.md +++ b/ske-cluster/README.md @@ -9,6 +9,7 @@ module "ske-cluster" { source = "git::https://stackit-hackathon-2025.git.qa.onstackit.cloud/commerce-platform/hackdays-common-infra-poc//terraform/modules/ske-cluster" stackit_project_id = local.stackit_project_id ske_cluster_name = "example-cluster" + ske_k8s_version_min = "1.32.7" ske_node_pools = [ { name = "example-pool" diff --git a/ske-cluster/providers.tf b/ske-cluster/providers.tf index 4d06b01..c3406cd 100644 --- a/ske-cluster/providers.tf +++ b/ske-cluster/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = ">= 0.50.0" + version = "~> 0.62.0" } } } diff --git a/ske-cluster/ske-cluster.tf b/ske-cluster/ske-cluster.tf index 7ba0827..a8ecc50 100644 --- a/ske-cluster/ske-cluster.tf +++ b/ske-cluster/ske-cluster.tf @@ -4,13 +4,7 @@ resource "stackit_ske_cluster" "this" { name = var.ske_cluster_name maintenance = var.ske_maintenance node_pools = var.ske_node_pools - #] - #extensions = { - # argus = { - # enabled = true - # argus_instance_id = var.observability-instance-id - # } - #} + kubernetes_version_min = var.ske_k8s_version_min } // Kubeconfig diff --git a/ske-cluster/variables.tf b/ske-cluster/variables.tf index 391681d..7836271 100644 --- a/ske-cluster/variables.tf +++ b/ske-cluster/variables.tf @@ -29,8 +29,7 @@ variable "ske_maintenance" { }) } -#variable "observability-instance-id" { -# description = "instance id of the observability instance for cluster monitoring" -# type = string -# -#} \ No newline at end of file +variable "ske_k8s_version_min" { + description = "minimum Kubernetes version" + type = string +} \ No newline at end of file From a6c34ee9bcb01ef4cf23c24901fb5207741598a0 Mon Sep 17 00:00:00 2001 From: Michael Messmer Date: Tue, 2 Sep 2025 18:08:11 +0200 Subject: [PATCH 20/34] add node os version --- ske-cluster/README.md | 1 + ske-cluster/variables.tf | 1 + 2 files changed, 2 insertions(+) diff --git a/ske-cluster/README.md b/ske-cluster/README.md index 46c065b..7c0f0ff 100644 --- a/ske-cluster/README.md +++ b/ske-cluster/README.md @@ -16,6 +16,7 @@ module "ske-cluster" { machine_type = "c1.2" minimum = "2" maximum = "3" + os_version_min = "4230.2.0" availability_zones = ["eu01-3"] } ] diff --git a/ske-cluster/variables.tf b/ske-cluster/variables.tf index 7836271..1db1f4c 100644 --- a/ske-cluster/variables.tf +++ b/ske-cluster/variables.tf @@ -15,6 +15,7 @@ variable "ske_node_pools" { machine_type = string minimum = number maximum = number + os_version_min = string availability_zones = list(string) })) } From 9539b43f7d3bcc9bd74285bf2d45d46b705cf47f Mon Sep 17 00:00:00 2001 From: Michael Messmer Date: Mon, 8 Sep 2025 09:30:45 +0200 Subject: [PATCH 21/34] dafine list for user and db in postgbres module --- postgres/postgres.tf | 18 ++++++++++++------ postgres/variables.tf | 28 ++++++++-------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/postgres/postgres.tf b/postgres/postgres.tf index 22b1cd0..ccdda26 100644 --- a/postgres/postgres.tf +++ b/postgres/postgres.tf @@ -12,18 +12,24 @@ resource "stackit_postgresflex_instance" "this" { // Postgres User resource "stackit_postgresflex_user" "this" { + for_each = { + for db in var.postgres_databases : db.user_name => db + } depends_on = [ stackit_postgresflex_instance.this ] project_id = var.stackit_project_id instance_id = stackit_postgresflex_instance.this.instance_id - username = var.postgres_db_user_name - roles = var.postgres_db_user_roles + username = each.value.user_name + roles = each.value.user_roles } // Postgres Database resource "stackit_postgresflex_database" "this" { - depends_on = [ stackit_postgresflex_user.this ] + for_each = { + for db in var.postgres_databases : db.db_name => db + } + depends_on = [stackit_postgresflex_user.this] project_id = var.stackit_project_id instance_id = stackit_postgresflex_instance.this.instance_id - name = var.postgres_db_name - owner = var.postgres_db_user_name -} \ No newline at end of file + name = each.value.db_name + owner = each.value.user_name +} diff --git a/postgres/variables.tf b/postgres/variables.tf index b29923d..886fd59 100644 --- a/postgres/variables.tf +++ b/postgres/variables.tf @@ -10,11 +10,6 @@ variable "postgres_instance_name" { type = string } -# variable "postegres_instance_id" { -# description = "postgres instance id" -# type = string -# } - variable "postgres_instance_replicas" { description = "number of replicas for postgres instance" type = number @@ -58,19 +53,12 @@ variable "postgres_instance_region" { type = string } -# Postgres User Configs -variable "postgres_db_user_name" { - description = "username and owner for postgres db" - type = string -} - -variable "postgres_db_user_roles" { - description = "List of database access levels for the user. Supported values are: login, createdb." - type = list(string) -} - -# Postgres Database Configs -variable "postgres_db_name" { - description = "db name inside the instance" - type = string +# Postgres User and DB Configs +variable "postgres_databases" { + description = "list of users and databases" + type = list(object({ + db_name = string # db name inside the instance + user_name = string # username and owner for postgres db + user_roles = list(string) # List of database access levels for the user. Supported values are: login, createdb. + })) } \ No newline at end of file From d928465802bdcc5c04f6ddb6ead3f8305a18cefa Mon Sep 17 00:00:00 2001 From: Michael Messmer Date: Mon, 8 Sep 2025 14:03:42 +0200 Subject: [PATCH 22/34] add credential handling for multiple user/dbs --- postgres/outputs.tf | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/postgres/outputs.tf b/postgres/outputs.tf index 7e650ed..771bd13 100644 --- a/postgres/outputs.tf +++ b/postgres/outputs.tf @@ -39,3 +39,17 @@ output "postgres_user_id" { value = stackit_postgresflex_user.this.user_id } +output "postgres_credentials" { + value = { + for k, u in stackit_postgresflex_user.this : + k => { + host = u.host + username = u.username + password = u.password + port = u.port + db_name = stackit_postgresflex_database.this[u.owner].name + uri = u.uri + } + } + sensitive = true +} \ No newline at end of file From 09bff53f309b20ed085e6b09ba3ba07d7551307c Mon Sep 17 00:00:00 2001 From: Michael Messmer Date: Mon, 8 Sep 2025 14:27:49 +0200 Subject: [PATCH 23/34] test output --- postgres/outputs.tf | 60 ++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/postgres/outputs.tf b/postgres/outputs.tf index 771bd13..7e8f64f 100644 --- a/postgres/outputs.tf +++ b/postgres/outputs.tf @@ -1,43 +1,43 @@ # Postgres Instance Output -output "postgres_instance_id" { - value = stackit_postgresflex_instance.this.instance_id -} +# output "postgres_instance_id" { +# value = stackit_postgresflex_instance.this.instance_id +# } - # Postgres Database Output - output "postgres_database_id" { - value = stackit_postgresflex_database.this.database_id - } +# Postgres Database Output +# output "postgres_database_id" { +# value = stackit_postgresflex_database.this.database_id +# } # Postgres User Output -output "postgres_host" { - value = stackit_postgresflex_user.this.host -} +# output "postgres_host" { +# value = stackit_postgresflex_user.this.host +# } -output "postgres_password" { - value = stackit_postgresflex_user.this.password - sensitive = true -} +# output "postgres_password" { +# value = stackit_postgresflex_user.this.password +# sensitive = true +# } -output "postgres_user" { - value = stackit_postgresflex_user.this.username -} +# output "postgres_user" { +# value = stackit_postgresflex_user.this.username +# } -output "postgres_port" { - value = stackit_postgresflex_user.this.port -} +# output "postgres_port" { +# value = stackit_postgresflex_user.this.port +# } -output "postgres_db_name" { - value = stackit_postgresflex_database.this.name -} +# output "postgres_db_name" { +# value = stackit_postgresflex_database.this.name +# } -output "postgres_uri" { - value = stackit_postgresflex_user.this.uri - sensitive = true -} +# output "postgres_uri" { +# value = stackit_postgresflex_user.this.uri +# sensitive = true +# } -output "postgres_user_id" { - value = stackit_postgresflex_user.this.user_id -} +# output "postgres_user_id" { +# value = stackit_postgresflex_user.this.user_id +# } output "postgres_credentials" { value = { From 3eed77d451a4140c7ec91df0db3fa583c89a1bc8 Mon Sep 17 00:00:00 2001 From: Michael Messmer Date: Mon, 8 Sep 2025 14:35:40 +0200 Subject: [PATCH 24/34] fix attribute --- postgres/outputs.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres/outputs.tf b/postgres/outputs.tf index 7e8f64f..aa7b509 100644 --- a/postgres/outputs.tf +++ b/postgres/outputs.tf @@ -47,7 +47,7 @@ output "postgres_credentials" { username = u.username password = u.password port = u.port - db_name = stackit_postgresflex_database.this[u.owner].name + db_name = stackit_postgresflex_database.this[u.username].name uri = u.uri } } From 2cade4eba27bc2e4a3873f59d551428afff50c58 Mon Sep 17 00:00:00 2001 From: Michael Messmer Date: Mon, 8 Sep 2025 16:00:13 +0200 Subject: [PATCH 25/34] finalize changes --- postgres/outputs.tf | 43 +++-------------------------- postgres/readme.md | 67 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 39 deletions(-) create mode 100644 postgres/readme.md diff --git a/postgres/outputs.tf b/postgres/outputs.tf index aa7b509..e846bfb 100644 --- a/postgres/outputs.tf +++ b/postgres/outputs.tf @@ -1,44 +1,9 @@ # Postgres Instance Output -# output "postgres_instance_id" { -# value = stackit_postgresflex_instance.this.instance_id -# } - -# Postgres Database Output -# output "postgres_database_id" { -# value = stackit_postgresflex_database.this.database_id -# } - -# Postgres User Output -# output "postgres_host" { -# value = stackit_postgresflex_user.this.host -# } - -# output "postgres_password" { -# value = stackit_postgresflex_user.this.password -# sensitive = true -# } - -# output "postgres_user" { -# value = stackit_postgresflex_user.this.username -# } - -# output "postgres_port" { -# value = stackit_postgresflex_user.this.port -# } - -# output "postgres_db_name" { -# value = stackit_postgresflex_database.this.name -# } - -# output "postgres_uri" { -# value = stackit_postgresflex_user.this.uri -# sensitive = true -# } - -# output "postgres_user_id" { -# value = stackit_postgresflex_user.this.user_id -# } +output "postgres_instance_id" { + value = stackit_postgresflex_instance.this.instance_id +} +# Postgres Credential Output output "postgres_credentials" { value = { for k, u in stackit_postgresflex_user.this : diff --git a/postgres/readme.md b/postgres/readme.md new file mode 100644 index 0000000..d6635c0 --- /dev/null +++ b/postgres/readme.md @@ -0,0 +1,67 @@ +# Module for creating Postgres Flex Instance with Databases and Users + +## Example + +```main.tf + +# Postgres Flex Instance +module "postgres-flex" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//postgres?ref=main + stackit_project_id = local.stackit_project_id + postgres_instance_name = "example-db" + postgres_instance_replicas = 1 + postgres_instance_storage = { + class = "premium-perf2-stackit" + size = 5 + } + + postgres_instance_flavor = { + cpu = 2 + ram = 4 + } + + postgres_instance_acl = [ + "193.148.160.0/19", + "45.129.40.0/21" + ] + + postgres_instance_backup_schedule = "00 02 * * *" + postgres_instance_version = "17" + postgres_instance_region = "eu01" + + postgres_databases = [ + { + db_name = "database-a" + user_name = "user-a" + user_roles = ["createdb", "login"] + }, + { + db_name = "database-b" + user_name = "user-b" + user_roles = ["createdb", "login"] + }, + ] +} + +# safe credentials +module "postgres-credentials-sm-a" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//create-secret?ref=main" + secret_manager_instance_id = local.secret_manager_instance_id + secret_manager_username = var.secret_manager_username + secret_manager_password = var.secret_manager_password + + secrets_path = "service-a/postgres" + secret_data = module.postgres-flex.postgres_credentials["user-a"] +} + +module "postgres-credentials-sm-b" { + source = "git::https://commerce-platform.git.onstackit.cloud/commerce-platform-public/terraform-modules//create-secret?ref=main" + secret_manager_instance_id = local.secret_manager_instance_id + secret_manager_username = var.secret_manager_username + secret_manager_password = var.secret_manager_password + + secrets_path = "service-b/postgres" + secret_data = module.postgres-flex.postgres_credentials["user-b"] +} + +``` \ No newline at end of file From 3909eeab7bef34b84feb0978a5fffc2c47a94fe1 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Wed, 15 Oct 2025 09:32:20 +0200 Subject: [PATCH 26/34] Bump stackit version to 0.68.0 --- dns/providers.tf | 2 +- mongodb/providers.tf | 2 +- objectstorage/providers.tf | 2 +- observability/providers.tf | 2 +- postgres/providers.tf | 2 +- rabbitmq/providers.tf | 2 +- redis/providers.tf | 2 +- ske-cluster/providers.tf | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dns/providers.tf b/dns/providers.tf index 96e3e65..954cee6 100644 --- a/dns/providers.tf +++ b/dns/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.61.0" + version = "~> 0.68.0" } } } diff --git a/mongodb/providers.tf b/mongodb/providers.tf index 792bab1..954cee6 100644 --- a/mongodb/providers.tf +++ b/mongodb/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.50.0" + version = "~> 0.68.0" } } } diff --git a/objectstorage/providers.tf b/objectstorage/providers.tf index 792bab1..954cee6 100644 --- a/objectstorage/providers.tf +++ b/objectstorage/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.50.0" + version = "~> 0.68.0" } } } diff --git a/observability/providers.tf b/observability/providers.tf index b96dabd..f073db7 100644 --- a/observability/providers.tf +++ b/observability/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.50.0" + version = "~> 0.68.0" } grafana = { source = "grafana/grafana" diff --git a/postgres/providers.tf b/postgres/providers.tf index 792bab1..954cee6 100644 --- a/postgres/providers.tf +++ b/postgres/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.50.0" + version = "~> 0.68.0" } } } diff --git a/rabbitmq/providers.tf b/rabbitmq/providers.tf index 792bab1..954cee6 100644 --- a/rabbitmq/providers.tf +++ b/rabbitmq/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.50.0" + version = "~> 0.68.0" } } } diff --git a/redis/providers.tf b/redis/providers.tf index 1d527de..9596bbb 100644 --- a/redis/providers.tf +++ b/redis/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.50.0" + version = "~> 0.68.0" } } diff --git a/ske-cluster/providers.tf b/ske-cluster/providers.tf index c3406cd..954cee6 100644 --- a/ske-cluster/providers.tf +++ b/ske-cluster/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.62.0" + version = "~> 0.68.0" } } } From 68a6e7b5a8dbcf41f56b74d323a672f05204d27e Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Tue, 4 Nov 2025 13:14:40 +0100 Subject: [PATCH 27/34] add new option variables --- mongodb/variables.tf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongodb/variables.tf b/mongodb/variables.tf index e69bd6f..b3f127f 100644 --- a/mongodb/variables.tf +++ b/mongodb/variables.tf @@ -30,7 +30,9 @@ variable "mongodb_instance_flavor" { variable "mongodb_instance_options" { description = "options for mongodb" type = object({ - type = string + type = string + snapshot_retention_days = number + point_in_time_window_hours = number }) } From c23b03d9c52ad5e94d2cf5f635101f6f87a87033 Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Tue, 4 Nov 2025 17:38:12 +0100 Subject: [PATCH 28/34] cleanup --- mongodb/mongodb.tf | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/mongodb/mongodb.tf b/mongodb/mongodb.tf index 5ee3a58..010010c 100644 --- a/mongodb/mongodb.tf +++ b/mongodb/mongodb.tf @@ -19,30 +19,3 @@ resource "stackit_mongodbflex_user" "this" { roles = var.mongodb_user_roles database = var.mongodb_user_database } - -# // Configure Secret Manager Provider -# provider "vault" { -# address = "https://prod.sm.eu01.stackit.cloud" -# skip_child_token = true -# auth_login_userpass { -# username = var.secret_manager_username -# password = var.secret_manager_password -# } -# } - -# // Store MongoDB Credentials in Secret Manager -# resource "vault_kv_secret_v2" "mongodb_cred_save" { -# mount = var.secret_manager_instance_id -# name = var.mongodb_secrets_path -# cas = 1 -# delete_all_versions = true -# data_json = jsonencode( -# { -# username = stackit_mongodbflex_user.mongodb_user.username, -# password = stackit_mongodbflex_user.mongodb_user.password, -# host = stackit_mongodbflex_user.mongodb_user.host, -# port = stackit_mongodbflex_user.mongodb_user.port, -# uri = stackit_mongodbflex_user.mongodb_user.uri -# } -# ) -# } From 690920850522f9bad2b456d4afbc6240198612bc Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Wed, 26 Nov 2025 10:23:57 +0100 Subject: [PATCH 29/34] enable edits to contact-points --- grafana/contact-point-gchat/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/grafana/contact-point-gchat/main.tf b/grafana/contact-point-gchat/main.tf index bed73bf..6056258 100644 --- a/grafana/contact-point-gchat/main.tf +++ b/grafana/contact-point-gchat/main.tf @@ -1,5 +1,6 @@ resource "grafana_contact_point" "this" { name = var.contact_point_name + disable_provenance = true googlechat { url = var.gchat_url From d361fa8da99a185173eec9a0dd27e62c014d39c2 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Wed, 26 Nov 2025 10:32:30 +0100 Subject: [PATCH 30/34] add edits to notification policy --- grafana/notification-policy/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/grafana/notification-policy/main.tf b/grafana/notification-policy/main.tf index 6ea9ebc..88df625 100644 --- a/grafana/notification-policy/main.tf +++ b/grafana/notification-policy/main.tf @@ -1,6 +1,7 @@ resource "grafana_notification_policy" "this" { contact_point = var.default_contact_point_uid group_by = var.group_by + disable_provenance = true dynamic "policy" { for_each = var.folder_policies From 010ac595c1bf217d84e1668de0a27dc3b6fd1a79 Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Tue, 16 Dec 2025 13:51:14 +0100 Subject: [PATCH 31/34] build and use local map of username to dbname --- postgres/outputs.tf | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/postgres/outputs.tf b/postgres/outputs.tf index e846bfb..bf89a2a 100644 --- a/postgres/outputs.tf +++ b/postgres/outputs.tf @@ -3,6 +3,14 @@ output "postgres_instance_id" { value = stackit_postgresflex_instance.this.instance_id } +locals { + # Build a map: username => db_name + user_to_db = { + for db in var.postgres_databases : + db.user_name => db.db_name + } +} + # Postgres Credential Output output "postgres_credentials" { value = { @@ -12,9 +20,9 @@ output "postgres_credentials" { username = u.username password = u.password port = u.port - db_name = stackit_postgresflex_database.this[u.username].name + db_name = stackit_postgresflex_database.this[local.user_to_db[u.username]].name uri = u.uri } } sensitive = true -} \ No newline at end of file +} From 4c5496879358ef8c8a961d92ec5658055bc46894 Mon Sep 17 00:00:00 2001 From: Florian Heuer Date: Tue, 16 Dec 2025 13:52:39 +0100 Subject: [PATCH 32/34] add validation for uniqueness of username and dbname --- postgres/variables.tf | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/postgres/variables.tf b/postgres/variables.tf index 886fd59..f1906fa 100644 --- a/postgres/variables.tf +++ b/postgres/variables.tf @@ -55,10 +55,26 @@ variable "postgres_instance_region" { # Postgres User and DB Configs variable "postgres_databases" { - description = "list of users and databases" - type = list(object({ - db_name = string # db name inside the instance - user_name = string # username and owner for postgres db - user_roles = list(string) # List of database access levels for the user. Supported values are: login, createdb. + description = "list of users and databases" + type = list(object({ + db_name = string # db name inside the instance + user_name = string # username and owner for postgres db + user_roles = list(string) # List of database access levels for the user. Supported values are: login, createdb. })) -} \ No newline at end of file + + # ----------------------------------------------------------------- + # Validation: each db_name must be unique + # ----------------------------------------------------------------- + validation { + condition = length(distinct([for db in var.postgres_databases : db.db_name])) == length(var.postgres_databases) + error_message = "Each db_name must be unique." + } + + # ----------------------------------------------------------------- + # Validation: each user_name must be unique + # ----------------------------------------------------------------- + validation { + condition = length(distinct([for db in var.postgres_databases : db.user_name])) == length(var.postgres_databases) + error_message = "Each user_name must be unique." + } +} From 4ecc7c67c8523043435c35793addba293b10a645 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Mon, 2 Feb 2026 09:12:43 +0100 Subject: [PATCH 33/34] remove itdoc link --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 66c946d..bbf6e9b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Terraform modules for STACKIT resources -## Overview - -You can find general overview of Terraform in [ITDOC](https://itdoc.schwarz/display/STACKIT/Terraform+overview) - ## How to use You can find examples in README.md of each module folder, e.g. for [Redis](./redis/README.md) From 527a67563418d339995bab2d78533039d69e0707 Mon Sep 17 00:00:00 2001 From: Stanislav Kopp Date: Thu, 5 Feb 2026 16:21:42 +0100 Subject: [PATCH 34/34] temporary pin provider version to 0.74.0 to avoid breaking changes --- dns/providers.tf | 2 +- mongodb/providers.tf | 2 +- objectstorage/providers.tf | 2 +- observability/providers.tf | 2 +- postgres/providers.tf | 2 +- rabbitmq/providers.tf | 2 +- redis/providers.tf | 2 +- secrets-manager/providers.tf | 2 +- service-account/providers.tf | 2 +- ske-cluster/providers.tf | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dns/providers.tf b/dns/providers.tf index 954cee6..91238eb 100644 --- a/dns/providers.tf +++ b/dns/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } } } diff --git a/mongodb/providers.tf b/mongodb/providers.tf index 954cee6..91238eb 100644 --- a/mongodb/providers.tf +++ b/mongodb/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } } } diff --git a/objectstorage/providers.tf b/objectstorage/providers.tf index 954cee6..91238eb 100644 --- a/objectstorage/providers.tf +++ b/objectstorage/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } } } diff --git a/observability/providers.tf b/observability/providers.tf index f073db7..14b0fba 100644 --- a/observability/providers.tf +++ b/observability/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } grafana = { source = "grafana/grafana" diff --git a/postgres/providers.tf b/postgres/providers.tf index 954cee6..91238eb 100644 --- a/postgres/providers.tf +++ b/postgres/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } } } diff --git a/rabbitmq/providers.tf b/rabbitmq/providers.tf index 954cee6..91238eb 100644 --- a/rabbitmq/providers.tf +++ b/rabbitmq/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } } } diff --git a/redis/providers.tf b/redis/providers.tf index 9596bbb..83a86da 100644 --- a/redis/providers.tf +++ b/redis/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } } diff --git a/secrets-manager/providers.tf b/secrets-manager/providers.tf index 419a151..636035d 100644 --- a/secrets-manager/providers.tf +++ b/secrets-manager/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "0.43.3" + version = "0.74.0" } } } \ No newline at end of file diff --git a/service-account/providers.tf b/service-account/providers.tf index 28dfab1..91238eb 100644 --- a/service-account/providers.tf +++ b/service-account/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.59.0" + version = "0.74.0" } } } diff --git a/ske-cluster/providers.tf b/ske-cluster/providers.tf index 954cee6..91238eb 100644 --- a/ske-cluster/providers.tf +++ b/ske-cluster/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.68.0" + version = "0.74.0" } } }