example(telemetry-router): build hub and spoke architecture for logs #30

Merged
mauritz.uphoff merged 1 commit from telemetry-router-example into main 2026-06-01 18:19:59 +00:00
16 changed files with 784 additions and 1 deletions

3
.gitignore vendored
View file

@ -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/

View file

@ -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",
]
}

View file

@ -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"]
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"]
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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.

View file

@ -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._

View file

@ -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"

View file

@ -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"