diff --git a/infrastructure/modules/logic-app-slack-alert/README.md b/infrastructure/modules/logic-app-slack-alert/README.md new file mode 100644 index 00000000..41696c98 --- /dev/null +++ b/infrastructure/modules/logic-app-slack-alert/README.md @@ -0,0 +1,72 @@ +# logic-app-slack-alert + +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. + +## Terraform documentation + +For the list of inputs, outputs, resources... check the [terraform module documentation](tfdocs.md). + +## Prerequisites + +- 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`. +- 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. + +## Usage + +```hcl +module "logic_app_slack_alert" { + source = "../../../dtos-devops-templates/infrastructure/modules/logic-app-slack-alert" + + name = "logic-slack-alert-myapp-dev-uks" + resource_group_name = azurerm_resource_group.main.name + location = "uksouth" + slack_webhook_url = data.azurerm_key_vault_secret.slack_webhook.value +} + +resource "azurerm_monitor_action_group" "slack" { + name = "ag-slack-myapp-dev-uks" + resource_group_name = azurerm_resource_group.main.name + short_name = "slack" + + webhook_receiver { + name = "logic-app-slack" + service_uri = module.logic_app_slack_alert.trigger_callback_url + use_common_alert_schema = true + } +} +``` + +Then attach the action group to any Azure Monitor alert rule: + +```hcl +resource "azurerm_monitor_metric_alert" "example" { + # ... + action { + action_group_id = azurerm_monitor_action_group.slack.id + } +} +``` + +## Slack message format + +Each alert posts a Block Kit message to the configured channel containing: + +| Field | Source | +|-------|--------| +| Header | Alert rule name, prefixed with 🚨 (fired) or ✅ (resolved) | +| Severity | `Sev0`–`Sev4` from the alert essentials | +| Status | `Fired` or `Resolved` | +| Fired At | UTC timestamp from the alert payload | +| Resource | First configuration item (affected resource name) | +| Description | Alert rule description | +| Link | Deep link to the Failures blade (App Insights alerts) or the resource overview (all other alert types) | + +## Common alert schema + +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. + +## Webhook URL security + +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. + +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. diff --git a/infrastructure/modules/logic-app-slack-alert/main.tf b/infrastructure/modules/logic-app-slack-alert/main.tf new file mode 100644 index 00000000..5f9afb21 --- /dev/null +++ b/infrastructure/modules/logic-app-slack-alert/main.tf @@ -0,0 +1,122 @@ +resource "azurerm_logic_app_workflow" "this" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + + workflow_parameters = { + "slackWebhookUrl" = jsonencode({ + type = "SecureString" + defaultValue = "" + }) + } + + parameters = { + "slackWebhookUrl" = var.slack_webhook_url + } +} + +resource "azurerm_logic_app_trigger_http_request" "this" { + name = "When_an_HTTP_request_is_received" + logic_app_id = azurerm_logic_app_workflow.this.id + method = "POST" + + schema = jsonencode({ + "$schema" = "http://json-schema.org/draft-04/schema#" + type = "object" + properties = { + schemaId = { type = "string" } + data = { + type = "object" + properties = { + essentials = { + type = "object" + properties = { + alertRule = { type = "string" } + severity = { type = "string" } + firedDateTime = { type = "string" } + resolvedDateTime = { type = "string" } + monitorCondition = { type = "string" } + description = { type = "string" } + alertTargetIDs = { + type = "array" + items = { type = "string" } + } + configurationItems = { + type = "array" + items = { type = "string" } + } + } + } + } + } + } + }) +} + +resource "azurerm_logic_app_action_custom" "post_to_slack" { + name = "Post_to_Slack" + logic_app_id = azurerm_logic_app_workflow.this.id + + body = <<-BODY + { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@parameters('slackWebhookUrl')", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "@{if(equals(triggerBody()?['data']?['essentials']?['monitorCondition'], 'Resolved'), concat('✅ Resolved – ', triggerBody()?['data']?['essentials']?['alertRule']), concat('🚨 Alert – ', triggerBody()?['data']?['essentials']?['alertRule']))}", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "@{concat('*Severity*\n', triggerBody()?['data']?['essentials']?['severity'])}" + }, + { + "type": "mrkdwn", + "text": "@{concat('*Status*\n', triggerBody()?['data']?['essentials']?['monitorCondition'])}" + }, + { + "type": "mrkdwn", + "text": "@{if(equals(triggerBody()?['data']?['essentials']?['monitorCondition'], 'Resolved'), concat('*Resolved At*\n', triggerBody()?['data']?['essentials']?['resolvedDateTime']), concat('*Fired At*\n', triggerBody()?['data']?['essentials']?['firedDateTime']))}" + }, + { + "type": "mrkdwn", + "text": "@{concat('*Resource*\n`', coalesce(triggerBody()?['data']?['essentials']?['configurationItems']?[0], 'N/A'), '`')}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "@{concat('*Description*\n> ', coalesce(triggerBody()?['data']?['essentials']?['description'], 'N/A'))}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "@{if(contains(coalesce(triggerBody()?['data']?['essentials']?['alertTargetIDs']?[0], ''), 'microsoft.insights/components'), concat(':mag: '), concat(':mag: '))}" + } + } + ] + } + }, + "runAfter": {} + } + BODY + + depends_on = [azurerm_logic_app_trigger_http_request.this] +} diff --git a/infrastructure/modules/logic-app-slack-alert/outputs.tf b/infrastructure/modules/logic-app-slack-alert/outputs.tf new file mode 100644 index 00000000..b002dbb2 --- /dev/null +++ b/infrastructure/modules/logic-app-slack-alert/outputs.tf @@ -0,0 +1,5 @@ +output "trigger_callback_url" { + description = "HTTP trigger callback URL to register with an Azure Monitor action group webhook receiver." + value = azurerm_logic_app_trigger_http_request.this.callback_url + sensitive = true +} diff --git a/infrastructure/modules/logic-app-slack-alert/tfdocs.md b/infrastructure/modules/logic-app-slack-alert/tfdocs.md new file mode 100644 index 00000000..b21deb2d --- /dev/null +++ b/infrastructure/modules/logic-app-slack-alert/tfdocs.md @@ -0,0 +1,44 @@ +# Module documentation + +## Required Inputs + +The following input variables are required: + +### [location](#input\_location) + +Description: Azure region in which to create the Logic App. + +Type: `string` + +### [name](#input\_name) + +Description: Name of the Logic App workflow. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Resource group in which to create the Logic App. + +Type: `string` + +### [slack\_webhook\_url](#input\_slack\_webhook\_url) + +Description: Slack incoming webhook URL. Stored as a SecureString workflow parameter. + +Type: `string` + +## Outputs + +The following outputs are exported: + +### [trigger\_callback\_url](#output\_trigger\_callback\_url) + +Description: HTTP trigger callback URL to register with an Azure Monitor action group webhook receiver. +## Resources + +The following resources are used by this module: + +- [azurerm_logic_app_action_custom.post_to_slack](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/logic_app_action_custom) (resource) +- [azurerm_logic_app_trigger_http_request.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/logic_app_trigger_http_request) (resource) +- [azurerm_logic_app_workflow.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/logic_app_workflow) (resource) diff --git a/infrastructure/modules/logic-app-slack-alert/variables.tf b/infrastructure/modules/logic-app-slack-alert/variables.tf new file mode 100644 index 00000000..963f16b1 --- /dev/null +++ b/infrastructure/modules/logic-app-slack-alert/variables.tf @@ -0,0 +1,20 @@ +variable "name" { + type = string + description = "Name of the Logic App workflow." +} + +variable "resource_group_name" { + type = string + description = "Resource group in which to create the Logic App." +} + +variable "location" { + type = string + description = "Azure region in which to create the Logic App." +} + +variable "slack_webhook_url" { + type = string + description = "Slack incoming webhook URL. Stored as a SecureString workflow parameter." + sensitive = true +} diff --git a/infrastructure/modules/virtual-desktop/tfdocs.md b/infrastructure/modules/virtual-desktop/tfdocs.md index 21db1e2f..653fec8f 100644 --- a/infrastructure/modules/virtual-desktop/tfdocs.md +++ b/infrastructure/modules/virtual-desktop/tfdocs.md @@ -168,6 +168,14 @@ Type: `number` Default: `16` +### [principal\_id](#input\_principal\_id) + +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. + +Type: `string` + +Default: `null` + ### [source\_image\_from\_gallery](#input\_source\_image\_from\_gallery) Description: n/a @@ -279,6 +287,7 @@ Default: `null` The following resources are used by this module: - [azurerm_network_interface.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_interface) (resource) +- [azurerm_role_assignment.avd_autoscale_hostpool](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource) - [azurerm_role_assignment.dag_admins](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource) - [azurerm_role_assignment.dag_users](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource) - [azurerm_role_assignment.rg_admins](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource)