diff --git a/terraform/cluster/azure-aks/main.tf b/terraform/cluster/azure-aks/main.tf
index f388c36c4..4177ad9b4 100644
--- a/terraform/cluster/azure-aks/main.tf
+++ b/terraform/cluster/azure-aks/main.tf
@@ -266,6 +266,7 @@ resource "azurerm_kubernetes_cluster" "main" {
vnet_subnet_id = coalesce(var.vnet_subnet_id, try(data.azurerm_subnet.private[0].id, null))
orchestrator_version = var.kubernetes_version
only_critical_addons_enabled = var.default_node_pool.only_critical_addons_enabled
+ zones = var.default_node_pool.availability_zones
# checkov:skip=CKV_AZURE_226: we are using the managed disk type to reduce costs
os_disk_type = var.default_node_pool.os_disk_type
@@ -297,10 +298,13 @@ resource "azurerm_kubernetes_cluster" "main" {
workload_identity_enabled = var.workload_identity_enabled
network_profile {
- network_plugin = "azure"
- network_policy = "cilium"
- service_cidr = var.service_cidr
- dns_service_ip = var.dns_service_ip
+ network_plugin = "azure"
+ network_plugin_mode = "overlay"
+ network_policy = "cilium"
+ network_data_plane = "cilium"
+ outbound_type = var.outbound_type
+ service_cidr = var.service_cidr
+ dns_service_ip = var.dns_service_ip
}
# Use system-assigned managed identity (Microsoft default and best practice)
@@ -324,6 +328,7 @@ resource "azurerm_kubernetes_cluster_node_pool" "autoscaled" {
auto_scaling_enabled = true
min_count = var.autoscaled_node_pool.min_count
max_count = var.autoscaled_node_pool.max_count
+ zones = var.autoscaled_node_pool.availability_zones
vnet_subnet_id = coalesce(
var.vnet_subnet_id,
try(data.azurerm_subnet.private[length(local.private_subnets) - 1].id, null)
diff --git a/terraform/cluster/azure-aks/test.tftest.hcl b/terraform/cluster/azure-aks/test.tftest.hcl
index 3fa52e911..a89e75121 100644
--- a/terraform/cluster/azure-aks/test.tftest.hcl
+++ b/terraform/cluster/azure-aks/test.tftest.hcl
@@ -29,6 +29,7 @@ mock_provider "azurerm" {
}
}
+
# Verifies that the module creates an AKS cluster with minimal configuration,
# ensuring that all default values are correctly applied and only required variables are set.
run "minimal_configuration" {
@@ -125,6 +126,21 @@ run "minimal_configuration" {
error_message = "Workload Identity should be enabled by default"
}
+ assert {
+ condition = azurerm_kubernetes_cluster.main.network_profile[0].outbound_type == "userAssignedNATGateway"
+ error_message = "Default outbound type should be 'userAssignedNATGateway'"
+ }
+
+ assert {
+ condition = azurerm_kubernetes_cluster.main.network_profile[0].network_plugin_mode == "overlay"
+ error_message = "Network plugin mode should be 'overlay'"
+ }
+
+ assert {
+ condition = azurerm_kubernetes_cluster.main.network_profile[0].network_data_plane == "cilium"
+ error_message = "Network data plane should be 'cilium'"
+ }
+
assert {
condition = contains(azurerm_role_definition.aks_kubelet_vmss_disk_manager.permissions[0].actions, "Microsoft.Compute/snapshots/read")
error_message = "Snapshot permissions should be included when enable_volume_snapshots is true (default)"
@@ -159,6 +175,7 @@ run "full_configuration" {
max_count = 3
node_count = 1
only_critical_addons_enabled = false
+ availability_zones = ["1", "2", "3"]
}
autoscaled_node_pool = {
enabled = true
@@ -170,11 +187,13 @@ run "full_configuration" {
host_encryption_enabled = true
min_count = 1
max_count = 3
+ availability_zones = ["1", "2"]
}
role_based_access_control_enabled = true
private_cluster_enabled = false
azure_policy_enabled = true
local_account_disabled = false
+ outbound_type = "loadBalancer"
authorized_ip_ranges = ["10.0.0.0/8"]
admin_object_ids = ["55555555-5555-5555-5555-555555555555"]
enable_volume_snapshots = true
@@ -252,7 +271,7 @@ run "full_configuration" {
assert {
condition = azurerm_kubernetes_cluster.main.local_account_disabled == false
- error_message = "Local accounts should be enabled"
+ error_message = "Local accounts should be disabled when explicitly set to false"
}
assert {
@@ -260,6 +279,31 @@ run "full_configuration" {
error_message = "Cluster should use system-assigned identity"
}
+ assert {
+ condition = length(azurerm_kubernetes_cluster.main.default_node_pool[0].zones) == 3 && contains(azurerm_kubernetes_cluster.main.default_node_pool[0].zones, "1") && contains(azurerm_kubernetes_cluster.main.default_node_pool[0].zones, "2") && contains(azurerm_kubernetes_cluster.main.default_node_pool[0].zones, "3")
+ error_message = "Default node pool zones should match input value"
+ }
+
+ assert {
+ condition = length(azurerm_kubernetes_cluster_node_pool.autoscaled[0].zones) == 2 && contains(azurerm_kubernetes_cluster_node_pool.autoscaled[0].zones, "1") && contains(azurerm_kubernetes_cluster_node_pool.autoscaled[0].zones, "2")
+ error_message = "Autoscaled node pool zones should match input value"
+ }
+
+ assert {
+ condition = azurerm_kubernetes_cluster.main.network_profile[0].outbound_type == "loadBalancer"
+ error_message = "Outbound type should match input value"
+ }
+
+ assert {
+ condition = azurerm_kubernetes_cluster.main.network_profile[0].network_plugin_mode == "overlay"
+ error_message = "Network plugin mode should be 'overlay'"
+ }
+
+ assert {
+ condition = azurerm_kubernetes_cluster.main.network_profile[0].network_data_plane == "cilium"
+ error_message = "Network data plane should be 'cilium'"
+ }
+
assert {
condition = azurerm_kubernetes_cluster.main.oidc_issuer_enabled == true
error_message = "OIDC issuer should be enabled"
@@ -457,10 +501,12 @@ run "multiple_invalid_inputs" {
command = plan
expect_failures = [
var.kubernetes_version,
+ var.outbound_type,
]
variables {
context_id = "test"
kubernetes_version = "v1.32"
+ outbound_type = "invalid"
}
}
diff --git a/terraform/cluster/azure-aks/variables.tf b/terraform/cluster/azure-aks/variables.tf
index 3c8d35ebe..b7fc75acc 100644
--- a/terraform/cluster/azure-aks/variables.tf
+++ b/terraform/cluster/azure-aks/variables.tf
@@ -79,6 +79,7 @@ variable "default_node_pool" {
max_count = number
node_count = number
only_critical_addons_enabled = bool
+ availability_zones = optional(list(string))
})
default = {
name = "system"
@@ -105,6 +106,7 @@ variable "autoscaled_node_pool" {
host_encryption_enabled = bool
min_count = number
max_count = number
+ availability_zones = optional(list(string))
})
default = {
enabled = true
@@ -247,6 +249,16 @@ variable "endpoint_private_access" {
default = false
}
+variable "outbound_type" {
+ description = "The outbound (egress) routing method which should be used for this Kubernetes Cluster."
+ type = string
+ default = "userAssignedNATGateway"
+ validation {
+ condition = contains(["loadBalancer", "userDefinedRouting", "managedNATGateway", "userAssignedNATGateway"], var.outbound_type)
+ error_message = "The outbound_type must be one of: loadBalancer, userDefinedRouting, managedNATGateway, userAssignedNATGateway."
+ }
+}
+
variable "enable_volume_snapshots" {
description = "Enable volume snapshot permissions for the kubelet identity. Set to false to use minimal permissions if volume snapshots are not needed."
type = bool
diff --git a/terraform/network/aws-vpc/.terraform.lock.hcl b/terraform/network/aws-vpc/.terraform.lock.hcl
index aa48e2247..5c0fff4e8 100644
--- a/terraform/network/aws-vpc/.terraform.lock.hcl
+++ b/terraform/network/aws-vpc/.terraform.lock.hcl
@@ -13,6 +13,7 @@ provider "registry.terraform.io/hashicorp/aws" {
"h1:PQ3jzG6VNrfS35adtrBeLnVTnJef3f2t5SUV7XNikgo=",
"h1:QAcpv9yoqEtVaBVyQ3hHsTc558AchV5/8lfAGoqmUkA=",
"h1:QJEljz77aB459tng0v+5xIdV6mkmCM4RZO6ztk3pOEA=",
+ "h1:QJr1C4scuvEslohwPKrBPEjhQRyMT26HzhxZlLjl3cw=",
"h1:RB3r7K1PgJ6S3J0l4u5/nB7G/inM2goPWY+QHesxuGo=",
"h1:UT4pxGbPuANnxyCeDn5/Ybr476EkyxcsYsU+q0iYt/Q=",
"h1:Uv/PPkYgnjKcsesOVWiTHaY/1R5/OgH1UYnXvtu0K58=",
diff --git a/terraform/network/aws-vpc/README.md b/terraform/network/aws-vpc/README.md
index 09a54fdaf..6a0f40bce 100644
--- a/terraform/network/aws-vpc/README.md
+++ b/terraform/network/aws-vpc/README.md
@@ -4,14 +4,14 @@
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | >=1.8 |
-| [aws](#requirement\_aws) | 6.18.0 |
+| [aws](#requirement\_aws) | 6.25.0 |
| [random](#requirement\_random) | 3.7.2 |
## Providers
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | 6.18.0 |
+| [aws](#provider\_aws) | 6.25.0 |
| [null](#provider\_null) | 3.2.4 |
| [random](#provider\_random) | 3.7.2 |
@@ -23,32 +23,32 @@ No modules.
| Name | Type |
|------|------|
-| [aws_cloudwatch_log_group.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/cloudwatch_log_group) | resource |
-| [aws_default_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/default_security_group) | resource |
-| [aws_eip.nat](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/eip) | resource |
-| [aws_flow_log.main](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/flow_log) | resource |
-| [aws_iam_role.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/iam_role) | resource |
-| [aws_iam_role_policy.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/iam_role_policy) | resource |
-| [aws_internet_gateway.main](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/internet_gateway) | resource |
-| [aws_kms_alias.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/kms_alias) | resource |
-| [aws_kms_key.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/kms_key) | resource |
-| [aws_nat_gateway.main](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/nat_gateway) | resource |
-| [aws_route53_zone.main](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/route53_zone) | resource |
-| [aws_route_table.isolated](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/route_table) | resource |
-| [aws_route_table.private](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/route_table) | resource |
-| [aws_route_table.public](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/route_table) | resource |
-| [aws_route_table_association.isolated](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/route_table_association) | resource |
-| [aws_route_table_association.private](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/route_table_association) | resource |
-| [aws_route_table_association.public](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/route_table_association) | resource |
-| [aws_subnet.isolated](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/subnet) | resource |
-| [aws_subnet.private](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/subnet) | resource |
-| [aws_subnet.public](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/subnet) | resource |
-| [aws_vpc.main](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/resources/vpc) | resource |
+| [aws_cloudwatch_log_group.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/cloudwatch_log_group) | resource |
+| [aws_default_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/default_security_group) | resource |
+| [aws_eip.nat](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/eip) | resource |
+| [aws_flow_log.main](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/flow_log) | resource |
+| [aws_iam_role.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/iam_role_policy) | resource |
+| [aws_internet_gateway.main](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/internet_gateway) | resource |
+| [aws_kms_alias.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/kms_alias) | resource |
+| [aws_kms_key.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/kms_key) | resource |
+| [aws_nat_gateway.main](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/nat_gateway) | resource |
+| [aws_route53_zone.main](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/route53_zone) | resource |
+| [aws_route_table.isolated](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/route_table) | resource |
+| [aws_route_table.private](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/route_table) | resource |
+| [aws_route_table.public](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/route_table) | resource |
+| [aws_route_table_association.isolated](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/route_table_association) | resource |
+| [aws_route_table_association.private](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/route_table_association) | resource |
+| [aws_route_table_association.public](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/route_table_association) | resource |
+| [aws_subnet.isolated](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/subnet) | resource |
+| [aws_subnet.private](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/subnet) | resource |
+| [aws_subnet.public](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/subnet) | resource |
+| [aws_vpc.main](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/resources/vpc) | resource |
| [null_resource.delete_vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
| [random_string.log_group_suffix](https://registry.terraform.io/providers/hashicorp/random/3.7.2/docs/resources/string) | resource |
-| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/data-sources/availability_zones) | data source |
-| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/data-sources/caller_identity) | data source |
-| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/6.18.0/docs/data-sources/region) | data source |
+| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/data-sources/availability_zones) | data source |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/data-sources/caller_identity) | data source |
+| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/6.25.0/docs/data-sources/region) | data source |
## Inputs
diff --git a/terraform/network/azure-vnet/README.md b/terraform/network/azure-vnet/README.md
index 49910754b..0be772f1d 100644
--- a/terraform/network/azure-vnet/README.md
+++ b/terraform/network/azure-vnet/README.md
@@ -4,13 +4,13 @@
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | >=1.8 |
-| [azurerm](#requirement\_azurerm) | ~> 4.50.0 |
+| [azurerm](#requirement\_azurerm) | ~> 4.55.0 |
## Providers
| Name | Version |
|------|---------|
-| [azurerm](#provider\_azurerm) | 4.50.0 |
+| [azurerm](#provider\_azurerm) | 4.55.0 |
## Modules
@@ -24,10 +24,12 @@ No modules.
| [azurerm_nat_gateway_public_ip_association.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/nat_gateway_public_ip_association) | resource |
| [azurerm_public_ip.nat](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip) | resource |
| [azurerm_resource_group.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource |
+| [azurerm_route_table.private](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/route_table) | resource |
| [azurerm_subnet.isolated](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource |
| [azurerm_subnet.private](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource |
| [azurerm_subnet.public](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource |
| [azurerm_subnet_nat_gateway_association.private](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_nat_gateway_association) | resource |
+| [azurerm_subnet_route_table_association.private](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_route_table_association) | resource |
| [azurerm_virtual_network.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network) | resource |
## Inputs
diff --git a/terraform/network/azure-vnet/main.tf b/terraform/network/azure-vnet/main.tf
index 8fee9e21a..664c7236a 100644
--- a/terraform/network/azure-vnet/main.tf
+++ b/terraform/network/azure-vnet/main.tf
@@ -125,7 +125,22 @@ resource "azurerm_nat_gateway_public_ip_association" "main" {
public_ip_address_id = azurerm_public_ip.nat[count.index].id
}
-# Associate NAT Gateway with private subnet
+resource "azurerm_route_table" "private" {
+ count = var.vnet_zones
+ name = "${var.name}-private-${count.index + 1}-${var.context_id}"
+ location = azurerm_resource_group.main.location
+ resource_group_name = azurerm_resource_group.main.name
+ tags = merge({
+ Name = "${var.name}-private-${count.index + 1}-${var.context_id}"
+ }, local.tags)
+}
+
+resource "azurerm_subnet_route_table_association" "private" {
+ count = var.vnet_zones
+ subnet_id = azurerm_subnet.private[count.index].id
+ route_table_id = azurerm_route_table.private[count.index].id
+}
+
resource "azurerm_subnet_nat_gateway_association" "private" {
count = var.enable_nat_gateway ? var.vnet_zones : 0
subnet_id = azurerm_subnet.private[count.index].id
diff --git a/terraform/network/azure-vnet/test.tftest.hcl b/terraform/network/azure-vnet/test.tftest.hcl
index 9963e5898..75c5e12ed 100644
--- a/terraform/network/azure-vnet/test.tftest.hcl
+++ b/terraform/network/azure-vnet/test.tftest.hcl
@@ -44,6 +44,21 @@ run "minimal_configuration" {
condition = length(azurerm_nat_gateway.main) == 1
error_message = "One NAT Gateway should be created by default"
}
+
+ assert {
+ condition = length(azurerm_route_table.private) == 1
+ error_message = "One route table should be created for private subnets by default"
+ }
+
+ assert {
+ condition = length(azurerm_subnet_route_table_association.private) == 1
+ error_message = "One route table association should be created for private subnets by default"
+ }
+
+ assert {
+ condition = azurerm_route_table.private[0].name == "windsor-vnet-private-1-test"
+ error_message = "Route table name should follow naming convention"
+ }
}
# Tests a full configuration with all optional variables explicitly set.
@@ -100,6 +115,26 @@ run "full_configuration" {
condition = length(azurerm_nat_gateway.main) == 2
error_message = "Two NAT Gateways should be created"
}
+
+ assert {
+ condition = length(azurerm_route_table.private) == 2
+ error_message = "Two route tables should be created for private subnets"
+ }
+
+ assert {
+ condition = length(azurerm_subnet_route_table_association.private) == 2
+ error_message = "Two route table associations should be created for private subnets"
+ }
+
+ assert {
+ condition = azurerm_route_table.private[0].name == "custom-private-1-test"
+ error_message = "First route table name should follow naming convention"
+ }
+
+ assert {
+ condition = azurerm_route_table.private[1].name == "custom-private-2-test"
+ error_message = "Second route table name should follow naming convention"
+ }
}
# Tests NAT Gateway configuration
@@ -191,6 +226,31 @@ run "automatic_subnet_creation" {
condition = azurerm_subnet.public[2].address_prefixes[0] == "10.0.53.0/24"
error_message = "Third public subnet should be 10.0.53.0/24"
}
+
+ assert {
+ condition = length(azurerm_route_table.private) == 3
+ error_message = "Three route tables should be created for private subnets when vnet_zones is 3"
+ }
+
+ assert {
+ condition = length(azurerm_subnet_route_table_association.private) == 3
+ error_message = "Three route table associations should be created for private subnets when vnet_zones is 3"
+ }
+
+ assert {
+ condition = azurerm_route_table.private[0].name == "test-network-private-1-test"
+ error_message = "First route table name should follow naming convention"
+ }
+
+ assert {
+ condition = azurerm_route_table.private[1].name == "test-network-private-2-test"
+ error_message = "Second route table name should follow naming convention"
+ }
+
+ assert {
+ condition = azurerm_route_table.private[2].name == "test-network-private-3-test"
+ error_message = "Third route table name should follow naming convention"
+ }
}
# Tests validation rules for required variables