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-private-internal-endpoint`** `[iaas, alb, load-balancer, layer7, tls, pki, cert, encryption, networking, ha, cross-az]`
An internal-only STACKIT Application Load Balancer: no public address, load balancer and backends in separate networks, the target security group of the load balancer assigned by the configuration instead of by the service, and TLS from the client to the load balancer and again from the load balancer to the backends, verified against a private CA
- **`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-private-internal-endpoint/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
}
133 changes: 133 additions & 0 deletions examples/alb-private-internal-endpoint/020-variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 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-priv"

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 "internal_domain" {
description = "Domain of the internal API. The listener routes api.<internal_domain>; the backend certificates carry <backend name>.<internal_domain>. No DNS zone is created."
type = string
default = "internal.example"

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

variable "listener_network_cidr" {
description = "IPv4 prefix of the network that carries the listener of the load balancer and the jump host. 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.2.0/24"

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

variable "target_network_cidr" {
description = "IPv4 prefix of the network that hosts the backend VMs. Must not overlap the listener network and, in a Network Area, must lie inside its network ranges."
type = string
default = "10.20.3.0/24"

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

variable "availability_zones" {
description = "Availability zones for the backend VMs. One VM is created per zone."
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 and the jump host."
type = string
default = "c2i.1"
}

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

variable "boot_volume_size_gb" {
description = "Boot volume size of each 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 "admin_cidr" {
description = "Source CIDR that may reach the jump host over SSH, e.g. your egress address as 203.0.113.10/32. Avoid 0.0.0.0/0."
type = string

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

variable "ssh_public_key" {
description = "SSH public key that is registered for the jump host, e.g. the content of ~/.ssh/id_ed25519.pub."
type = string

validation {
condition = can(regex("^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp[0-9]+) ", var.ssh_public_key))
error_message = "The ssh_public_key must be an OpenSSH public key (ssh-ed25519, ssh-rsa or ecdsa-sha2-*)."
}
}
54 changes: 54 additions & 0 deletions examples/alb-private-internal-endpoint/030-locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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-private-internal-endpoint"
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 }
backend_names = { for key, az in local.backends : key => "${var.name_prefix}-backend-${key}" }

# The backends get fixed addresses. The load balancer needs the target
# addresses when it is created, and the backend interfaces in turn need the
# target security group that the load balancer creates, so the addresses
# cannot be read from the interfaces. The offset leaves the first addresses
# of the network to the gateway and the DHCP service.
backend_ips = { for key, az in local.backends : key => cidrhost(var.target_network_cidr, 10 + tonumber(key)) }

api_host = "api.${var.internal_domain}"

# The backends serve two HTTPS ports: one with a certificate issued by the
# private CA of this example, one with a self-signed certificate created on
# the VM, which the load balancer must refuse.
backend_port = 8443
untrusted_port = 8444

pool_backends = "${var.name_prefix}-backends"
pool_untrusted = "${var.name_prefix}-untrusted"

active_health_check = {
interval = "5s"
interval_jitter = "1s"
timeout = "3s"
healthy_threshold = 2
unhealthy_threshold = 2
http_health_checks = {
path = "/healthz"
ok_status = ["200"]
}
}
}
72 changes: 72 additions & 0 deletions examples/alb-private-internal-endpoint/040-network.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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.

# The listener of the load balancer and the jump host live in one network,
# the backends in another. Both are routed networks of the same project, so
# traffic between them is forwarded by the project router.
resource "stackit_network" "listener" {
project_id = var.stackit_project_id
name = "${var.name_prefix}-listener"
ipv4_prefix = var.listener_network_cidr
ipv4_nameservers = ["1.1.1.1", "9.9.9.9"]
labels = local.labels
}

resource "stackit_network" "target" {
project_id = var.stackit_project_id
name = "${var.name_prefix}-target"
ipv4_prefix = var.target_network_cidr
ipv4_nameservers = ["1.1.1.1", "9.9.9.9"]
labels = local.labels
}

resource "stackit_security_group" "jumphost" {
project_id = var.stackit_project_id
name = "${var.name_prefix}-jumphost"
description = "Jump host of the ${var.name_prefix} example"
stateful = true
labels = local.labels
}

resource "stackit_security_group_rule" "jumphost_ssh" {
project_id = var.stackit_project_id
security_group_id = stackit_security_group.jumphost.security_group_id
direction = "ingress"
description = "SSH from the admin address range"
protocol = { name = "tcp" }
port_range = { min = 22, max = 22 }
ip_range = var.admin_cidr
}

# Traffic from the load balancer is permitted by the target security group
# that the load balancer exports (see 060-backends.tf). This group covers
# everything else the backends need: outbound traffic for cloud-init and
# direct access from the jump host for inspecting the backend certificates.
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_https_from_jumphost" {
project_id = var.stackit_project_id
security_group_id = stackit_security_group.backend.security_group_id
direction = "ingress"
description = "Backend HTTPS ports, reachable from the jump host"
protocol = { name = "tcp" }
port_range = { min = local.backend_port, max = local.untrusted_port }
remote_security_group_id = stackit_security_group.jumphost.security_group_id
}
65 changes: 65 additions & 0 deletions examples/alb-private-internal-endpoint/050-jumphost.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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" "debian" {
project_id = var.stackit_project_id
name = var.image_name
}

# The jump host is the only resource with a public address. It exists so that
# the private load balancer can be tested without a VPN; in production the
# clients come from on-premises or from other projects instead.
resource "stackit_key_pair" "jumphost" {
name = "${var.name_prefix}-jumphost"
public_key = chomp(var.ssh_public_key)
labels = local.labels
}

resource "stackit_network_interface" "jumphost" {
project_id = var.stackit_project_id
network_id = stackit_network.listener.network_id
name = "${var.name_prefix}-jumphost"
security = true
security_group_ids = [stackit_security_group.jumphost.security_group_id]
}

resource "stackit_public_ip" "jumphost" {
project_id = var.stackit_project_id
network_interface_id = stackit_network_interface.jumphost.network_interface_id
labels = local.labels
}

resource "stackit_server" "jumphost" {
project_id = var.stackit_project_id
name = "${var.name_prefix}-jumphost"
availability_zone = var.availability_zones[0]
machine_type = var.machine_type
keypair_name = stackit_key_pair.jumphost.name
labels = local.labels

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

network_interfaces = [stackit_network_interface.jumphost.network_interface_id]

# The jump host trusts the private CA, so curl and openssl can verify the
# certificates of the load balancer and the backends without extra options.
user_data = templatefile("${path.module}/jumphost-cloud-init.yaml.tftpl", {
ca_cert = tls_self_signed_cert.ca.cert_pem
})
}
Loading
Loading