diff --git a/iam.tf b/iam.tf index 27981fc5..738f009b 100644 --- a/iam.tf +++ b/iam.tf @@ -12,4 +12,5 @@ resource "azurerm_user_assigned_identity" "uai" { name = "${var.prefix}-aks-identity" resource_group_name = local.aks_rg.name location = var.location + tags = var.tags } diff --git a/locals.tf b/locals.tf index 19666a15..07a13e4e 100644 --- a/locals.tf +++ b/locals.tf @@ -16,7 +16,7 @@ locals { cluster_endpoint_public_access_cidrs = var.cluster_api_mode == "private" ? [] : (var.cluster_endpoint_public_access_cidrs == null ? local.default_public_access_cidrs : var.cluster_endpoint_public_access_cidrs) postgres_public_access_cidrs = var.postgres_public_access_cidrs == null ? local.default_public_access_cidrs : var.postgres_public_access_cidrs - subnets = { for k, v in var.subnets : k => v if !(k == "netapp" && var.storage_type == "standard") } + subnets = { for k, v in var.subnets : k => v if !(k == "netapp" && var.storage_type == "standard") && !(k == "gateway" && !var.create_app_gateway) } # Kubernetes kubeconfig_filename = "${var.prefix}-aks-kubeconfig.conf" @@ -39,6 +39,11 @@ locals { } } : {} + # App Gateway + app_gateway_config = merge(var.app_gateway_defaults, var.app_gateway_config) + waf_policy_config = var.waf_policy != null ? jsondecode(file(var.waf_policy)) : null + waf_policy_enabled = local.waf_policy_config != null ? length(local.waf_policy_config) != 0 ? true : false : false + # Container Registry container_registry_sku = title(var.container_registry_sku) diff --git a/main.tf b/main.tf index 0d716e77..2ee14dfc 100644 --- a/main.tf +++ b/main.tf @@ -266,6 +266,30 @@ module "message_broker" { tags = var.tags } +module "app_gateway" { + source = "./modules/azurerm_app_gateway" + count = var.create_app_gateway ? 1 : 0 + + prefix = var.prefix + resource_group_name = local.aks_rg.name + location = var.location + sku = local.app_gateway_config.sku + port = local.app_gateway_config.port + protocol = local.app_gateway_config.protocol + waf_policy_enabled = local.waf_policy_enabled + waf_policy_config = local.waf_policy_config + backend_host_name = local.app_gateway_config.backend_host_name + backend_trusted_root_certificate = local.app_gateway_config.backend_trusted_root_certificate + ssl_certificate = local.app_gateway_config.ssl_certificate + identity_ids = local.app_gateway_config.identity_ids + backend_address_pool_fqdn = local.app_gateway_config.backend_address_pool_fqdn + probe = local.app_gateway_config.probe + subnet_id = module.vnet.subnets["gateway"].id + tags = var.tags + + depends_on = [module.vnet] +} + data "external" "git_hash" { program = ["files/tools/iac_git_info.sh"] } diff --git a/modules/azure_aks/main.tf b/modules/azure_aks/main.tf index d1d0098c..85078514 100644 --- a/modules/azure_aks/main.tf +++ b/modules/azure_aks/main.tf @@ -3,22 +3,22 @@ # Reference: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster resource "azurerm_kubernetes_cluster" "aks" { - name = var.aks_cluster_name - location = var.aks_cluster_location - resource_group_name = var.aks_cluster_rg - dns_prefix = var.aks_private_cluster == false || var.aks_cluster_private_dns_zone_id == "" ? var.aks_cluster_dns_prefix : null - dns_prefix_private_cluster = var.aks_private_cluster && var.aks_cluster_private_dns_zone_id != "" ? var.aks_cluster_dns_prefix : null - - sku_tier = var.aks_cluster_sku_tier - role_based_access_control_enabled = true - http_application_routing_enabled = false - + name = var.aks_cluster_name + location = var.aks_cluster_location + resource_group_name = var.aks_cluster_rg + dns_prefix = var.aks_private_cluster == false || var.aks_cluster_private_dns_zone_id == "" ? var.aks_cluster_dns_prefix : null + dns_prefix_private_cluster = var.aks_private_cluster && var.aks_cluster_private_dns_zone_id != "" ? var.aks_cluster_dns_prefix : null + + sku_tier = var.aks_cluster_sku_tier + role_based_access_control_enabled = true + http_application_routing_enabled = false + # https://docs.microsoft.com/en-us/azure/aks/supported-kubernetes-versions # az aks get-versions --location eastus -o table - kubernetes_version = var.kubernetes_version - api_server_authorized_ip_ranges = var.aks_cluster_endpoint_public_access_cidrs - private_cluster_enabled = var.aks_private_cluster - private_dns_zone_id = var.aks_private_cluster && var.aks_cluster_private_dns_zone_id != "" ? var.aks_cluster_private_dns_zone_id : (var.aks_private_cluster ? "System" : null) + kubernetes_version = var.kubernetes_version + api_server_authorized_ip_ranges = var.aks_cluster_endpoint_public_access_cidrs + private_cluster_enabled = var.aks_private_cluster + private_dns_zone_id = var.aks_private_cluster && var.aks_cluster_private_dns_zone_id != "" ? var.aks_cluster_private_dns_zone_id : (var.aks_private_cluster ? "System" : null) network_profile { network_plugin = var.aks_network_plugin @@ -45,7 +45,7 @@ resource "azurerm_kubernetes_cluster" "aks" { content { admin_username = var.aks_cluster_node_admin ssh_key { - key_data = var.aks_cluster_ssh_public_key + key_data = var.aks_cluster_ssh_public_key } } } @@ -80,7 +80,7 @@ resource "azurerm_kubernetes_cluster" "aks" { dynamic "identity" { for_each = var.aks_uai_id == null ? [] : [1] content { - type = "UserAssigned" + type = "UserAssigned" identity_ids = [var.aks_uai_id] } } @@ -108,8 +108,8 @@ resource "azurerm_kubernetes_cluster" "aks" { } - data "azurerm_public_ip" "cluster_public_ip" { - count = var.cluster_egress_type == "loadBalancer" ? 1 : 0 +data "azurerm_public_ip" "cluster_public_ip" { + count = var.cluster_egress_type == "loadBalancer" ? 1 : 0 # effective_outbound_ips is a set of strings, that needs to be converted to a list type name = split("/", tolist(azurerm_kubernetes_cluster.aks.network_profile[0].load_balancer_profile[0].effective_outbound_ips)[0])[8] diff --git a/modules/azure_aks/variables.tf b/modules/azure_aks/variables.tf index 92795558..2fd8e465 100644 --- a/modules/azure_aks/variables.tf +++ b/modules/azure_aks/variables.tf @@ -146,7 +146,7 @@ variable "aks_dns_service_ip" { type = string default = "10.0.0.10" validation { - condition = var.aks_dns_service_ip != null ? can(regex("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",var.aks_dns_service_ip)) : false + condition = var.aks_dns_service_ip != null ? can(regex("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", var.aks_dns_service_ip)) : false error_message = "ERROR: aks_dns_service_ip - value must not be null and must be a valid IP address." } @@ -225,6 +225,6 @@ variable "cluster_egress_type" { } variable "aks_cluster_private_dns_zone_id" { - type = string + type = string default = "" } diff --git a/modules/azurerm_app_gateway/main.tf b/modules/azurerm_app_gateway/main.tf new file mode 100644 index 00000000..ee939e3d --- /dev/null +++ b/modules/azurerm_app_gateway/main.tf @@ -0,0 +1,216 @@ +# Copyright © 2020-2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +## https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/application_gateway +## https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/web_application_firewall_policy + +locals { + backend_address_pool_name = "${var.prefix}-backend-pool" + frontend_port_name = "${var.prefix}-frontend-port" + frontend_ip_configuration_name = "${var.prefix}-frontend-ip" + http_setting_name = "${var.prefix}-backend-setting" + listener_name = "${var.prefix}-listener" + request_routing_rule_name = "${var.prefix}-routing-rule" +} + +resource "azurerm_public_ip" "gateway_ip" { + name = "${var.prefix}-gateway-public_ip" + location = var.location + resource_group_name = var.resource_group_name + allocation_method = "Static" + sku = "Standard" + domain_name_label = var.backend_host_name == null ? "${var.prefix}-appgateway" : null + tags = var.tags +} + +resource "azurerm_web_application_firewall_policy" "waf_policy" { + count = var.waf_policy_enabled ? 1 : 0 + + name = "${var.prefix}-waf-policy" + resource_group_name = var.resource_group_name + location = var.location + tags = var.tags + + dynamic "custom_rules" { + for_each = var.waf_policy_config.custom_rules + content { + name = custom_rules.value.name + priority = custom_rules.value.priority + rule_type = custom_rules.value.rule_type + action = custom_rules.value.action + dynamic "match_conditions" { + for_each = custom_rules.value.match_conditions + content { + operator = match_conditions.value.operator + negation_condition = match_conditions.value.negation_condition + match_values = match_conditions.value.match_values + dynamic "match_variables" { + for_each = match_conditions.value.match_variables + content { + variable_name = match_variables.value.variable_name + } + } + } + } + } + } + + dynamic "policy_settings" { + for_each = var.waf_policy_config.policy_settings != null ? [var.waf_policy_config.policy_settings] : [] + content { + enabled = policy_settings.value.enabled + mode = policy_settings.value.mode + request_body_check = policy_settings.value.request_body_check + file_upload_limit_in_mb = policy_settings.value.file_upload_limit_in_mb + max_request_body_size_in_kb = policy_settings.value.max_request_body_size_in_kb + } + } + + managed_rules { + dynamic "exclusion" { + for_each = var.waf_policy_config.managed_rules.exclusion + content { + match_variable = exclusion.value.match_variable + selector = exclusion.value.selector + selector_match_operator = exclusion.value.selector_match_operator + dynamic "excluded_rule_set" { + for_each = exclusion.value.excluded_rule_set + content { + type = excluded_rule_set.value.type + version = excluded_rule_set.value.version + dynamic "rule_group" { + for_each = excluded_rule_set.value.rule_group + content { + rule_group_name = rule_group.value.rule_group_name + excluded_rules = rule_group.value.excluded_rules + } + } + } + } + } + } + + dynamic "managed_rule_set" { + for_each = var.waf_policy_config.managed_rules.managed_rule_set + content { + type = managed_rule_set.value.type + version = managed_rule_set.value.version + dynamic "rule_group_override" { + for_each = managed_rule_set.value.rule_group_override + content { + rule_group_name = rule_group_override.value.rule_group_name + dynamic "rule" { + for_each = rule_group_override.value.rule + content { + id = rule.value.id + enabled = rule.value.enabled + action = rule.value.action + } + } + } + } + } + } + } +} + + +resource "azurerm_application_gateway" "appgateway" { + name = "${var.prefix}-appgateway" + resource_group_name = var.resource_group_name + location = var.location + firewall_policy_id = var.waf_policy_enabled ? azurerm_web_application_firewall_policy.waf_policy[0].id : null + force_firewall_policy_association = var.waf_policy_enabled ? true : false + + sku { + name = var.waf_policy_enabled ? "WAF_v2" : var.sku + tier = var.waf_policy_enabled ? "WAF_v2" : "Standard_v2" + capacity = 2 + } + + gateway_ip_configuration { + name = "${var.prefix}-gateway-ip-configuration" + subnet_id = var.subnet_id + } + + frontend_port { + name = local.frontend_port_name + port = var.port + } + + frontend_ip_configuration { + name = local.frontend_ip_configuration_name + public_ip_address_id = azurerm_public_ip.gateway_ip.id + } + + backend_address_pool { + name = local.backend_address_pool_name + fqdns = var.backend_address_pool_fqdn != null ? length(var.backend_address_pool_fqdn) != 0 ? var.backend_address_pool_fqdn : null : null + } + + dynamic "trusted_root_certificate" { + for_each = var.backend_trusted_root_certificate == null ? [] : [1] + + content { + name = "root-cert" + data = filebase64(var.backend_trusted_root_certificate) + } + } + + dynamic "ssl_certificate" { + for_each = var.ssl_certificate == null ? [] : var.ssl_certificate + + content { + name = "ListenerCert" + data = ssl_certificate.value.data != null ? filebase64(ssl_certificate.value.data) : null + password = ssl_certificate.value.password + key_vault_secret_id = ssl_certificate.value.data != null ? null : ssl_certificate.value.key_vault_secret_id + } + } + + backend_http_settings { + name = local.http_setting_name + cookie_based_affinity = "Disabled" + port = var.port + protocol = var.protocol + request_timeout = 60 + probe_name = var.probe != null ? "default-probe" : null + host_name = var.backend_host_name == null ? azurerm_public_ip.gateway_ip.fqdn : var.backend_host_name + trusted_root_certificate_names = var.backend_trusted_root_certificate == null ? null : ["root-cert"] + } + + http_listener { + name = local.listener_name + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = local.frontend_port_name + protocol = var.protocol + ssl_certificate_name = var.ssl_certificate == null ? null : "ListenerCert" + } + + request_routing_rule { + name = local.request_routing_rule_name + rule_type = "Basic" + http_listener_name = local.listener_name + backend_address_pool_name = local.backend_address_pool_name + backend_http_settings_name = local.http_setting_name + priority = 1 + } + + dynamic "probe" { + for_each = var.probe != null ? var.probe : [] + + content { + name = probe.value.name + interval = 60 + protocol = var.protocol + path = probe.value.path + timeout = 30 + unhealthy_threshold = 3 + pick_host_name_from_backend_http_settings = true + } + } + + tags = var.tags + + depends_on = [azurerm_web_application_firewall_policy.waf_policy] +} diff --git a/modules/azurerm_app_gateway/outputs.tf b/modules/azurerm_app_gateway/outputs.tf new file mode 100644 index 00000000..bb5a6b44 --- /dev/null +++ b/modules/azurerm_app_gateway/outputs.tf @@ -0,0 +1,6 @@ +# Copyright © 2020-2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +output "gateway_frontend_ip" { + value = azurerm_public_ip.gateway_ip.ip_address +} diff --git a/modules/azurerm_app_gateway/variables.tf b/modules/azurerm_app_gateway/variables.tf new file mode 100644 index 00000000..0c03e2c9 --- /dev/null +++ b/modules/azurerm_app_gateway/variables.tf @@ -0,0 +1,86 @@ +# Copyright © 2020-2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +variable "prefix" { + description = "A prefix used in the name for all the Azure resources created by this script." + type = string +} + +variable "resource_group_name" { + description = "The name of the resource group in which to create the Azure Application Gateway. Changing this forces a new resource to be created." + type = string +} + +variable "location" { + description = "Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." + type = string +} + +variable "subnet_id" { + description = "The ID of the Subnet which the Application Gateway should be connected to." + type = string +} + +variable "tags" { + description = "Map of common tags to be placed on the Resources" + type = map(any) +} + +variable "sku" { + description = "The Name of the SKU to use for this Application Gateway." + type = string +} + +variable "port" { + description = "The port which should be used for this Application Gateway." + type = string +} + +variable "protocol" { + description = "The Protocol which should be used. Possible values are Http and Https." + type = string +} + +variable "backend_host_name" { + description = "Host header to be sent to the backend servers." + type = string + default = null +} + +variable "backend_trusted_root_certificate" { + description = "The Trusted Root Certificate to use." + type = string + default = null +} + +variable "ssl_certificate" { + description = "The associated SSL Certificate which should be used for this HTTP Listener." + type = any + default = null +} + +variable "backend_address_pool_fqdn" { + description = "A list of FQDN's which should be part of the Backend Address Pool." + type = list(any) +} + +variable "identity_ids" { + description = "Specifies a list of User Assigned Managed Identity IDs to be assigned to this Application Gateway." + type = list(any) + default = null +} + +variable "waf_policy_enabled" { + description = "Is the Web Application Firewall enabled?" + type = bool +} + +variable "waf_policy_config" { + description = "Azure Web Application Firewall Policy instance configuration." + type = any +} + +variable "probe" { + description = "Health probes to be created for the Application Gateway." + type = any +} diff --git a/outputs.tf b/outputs.tf index d8e52904..f63b6b3d 100644 --- a/outputs.tf +++ b/outputs.tf @@ -162,3 +162,12 @@ output "message_broker_primary_key" { output "message_broker_name" { value = var.create_azure_message_broker ? var.message_broker_name : null } + +## Application Gateway +output "app_gateway_enabled" { + value = var.create_app_gateway ? var.create_app_gateway : null +} + +output "app_gateway_frontend_ip" { + value = var.create_app_gateway ? module.app_gateway[0].gateway_frontend_ip : null +} diff --git a/variables.tf b/variables.tf index ee188417..6d622777 100644 --- a/variables.tf +++ b/variables.tf @@ -718,6 +718,13 @@ variable "subnets" { } } } + gateway = { + "prefixes" : ["192.168.4.0/24"], + "service_endpoints" : [], + "private_endpoint_network_policies_enabled" : true, + "private_link_service_network_policies_enabled" : false, + "service_delegations" : {} + } } } @@ -756,8 +763,8 @@ variable "aks_identity" { variable "aks_cluster_private_dns_zone_id" { description = "Specify private DNS zone resource ID for AKS private cluster to use." - type = string - default = "" + type = string + default = "" } ## Message Broker - Azure Service Bus - Experimental @@ -784,3 +791,46 @@ variable "message_broker_capacity" { type = number default = 1 } + +## Azure Application Gateway +variable "create_app_gateway" { + description = "Allows user to create Azure Application Gateway" + type = bool + default = false +} + +# Defaults +variable "app_gateway_defaults" { + description = "Default config for Azure Application Gateway" + type = any + default = { + sku = "Standard_v2" + port = "443" + protocol = "Https" + backend_host_name = null + backend_trusted_root_certificate = null + ssl_certificate = [{ + data = null + password = null + key_vault_secret_id = null + }] + identity_ids = [] + backend_address_pool_fqdn = [] + probe = [{ + name = "default-probe" + path = "/SASLogon/apiMeta" + }] + } +} + +variable "app_gateway_config" { + description = "Map of Application Gateway configuration objects" + type = any + default = {} +} + +variable "waf_policy" { + description = "A JSON file with all the WAF_Policy rules" + type = string + default = null +}