Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ Fetch only the files relevant to the task. A typical example contains

## Examples

- **`alb-multi-tenant-routing`** `[iaas, alb, load-balancer, layer7, routing, tls, sni, ha, cross-az, canary, websocket]`
One STACKIT Application Load Balancer in front of several applications: two hostnames on a single HTTPS listener, path, header and query parameter rules, cookie persistence, WebSocket, and a target pool with its own health check per application
- **`alb-tls-examples`** `[alb, tls, certificate, load-balancer, lets-encrypt, iaas, ske]`
A collection of STACKIT Application Load Balancer (ALB) showcases with different TLS strategies — from self-signed to Let's Encrypt, from a single VM to Kubernetes
- **`cdn-s3-static-website`** `[cdn, s3, object-storage, static-website, waf]`
Expand Down
34 changes: 34 additions & 0 deletions examples/alb-multi-tenant-routing/010-provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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_version = ">= 1.5.0"
required_providers {
stackit = {
source = "stackitcloud/stackit"
version = ">= 0.113.0"
}
tls = {
source = "hashicorp/tls"
version = ">= 4.0.0"
}
}
}

provider "stackit" {
default_region = var.stackit_region
service_account_key_path = var.stackit_service_account_key_path
# required for the stackit_image_v2 data source
enable_beta_resources = true
}
119 changes: 119 additions & 0 deletions examples/alb-multi-tenant-routing/020-variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 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_project_id" {
description = "The STACKIT project ID to deploy resources into."
type = string

validation {
condition = can(regex("^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$", var.stackit_project_id))
error_message = "The stackit_project_id must be a valid UUID."
}
}

variable "stackit_region" {
description = "The STACKIT region to deploy resources into."
type = string
default = "eu01"
}

variable "stackit_service_account_key_path" {
description = "Path to the STACKIT service account key JSON file used for provider authentication."
type = string
}

variable "name_prefix" {
description = "Prefix applied to the names of all resources. Lowercase letters, digits and hyphens only."
type = string
default = "alb-mt"

validation {
condition = can(regex("^[a-z0-9]+(-[a-z0-9]+)*$", var.name_prefix)) && length(var.name_prefix) <= 20
error_message = "The name_prefix must be 1-20 characters of lowercase letters, digits and single hyphens, starting and ending with a letter or digit."
}
}

variable "domain" {
description = "Domain under which the applications are published. The listener routes app.<domain> and admin.<domain>. No DNS zone is created; the hostnames are resolved on the client with curl --resolve."
type = string
default = "example.internal"

validation {
condition = can(regex("^[a-z0-9]+([-.][a-z0-9]+)*$", var.domain))
error_message = "The domain must consist of lowercase letters, digits, hyphens and dots."
}
}

variable "network_cidr" {
description = "IPv4 prefix of the private network that hosts the backends and the load balancer. In a project that belongs to a STACKIT Network Area the prefix must lie inside the network ranges of that area."
type = string
default = "10.20.1.0/24"

validation {
condition = can(cidrnetmask(var.network_cidr))
error_message = "The network_cidr must be a valid IPv4 CIDR, e.g. 10.20.1.0/24."
}
}

variable "availability_zones" {
description = "Availability zones for the backend VMs. One VM is created per zone, and every VM is a target in every pool."
type = list(string)
default = ["eu01-1", "eu01-2"]

validation {
condition = length(var.availability_zones) >= 1 && length(var.availability_zones) <= 3
error_message = "Provide between one and three availability zones."
}
}

variable "machine_type" {
description = "Machine type of the backend VMs."
type = string
default = "c2i.1"
}

variable "image_name" {
description = "Name of the boot image for the backend VMs, resolved via the stackit_image_v2 data source. The image must ship python3."
type = string
default = "Debian 12"
}

variable "boot_volume_size_gb" {
description = "Boot volume size of each backend VM in GB."
type = number
default = 20
}

variable "alb_plan_id" {
description = "Service plan of the Application Load Balancer. List the plans of your region with `stackit beta alb plans`; p10 is the only plan available at the time of writing."
type = string
default = "p10"
}

variable "alb_allowed_source_ranges" {
description = "Source CIDRs that may reach the load balancer listener."
type = list(string)
default = ["0.0.0.0/0"]
}

variable "session_cookie_ttl" {
description = "Time-to-live of the persistence cookie the load balancer sets for the admin application, as whole seconds with an s suffix. 0s creates a session cookie that expires with the client session."
type = string
default = "300s"

validation {
condition = can(regex("^[0-9]{1,8}s$", var.session_cookie_ttl))
error_message = "The session_cookie_ttl must be a whole number of seconds followed by s, e.g. 300s or 0s."
}
}
61 changes: 61 additions & 0 deletions examples/alb-multi-tenant-routing/030-locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.

locals {
labels = {
example = "alb-multi-tenant-routing"
managed-by = "terraform"
}

# One backend VM per availability zone, keyed by a two-digit index.
backends = { for idx, az in var.availability_zones : format("%02d", idx + 1) => az }

# The hostnames the listener routes. Both share the listener and the
# public IP; the load balancer selects the certificate by SNI.
app_host = "app.${var.domain}"
admin_host = "admin.${var.domain}"

# One application per target pool. Every backend VM runs all of them, each
# on its own port, so every pool contains every VM and two machines are
# enough to make every route highly available.
applications = {
web = { port = 8081 }
api = { port = 8082 }
admin = { port = 8083 }
canary = { port = 8084 }
}

pool_names = { for name, app in local.applications : name => "${var.name_prefix}-${name}" }

# Command line of the backend application: one <port>=<pool> pair per
# application, so that the port decides which pool a response reports.
backend_arguments = join(" ", [for name, app in local.applications : "${app.port}=${name}"])

backend_port_min = min([for app in values(local.applications) : app.port]...)
backend_port_max = max([for app in values(local.applications) : app.port]...)

# Health check shared by all target pools. The backend answers /healthz
# with 200 while healthy and 503 after it has been told to fail.
active_health_check = {
interval = "5s"
interval_jitter = "1s"
timeout = "3s"
healthy_threshold = 2
unhealthy_threshold = 2
http_health_checks = {
path = "/healthz"
ok_status = ["200"]
}
}
}
43 changes: 43 additions & 0 deletions examples/alb-multi-tenant-routing/040-network.tf
Original file line number Diff line number Diff line change
@@ -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_network" "this" {
project_id = var.stackit_project_id
name = "${var.name_prefix}-network"
ipv4_prefix = var.network_cidr
ipv4_nameservers = ["1.1.1.1", "9.9.9.9"]
labels = local.labels
}

# The load balancer attaches its own target security group to the backend
# interfaces. That group only allows traffic from the load balancer, so the
# backends get a group of their own. A new security group permits all outbound
# traffic by default, which cloud-init needs to reach the metadata service.
resource "stackit_security_group" "backend" {
project_id = var.stackit_project_id
name = "${var.name_prefix}-backend"
description = "Backend VMs of the ${var.name_prefix} load balancer"
stateful = true
labels = local.labels
}

resource "stackit_security_group_rule" "backend_http" {
project_id = var.stackit_project_id
security_group_id = stackit_security_group.backend.security_group_id
direction = "ingress"
description = "Application ports, reachable from inside the network"
protocol = { name = "tcp" }
port_range = { min = local.backend_port_min, max = local.backend_port_max }
ip_range = var.network_cidr
}
58 changes: 58 additions & 0 deletions examples/alb-multi-tenant-routing/050-machines.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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.

data "stackit_image_v2" "backend" {
project_id = var.stackit_project_id
name = var.image_name
}

resource "stackit_network_interface" "backend" {
for_each = local.backends

project_id = var.stackit_project_id
network_id = stackit_network.this.network_id
name = "${var.name_prefix}-backend-${each.key}"
security = true

security_group_ids = [stackit_security_group.backend.security_group_id]

# The load balancer adds its own target security group to the interface.
lifecycle {
ignore_changes = [security_group_ids]
}
}

resource "stackit_server" "backend" {
for_each = local.backends

project_id = var.stackit_project_id
name = "${var.name_prefix}-backend-${each.key}"
availability_zone = each.value
machine_type = var.machine_type
labels = local.labels

boot_volume = {
source_type = "image"
source_id = data.stackit_image_v2.backend.image_id
size = var.boot_volume_size_gb
delete_on_termination = true
}

network_interfaces = [stackit_network_interface.backend[each.key].network_interface_id]

user_data = templatefile("${path.module}/cloud-init.yaml.tftpl", {
server_py = file("${path.module}/files/server.py")
backend_arguments = local.backend_arguments
})
}
60 changes: 60 additions & 0 deletions examples/alb-multi-tenant-routing/060-certificates.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 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.

# One self-signed certificate per hostname. Both are attached to the same
# HTTPS listener; the load balancer presents the one whose name matches the
# SNI of the TLS handshake.
locals {
certificate_hosts = {
app = local.app_host
admin = local.admin_host
}
}

resource "tls_private_key" "this" {
for_each = local.certificate_hosts

algorithm = "RSA"
rsa_bits = 2048
}

resource "tls_self_signed_cert" "this" {
for_each = local.certificate_hosts

private_key_pem = tls_private_key.this[each.key].private_key_pem

subject {
common_name = each.value
organization = "STACKIT Example"
}

dns_names = [each.value]
validity_period_hours = 8760

allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
]
}

resource "stackit_alb_certificate" "this" {
for_each = local.certificate_hosts

project_id = var.stackit_project_id
region = var.stackit_region
name = "${var.name_prefix}-${each.key}"
private_key = tls_private_key.this[each.key].private_key_pem
public_key = tls_self_signed_cert.this[each.key].cert_pem
}
Loading
Loading