diff --git a/infrastructure/modules/application-insights-availability-test/alerts.tf b/infrastructure/modules/application-insights-availability-test/alerts.tf new file mode 100644 index 00000000..17678c80 --- /dev/null +++ b/infrastructure/modules/application-insights-availability-test/alerts.tf @@ -0,0 +1,23 @@ +resource "azurerm_monitor_metric_alert" "this" { + name = "${var.name}-availability-alert" + resource_group_name = var.resource_group_name + + scopes = [azurerm_application_insights_standard_web_test.this.id, var.application_insights_id] + severity = 0 + + frequency = var.alert.frequency + window_size = var.alert.window_size + auto_mitigate = var.alert.auto_mitigate + + application_insights_web_test_location_availability_criteria { + web_test_id = azurerm_application_insights_standard_web_test.this.id + component_id = var.application_insights_id + failed_location_count = var.alert.failed_location_count + } + + description = var.alert_description + + action { + action_group_id = var.action_group_id + } +} diff --git a/infrastructure/modules/application-insights-availability-test/main.tf b/infrastructure/modules/application-insights-availability-test/main.tf index 71742f29..354b990d 100644 --- a/infrastructure/modules/application-insights-availability-test/main.tf +++ b/infrastructure/modules/application-insights-availability-test/main.tf @@ -10,26 +10,37 @@ resource "azurerm_application_insights_standard_web_test" "this" { enabled = true request { - url = var.target_url - } + url = var.target_url + http_verb = var.http_verb + body = var.request_body - geo_locations = var.geo_locations -} + dynamic "header" { + for_each = var.headers + content { + name = header.key + value = header.value + } + } + } -resource "azurerm_monitor_metric_alert" "this" { - name = "${var.name}-availability-alert" - resource_group_name = var.resource_group_name - scopes = [azurerm_application_insights_standard_web_test.this.id, var.application_insights_id] - description = "availability test alert" - severity = 0 + # Validation rules + dynamic "validation_rules" { + for_each = var.ssl_validation.enabled ? [1] : [] + content { + expected_status_code = var.ssl_validation.expected_status_code + ssl_check_enabled = var.ssl_validation.ssl_check_enabled + ssl_cert_remaining_lifetime = var.ssl_validation.ssl_cert_remaining_lifetime - application_insights_web_test_location_availability_criteria { - web_test_id = azurerm_application_insights_standard_web_test.this.id - component_id = var.application_insights_id - failed_location_count = 2 + dynamic "content" { + for_each = var.ssl_validation.content == null ? [] : [var.ssl_validation.content] + content { + content_match = content.value.match + ignore_case = try(content.value.ignore_case, true) + pass_if_text_found = try(content.value.pass_if_text_found, true) + } + } + } } - action { - action_group_id = var.action_group_id - } + geo_locations = var.geo_locations } diff --git a/infrastructure/modules/application-insights-availability-test/tfdocs.md b/infrastructure/modules/application-insights-availability-test/tfdocs.md index d1e33f84..3da64023 100644 --- a/infrastructure/modules/application-insights-availability-test/tfdocs.md +++ b/infrastructure/modules/application-insights-availability-test/tfdocs.md @@ -53,7 +53,6 @@ Description: List of Azure test locations (provider-specific location strings fo Type: `list(string)` Default: - ```json [ "emea-ru-msa-edge", @@ -62,6 +61,9 @@ Default: ] ``` +Validations: +- At least one geo location must be provided. + ### [location](#input\_location) Description: The location/region where the availability test is deployed (must match App Insights location) @@ -78,6 +80,103 @@ Type: `number` Default: `30` +### [http_verb](#input\_http\_verb) + +Description: The HTTP verb used for the request. + +Type: `string` + +Allowed values: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS + +Default: GET + +### [headers](#input\_headers) + +Description: A map of HTTP request headers (name => value). + +Type: `map(string)` + +Default: {} + +### [request_body](#input\_request\_body) + +Description: Request body to send with the HTTP call. Use jsonencode() for JSON payloads. + +Type: `string` + +Default: null + +### [alert_description](#input\_alert\_description) + +Description: The description applied to the alert rule. + +Type: `string` + +Default: "Availability test alert" + +### [ssl_validation](#input\_ssl\_validation) + +Description: SSL validation configuration for the availability test. Set `enabled = false` to omit SSL validation completely. To validate response body content, set content.match to a non-null string. + +Type: +```hcl +object({ + enabled = optional(bool, true) + expected_status_code = optional(number, 200) + ssl_check_enabled = optional(bool, true) + ssl_cert_remaining_lifetime = optional(number, null) + content = optional(object({ + match = string + ignore_case = optional(bool, true) + pass_if_text_found = optional(bool, true) + }), null) +}) +``` + +Default: +```hcl +{ + enabled = true + expected_status_code = 200 + ssl_check_enabled = true + ssl_cert_remaining_lifetime = null + content = null +} +``` + +Validations: +- expected_status_code must be 0 or a valid HTTP status code (100–599) +- ssl_cert_remaining_lifetime must be null or between 1–365 + +### [alert](#input\_alert) + +Description: Configuration for the availability alert rule. + +Type: +```hcl +object({ + frequency = optional(string, "PT1H") + window_size = optional(string, "P1D") + auto_mitigate = optional(bool, true) + failed_location_count = optional(number, 2) +}) +``` + +Defaults: +```hcl +{ + frequency = "PT1H" + window_size = "P1D" + auto_mitigate = true +} +``` + +Validations: +- frequency must be one of: PT1M, PT5M, PT15M, PT30M, PT1H +- window_size must be one of: PT1M, PT5M, PT15M, PT30M, PT1H, PT6H, PT12H, P1D +- failed_location_count must be: + >= 1, and + <= the number of configured geo_locations ## Resources diff --git a/infrastructure/modules/application-insights-availability-test/variables.tf b/infrastructure/modules/application-insights-availability-test/variables.tf index 09775b91..26314ccd 100644 --- a/infrastructure/modules/application-insights-availability-test/variables.tf +++ b/infrastructure/modules/application-insights-availability-test/variables.tf @@ -42,15 +42,138 @@ variable "timeout" { type = number default = 30 description = "Timeout in seconds, defaults to 30." + validation { + condition = var.timeout > 0 + error_message = "Timeout must be a positive number of seconds." + } } variable "geo_locations" { type = list(string) default = ["emea-ru-msa-edge", "emea-se-sto-edge", "emea-gb-db3-azr"] description = "List of Azure test locations (provider-specific location strings for UK and Ireland)" + validation { + condition = length(var.geo_locations) >= 1 + error_message = "At least one geo location must be provided." + } } variable "target_url" { type = string description = "The target URL for the restful endpoint to hit to validate the application is available" + validation { + condition = can(regex("^https?://", var.target_url)) + error_message = "The target URL must start with http:// or https://." + } +} + +variable "http_verb" { + description = "HTTP verb (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)" + type = string + default = "GET" + validation { + condition = contains(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], var.http_verb) + error_message = "http_verb must be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS." + } +} + +variable "headers" { + description = "Map of request headers to send (name => value)" + type = map(string) + default = {} +} + +variable "request_body" { + description = "The request body string; use jsonencode(...) for JSON" + type = string + default = null +} + +variable "alert_description" { + description = "The description for alert" + type = string + default = "Availability test alert" +} + +variable "ssl_validation" { + description = <= 100 && + var.ssl_validation.expected_status_code < 600) + ) + error_message = "The expected status code must be 0 or a valid HTTP status code in [100, 599]." + } + + validation { + condition = ( + var.ssl_validation.ssl_cert_remaining_lifetime == null || + (var.ssl_validation.ssl_cert_remaining_lifetime >= 1 && + var.ssl_validation.ssl_cert_remaining_lifetime <= 365) + ) + error_message = "The SSL certificate remaining lifetime must be null or an integer between 1 and 365." + } + + default = { + enabled = true + expected_status_code = 200 + ssl_check_enabled = true + ssl_cert_remaining_lifetime = null + content = null + } +} + +variable "alert" { + type = object({ + frequency = optional(string, "PT1H") + window_size = optional(string, "P1D") + auto_mitigate = optional(bool, true) + failed_location_count = optional(number, 2) + }) + + validation { + condition = contains( + ["PT1M", "PT5M", "PT15M", "PT30M", "PT1H"], + var.alert.frequency + ) + error_message = "Frequency must be one of: PT1M, PT5M, PT15M, PT30M, PT1H" + } + + validation { + condition = contains( + ["PT1M", "PT5M", "PT15M", "PT30M", "PT1H", "PT6H", "PT12H", "P1D"], + var.alert.window_size + ) + error_message = "Window size must be one of: PT1M, PT5M, PT15M, PT30M, PT1H, PT6H, PT12H, P1D" + } + + validation { + condition = var.alert.failed_location_count >= 1 && var.alert.failed_location_count <= length(var.geo_locations) + error_message = "The failed location count must be >= 1 and cannot exceed the number of configured geo locations." + } + + default = { + frequency = "PT1H" # every 24 hours + window_size = "P1D" # last 24 hours + auto_mitigate = true # automatically mitigate the alert when thie issue is resolved + } }