From 807fe871bed314a56e1c4c54206c48e41dd3436d Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Fri, 29 May 2026 10:55:06 +0200 Subject: [PATCH] example(telemetry-router): build hub and spoke architecture for logs --- .gitignore | 3 +- .../.terraform.lock.hcl | 44 ++++++ .../010-provider.tf | 33 +++++ .../020-variables.tf | 30 +++++ .../030-resourcemanager.tf | 43 ++++++ .../040-observability-instance.tf | 26 ++++ .../041-logs-instance.tf | 28 ++++ .../042-s3-bucket.tf | 33 +++++ .../050-telemetry-router.tf | 88 ++++++++++++ .../060-telemetry-links.tf | 71 ++++++++++ .../070-log-generator.tf | 84 ++++++++++++ .../100-outputs.tf | 79 +++++++++++ .../MAINTAINERS.md | 9 ++ .../README.md | 126 ++++++++++++++++++ .../scripts/count-s3-items.sh | 31 +++++ .../scripts/download-s3-logs.sh | 57 ++++++++ 16 files changed, 784 insertions(+), 1 deletion(-) create mode 100644 examples/telemetry-router-hub-spoke-setup/.terraform.lock.hcl create mode 100644 examples/telemetry-router-hub-spoke-setup/010-provider.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/020-variables.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/030-resourcemanager.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/040-observability-instance.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/041-logs-instance.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/042-s3-bucket.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/050-telemetry-router.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/060-telemetry-links.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/070-log-generator.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/100-outputs.tf create mode 100644 examples/telemetry-router-hub-spoke-setup/MAINTAINERS.md create mode 100644 examples/telemetry-router-hub-spoke-setup/README.md create mode 100755 examples/telemetry-router-hub-spoke-setup/scripts/count-s3-items.sh create mode 100755 examples/telemetry-router-hub-spoke-setup/scripts/download-s3-logs.sh diff --git a/.gitignore b/.gitignore index 50637e9..bea598b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -### Terraform template +.### Terraform template # Local .terraform directories **/.terraform/* @@ -71,3 +71,4 @@ keys ### K8s .kubeconfig +/examples/telemetry-router-hub-spoke-setup/scripts/downloads/ diff --git a/examples/telemetry-router-hub-spoke-setup/.terraform.lock.hcl b/examples/telemetry-router-hub-spoke-setup/.terraform.lock.hcl new file mode 100644 index 0000000..e3e238d --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/time" { + version = "0.14.0" + hashes = [ + "h1:/hlxsUpuN/lvPTNL9+NyVGsOyRsK5NsxwFMsj5CdOp4=", + "zh:12abfd6b800e4d7fa6db7310dec8ffd440b31993861ef188c7ed5260b3073937", + "zh:23005521e800bb19e1597bf755c5f70d675d30b685d4255001ed5fa47d9df3f1", + "zh:2fea249b582ae97cd1cc10385187ea50993bb47c28cc5df0305e57ceaabf0a10", + "zh:322018d3b987b7aad08697178029a2bb667bed699e88328f0c89c52a2fd41341", + "zh:32a08e98fce2d273cb9b2c89d6c54727cc9f0a32e15bfd896be4e02cc6b48f95", + "zh:3db89aabd0e619616bd4b0f8b373a7586dfe60feffcea12a84a0bdbc445714b3", + "zh:7488f56c81d742dc020f29063626c8f07ca188aa97be61e7307e8d62397020a2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7cb4067f2e7559b13f7562ef722f948950901eb37834873e98360ab28f66e9d7", + "zh:9d552c8345f61e1b7db8e725144981345f18ac1014d58d6f5ddf0928a195fffb", + "zh:a8e69fb6b97fc9d86fb19a9f4d42abe33c4a68e700b15387ce2e17d2b9934bed", + "zh:aeeb900eb8dd0f790c60ea5c0e0c8d42bd6e4a54f391681d4decca15b544394b", + "zh:c239c619101a8c95e1f14061eb973c57a8d15fa0e68878ced5bbd76858ee5b79", + ] +} + +provider "registry.terraform.io/stackitcloud/stackit" { + version = "0.98.0" + constraints = ">= 0.95.0" + hashes = [ + "h1:/FB0wBnvmjumjykX+j90kSck6LMScDaYo1STO5Vp/kw=", + "zh:031028340fbaeeb5c4c6b1d5c6d6287a70cf253cfb89f04d462a1c0ab6237ffc", + "zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f", + "zh:0eee18f9a262fa58966c960f1f0863eed92cd953d0f0306ecc456b58cc2911f8", + "zh:1646966ebac0eb5d6c78ac5aa1528921d7a635f14d81300463a402c55e33cfd3", + "zh:5374ab9e5e6d837787b4f18bcf0125a1bf3ee2da40c022cc7695d6879fed111b", + "zh:6a5b9e1307055f8d358373da625ffcb4d77ec44f260d14473b10e5777380765e", + "zh:6c90090504474695ab7290d64386dd988f4fb65c90c74c9cf3a6da6226ae8a70", + "zh:8317218828f29be95ce712863646dc8968e146ec14e5ab258cb1e8f8b649245b", + "zh:9eef08e4fb7a75760f9dc8a422446f19a210ebf8177dd5aeb97444295f0120cf", + "zh:9f2147eee63feae75b96f17f3b3ebab8a29cd7164cdd08eb2bb871e5c425a77f", + "zh:b63ea754eea233292fb73d87a9810104da2bd347abf2ca0da44ac76591dcdddb", + "zh:de60bd928828a836e446f9f89e7a3bfc4e6dd73bac6827914087b34e4ad0c978", + "zh:f22d295b2e4e94ae1566e20fd752825e008a62250cf7243f1161c0bf4e986518", + "zh:f7e57bc7be2cc016983ff3ad50d2733b85e90bfaa7aa9e2192563dc9d422fb07", + ] +} diff --git a/examples/telemetry-router-hub-spoke-setup/010-provider.tf b/examples/telemetry-router-hub-spoke-setup/010-provider.tf new file mode 100644 index 0000000..c7d9575 --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/010-provider.tf @@ -0,0 +1,33 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">=0.95.0" + } + time = { + source = "hashicorp/time" + version = "~> 0.11" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + enable_beta_resources = true + experiments = ["iam"] +} diff --git a/examples/telemetry-router-hub-spoke-setup/020-variables.tf b/examples/telemetry-router-hub-spoke-setup/020-variables.tf new file mode 100644 index 0000000..84cd4eb --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/020-variables.tf @@ -0,0 +1,30 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "stackit_org_id" { + type = string +} + +variable "stackit_region" { + type = string + default = "eu01" +} + +variable "stackit_service_account_key_path" { + type = string +} + +variable "stackit_owner_email" { + type = string +} diff --git a/examples/telemetry-router-hub-spoke-setup/030-resourcemanager.tf b/examples/telemetry-router-hub-spoke-setup/030-resourcemanager.tf new file mode 100644 index 0000000..f79558e --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/030-resourcemanager.tf @@ -0,0 +1,43 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_resourcemanager_folder" "this" { + name = "telemetry-router-link-test" + owner_email = var.stackit_owner_email + parent_container_id = var.stackit_org_id +} + +resource "stackit_resourcemanager_project" "telemetry_hub" { + parent_container_id = stackit_resourcemanager_folder.this.container_id + name = "telemetry_hub" + owner_email = var.stackit_owner_email +} + +resource "stackit_resourcemanager_project" "telemetry_spoke1" { + parent_container_id = stackit_resourcemanager_folder.this.container_id + name = "telemetry_spoke1" + owner_email = var.stackit_owner_email +} + +resource "stackit_resourcemanager_project" "telemetry_spoke2" { + parent_container_id = stackit_resourcemanager_folder.this.container_id + name = "telemetry_spoke2" + owner_email = var.stackit_owner_email +} + +resource "stackit_resourcemanager_project" "telemetry_spoke3" { + parent_container_id = stackit_resourcemanager_folder.this.container_id + name = "telemetry_spoke3" + owner_email = var.stackit_owner_email +} diff --git a/examples/telemetry-router-hub-spoke-setup/040-observability-instance.tf b/examples/telemetry-router-hub-spoke-setup/040-observability-instance.tf new file mode 100644 index 0000000..50b32fd --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/040-observability-instance.tf @@ -0,0 +1,26 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_observability_instance" "this" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + name = "telemetry_hub" + plan_name = "Observability-Large-EU01" + alert_config = null + acl = ["0.0.0.0/0"] +} + +resource "stackit_observability_credential" "router_ingest" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + instance_id = stackit_observability_instance.this.instance_id +} diff --git a/examples/telemetry-router-hub-spoke-setup/041-logs-instance.tf b/examples/telemetry-router-hub-spoke-setup/041-logs-instance.tf new file mode 100644 index 0000000..924f906 --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/041-logs-instance.tf @@ -0,0 +1,28 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_logs_instance" "this" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + region = "eu01" + display_name = "telemetry_hub" + retention_days = 30 + acl = ["0.0.0.0/0"] +} + +resource "stackit_logs_access_token" "router_ingest" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + instance_id = stackit_logs_instance.this.instance_id + display_name = "router-ingest-token" + permissions = ["write", "read"] +} diff --git a/examples/telemetry-router-hub-spoke-setup/042-s3-bucket.tf b/examples/telemetry-router-hub-spoke-setup/042-s3-bucket.tf new file mode 100644 index 0000000..b14e2fe --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/042-s3-bucket.tf @@ -0,0 +1,33 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Create an S3 Bucket for log archiving +resource "stackit_objectstorage_bucket" "log_archive" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + name = "telemetry-log-archive" +} + +# Create a Credentials Group for Object Storage +resource "stackit_objectstorage_credentials_group" "router_group" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + name = "router-s3-group" + depends_on = [stackit_objectstorage_bucket.log_archive] +} + + +# Create Credentials for the Telemetry Router to access the bucket +resource "stackit_objectstorage_credential" "router_s3_creds" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + credentials_group_id = stackit_objectstorage_credentials_group.router_group.credentials_group_id +} diff --git a/examples/telemetry-router-hub-spoke-setup/050-telemetry-router.tf b/examples/telemetry-router-hub-spoke-setup/050-telemetry-router.tf new file mode 100644 index 0000000..4c32f81 --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/050-telemetry-router.tf @@ -0,0 +1,88 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Create the Telemetry Router Instance in the Hub project +resource "stackit_telemetryrouter_instance" "hub_router" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + display_name = "hub-telemetry-router" + description = "Central Telemetry Router for spoke projects and parent folder" +} + +# Create an Access Token for the Router +# This token will be used by the links to authenticate with the router +resource "stackit_telemetryrouter_access_token" "hub_router_token" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + instance_id = stackit_telemetryrouter_instance.hub_router.instance_id + display_name = "hub-router-link-token" +} + +# Create a Destination for the Router to send all telemetry data to the central Observability instance +resource "stackit_telemetryrouter_destination" "observability_destination" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + instance_id = stackit_telemetryrouter_instance.hub_router.instance_id + display_name = "observability-dest" + config = { + config_type = "OpenTelemetry" + opentelemetry = { + # Obs-stack has https:// in the attribute + uri = stackit_observability_instance.this.otlp_http_logs_url + basic_auth = { + username = stackit_observability_credential.router_ingest.username + password = stackit_observability_credential.router_ingest.password + } + } + } +} + +# Create a Destination for the Router to send filtered logs to the central Logs instance +# We only want logs from the 'service-account' service +resource "stackit_telemetryrouter_destination" "logs_destination" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + instance_id = stackit_telemetryrouter_instance.hub_router.instance_id + display_name = "logs-dest" + config = { + config_type = "OpenTelemetry" + filter = { + attributes = [{ + key = "service.name" + level = "logRecord" + matcher = "=" + values = ["service-account"] + }] + } + opentelemetry = { + # Prepend https:// as the OTLP URI must have a protocol + uri = "https://${stackit_logs_instance.this.ingest_otlp_url}" + bearer_token = stackit_logs_access_token.router_ingest.access_token + } + } +} + +# Create a Destination for the Router to archive all data in S3 +resource "stackit_telemetryrouter_destination" "s3_archive" { + project_id = stackit_resourcemanager_project.telemetry_hub.project_id + instance_id = stackit_telemetryrouter_instance.hub_router.instance_id + display_name = "s3-log-archive" + config = { + config_type = "S3" + s3 = { + access_key = { + id = stackit_objectstorage_credential.router_s3_creds.access_key + secret = stackit_objectstorage_credential.router_s3_creds.secret_access_key + } + bucket = stackit_objectstorage_bucket.log_archive.name + endpoint = regex("^https://[^/]+", stackit_objectstorage_bucket.log_archive.url_path_style) + } + } +} diff --git a/examples/telemetry-router-hub-spoke-setup/060-telemetry-links.tf b/examples/telemetry-router-hub-spoke-setup/060-telemetry-links.tf new file mode 100644 index 0000000..beff6d5 --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/060-telemetry-links.tf @@ -0,0 +1,71 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Link Hub Project to the Hub Router +# NOTE: The existence of a Telemetry Router in a project DOES NOT automatically link that project's logs. +# Every project (including the hub) must have an explicit Telemetry Link to forward its logs to a router. +resource "stackit_telemetrylink" "hub_link" { + resource_type = "project" + resource_id = stackit_resourcemanager_project.telemetry_hub.project_id + display_name = "hub-to-hub-link" + telemetry_router_id = stackit_telemetryrouter_instance.hub_router.instance_id + access_token = stackit_telemetryrouter_access_token.hub_router_token.access_token +} + +# Link Spoke Project 1 to the Hub Router +resource "stackit_telemetrylink" "spoke1_link" { + resource_type = "project" + resource_id = stackit_resourcemanager_project.telemetry_spoke1.project_id + display_name = "spoke1-to-hub-link" + telemetry_router_id = stackit_telemetryrouter_instance.hub_router.instance_id + access_token = stackit_telemetryrouter_access_token.hub_router_token.access_token +} + +# Link Spoke Project 2 to the Hub Router +resource "stackit_telemetrylink" "spoke2_link" { + resource_type = "project" + resource_id = stackit_resourcemanager_project.telemetry_spoke2.project_id + display_name = "spoke2-to-hub-link" + telemetry_router_id = stackit_telemetryrouter_instance.hub_router.instance_id + access_token = stackit_telemetryrouter_access_token.hub_router_token.access_token +} + +# Link Spoke Project 3 to the Hub Router +resource "stackit_telemetrylink" "spoke3_link" { + resource_type = "project" + resource_id = stackit_resourcemanager_project.telemetry_spoke3.project_id + display_name = "spoke3-to-hub-link" + telemetry_router_id = stackit_telemetryrouter_instance.hub_router.instance_id + access_token = stackit_telemetryrouter_access_token.hub_router_token.access_token +} + +# Link the entire Folder to the Hub Router +# This allows telemetry data from all projects within the folder (if configured) to be routed via the hub router +resource "stackit_telemetrylink" "folder_link" { + resource_type = "folder" + resource_id = stackit_resourcemanager_folder.this.folder_id + display_name = "folder-to-hub-link" + telemetry_router_id = stackit_telemetryrouter_instance.hub_router.instance_id + access_token = stackit_telemetryrouter_access_token.hub_router_token.access_token +} + +# Link the entire Organization to the Hub Router +# This is used to forward organization-level audit logs to the central router +resource "stackit_telemetrylink" "org_link" { + resource_type = "organization" + resource_id = var.stackit_org_id + display_name = "org-to-hub-link" + telemetry_router_id = stackit_telemetryrouter_instance.hub_router.instance_id + access_token = stackit_telemetryrouter_access_token.hub_router_token.access_token +} diff --git a/examples/telemetry-router-hub-spoke-setup/070-log-generator.tf b/examples/telemetry-router-hub-spoke-setup/070-log-generator.tf new file mode 100644 index 0000000..3ac688a --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/070-log-generator.tf @@ -0,0 +1,84 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file creates resources that are rotated every minute. +# The frequent rotation of credentials triggers audit logs in each project, +# allowing us to verify that the Telemetry Router and Links are working as expected. + +resource "time_rotating" "minute" { + rotation_minutes = 1 +} + +locals { + projects = { + hub = stackit_resourcemanager_project.telemetry_hub.project_id + spoke1 = stackit_resourcemanager_project.telemetry_spoke1.project_id + spoke2 = stackit_resourcemanager_project.telemetry_spoke2.project_id + spoke3 = stackit_resourcemanager_project.telemetry_spoke3.project_id + } +} + +# Create a bucket in each project +resource "stackit_objectstorage_bucket" "log_gen" { + for_each = local.projects + project_id = each.value + name = "log-gen-bucket-${each.key}" +} + +# Create a credentials group in each project +resource "stackit_objectstorage_credentials_group" "log_gen" { + for_each = local.projects + project_id = each.value + name = "log-gen-group-${each.key}" + + depends_on = [stackit_objectstorage_bucket.log_gen] +} + +# Create a credential in each project and rotate it every minute +resource "stackit_objectstorage_credential" "log_gen" { + for_each = local.projects + project_id = each.value + credentials_group_id = stackit_objectstorage_credentials_group.log_gen[each.key].credentials_group_id + + # This map forces recreation of the credential whenever the time_rotating resource rotates + rotate_when_changed = { + rotation_id = time_rotating.minute.id + } +} + +# Create a service account in each project to generate more IAM-related audit logs +resource "stackit_service_account" "log_gen" { + for_each = local.projects + project_id = each.value + name = "log-gen-sa-${each.key}" +} + + +resource "stackit_service_account_key" "log_gen" { + for_each = local.projects + project_id = each.value + service_account_email = stackit_service_account.log_gen[each.key].email + + rotate_when_changed = { + rotation = time_rotating.minute.id + } +} + +# Assign the 'reader' role to each service account (using IAM experimental resources) +resource "stackit_authorization_project_role_assignment" "log_gen" { + for_each = local.projects + resource_id = each.value + role = "reader" + subject = stackit_service_account.log_gen[each.key].email +} diff --git a/examples/telemetry-router-hub-spoke-setup/100-outputs.tf b/examples/telemetry-router-hub-spoke-setup/100-outputs.tf new file mode 100644 index 0000000..b87335a --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/100-outputs.tf @@ -0,0 +1,79 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "telemetry_router_id" { + description = "The ID of the central Telemetry Router" + value = stackit_telemetryrouter_instance.hub_router.instance_id +} + +output "telemetry_router_uri" { + description = "The OTLP ingest URI of the central Telemetry Router" + value = stackit_telemetryrouter_instance.hub_router.uri +} + +output "spoke1_link_id" { + description = "The ID of the Telemetry Link for Spoke Project 1" + value = stackit_telemetrylink.spoke1_link.id +} + +output "spoke2_link_id" { + description = "The ID of the Telemetry Link for Spoke Project 2" + value = stackit_telemetrylink.spoke2_link.id +} + +output "spoke3_link_id" { + description = "The ID of the Telemetry Link for Spoke Project 3" + value = stackit_telemetrylink.spoke3_link.id +} + +output "folder_link_id" { + description = "The ID of the Telemetry Link for the parent Folder" + value = stackit_telemetrylink.folder_link.id +} + +output "org_link_id" { + description = "The ID of the Telemetry Link for the Organization" + value = stackit_telemetrylink.org_link.id +} + +output "observability_logs_ingest_url" { + description = "The OTLP HTTP logs ingest URL for the Observability instance" + value = stackit_observability_instance.this.otlp_http_logs_url +} + +output "logs_ingest_url" { + description = "The OTLP ingest URL for the Logs instance" + value = "https://${stackit_logs_instance.this.ingest_otlp_url}" +} + +output "s3_archive_bucket" { + description = "The name of the S3 bucket used for log archiving" + value = stackit_objectstorage_bucket.log_archive.name +} + +output "s3_access_key" { + description = "The S3 access key for the log archive bucket" + value = stackit_objectstorage_credential.router_s3_creds.access_key +} + +output "s3_secret_key" { + description = "The S3 secret access key for the log archive bucket" + value = stackit_objectstorage_credential.router_s3_creds.secret_access_key + sensitive = true +} + +output "s3_endpoint" { + description = "The S3 endpoint for the log archive bucket" + value = regex("^https://[^/]+", stackit_objectstorage_bucket.log_archive.url_path_style) +} diff --git a/examples/telemetry-router-hub-spoke-setup/MAINTAINERS.md b/examples/telemetry-router-hub-spoke-setup/MAINTAINERS.md new file mode 100644 index 0000000..1aaefce --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/MAINTAINERS.md @@ -0,0 +1,9 @@ +# Maintainers + +General maintainers: + +- Mauritz Uphoff (mauritz.uphoff@digits.schwarz) + +This example is actively maintained. The owner is responsible for reviewing and updating dependencies and functionalities on a monthly basis. +For questions, issues, or feature requests, please email general maintainers. +Please include the BP name and version in your request. We will track your request as an issue. diff --git a/examples/telemetry-router-hub-spoke-setup/README.md b/examples/telemetry-router-hub-spoke-setup/README.md new file mode 100644 index 0000000..b93c34a --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/README.md @@ -0,0 +1,126 @@ +# Telemetry Router: Hub-and-Spoke Setup + +This example demonstrates how to use the **STACKIT Telemetry Router** to centralize observability data across multiple projects, folders, and even the entire organization. + +> ⚠️⚠️⚠️ **A Telemetry Router DOES NOT replace a Telemetry Link.** ⚠️⚠️⚠️\ +> Creating a Router in a project only provides the ingestion endpoint. To actually forward logs from that same project (or any other project) to the router, you **MUST** create an explicit `stackit_telemetrylink`. Every project in your hierarchy requires its own link to participate in the telemetry routing. + +## Architecture Overview + +```mermaid +graph TD + subgraph Organization [STACKIT Organization] + OrgLink([Link: org-to-hub-link]) + + subgraph ParentFolder [Folder: telemetry-router-link-test] + FolderLink([Link: folder-to-hub-link]) + + subgraph HubProject [Project: telemetry_hub] + Router{Router: hub-telemetry-router} + HubLink([Link: hub-to-hub-link]) + Obs[Observability Instance] + Logs[Logs Instance] + S3[S3 Log Archive] + + Router -->|OTLP| Obs + Router -->|OTLP| Logs + Router -->|S3| S3 + end + + subgraph Spoke1 [Project: telemetry_spoke1] + Link1([Link: spoke1-to-hub-link]) + end + + subgraph Spoke2 [Project: telemetry_spoke2] + Link2([Link: spoke2-to-hub-link]) + end + + subgraph Spoke3 [Project: telemetry_spoke3] + Link3([Link: spoke3-to-hub-link]) + end + end + end + + %% Connections from Links to Router + OrgLink -.->|Forward Organization Logs| Router + FolderLink -.->|Forward Folder Logs| Router + HubLink -.->|Forward Hub Logs| Router + Link1 -.->|Forward Spoke Logs| Router + Link2 -.->|Forward Spoke Logs| Router + Link3 -.->|Forward Spoke Logs| Router + + style Router fill:#f9f,stroke:#333,stroke-width:4px + style OrgLink fill:#bbf,stroke:#333 + style FolderLink fill:#bbf,stroke:#333 + style HubLink fill:#bbf,stroke:#333 + style Link1 fill:#bbf,stroke:#333 + style Link2 fill:#bbf,stroke:#333 + style Link3 fill:#bbf,stroke:#333 +``` + +## What this setup does + +1. **Centralizes Telemetry**: Creates a **Hub Project** that hosts a central Telemetry Router instance. +2. **Connects the Hierarchy**: Uses **Telemetry Links** at three different levels: + - **Organization Level**: Forwards organization-wide audit logs to the central router. + - **Folder Level**: Ensures all telemetry from a specific folder and its sub-projects is routed to the Hub. + - **Project Level**: Connects individual "Spoke" projects and the **Hub Project itself** to the Router. +3. **Broadcasts & Filters Data**: + - **Observability Destination**: All data is sent to a `stackit_observability_instance`. + - **Logs Destination (Filtered)**: Only logs from the **`service-account`** service are forwarded to the `stackit_logs_instance`. This demonstrates how to filter for specific high-value audit trails (like IAM actions). + - **S3 Archiving**: All data is also archived in a **STACKIT Object Storage (S3)** bucket for long-term retention. +4. **Generates Continuous Logs**: To demonstrate the setup, this example includes a **Log Generator** (`070-log-generator.tf`). It creates S3 credentials in every project and rotates them **every minute**. These frequent administrative actions trigger continuous Audit Logs, which you should see appearing in your Observability and Logs instances. +5. **Handles Authentication**: + - Uses a **Router Access Token** for the links to connect. + - Uses **Credentials/Access Tokens** for the router to push data to the backend Observability and Logs instances via OTLP. + +## Resource Architecture + +- **1 Organization**: Linked via an Org-level Telemetry Link. +- **1 Folder**: Contains all projects and is linked via a Folder-level Link. +- **1 Hub Project**: Contains the Router, Observability, Logs, and S3 Bucket instances. **⚠️⚠️⚠️ Crucially, it is also linked to its own Router.⚠️⚠️⚠️** +- **3 Spoke Projects**: Connected to the Hub via individual Project-level Links. + +## How to use + +1. Set your variables in a `terraform.tfvars` file (Org ID, Owner Email, etc.). +2. Initialize and apply: + ```bash + terraform init + terraform apply + ``` +3. Check the **outputs** for the Router URI and all Link IDs (Org, Folder, and Projects) to verify the connection. + +## Post-Deployment: Monitoring & Retrieval + +To avoid external dependencies during deployment, all scripts are located in the `scripts/` directory and should be run manually from within this folder. + +### Monitoring S3 Archive + +To check how many log objects are currently archived in S3: + +```bash +./scripts/count-s3-items.sh +``` + +This script retrieves the necessary credentials from the Terraform state and uses the AWS CLI to count the objects. + +### Log Retrieval, Extraction & Beautification + +To download, automatically unzip, and beautify all archived logs from S3: + +```bash +./scripts/download-s3-logs.sh +``` + +The script will: + +1. Download all logs from S3. +2. Unzip all `.gz` files. +3. Format all JSON files for better readability (using `jq`). + +The logs will be saved in the `scripts/downloads/` directory (which is ignored by Git). + +--- + +_Note: This service is currently in beta. `enable_beta_resources = true` is required in the provider configuration._ diff --git a/examples/telemetry-router-hub-spoke-setup/scripts/count-s3-items.sh b/examples/telemetry-router-hub-spoke-setup/scripts/count-s3-items.sh new file mode 100755 index 0000000..4d6c819 --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/scripts/count-s3-items.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# This script counts objects in a STACKIT S3 bucket. +# It automatically retrieves configuration and credentials from the terraform state. +# Requirements: aws cli, terraform, jq installed + +set -e + +echo "[*] Retrieving S3 configuration from Terraform state..." + +# Check if terraform state is available in current directory +if ! terraform output -json > /dev/null 2>&1; then + echo "Error: Could not read terraform output. Make sure you are in the terraform directory and have run 'terraform apply' first." + exit 1 +fi + +# Retrieve values from terraform output +ACCESS_KEY=$(terraform output -raw s3_access_key) +SECRET_KEY=$(terraform output -raw s3_secret_key) +ENDPOINT=$(terraform output -raw s3_endpoint) +BUCKET=$(terraform output -raw s3_archive_bucket) + +# Configure AWS CLI environment +export AWS_ACCESS_KEY_ID="$ACCESS_KEY" +export AWS_SECRET_ACCESS_KEY="$SECRET_KEY" +export AWS_DEFAULT_REGION="eu01" + +# Count objects using aws cli +# We use xargs to trim whitespace from wc output +COUNT=$(aws --endpoint-url "$ENDPOINT" s3 ls "s3://$BUCKET" --recursive | grep -v "^$" | wc -l | xargs) + +echo "[+] Current object count in s3://$BUCKET: $COUNT" diff --git a/examples/telemetry-router-hub-spoke-setup/scripts/download-s3-logs.sh b/examples/telemetry-router-hub-spoke-setup/scripts/download-s3-logs.sh new file mode 100755 index 0000000..d124bc6 --- /dev/null +++ b/examples/telemetry-router-hub-spoke-setup/scripts/download-s3-logs.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# This script downloads all objects from the STACKIT S3 archive bucket to a local directory, +# extracts compressed log files (.gz), and beautifies JSON content. +# +# It automatically retrieves configuration and credentials from the terraform state. +# Requirements: aws cli, terraform, jq, gunzip installed + +set -e + +echo "[*] Retrieving S3 configuration from Terraform state..." + +# Check if terraform state is available (script should be run from the terraform directory) +if ! terraform output -json > /dev/null 2>&1; then + echo "Error: Could not read terraform output. Make sure you have run 'terraform apply' first and are calling this script from the terraform directory." + exit 1 +fi + +# Retrieve values from terraform output +ACCESS_KEY=$(terraform output -raw s3_access_key) +SECRET_KEY=$(terraform output -raw s3_secret_key) +ENDPOINT=$(terraform output -raw s3_endpoint) +BUCKET=$(terraform output -raw s3_archive_bucket) + +# Get script directory to create downloads folder there +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOWNLOAD_DIR="$SCRIPT_DIR/downloads" + +# Create download directory +mkdir -p "$DOWNLOAD_DIR" + +# Configure AWS CLI environment +export AWS_ACCESS_KEY_ID="$ACCESS_KEY" +export AWS_SECRET_ACCESS_KEY="$SECRET_KEY" +export AWS_DEFAULT_REGION="eu01" + +echo "[*] Starting download from s3://$BUCKET to $DOWNLOAD_DIR..." +echo "[*] Endpoint: $ENDPOINT" + +# Use sync to download all files efficiently +aws --endpoint-url "$ENDPOINT" s3 sync "s3://$BUCKET" "$DOWNLOAD_DIR" + +echo "[*] Extracting compressed log files (.gz)..." +# Find all .gz files in the download directory and unzip them +find "$DOWNLOAD_DIR" -name "*.gz" -exec gunzip -f {} + + +echo "[*] Beautifying JSON log files..." +# Find all files (now uncompressed) and try to beautify them with jq if they contain JSON +# We use a temporary file to perform in-place beautification +find "$DOWNLOAD_DIR" -type f ! -name "*.gz" | while read -r file; do + if jq . "$file" > "$file.tmp" 2>/dev/null; then + mv "$file.tmp" "$file" + else + rm -f "$file.tmp" + fi +done + +echo "[+] Download, extraction, and beautification complete! Files are located in $DOWNLOAD_DIR" -- 2.49.1