Compare commits
33 commits
v25.7.3001
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d361fa8da9 | |||
| 6909208505 | |||
| 06e9a5a586 | |||
| c23b03d9c5 | |||
| 68a6e7b5a8 | |||
| a558573190 | |||
| 3909eeab7b | |||
| 4db34d12d8 | |||
| 2cade4eba2 | |||
| 3eed77d451 | |||
| 09bff53f30 | |||
| d928465802 | |||
| 9539b43f7d | |||
| 3318babf7c | |||
| a6c34ee9bc | |||
| a668f3ad93 | |||
| 9604dfb6f4 | |||
| cec9aff5e4 | |||
| d5a0c28c11 | |||
| 2ffaa5060e | |||
| ae40c80195 | |||
| 2bf17ea21e | |||
| 6446a9d123 | |||
| 61761c438a | |||
| fcbd49b302 | |||
| 0fc20b8741 | |||
| 0839a8adc4 | |||
| 4fbc356cbb | |||
| 77a36b4bb0 | |||
| 06b2db44bb | |||
| 4de6f8a4df | |||
| 92e4465889 | |||
| 7413af8b2b |
41 changed files with 1100 additions and 232 deletions
160
dns/README.md
Normal file
160
dns/README.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Terraform STACKIT DNS Zone and Record Set Module
|
||||
|
||||
This module allows you to declaratively manage DNS zones and their associated record sets in STACKIT.
|
||||
|
||||
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 = "./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"
|
||||
|
||||
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"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
# 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"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
# EXAMPLE 3: Create a new zone with no initial records
|
||||
"empty_domain" = {
|
||||
name = "My Empty Domain"
|
||||
dns_name = "empty-example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| 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 |
|
||||
|
||||
### Values for zones
|
||||
|
||||
| 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 |
|
||||
|
||||
### 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.
|
||||
94
dns/dns.tf
Normal file
94
dns/dns.tf
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# main.tf
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
# 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" {
|
||||
# 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 }
|
||||
|
||||
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
|
||||
}
|
||||
11
dns/outputs.tf
Normal file
11
dns/outputs.tf
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# 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 "record_sets" {
|
||||
description = "A map of all created DNS record set objects."
|
||||
value = stackit_dns_record_set.this
|
||||
}
|
||||
8
dns/providers.tf
Normal file
8
dns/providers.tf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.68.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
48
dns/variables.tf
Normal file
48
dns/variables.tf
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# variables.tf
|
||||
|
||||
variable "project_id" {
|
||||
type = string
|
||||
description = "STACKIT project ID to which the DNS resources are associated."
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
# Required attributes for new zones
|
||||
name = optional(string)
|
||||
dns_name = optional(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")
|
||||
|
||||
# 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)
|
||||
|
||||
# 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 = {}
|
||||
}
|
||||
|
|
@ -1,83 +1,252 @@
|
|||
# Modules for Grafana alerts and dashboards
|
||||
<!-- TOC -->
|
||||
|
||||
|
||||
## 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**“.
|
||||
Below is example for alerts using „**Prometheus/Thanos**“ datasource 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/
|
||||
│ ├── common-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 "gchat-contact-point" {
|
||||
datasources = {
|
||||
Thanos-Common-Infra-PRD = {
|
||||
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"
|
||||
basic_auth_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"
|
||||
gchat-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.gchat-contact-point.contact_name
|
||||
default_contact_point_uid = module.gchat-contact-point-coin.contact_point_name
|
||||
group_by = ["alertname"]
|
||||
|
||||
folder_policies = {
|
||||
"Alerts" = module.gchat-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.gchat-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="<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.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
locals {
|
||||
#################################################################
|
||||
# 1. Discover & decode alert YAML files from <root>/<alerts_dir>
|
||||
#################################################################
|
||||
# 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")
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,3 +2,10 @@ output "rule_group_ids" {
|
|||
description = "Map: <folder_uid>.<name> → Grafana rule-group ID"
|
||||
value = { for k, v in grafana_rule_group.this : k => v.id }
|
||||
}
|
||||
output "debug_alert_files" {
|
||||
value = local.alert_files
|
||||
}
|
||||
output "debug_groups" {
|
||||
description = "Parsed groups from YAML"
|
||||
value = local.groups
|
||||
}
|
||||
|
|
@ -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)."
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
resource "grafana_contact_point" "this" {
|
||||
name = var.contact-point-name
|
||||
name = var.contact_point_name
|
||||
disable_provenance = true
|
||||
|
||||
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\" . }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
24
grafana/contact-point-gchat/templates.tf
Normal file
24
grafana/contact-point-gchat/templates.tf
Normal file
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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 = ""
|
||||
}
|
||||
|
|
@ -1,11 +1,114 @@
|
|||
# Create the basic "shell" of each datasource.
|
||||
resource "grafana_data_source" "this" {
|
||||
type = "prometheus"
|
||||
name = var.datasource_name
|
||||
url = var.datasource_url
|
||||
basic_auth_enabled = true
|
||||
basic_auth_username = var.datasource_username
|
||||
for_each = var.datasources
|
||||
|
||||
secure_json_data_encoded = jsonencode({
|
||||
basicAuthPassword = var.datasource_password
|
||||
name = each.key
|
||||
type = each.value.type
|
||||
url = var.datasource_urls[each.value.url_key]
|
||||
is_default = coalesce(each.value.is_default, false)
|
||||
|
||||
# For HTTP Basic Auth (Loki, Prometheus, etc.)
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# 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.json_data != null && v.derived_fields == null && v.traces_to_logs == null
|
||||
}
|
||||
|
||||
uid = grafana_data_source.this[each.key].uid
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
# 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]
|
||||
}
|
||||
}
|
||||
|
||||
# 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 : {
|
||||
datasourceUid = grafana_data_source.this[field.target_datasource_name].uid
|
||||
matcherRegex = field.matcher_regex
|
||||
name = field.name
|
||||
url = field.url
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# This config must ignore the password, which is managed elsewhere.
|
||||
lifecycle {
|
||||
ignore_changes = [secure_json_data_encoded]
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
# This config must ignore the password, which is managed elsewhere.
|
||||
lifecycle {
|
||||
ignore_changes = [secure_json_data_encoded]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,56 @@
|
|||
# Grafana contact point
|
||||
variable "datasource_url" {
|
||||
type = string
|
||||
variable "datasources" {
|
||||
description = <<EOT
|
||||
Map of datasources to create. Keys are datasource names.
|
||||
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)
|
||||
|
||||
# Key to look up a database username
|
||||
db_user_key = optional(string)
|
||||
# Key to look up a basic auth username
|
||||
basic_auth_user_key = optional(string)
|
||||
|
||||
# Non-sensitive JSON data for Postgres, etc.
|
||||
json_data = optional(map(any))
|
||||
|
||||
# Linking Attributes (for Loki/Tempo)
|
||||
derived_fields = optional(list(object({
|
||||
target_datasource_name = string
|
||||
matcher_regex = string
|
||||
name = string
|
||||
url = string
|
||||
})))
|
||||
traces_to_logs = optional(object({
|
||||
target_datasource_name = string
|
||||
query = string
|
||||
custom_query = optional(bool)
|
||||
filter_by_span_id = optional(bool)
|
||||
filter_by_trace_id = optional(bool)
|
||||
span_start_time_shift = optional(string)
|
||||
span_end_time_shift = optional(string)
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
variable "datasource_name" {
|
||||
type = string
|
||||
variable "datasource_urls" {
|
||||
description = "Map of datasource URLs, keyed by url_key"
|
||||
type = map(string)
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "datasource_username" {
|
||||
type = string
|
||||
sensitive = true
|
||||
variable "datasource_users" {
|
||||
description = "Map of datasource usernames, keyed by db_user_key or basic_auth_user_key"
|
||||
type = map(string)
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "datasource_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
variable "datasource_passwords" {
|
||||
description = "Map of datasource passwords, keyed by pass_key"
|
||||
type = map(string)
|
||||
sensitive = true
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{{ define "gchat-body-template" -}}
|
||||
{{ define "google-chat-body-template" -}}
|
||||
{{- $alerts := .Alerts }}
|
||||
{{- if not $alerts }}{{ $alerts = . }}{{ end }}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{ define "gchat-title-template" -}}
|
||||
{{ define "google-chat-title-template" -}}
|
||||
{{- if eq .Status "firing" -}}
|
||||
🔥 Firing:
|
||||
{{- else if eq .Status "resolved" -}}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# }
|
||||
# )
|
||||
# }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.50.0"
|
||||
version = "~> 0.68.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.50.0"
|
||||
version = "~> 0.68.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.50.0"
|
||||
version = "~> 0.68.0"
|
||||
}
|
||||
grafana = {
|
||||
source = "grafana/grafana"
|
||||
|
|
|
|||
|
|
@ -2,40 +2,19 @@
|
|||
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
|
||||
# Postgres Credential Output
|
||||
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.username].name
|
||||
uri = u.uri
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
name = each.value.db_name
|
||||
owner = each.value.user_name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.50.0"
|
||||
version = "~> 0.68.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
postgres/readme.md
Normal file
67
postgres/readme.md
Normal file
|
|
@ -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"]
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -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.
|
||||
}))
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.50.0"
|
||||
version = "~> 0.68.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.50.0"
|
||||
version = "~> 0.68.0"
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
service-account/README.md
Normal file
59
service-account/README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Terraform Module: STACKIT Service Account
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Example 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
|
||||
}
|
||||
|
||||
# Save json created to secrets manager
|
||||
variable "secret_manager_username" {
|
||||
description = "username of the secrets manger to store credentials"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
25
service-account/outputs.tf
Normal file
25
service-account/outputs.tf
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
8
service-account/providers.tf
Normal file
8
service-account/providers.tf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.59.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
26
service-account/service-account.tf
Normal file
26
service-account/service-account.tf
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
resource "stackit_service_account" "this" {
|
||||
name = var.service_account_name
|
||||
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
|
||||
|
||||
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_server_service_account_attach" "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
|
||||
}
|
||||
52
service-account/variables.tf
Normal file
52
service-account/variables.tf
Normal file
|
|
@ -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 = ""
|
||||
}
|
||||
|
|
@ -9,12 +9,14 @@ 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"
|
||||
machine_type = "c1.2"
|
||||
minimum = "2"
|
||||
maximum = "3"
|
||||
os_version_min = "4230.2.0"
|
||||
availability_zones = ["eu01-3"]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
stackit = {
|
||||
source = "stackitcloud/stackit"
|
||||
version = "~> 0.50.0"
|
||||
version = "~> 0.68.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ variable "ske_node_pools" {
|
|||
machine_type = string
|
||||
minimum = number
|
||||
maximum = number
|
||||
os_version_min = string
|
||||
availability_zones = list(string)
|
||||
}))
|
||||
}
|
||||
|
|
@ -29,8 +30,7 @@ variable "ske_maintenance" {
|
|||
})
|
||||
}
|
||||
|
||||
#variable "observability-instance-id" {
|
||||
# description = "instance id of the observability instance for cluster monitoring"
|
||||
# type = string
|
||||
#
|
||||
#}
|
||||
variable "ske_k8s_version_min" {
|
||||
description = "minimum Kubernetes version"
|
||||
type = string
|
||||
}
|
||||
Loading…
Reference in a new issue