Skip to content

Commit 9dfe816

Browse files
DTOSS-12524: Add logic-app-slack-alert module for Azure Monitor Slack notifications (#286)
1 parent 0abead1 commit 9dfe816

6 files changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# logic-app-slack-alert
2+
3+
Deploy an [Azure Logic App](https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-overview) that receives [Azure Monitor alert notifications](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups) via HTTP and forwards them to a Slack channel as formatted messages.
4+
5+
## Terraform documentation
6+
7+
For the list of inputs, outputs, resources... check the [terraform module documentation](tfdocs.md).
8+
9+
## Prerequisites
10+
11+
- A Slack incoming webhook URL. Create one via **Slack App > Incoming Webhooks** and store it in Azure Key Vault. Pass the resolved secret value as `slack_webhook_url`.
12+
- An [Azure Monitor action group](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups) to register the Logic App trigger URL with as a webhook receiver. Use the `trigger_callback_url` output for this.
13+
14+
## Usage
15+
16+
```hcl
17+
module "logic_app_slack_alert" {
18+
source = "../../../dtos-devops-templates/infrastructure/modules/logic-app-slack-alert"
19+
20+
name = "logic-slack-alert-myapp-dev-uks"
21+
resource_group_name = azurerm_resource_group.main.name
22+
location = "uksouth"
23+
slack_webhook_url = data.azurerm_key_vault_secret.slack_webhook.value
24+
}
25+
26+
resource "azurerm_monitor_action_group" "slack" {
27+
name = "ag-slack-myapp-dev-uks"
28+
resource_group_name = azurerm_resource_group.main.name
29+
short_name = "slack"
30+
31+
webhook_receiver {
32+
name = "logic-app-slack"
33+
service_uri = module.logic_app_slack_alert.trigger_callback_url
34+
use_common_alert_schema = true
35+
}
36+
}
37+
```
38+
39+
Then attach the action group to any Azure Monitor alert rule:
40+
41+
```hcl
42+
resource "azurerm_monitor_metric_alert" "example" {
43+
# ...
44+
action {
45+
action_group_id = azurerm_monitor_action_group.slack.id
46+
}
47+
}
48+
```
49+
50+
## Slack message format
51+
52+
Each alert posts a Block Kit message to the configured channel containing:
53+
54+
| Field | Source |
55+
|-------|--------|
56+
| Header | Alert rule name, prefixed with 🚨 (fired) or ✅ (resolved) |
57+
| Severity | `Sev0``Sev4` from the alert essentials |
58+
| Status | `Fired` or `Resolved` |
59+
| Fired At | UTC timestamp from the alert payload |
60+
| Resource | First configuration item (affected resource name) |
61+
| Description | Alert rule description |
62+
| Link | Deep link to the Failures blade (App Insights alerts) or the resource overview (all other alert types) |
63+
64+
## Common alert schema
65+
66+
The Logic App trigger is configured to accept the [Azure Monitor common alert schema](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-common-schema). Ensure `use_common_alert_schema = true` is set on the action group webhook receiver (as shown in the usage example above), otherwise the trigger will reject the payload.
67+
68+
## Webhook URL security
69+
70+
The `trigger_callback_url` output is marked `sensitive`. It contains a SAS token that grants the ability to trigger the Logic App — treat it like a secret. Azure regenerates this URL if the Logic App is recreated.
71+
72+
The `slack_webhook_url` is stored as a `SecureString` workflow parameter in the Logic App, meaning it does not appear in the run history or trigger inputs in the Azure portal.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
resource "azurerm_logic_app_workflow" "this" {
2+
name = var.name
3+
resource_group_name = var.resource_group_name
4+
location = var.location
5+
6+
workflow_parameters = {
7+
"slackWebhookUrl" = jsonencode({
8+
type = "SecureString"
9+
defaultValue = ""
10+
})
11+
}
12+
13+
parameters = {
14+
"slackWebhookUrl" = var.slack_webhook_url
15+
}
16+
}
17+
18+
resource "azurerm_logic_app_trigger_http_request" "this" {
19+
name = "When_an_HTTP_request_is_received"
20+
logic_app_id = azurerm_logic_app_workflow.this.id
21+
method = "POST"
22+
23+
schema = jsonencode({
24+
"$schema" = "http://json-schema.org/draft-04/schema#"
25+
type = "object"
26+
properties = {
27+
schemaId = { type = "string" }
28+
data = {
29+
type = "object"
30+
properties = {
31+
essentials = {
32+
type = "object"
33+
properties = {
34+
alertRule = { type = "string" }
35+
severity = { type = "string" }
36+
firedDateTime = { type = "string" }
37+
resolvedDateTime = { type = "string" }
38+
monitorCondition = { type = "string" }
39+
description = { type = "string" }
40+
alertTargetIDs = {
41+
type = "array"
42+
items = { type = "string" }
43+
}
44+
configurationItems = {
45+
type = "array"
46+
items = { type = "string" }
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}
53+
})
54+
}
55+
56+
resource "azurerm_logic_app_action_custom" "post_to_slack" {
57+
name = "Post_to_Slack"
58+
logic_app_id = azurerm_logic_app_workflow.this.id
59+
60+
body = <<-BODY
61+
{
62+
"type": "Http",
63+
"inputs": {
64+
"method": "POST",
65+
"uri": "@parameters('slackWebhookUrl')",
66+
"headers": {
67+
"Content-Type": "application/json"
68+
},
69+
"body": {
70+
"blocks": [
71+
{
72+
"type": "header",
73+
"text": {
74+
"type": "plain_text",
75+
"text": "@{if(equals(triggerBody()?['data']?['essentials']?['monitorCondition'], 'Resolved'), concat('✅ Resolved – ', triggerBody()?['data']?['essentials']?['alertRule']), concat('🚨 Alert – ', triggerBody()?['data']?['essentials']?['alertRule']))}",
76+
"emoji": true
77+
}
78+
},
79+
{
80+
"type": "section",
81+
"fields": [
82+
{
83+
"type": "mrkdwn",
84+
"text": "@{concat('*Severity*\n', triggerBody()?['data']?['essentials']?['severity'])}"
85+
},
86+
{
87+
"type": "mrkdwn",
88+
"text": "@{concat('*Status*\n', triggerBody()?['data']?['essentials']?['monitorCondition'])}"
89+
},
90+
{
91+
"type": "mrkdwn",
92+
"text": "@{if(equals(triggerBody()?['data']?['essentials']?['monitorCondition'], 'Resolved'), concat('*Resolved At*\n', triggerBody()?['data']?['essentials']?['resolvedDateTime']), concat('*Fired At*\n', triggerBody()?['data']?['essentials']?['firedDateTime']))}"
93+
},
94+
{
95+
"type": "mrkdwn",
96+
"text": "@{concat('*Resource*\n`', coalesce(triggerBody()?['data']?['essentials']?['configurationItems']?[0], 'N/A'), '`')}"
97+
}
98+
]
99+
},
100+
{
101+
"type": "section",
102+
"text": {
103+
"type": "mrkdwn",
104+
"text": "@{concat('*Description*\n> ', coalesce(triggerBody()?['data']?['essentials']?['description'], 'N/A'))}"
105+
}
106+
},
107+
{
108+
"type": "section",
109+
"text": {
110+
"type": "mrkdwn",
111+
"text": "@{if(contains(coalesce(triggerBody()?['data']?['essentials']?['alertTargetIDs']?[0], ''), 'microsoft.insights/components'), concat(':mag: <https://portal.azure.com/#resource', triggerBody()?['data']?['essentials']?['alertTargetIDs']?[0], '/failures|View Failures in App Insights>'), concat(':mag: <https://portal.azure.com/#resource', coalesce(triggerBody()?['data']?['essentials']?['alertTargetIDs']?[0], ''), '|View Resource in Azure Portal>'))}"
112+
}
113+
}
114+
]
115+
}
116+
},
117+
"runAfter": {}
118+
}
119+
BODY
120+
121+
depends_on = [azurerm_logic_app_trigger_http_request.this]
122+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
output "trigger_callback_url" {
2+
description = "HTTP trigger callback URL to register with an Azure Monitor action group webhook receiver."
3+
value = azurerm_logic_app_trigger_http_request.this.callback_url
4+
sensitive = true
5+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Module documentation
2+
3+
## Required Inputs
4+
5+
The following input variables are required:
6+
7+
### <a name="input_location"></a> [location](#input\_location)
8+
9+
Description: Azure region in which to create the Logic App.
10+
11+
Type: `string`
12+
13+
### <a name="input_name"></a> [name](#input\_name)
14+
15+
Description: Name of the Logic App workflow.
16+
17+
Type: `string`
18+
19+
### <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name)
20+
21+
Description: Resource group in which to create the Logic App.
22+
23+
Type: `string`
24+
25+
### <a name="input_slack_webhook_url"></a> [slack\_webhook\_url](#input\_slack\_webhook\_url)
26+
27+
Description: Slack incoming webhook URL. Stored as a SecureString workflow parameter.
28+
29+
Type: `string`
30+
31+
## Outputs
32+
33+
The following outputs are exported:
34+
35+
### <a name="output_trigger_callback_url"></a> [trigger\_callback\_url](#output\_trigger\_callback\_url)
36+
37+
Description: HTTP trigger callback URL to register with an Azure Monitor action group webhook receiver.
38+
## Resources
39+
40+
The following resources are used by this module:
41+
42+
- [azurerm_logic_app_action_custom.post_to_slack](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/logic_app_action_custom) (resource)
43+
- [azurerm_logic_app_trigger_http_request.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/logic_app_trigger_http_request) (resource)
44+
- [azurerm_logic_app_workflow.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/logic_app_workflow) (resource)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
variable "name" {
2+
type = string
3+
description = "Name of the Logic App workflow."
4+
}
5+
6+
variable "resource_group_name" {
7+
type = string
8+
description = "Resource group in which to create the Logic App."
9+
}
10+
11+
variable "location" {
12+
type = string
13+
description = "Azure region in which to create the Logic App."
14+
}
15+
16+
variable "slack_webhook_url" {
17+
type = string
18+
description = "Slack incoming webhook URL. Stored as a SecureString workflow parameter."
19+
sensitive = true
20+
}

infrastructure/modules/virtual-desktop/tfdocs.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ Type: `number`
168168

169169
Default: `16`
170170

171+
### <a name="input_principal_id"></a> [principal\_id](#input\_principal\_id)
172+
173+
Description: The principal (object) ID to assign the 'Desktop Virtualization Power On Off Contributor' role to the host pool. If null, the role assignment will not be created. This maintains backward compatibility for existing deployments. The role is required for autoscaling but can be omitted if autoscaling is not used or the role is assigned manually.
174+
175+
Type: `string`
176+
177+
Default: `null`
178+
171179
### <a name="input_source_image_from_gallery"></a> [source\_image\_from\_gallery](#input\_source\_image\_from\_gallery)
172180

173181
Description: n/a
@@ -279,6 +287,7 @@ Default: `null`
279287
The following resources are used by this module:
280288

281289
- [azurerm_network_interface.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_interface) (resource)
290+
- [azurerm_role_assignment.avd_autoscale_hostpool](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource)
282291
- [azurerm_role_assignment.dag_admins](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource)
283292
- [azurerm_role_assignment.dag_users](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource)
284293
- [azurerm_role_assignment.rg_admins](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource)

0 commit comments

Comments
 (0)