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
+ }
}