diff --git a/.agents/skills/stackit-backplane.md b/.agents/skills/stackit-backplane.md new file mode 100644 index 00000000..80805d3e --- /dev/null +++ b/.agents/skills/stackit-backplane.md @@ -0,0 +1,165 @@ +--- +description: STACKIT backplane identity conventions for meshstack-hub modules under modules/stackit/. Covers service account + key pattern, required variables/outputs, provider configuration, meshstack_integration.tf wiring, and the STACKIT backplane checklist. +--- + +# STACKIT Backplane Identity Conventions + +STACKIT backplanes **must** use a **service account with a long-lived key** as the automation +principal for building block execution. The key JSON is provisioned in the backplane and injected +as a sensitive static input into the building block definition. + +## Rationale + +- **Self-contained credentials**: The service account and its key are provisioned once in the + backplane Terraform module. The key JSON is a single credential that bundles the service account + email, key ID, and private key — no extra wiring needed. +- **Least-privilege**: Each building block gets its own service account with exactly the roles it + needs (project-scoped or organization-scoped). +- **No provider configuration in backplane**: The backplane module does not include a `provider.tf`. + Authentication for the backplane itself is configured by the caller (e.g. the platform team running + `tofu apply` or the integration runtime). +- **Sensitive by default**: The `service_account_key_json` output is marked `sensitive = true`. + meshStack's STATIC input wiring uses the `sensitive.argument.secret_value` field to ensure the + key is stored and transmitted as a secret. + + +## Implementation Pattern + +```hcl +# backplane/main.tf — service account + key + role assignments + +resource "stackit_service_account" "backplane" { + project_id = var.project_id + name = "mesh-" +} + +resource "stackit_service_account_key" "backplane" { + project_id = var.project_id + service_account_email = stackit_service_account.backplane.email +} + +# Project-scoped role assignment (use this for project-level resources): +resource "stackit_authorization_project_role_assignment" "this" { + resource_id = var.project_id + role = "" + subject = stackit_service_account.backplane.email +} + +# Organization-scoped role assignment (use this for org-level resources): +resource "stackit_authorization_organization_role_assignment" "this" { + resource_id = var.organization_id + role = "" + subject = stackit_service_account.backplane.email +} +``` + + +## Backplane Outputs (STACKIT) + +Every STACKIT backplane must output the service account key JSON: + +```hcl +output "service_account_key_json" { + value = stackit_service_account_key.backplane.json + description = "Service account key JSON for authenticating the STACKIT provider in the buildingblock." + sensitive = true +} +``` + +Additional outputs (e.g. `project_id`, resource IDs) can be added as needed. + +## Backplane Variables (STACKIT) + +All STACKIT backplanes require at minimum: + +```hcl +variable "project_id" { + type = string + nullable = false + description = "STACKIT project ID where the service account will be created." +} +``` + +Backplanes that manage organization-level resources also require: + +```hcl +variable "organization_id" { + type = string + nullable = false + description = "STACKIT organization ID where the service account will be granted permissions." +} +``` + + +## Buildingblock Provider Configuration + +The buildingblock `provider.tf` must use `service_account_key` for authentication. +Do **not** use `service_account_email` alone — it does not authenticate. + +```hcl +# buildingblock/provider.tf +provider "stackit" { + service_account_key = var.service_account_key_json + # Add any extra provider flags required by the resources (e.g. enable_beta_resources, experiments): + # enable_beta_resources = true + # experiments = ["some-feature"] +} +``` + +## Buildingblock Variable + +```hcl +variable "service_account_key_json" { + type = string + nullable = false + sensitive = true + description = "Service account key JSON for authenticating the STACKIT provider." +} +``` + +The key JSON bundles the service account email — do **not** add a separate `service_account_email` +variable when `service_account_key_json` is present. + +## `meshstack_integration.tf` Wiring (STACKIT) + +Pass the key from the backplane as a **STATIC sensitive** input: + +```hcl +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/stackit//backplane?ref=${var.hub.git_ref}" + + project_id = var.stackit_project_id + # organization_id = var.stackit_organization_id # if org-scoped roles are needed +} + +# Inside meshstack_backplane_definition version_spec.inputs: +service_account_key_json = { + display_name = "Service Account Key JSON" + description = "Service account key JSON for authenticating the STACKIT provider." + type = "STRING" + assignment_type = "STATIC" + sensitive = { + argument = { + secret_value = module.backplane.service_account_key_json + } + } +} +``` + +## What to Avoid + +- ❌ `service_account_email` alone in the provider — missing authentication credential +- ❌ Long-lived `STACKIT_SERVICE_ACCOUNT_TOKEN` injected via env var — not reproducible across runs +- ❌ Hardcoded key values in integration files +- ❌ Non-sensitive output for `service_account_key_json` — always mark it `sensitive = true` + +## Checklist for STACKIT Backplanes + +- [ ] `stackit_service_account` resource present +- [ ] `stackit_service_account_key` resource present (same project as the service account) +- [ ] Required role assignments present (`stackit_authorization_project_role_assignment` or `stackit_authorization_organization_role_assignment`) +- [ ] `service_account_key_json` output marked `sensitive = true` +- [ ] Buildingblock `provider.tf` uses `service_account_key = var.service_account_key_json` +- [ ] Buildingblock `variables.tf` has `service_account_key_json` (sensitive, nullable = false) +- [ ] No separate `service_account_email` variable in buildingblock when key is present +- [ ] `meshstack_integration.tf` wires key via `sensitive.argument.secret_value` diff --git a/AGENTS.md b/AGENTS.md index 7ae13426..a839852c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,6 +201,10 @@ See [.agents/skills/aws-backplane.md](.agents/skills/aws-backplane.md) for the f See [.agents/skills/azure-backplane.md](.agents/skills/azure-backplane.md) for the full Azure backplane identity conventions, including UAMI patterns, WIF wiring, required variables/outputs, and the Azure backplane checklist. +## STACKIT Backplane Identity Conventions + +See [.agents/skills/stackit-backplane.md](.agents/skills/stackit-backplane.md) for the full STACKIT backplane identity conventions, including the service account + key pattern, required variables/outputs, provider configuration, and the STACKIT backplane checklist. + --- diff --git a/modules/stackit/meshstack_integration.tf b/modules/stackit/meshstack_integration.tf index 85bcf64b..c9981ed2 100644 --- a/modules/stackit/meshstack_integration.tf +++ b/modules/stackit/meshstack_integration.tf @@ -246,7 +246,7 @@ terraform { } stackit = { source = "stackitcloud/stackit" - version = "~> 0.89.0" + version = ">= 0.88.0" } } } diff --git a/modules/stackit/project/backplane/README.md b/modules/stackit/project/backplane/README.md index 17547f32..8b4c3587 100644 --- a/modules/stackit/project/backplane/README.md +++ b/modules/stackit/project/backplane/README.md @@ -33,7 +33,7 @@ module "project_backplane" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.11.0 | -| [stackit](#requirement\_stackit) | ~> 0.89.0 | +| [stackit](#requirement\_stackit) | >= 0.88.0 | ## Modules diff --git a/modules/stackit/project/backplane/versions.tf b/modules/stackit/project/backplane/versions.tf index 7187e1b5..706aef93 100644 --- a/modules/stackit/project/backplane/versions.tf +++ b/modules/stackit/project/backplane/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.89.0" + version = ">= 0.88.0" } } } diff --git a/modules/stackit/spoke-network/backplane/main.tf b/modules/stackit/spoke-network/backplane/main.tf new file mode 100644 index 00000000..afe5944f --- /dev/null +++ b/modules/stackit/spoke-network/backplane/main.tf @@ -0,0 +1,35 @@ +resource "stackit_service_account" "backplane" { + project_id = var.project_id + name = "mesh-spoke-network" +} + +resource "stackit_service_account_federated_identity_provider" "backplane" { + for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s } + + project_id = var.project_id + service_account_email = stackit_service_account.backplane.email + name = "meshstack-${each.key}" + issuer = var.workload_identity_federation.issuer + + assertions = [ + { + item = "aud" + operator = "equals" + value = "api://AzureADTokenExchange" + }, + { + item = "sub" + operator = "equals" + value = each.value + } + ] +} + +# network.admin at org scope allows managing routing tables in the network area +# and routed networks in tenant projects. Least-privilege alternative: if STACKIT +# introduces a narrower "network.editor" role, prefer that. +resource "stackit_authorization_organization_role_assignment" "network_admin" { + resource_id = var.organization_id + role = "iaas.network.admin" + subject = stackit_service_account.backplane.email +} diff --git a/modules/stackit/spoke-network/backplane/outputs.tf b/modules/stackit/spoke-network/backplane/outputs.tf new file mode 100644 index 00000000..6d706dde --- /dev/null +++ b/modules/stackit/spoke-network/backplane/outputs.tf @@ -0,0 +1,4 @@ +output "service_account_email" { + value = stackit_service_account.backplane.email + description = "Email of the STACKIT service account used by the buildingblock provider via WIF." +} diff --git a/modules/stackit/spoke-network/backplane/variables.tf b/modules/stackit/spoke-network/backplane/variables.tf new file mode 100644 index 00000000..751832eb --- /dev/null +++ b/modules/stackit/spoke-network/backplane/variables.tf @@ -0,0 +1,20 @@ +variable "project_id" { + type = string + nullable = false + description = "STACKIT project ID where the service account will be created." +} + +variable "organization_id" { + type = string + nullable = false + description = "STACKIT organization ID where the service account will be granted network management permissions." +} + +variable "workload_identity_federation" { + type = object({ + issuer = string + subjects = list(string) + }) + nullable = false + description = "WIF issuer URL and subject list for the meshStack building block identity provider." +} diff --git a/modules/stackit/spoke-network/backplane/versions.tf b/modules/stackit/spoke-network/backplane/versions.tf new file mode 100644 index 00000000..9d46ff3a --- /dev/null +++ b/modules/stackit/spoke-network/backplane/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.98.0" + } + } +} diff --git a/modules/stackit/spoke-network/buildingblock/README.md b/modules/stackit/spoke-network/buildingblock/README.md new file mode 100644 index 00000000..b1e6c656 --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/README.md @@ -0,0 +1,31 @@ +--- +name: STACKIT Spoke Network +supportedPlatforms: + - stackit +description: Provisions a routed network in a STACKIT project and attaches it to the platform hub network area. +--- + +# STACKIT Spoke Network — Building Block + +Provisions a routed network in a STACKIT project and attaches it to the platform hub network area. Optionally creates a custom routing table with a default route via a firewall next-hop. + +## Inputs + +| Name | Type | Description | +|------|------|-------------| +| `project_id` | string | Tenant STACKIT project ID (from PLATFORM_TENANT_ID) | +| `organization_id` | string | STACKIT organization ID | +| `network_area_id` | string | Hub network area ID | +| `service_account_key_json` | string (sensitive) | Backplane SA credentials | +| `network_prefix_length` | number | Subnet prefix length (24–28, default 25) | +| `firewall_next_hop_ip` | string | Next-hop IP for default route; null = no routing table | +| `ipv4_nameservers` | string | JSON-encoded nameserver list; null = STACKIT defaults | + +## Outputs + +| Name | Description | +|------|-------------| +| `network_id` | Spoke network ID | +| `network_cidr` | Allocated CIDR block | +| `routing_table_id` | Custom routing table ID (null if no firewall) | +| `summary` | Markdown summary rendered in meshStack | diff --git a/modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl b/modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl new file mode 100644 index 00000000..62e50d96 --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl @@ -0,0 +1,10 @@ +# Spoke Network + +| Property | Value | +|----------|-------| +| **Network ID** | `${network_id}` | +| **Network CIDR** | `${network_cidr}` | +| **Hub Network Area** | `${network_area_id}` | +%{~ if has_routing_table} +| **Routing Table** | `${routing_table_id}` | +%{~ endif} diff --git a/modules/stackit/spoke-network/buildingblock/logo.png b/modules/stackit/spoke-network/buildingblock/logo.png new file mode 100644 index 00000000..0894e560 Binary files /dev/null and b/modules/stackit/spoke-network/buildingblock/logo.png differ diff --git a/modules/stackit/spoke-network/buildingblock/main.tf b/modules/stackit/spoke-network/buildingblock/main.tf new file mode 100644 index 00000000..98e855b0 --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/main.tf @@ -0,0 +1,29 @@ +locals { + nameservers = var.ipv4_nameservers != null && var.ipv4_nameservers != "" ? split(",", var.ipv4_nameservers) : null +} + +resource "stackit_routing_table" "this" { + count = var.firewall_next_hop_ip != null ? 1 : 0 + organization_id = var.organization_id + network_area_id = var.network_area_id + name = "spoke-${var.project_id}" + system_routes = false +} + +resource "stackit_routing_table_route" "this" { + count = var.firewall_next_hop_ip != null ? 1 : 0 + organization_id = var.organization_id + network_area_id = var.network_area_id + routing_table_id = stackit_routing_table.this[0].routing_table_id + destination = { type = "cidrv4", value = "0.0.0.0/0" } + next_hop = { type = "ipv4", value = var.firewall_next_hop_ip } +} + +resource "stackit_network" "this" { + project_id = var.project_id + name = "spoke-routed" + ipv4_prefix_length = var.network_prefix_length + ipv4_nameservers = local.nameservers + routed = true + routing_table_id = var.firewall_next_hop_ip != null ? stackit_routing_table.this[0].routing_table_id : null +} diff --git a/modules/stackit/spoke-network/buildingblock/outputs.tf b/modules/stackit/spoke-network/buildingblock/outputs.tf new file mode 100644 index 00000000..7f864b37 --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/outputs.tf @@ -0,0 +1,25 @@ +output "network_id" { + value = stackit_network.this.network_id + description = "ID of the spoke network." +} + +output "network_cidr" { + value = stackit_network.this.ipv4_prefix + description = "Allocated IPv4 CIDR block of the spoke network." +} + +output "routing_table_id" { + value = var.firewall_next_hop_ip != null ? stackit_routing_table.this[0].routing_table_id : null + description = "ID of the custom routing table, or null if no firewall next-hop is configured." +} + +output "summary" { + description = "Summary with spoke network details." + value = templatefile("${path.module}/SUMMARY.md.tftpl", { + network_id = stackit_network.this.network_id + network_cidr = stackit_network.this.ipv4_prefix + network_area_id = var.network_area_id + has_routing_table = var.firewall_next_hop_ip != null + routing_table_id = var.firewall_next_hop_ip != null ? stackit_routing_table.this[0].routing_table_id : "" + }) +} diff --git a/modules/stackit/spoke-network/buildingblock/provider.tf b/modules/stackit/spoke-network/buildingblock/provider.tf new file mode 100644 index 00000000..1db1937b --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/provider.tf @@ -0,0 +1,6 @@ +provider "stackit" { + service_account_email = var.service_account_email + use_oidc = true + enable_beta_resources = true + experiments = ["routing-tables", "network"] +} diff --git a/modules/stackit/spoke-network/buildingblock/variables.tf b/modules/stackit/spoke-network/buildingblock/variables.tf new file mode 100644 index 00000000..e8300694 --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/variables.tf @@ -0,0 +1,52 @@ +# ── Backplane inputs (static, set once per building block definition) ────────── + +variable "service_account_email" { + type = string + nullable = false + description = "Email of the STACKIT service account for WIF-based authentication." +} + +variable "project_id" { + type = string + nullable = false + description = "STACKIT project ID of the application team's tenant (injected from PLATFORM_TENANT_ID)." +} + +variable "organization_id" { + type = string + nullable = false + description = "STACKIT organization ID." +} + +variable "network_area_id" { + type = string + nullable = false + description = "STACKIT network area ID of the platform hub. The spoke network will be attached to this area." +} + +variable "firewall_next_hop_ip" { + type = string + default = null + description = "IPv4 address of the firewall next-hop. When set, creates a routing table with a 0.0.0.0/0 default route via this address." +} + +# ── User inputs (set per building block instance) ───────────────────────────── + +variable "network_prefix_length" { + type = number + default = 25 + nullable = false + description = "IPv4 prefix length for the spoke network (24–28). Controls subnet size: /24 = 254 hosts, /25 = 126, /26 = 62, /27 = 30, /28 = 14." + + validation { + condition = var.network_prefix_length >= 24 && var.network_prefix_length <= 28 + error_message = "network_prefix_length must be between 24 and 28 (inclusive)." + } +} + +variable "ipv4_nameservers" { + type = string + default = null + nullable = true + description = "Comma-separated list of IPv4 DNS nameservers, e.g. '8.8.8.8,8.8.4.4'. Leave null to use STACKIT defaults." +} diff --git a/modules/stackit/spoke-network/buildingblock/versions.tf b/modules/stackit/spoke-network/buildingblock/versions.tf new file mode 100644 index 00000000..990450fb --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.96.0" + } + } +} diff --git a/modules/stackit/spoke-network/e2e/main.tf b/modules/stackit/spoke-network/e2e/main.tf new file mode 100644 index 00000000..30e4418c --- /dev/null +++ b/modules/stackit/spoke-network/e2e/main.tf @@ -0,0 +1,62 @@ +variable "stackit_service_account_key" { + type = string + nullable = false + sensitive = true +} + +variable "test_context" { + type = object({ + hub_git_ref = string + workspace = string + project = string + name_suffix = string + + fixtures = object({ + stackit = object({ + project_id = string + mesh_tenant_id = string + organization_id = string + network_area_id = string + }) + }) + }) + nullable = false +} + +provider "stackit" { + service_account_key = var.stackit_service_account_key + experiments = ["iam"] +} + +module "stackit_spoke_network" { + source = "../" + meshstack = { + owning_workspace_identifier = var.test_context.workspace + tags = {} + } + hub = { + git_ref = var.test_context.hub_git_ref + bbd_draft = true + } + + stackit_project_id = var.test_context.fixtures.stackit.project_id + stackit_organization_id = var.test_context.fixtures.stackit.organization_id + network_area_id = var.test_context.fixtures.stackit.network_area_id +} + +resource "meshstack_building_block_v2" "this" { + wait_for_completion = true + spec = { + building_block_definition_version_ref = module.stackit_spoke_network.building_block_definition.version_ref + + display_name = "smoke-test-spoke-network-${var.test_context.name_suffix}" + target_ref = { + kind = "meshTenant" + uuid = var.test_context.fixtures.stackit.mesh_tenant_id + } + + inputs = { + network_prefix_length = { value_string = "28" } + } + } +} diff --git a/modules/stackit/spoke-network/e2e/terraform.tf b/modules/stackit/spoke-network/e2e/terraform.tf new file mode 100644 index 00000000..6a7a402f --- /dev/null +++ b/modules/stackit/spoke-network/e2e/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + } + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.98.0" + } + } +} diff --git a/modules/stackit/spoke-network/e2e/tests/stackit_spoke_network_hub.tftest.hcl b/modules/stackit/spoke-network/e2e/tests/stackit_spoke_network_hub.tftest.hcl new file mode 100644 index 00000000..7fafafaf --- /dev/null +++ b/modules/stackit/spoke-network/e2e/tests/stackit_spoke_network_hub.tftest.hcl @@ -0,0 +1,16 @@ +run "stackit_spoke_network_hub" { + assert { + condition = meshstack_building_block_v2.this.status.status == "SUCCEEDED" + error_message = "stackit spoke-network hub building block expected SUCCEEDED, got ${meshstack_building_block_v2.this.status.status}" + } + + assert { + condition = length(meshstack_building_block_v2.this.status.outputs["network_id"].value_string) > 0 + error_message = "stackit spoke-network hub building block expected non-empty network_id" + } + + assert { + condition = strcontains(meshstack_building_block_v2.this.status.outputs["network_cidr"].value_string, "/28") + error_message = "stackit spoke-network hub building block expected network_cidr to contain /28, got ${meshstack_building_block_v2.this.status.outputs["network_cidr"].value_string}" + } +} diff --git a/modules/stackit/spoke-network/meshstack_integration.tf b/modules/stackit/spoke-network/meshstack_integration.tf new file mode 100644 index 00000000..fa5ac3f2 --- /dev/null +++ b/modules/stackit/spoke-network/meshstack_integration.tf @@ -0,0 +1,248 @@ +variable "stackit_project_id" { + type = string + description = "STACKIT project ID where the backplane service account will be created." +} + +variable "stackit_organization_id" { + type = string + description = "STACKIT organization ID." +} + +variable "network_area_id" { + type = string + description = "STACKIT network area ID (from LZA hub) used for spoke network attachment." +} + +variable "firewall_next_hop_ip" { + type = string + default = null + description = "IPv4 address of the firewall next-hop. Pass null if no firewall is configured (route-optional)." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = {} + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of meshcloud/meshstack-hub repo. + `bbd_draft`: If true, allows changing the building block definition for upgrading dependent building blocks. + EOT +} + +output "building_block_definition" { + description = "BBD is consumed in building block compositions." + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } +} + +data "meshstack_integrations" "integrations" {} + +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/stackit/spoke-network/backplane?ref=${var.hub.git_ref}" + + project_id = var.stackit_project_id + organization_id = var.stackit_organization_id + + workload_identity_federation = { + issuer = data.meshstack_integrations.integrations.workload_identity_federation.replicator.issuer + subjects = [ + "${trimsuffix(data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject, ":replicator")}:workspace.${var.meshstack.owning_workspace_identifier}.buildingblockdefinition.${meshstack_building_block_definition.this.metadata.uuid}" + ] + } +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = "STACKIT Spoke Network" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/stackit/spoke-network/buildingblock/logo.png" + description = "Provisions a routed network in an application team's STACKIT project and attaches it to the platform hub network area." + target_type = "TENANT_LEVEL" + supported_platforms = [{ name = "STACKIT" }] + run_transparency = true + readme = chomp(<<-EOT + This building block provisions a **routed STACKIT network** in your project and attaches it + to the shared platform hub via the network area, enabling corporate connectivity and controlled + internet egress. + + ## 🎯 When to use it + + Use this building block when your application: + - Needs to communicate with other corporate workloads over private IP. + - Should route internet traffic through the platform firewall (when one is configured). + - Requires a dedicated IPv4 subnet within the STACKIT project. + + ## 💡 Usage examples + + **Example 1: Backend service on corporate network** + A microservice needs to call an on-premises API over private IP. Adding the Spoke Network + building block provisions a /25 subnet in your STACKIT project and connects it to the hub, + enabling private routing without exposing the service to the public internet. + + **Example 2: Controlled internet egress** + When the platform firewall is enabled, all outbound traffic from the spoke network passes + through it, allowing the platform team to enforce egress policies across all application teams. + + ## 📊 Shared Responsibility + + | Responsibility | Platform Team | Application Team | + |---|:---:|:---:| + | Provision the routed network | ✅ | ❌ | + | Attach network to hub network area | ✅ | ❌ | + | Configure routing table (when firewall present) | ✅ | ❌ | + | Choose network prefix length | ❌ | ✅ | + | Deploy workloads within the network | ❌ | ✅ | + | Manage security groups and firewall rules per VM | ❌ | ✅ | + EOT + ) + } + + version_spec = { + draft = var.hub.bbd_draft + deletion_mode = "DELETE" + + implementation = { + terraform = { + terraform_version = "1.11.0" + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/stackit/spoke-network/buildingblock" + ref_name = var.hub.git_ref + async = false + use_mesh_http_backend_fallback = true + } + } + + inputs = { + project_id = { + display_name = "STACKIT Project ID" + description = "STACKIT project ID of the application team's tenant (set automatically from platform tenant identity)." + type = "STRING" + assignment_type = "PLATFORM_TENANT_ID" + } + + organization_id = { + display_name = "Organization ID" + description = "STACKIT organization ID." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.stackit_organization_id) + } + + network_area_id = { + display_name = "Network Area ID" + description = "STACKIT network area ID of the platform hub." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.network_area_id) + } + + service_account_email = { + display_name = "Service Account Email" + description = "Email of the STACKIT service account for WIF-based authentication." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(module.backplane.service_account_email) + } + + STACKIT_USE_OIDC = { + display_name = "STACKIT Use OIDC" + description = "Enables OIDC-based WIF for the STACKIT provider." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("1") + } + + STACKIT_FEDERATED_TOKEN_FILE = { + display_name = "STACKIT Federated Token File" + description = "Path to the WIF token file injected by meshStack." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("/var/run/secrets/workload-identity/azure/token") + } + + # firewall_next_hop_ip = { + # display_name = "Firewall Next-Hop IP" + # description = "IPv4 address of the firewall next-hop. Null if no firewall is configured." + # type = "STRING" + # assignment_type = "STATIC" + # argument = jsonencode(var.firewall_next_hop_ip) + # } + + network_prefix_length = { + display_name = "Network Prefix Length" + description = "IPv4 prefix length for the spoke network (24–28). Determines subnet size: /24 = 254 hosts, /25 = 126, /26 = 62, /27 = 30, /28 = 14." + type = "INTEGER" + assignment_type = "USER_INPUT" + default_value = "25" + value_validation_regex = "^(24|25|26|27|28)$" + validation_regex_error_message = "Prefix length must be between 24 and 28." + } + + ipv4_nameservers = { + display_name = "DNS Nameservers" + description = "Comma-separated list of IPv4 DNS nameservers, e.g. '8.8.8.8,8.8.4.4'. Leave blank to use STACKIT defaults." + type = "STRING" + assignment_type = "USER_INPUT" + } + } + + outputs = { + network_id = { + display_name = "Network ID" + type = "STRING" + assignment_type = "NONE" + } + + network_cidr = { + display_name = "Network CIDR" + type = "STRING" + assignment_type = "NONE" + } + + routing_table_id = { + display_name = "Routing Table ID" + type = "STRING" + assignment_type = "NONE" + } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } + } + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.21.0" + } + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.98.0" + } + } +}