Skip to content

Commit 7b892b9

Browse files
committed
feat: implemented alerting for expiring certificates for application gateway
1 parent 51ab3ef commit 7b892b9

4 files changed

Lines changed: 241 additions & 17 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
resource "azurerm_monitor_metric_alert" "this" {
2+
name = "${var.name}-availability-alert"
3+
resource_group_name = var.resource_group_name
4+
5+
scopes = [azurerm_application_insights_standard_web_test.this.id, var.application_insights_id]
6+
severity = 0
7+
8+
frequency = var.alert.frequency
9+
window_size = var.alert.window_size
10+
auto_mitigate = var.alert.auto_mitigate
11+
12+
application_insights_web_test_location_availability_criteria {
13+
web_test_id = azurerm_application_insights_standard_web_test.this.id
14+
component_id = var.application_insights_id
15+
failed_location_count = var.alert.failed_location_count
16+
}
17+
18+
description = var.alert_description
19+
20+
action {
21+
action_group_id = var.action_group_id
22+
}
23+
}

infrastructure/modules/application-insights-availability-test/main.tf

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,37 @@ resource "azurerm_application_insights_standard_web_test" "this" {
1010
enabled = true
1111

1212
request {
13-
url = var.target_url
14-
}
13+
url = var.target_url
14+
http_verb = var.http_verb
15+
body = var.request_body
1516

16-
geo_locations = var.geo_locations
17-
}
17+
dynamic "header" {
18+
for_each = var.headers
19+
content {
20+
name = header.key
21+
value = header.value
22+
}
23+
}
24+
}
1825

19-
resource "azurerm_monitor_metric_alert" "this" {
20-
name = "${var.name}-availability-alert"
21-
resource_group_name = var.resource_group_name
22-
scopes = [azurerm_application_insights_standard_web_test.this.id, var.application_insights_id]
23-
description = "availability test alert"
24-
severity = 0
26+
# Validation rules
27+
dynamic "validation_rules" {
28+
for_each = var.ssl_validation.enabled ? [1] : []
29+
content {
30+
expected_status_code = var.ssl_validation.expected_status_code
31+
ssl_check_enabled = var.ssl_validation.ssl_check_enabled
32+
ssl_cert_remaining_lifetime = var.ssl_validation.ssl_cert_remaining_lifetime
2533

26-
application_insights_web_test_location_availability_criteria {
27-
web_test_id = azurerm_application_insights_standard_web_test.this.id
28-
component_id = var.application_insights_id
29-
failed_location_count = 2
34+
dynamic "content" {
35+
for_each = var.ssl_validation.content == null ? [] : [var.ssl_validation.content]
36+
content {
37+
content_match = content.value.match
38+
ignore_case = try(content.value.ignore_case, true)
39+
pass_if_text_found = try(content.value.pass_if_text_found, true)
40+
}
41+
}
42+
}
3043
}
3144

32-
action {
33-
action_group_id = var.action_group_id
34-
}
45+
geo_locations = var.geo_locations
3546
}

infrastructure/modules/application-insights-availability-test/tfdocs.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,73 @@ Type: `number`
7878

7979
Default: `30`
8080

81+
### <a name="input_http_verb"></a> [http_verb](#input\_http\_verb)
82+
83+
Description: The HTTP verb used for the request.
84+
85+
Type: `string`
86+
87+
Allowed values: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
88+
89+
Default: GET
90+
91+
### <a name="input_headers"></a> [headers](#input\_headers)
92+
93+
Description: A map of HTTP request headers (name => value).
94+
95+
Type: `map(string)`
96+
97+
Default: {}
98+
99+
### <a name="input_request_body"></a> [request_body](#input\_request\_body)
100+
101+
Description: Request body to send with the HTTP call. Use jsonencode() for JSON payloads.
102+
103+
Type: `string`
104+
105+
Default: null
106+
107+
### <a name="input_alert_description"></a> [alert_description](#input\_alert\_description)
108+
109+
Description: The description applied to the alert rule.
110+
111+
Type: `string`
112+
113+
Default: "Availability test alert"
114+
115+
### <a name="input_ssl_validation"></a> [ssl_validation](#input\_ssl\_validation)
116+
117+
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.
118+
119+
Type:
120+
```hcl
121+
object({
122+
enabled = optional(bool, true)
123+
expected_status_code = optional(number, 200)
124+
ssl_check_enabled = optional(bool, true)
125+
ssl_cert_remaining_lifetime = optional(number, null)
126+
content = optional(object({
127+
match = string
128+
ignore_case = optional(bool, true)
129+
pass_if_text_found = optional(bool, true)
130+
}), null)
131+
})
132+
```
133+
134+
Default:
135+
```hcl
136+
{
137+
enabled = true
138+
expected_status_code = 200
139+
ssl_check_enabled = true
140+
ssl_cert_remaining_lifetime = null
141+
content = null
142+
}
143+
```
144+
145+
Validations:
146+
- expected_status_code must be 0 or a valid HTTP status code (100–599)
147+
- ssl_cert_remaining_lifetime must be null or between 1–365
81148

82149
## Resources
83150

infrastructure/modules/application-insights-availability-test/variables.tf

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,138 @@ variable "timeout" {
4242
type = number
4343
default = 30
4444
description = "Timeout in seconds, defaults to 30."
45+
validation {
46+
condition = var.timeout > 0
47+
error_message = "Timeout must be a positive number of seconds."
48+
}
4549
}
4650

4751
variable "geo_locations" {
4852
type = list(string)
4953
default = ["emea-ru-msa-edge", "emea-se-sto-edge", "emea-gb-db3-azr"]
5054
description = "List of Azure test locations (provider-specific location strings for UK and Ireland)"
55+
validation {
56+
condition = length(var.geo_locations) >= 1
57+
error_message = "At least one geo location must be provided."
58+
}
5159
}
5260

5361
variable "target_url" {
5462
type = string
5563
description = "The target URL for the restful endpoint to hit to validate the application is available"
64+
validation {
65+
condition = can(regex("^https?://", var.target_url))
66+
error_message = "The target URL must start with http:// or https://."
67+
}
68+
}
69+
70+
variable "http_verb" {
71+
description = "HTTP verb (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)"
72+
type = string
73+
default = "GET"
74+
validation {
75+
condition = contains(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], var.http_verb)
76+
error_message = "http_verb must be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS."
77+
}
78+
}
79+
80+
variable "headers" {
81+
description = "Map of request headers to send (name => value)"
82+
type = map(string)
83+
default = {}
84+
}
85+
86+
variable "request_body" {
87+
description = "The request body string; use jsonencode(...) for JSON"
88+
type = string
89+
default = null
90+
}
91+
92+
variable "alert_description" {
93+
description = "The description for alert"
94+
type = string
95+
default = "Availability test alert"
96+
}
97+
98+
variable "ssl_validation" {
99+
description = <<EOT
100+
The SSL validation settings for the standard web test.
101+
102+
Set `enabled = false` to omit the SSL validation block entirely.
103+
If you want to validate response body text, set content's match to a non-null string.
104+
EOT
105+
type = object({
106+
enabled = optional(bool, true)
107+
expected_status_code = optional(number, 200)
108+
ssl_check_enabled = optional(bool, true)
109+
ssl_cert_remaining_lifetime = optional(number, null)
110+
111+
content = optional(object({
112+
match = string
113+
ignore_case = optional(bool, true)
114+
pass_if_text_found = optional(bool, true)
115+
}), null)
116+
})
117+
118+
validation {
119+
condition = (
120+
var.ssl_validation.expected_status_code == 0 ||
121+
(var.ssl_validation.expected_status_code >= 100 &&
122+
var.ssl_validation.expected_status_code < 600)
123+
)
124+
error_message = "The expected status code must be 0 or a valid HTTP status code in [100, 599]."
125+
}
126+
127+
validation {
128+
condition = (
129+
var.ssl_validation.ssl_cert_remaining_lifetime == null ||
130+
(var.ssl_validation.ssl_cert_remaining_lifetime >= 1 &&
131+
var.ssl_validation.ssl_cert_remaining_lifetime <= 365)
132+
)
133+
error_message = "The SSL certificate remaining lifetime must be null or an integer between 1 and 365."
134+
}
135+
136+
default = {
137+
enabled = true
138+
expected_status_code = 200
139+
ssl_check_enabled = true
140+
ssl_cert_remaining_lifetime = null
141+
content = null
142+
}
143+
}
144+
145+
variable "alert" {
146+
type = object({
147+
frequency = optional(string, "PT1H")
148+
window_size = optional(string, "P1D")
149+
auto_mitigate = optional(bool, true)
150+
failed_location_count = optional(number, 2)
151+
})
152+
153+
validation {
154+
condition = contains(
155+
["PT1M", "PT5M", "PT15M", "PT30M", "PT1H"],
156+
var.alert.frequency
157+
)
158+
error_message = "Frequency must be one of: PT1M, PT5M, PT15M, PT30M, PT1H"
159+
}
160+
161+
validation {
162+
condition = contains(
163+
["PT1M", "PT5M", "PT15M", "PT30M", "PT1H", "PT6H", "PT12H", "P1D"],
164+
var.alert.window_size
165+
)
166+
error_message = "Window size must be one of: PT1M, PT5M, PT15M, PT30M, PT1H, PT6H, PT12H, P1D"
167+
}
168+
169+
validation {
170+
condition = var.alert.failed_location_count >= 1 && var.alert.failed_location_count <= length(var.geo_locations)
171+
error_message = "The failed location count must be >= 1 and cannot exceed the number of configured geo locations."
172+
}
173+
174+
default = {
175+
frequency = "PT1H" # every 24 hours
176+
window_size = "P1D" # last 24 hours
177+
auto_mitigate = true # automatically mitigate the alert when thie issue is resolved
178+
}
56179
}

0 commit comments

Comments
 (0)