diff --git a/6_monitoring-management/advisor/README.md b/6_monitoring-management/advisor/README.md new file mode 100644 index 0000000..f346da9 --- /dev/null +++ b/6_monitoring-management/advisor/README.md @@ -0,0 +1,27 @@ +# Terraform Template - Azure Advisor Alerts (Activity Log) + +Costa Rica + +[![GitHub](https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff)](https://github.com/) +[brown9804](https://github.com/brown9804) + +Last updated: 2026-02-16 + +------------------------------------------ + +> This template contains Terraform configurations to create an Action Group and an Activity Log Alert for new Azure Advisor recommendations. + +> [!NOTE] +> This is based on the Microsoft Learn quickstart for Advisor alerts. + +image + +## Usage + +```sh +az login +terraform init -upgrade +terraform validate +terraform plan +terraform apply -auto-approve +``` diff --git a/6_monitoring-management/advisor/main.tf b/6_monitoring-management/advisor/main.tf new file mode 100644 index 0000000..3b88a2d --- /dev/null +++ b/6_monitoring-management/advisor/main.tf @@ -0,0 +1,139 @@ +# main.tf +# Creates an Action Group + Activity Log Alert for new Azure Advisor recommendations. +# Based on Microsoft Learn quickstart: https://learn.microsoft.com/azure/advisor/advisor-alerts-arm + +data "azurerm_client_config" "current" {} + +resource "azapi_resource" "resource_group" { + type = "Microsoft.Resources/resourceGroups@2022-09-01" + name = var.resource_group_name + location = var.location + parent_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}" + + body = jsonencode({ + tags = var.tags + }) + + response_export_values = [ + "id", + "name" + ] +} + +resource "random_string" "suffix" { + length = var.random_suffix_length + upper = false + special = false + numeric = true + + keepers = { + resource_group_name = var.resource_group_name + location = var.location + action_group_base = var.action_group_name + alert_base = var.activity_log_alert_name + email = var.email_address + category = var.recommendation_category + impact = var.recommendation_impact + } +} + +locals { + suffix = var.append_random_suffix ? random_string.suffix.result : "" + + action_group_name = var.append_random_suffix ? "${var.action_group_name}-${local.suffix}" : var.action_group_name + activity_log_alert_name = var.append_random_suffix ? "${var.activity_log_alert_name}-${local.suffix}" : var.activity_log_alert_name + + subscription_scope = "/subscriptions/${data.azurerm_client_config.current.subscription_id}" + alert_scope = ( + var.scope_resource_group_name == null + ? local.subscription_scope + : "${local.subscription_scope}/resourceGroups/${var.scope_resource_group_name}" + ) +} + +resource "azapi_resource" "action_group" { + type = "Microsoft.Insights/actionGroups@2019-06-01" + name = local.action_group_name + location = "Global" + parent_id = azapi_resource.resource_group.id + + body = jsonencode({ + properties = { + groupShortName = var.action_group_short_name + enabled = true + emailReceivers = [ + { + name = "email" + emailAddress = var.email_address + } + ] + smsReceivers = [] + webhookReceivers = [] + } + tags = var.tags + }) + + response_export_values = [ + "id", + "name" + ] + + depends_on = [ + azapi_resource.resource_group + ] +} + +resource "azapi_resource" "advisor_activity_log_alert" { + type = "Microsoft.Insights/activityLogAlerts@2017-04-01" + name = local.activity_log_alert_name + location = "Global" + parent_id = azapi_resource.resource_group.id + + body = jsonencode({ + properties = { + scopes = [ + local.alert_scope + ] + condition = { + allOf = [ + { + field = "category" + equals = "Recommendation" + }, + { + field = "properties.recommendationCategory" + equals = var.recommendation_category + }, + { + field = "properties.recommendationImpact" + equals = var.recommendation_impact + }, + { + field = "operationName" + equals = "Microsoft.Advisor/recommendations/available/action" + } + ] + } + actions = { + actionGroups = [ + { + actionGroupId = azapi_resource.action_group.id + webhookProperties = {} + } + ] + } + enabled = var.enabled + description = var.description + } + tags = var.tags + }) + + response_export_values = [ + "id", + "name" + ] + + depends_on = [ + azapi_resource.action_group + ] +} diff --git a/6_monitoring-management/advisor/outputs.tf b/6_monitoring-management/advisor/outputs.tf new file mode 100644 index 0000000..c19043c --- /dev/null +++ b/6_monitoring-management/advisor/outputs.tf @@ -0,0 +1,14 @@ +output "resource_group_id" { + description = "The resource ID of the Resource Group." + value = azapi_resource.resource_group.id +} + +output "action_group_id" { + description = "The resource ID of the Action Group." + value = azapi_resource.action_group.id +} + +output "activity_log_alert_id" { + description = "The resource ID of the Activity Log Alert." + value = azapi_resource.advisor_activity_log_alert.id +} diff --git a/6_monitoring-management/advisor/provider.tf b/6_monitoring-management/advisor/provider.tf new file mode 100644 index 0000000..5483396 --- /dev/null +++ b/6_monitoring-management/advisor/provider.tf @@ -0,0 +1,37 @@ +# provider.tf + +terraform { + required_version = ">= 1.8, < 2.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.116" + } + + azapi = { + source = "Azure/azapi" + version = "~> 1.13" + } + + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + } +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } + + # Uses the current Azure CLI context (az login + az account set) + skip_provider_registration = false +} + +provider "azapi" { + # Uses the current Azure CLI context (az login + az account set) +} diff --git a/6_monitoring-management/advisor/terraform.tfvars b/6_monitoring-management/advisor/terraform.tfvars new file mode 100644 index 0000000..8230717 --- /dev/null +++ b/6_monitoring-management/advisor/terraform.tfvars @@ -0,0 +1,27 @@ +resource_group_name = "rg-monitoring-dev-advisor" +location = "eastus" + +append_random_suffix = true +random_suffix_length = 6 + +email_address = "user@domain.com" + +action_group_name = "advisorAlert" +action_group_short_name = "advisor" + +activity_log_alert_name = "AdvisorAlerts" + +recommendation_category = "Cost" +recommendation_impact = "Medium" + +# Optional: scope to a specific resource group +# scope_resource_group_name = "rg-some-app-dev" + +enabled = true +description = "" + +tags = { + env = "dev" + area = "monitoring-management" + iac = "terraform" +} diff --git a/6_monitoring-management/advisor/variables.tf b/6_monitoring-management/advisor/variables.tf new file mode 100644 index 0000000..d4f923b --- /dev/null +++ b/6_monitoring-management/advisor/variables.tf @@ -0,0 +1,107 @@ +# variables.tf + +variable "resource_group_name" { + description = "The name of the Azure Resource Group to deploy into. This template will create the RG if it does not exist (idempotent ARM PUT)." + type = string + + validation { + condition = length(trimspace(var.resource_group_name)) > 0 + error_message = "resource_group_name must not be empty." + } +} + +variable "location" { + description = "The Azure region where the Resource Group will be created (Advisor alerts are global resources but live in a Resource Group)." + type = string + + validation { + condition = length(trimspace(var.location)) > 0 + error_message = "location must not be empty." + } +} + +variable "append_random_suffix" { + description = "Whether to append a random suffix to names to reduce collisions." + type = bool + default = true +} + +variable "random_suffix_length" { + description = "Length of the random suffix appended when append_random_suffix is true." + type = number + default = 6 + + validation { + condition = var.random_suffix_length >= 4 && var.random_suffix_length <= 16 + error_message = "random_suffix_length must be between 4 and 16." + } +} + +variable "action_group_name" { + description = "Base name of the Action Group. If append_random_suffix is true, the final name will be '-'." + type = string + default = "advisorAlert" +} + +variable "action_group_short_name" { + description = "Short name for the Action Group (max 12 characters)." + type = string + default = "advisor" + + validation { + condition = length(trimspace(var.action_group_short_name)) >= 1 && length(var.action_group_short_name) <= 12 + error_message = "action_group_short_name must be 1-12 characters." + } +} + +variable "activity_log_alert_name" { + description = "Base name of the Activity Log Alert. If append_random_suffix is true, the final name will be '-'." + type = string + default = "AdvisorAlerts" +} + +variable "email_address" { + description = "Email address to receive Advisor recommendation alerts." + type = string + + validation { + condition = length(trimspace(var.email_address)) > 3 && can(regex("@", var.email_address)) + error_message = "email_address must look like an email address." + } +} + +variable "recommendation_category" { + description = "Advisor recommendation category filter (for example: Cost, Performance, HighAvailability)." + type = string + default = "Cost" +} + +variable "recommendation_impact" { + description = "Advisor recommendation impact filter (for example: High, Medium, Low)." + type = string + default = "Medium" +} + +variable "scope_resource_group_name" { + description = "Optional resource group name to scope the Activity Log Alert to. If null, alert is scoped to the subscription." + type = string + default = null +} + +variable "enabled" { + description = "Whether the Activity Log Alert is enabled." + type = bool + default = true +} + +variable "description" { + description = "Optional description for the alert." + type = string + default = "" +} + +variable "tags" { + description = "A map of tags to assign to the resources." + type = map(string) + default = {} +} diff --git a/6_monitoring-management/automation/README.md b/6_monitoring-management/automation/README.md new file mode 100644 index 0000000..6e03c82 --- /dev/null +++ b/6_monitoring-management/automation/README.md @@ -0,0 +1,34 @@ +# Terraform Template - Azure Automation Account + +Costa Rica + +[![GitHub](https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff)](https://github.com/) +[brown9804](https://github.com/brown9804) + +Last updated: 2026-02-16 + +------------------------------------------ + +> This template contains Terraform configurations to create an Azure Automation Account. + +image + +image + +## File Descriptions + +- **main.tf**: Creates the Resource Group (AzAPI) and Automation Account. +- **variables.tf**: Input variables. +- **provider.tf**: AzureRM + AzAPI providers. +- **terraform.tfvars**: Example values. +- **outputs.tf**: Outputs (IDs/names). + +## Usage + +```sh +az login +terraform init -upgrade +terraform validate +terraform plan +terraform apply -auto-approve +``` diff --git a/6_monitoring-management/automation/main.tf b/6_monitoring-management/automation/main.tf new file mode 100644 index 0000000..cc0e16a --- /dev/null +++ b/6_monitoring-management/automation/main.tf @@ -0,0 +1,58 @@ +# main.tf +# Creates an Azure Automation Account. + +data "azurerm_client_config" "current" {} + +resource "azapi_resource" "resource_group" { + type = "Microsoft.Resources/resourceGroups@2022-09-01" + name = var.resource_group_name + location = var.location + parent_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}" + + body = jsonencode({ + tags = var.tags + }) + + response_export_values = [ + "id", + "name" + ] +} + +resource "random_string" "suffix" { + length = var.random_suffix_length + upper = false + special = false + numeric = true + + keepers = { + resource_group_name = var.resource_group_name + location = var.location + base_name = var.automation_account_name + sku = var.sku_name + } +} + +locals { + automation_account_name = var.append_random_suffix ? "${var.automation_account_name}-${random_string.suffix.result}" : var.automation_account_name +} + +resource "azurerm_automation_account" "aa" { + name = local.automation_account_name + location = var.location + resource_group_name = var.resource_group_name + sku_name = var.sku_name + + dynamic "identity" { + for_each = var.enable_system_assigned_identity ? [1] : [] + content { + type = "SystemAssigned" + } + } + + tags = var.tags + + depends_on = [ + azapi_resource.resource_group + ] +} diff --git a/6_monitoring-management/automation/outputs.tf b/6_monitoring-management/automation/outputs.tf new file mode 100644 index 0000000..b428150 --- /dev/null +++ b/6_monitoring-management/automation/outputs.tf @@ -0,0 +1,19 @@ +output "resource_group_id" { + description = "The resource ID of the Resource Group." + value = azapi_resource.resource_group.id +} + +output "automation_account_id" { + description = "The resource ID of the Automation Account." + value = azurerm_automation_account.aa.id +} + +output "automation_account_name" { + description = "The name of the Automation Account." + value = azurerm_automation_account.aa.name +} + +output "automation_account_identity_principal_id" { + description = "Principal ID of the System Assigned Managed Identity (if enabled)." + value = try(azurerm_automation_account.aa.identity[0].principal_id, null) +} diff --git a/6_monitoring-management/automation/provider.tf b/6_monitoring-management/automation/provider.tf new file mode 100644 index 0000000..5483396 --- /dev/null +++ b/6_monitoring-management/automation/provider.tf @@ -0,0 +1,37 @@ +# provider.tf + +terraform { + required_version = ">= 1.8, < 2.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.116" + } + + azapi = { + source = "Azure/azapi" + version = "~> 1.13" + } + + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + } +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } + + # Uses the current Azure CLI context (az login + az account set) + skip_provider_registration = false +} + +provider "azapi" { + # Uses the current Azure CLI context (az login + az account set) +} diff --git a/6_monitoring-management/automation/terraform.tfvars b/6_monitoring-management/automation/terraform.tfvars new file mode 100644 index 0000000..c8ff01b --- /dev/null +++ b/6_monitoring-management/automation/terraform.tfvars @@ -0,0 +1,15 @@ +resource_group_name = "rg-monitoring-dev-auto" +location = "eastus" + +append_random_suffix = true +random_suffix_length = 6 + +automation_account_name = "aa-monitoring-dev" +sku_name = "Basic" +enable_system_assigned_identity = true + +tags = { + env = "dev" + area = "monitoring-management" + iac = "terraform" +} diff --git a/6_monitoring-management/automation/variables.tf b/6_monitoring-management/automation/variables.tf new file mode 100644 index 0000000..51918fe --- /dev/null +++ b/6_monitoring-management/automation/variables.tf @@ -0,0 +1,70 @@ +# variables.tf + +variable "resource_group_name" { + description = "The name of the Azure Resource Group to deploy into. This template will create the RG if it does not exist (idempotent ARM PUT)." + type = string + + validation { + condition = length(trimspace(var.resource_group_name)) > 0 + error_message = "resource_group_name must not be empty." + } +} + +variable "location" { + description = "The Azure region where the Resource Group and Automation Account will be created." + type = string + + validation { + condition = length(trimspace(var.location)) > 0 + error_message = "location must not be empty." + } +} + +variable "append_random_suffix" { + description = "Whether to append a random suffix to the Automation Account name to reduce naming collisions." + type = bool + default = true +} + +variable "random_suffix_length" { + description = "Length of the random suffix appended when append_random_suffix is true." + type = number + default = 6 + + validation { + condition = var.random_suffix_length >= 4 && var.random_suffix_length <= 16 + error_message = "random_suffix_length must be between 4 and 16." + } +} + +variable "automation_account_name" { + description = "Base name of the Automation Account. If append_random_suffix is true, the final name will be '-'." + type = string + + validation { + condition = ( + length(trimspace(var.automation_account_name)) > 0 + && length(var.automation_account_name) <= (var.append_random_suffix ? (50 - 1 - var.random_suffix_length) : 50) + && can(regex("^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$", var.automation_account_name)) + ) + error_message = "automation_account_name must be 1-50 chars, contain only alphanumeric or '-', start/end with alphanumeric, and leave room for '-' when append_random_suffix is true." + } +} + +variable "sku_name" { + description = "Automation Account SKU." + type = string + default = "Basic" +} + +variable "enable_system_assigned_identity" { + description = "Whether to enable a System Assigned Managed Identity on the Automation Account." + type = bool + default = true +} + +variable "tags" { + description = "A map of tags to assign to the resources." + type = map(string) + default = {} +}