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