From bf64470362865ce3fa393f44730e986e17998c77 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 15:02:40 +0200 Subject: [PATCH 01/63] Adds the new `disable_security_group_assignment` boolean attribute to the load balancer resource. This allows disabling the automatic assignment of a security group to the targets. --- docs/data-sources/loadbalancer.md | 1 + docs/resources/loadbalancer.md | 1 + .../loadbalancer/loadbalancer/datasource.go | 5 ++ .../loadbalancer/loadbalancer/resource.go | 51 ++++++++++++------- .../loadbalancer/loadbalancer_acc_test.go | 27 ++++++---- .../loadbalancer/testfiles/resource-max.tf | 2 + .../loadbalancer/testfiles/resource-min.tf | 2 +- 7 files changed, 59 insertions(+), 30 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index c59fc8428..65cc06851 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -33,6 +33,7 @@ data "stackit_loadbalancer" "example" { ### Read-Only +- `disable_security_group_assignment` (Boolean) Disables the target security group assignment. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 18c156515..42d28c27f 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -124,6 +124,7 @@ resource "stackit_loadbalancer" "example" { ### Optional +- `disable_security_group_assignment` (Boolean) If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index d7f3a2157..d5e3f4f20 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -69,6 +69,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance.", @@ -125,6 +126,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["external_address"], Computed: true, }, + "disable_security_group_assignment": schema.BoolAttribute{ + Description: "Disables the target security group assignment.", + Computed: true, + }, "plan_id": schema.StringAttribute{ Description: descriptions["plan_id"], Computed: true, diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 36e0e1f74..0f6b596b5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,6 +3,7 @@ package loadbalancer import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -48,17 +49,18 @@ var ( ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - ExternalAddress types.String `tfsdk:"external_address"` - Listeners types.List `tfsdk:"listeners"` - Name types.String `tfsdk:"name"` - PlanId types.String `tfsdk:"plan_id"` - Networks types.List `tfsdk:"networks"` - Options types.Object `tfsdk:"options"` - PrivateAddress types.String `tfsdk:"private_address"` - TargetPools types.List `tfsdk:"target_pools"` - Region types.String `tfsdk:"region"` + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + ExternalAddress types.String `tfsdk:"external_address"` + DisableSecurityGroupAssignment types.Bool `tfsdk:"disable_security_group_assignment"` + Listeners types.List `tfsdk:"listeners"` + Name types.String `tfsdk:"name"` + PlanId types.String `tfsdk:"plan_id"` + Networks types.List `tfsdk:"networks"` + Options types.Object `tfsdk:"options"` + PrivateAddress types.String `tfsdk:"private_address"` + TargetPools types.List `tfsdk:"target_pools"` + Region types.String `tfsdk:"region"` } // Struct corresponding to Model.Listeners[i] @@ -303,6 +305,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance. " + utils.SupportedValuesDocumentation(protocolOptions), @@ -373,6 +376,16 @@ The example below creates the supporting infrastructure using the STACKIT Terraf stringplanmodifier.RequiresReplace(), }, }, + "disable_security_group_assignment": schema.BoolAttribute{ + Description: descriptions["disable_security_group_assignment"], + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + boolplanmodifier.UseStateForUnknown(), + }, + }, "plan_id": schema.StringAttribute{ Description: descriptions["plan_id"], Optional: true, @@ -907,13 +920,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoa } return &loadbalancer.CreateLoadBalancerPayload{ - ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress), - Listeners: listenersPayload, - Name: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - Networks: networksPayload, - Options: optionsPayload, - TargetPools: targetPoolsPayload, + ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress), + DisableTargetSecurityGroupAssignment: conversion.BoolValueToPointer(model.DisableSecurityGroupAssignment), + Listeners: listenersPayload, + Name: conversion.StringValueToPointer(model.Name), + PlanId: conversion.StringValueToPointer(model.PlanId), + Networks: networksPayload, + Options: optionsPayload, + TargetPools: targetPoolsPayload, }, nil } @@ -1221,6 +1235,7 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg m.PlanId = types.StringPointerValue(lb.PlanId) m.ExternalAddress = types.StringPointerValue(lb.ExternalAddress) m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress) + m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment) err := mapListeners(lb, m) if err != nil { diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 2d3c974b9..599b5ffa1 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -47,17 +47,18 @@ var testConfigVarsMin = config.Variables{ } var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "plan_id": config.StringVariable("p10"), - "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "target_pool_name": config.StringVariable("example-target-pool"), - "target_port": config.StringVariable("5432"), - "target_display_name": config.StringVariable("example-target"), - "listener_port": config.StringVariable("5432"), - "listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), - "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), + "project_id": config.StringVariable(testutil.ProjectId), + "plan_id": config.StringVariable("p10"), + "disable_security_group_assignment": config.StringVariable("true"), + "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "target_pool_name": config.StringVariable("example-target-pool"), + "target_port": config.StringVariable("5432"), + "target_display_name": config.StringVariable("example-target"), + "listener_port": config.StringVariable("5432"), + "listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), + "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), "listener_display_name": config.StringVariable("example-listener"), "listener_server_name_indicators": config.StringVariable("acc-test.runs.onstackit.cloud"), @@ -118,6 +119,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), @@ -168,6 +170,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), @@ -234,6 +237,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), @@ -304,6 +308,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf index aabd752d5..ac0d913a1 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf @@ -11,6 +11,7 @@ variable "target_display_name" {} variable "listener_port" {} variable "listener_protocol" {} variable "network_role" {} +variable "disable_security_group_assignment" {} variable "listener_display_name" {} variable "listener_server_name_indicators" {} @@ -75,6 +76,7 @@ resource "stackit_loadbalancer" "loadbalancer" { project_id = var.project_id name = var.loadbalancer_name plan_id = var.plan_id + disable_security_group_assignment = var.disable_security_group_assignment target_pools = [ { name = var.target_pool_name diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf index 2b6d65793..b7ebf3d7b 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf @@ -19,7 +19,7 @@ resource "stackit_network" "network" { project_id = var.project_id name = var.network_name ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.2.0/25" + ipv4_prefix = "192.168.4.0/25" # todo: change me back to 192.168.2.0/25 routed = "true" } From b0e1e52d5a691f28acd9f1ccd4d7321794f77ebd Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:04:37 +0200 Subject: [PATCH 02/63] Exposes the ID of the Load Balancer's egress security group. This allows users to reference the ID in security group rules for targets in other networks, enabling cross-network traffic. --- docs/data-sources/loadbalancer.md | 1 + docs/resources/loadbalancer.md | 1 + .../loadbalancer/loadbalancer/datasource.go | 4 ++++ .../services/loadbalancer/loadbalancer/resource.go | 13 +++++++++++++ .../loadbalancer/loadbalancer/resource_test.go | 12 +++++++++--- .../services/loadbalancer/loadbalancer_acc_test.go | 4 ++++ 6 files changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 65cc06851..7296336b1 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -41,6 +41,7 @@ data "stackit_loadbalancer" "example" { - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. - `private_address` (String) Transient private Load Balancer IP address. It can change any time. +- `security_group_id` (String) The ID of the security group automatically assigned to the load balancer's targets. - `target_pools` (Attributes List) List of all target pools which will be used in the Load Balancer. Limited to 20. (see [below for nested schema](#nestedatt--target_pools)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 42d28c27f..b04309a9b 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -134,6 +134,7 @@ resource "stackit_loadbalancer" "example" { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index d5e3f4f20..15fcd892e 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -344,6 +344,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Optional: true, Description: descriptions["region"], }, + "security_group_id": schema.StringAttribute{ + Description: "The ID of the security group automatically assigned to the load balancer's targets.", + Computed: true, + }, }, } } diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 0f6b596b5..06e633c5b 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -61,6 +61,7 @@ type Model struct { PrivateAddress types.String `tfsdk:"private_address"` TargetPools types.List `tfsdk:"target_pools"` Region types.String `tfsdk:"region"` + SecurityGroupId types.String `tfsdk:"security_group_id"` } // Struct corresponding to Model.Listeners[i] @@ -682,6 +683,13 @@ The example below creates the supporting infrastructure using the STACKIT Terraf stringplanmodifier.RequiresReplace(), }, }, + "security_group_id": schema.StringAttribute{ + Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, }, } } @@ -1237,6 +1245,11 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress) m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment) + if lb.TargetSecurityGroup != nil { + m.SecurityGroupId = types.StringPointerValue(lb.TargetSecurityGroup.Id) + } else { + m.SecurityGroupId = types.StringNull() + } err := mapListeners(lb, m) if err != nil { return fmt.Errorf("mapping listeners: %w", err) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go index 2a1a85b79..810e9c395 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go @@ -489,9 +489,10 @@ func TestMapFields(t *testing.T) { }), }), }), - PrivateAddress: types.StringNull(), - TargetPools: types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}), - Region: types.StringValue(testRegion), + PrivateAddress: types.StringNull(), + SecurityGroupId: types.StringNull(), + TargetPools: types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}), + Region: types.StringValue(testRegion), }, true, }, @@ -536,6 +537,10 @@ func TestMapFields(t *testing.T) { }, }, }), + TargetSecurityGroup: loadbalancer.LoadBalancerGetTargetSecurityGroupAttributeType(&loadbalancer.SecurityGroup{ + Id: utils.Ptr("sg-id-12345"), + Name: utils.Ptr("sg-name-abcde"), + }), TargetPools: utils.Ptr([]loadbalancer.TargetPool{ { ActiveHealthCheck: utils.Ptr(loadbalancer.ActiveHealthCheck{ @@ -565,6 +570,7 @@ func TestMapFields(t *testing.T) { Id: types.StringValue(id), ProjectId: types.StringValue("pid"), ExternalAddress: types.StringValue("external_address"), + SecurityGroupId: types.StringValue("sg-id-12345"), Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{ types.ObjectValueMust(listenerTypes, map[string]attr.Value{ "display_name": types.StringValue("display_name"), diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 599b5ffa1..51bb9293d 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -124,6 +124,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), + resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), // Loadbalancer observability credentials resource resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -175,6 +176,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), + resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), )}, // Import { @@ -238,6 +240,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), @@ -309,6 +312,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), From 19e167f8f2f72b74e5c13f8e33201895195908fd Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:13:37 +0200 Subject: [PATCH 03/63] Exposes the ID of the Load Balancer's egress security group. This allows users to reference the ID in security group rules for targets in other networks, enabling cross-network traffic. --- .../internal/services/loadbalancer/testfiles/resource-min.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf index b7ebf3d7b..2b6d65793 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf @@ -19,7 +19,7 @@ resource "stackit_network" "network" { project_id = var.project_id name = var.network_name ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.4.0/25" # todo: change me back to 192.168.2.0/25 + ipv4_prefix = "192.168.2.0/25" routed = "true" } From 065cd04d3fd0d3accd281bef5e3c1d1d0d0ad865 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:22:44 +0200 Subject: [PATCH 04/63] change order of imports --- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 06e633c5b..9f43d00f4 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,7 +3,6 @@ package loadbalancer import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -19,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" From 288cf7531278049a1072a4d37cf059c21d8a73d5 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Tue, 22 Jul 2025 14:59:43 +0200 Subject: [PATCH 05/63] added a example for the new feature --- .../stackit_loadbalancer/resource.tf | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 34dcdf496..0fac4a7cb 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -89,4 +89,96 @@ resource "stackit_loadbalancer" "example" { options = { private_network_only = false } +} + +# This example shows an advanced setup where the load balancer is in one +# network and the target server is in another. This requires manual +# security group configuration. + +# 1. Create a network for the Load Balancer +resource "stackit_network" "lb_network" { + project_id = var.project_id + name = "lb-network" + ipv4_prefix = "192.168.1.0/24" +} + +# 2. Create a separate network for the Target Server +resource "stackit_network" "target_network" { + project_id = var.project_id + name = "target-network" + ipv4_prefix = "192.168.2.0/24" +} + +# 3. Create the Load Balancer and disable automatic security groups +resource "stackit_loadbalancer" "example_advanced" { + project_id = var.project_id + name = "advanced-lb" + + # This is the key setting for manual mode + disable_security_group_assignment = true + + networks = [{ + network_id = stackit_network.lb_network.id + role = "ROLE_LISTENERS_AND_TARGETS" + }] + + target_pools = [{ + name = "cross-network-pool" + target_port = 80 + targets = [{ + display_name = "remote-target-server" + ip = stackit_server.target_server.network_interfaces[0].ipv4 + }] + }] + + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] +} + +# 4. Create a new security group for the target server +resource "stackit_security_group" "target_sg" { + project_id = var.project_id + name = "target-sg-for-lb-access" + description = "Allows ingress traffic from the advanced load balancer." +} + +# 5. Create a rule to allow traffic FROM the load balancer +# This is the core of the manual setup. +resource "stackit_security_group_rule" "allow_lb_ingress" { + security_group_id = stackit_security_group.target_sg.id + direction = "ingress" + protocol = "tcp" + + # Use the computed security_group_id from the load balancer + remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + + port_range = { + min = 80 + max = 80 + } +} + +# 6. Create the target server and assign the new security group to it +resource "stackit_server" "target_server" { + project_id = var.project_id + name = "remote-target-server" + machine_type = "c1.1" + availability_zone = "eu01-1" + + boot_volume = { + source_type = "image" + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + size = 10 + } + + network_interfaces = [{ + network_id = stackit_network.target_network.id + security_groups = [stackit_security_group.target_sg.id] + }] + + # Ensure the rule is created before the server + depends_on = [stackit_security_group_rule.allow_lb_ingress] } \ No newline at end of file From 8bafad8163ca8a4511cf3f92b66f5498a3e711a5 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Tue, 22 Jul 2025 16:46:54 +0200 Subject: [PATCH 06/63] update docs --- docs/resources/loadbalancer.md | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index b04309a9b..a6469e9d3 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -109,6 +109,98 @@ resource "stackit_loadbalancer" "example" { private_network_only = false } } + +# This example shows an advanced setup where the load balancer is in one +# network and the target server is in another. This requires manual +# security group configuration. + +# 1. Create a network for the Load Balancer +resource "stackit_network" "lb_network" { + project_id = var.project_id + name = "lb-network" + ipv4_prefix = "192.168.1.0/24" +} + +# 2. Create a separate network for the Target Server +resource "stackit_network" "target_network" { + project_id = var.project_id + name = "target-network" + ipv4_prefix = "192.168.2.0/24" +} + +# 3. Create the Load Balancer and disable automatic security groups +resource "stackit_loadbalancer" "example_advanced" { + project_id = var.project_id + name = "advanced-lb" + + # This is the key setting for manual mode + disable_security_group_assignment = true + + networks = [{ + network_id = stackit_network.lb_network.id + role = "ROLE_LISTENERS_AND_TARGETS" + }] + + target_pools = [{ + name = "cross-network-pool" + target_port = 80 + targets = [{ + display_name = "remote-target-server" + ip = stackit_server.target_server.network_interfaces[0].ipv4 + }] + }] + + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] +} + +# 4. Create a new security group for the target server +resource "stackit_security_group" "target_sg" { + project_id = var.project_id + name = "target-sg-for-lb-access" + description = "Allows ingress traffic from the advanced load balancer." +} + +# 5. Create a rule to allow traffic FROM the load balancer +# This is the core of the manual setup. +resource "stackit_security_group_rule" "allow_lb_ingress" { + security_group_id = stackit_security_group.target_sg.id + direction = "ingress" + protocol = "tcp" + + # Use the computed security_group_id from the load balancer + remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + + port_range = { + min = 80 + max = 80 + } +} + +# 6. Create the target server and assign the new security group to it +resource "stackit_server" "target_server" { + project_id = var.project_id + name = "remote-target-server" + machine_type = "c1.1" + availability_zone = "eu01-1" + + boot_volume = { + source_type = "image" + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + size = 10 + } + + network_interfaces = [{ + network_id = stackit_network.target_network.id + security_groups = [stackit_security_group.target_sg.id] + }] + + # Ensure the rule is created before the server + depends_on = [stackit_security_group_rule.allow_lb_ingress] +} ``` From c22bacd3640b5839e02b360b915880724193aff5 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 25 Jul 2025 12:25:36 +0200 Subject: [PATCH 07/63] terraform fmt --- docs/resources/loadbalancer.md | 8 +++---- .../stackit_loadbalancer/resource.tf | 8 +++---- .../loadbalancer/testfiles/resource-max.tf | 22 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index a6469e9d3..7e1c970b5 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -116,15 +116,15 @@ resource "stackit_loadbalancer" "example" { # 1. Create a network for the Load Balancer resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = var.project_id + name = "lb-network" ipv4_prefix = "192.168.1.0/24" } # 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = var.project_id + name = "target-network" ipv4_prefix = "192.168.2.0/24" } diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 0fac4a7cb..3bb689329 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -97,15 +97,15 @@ resource "stackit_loadbalancer" "example" { # 1. Create a network for the Load Balancer resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = var.project_id + name = "lb-network" ipv4_prefix = "192.168.1.0/24" } # 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = var.project_id + name = "target-network" ipv4_prefix = "192.168.2.0/24" } diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf index ac0d913a1..491a64803 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf @@ -73,9 +73,9 @@ resource "stackit_server" "server" { } resource "stackit_loadbalancer" "loadbalancer" { - project_id = var.project_id - name = var.loadbalancer_name - plan_id = var.plan_id + project_id = var.project_id + name = var.loadbalancer_name + plan_id = var.plan_id disable_security_group_assignment = var.disable_security_group_assignment target_pools = [ { @@ -122,14 +122,14 @@ resource "stackit_loadbalancer" "loadbalancer" { private_network_only = var.private_network_only acl = [var.acl] observability = { - logs = { - credentials_ref = stackit_loadbalancer_observability_credential.logs.credentials_ref - push_url = var.observability_logs_push_url - } - metrics = { - credentials_ref = stackit_loadbalancer_observability_credential.metrics.credentials_ref - push_url = var.observability_metrics_push_url - } + logs = { + credentials_ref = stackit_loadbalancer_observability_credential.logs.credentials_ref + push_url = var.observability_logs_push_url + } + metrics = { + credentials_ref = stackit_loadbalancer_observability_credential.metrics.credentials_ref + push_url = var.observability_metrics_push_url + } } } external_address = stackit_public_ip.public_ip.ip From 895ac47652e1fa51a3a36bde69f49e84a3916249 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Thu, 31 Jul 2025 08:56:28 +0200 Subject: [PATCH 08/63] add PR feedback --- docs/data-sources/loadbalancer.md | 2 +- docs/resources/loadbalancer.md | 4 +--- examples/resources/stackit_loadbalancer/resource.tf | 4 +--- .../services/loadbalancer/loadbalancer/datasource.go | 3 ++- .../internal/services/loadbalancer/loadbalancer_acc_test.go | 6 +++--- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 7296336b1..687adbeba 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -41,7 +41,7 @@ data "stackit_loadbalancer" "example" { - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) The ID of the security group automatically assigned to the load balancer's targets. +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. - `target_pools` (Attributes List) List of all target pools which will be used in the Load Balancer. Limited to 20. (see [below for nested schema](#nestedatt--target_pools)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 7e1c970b5..ae5db0745 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -167,6 +167,7 @@ resource "stackit_security_group" "target_sg" { # 5. Create a rule to allow traffic FROM the load balancer # This is the core of the manual setup. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -197,9 +198,6 @@ resource "stackit_server" "target_server" { network_id = stackit_network.target_network.id security_groups = [stackit_security_group.target_sg.id] }] - - # Ensure the rule is created before the server - depends_on = [stackit_security_group_rule.allow_lb_ingress] } ``` diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 3bb689329..b2351564a 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -148,6 +148,7 @@ resource "stackit_security_group" "target_sg" { # 5. Create a rule to allow traffic FROM the load balancer # This is the core of the manual setup. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -178,7 +179,4 @@ resource "stackit_server" "target_server" { network_id = stackit_network.target_network.id security_groups = [stackit_security_group.target_sg.id] }] - - # Ensure the rule is created before the server - depends_on = [stackit_security_group_rule.allow_lb_ingress] } \ No newline at end of file diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index 15fcd892e..aaac7b53c 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -70,6 +70,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", + "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance.", @@ -345,7 +346,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["region"], }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the security group automatically assigned to the load balancer's targets.", + Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", Computed: true, }, }, diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 51bb9293d..34693a7c5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -49,7 +49,7 @@ var testConfigVarsMin = config.Variables{ var testConfigVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "plan_id": config.StringVariable("p10"), - "disable_security_group_assignment": config.StringVariable("true"), + "disable_security_group_assignment": config.BoolVariable(true), "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), @@ -239,7 +239,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), @@ -311,7 +311,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), From 4f4aa53cddb6a3aa6921a0743d2590721c2e834d Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 14:59:37 +0200 Subject: [PATCH 09/63] update stackit_loadbalancer resource to no longer wait until LB becomes ready if disabletargetsecuritygroupassignment is true --- .../loadbalancer/loadbalancer/resource.go | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 9f43d00f4..cd4f3a946 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -721,15 +721,50 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) return } - - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) - return + loadBalancerName := *createResp.Name + + // This variable will hold the final load balancer object + var lb *loadbalancer.LoadBalancer + + if model.DisableSecurityGroupAssignment.ValueBool() { + // MANUAL MODE: Manually poll the resource until the security_group_id is available. + timeout := 5 * time.Minute + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-time.After(timeout): + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("timed out waiting for security_group_id to be populated for load balancer %q", loadBalancerName)) + return + case <-ticker.C: + getResp, err := r.client.GetLoadBalancer(ctx, projectId, region, loadBalancerName).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("polling for security_group_id: %v", err)) + return + } + + // Check if the security group has been created and attached yet. + if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { + lb = getResp // Success! The ID is available. + goto POLLING_DONE + } + // The ID is not ready yet, the loop will continue on the next tick. + } + } + POLLING_DONE: + } else { + // AUTOMATIC MODE: Wait for the load balancer to become active. + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) + return + } + lb = waitResp } // Map response body to schema - err = mapFields(ctx, waitResp, &model, region) + err = mapFields(ctx, lb, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return From d803bc7ff403c591a4c7a1e571dc895695c3ddf9 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:02:23 +0200 Subject: [PATCH 10/63] cleanup --- .../services/loadbalancer/loadbalancer/resource.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index cd4f3a946..659c97ec5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -696,7 +696,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf // Create creates the resource and sets the initial Terraform state. func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -708,14 +707,12 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - // Generate API request body from model payload, err := toCreatePayload(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err)) return } - // Create a new load balancer createResp, err := r.client.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) @@ -723,7 +720,6 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - // This variable will hold the final load balancer object var lb *loadbalancer.LoadBalancer if model.DisableSecurityGroupAssignment.ValueBool() { @@ -744,9 +740,8 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe return } - // Check if the security group has been created and attached yet. if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { - lb = getResp // Success! The ID is available. + lb = getResp goto POLLING_DONE } // The ID is not ready yet, the loop will continue on the next tick. @@ -763,14 +758,12 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe lb = waitResp } - // Map response body to schema err = mapFields(ctx, lb, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return } - // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { From 545aabb853a18e7e3c05d35a9453266670993ca7 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:51:11 +0200 Subject: [PATCH 11/63] update docs and example --- docs/resources/loadbalancer.md | 75 ++++++++++--------- .../stackit_loadbalancer/resource.tf | 75 ++++++++++--------- 2 files changed, 80 insertions(+), 70 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index ae5db0745..eeffbd442 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -110,30 +110,34 @@ resource "stackit_loadbalancer" "example" { } } -# This example shows an advanced setup where the load balancer is in one +# This example demonstrates an advanced setup where the Load Balancer is in one # network and the target server is in another. This requires manual -# security group configuration. +# security group configuration using the `disable_security_group_assignment` +# and `security_group_id` attributes. -# 1. Create a network for the Load Balancer +# We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" ipv4_prefix = "192.168.1.0/24" } -# 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" ipv4_prefix = "192.168.2.0/24" } -# 3. Create the Load Balancer and disable automatic security groups -resource "stackit_loadbalancer" "example_advanced" { - project_id = var.project_id - name = "advanced-lb" +resource "stackit_public_ip" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_loadbalancer" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-advanced-lb" + external_address = stackit_public_ip.example.ip - # This is the key setting for manual mode + # Key setting for manual mode: disables automatic security group handling. disable_security_group_assignment = true networks = [{ @@ -141,39 +145,38 @@ resource "stackit_loadbalancer" "example_advanced" { role = "ROLE_LISTENERS_AND_TARGETS" }] + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] + target_pools = [{ name = "cross-network-pool" target_port = 80 targets = [{ - display_name = "remote-target-server" - ip = stackit_server.target_server.network_interfaces[0].ipv4 + display_name = stackit_server.example.name + ip = stackit_network_interface.nic.ipv4 }] }] - - listeners = [{ - port = 80 - protocol = "PROTOCOL_TCP" - target_pool = "cross-network-pool" - }] } -# 4. Create a new security group for the target server +# Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { project_id = var.project_id name = "target-sg-for-lb-access" - description = "Allows ingress traffic from the advanced load balancer." + description = "Allows ingress traffic from the example load balancer." } -# 5. Create a rule to allow traffic FROM the load balancer -# This is the core of the manual setup. +# Create a rule to allow traffic FROM the load balancer. +# This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" - # Use the computed security_group_id from the load balancer - remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + # This is the crucial link: it allows traffic from the LB's security group. + remote_security_group_id = stackit_loadbalancer.example.security_group_id port_range = { min = 80 @@ -181,22 +184,24 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } } -# 6. Create the target server and assign the new security group to it -resource "stackit_server" "target_server" { +resource "stackit_server" "example" { project_id = var.project_id - name = "remote-target-server" + name = "example-remote-target" machine_type = "c1.1" - availability_zone = "eu01-1" + availability_zone = data.stackit_availability_zones.example.names[0] boot_volume = { source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + source_id = data.stackit_image.example_ubuntu.id size = 10 } network_interfaces = [{ - network_id = stackit_network.target_network.id - security_groups = [stackit_security_group.target_sg.id] + network_id = stackit_network.target_network.id + # Assign the manually configured security group to the server's NIC. + security_groups = [ + stackit_security_group.target_sg.id + ] }] } ``` diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index b2351564a..ef9276cc1 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -91,30 +91,34 @@ resource "stackit_loadbalancer" "example" { } } -# This example shows an advanced setup where the load balancer is in one +# This example demonstrates an advanced setup where the Load Balancer is in one # network and the target server is in another. This requires manual -# security group configuration. +# security group configuration using the `disable_security_group_assignment` +# and `security_group_id` attributes. -# 1. Create a network for the Load Balancer +# We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" ipv4_prefix = "192.168.1.0/24" } -# 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" ipv4_prefix = "192.168.2.0/24" } -# 3. Create the Load Balancer and disable automatic security groups -resource "stackit_loadbalancer" "example_advanced" { - project_id = var.project_id - name = "advanced-lb" +resource "stackit_public_ip" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_loadbalancer" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-advanced-lb" + external_address = stackit_public_ip.example.ip - # This is the key setting for manual mode + # Key setting for manual mode: disables automatic security group handling. disable_security_group_assignment = true networks = [{ @@ -122,39 +126,38 @@ resource "stackit_loadbalancer" "example_advanced" { role = "ROLE_LISTENERS_AND_TARGETS" }] + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] + target_pools = [{ name = "cross-network-pool" target_port = 80 targets = [{ - display_name = "remote-target-server" - ip = stackit_server.target_server.network_interfaces[0].ipv4 + display_name = stackit_server.example.name + ip = stackit_network_interface.nic.ipv4 }] }] - - listeners = [{ - port = 80 - protocol = "PROTOCOL_TCP" - target_pool = "cross-network-pool" - }] } -# 4. Create a new security group for the target server +# Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { project_id = var.project_id name = "target-sg-for-lb-access" - description = "Allows ingress traffic from the advanced load balancer." + description = "Allows ingress traffic from the example load balancer." } -# 5. Create a rule to allow traffic FROM the load balancer -# This is the core of the manual setup. +# Create a rule to allow traffic FROM the load balancer. +# This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" - # Use the computed security_group_id from the load balancer - remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + # This is the crucial link: it allows traffic from the LB's security group. + remote_security_group_id = stackit_loadbalancer.example.security_group_id port_range = { min = 80 @@ -162,21 +165,23 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } } -# 6. Create the target server and assign the new security group to it -resource "stackit_server" "target_server" { +resource "stackit_server" "example" { project_id = var.project_id - name = "remote-target-server" + name = "example-remote-target" machine_type = "c1.1" - availability_zone = "eu01-1" + availability_zone = data.stackit_availability_zones.example.names[0] boot_volume = { source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + source_id = data.stackit_image.example_ubuntu.id size = 10 } network_interfaces = [{ - network_id = stackit_network.target_network.id - security_groups = [stackit_security_group.target_sg.id] + network_id = stackit_network.target_network.id + # Assign the manually configured security group to the server's NIC. + security_groups = [ + stackit_security_group.target_sg.id + ] }] } \ No newline at end of file From a30cead19589963c6314cb856637af8378a4a9e1 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:53:59 +0200 Subject: [PATCH 12/63] update docs and example --- docs/data-sources/loadbalancer.md | 2 +- .../internal/services/loadbalancer/loadbalancer/datasource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 687adbeba..d3c1c4eae 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -33,7 +33,7 @@ data "stackit_loadbalancer" "example" { ### Read-Only -- `disable_security_group_assignment` (Boolean) Disables the target security group assignment. +- `disable_security_group_assignment` (Boolean) If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners)) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index aaac7b53c..e00613931 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -128,7 +128,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Computed: true, }, "disable_security_group_assignment": schema.BoolAttribute{ - Description: "Disables the target security group assignment.", + Description: descriptions["disable_security_group_assignment"], Computed: true, }, "plan_id": schema.StringAttribute{ From e6d3654799fe5332ba3434e65c5dd4fc675a5a69 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:56:41 +0200 Subject: [PATCH 13/63] update docs and example --- docs/resources/loadbalancer.md | 5 +++-- examples/resources/stackit_loadbalancer/resource.tf | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index eeffbd442..7bc516af0 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -163,7 +163,7 @@ resource "stackit_loadbalancer" "example" { # Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "target-sg-for-lb-access" description = "Allows ingress traffic from the example load balancer." } @@ -171,6 +171,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -185,7 +186,7 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } resource "stackit_server" "example" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" availability_zone = data.stackit_availability_zones.example.names[0] diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index ef9276cc1..3a1756e59 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -144,7 +144,7 @@ resource "stackit_loadbalancer" "example" { # Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "target-sg-for-lb-access" description = "Allows ingress traffic from the example load balancer." } @@ -152,6 +152,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -166,7 +167,7 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } resource "stackit_server" "example" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" availability_zone = data.stackit_availability_zones.example.names[0] From 180aa11cf7e7ce6c1198768fa8a41e0d75de3e1f Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 16:02:17 +0200 Subject: [PATCH 14/63] update docs and example --- docs/resources/loadbalancer.md | 16 +++++++++------- .../resources/stackit_loadbalancer/resource.tf | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 7bc516af0..e283a367f 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -117,15 +117,17 @@ resource "stackit_loadbalancer" "example" { # We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "lb-network-example" - ipv4_prefix = "192.168.1.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" + ipv4_prefix = "192.168.1.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_network" "target_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "target-network-example" - ipv4_prefix = "192.168.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" + ipv4_prefix = "192.168.2.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_public_ip" "example" { @@ -171,7 +173,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 3a1756e59..06f8a719e 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -98,15 +98,17 @@ resource "stackit_loadbalancer" "example" { # We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "lb-network-example" - ipv4_prefix = "192.168.1.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" + ipv4_prefix = "192.168.1.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_network" "target_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "target-network-example" - ipv4_prefix = "192.168.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" + ipv4_prefix = "192.168.2.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_public_ip" "example" { @@ -152,7 +154,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" From b4a9a7d2124dacac38152256fdb8c1434a55828d Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 16:19:07 +0200 Subject: [PATCH 15/63] update docs and example --- .../internal/services/loadbalancer/loadbalancer/datasource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index e00613931..c72306656 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -346,7 +346,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["region"], }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Description: descriptions["security_group_id"], Computed: true, }, }, From b9bc3aabe1f74435b072ac3a16c7b1eb4f38c27a Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:23:35 +0200 Subject: [PATCH 16/63] revert to simpler function to allow the LB to become active before proceeding, updated docs --- docs/resources/loadbalancer.md | 26 +++++----- .../stackit_loadbalancer/resource.tf | 26 +++++----- .../loadbalancer/loadbalancer/resource.go | 49 +++++-------------- 3 files changed, 42 insertions(+), 59 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index e283a367f..d5a1c764a 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -143,7 +143,7 @@ resource "stackit_loadbalancer" "example" { disable_security_group_assignment = true networks = [{ - network_id = stackit_network.lb_network.id + network_id = stackit_network.lb_network.network_id role = "ROLE_LISTENERS_AND_TARGETS" }] @@ -176,7 +176,9 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" - protocol = "tcp" + protocol = { + name = "tcp" + } # This is the crucial link: it allows traffic from the LB's security group. remote_security_group_id = stackit_loadbalancer.example.security_group_id @@ -191,21 +193,23 @@ resource "stackit_server" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" - availability_zone = data.stackit_availability_zones.example.names[0] + availability_zone = "eu01-1" boot_volume = { source_type = "image" - source_id = data.stackit_image.example_ubuntu.id + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 10 } - network_interfaces = [{ - network_id = stackit_network.target_network.id - # Assign the manually configured security group to the server's NIC. - security_groups = [ - stackit_security_group.target_sg.id - ] - }] + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.target_network.network_id + security_group_ids = [stackit_security_group.target_sg.security_group_id] } ``` diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 06f8a719e..e539f26a3 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -124,7 +124,7 @@ resource "stackit_loadbalancer" "example" { disable_security_group_assignment = true networks = [{ - network_id = stackit_network.lb_network.id + network_id = stackit_network.lb_network.network_id role = "ROLE_LISTENERS_AND_TARGETS" }] @@ -157,7 +157,9 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" - protocol = "tcp" + protocol = { + name = "tcp" + } # This is the crucial link: it allows traffic from the LB's security group. remote_security_group_id = stackit_loadbalancer.example.security_group_id @@ -172,19 +174,21 @@ resource "stackit_server" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" - availability_zone = data.stackit_availability_zones.example.names[0] + availability_zone = "eu01-1" boot_volume = { source_type = "image" - source_id = data.stackit_image.example_ubuntu.id + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 10 } - network_interfaces = [{ - network_id = stackit_network.target_network.id - # Assign the manually configured security group to the server's NIC. - security_groups = [ - stackit_security_group.target_sg.id - ] - }] + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.target_network.network_id + security_group_ids = [stackit_security_group.target_sg.security_group_id] } \ No newline at end of file diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 659c97ec5..b6a66fd9c 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -696,6 +696,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf // Create creates the resource and sets the initial Terraform state. func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -707,12 +708,14 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) + // Generate API request body from model payload, err := toCreatePayload(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err)) return } + // Create a new load balancer createResp, err := r.client.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) @@ -720,50 +723,22 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - var lb *loadbalancer.LoadBalancer - - if model.DisableSecurityGroupAssignment.ValueBool() { - // MANUAL MODE: Manually poll the resource until the security_group_id is available. - timeout := 5 * time.Minute - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-time.After(timeout): - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("timed out waiting for security_group_id to be populated for load balancer %q", loadBalancerName)) - return - case <-ticker.C: - getResp, err := r.client.GetLoadBalancer(ctx, projectId, region, loadBalancerName).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("polling for security_group_id: %v", err)) - return - } - - if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { - lb = getResp - goto POLLING_DONE - } - // The ID is not ready yet, the loop will continue on the next tick. - } - } - POLLING_DONE: - } else { - // AUTOMATIC MODE: Wait for the load balancer to become active. - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) - return - } - lb = waitResp + // Wait for the load balancer to become active. + // This single handler is now used for both automatic and manual modes. + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) + return } - err = mapFields(ctx, lb, &model, region) + // Map response body to schema + err = mapFields(ctx, waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { From 2858cb1797d91b97a398556b32588c754cae12b2 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:28:00 +0200 Subject: [PATCH 17/63] update description --- docs/resources/loadbalancer.md | 2 +- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index d5a1c764a..9c891b491 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -236,7 +236,7 @@ resource "stackit_network_interface" "nic" { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. +- `security_group_id` (String) ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index b6a66fd9c..2d30b00f7 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -684,7 +684,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf }, }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Description: descriptions["security_group_id"], Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), From 77dc880d15b9b2063118daaf5619ff0a478708e2 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:31:59 +0200 Subject: [PATCH 18/63] update description --- docs/resources/loadbalancer.md | 2 +- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 9c891b491..d5a1c764a 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -236,7 +236,7 @@ resource "stackit_network_interface" "nic" { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 2d30b00f7..f471713fd 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -343,6 +343,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques "targets.display_name": "Target display name", "ip": "Target IP", "region": "The resource region. If not defined, the provider region is used.", + "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", } resp.Schema = schema.Schema{ From 7517389ea6cbd47377959ad56cd545dcffeeb0cf Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:33:38 +0200 Subject: [PATCH 19/63] update description --- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index f471713fd..ead3e73fd 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -724,8 +724,6 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - // Wait for the load balancer to become active. - // This single handler is now used for both automatic and manual modes. waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) From e63bdede7d05019ac4e195930167abbcef30ae38 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:36:29 +0200 Subject: [PATCH 20/63] small correction of unnecessary var --- .../internal/services/loadbalancer/loadbalancer/resource.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index ead3e73fd..0beda14f9 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,6 +3,7 @@ package loadbalancer import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -18,7 +19,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" @@ -722,9 +722,8 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) return } - loadBalancerName := *createResp.Name - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) return From 8e0ebfeab61483f5a44cccf0adc6d41031f150b7 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 16:30:28 +0200 Subject: [PATCH 21/63] update example --- docs/resources/loadbalancer.md | 2 +- examples/resources/stackit_loadbalancer/resource.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index d5a1c764a..6caab6801 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -174,7 +174,7 @@ resource "stackit_security_group" "target_sg" { # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - security_group_id = stackit_security_group.target_sg.id + security_group_id = stackit_security_group.target_sg.security_group_id direction = "ingress" protocol = { name = "tcp" diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index e539f26a3..404a72912 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -155,7 +155,7 @@ resource "stackit_security_group" "target_sg" { # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - security_group_id = stackit_security_group.target_sg.id + security_group_id = stackit_security_group.target_sg.security_group_id direction = "ingress" protocol = { name = "tcp" From e30d5547b287ceaddda0a5c6da9aa7abb4c9faa7 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 15:02:40 +0200 Subject: [PATCH 22/63] Adds the new `disable_security_group_assignment` boolean attribute to the load balancer resource. This allows disabling the automatic assignment of a security group to the targets. --- docs/data-sources/loadbalancer.md | 1 + docs/resources/loadbalancer.md | 1 + .../loadbalancer/loadbalancer/datasource.go | 5 ++ .../loadbalancer/loadbalancer/resource.go | 51 ++++++++++++------- .../loadbalancer/loadbalancer_acc_test.go | 27 ++++++---- .../loadbalancer/testfiles/resource-max.tf | 2 + .../loadbalancer/testfiles/resource-min.tf | 2 +- 7 files changed, 59 insertions(+), 30 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index c59fc8428..65cc06851 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -33,6 +33,7 @@ data "stackit_loadbalancer" "example" { ### Read-Only +- `disable_security_group_assignment` (Boolean) Disables the target security group assignment. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index ab77a41dd..6170e8f95 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -130,6 +130,7 @@ import { ### Optional +- `disable_security_group_assignment` (Boolean) If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index d7f3a2157..d5e3f4f20 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -69,6 +69,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance.", @@ -125,6 +126,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["external_address"], Computed: true, }, + "disable_security_group_assignment": schema.BoolAttribute{ + Description: "Disables the target security group assignment.", + Computed: true, + }, "plan_id": schema.StringAttribute{ Description: descriptions["plan_id"], Computed: true, diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 36e0e1f74..0f6b596b5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,6 +3,7 @@ package loadbalancer import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -48,17 +49,18 @@ var ( ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - ExternalAddress types.String `tfsdk:"external_address"` - Listeners types.List `tfsdk:"listeners"` - Name types.String `tfsdk:"name"` - PlanId types.String `tfsdk:"plan_id"` - Networks types.List `tfsdk:"networks"` - Options types.Object `tfsdk:"options"` - PrivateAddress types.String `tfsdk:"private_address"` - TargetPools types.List `tfsdk:"target_pools"` - Region types.String `tfsdk:"region"` + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + ExternalAddress types.String `tfsdk:"external_address"` + DisableSecurityGroupAssignment types.Bool `tfsdk:"disable_security_group_assignment"` + Listeners types.List `tfsdk:"listeners"` + Name types.String `tfsdk:"name"` + PlanId types.String `tfsdk:"plan_id"` + Networks types.List `tfsdk:"networks"` + Options types.Object `tfsdk:"options"` + PrivateAddress types.String `tfsdk:"private_address"` + TargetPools types.List `tfsdk:"target_pools"` + Region types.String `tfsdk:"region"` } // Struct corresponding to Model.Listeners[i] @@ -303,6 +305,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance. " + utils.SupportedValuesDocumentation(protocolOptions), @@ -373,6 +376,16 @@ The example below creates the supporting infrastructure using the STACKIT Terraf stringplanmodifier.RequiresReplace(), }, }, + "disable_security_group_assignment": schema.BoolAttribute{ + Description: descriptions["disable_security_group_assignment"], + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + boolplanmodifier.UseStateForUnknown(), + }, + }, "plan_id": schema.StringAttribute{ Description: descriptions["plan_id"], Optional: true, @@ -907,13 +920,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoa } return &loadbalancer.CreateLoadBalancerPayload{ - ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress), - Listeners: listenersPayload, - Name: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - Networks: networksPayload, - Options: optionsPayload, - TargetPools: targetPoolsPayload, + ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress), + DisableTargetSecurityGroupAssignment: conversion.BoolValueToPointer(model.DisableSecurityGroupAssignment), + Listeners: listenersPayload, + Name: conversion.StringValueToPointer(model.Name), + PlanId: conversion.StringValueToPointer(model.PlanId), + Networks: networksPayload, + Options: optionsPayload, + TargetPools: targetPoolsPayload, }, nil } @@ -1221,6 +1235,7 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg m.PlanId = types.StringPointerValue(lb.PlanId) m.ExternalAddress = types.StringPointerValue(lb.ExternalAddress) m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress) + m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment) err := mapListeners(lb, m) if err != nil { diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 2d3c974b9..599b5ffa1 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -47,17 +47,18 @@ var testConfigVarsMin = config.Variables{ } var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "plan_id": config.StringVariable("p10"), - "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "target_pool_name": config.StringVariable("example-target-pool"), - "target_port": config.StringVariable("5432"), - "target_display_name": config.StringVariable("example-target"), - "listener_port": config.StringVariable("5432"), - "listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), - "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), + "project_id": config.StringVariable(testutil.ProjectId), + "plan_id": config.StringVariable("p10"), + "disable_security_group_assignment": config.StringVariable("true"), + "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "target_pool_name": config.StringVariable("example-target-pool"), + "target_port": config.StringVariable("5432"), + "target_display_name": config.StringVariable("example-target"), + "listener_port": config.StringVariable("5432"), + "listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), + "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), "listener_display_name": config.StringVariable("example-listener"), "listener_server_name_indicators": config.StringVariable("acc-test.runs.onstackit.cloud"), @@ -118,6 +119,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), @@ -168,6 +170,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), @@ -234,6 +237,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), @@ -304,6 +308,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf index aabd752d5..ac0d913a1 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf @@ -11,6 +11,7 @@ variable "target_display_name" {} variable "listener_port" {} variable "listener_protocol" {} variable "network_role" {} +variable "disable_security_group_assignment" {} variable "listener_display_name" {} variable "listener_server_name_indicators" {} @@ -75,6 +76,7 @@ resource "stackit_loadbalancer" "loadbalancer" { project_id = var.project_id name = var.loadbalancer_name plan_id = var.plan_id + disable_security_group_assignment = var.disable_security_group_assignment target_pools = [ { name = var.target_pool_name diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf index 2b6d65793..b7ebf3d7b 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf @@ -19,7 +19,7 @@ resource "stackit_network" "network" { project_id = var.project_id name = var.network_name ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.2.0/25" + ipv4_prefix = "192.168.4.0/25" # todo: change me back to 192.168.2.0/25 routed = "true" } From 7ab6beb49edc695896cbb93e0a3fe3b26d54b5ad Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:04:37 +0200 Subject: [PATCH 23/63] Exposes the ID of the Load Balancer's egress security group. This allows users to reference the ID in security group rules for targets in other networks, enabling cross-network traffic. --- docs/data-sources/loadbalancer.md | 1 + docs/resources/loadbalancer.md | 1 + .../loadbalancer/loadbalancer/datasource.go | 4 ++++ .../services/loadbalancer/loadbalancer/resource.go | 13 +++++++++++++ .../loadbalancer/loadbalancer/resource_test.go | 12 +++++++++--- .../services/loadbalancer/loadbalancer_acc_test.go | 4 ++++ 6 files changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 65cc06851..7296336b1 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -41,6 +41,7 @@ data "stackit_loadbalancer" "example" { - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. - `private_address` (String) Transient private Load Balancer IP address. It can change any time. +- `security_group_id` (String) The ID of the security group automatically assigned to the load balancer's targets. - `target_pools` (Attributes List) List of all target pools which will be used in the Load Balancer. Limited to 20. (see [below for nested schema](#nestedatt--target_pools)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 6170e8f95..1ae11efb8 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -140,6 +140,7 @@ import { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index d5e3f4f20..15fcd892e 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -344,6 +344,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Optional: true, Description: descriptions["region"], }, + "security_group_id": schema.StringAttribute{ + Description: "The ID of the security group automatically assigned to the load balancer's targets.", + Computed: true, + }, }, } } diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 0f6b596b5..06e633c5b 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -61,6 +61,7 @@ type Model struct { PrivateAddress types.String `tfsdk:"private_address"` TargetPools types.List `tfsdk:"target_pools"` Region types.String `tfsdk:"region"` + SecurityGroupId types.String `tfsdk:"security_group_id"` } // Struct corresponding to Model.Listeners[i] @@ -682,6 +683,13 @@ The example below creates the supporting infrastructure using the STACKIT Terraf stringplanmodifier.RequiresReplace(), }, }, + "security_group_id": schema.StringAttribute{ + Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, }, } } @@ -1237,6 +1245,11 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress) m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment) + if lb.TargetSecurityGroup != nil { + m.SecurityGroupId = types.StringPointerValue(lb.TargetSecurityGroup.Id) + } else { + m.SecurityGroupId = types.StringNull() + } err := mapListeners(lb, m) if err != nil { return fmt.Errorf("mapping listeners: %w", err) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go index 2a1a85b79..810e9c395 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go @@ -489,9 +489,10 @@ func TestMapFields(t *testing.T) { }), }), }), - PrivateAddress: types.StringNull(), - TargetPools: types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}), - Region: types.StringValue(testRegion), + PrivateAddress: types.StringNull(), + SecurityGroupId: types.StringNull(), + TargetPools: types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}), + Region: types.StringValue(testRegion), }, true, }, @@ -536,6 +537,10 @@ func TestMapFields(t *testing.T) { }, }, }), + TargetSecurityGroup: loadbalancer.LoadBalancerGetTargetSecurityGroupAttributeType(&loadbalancer.SecurityGroup{ + Id: utils.Ptr("sg-id-12345"), + Name: utils.Ptr("sg-name-abcde"), + }), TargetPools: utils.Ptr([]loadbalancer.TargetPool{ { ActiveHealthCheck: utils.Ptr(loadbalancer.ActiveHealthCheck{ @@ -565,6 +570,7 @@ func TestMapFields(t *testing.T) { Id: types.StringValue(id), ProjectId: types.StringValue("pid"), ExternalAddress: types.StringValue("external_address"), + SecurityGroupId: types.StringValue("sg-id-12345"), Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{ types.ObjectValueMust(listenerTypes, map[string]attr.Value{ "display_name": types.StringValue("display_name"), diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 599b5ffa1..51bb9293d 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -124,6 +124,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), + resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), // Loadbalancer observability credentials resource resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -175,6 +176,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), + resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), )}, // Import { @@ -238,6 +240,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), @@ -309,6 +312,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), From e73871a2c66d083a8dc76c12d6c23ebb38928ab6 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:13:37 +0200 Subject: [PATCH 24/63] Exposes the ID of the Load Balancer's egress security group. This allows users to reference the ID in security group rules for targets in other networks, enabling cross-network traffic. --- .../internal/services/loadbalancer/testfiles/resource-min.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf index b7ebf3d7b..2b6d65793 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf @@ -19,7 +19,7 @@ resource "stackit_network" "network" { project_id = var.project_id name = var.network_name ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.4.0/25" # todo: change me back to 192.168.2.0/25 + ipv4_prefix = "192.168.2.0/25" routed = "true" } From 9e8d943e0d8c528c28924a729c1831784b9f0c45 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:22:44 +0200 Subject: [PATCH 25/63] change order of imports --- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 06e633c5b..9f43d00f4 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,7 +3,6 @@ package loadbalancer import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -19,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" From ac2a715c4fd614f6fc8c107e68919f41ff93bcc5 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 16:33:15 +0200 Subject: [PATCH 26/63] update example --- docs/resources/loadbalancer.md | 92 ++++++++++++++++++ .../stackit_loadbalancer/resource.tf | 94 ++++++++++++++++++- 2 files changed, 185 insertions(+), 1 deletion(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 1ae11efb8..f27aa14ea 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -110,6 +110,98 @@ resource "stackit_loadbalancer" "example" { } } +# This example shows an advanced setup where the load balancer is in one +# network and the target server is in another. This requires manual +# security group configuration. + +# 1. Create a network for the Load Balancer +resource "stackit_network" "lb_network" { + project_id = var.project_id + name = "lb-network" + ipv4_prefix = "192.168.1.0/24" +} + +# 2. Create a separate network for the Target Server +resource "stackit_network" "target_network" { + project_id = var.project_id + name = "target-network" + ipv4_prefix = "192.168.2.0/24" +} + +# 3. Create the Load Balancer and disable automatic security groups +resource "stackit_loadbalancer" "example_advanced" { + project_id = var.project_id + name = "advanced-lb" + + # This is the key setting for manual mode + disable_security_group_assignment = true + + networks = [{ + network_id = stackit_network.lb_network.id + role = "ROLE_LISTENERS_AND_TARGETS" + }] + + target_pools = [{ + name = "cross-network-pool" + target_port = 80 + targets = [{ + display_name = "remote-target-server" + ip = stackit_server.target_server.network_interfaces[0].ipv4 + }] + }] + + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] +} + +# 4. Create a new security group for the target server +resource "stackit_security_group" "target_sg" { + project_id = var.project_id + name = "target-sg-for-lb-access" + description = "Allows ingress traffic from the advanced load balancer." +} + +# 5. Create a rule to allow traffic FROM the load balancer +# This is the core of the manual setup. +resource "stackit_security_group_rule" "allow_lb_ingress" { + security_group_id = stackit_security_group.target_sg.id + direction = "ingress" + protocol = "tcp" + + # Use the computed security_group_id from the load balancer + remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + + port_range = { + min = 80 + max = 80 + } +} + +# 6. Create the target server and assign the new security group to it +resource "stackit_server" "target_server" { + project_id = var.project_id + name = "remote-target-server" + machine_type = "c1.1" + availability_zone = "eu01-1" + + boot_volume = { + source_type = "image" + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + size = 10 + } + + network_interfaces = [{ + network_id = stackit_network.target_network.id + security_groups = [stackit_security_group.target_sg.id] + }] + + # Ensure the rule is created before the server + depends_on = [stackit_security_group_rule.allow_lb_ingress] +} + # Only use the import statement, if you want to import an existing loadbalancer import { to = stackit_loadbalancer.import-example diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index fd720c50f..358c651c2 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -91,8 +91,100 @@ resource "stackit_loadbalancer" "example" { } } +# This example shows an advanced setup where the load balancer is in one +# network and the target server is in another. This requires manual +# security group configuration. + +# 1. Create a network for the Load Balancer +resource "stackit_network" "lb_network" { + project_id = var.project_id + name = "lb-network" + ipv4_prefix = "192.168.1.0/24" +} + +# 2. Create a separate network for the Target Server +resource "stackit_network" "target_network" { + project_id = var.project_id + name = "target-network" + ipv4_prefix = "192.168.2.0/24" +} + +# 3. Create the Load Balancer and disable automatic security groups +resource "stackit_loadbalancer" "example_advanced" { + project_id = var.project_id + name = "advanced-lb" + + # This is the key setting for manual mode + disable_security_group_assignment = true + + networks = [{ + network_id = stackit_network.lb_network.id + role = "ROLE_LISTENERS_AND_TARGETS" + }] + + target_pools = [{ + name = "cross-network-pool" + target_port = 80 + targets = [{ + display_name = "remote-target-server" + ip = stackit_server.target_server.network_interfaces[0].ipv4 + }] + }] + + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] +} + +# 4. Create a new security group for the target server +resource "stackit_security_group" "target_sg" { + project_id = var.project_id + name = "target-sg-for-lb-access" + description = "Allows ingress traffic from the advanced load balancer." +} + +# 5. Create a rule to allow traffic FROM the load balancer +# This is the core of the manual setup. +resource "stackit_security_group_rule" "allow_lb_ingress" { + security_group_id = stackit_security_group.target_sg.id + direction = "ingress" + protocol = "tcp" + + # Use the computed security_group_id from the load balancer + remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + + port_range = { + min = 80 + max = 80 + } +} + +# 6. Create the target server and assign the new security group to it +resource "stackit_server" "target_server" { + project_id = var.project_id + name = "remote-target-server" + machine_type = "c1.1" + availability_zone = "eu01-1" + + boot_volume = { + source_type = "image" + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + size = 10 + } + + network_interfaces = [{ + network_id = stackit_network.target_network.id + security_groups = [stackit_security_group.target_sg.id] + }] + + # Ensure the rule is created before the server + depends_on = [stackit_security_group_rule.allow_lb_ingress] +} + # Only use the import statement, if you want to import an existing loadbalancer import { to = stackit_loadbalancer.import-example id = "${var.project_id},${var.region},${var.loadbalancer_name}" -} +} \ No newline at end of file From 53e5f8076245de5667f0c2403c44f85a87b2a0fc Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 25 Jul 2025 12:25:36 +0200 Subject: [PATCH 27/63] terraform fmt --- docs/resources/loadbalancer.md | 8 +++---- .../stackit_loadbalancer/resource.tf | 8 +++---- .../loadbalancer/testfiles/resource-max.tf | 22 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index f27aa14ea..458a2c522 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -116,15 +116,15 @@ resource "stackit_loadbalancer" "example" { # 1. Create a network for the Load Balancer resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = var.project_id + name = "lb-network" ipv4_prefix = "192.168.1.0/24" } # 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = var.project_id + name = "target-network" ipv4_prefix = "192.168.2.0/24" } diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 358c651c2..3aa5c3b7f 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -97,15 +97,15 @@ resource "stackit_loadbalancer" "example" { # 1. Create a network for the Load Balancer resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = var.project_id + name = "lb-network" ipv4_prefix = "192.168.1.0/24" } # 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = var.project_id + name = "target-network" ipv4_prefix = "192.168.2.0/24" } diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf index ac0d913a1..491a64803 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf @@ -73,9 +73,9 @@ resource "stackit_server" "server" { } resource "stackit_loadbalancer" "loadbalancer" { - project_id = var.project_id - name = var.loadbalancer_name - plan_id = var.plan_id + project_id = var.project_id + name = var.loadbalancer_name + plan_id = var.plan_id disable_security_group_assignment = var.disable_security_group_assignment target_pools = [ { @@ -122,14 +122,14 @@ resource "stackit_loadbalancer" "loadbalancer" { private_network_only = var.private_network_only acl = [var.acl] observability = { - logs = { - credentials_ref = stackit_loadbalancer_observability_credential.logs.credentials_ref - push_url = var.observability_logs_push_url - } - metrics = { - credentials_ref = stackit_loadbalancer_observability_credential.metrics.credentials_ref - push_url = var.observability_metrics_push_url - } + logs = { + credentials_ref = stackit_loadbalancer_observability_credential.logs.credentials_ref + push_url = var.observability_logs_push_url + } + metrics = { + credentials_ref = stackit_loadbalancer_observability_credential.metrics.credentials_ref + push_url = var.observability_metrics_push_url + } } } external_address = stackit_public_ip.public_ip.ip From 52e6da981e73cc69f0c5f64f9ab15772714fd9d5 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Thu, 31 Jul 2025 08:56:28 +0200 Subject: [PATCH 28/63] add PR feedback --- docs/data-sources/loadbalancer.md | 2 +- docs/resources/loadbalancer.md | 4 +--- examples/resources/stackit_loadbalancer/resource.tf | 1 + .../services/loadbalancer/loadbalancer/datasource.go | 3 ++- .../internal/services/loadbalancer/loadbalancer_acc_test.go | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 7296336b1..687adbeba 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -41,7 +41,7 @@ data "stackit_loadbalancer" "example" { - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) The ID of the security group automatically assigned to the load balancer's targets. +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. - `target_pools` (Attributes List) List of all target pools which will be used in the Load Balancer. Limited to 20. (see [below for nested schema](#nestedatt--target_pools)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 458a2c522..d567b8aad 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -167,6 +167,7 @@ resource "stackit_security_group" "target_sg" { # 5. Create a rule to allow traffic FROM the load balancer # This is the core of the manual setup. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -197,9 +198,6 @@ resource "stackit_server" "target_server" { network_id = stackit_network.target_network.id security_groups = [stackit_security_group.target_sg.id] }] - - # Ensure the rule is created before the server - depends_on = [stackit_security_group_rule.allow_lb_ingress] } # Only use the import statement, if you want to import an existing loadbalancer diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 3aa5c3b7f..f81a96a91 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -148,6 +148,7 @@ resource "stackit_security_group" "target_sg" { # 5. Create a rule to allow traffic FROM the load balancer # This is the core of the manual setup. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index 15fcd892e..aaac7b53c 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -70,6 +70,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", + "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance.", @@ -345,7 +346,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["region"], }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the security group automatically assigned to the load balancer's targets.", + Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", Computed: true, }, }, diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 51bb9293d..34693a7c5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -49,7 +49,7 @@ var testConfigVarsMin = config.Variables{ var testConfigVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "plan_id": config.StringVariable("p10"), - "disable_security_group_assignment": config.StringVariable("true"), + "disable_security_group_assignment": config.BoolVariable(true), "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), @@ -239,7 +239,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), @@ -311,7 +311,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), From 588d00f13a7a68be407b4d9b4c054c770b5fb9ad Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 14:59:37 +0200 Subject: [PATCH 29/63] update stackit_loadbalancer resource to no longer wait until LB becomes ready if disabletargetsecuritygroupassignment is true --- .../loadbalancer/loadbalancer/resource.go | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 9f43d00f4..cd4f3a946 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -721,15 +721,50 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) return } - - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) - return + loadBalancerName := *createResp.Name + + // This variable will hold the final load balancer object + var lb *loadbalancer.LoadBalancer + + if model.DisableSecurityGroupAssignment.ValueBool() { + // MANUAL MODE: Manually poll the resource until the security_group_id is available. + timeout := 5 * time.Minute + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-time.After(timeout): + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("timed out waiting for security_group_id to be populated for load balancer %q", loadBalancerName)) + return + case <-ticker.C: + getResp, err := r.client.GetLoadBalancer(ctx, projectId, region, loadBalancerName).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("polling for security_group_id: %v", err)) + return + } + + // Check if the security group has been created and attached yet. + if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { + lb = getResp // Success! The ID is available. + goto POLLING_DONE + } + // The ID is not ready yet, the loop will continue on the next tick. + } + } + POLLING_DONE: + } else { + // AUTOMATIC MODE: Wait for the load balancer to become active. + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) + return + } + lb = waitResp } // Map response body to schema - err = mapFields(ctx, waitResp, &model, region) + err = mapFields(ctx, lb, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return From 795c64a98baf529911737371e1d2814da8cf657f Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:02:23 +0200 Subject: [PATCH 30/63] cleanup --- .../services/loadbalancer/loadbalancer/resource.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index cd4f3a946..659c97ec5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -696,7 +696,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf // Create creates the resource and sets the initial Terraform state. func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -708,14 +707,12 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - // Generate API request body from model payload, err := toCreatePayload(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err)) return } - // Create a new load balancer createResp, err := r.client.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) @@ -723,7 +720,6 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - // This variable will hold the final load balancer object var lb *loadbalancer.LoadBalancer if model.DisableSecurityGroupAssignment.ValueBool() { @@ -744,9 +740,8 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe return } - // Check if the security group has been created and attached yet. if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { - lb = getResp // Success! The ID is available. + lb = getResp goto POLLING_DONE } // The ID is not ready yet, the loop will continue on the next tick. @@ -763,14 +758,12 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe lb = waitResp } - // Map response body to schema err = mapFields(ctx, lb, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return } - // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { From 0e51e0780ee50aed7d7a5afa1be759d0d750919e Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:51:11 +0200 Subject: [PATCH 31/63] update docs and example --- docs/resources/loadbalancer.md | 75 ++++++++++--------- .../stackit_loadbalancer/resource.tf | 75 ++++++++++--------- 2 files changed, 80 insertions(+), 70 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index d567b8aad..4f1b78d16 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -110,30 +110,34 @@ resource "stackit_loadbalancer" "example" { } } -# This example shows an advanced setup where the load balancer is in one +# This example demonstrates an advanced setup where the Load Balancer is in one # network and the target server is in another. This requires manual -# security group configuration. +# security group configuration using the `disable_security_group_assignment` +# and `security_group_id` attributes. -# 1. Create a network for the Load Balancer +# We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" ipv4_prefix = "192.168.1.0/24" } -# 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" ipv4_prefix = "192.168.2.0/24" } -# 3. Create the Load Balancer and disable automatic security groups -resource "stackit_loadbalancer" "example_advanced" { - project_id = var.project_id - name = "advanced-lb" +resource "stackit_public_ip" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_loadbalancer" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-advanced-lb" + external_address = stackit_public_ip.example.ip - # This is the key setting for manual mode + # Key setting for manual mode: disables automatic security group handling. disable_security_group_assignment = true networks = [{ @@ -141,39 +145,38 @@ resource "stackit_loadbalancer" "example_advanced" { role = "ROLE_LISTENERS_AND_TARGETS" }] + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] + target_pools = [{ name = "cross-network-pool" target_port = 80 targets = [{ - display_name = "remote-target-server" - ip = stackit_server.target_server.network_interfaces[0].ipv4 + display_name = stackit_server.example.name + ip = stackit_network_interface.nic.ipv4 }] }] - - listeners = [{ - port = 80 - protocol = "PROTOCOL_TCP" - target_pool = "cross-network-pool" - }] } -# 4. Create a new security group for the target server +# Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { project_id = var.project_id name = "target-sg-for-lb-access" - description = "Allows ingress traffic from the advanced load balancer." + description = "Allows ingress traffic from the example load balancer." } -# 5. Create a rule to allow traffic FROM the load balancer -# This is the core of the manual setup. +# Create a rule to allow traffic FROM the load balancer. +# This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" - # Use the computed security_group_id from the load balancer - remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + # This is the crucial link: it allows traffic from the LB's security group. + remote_security_group_id = stackit_loadbalancer.example.security_group_id port_range = { min = 80 @@ -181,22 +184,24 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } } -# 6. Create the target server and assign the new security group to it -resource "stackit_server" "target_server" { +resource "stackit_server" "example" { project_id = var.project_id - name = "remote-target-server" + name = "example-remote-target" machine_type = "c1.1" - availability_zone = "eu01-1" + availability_zone = data.stackit_availability_zones.example.names[0] boot_volume = { source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + source_id = data.stackit_image.example_ubuntu.id size = 10 } network_interfaces = [{ - network_id = stackit_network.target_network.id - security_groups = [stackit_security_group.target_sg.id] + network_id = stackit_network.target_network.id + # Assign the manually configured security group to the server's NIC. + security_groups = [ + stackit_security_group.target_sg.id + ] }] } diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index f81a96a91..89e93b155 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -91,30 +91,34 @@ resource "stackit_loadbalancer" "example" { } } -# This example shows an advanced setup where the load balancer is in one +# This example demonstrates an advanced setup where the Load Balancer is in one # network and the target server is in another. This requires manual -# security group configuration. +# security group configuration using the `disable_security_group_assignment` +# and `security_group_id` attributes. -# 1. Create a network for the Load Balancer +# We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" ipv4_prefix = "192.168.1.0/24" } -# 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" ipv4_prefix = "192.168.2.0/24" } -# 3. Create the Load Balancer and disable automatic security groups -resource "stackit_loadbalancer" "example_advanced" { - project_id = var.project_id - name = "advanced-lb" +resource "stackit_public_ip" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_loadbalancer" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-advanced-lb" + external_address = stackit_public_ip.example.ip - # This is the key setting for manual mode + # Key setting for manual mode: disables automatic security group handling. disable_security_group_assignment = true networks = [{ @@ -122,39 +126,38 @@ resource "stackit_loadbalancer" "example_advanced" { role = "ROLE_LISTENERS_AND_TARGETS" }] + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] + target_pools = [{ name = "cross-network-pool" target_port = 80 targets = [{ - display_name = "remote-target-server" - ip = stackit_server.target_server.network_interfaces[0].ipv4 + display_name = stackit_server.example.name + ip = stackit_network_interface.nic.ipv4 }] }] - - listeners = [{ - port = 80 - protocol = "PROTOCOL_TCP" - target_pool = "cross-network-pool" - }] } -# 4. Create a new security group for the target server +# Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { project_id = var.project_id name = "target-sg-for-lb-access" - description = "Allows ingress traffic from the advanced load balancer." + description = "Allows ingress traffic from the example load balancer." } -# 5. Create a rule to allow traffic FROM the load balancer -# This is the core of the manual setup. +# Create a rule to allow traffic FROM the load balancer. +# This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" - # Use the computed security_group_id from the load balancer - remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + # This is the crucial link: it allows traffic from the LB's security group. + remote_security_group_id = stackit_loadbalancer.example.security_group_id port_range = { min = 80 @@ -162,22 +165,24 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } } -# 6. Create the target server and assign the new security group to it -resource "stackit_server" "target_server" { +resource "stackit_server" "example" { project_id = var.project_id - name = "remote-target-server" + name = "example-remote-target" machine_type = "c1.1" - availability_zone = "eu01-1" + availability_zone = data.stackit_availability_zones.example.names[0] boot_volume = { source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + source_id = data.stackit_image.example_ubuntu.id size = 10 } network_interfaces = [{ - network_id = stackit_network.target_network.id - security_groups = [stackit_security_group.target_sg.id] + network_id = stackit_network.target_network.id + # Assign the manually configured security group to the server's NIC. + security_groups = [ + stackit_security_group.target_sg.id + ] }] # Ensure the rule is created before the server From 2c9f1f214a62b3ce0c77eb4be89a5c59983ded7d Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:53:59 +0200 Subject: [PATCH 32/63] update docs and example --- docs/data-sources/loadbalancer.md | 2 +- .../internal/services/loadbalancer/loadbalancer/datasource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 687adbeba..d3c1c4eae 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -33,7 +33,7 @@ data "stackit_loadbalancer" "example" { ### Read-Only -- `disable_security_group_assignment` (Boolean) Disables the target security group assignment. +- `disable_security_group_assignment` (Boolean) If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners)) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index aaac7b53c..e00613931 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -128,7 +128,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Computed: true, }, "disable_security_group_assignment": schema.BoolAttribute{ - Description: "Disables the target security group assignment.", + Description: descriptions["disable_security_group_assignment"], Computed: true, }, "plan_id": schema.StringAttribute{ From fbc2293ebb65b85bd47b9d111bc235c5e3b0abfe Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:56:41 +0200 Subject: [PATCH 33/63] update docs and example --- docs/resources/loadbalancer.md | 5 +++-- examples/resources/stackit_loadbalancer/resource.tf | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 4f1b78d16..2ea0fa58c 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -163,7 +163,7 @@ resource "stackit_loadbalancer" "example" { # Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "target-sg-for-lb-access" description = "Allows ingress traffic from the example load balancer." } @@ -171,6 +171,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -185,7 +186,7 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } resource "stackit_server" "example" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" availability_zone = data.stackit_availability_zones.example.names[0] diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 89e93b155..8cc8110d4 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -144,7 +144,7 @@ resource "stackit_loadbalancer" "example" { # Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "target-sg-for-lb-access" description = "Allows ingress traffic from the example load balancer." } @@ -152,6 +152,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -166,7 +167,7 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } resource "stackit_server" "example" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" availability_zone = data.stackit_availability_zones.example.names[0] From 41951d2ea13826ac49859dc626c528caaf9ecd62 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 16:02:17 +0200 Subject: [PATCH 34/63] update docs and example --- docs/resources/loadbalancer.md | 16 +++++++++------- .../resources/stackit_loadbalancer/resource.tf | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 2ea0fa58c..a5b80ed98 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -117,15 +117,17 @@ resource "stackit_loadbalancer" "example" { # We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "lb-network-example" - ipv4_prefix = "192.168.1.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" + ipv4_prefix = "192.168.1.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_network" "target_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "target-network-example" - ipv4_prefix = "192.168.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" + ipv4_prefix = "192.168.2.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_public_ip" "example" { @@ -171,7 +173,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 8cc8110d4..336744e29 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -98,15 +98,17 @@ resource "stackit_loadbalancer" "example" { # We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "lb-network-example" - ipv4_prefix = "192.168.1.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" + ipv4_prefix = "192.168.1.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_network" "target_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "target-network-example" - ipv4_prefix = "192.168.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" + ipv4_prefix = "192.168.2.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_public_ip" "example" { @@ -152,7 +154,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" From 854044a26779fd11749076312edde80e77aed305 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 16:19:07 +0200 Subject: [PATCH 35/63] update docs and example --- .../internal/services/loadbalancer/loadbalancer/datasource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index e00613931..c72306656 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -346,7 +346,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["region"], }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Description: descriptions["security_group_id"], Computed: true, }, }, From da756fcdb2fd4f6790e3460317d68f8a36674dd4 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:23:35 +0200 Subject: [PATCH 36/63] revert to simpler function to allow the LB to become active before proceeding, updated docs --- docs/resources/loadbalancer.md | 26 +++++----- .../stackit_loadbalancer/resource.tf | 27 +++++----- .../loadbalancer/loadbalancer/resource.go | 49 +++++-------------- 3 files changed, 41 insertions(+), 61 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index a5b80ed98..1712523de 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -143,7 +143,7 @@ resource "stackit_loadbalancer" "example" { disable_security_group_assignment = true networks = [{ - network_id = stackit_network.lb_network.id + network_id = stackit_network.lb_network.network_id role = "ROLE_LISTENERS_AND_TARGETS" }] @@ -176,7 +176,9 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" - protocol = "tcp" + protocol = { + name = "tcp" + } # This is the crucial link: it allows traffic from the LB's security group. remote_security_group_id = stackit_loadbalancer.example.security_group_id @@ -191,21 +193,23 @@ resource "stackit_server" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" - availability_zone = data.stackit_availability_zones.example.names[0] + availability_zone = "eu01-1" boot_volume = { source_type = "image" - source_id = data.stackit_image.example_ubuntu.id + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 10 } - network_interfaces = [{ - network_id = stackit_network.target_network.id - # Assign the manually configured security group to the server's NIC. - security_groups = [ - stackit_security_group.target_sg.id - ] - }] + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.target_network.network_id + security_group_ids = [stackit_security_group.target_sg.security_group_id] } # Only use the import statement, if you want to import an existing loadbalancer diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 336744e29..5af21b46e 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -124,7 +124,7 @@ resource "stackit_loadbalancer" "example" { disable_security_group_assignment = true networks = [{ - network_id = stackit_network.lb_network.id + network_id = stackit_network.lb_network.network_id role = "ROLE_LISTENERS_AND_TARGETS" }] @@ -157,7 +157,9 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" - protocol = "tcp" + protocol = { + name = "tcp" + } # This is the crucial link: it allows traffic from the LB's security group. remote_security_group_id = stackit_loadbalancer.example.security_group_id @@ -172,24 +174,23 @@ resource "stackit_server" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" - availability_zone = data.stackit_availability_zones.example.names[0] + availability_zone = "eu01-1" boot_volume = { source_type = "image" - source_id = data.stackit_image.example_ubuntu.id + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 10 } - network_interfaces = [{ - network_id = stackit_network.target_network.id - # Assign the manually configured security group to the server's NIC. - security_groups = [ - stackit_security_group.target_sg.id - ] - }] + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] +} - # Ensure the rule is created before the server - depends_on = [stackit_security_group_rule.allow_lb_ingress] +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.target_network.network_id + security_group_ids = [stackit_security_group.target_sg.security_group_id] } # Only use the import statement, if you want to import an existing loadbalancer diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 659c97ec5..b6a66fd9c 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -696,6 +696,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf // Create creates the resource and sets the initial Terraform state. func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -707,12 +708,14 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) + // Generate API request body from model payload, err := toCreatePayload(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err)) return } + // Create a new load balancer createResp, err := r.client.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) @@ -720,50 +723,22 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - var lb *loadbalancer.LoadBalancer - - if model.DisableSecurityGroupAssignment.ValueBool() { - // MANUAL MODE: Manually poll the resource until the security_group_id is available. - timeout := 5 * time.Minute - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-time.After(timeout): - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("timed out waiting for security_group_id to be populated for load balancer %q", loadBalancerName)) - return - case <-ticker.C: - getResp, err := r.client.GetLoadBalancer(ctx, projectId, region, loadBalancerName).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("polling for security_group_id: %v", err)) - return - } - - if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { - lb = getResp - goto POLLING_DONE - } - // The ID is not ready yet, the loop will continue on the next tick. - } - } - POLLING_DONE: - } else { - // AUTOMATIC MODE: Wait for the load balancer to become active. - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) - return - } - lb = waitResp + // Wait for the load balancer to become active. + // This single handler is now used for both automatic and manual modes. + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) + return } - err = mapFields(ctx, lb, &model, region) + // Map response body to schema + err = mapFields(ctx, waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { From 5f6e84e159f494183357f941dc5ab64a07892c71 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:28:00 +0200 Subject: [PATCH 37/63] update description --- docs/resources/loadbalancer.md | 2 +- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 1712523de..39658b0f8 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -242,7 +242,7 @@ import { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. +- `security_group_id` (String) ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index b6a66fd9c..2d30b00f7 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -684,7 +684,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf }, }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Description: descriptions["security_group_id"], Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), From 2c7a44604490c9ef7600589b92f917260aa36ecd Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:31:59 +0200 Subject: [PATCH 38/63] update description --- docs/resources/loadbalancer.md | 2 +- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 39658b0f8..1712523de 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -242,7 +242,7 @@ import { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 2d30b00f7..f471713fd 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -343,6 +343,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques "targets.display_name": "Target display name", "ip": "Target IP", "region": "The resource region. If not defined, the provider region is used.", + "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", } resp.Schema = schema.Schema{ From f1e7cf7255d39c4491bcbdb298486474610f8258 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:33:38 +0200 Subject: [PATCH 39/63] update description --- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index f471713fd..ead3e73fd 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -724,8 +724,6 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - // Wait for the load balancer to become active. - // This single handler is now used for both automatic and manual modes. waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) From c507bdfe088f8d264bab9da558bd67545249bce1 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:36:29 +0200 Subject: [PATCH 40/63] small correction of unnecessary var --- .../internal/services/loadbalancer/loadbalancer/resource.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index ead3e73fd..0beda14f9 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,6 +3,7 @@ package loadbalancer import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -18,7 +19,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" @@ -722,9 +722,8 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) return } - loadBalancerName := *createResp.Name - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) return From bd862ce77d5fbe62f0a837ec054625404f52c7da Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 16:30:28 +0200 Subject: [PATCH 41/63] update example --- docs/resources/loadbalancer.md | 2 +- examples/resources/stackit_loadbalancer/resource.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 1712523de..752782155 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -174,7 +174,7 @@ resource "stackit_security_group" "target_sg" { # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - security_group_id = stackit_security_group.target_sg.id + security_group_id = stackit_security_group.target_sg.security_group_id direction = "ingress" protocol = { name = "tcp" diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 5af21b46e..04441957c 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -155,7 +155,7 @@ resource "stackit_security_group" "target_sg" { # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - security_group_id = stackit_security_group.target_sg.id + security_group_id = stackit_security_group.target_sg.security_group_id direction = "ingress" protocol = { name = "tcp" From dbd035ec1006d338b02d6ab589e84ed25b9ec728 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 15:02:40 +0200 Subject: [PATCH 42/63] Adds the new `disable_security_group_assignment` boolean attribute to the load balancer resource. This allows disabling the automatic assignment of a security group to the targets. --- docs/data-sources/loadbalancer.md | 1 + docs/resources/loadbalancer.md | 1 + .../loadbalancer/loadbalancer/datasource.go | 5 ++ .../loadbalancer/loadbalancer/resource.go | 51 ++++++++++++------- .../loadbalancer/loadbalancer_acc_test.go | 27 ++++++---- .../loadbalancer/testfiles/resource-max.tf | 2 + .../loadbalancer/testfiles/resource-min.tf | 2 +- 7 files changed, 59 insertions(+), 30 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index c59fc8428..65cc06851 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -33,6 +33,7 @@ data "stackit_loadbalancer" "example" { ### Read-Only +- `disable_security_group_assignment` (Boolean) Disables the target security group assignment. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index ab77a41dd..6170e8f95 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -130,6 +130,7 @@ import { ### Optional +- `disable_security_group_assignment` (Boolean) If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index d7f3a2157..d5e3f4f20 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -69,6 +69,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance.", @@ -125,6 +126,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["external_address"], Computed: true, }, + "disable_security_group_assignment": schema.BoolAttribute{ + Description: "Disables the target security group assignment.", + Computed: true, + }, "plan_id": schema.StringAttribute{ Description: descriptions["plan_id"], Computed: true, diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 36e0e1f74..0f6b596b5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,6 +3,7 @@ package loadbalancer import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -48,17 +49,18 @@ var ( ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - ExternalAddress types.String `tfsdk:"external_address"` - Listeners types.List `tfsdk:"listeners"` - Name types.String `tfsdk:"name"` - PlanId types.String `tfsdk:"plan_id"` - Networks types.List `tfsdk:"networks"` - Options types.Object `tfsdk:"options"` - PrivateAddress types.String `tfsdk:"private_address"` - TargetPools types.List `tfsdk:"target_pools"` - Region types.String `tfsdk:"region"` + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + ExternalAddress types.String `tfsdk:"external_address"` + DisableSecurityGroupAssignment types.Bool `tfsdk:"disable_security_group_assignment"` + Listeners types.List `tfsdk:"listeners"` + Name types.String `tfsdk:"name"` + PlanId types.String `tfsdk:"plan_id"` + Networks types.List `tfsdk:"networks"` + Options types.Object `tfsdk:"options"` + PrivateAddress types.String `tfsdk:"private_address"` + TargetPools types.List `tfsdk:"target_pools"` + Region types.String `tfsdk:"region"` } // Struct corresponding to Model.Listeners[i] @@ -303,6 +305,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance. " + utils.SupportedValuesDocumentation(protocolOptions), @@ -373,6 +376,16 @@ The example below creates the supporting infrastructure using the STACKIT Terraf stringplanmodifier.RequiresReplace(), }, }, + "disable_security_group_assignment": schema.BoolAttribute{ + Description: descriptions["disable_security_group_assignment"], + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + boolplanmodifier.UseStateForUnknown(), + }, + }, "plan_id": schema.StringAttribute{ Description: descriptions["plan_id"], Optional: true, @@ -907,13 +920,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoa } return &loadbalancer.CreateLoadBalancerPayload{ - ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress), - Listeners: listenersPayload, - Name: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - Networks: networksPayload, - Options: optionsPayload, - TargetPools: targetPoolsPayload, + ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress), + DisableTargetSecurityGroupAssignment: conversion.BoolValueToPointer(model.DisableSecurityGroupAssignment), + Listeners: listenersPayload, + Name: conversion.StringValueToPointer(model.Name), + PlanId: conversion.StringValueToPointer(model.PlanId), + Networks: networksPayload, + Options: optionsPayload, + TargetPools: targetPoolsPayload, }, nil } @@ -1221,6 +1235,7 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg m.PlanId = types.StringPointerValue(lb.PlanId) m.ExternalAddress = types.StringPointerValue(lb.ExternalAddress) m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress) + m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment) err := mapListeners(lb, m) if err != nil { diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 2d3c974b9..599b5ffa1 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -47,17 +47,18 @@ var testConfigVarsMin = config.Variables{ } var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "plan_id": config.StringVariable("p10"), - "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "target_pool_name": config.StringVariable("example-target-pool"), - "target_port": config.StringVariable("5432"), - "target_display_name": config.StringVariable("example-target"), - "listener_port": config.StringVariable("5432"), - "listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), - "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), + "project_id": config.StringVariable(testutil.ProjectId), + "plan_id": config.StringVariable("p10"), + "disable_security_group_assignment": config.StringVariable("true"), + "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "target_pool_name": config.StringVariable("example-target-pool"), + "target_port": config.StringVariable("5432"), + "target_display_name": config.StringVariable("example-target"), + "listener_port": config.StringVariable("5432"), + "listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), + "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), "listener_display_name": config.StringVariable("example-listener"), "listener_server_name_indicators": config.StringVariable("acc-test.runs.onstackit.cloud"), @@ -118,6 +119,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), @@ -168,6 +170,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), @@ -234,6 +237,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), @@ -304,6 +308,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf index aabd752d5..ac0d913a1 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf @@ -11,6 +11,7 @@ variable "target_display_name" {} variable "listener_port" {} variable "listener_protocol" {} variable "network_role" {} +variable "disable_security_group_assignment" {} variable "listener_display_name" {} variable "listener_server_name_indicators" {} @@ -75,6 +76,7 @@ resource "stackit_loadbalancer" "loadbalancer" { project_id = var.project_id name = var.loadbalancer_name plan_id = var.plan_id + disable_security_group_assignment = var.disable_security_group_assignment target_pools = [ { name = var.target_pool_name diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf index 2b6d65793..b7ebf3d7b 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf @@ -19,7 +19,7 @@ resource "stackit_network" "network" { project_id = var.project_id name = var.network_name ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.2.0/25" + ipv4_prefix = "192.168.4.0/25" # todo: change me back to 192.168.2.0/25 routed = "true" } From 3c9d0654ece4cc1427129ec20cdc492fa46889ab Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:04:37 +0200 Subject: [PATCH 43/63] Exposes the ID of the Load Balancer's egress security group. This allows users to reference the ID in security group rules for targets in other networks, enabling cross-network traffic. --- docs/data-sources/loadbalancer.md | 1 + docs/resources/loadbalancer.md | 1 + .../loadbalancer/loadbalancer/datasource.go | 4 ++++ .../services/loadbalancer/loadbalancer/resource.go | 13 +++++++++++++ .../loadbalancer/loadbalancer/resource_test.go | 12 +++++++++--- .../services/loadbalancer/loadbalancer_acc_test.go | 4 ++++ 6 files changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 65cc06851..7296336b1 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -41,6 +41,7 @@ data "stackit_loadbalancer" "example" { - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. - `private_address` (String) Transient private Load Balancer IP address. It can change any time. +- `security_group_id` (String) The ID of the security group automatically assigned to the load balancer's targets. - `target_pools` (Attributes List) List of all target pools which will be used in the Load Balancer. Limited to 20. (see [below for nested schema](#nestedatt--target_pools)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 6170e8f95..1ae11efb8 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -140,6 +140,7 @@ import { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index d5e3f4f20..15fcd892e 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -344,6 +344,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Optional: true, Description: descriptions["region"], }, + "security_group_id": schema.StringAttribute{ + Description: "The ID of the security group automatically assigned to the load balancer's targets.", + Computed: true, + }, }, } } diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 0f6b596b5..06e633c5b 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -61,6 +61,7 @@ type Model struct { PrivateAddress types.String `tfsdk:"private_address"` TargetPools types.List `tfsdk:"target_pools"` Region types.String `tfsdk:"region"` + SecurityGroupId types.String `tfsdk:"security_group_id"` } // Struct corresponding to Model.Listeners[i] @@ -682,6 +683,13 @@ The example below creates the supporting infrastructure using the STACKIT Terraf stringplanmodifier.RequiresReplace(), }, }, + "security_group_id": schema.StringAttribute{ + Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, }, } } @@ -1237,6 +1245,11 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress) m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment) + if lb.TargetSecurityGroup != nil { + m.SecurityGroupId = types.StringPointerValue(lb.TargetSecurityGroup.Id) + } else { + m.SecurityGroupId = types.StringNull() + } err := mapListeners(lb, m) if err != nil { return fmt.Errorf("mapping listeners: %w", err) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go index 2a1a85b79..810e9c395 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go @@ -489,9 +489,10 @@ func TestMapFields(t *testing.T) { }), }), }), - PrivateAddress: types.StringNull(), - TargetPools: types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}), - Region: types.StringValue(testRegion), + PrivateAddress: types.StringNull(), + SecurityGroupId: types.StringNull(), + TargetPools: types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}), + Region: types.StringValue(testRegion), }, true, }, @@ -536,6 +537,10 @@ func TestMapFields(t *testing.T) { }, }, }), + TargetSecurityGroup: loadbalancer.LoadBalancerGetTargetSecurityGroupAttributeType(&loadbalancer.SecurityGroup{ + Id: utils.Ptr("sg-id-12345"), + Name: utils.Ptr("sg-name-abcde"), + }), TargetPools: utils.Ptr([]loadbalancer.TargetPool{ { ActiveHealthCheck: utils.Ptr(loadbalancer.ActiveHealthCheck{ @@ -565,6 +570,7 @@ func TestMapFields(t *testing.T) { Id: types.StringValue(id), ProjectId: types.StringValue("pid"), ExternalAddress: types.StringValue("external_address"), + SecurityGroupId: types.StringValue("sg-id-12345"), Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{ types.ObjectValueMust(listenerTypes, map[string]attr.Value{ "display_name": types.StringValue("display_name"), diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 599b5ffa1..51bb9293d 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -124,6 +124,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), + resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), // Loadbalancer observability credentials resource resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -175,6 +176,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), + resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), )}, // Import { @@ -238,6 +240,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), @@ -309,6 +312,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["health_interval"])), From fa164d7905c8d12ea43b9c589a1360cd851ed5c9 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:13:37 +0200 Subject: [PATCH 44/63] Exposes the ID of the Load Balancer's egress security group. This allows users to reference the ID in security group rules for targets in other networks, enabling cross-network traffic. --- .../internal/services/loadbalancer/testfiles/resource-min.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf index b7ebf3d7b..2b6d65793 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf @@ -19,7 +19,7 @@ resource "stackit_network" "network" { project_id = var.project_id name = var.network_name ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.4.0/25" # todo: change me back to 192.168.2.0/25 + ipv4_prefix = "192.168.2.0/25" routed = "true" } From aaa1aa3580565bef827a6b031bee10d3c5a538b0 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Mon, 21 Jul 2025 16:22:44 +0200 Subject: [PATCH 45/63] change order of imports --- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 06e633c5b..9f43d00f4 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,7 +3,6 @@ package loadbalancer import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -19,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" From 616d20227a12d9674ab4e94f12833a7a6b76f774 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Tue, 22 Jul 2025 14:59:43 +0200 Subject: [PATCH 46/63] added a example for the new feature --- .../stackit_loadbalancer/resource.tf | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index fd720c50f..27ea8b50b 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -91,6 +91,98 @@ resource "stackit_loadbalancer" "example" { } } +# This example shows an advanced setup where the load balancer is in one +# network and the target server is in another. This requires manual +# security group configuration. + +# 1. Create a network for the Load Balancer +resource "stackit_network" "lb_network" { + project_id = var.project_id + name = "lb-network" + ipv4_prefix = "192.168.1.0/24" +} + +# 2. Create a separate network for the Target Server +resource "stackit_network" "target_network" { + project_id = var.project_id + name = "target-network" + ipv4_prefix = "192.168.2.0/24" +} + +# 3. Create the Load Balancer and disable automatic security groups +resource "stackit_loadbalancer" "example_advanced" { + project_id = var.project_id + name = "advanced-lb" + + # This is the key setting for manual mode + disable_security_group_assignment = true + + networks = [{ + network_id = stackit_network.lb_network.id + role = "ROLE_LISTENERS_AND_TARGETS" + }] + + target_pools = [{ + name = "cross-network-pool" + target_port = 80 + targets = [{ + display_name = "remote-target-server" + ip = stackit_server.target_server.network_interfaces[0].ipv4 + }] + }] + + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] +} + +# 4. Create a new security group for the target server +resource "stackit_security_group" "target_sg" { + project_id = var.project_id + name = "target-sg-for-lb-access" + description = "Allows ingress traffic from the advanced load balancer." +} + +# 5. Create a rule to allow traffic FROM the load balancer +# This is the core of the manual setup. +resource "stackit_security_group_rule" "allow_lb_ingress" { + security_group_id = stackit_security_group.target_sg.id + direction = "ingress" + protocol = "tcp" + + # Use the computed security_group_id from the load balancer + remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + + port_range = { + min = 80 + max = 80 + } +} + +# 6. Create the target server and assign the new security group to it +resource "stackit_server" "target_server" { + project_id = var.project_id + name = "remote-target-server" + machine_type = "c1.1" + availability_zone = "eu01-1" + + boot_volume = { + source_type = "image" + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + size = 10 + } + + network_interfaces = [{ + network_id = stackit_network.target_network.id + security_groups = [stackit_security_group.target_sg.id] + }] + + # Ensure the rule is created before the server + depends_on = [stackit_security_group_rule.allow_lb_ingress] +} + # Only use the import statement, if you want to import an existing loadbalancer import { to = stackit_loadbalancer.import-example From 0dd3fc3fc4d2597d560148e46d62714bd597a921 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Tue, 22 Jul 2025 16:46:54 +0200 Subject: [PATCH 47/63] update docs --- docs/resources/loadbalancer.md | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 1ae11efb8..f27aa14ea 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -110,6 +110,98 @@ resource "stackit_loadbalancer" "example" { } } +# This example shows an advanced setup where the load balancer is in one +# network and the target server is in another. This requires manual +# security group configuration. + +# 1. Create a network for the Load Balancer +resource "stackit_network" "lb_network" { + project_id = var.project_id + name = "lb-network" + ipv4_prefix = "192.168.1.0/24" +} + +# 2. Create a separate network for the Target Server +resource "stackit_network" "target_network" { + project_id = var.project_id + name = "target-network" + ipv4_prefix = "192.168.2.0/24" +} + +# 3. Create the Load Balancer and disable automatic security groups +resource "stackit_loadbalancer" "example_advanced" { + project_id = var.project_id + name = "advanced-lb" + + # This is the key setting for manual mode + disable_security_group_assignment = true + + networks = [{ + network_id = stackit_network.lb_network.id + role = "ROLE_LISTENERS_AND_TARGETS" + }] + + target_pools = [{ + name = "cross-network-pool" + target_port = 80 + targets = [{ + display_name = "remote-target-server" + ip = stackit_server.target_server.network_interfaces[0].ipv4 + }] + }] + + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] +} + +# 4. Create a new security group for the target server +resource "stackit_security_group" "target_sg" { + project_id = var.project_id + name = "target-sg-for-lb-access" + description = "Allows ingress traffic from the advanced load balancer." +} + +# 5. Create a rule to allow traffic FROM the load balancer +# This is the core of the manual setup. +resource "stackit_security_group_rule" "allow_lb_ingress" { + security_group_id = stackit_security_group.target_sg.id + direction = "ingress" + protocol = "tcp" + + # Use the computed security_group_id from the load balancer + remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + + port_range = { + min = 80 + max = 80 + } +} + +# 6. Create the target server and assign the new security group to it +resource "stackit_server" "target_server" { + project_id = var.project_id + name = "remote-target-server" + machine_type = "c1.1" + availability_zone = "eu01-1" + + boot_volume = { + source_type = "image" + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + size = 10 + } + + network_interfaces = [{ + network_id = stackit_network.target_network.id + security_groups = [stackit_security_group.target_sg.id] + }] + + # Ensure the rule is created before the server + depends_on = [stackit_security_group_rule.allow_lb_ingress] +} + # Only use the import statement, if you want to import an existing loadbalancer import { to = stackit_loadbalancer.import-example From 61b4856aecacc3b02fa8e875281df0fd71396898 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 25 Jul 2025 12:25:36 +0200 Subject: [PATCH 48/63] terraform fmt --- docs/resources/loadbalancer.md | 8 +++---- .../stackit_loadbalancer/resource.tf | 8 +++---- .../loadbalancer/testfiles/resource-max.tf | 22 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index f27aa14ea..458a2c522 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -116,15 +116,15 @@ resource "stackit_loadbalancer" "example" { # 1. Create a network for the Load Balancer resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = var.project_id + name = "lb-network" ipv4_prefix = "192.168.1.0/24" } # 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = var.project_id + name = "target-network" ipv4_prefix = "192.168.2.0/24" } diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 27ea8b50b..f1d196ef1 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -97,15 +97,15 @@ resource "stackit_loadbalancer" "example" { # 1. Create a network for the Load Balancer resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = var.project_id + name = "lb-network" ipv4_prefix = "192.168.1.0/24" } # 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = var.project_id + name = "target-network" ipv4_prefix = "192.168.2.0/24" } diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf index ac0d913a1..491a64803 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf @@ -73,9 +73,9 @@ resource "stackit_server" "server" { } resource "stackit_loadbalancer" "loadbalancer" { - project_id = var.project_id - name = var.loadbalancer_name - plan_id = var.plan_id + project_id = var.project_id + name = var.loadbalancer_name + plan_id = var.plan_id disable_security_group_assignment = var.disable_security_group_assignment target_pools = [ { @@ -122,14 +122,14 @@ resource "stackit_loadbalancer" "loadbalancer" { private_network_only = var.private_network_only acl = [var.acl] observability = { - logs = { - credentials_ref = stackit_loadbalancer_observability_credential.logs.credentials_ref - push_url = var.observability_logs_push_url - } - metrics = { - credentials_ref = stackit_loadbalancer_observability_credential.metrics.credentials_ref - push_url = var.observability_metrics_push_url - } + logs = { + credentials_ref = stackit_loadbalancer_observability_credential.logs.credentials_ref + push_url = var.observability_logs_push_url + } + metrics = { + credentials_ref = stackit_loadbalancer_observability_credential.metrics.credentials_ref + push_url = var.observability_metrics_push_url + } } } external_address = stackit_public_ip.public_ip.ip From f778676c7374f8cfacf6af729f2d2bf0c94a9366 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Thu, 31 Jul 2025 08:56:28 +0200 Subject: [PATCH 49/63] add PR feedback --- docs/data-sources/loadbalancer.md | 2 +- docs/resources/loadbalancer.md | 4 +--- examples/resources/stackit_loadbalancer/resource.tf | 4 +--- .../services/loadbalancer/loadbalancer/datasource.go | 3 ++- .../internal/services/loadbalancer/loadbalancer_acc_test.go | 6 +++--- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 7296336b1..687adbeba 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -41,7 +41,7 @@ data "stackit_loadbalancer" "example" { - `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options)) - `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`. - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) The ID of the security group automatically assigned to the load balancer's targets. +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. - `target_pools` (Attributes List) List of all target pools which will be used in the Load Balancer. Limited to 20. (see [below for nested schema](#nestedatt--target_pools)) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 458a2c522..d567b8aad 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -167,6 +167,7 @@ resource "stackit_security_group" "target_sg" { # 5. Create a rule to allow traffic FROM the load balancer # This is the core of the manual setup. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -197,9 +198,6 @@ resource "stackit_server" "target_server" { network_id = stackit_network.target_network.id security_groups = [stackit_security_group.target_sg.id] }] - - # Ensure the rule is created before the server - depends_on = [stackit_security_group_rule.allow_lb_ingress] } # Only use the import statement, if you want to import an existing loadbalancer diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index f1d196ef1..6a583d261 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -148,6 +148,7 @@ resource "stackit_security_group" "target_sg" { # 5. Create a rule to allow traffic FROM the load balancer # This is the core of the manual setup. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -178,9 +179,6 @@ resource "stackit_server" "target_server" { network_id = stackit_network.target_network.id security_groups = [stackit_security_group.target_sg.id] }] - - # Ensure the rule is created before the server - depends_on = [stackit_security_group_rule.allow_lb_ingress] } # Only use the import statement, if you want to import an existing loadbalancer diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index 15fcd892e..aaac7b53c 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -70,6 +70,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe "project_id": "STACKIT project ID to which the Load Balancer is associated.", "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", + "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", "listeners": "List of all listeners which will accept traffic. Limited to 20.", "port": "Port number where we listen for traffic.", "protocol": "Protocol is the highest network protocol we understand to load balance.", @@ -345,7 +346,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["region"], }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the security group automatically assigned to the load balancer's targets.", + Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", Computed: true, }, }, diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 51bb9293d..34693a7c5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -49,7 +49,7 @@ var testConfigVarsMin = config.Variables{ var testConfigVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "plan_id": config.StringVariable("p10"), - "disable_security_group_assignment": config.StringVariable("true"), + "disable_security_group_assignment": config.BoolVariable(true), "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), @@ -239,7 +239,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), @@ -311,7 +311,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "true"), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["healthy_threshold"])), From 6396efa860a9f6c9d2f033d445e171c5465d8d6f Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 14:59:37 +0200 Subject: [PATCH 50/63] update stackit_loadbalancer resource to no longer wait until LB becomes ready if disabletargetsecuritygroupassignment is true --- .../loadbalancer/loadbalancer/resource.go | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 9f43d00f4..cd4f3a946 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -721,15 +721,50 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) return } - - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) - return + loadBalancerName := *createResp.Name + + // This variable will hold the final load balancer object + var lb *loadbalancer.LoadBalancer + + if model.DisableSecurityGroupAssignment.ValueBool() { + // MANUAL MODE: Manually poll the resource until the security_group_id is available. + timeout := 5 * time.Minute + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-time.After(timeout): + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("timed out waiting for security_group_id to be populated for load balancer %q", loadBalancerName)) + return + case <-ticker.C: + getResp, err := r.client.GetLoadBalancer(ctx, projectId, region, loadBalancerName).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("polling for security_group_id: %v", err)) + return + } + + // Check if the security group has been created and attached yet. + if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { + lb = getResp // Success! The ID is available. + goto POLLING_DONE + } + // The ID is not ready yet, the loop will continue on the next tick. + } + } + POLLING_DONE: + } else { + // AUTOMATIC MODE: Wait for the load balancer to become active. + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) + return + } + lb = waitResp } // Map response body to schema - err = mapFields(ctx, waitResp, &model, region) + err = mapFields(ctx, lb, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return From c8a768a187479cc8dc314abe2dcf3208d29d2b12 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:02:23 +0200 Subject: [PATCH 51/63] cleanup --- .../services/loadbalancer/loadbalancer/resource.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index cd4f3a946..659c97ec5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -696,7 +696,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf // Create creates the resource and sets the initial Terraform state. func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -708,14 +707,12 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - // Generate API request body from model payload, err := toCreatePayload(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err)) return } - // Create a new load balancer createResp, err := r.client.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) @@ -723,7 +720,6 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - // This variable will hold the final load balancer object var lb *loadbalancer.LoadBalancer if model.DisableSecurityGroupAssignment.ValueBool() { @@ -744,9 +740,8 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe return } - // Check if the security group has been created and attached yet. if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { - lb = getResp // Success! The ID is available. + lb = getResp goto POLLING_DONE } // The ID is not ready yet, the loop will continue on the next tick. @@ -763,14 +758,12 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe lb = waitResp } - // Map response body to schema err = mapFields(ctx, lb, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return } - // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { From b1e18a4e3d94bb9801de2f59ed3f0ae13ad40c89 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:51:11 +0200 Subject: [PATCH 52/63] update docs and example --- docs/resources/loadbalancer.md | 75 ++++++++++--------- .../stackit_loadbalancer/resource.tf | 75 ++++++++++--------- 2 files changed, 80 insertions(+), 70 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index d567b8aad..4f1b78d16 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -110,30 +110,34 @@ resource "stackit_loadbalancer" "example" { } } -# This example shows an advanced setup where the load balancer is in one +# This example demonstrates an advanced setup where the Load Balancer is in one # network and the target server is in another. This requires manual -# security group configuration. +# security group configuration using the `disable_security_group_assignment` +# and `security_group_id` attributes. -# 1. Create a network for the Load Balancer +# We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" ipv4_prefix = "192.168.1.0/24" } -# 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" ipv4_prefix = "192.168.2.0/24" } -# 3. Create the Load Balancer and disable automatic security groups -resource "stackit_loadbalancer" "example_advanced" { - project_id = var.project_id - name = "advanced-lb" +resource "stackit_public_ip" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_loadbalancer" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-advanced-lb" + external_address = stackit_public_ip.example.ip - # This is the key setting for manual mode + # Key setting for manual mode: disables automatic security group handling. disable_security_group_assignment = true networks = [{ @@ -141,39 +145,38 @@ resource "stackit_loadbalancer" "example_advanced" { role = "ROLE_LISTENERS_AND_TARGETS" }] + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] + target_pools = [{ name = "cross-network-pool" target_port = 80 targets = [{ - display_name = "remote-target-server" - ip = stackit_server.target_server.network_interfaces[0].ipv4 + display_name = stackit_server.example.name + ip = stackit_network_interface.nic.ipv4 }] }] - - listeners = [{ - port = 80 - protocol = "PROTOCOL_TCP" - target_pool = "cross-network-pool" - }] } -# 4. Create a new security group for the target server +# Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { project_id = var.project_id name = "target-sg-for-lb-access" - description = "Allows ingress traffic from the advanced load balancer." + description = "Allows ingress traffic from the example load balancer." } -# 5. Create a rule to allow traffic FROM the load balancer -# This is the core of the manual setup. +# Create a rule to allow traffic FROM the load balancer. +# This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" - # Use the computed security_group_id from the load balancer - remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + # This is the crucial link: it allows traffic from the LB's security group. + remote_security_group_id = stackit_loadbalancer.example.security_group_id port_range = { min = 80 @@ -181,22 +184,24 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } } -# 6. Create the target server and assign the new security group to it -resource "stackit_server" "target_server" { +resource "stackit_server" "example" { project_id = var.project_id - name = "remote-target-server" + name = "example-remote-target" machine_type = "c1.1" - availability_zone = "eu01-1" + availability_zone = data.stackit_availability_zones.example.names[0] boot_volume = { source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + source_id = data.stackit_image.example_ubuntu.id size = 10 } network_interfaces = [{ - network_id = stackit_network.target_network.id - security_groups = [stackit_security_group.target_sg.id] + network_id = stackit_network.target_network.id + # Assign the manually configured security group to the server's NIC. + security_groups = [ + stackit_security_group.target_sg.id + ] }] } diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 6a583d261..20d7894c3 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -91,30 +91,34 @@ resource "stackit_loadbalancer" "example" { } } -# This example shows an advanced setup where the load balancer is in one +# This example demonstrates an advanced setup where the Load Balancer is in one # network and the target server is in another. This requires manual -# security group configuration. +# security group configuration using the `disable_security_group_assignment` +# and `security_group_id` attributes. -# 1. Create a network for the Load Balancer +# We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = var.project_id - name = "lb-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" ipv4_prefix = "192.168.1.0/24" } -# 2. Create a separate network for the Target Server resource "stackit_network" "target_network" { - project_id = var.project_id - name = "target-network" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" ipv4_prefix = "192.168.2.0/24" } -# 3. Create the Load Balancer and disable automatic security groups -resource "stackit_loadbalancer" "example_advanced" { - project_id = var.project_id - name = "advanced-lb" +resource "stackit_public_ip" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_loadbalancer" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-advanced-lb" + external_address = stackit_public_ip.example.ip - # This is the key setting for manual mode + # Key setting for manual mode: disables automatic security group handling. disable_security_group_assignment = true networks = [{ @@ -122,39 +126,38 @@ resource "stackit_loadbalancer" "example_advanced" { role = "ROLE_LISTENERS_AND_TARGETS" }] + listeners = [{ + port = 80 + protocol = "PROTOCOL_TCP" + target_pool = "cross-network-pool" + }] + target_pools = [{ name = "cross-network-pool" target_port = 80 targets = [{ - display_name = "remote-target-server" - ip = stackit_server.target_server.network_interfaces[0].ipv4 + display_name = stackit_server.example.name + ip = stackit_network_interface.nic.ipv4 }] }] - - listeners = [{ - port = 80 - protocol = "PROTOCOL_TCP" - target_pool = "cross-network-pool" - }] } -# 4. Create a new security group for the target server +# Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { project_id = var.project_id name = "target-sg-for-lb-access" - description = "Allows ingress traffic from the advanced load balancer." + description = "Allows ingress traffic from the example load balancer." } -# 5. Create a rule to allow traffic FROM the load balancer -# This is the core of the manual setup. +# Create a rule to allow traffic FROM the load balancer. +# This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = var.project_id security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" - # Use the computed security_group_id from the load balancer - remote_security_group_id = stackit_loadbalancer.example_advanced.security_group_id + # This is the crucial link: it allows traffic from the LB's security group. + remote_security_group_id = stackit_loadbalancer.example.security_group_id port_range = { min = 80 @@ -162,22 +165,24 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } } -# 6. Create the target server and assign the new security group to it -resource "stackit_server" "target_server" { +resource "stackit_server" "example" { project_id = var.project_id - name = "remote-target-server" + name = "example-remote-target" machine_type = "c1.1" - availability_zone = "eu01-1" + availability_zone = data.stackit_availability_zones.example.names[0] boot_volume = { source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // e.g., an Ubuntu image ID + source_id = data.stackit_image.example_ubuntu.id size = 10 } network_interfaces = [{ - network_id = stackit_network.target_network.id - security_groups = [stackit_security_group.target_sg.id] + network_id = stackit_network.target_network.id + # Assign the manually configured security group to the server's NIC. + security_groups = [ + stackit_security_group.target_sg.id + ] }] } From ae39a484d25e1bfbd7b2df914168b2cb4b9d2fd2 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:53:59 +0200 Subject: [PATCH 53/63] update docs and example --- docs/data-sources/loadbalancer.md | 2 +- .../internal/services/loadbalancer/loadbalancer/datasource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 687adbeba..d3c1c4eae 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -33,7 +33,7 @@ data "stackit_loadbalancer" "example" { ### Read-Only -- `disable_security_group_assignment` (Boolean) Disables the target security group assignment. +- `disable_security_group_assignment` (Boolean) If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA. When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created. - `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners)) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index aaac7b53c..e00613931 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -128,7 +128,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Computed: true, }, "disable_security_group_assignment": schema.BoolAttribute{ - Description: "Disables the target security group assignment.", + Description: descriptions["disable_security_group_assignment"], Computed: true, }, "plan_id": schema.StringAttribute{ From 00c9c3277bdb9bcf083a83decf502758e3b08500 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 15:56:41 +0200 Subject: [PATCH 54/63] update docs and example --- docs/resources/loadbalancer.md | 5 +++-- examples/resources/stackit_loadbalancer/resource.tf | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 4f1b78d16..2ea0fa58c 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -163,7 +163,7 @@ resource "stackit_loadbalancer" "example" { # Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "target-sg-for-lb-access" description = "Allows ingress traffic from the example load balancer." } @@ -171,6 +171,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -185,7 +186,7 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } resource "stackit_server" "example" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" availability_zone = data.stackit_availability_zones.example.names[0] diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 20d7894c3..75d38bc3f 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -144,7 +144,7 @@ resource "stackit_loadbalancer" "example" { # Create a new security group to be assigned to the target server. resource "stackit_security_group" "target_sg" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "target-sg-for-lb-access" description = "Allows ingress traffic from the example load balancer." } @@ -152,6 +152,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" @@ -166,7 +167,7 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { } resource "stackit_server" "example" { - project_id = var.project_id + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" availability_zone = data.stackit_availability_zones.example.names[0] From 93d704e1a6b4fd35667675362f2b360e5ebfbf84 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 16:02:17 +0200 Subject: [PATCH 55/63] update docs and example --- docs/resources/loadbalancer.md | 16 +++++++++------- .../resources/stackit_loadbalancer/resource.tf | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 2ea0fa58c..a5b80ed98 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -117,15 +117,17 @@ resource "stackit_loadbalancer" "example" { # We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "lb-network-example" - ipv4_prefix = "192.168.1.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" + ipv4_prefix = "192.168.1.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_network" "target_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "target-network-example" - ipv4_prefix = "192.168.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" + ipv4_prefix = "192.168.2.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_public_ip" "example" { @@ -171,7 +173,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 75d38bc3f..7a276367e 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -98,15 +98,17 @@ resource "stackit_loadbalancer" "example" { # We create two separate networks: one for the load balancer and one for the target. resource "stackit_network" "lb_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "lb-network-example" - ipv4_prefix = "192.168.1.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "lb-network-example" + ipv4_prefix = "192.168.1.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_network" "target_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "target-network-example" - ipv4_prefix = "192.168.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "target-network-example" + ipv4_prefix = "192.168.2.0/24" + ipv4_nameservers = ["8.8.8.8"] } resource "stackit_public_ip" "example" { @@ -152,7 +154,7 @@ resource "stackit_security_group" "target_sg" { # Create a rule to allow traffic FROM the load balancer. # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" protocol = "tcp" From f1dc94e8d8f94645bbca3132957ac6381e79266d Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 8 Aug 2025 16:19:07 +0200 Subject: [PATCH 56/63] update docs and example --- .../internal/services/loadbalancer/loadbalancer/datasource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index e00613931..c72306656 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -346,7 +346,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["region"], }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Description: descriptions["security_group_id"], Computed: true, }, }, From e93b07d84d2731be0eaef68ab1adf99134f9df62 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:23:35 +0200 Subject: [PATCH 57/63] revert to simpler function to allow the LB to become active before proceeding, updated docs --- docs/resources/loadbalancer.md | 26 +++++----- .../stackit_loadbalancer/resource.tf | 26 +++++----- .../loadbalancer/loadbalancer/resource.go | 49 +++++-------------- 3 files changed, 42 insertions(+), 59 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index a5b80ed98..1712523de 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -143,7 +143,7 @@ resource "stackit_loadbalancer" "example" { disable_security_group_assignment = true networks = [{ - network_id = stackit_network.lb_network.id + network_id = stackit_network.lb_network.network_id role = "ROLE_LISTENERS_AND_TARGETS" }] @@ -176,7 +176,9 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" - protocol = "tcp" + protocol = { + name = "tcp" + } # This is the crucial link: it allows traffic from the LB's security group. remote_security_group_id = stackit_loadbalancer.example.security_group_id @@ -191,21 +193,23 @@ resource "stackit_server" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" - availability_zone = data.stackit_availability_zones.example.names[0] + availability_zone = "eu01-1" boot_volume = { source_type = "image" - source_id = data.stackit_image.example_ubuntu.id + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 10 } - network_interfaces = [{ - network_id = stackit_network.target_network.id - # Assign the manually configured security group to the server's NIC. - security_groups = [ - stackit_security_group.target_sg.id - ] - }] + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.target_network.network_id + security_group_ids = [stackit_security_group.target_sg.security_group_id] } # Only use the import statement, if you want to import an existing loadbalancer diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index 7a276367e..f8fd89523 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -124,7 +124,7 @@ resource "stackit_loadbalancer" "example" { disable_security_group_assignment = true networks = [{ - network_id = stackit_network.lb_network.id + network_id = stackit_network.lb_network.network_id role = "ROLE_LISTENERS_AND_TARGETS" }] @@ -157,7 +157,9 @@ resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" security_group_id = stackit_security_group.target_sg.id direction = "ingress" - protocol = "tcp" + protocol = { + name = "tcp" + } # This is the crucial link: it allows traffic from the LB's security group. remote_security_group_id = stackit_loadbalancer.example.security_group_id @@ -172,21 +174,23 @@ resource "stackit_server" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-remote-target" machine_type = "c1.1" - availability_zone = data.stackit_availability_zones.example.names[0] + availability_zone = "eu01-1" boot_volume = { source_type = "image" - source_id = data.stackit_image.example_ubuntu.id + source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 10 } - network_interfaces = [{ - network_id = stackit_network.target_network.id - # Assign the manually configured security group to the server's NIC. - security_groups = [ - stackit_security_group.target_sg.id - ] - }] + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.target_network.network_id + security_group_ids = [stackit_security_group.target_sg.security_group_id] } # Only use the import statement, if you want to import an existing loadbalancer diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 659c97ec5..b6a66fd9c 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -696,6 +696,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf // Create creates the resource and sets the initial Terraform state. func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -707,12 +708,14 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) + // Generate API request body from model payload, err := toCreatePayload(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err)) return } + // Create a new load balancer createResp, err := r.client.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) @@ -720,50 +723,22 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - var lb *loadbalancer.LoadBalancer - - if model.DisableSecurityGroupAssignment.ValueBool() { - // MANUAL MODE: Manually poll the resource until the security_group_id is available. - timeout := 5 * time.Minute - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-time.After(timeout): - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("timed out waiting for security_group_id to be populated for load balancer %q", loadBalancerName)) - return - case <-ticker.C: - getResp, err := r.client.GetLoadBalancer(ctx, projectId, region, loadBalancerName).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("polling for security_group_id: %v", err)) - return - } - - if getResp.TargetSecurityGroup != nil && getResp.TargetSecurityGroup.Id != nil && *getResp.TargetSecurityGroup.Id != "" { - lb = getResp - goto POLLING_DONE - } - // The ID is not ready yet, the loop will continue on the next tick. - } - } - POLLING_DONE: - } else { - // AUTOMATIC MODE: Wait for the load balancer to become active. - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) - return - } - lb = waitResp + // Wait for the load balancer to become active. + // This single handler is now used for both automatic and manual modes. + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) + return } - err = mapFields(ctx, lb, &model, region) + // Map response body to schema + err = mapFields(ctx, waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { From e7f8579d4a3869df4597d068ad8701e48e59c08a Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:28:00 +0200 Subject: [PATCH 58/63] update description --- docs/resources/loadbalancer.md | 2 +- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 1712523de..39658b0f8 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -242,7 +242,7 @@ import { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. +- `security_group_id` (String) ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index b6a66fd9c..2d30b00f7 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -684,7 +684,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf }, }, "security_group_id": schema.StringAttribute{ - Description: "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", + Description: descriptions["security_group_id"], Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), From e968326f335332cebdfb60211e7e1ffe89169f6b Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:31:59 +0200 Subject: [PATCH 59/63] update description --- docs/resources/loadbalancer.md | 2 +- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 39658b0f8..1712523de 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -242,7 +242,7 @@ import { - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`". - `private_address` (String) Transient private Load Balancer IP address. It can change any time. -- `security_group_id` (String) +- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`. ### Nested Schema for `listeners` diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 2d30b00f7..f471713fd 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -343,6 +343,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques "targets.display_name": "Target display name", "ip": "Target IP", "region": "The resource region. If not defined, the provider region is used.", + "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT NETWORK AREAS (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", } resp.Schema = schema.Schema{ From 88fa6d05819f8b40cc0003965b3a9e928ff834c0 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:33:38 +0200 Subject: [PATCH 60/63] update description --- stackit/internal/services/loadbalancer/loadbalancer/resource.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index f471713fd..ead3e73fd 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -724,8 +724,6 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } loadBalancerName := *createResp.Name - // Wait for the load balancer to become active. - // This single handler is now used for both automatic and manual modes. waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) From 860ae7b1ef6fbba3a4ae36c3aef5db0c60d9fb22 Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 12:36:29 +0200 Subject: [PATCH 61/63] small correction of unnecessary var --- .../internal/services/loadbalancer/loadbalancer/resource.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index ead3e73fd..0beda14f9 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,6 +3,7 @@ package loadbalancer import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -18,7 +19,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" @@ -722,9 +722,8 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) return } - loadBalancerName := *createResp.Name - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, loadBalancerName).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) return From 86da55e9037099cd2aeec753d5997426861f895f Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Fri, 15 Aug 2025 16:30:28 +0200 Subject: [PATCH 62/63] update example --- docs/resources/loadbalancer.md | 2 +- examples/resources/stackit_loadbalancer/resource.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 1712523de..752782155 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -174,7 +174,7 @@ resource "stackit_security_group" "target_sg" { # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - security_group_id = stackit_security_group.target_sg.id + security_group_id = stackit_security_group.target_sg.security_group_id direction = "ingress" protocol = { name = "tcp" diff --git a/examples/resources/stackit_loadbalancer/resource.tf b/examples/resources/stackit_loadbalancer/resource.tf index f8fd89523..9744bf5c8 100644 --- a/examples/resources/stackit_loadbalancer/resource.tf +++ b/examples/resources/stackit_loadbalancer/resource.tf @@ -155,7 +155,7 @@ resource "stackit_security_group" "target_sg" { # This rule uses the computed `security_group_id` of the load balancer. resource "stackit_security_group_rule" "allow_lb_ingress" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - security_group_id = stackit_security_group.target_sg.id + security_group_id = stackit_security_group.target_sg.security_group_id direction = "ingress" protocol = { name = "tcp" From 71392855cce917b0d75d3ec67d2561f0db4e454d Mon Sep 17 00:00:00 2001 From: schlotterbmx Date: Tue, 19 Aug 2025 15:09:14 +0200 Subject: [PATCH 63/63] update tests --- .../internal/services/loadbalancer/loadbalancer/resource.go | 2 +- .../internal/services/loadbalancer/testfiles/resource-min.tf | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 0beda14f9..61a2ad6c1 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -3,7 +3,6 @@ package loadbalancer import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "net/http" "strings" "time" @@ -19,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf index 2b6d65793..f2692c35d 100644 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf @@ -27,6 +27,11 @@ resource "stackit_network_interface" "network_interface" { project_id = stackit_network.network.project_id network_id = stackit_network.network.network_id name = "name" + lifecycle { + ignore_changes = [ + security_group_ids, + ] + } } resource "stackit_public_ip" "public_ip" {