From ef3768ff067661834e642a2432dbe96b805fbe45 Mon Sep 17 00:00:00 2001 From: msbrett Date: Mon, 2 Nov 2026 10:46:18 -0800 Subject: [PATCH 1/2] Add cost-management SRE Agent recipe Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../recipes/cost-management/.gitignore | 3 + .../recipes/cost-management/README.md | 134 +++++ .../recipes/cost-management/agent.json | 64 ++ .../advisor-suppression-review.yaml | 57 ++ .../ai-workload-cost-analysis.yaml | 115 ++++ .../scheduled-tasks/alert-coverage-audit.yaml | 58 ++ .../benefit-recommendation-review.yaml | 59 ++ .../budget-coverage-audit.yaml | 57 ++ .../capacity-daily-monitor.yaml | 139 +++++ .../capacity-monthly-planning.yaml | 170 ++++++ .../capacity-quarterly-strategy.yaml | 164 +++++ .../capacity-weekly-supply-review.yaml | 145 +++++ .../compute-utilization-trend.yaml | 77 +++ .../scheduled-tasks/cost-optimization.yaml | 190 ++++++ .../scheduled-tasks/db-quota-audit.yaml | 59 ++ .../scheduled-tasks/monthly-report.yaml | 89 +++ .../non-compute-quota-audit.yaml | 77 +++ .../sku-availability-audit.yaml | 85 +++ .../storage-paas-growth-forecast.yaml | 79 +++ .../scheduled-tasks/yoy-report.yaml | 92 +++ .../config/common-prompts/safety-rules.yaml | 15 + .../config/connectorv2/microsoft-teams.yaml | 46 ++ .../plugins/installations/azcapman.yaml | 8 + .../config/plugins/marketplaces/azcapman.yaml | 6 + .../config/skills/azure-cost-management.md | 197 ++++++ .../config/skills/azure-cost-management.yaml | 38 ++ .../references/azure-advisor.md | 259 ++++++++ .../references/azure-anomaly-alerts.md | 292 +++++++++ .../references/azure-budgets.md | 188 ++++++ .../azure-commitment-discount-decision.md | 218 +++++++ .../references/azure-cost-exports.md | 167 ++++++ .../references/azure-credits.md | 163 +++++ .../references/azure-macc.md | 131 ++++ .../references/azure-orphaned-resources.md | 329 ++++++++++ .../references/azure-reservations.md | 353 +++++++++++ .../references/azure-retail-prices.md | 263 ++++++++ .../references/azure-savings-plans.md | 424 +++++++++++++ .../references/azure-vm-rightsizing.md | 293 +++++++++ .../subagents/azure-capacity-manager.yaml | 145 +++++ .../subagents/chief-financial-officer.yaml | 47 ++ .../config/subagents/finops-practitioner.yaml | 109 ++++ .../config/tools/benefit-recommendations.yaml | 186 ++++++ .../tools/capacity-reservation-groups.yaml | 199 +++++++ .../config/tools/db-service-quotas.yaml | 562 ++++++++++++++++++ .../config/tools/deploy-anomaly-alert.yaml | 114 ++++ .../config/tools/deploy-budget.yaml | 176 ++++++ .../tools/deploy-bulk-anomaly-alerts.yaml | 153 +++++ .../config/tools/deploy-bulk-budgets.yaml | 202 +++++++ .../config/tools/non-compute-quotas.yaml | 505 ++++++++++++++++ .../config/tools/resource-graph-query.yaml | 129 ++++ .../config/tools/sku-availability.yaml | 155 +++++ .../suppress-advisor-recommendations.yaml | 161 +++++ .../config/tools/vm-quota-usage.yaml | 196 ++++++ .../config/tools/zone-mapping.yaml | 288 +++++++++ .../recipes/cost-management/connectors.json | 62 ++ .../cost-management/data/knowledge-items.json | 51 ++ .../cost-management/expected-config.json | 76 +++ .../cost-management/tool-overrides.json | 40 ++ .../cost-management/tool-permissions.json | 21 + 59 files changed, 8880 insertions(+) create mode 100644 sreagent-templates/recipes/cost-management/.gitignore create mode 100644 sreagent-templates/recipes/cost-management/README.md create mode 100644 sreagent-templates/recipes/cost-management/agent.json create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/advisor-suppression-review.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/ai-workload-cost-analysis.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/alert-coverage-audit.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/benefit-recommendation-review.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/budget-coverage-audit.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-daily-monitor.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-monthly-planning.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-quarterly-strategy.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-weekly-supply-review.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/compute-utilization-trend.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/cost-optimization.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/db-quota-audit.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/monthly-report.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/non-compute-quota-audit.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/sku-availability-audit.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/storage-paas-growth-forecast.yaml create mode 100644 sreagent-templates/recipes/cost-management/automations/scheduled-tasks/yoy-report.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/common-prompts/safety-rules.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/plugins/installations/azcapman.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/plugins/marketplaces/azcapman.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-advisor.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-anomaly-alerts.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-budgets.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-commitment-discount-decision.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-cost-exports.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-credits.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-macc.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-orphaned-resources.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-reservations.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-retail-prices.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-savings-plans.md create mode 100644 sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-vm-rightsizing.md create mode 100644 sreagent-templates/recipes/cost-management/config/subagents/azure-capacity-manager.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/subagents/chief-financial-officer.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/subagents/finops-practitioner.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/benefit-recommendations.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/capacity-reservation-groups.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/db-service-quotas.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/deploy-anomaly-alert.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/deploy-budget.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-anomaly-alerts.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-budgets.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/non-compute-quotas.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/resource-graph-query.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/sku-availability.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/suppress-advisor-recommendations.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/vm-quota-usage.yaml create mode 100644 sreagent-templates/recipes/cost-management/config/tools/zone-mapping.yaml create mode 100644 sreagent-templates/recipes/cost-management/connectors.json create mode 100644 sreagent-templates/recipes/cost-management/data/knowledge-items.json create mode 100644 sreagent-templates/recipes/cost-management/expected-config.json create mode 100644 sreagent-templates/recipes/cost-management/tool-overrides.json create mode 100644 sreagent-templates/recipes/cost-management/tool-permissions.json diff --git a/sreagent-templates/recipes/cost-management/.gitignore b/sreagent-templates/recipes/cost-management/.gitignore new file mode 100644 index 000000000..943e0ff3d --- /dev/null +++ b/sreagent-templates/recipes/cost-management/.gitignore @@ -0,0 +1,3 @@ +connectors.secrets.env +*.secrets.env +live-reports/*.local.html diff --git a/sreagent-templates/recipes/cost-management/README.md b/sreagent-templates/recipes/cost-management/README.md new file mode 100644 index 000000000..73d17cf14 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/README.md @@ -0,0 +1,134 @@ +# Cost Management + +Deploy an Azure SRE Agent for Azure Cost Management analytics and FinOps operations. + +## Prerequisites + +- An Azure subscription with Cost Management Reader access +- At least one Azure resource group to monitor +- All [CLI tools](../../README.md#prerequisites) installed +- A Microsoft Teams account for post-deployment connector authorization +- SRE Agent Author or Administrator access to install public marketplace plugins +- Reader access for the agent identity on every subscription assessed for quota, + capacity, or cross-subscription availability-zone mapping + +## Quick Start + +Generate an agent configuration: + +```bash +./bin/new-agent.sh --recipe cost-management --non-interactive \ + --set agentName=cost-management-sre \ + --set resourceGroup=cost-management-sre \ + --set location=eastus2 \ + --set targetRGs=cost-management-demo \ + -o agents/cost-management/ +``` + +Deploy the generated configuration: + +```bash +./bin/deploy.sh agents/cost-management/ +``` + +## Post-deployment setup + +Complete the interactive OAuth authorization for Microsoft Teams after every +new deployment. + +### Authorize Microsoft Teams + +1. Open [Azure SRE Agent](https://sre.azure.com). +2. Select the deployed agent, then go to **Builder** > **Connectors**. +3. Open **Microsoft Teams** and complete the interactive OAuth sign-in. + +Teams authorization is intentionally left to the user. The recipe does not +collect, store, or automate user credentials. + +### Authorize Azure Resource Manager MCP + +Grant the agent's system-assigned managed identity access to the required Cost +Management and billing scopes. The **azure-resource-manager-mcp** connector uses +the ARM MCP endpoint as its data source and requests an `AzureARM` token for +`https://mcp.management.azure.com/.default`. It does not require interactive user +authentication or stored credentials. + +## Parameters + +| Parameter | Required | Description | +|---|---|---| +| `agentName` | Yes | Lowercase Azure SRE Agent name | +| `resourceGroup` | Yes | Resource group for the agent | +| `location` | Yes | Supported Azure SRE Agent region | +| `targetRGs` | Yes | Comma-separated resource groups to monitor | +| `existingUamiId` | No | Existing user-assigned managed identity resource ID | +| `modelProvider` | No | `Anthropic` or `Azure OpenAI` | +| `existingAgentAppInsightsId` | No | Existing Application Insights resource ID for agent telemetry | + +## What You Get + +| Category | Items | +|---|---| +| **Agent** | Review-mode Cost Management SRE Agent | +| **Connectors** | Microsoft Teams and Azure Resource Manager HTTP MCP | +| **Tools** | 13 Python tools and 14 selected Cost Management and Pricing MCP tools; five visualization and four Log Query tools are enabled and globally allowed | +| **Skills** | Azure capacity management from its public marketplace and the standalone Azure Cost Management skill | +| **Subagents** | FinOps practitioner, finance, and capacity specialists | +| **Automations** | Scheduled FinOps and capacity operations | +| **Knowledge** | Seven public Azure capacity, quota, reservation, zone-mapping, AKS upgrade, and resilience references | + +The Azure Resource Manager MCP connector uses the `x-mcp-toolset: CostManagement` +custom header, `AzureARM` authentication, the +`https://mcp.management.azure.com/.default` token scope, and the agent's +system-assigned managed identity. `create_budget` remains governed by the +agent's **Review** action mode and is not included in the global auto-allow policy. +`tool-overrides.json` controls the portal's On/Off state; `tool-permissions.json` +controls the separate Allow/Ask/Deny approval policy. + +## Plugin skills + +The recipe registers the public +[`microsoft/azcapman`](https://github.com/microsoft/azcapman) marketplace and +installs `azure-capacity-management` from it. The standalone +`azure-cost-management` skill is included in the recipe from its +[official source](https://github.com/microsoft/finops-toolkit/tree/57dece2be058228c6dca6d2e023ae652b7559fc6/src/templates/agent-skills/azure-cost-management). + +## Knowledge sources + +The recipe defines seven URL-backed knowledge items in +`data/knowledge-items.json`. Deployment creates them as `KnowledgeWebPage` +items through the SRE Agent data plane, which fetches and indexes each URL. + +| Knowledge item | Source | +|---|---| +| `well-architected-capacity-planning` | [Well-Architected capacity planning](https://learn.microsoft.com/azure/well-architected/performance-efficiency/capacity-planning) | +| `well-architected-regions-and-zones` | [Well-Architected regions and availability zones](https://learn.microsoft.com/azure/well-architected/design-guides/regions-availability-zones) | +| `well-architected-disaster-recovery` | [Well-Architected disaster recovery](https://learn.microsoft.com/azure/well-architected/reliability/disaster-recovery) | +| `azure-quotas-overview` | [Azure Quotas overview](https://learn.microsoft.com/azure/quotas/quotas-overview) | +| `azure-capacity-reservations` | [On-demand capacity reservations](https://learn.microsoft.com/azure/virtual-machines/capacity-reservation-overview) | +| `azure-check-zone-peers` | [Check Zone Peers API](https://learn.microsoft.com/rest/api/resources/subscriptions/check-zone-peers?view=rest-resources-2022-12-01) | +| `aks-upgrade-capacity-planning` | [AKS upgrade capacity planning](https://learn.microsoft.com/azure/aks/upgrade-capacity-cost-planning) | + +The Microsoft review site is authentication-gated documentation infrastructure, +not an agent connector or identity requirement. The recipe uses only public +Microsoft Learn URLs at runtime. + +## Capacity resilience preflight + +Run an on-demand preflight before a launch, onboarding wave, new deployment +stamp or region, AKS/VMSS upgrade or replacement, failover topology change, +disaster-recovery drill, large scale event, or allocation-failure retry: + +> Ask `azure-capacity-manager` to run a read-only capacity resilience preflight +> for ``. Return PASS, BLOCK, or INSUFFICIENT EVIDENCE. +> Model complete primary and recovery scale units, normal demand, upgrade or +> replacement surge, failover demand, VM and non-compute quota, subnet IP and +> load-balancer limits, SKU and region access, CRG coverage, fallback +> compatibility, zone mapping where subscriptions differ, evidence freshness, +> runway, owners, and escalation deadlines. Classify any allocation failure +> before recommending action. Do not submit requests or change resources. + +Subscription-level evidence requires Reader access at each assessed +subscription. Target-resource-group Reader alone cannot establish +subscription quota or cross-subscription zone mapping. Missing or stale access +evidence must produce `INSUFFICIENT EVIDENCE`, not a guessed mapping. diff --git a/sreagent-templates/recipes/cost-management/agent.json b/sreagent-templates/recipes/cost-management/agent.json new file mode 100644 index 000000000..e629c24b0 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/agent.json @@ -0,0 +1,64 @@ +{ + "_scenario": "cost-management", + "_description": "Azure Cost Management SRE Agent with ARM MCP cost analytics, FinOps subagents, and scheduled operations.", + "_prerequisites": [ + "Azure subscription with Cost Management Reader access", + "At least one resource group to monitor" + ], + "_prompts": { + "agentName": { + "ask": "Agent name (lowercase, hyphens ok)", + "default": "my-sre-agent" + }, + "resourceGroup": { + "ask": "Resource group for the agent", + "default": "sre-agent-rg" + }, + "location": { + "ask": "Region", + "options": ["eastus2", "swedencentral", "uksouth", "australiaeast"], + "required": true + }, + "targetRGs": { + "ask": "Resource groups to monitor (comma-separated)", + "required": true + }, + "existingUamiId": { + "ask": "Existing UAMI resource ID (leave blank to create new)", + "default": "" + }, + "modelProvider": { + "ask": "AI model provider", + "options": ["Anthropic", "Azure OpenAI"], + "default": "Azure OpenAI" + }, + "existingAgentAppInsightsId": { + "ask": "Existing App Insights resource ID for agent telemetry (leave blank to create new)", + "default": "" + } + }, + "identity": { + "agentName": "{{agentName}}", + "resourceGroup": "{{resourceGroup}}", + "subscription": "", + "location": "{{location}}", + "targetResourceGroups": "{{targetRGs}}" + }, + "access": { + "accessLevel": "Low", + "actionMode": "Review" + }, + "upgradeChannel": "Preview", + "defaultModelProvider": "{{modelProvider}}", + "monthlyAgentUnitLimit": 10000, + "tags": { + "finops": "cost-management", + "source": "azure-sre-agent-recipes" + }, + "toggles": { + "enableWebhookBridge": false, + "webhookBridgeTriggerUrl": "" + }, + "existingUamiId": "{{existingUamiId}}", + "existingAgentAppInsightsId": "{{existingAgentAppInsightsId}}" +} diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/advisor-suppression-review.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/advisor-suppression-review.yaml new file mode 100644 index 000000000..aecb538f0 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/advisor-suppression-review.yaml @@ -0,0 +1,57 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: AdvisorSuppressionReview +spec: + name: AdvisorSuppressionReview + description: Monthly review of active Advisor recommendation suppressions for stale or expired decisions + schedule: 0 9 1 * * + mode: Review + agent: finops-practitioner + prompt: |- + # Advisor Suppression Review + + ## Phase 0: Preparation + + Before starting, read all documents in the knowledge base. Pay special attention to Advisor suppression governance, prior suppression decisions, and exception review cadence. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + Apply the FinOps practitioner guidance in this task. Run `resource-graph-query` directly to collect active Advisor recommendation suppressions across in-scope subscriptions. + + ## Specialist tools to request + + Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `finops-practitioner`: Run `resource-graph-query` with `query`, optional `subscription_ids` to find active Advisor recommendation suppressions, suppression metadata, owners, expiration dates, and related resources. + + ## 1. Establish suppression inventory + + Run `resource-graph-query` to find active Advisor recommendation suppressions across in-scope subscriptions. Capture suppression name, subscription, resource, recommendation category, owner metadata, creation date, expiration date, and justification where available. + + ## 2. Classify suppression status + + Classify suppressions as active and current, expired, stale without expiration, missing owner, missing justification, or no longer linked to an active recommendation or resource. + + ## 3. Review stale or expired suppressions + + Flag suppressions that should be reconsidered because they are expired, older than the governance review window, missing justification, suppressing high-value recommendations, or attached to resources that changed materially. + + ## 4. Recommend actions + + Recommend renew, remove, update justification, assign owner, or validate recommendation state for each flagged suppression. Prioritize suppressions that hide optimization, reliability, or security recommendations. + + ## Report format + + Produce a markdown report with an executive summary, an active suppression table, a stale or expired suppression table, missing metadata findings, and recommended owner actions. + + ## Persistence + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, recommendation savings amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + ## Visualizations + + Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum a suppression status chart and a stale suppression age chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/ai-workload-cost-analysis.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/ai-workload-cost-analysis.yaml new file mode 100644 index 000000000..63dc679c6 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/ai-workload-cost-analysis.yaml @@ -0,0 +1,115 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: AIWorkloadCostAnalysis +spec: + name: AIWorkloadCostAnalysis + description: Monthly AI workload cost analysis — token economics, model efficiency, and cost allocation for Azure OpenAI + schedule: 0 10 1 * * + mode: Review + agent: finops-practitioner + prompt: |- + # AI Workload Cost Analysis + + ## Phase 0: Preparation + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + Lead the analysis as the FinOps practitioner. Collect AI cost, pricing, and resource inventory evidence directly; delegate GPU and regional capacity checks to `azure-capacity-manager` and consult `chief-financial-officer` for executive framing. + + Azure OpenAI charges based on token usage — a fundamentally different cost model from traditional compute. This analysis tracks token economics, model efficiency, and AI cost allocation aligned with the FinOps Foundation AI Working Group guidance and FOCUS specification. + ## Evidence requests for this task + + Run `azure-resource-manager-mcp_query_costs` directly, filtered to Azure OpenAI, Cognitive Services, and AI Foundry meters and grouped by `MeterSubCategory`, `Meter`, `ResourceId`, `ResourceGroup`, and application, team, and environment tags for the current 30-day and prior 30-day windows. Use daily granularity for the 30-day trend. Run `azure-resource-manager-mcp_get_retail_prices` for model unit-price comparison and `resource-graph-query` for AI resource and tag discovery. Ask `azure-capacity-manager` to run `vm-quota-usage` with `subscription_id` and `location` for relevant GPU-capable VM families. + + Token quantities and per-token measures depend on meter units and available evidence. Convert units only when supported by the returned data; otherwise report the limitation and do not infer token counts. + + ## FOCUS Alignment + + Where FOCUS-aligned data is available, review `ConsumedQuantity`, `ConsumedUnit`, `x_SkuMeterCategory`, `x_SkuMeterSubcategory`, and `EffectiveCost` to derive unit economics. Map returned meter fields and units explicitly, and report any unavailable fields or conversions. + + ## Phase 1: Token Economics + + Use the current 30-day cost results and available meter quantities as the evidence set. Identify the source, unit, and any conversion for each metric. + + ### 1.1 Token Usage Breakdown + Use the current 30-day cost query grouped by model meter and direction. Where meter units support it, record per-model, per-direction (input/output): + - Total tokens consumed + - Effective cost + - Unit cost per token + - Cost per 1K tokens + + ### 1.2 Model Cost Comparison + Use the cost query and `azure-resource-manager-mcp_get_retail_prices` for the same period. Compare cost per 1K tokens where units permit. Identify: + - Which models are most expensive per token + - Which models have the best discount (list vs effective) + - Opportunities to switch to cheaper models for equivalent tasks + + ### 1.3 Daily Trend + Use the daily-granularity cost query for the current 30-day window. Look for: + - Usage pattern (weekday vs weekend) + - Cost spikes (any day > 2x the 7-day rolling average) + - Growth trajectory (are AI costs accelerating?) + - If spikes detected, use deep investigation to hypothesize causes + + ### 1.4 Cost Allocation + Use `resource-graph-query` to discover Azure OpenAI, AI Foundry, Machine Learning, and GPU compute resources, and the grouped cost query for the same period. Break down by: + - Application tag — which apps consume the most tokens? + - Team/CostCenter — who should be charged? + - Environment — production vs dev/test ratio + - ResourceName — which Azure OpenAI deployments are driving cost? + + **Checkpoint:** Token economics collected. Proceed to month-over-month comparison. + + ## Phase 2: Month-over-Month AI Analysis + + ### 2.1 Prior Month Comparison + Compare the prior 30-day cost query with the current 30-day window: + - Token volume change (% growth) + - Cost change (% growth) + - Unit cost change (are we getting cheaper or more expensive per token?) + - Model mix shift (are we using more expensive models?) + + ### 2.2 Input/Output Ratio + Calculate the input-to-output token ratio for each model. Long prompts with short responses indicate potential prompt optimization. Flag any model where input tokens > 10x output tokens. + + ## Phase 3: Optimization Recommendations + + ### 3.1 Model Substitution + For each high-cost model, check if a cheaper model could handle the same workload. Reference the model cost comparison data. Calculate potential savings from model downgrades. + + ### 3.2 Prompt Engineering Impact + If input tokens are disproportionately high, recommend prompt optimization. Shorter context windows = fewer tokens = lower cost. + + ### 3.3 Environment Rightsizing + If dev/test environments are using production-tier models, recommend cheaper models for non-production. + + ### 3.4 Commitment Coverage + Check if Azure OpenAI Provisioned Throughput Units (PTUs) would be cheaper than pay-as-you-go for high-volume workloads. + Ask `azure-capacity-manager` to run `vm-quota-usage` with `subscription_id` and `location` for relevant GPU-capable regions and VM families. Identify constraints for VM-hosted inference or training. + + ## Phase 4: Report + + Format as a professional AI cost report: + - Executive summary with total AI spend, token volume, and cost per 1K tokens + - Model efficiency table (cost per 1K tokens by model, input vs output) + - Application allocation table (cost by app/team/environment) + - Month-over-month trend chart description + - Top 3 optimization recommendations with dollar savings estimates + - Meter units, data limitations, and assumptions + + Use 🤖 for AI metrics, 💰 for savings, 📊 for trends, 🎯 for recommendations. + + Do not stop until the report is complete. + + ## Phase 5: Deliver + + + **Visualizations:** Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. + + **Microsoft Teams (financial results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — model availability changes, query patterns for AI billing data, FOCUS column mappings discovered, tool errors encountered. Never save financial figures, cost amounts, token costs, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/alert-coverage-audit.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/alert-coverage-audit.yaml new file mode 100644 index 000000000..ab5ba3671 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/alert-coverage-audit.yaml @@ -0,0 +1,58 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: AlertCoverageAudit +spec: + name: AlertCoverageAudit + description: Monthly audit of cost anomaly alert coverage across active subscriptions + schedule: 0 8 16 * * + mode: Review + agent: finops-practitioner + prompt: |- + # Alert Coverage Audit + + ## Phase 0: Preparation + + Before starting, read all documents in the knowledge base. Pay special attention to anomaly detection standards, monitoring ownership notes, and prior alert coverage gaps. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + Apply the FinOps practitioner guidance in this task. Run the evidence tools directly on `finops-practitioner`. + + ## Specialist tools to request + + Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `finops-practitioner`: Run `resource-graph-query` to inventory active subscriptions, configured cost anomaly alerts (`microsoft.costmanagement/scheduledactions`), action groups, and alert rules. Run `azure-resource-manager-mcp_list_alerts` to review fired cost alerts over the monitoring window. + + ## 1. Establish subscription inventory + + Run `resource-graph-query` to identify active subscriptions and ownership metadata. Exclude subscriptions explicitly documented as out of scope. + + ## 2. Find anomaly alert coverage + + Run `resource-graph-query` to detect cost anomaly alerts, scheduled query rules, metric alerts, action groups, and any documented alerting resources that implement cost anomaly detection. + + ## 3. Identify missing anomaly detection + + Compare active subscriptions with detected anomaly alert coverage. Flag subscriptions missing anomaly detection, subscriptions with disabled or incomplete alerts, and subscriptions missing notification routing. + Run `azure-resource-manager-mcp_list_alerts` for the same monitoring window to review fired cost alerts. Distinguish missing coverage from low observed alert volume, and state when alert evidence is unavailable. + + ## 4. Recommend remediation + + Recommend alert deployment, action group updates, ownership remediation, or follow-up validation. Prioritize production and business-critical subscriptions. + + ## Report format + + Produce a markdown report with an executive summary, an alert coverage table, a missing anomaly detection table, disabled or incomplete alert findings, and recommended remediation actions. + + ## Persistence + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, alert threshold amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + ## Visualizations + + Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum an anomaly alert coverage chart and a missing alert count by owner or environment chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/benefit-recommendation-review.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/benefit-recommendation-review.yaml new file mode 100644 index 000000000..230472d86 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/benefit-recommendation-review.yaml @@ -0,0 +1,59 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: BenefitRecommendationReview +spec: + name: BenefitRecommendationReview + description: Weekly executive review of reservation and savings plan recommendations + schedule: 0 8 * * 5 + mode: Review + agent: finops-practitioner + prompt: |- + # Benefit Recommendation Review + + ## Phase 0: Preparation + + Before starting, read all documents in the knowledge base. Pay special attention to commitment discount strategy, reservation governance, savings plan policy, and prior recommendation review notes. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + Lead this as the FinOps practitioner. Run `benefit-recommendations`, `azure-resource-manager-mcp_get_benefit_recommendations`, `azure-resource-manager-mcp_list_benefit_utilization`, `azure-resource-manager-mcp_list_reservation_transactions`, and `azure-resource-manager-mcp_query_costs` directly for recommendation, utilization, transaction, coverage, and savings evidence. Ask `azure-capacity-manager` to run `benefit-recommendations` and consult `chief-financial-officer` for approval framing, commitment risk, and executive decision language. + + ## Specialist tools to request + + Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `azure-capacity-manager`: Run `benefit-recommendations` with `billing_scope`, optional `lookback_period`, optional `term` to retrieve reservation and savings plan recommendations. + - `finops-practitioner`: Run the benefit, utilization, reservation transaction, and cost tools listed above for commitment evidence. + + ## 1. Establish billing scope + + Identify the billing scope or subscription scope from knowledge base guidance. If multiple scopes are documented, run the review for each and keep results separated by scope. + + ## 2. Retrieve latest recommendations + + Ask `azure-capacity-manager` to run `benefit-recommendations` with `billing_scope`, `lookback_period`, `term`. Review both reservation and savings plan recommendation types where available. + + ## 3. Summarize savings opportunity + + Summarize estimated savings, recommended cost, term, break-even information, and recommendation count. Separate recommendations by type and highlight assumptions or missing fields in the source payload. + Use the direct benefit recommendations, utilization, reservation transaction, and cost query results to reconcile recommendations against realized utilization, commitment waste, compute coverage, and savings where supported. State missing evidence and assumptions. + + ## 4. Consult finance and recommend executive actions + + Consult `chief-financial-officer` for the financial decision frame, then recommend which opportunities need purchase approval, deeper workload validation, commitment adjustment, or no action. Highlight governance risks such as over-commitment, insufficient utilization history, or missing owner accountability. + + ## Report format + + Produce a markdown report with an executive summary, a reservation recommendation table, a savings plan recommendation table, total potential savings summary, key risks, and recommended executive decisions. + + ## Persistence + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, savings amounts, cost amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + ## Visualizations + + Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum a savings by recommendation type chart and a cost versus savings chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/budget-coverage-audit.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/budget-coverage-audit.yaml new file mode 100644 index 000000000..2d610701b --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/budget-coverage-audit.yaml @@ -0,0 +1,57 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: BudgetCoverageAudit +spec: + name: BudgetCoverageAudit + description: Monthly audit of subscription budget coverage and missing budget controls + schedule: 0 8 15 * * + mode: Review + agent: finops-practitioner + prompt: |- + # Budget Coverage Audit + + ## Phase 0: Preparation + + Before starting, read all documents in the knowledge base. Pay special attention to budgeting standards, subscription ownership notes, and prior missing-budget findings. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + Apply the FinOps practitioner guidance in this task. Run the evidence tools directly on `finops-practitioner`. + + ## Specialist tools to request + + Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `finops-practitioner`: Run `resource-graph-query` with `query`, optional `subscription_ids` to find subscriptions, budget resources, tags, ownership metadata, and budget coverage gaps. + + ## 1. Establish subscription inventory + + Run `resource-graph-query` to identify active subscriptions and subscription metadata needed for ownership, environment, cost center, or portfolio grouping. Exclude subscriptions that are documented as out of scope. + + ## 2. Find configured budgets + + Run `resource-graph-query` against budget resource types and related policy or governance metadata. Run `azure-resource-manager-mcp_list_budgets` and `azure-resource-manager-mcp_get_budget` directly to inventory budgets and inspect their details. Identify subscription-level budgets and any documented management-group or shared budget coverage that should count as coverage. + + ## 3. Identify coverage gaps + + Compare active subscriptions with detected budget coverage. Report subscriptions missing budgets, subscriptions with ambiguous budget scope, and subscriptions where owner metadata is missing or incomplete. + + ## 4. Recommend remediation + + Recommend budget creation, owner follow-up, tag correction, or policy remediation for each uncovered subscription. Prioritize production and high-risk environments first. + + ## Report format + + Produce a markdown report with an executive summary, a subscription budget coverage table, a missing-budget table, ownership gaps, and recommended remediation actions. + + ## Persistence + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, budget amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + ## Visualizations + + Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum a budget coverage by portfolio chart and a missing-budget count by environment chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-daily-monitor.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-daily-monitor.yaml new file mode 100644 index 000000000..fa9c33359 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-daily-monitor.yaml @@ -0,0 +1,139 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: CapacityDailyMonitor +spec: + name: CapacityDailyMonitor + description: Daily FinOps capacity management health check — quota usage, CRG utilization, zone capacity + schedule: 30 6 * * * + mode: Review + agent: finops-practitioner + prompt: '# Daily FinOps Capacity Management Monitor + + + ## Phase 0: Preparation + + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate capacity evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to apply its capacity-management guidance during the `Task` invocation. + + + This daily check maps Azure capacity health into FinOps Automation, Tools, & Services, Usage Optimization, and Governance, Policy & Risk signals so issues are visible before they block deployments. + + ## Specialist tools to request from `azure-capacity-manager` + + - Ask `azure-capacity-manager` to run `vm-quota-usage` and `non-compute-quotas` with `subscription_id` and `location`, and `capacity-reservation-groups` with `subscription_id` and optional `resource_group`. `finops-practitioner` runs `resource-graph-query`, `azure-resource-manager-mcp_list_alerts`, and `azure-resource-manager-mcp_list_budgets` directly. + + + ## 1. Quota Health + + + Ask `azure-capacity-manager` to run `vm-quota-usage` with `subscription_id` and `location` for each scoped subscription and region to check: + + - VM family quota usage vs limits + + - Flag any family at >80% utilization — these are deployment risks + + - Flag any family at >95% — these are imminent blockers + + - Check quota groups if configured — report group-level headroom + + - For documented critical workloads, compare remaining headroom with one complete deployable scale unit and flag critical when less than one unit remains, even if percentage thresholds have not been crossed + + Ask `azure-capacity-manager` to run `non-compute-quotas` with `subscription_id` and `location` for Storage and Network quota usage. + + + + + ## 2. Capacity Reservation Utilization + + + Ask `azure-capacity-manager` to run `capacity-reservation-groups` with `subscription_id` and optional `resource_group` to enumerate CRGs and their instanceView utilization: + + - Check reserved vs allocated VM count + + - Flag any CRG where utilization < 50% (paying for unused capacity) + + - Flag any CRG where overallocation exceeds 2x reservation + + - Check zone alignment for shared CRGs + + - Label each CRG as reserved-available, consumed, or overallocated; overallocated instances do not inherit the reserved-capacity SLA + + + + + ## 3. AKS Node Pool Capacity + + + Have `finops-practitioner` run `resource-graph-query` to inventory AKS node pools, VM sizes, autoscaler bounds, upgrade settings, subnet configuration, and CRG associations, then ask `azure-capacity-manager` to assess: + + - Verify node pool CRG association is still active + + - Check if autoscaler is hitting quota limits + + - Report node pool scale events in last 24 hours + + - Calculate temporary upgrade capacity from base nodes and `maxSurge`, including VM-family quota, subnet IP, load-balancer, and CRG headroom + + - Treat unknown node-pool, network, or upgrade settings as insufficient evidence rather than healthy status + + ## 4. Failure Classification and Follow-up + + Classify any recent allocation failure as quota exhaustion, hard access or offer restriction, over-constrained placement, or transient inventory stockout before recommending action. Retry only transient stockouts. If a previously approved quota, access, CRG, or placement change is recorded, rerun the relevant check and report before/after evidence. + + + + + ## 5. Alert Status + + + Have `finops-practitioner` run `azure-resource-manager-mcp_list_alerts` and `azure-resource-manager-mcp_list_budgets` to check for any fired alerts in the last 24 hours: + + - Quota threshold alerts + + - Budget alerts + + - Cost anomaly alerts + + Record evidence collection timestamps and identify stale or unavailable alert and cost context. + + + + + ## Output + + + Use #remember to note key findings. Escalate: + + - 🔴 CRITICAL: Any quota at >95% or CRG billing waste >$1000/day + + - 🟡 WARNING: Any quota at >80% or CRG utilization <50% + + - 🟢 HEALTHY: All within thresholds + + Include each evidence source and collection timestamp. Mark evidence older than this daily cadence as STALE and lower confidence. + + + Do not stop until all checks are complete. + + + ## Deliver + + + + **Visualizations:** Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Microsoft Teams (status results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — Python tool failures, quota API workarounds, CRG query patterns, zone alignment discoveries. Never save financial figures, cost amounts, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-monthly-planning.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-monthly-planning.yaml new file mode 100644 index 000000000..497a110d9 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-monthly-planning.yaml @@ -0,0 +1,170 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: CapacityMonthlyPlanning +spec: + name: CapacityMonthlyPlanning + description: Monthly capacity planning cycle — demand forecast, capacity request pipeline, governance review + schedule: 0 9 1 * * + mode: Review + agent: finops-practitioner + prompt: '# Monthly Capacity Planning Cycle + + + ## Phase 0: Preparation + + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate capacity evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to apply its capacity-management guidance during the `Task` invocation. + + + Monthly planning maps Azure capacity signals into FinOps Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Budgeting support, and Governance, Policy & Risk. + + ## Specialist tools to request from `azure-capacity-manager` + + - Ask `azure-capacity-manager` to run `vm-quota-usage`, `capacity-reservation-groups`, and `benefit-recommendations` for the relevant scopes. + + + ## 1. Demand Forecast + + + Have `finops-practitioner` run `azure-resource-manager-mcp_forecast_costs` and `azure-resource-manager-mcp_query_costs` filtered to compute services, using 180 days of history and a 90-day forecast where supported. + + Filter to compute services. Project: + + - VM core demand by family for next 3 months + + - Growth rate by region + + - Seasonal patterns (if detectable) + + Translate demand into complete deployable scale units or deployment stamps by subscription, region, zone, SKU family, and dependent service. Include normal demand plus temporary onboarding, upgrade, replacement, seasonal, and failover demand. + + + Cross-reference with business targets: + + - New customer deployments planned? + + - Stamp scale-out events expected? + + + + + ## 2. Capacity Request Pipeline + + + Based on the demand forecast, ask `azure-capacity-manager` to run `vm-quota-usage` for each scoped subscription and location, and build a capacity request pipeline: + + - Quota increases needed (by subscription, region, family) + + - Region access requests needed + + - Zonal enablement requests needed + + - Quota group limit increases needed + + - Estimated lead times for each request type + + Compare the forecast horizon with the slowest applicable region-access, zonal-enablement, quota, and capacity-reservation lead time. Report days of runway, request or ticket status, ticket age, owner, escalation deadline, and INSUFFICIENT HORIZON when demand can bind before the prerequisite is cleared. + + + + + ## 3. Allocation Review + + + Ask `azure-capacity-manager` to run `capacity-reservation-groups` to enumerate the current CRG portfolio and review instanceView utilization: + + - Utilization trends over the past month (from daily checks) + + - Rightsizing recommendations (reduce or increase reservations) + + - New CRGs needed based on demand forecast + + - CRG sharing opportunities across subscriptions + + - Overallocation policy review + + - Primary and recovery scale-unit coverage, including temporary surge and failover quantities + + - Fallback SKU, zone, and region compatibility plus latest deployment-test status + + + + + ## 4. Cost Impact + + + Ask `azure-capacity-manager` to run `benefit-recommendations`. Have `finops-practitioner` run `azure-resource-manager-mcp_list_benefit_utilization`, `azure-resource-manager-mcp_get_benefit_recommendations`, and `azure-resource-manager-mcp_query_costs` for rate optimization and capacity cost evidence. + + Calculate: + + - Monthly cost of unused CRG capacity + + - Savings from Azure Reservations paired with CRGs + + - Potential savings from new commitment purchases aligned with capacity needs + + - ESR impact of capacity decisions + + + ## 5. Governance Review + + + Check adherence to the governance program. Have `finops-practitioner` run `resource-graph-query`, `azure-resource-manager-mcp_list_alerts`, and `azure-resource-manager-mcp_list_budgets` for governance evidence: + + - Are all subscriptions in quota groups? + + - Are anomaly alerts deployed across all subscriptions? + + - Are budget alerts configured? + + - Any post-incident reviews needed from scaling failures? + + - Were previously approved capacity changes revalidated with before/after evidence? + + Trigger an on-demand capacity resilience preflight between monthly runs for launches, onboarding waves, new stamps or regions, architecture or failover changes, acquisitions, seasonal peaks, large scale events, and allocation failures. + + + + + ## Output + + + Produce a monthly capacity planning report: + + - Demand forecast table (VM family × region × month) + + - Capacity request pipeline with status and lead times + + - CRG portfolio review with cost impact + + - Governance compliance scorecard + + - Recommended actions with owners and deadlines + + - Evidence timestamps, freshness, confidence, days of runway, and post-change validation status + + + Use deep investigation for any critical capacity gaps. Use #remember to note key capacity findings. + + + ## Deliver + + + + **Visualizations:** Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Microsoft Teams (capacity results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — forecasting model accuracy, capacity request lead time observations, governance gaps discovered, CLI workarounds. Never save financial figures, cost amounts, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-quarterly-strategy.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-quarterly-strategy.yaml new file mode 100644 index 000000000..e95815d2b --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-quarterly-strategy.yaml @@ -0,0 +1,164 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: CapacityQuarterlyStrategy +spec: + name: CapacityQuarterlyStrategy + description: Quarterly capacity strategy review — FinOps maturity, commitment alignment, architecture evolution + schedule: 0 9 1 1,4,7,10 * + mode: Review + agent: finops-practitioner + prompt: '# Quarterly Capacity Strategy Review + + + ## Phase 0: Preparation + + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate capacity evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to apply its capacity-management guidance during the `Task` invocation. + + + Quarterly strategic review bridging capacity management with FinOps Rate Optimization, Architecting & Workload Placement, and leadership strategy alignment. + + ## Specialist tools to request from `azure-capacity-manager` + + - Ask `azure-capacity-manager` to run `benefit-recommendations`, `vm-quota-usage`, `capacity-reservation-groups`, `non-compute-quotas`, and `db-service-quotas` for the relevant scopes. + + + ## 1. FinOps Capability Maturity Assessment + + + Assess maturity across the FinOps capabilities this capacity review supports: + + + | Capability | Crawl | Walk | Run | + + |------|-------|------|-----| + + | Planning & Estimating | Ad-hoc estimates | Telemetry-based estimates | Estimate checks integrated into workload intake | + + | Forecasting | Reactive demand projection | Forecasts from cost and utilization trends | Forecasts integrated into budget and engineering planning | + + | Architecting & Workload Placement | Ad-hoc placement decisions | Regular architecture and placement review | Placement decisions integrated into engineering intake and governance | + + | Automation, Tools, & Services | Manual quota checks | Alert-based quota and capacity governance | Automated guardrails and exception review | + + + Score current maturity and recommend next-level actions. + + + ## 2. Commitment Alignment + + + Ask `azure-capacity-manager` to run `benefit-recommendations`. Have `finops-practitioner` run `azure-resource-manager-mcp_list_benefit_utilization`, `azure-resource-manager-mcp_get_benefit_recommendations`, and `azure-resource-manager-mcp_query_costs` for quarterly commitment, savings, coverage, and cost evidence. + + + Align capacity reservations with commitment purchases: + + - Which CRGs have matching Azure Reservations? (ideal) + + - Which CRGs are paying full price? (needs reservation) + + - Which reservations don''t have CRG capacity? (supply risk) + + - RI/SP purchase recommendations for next quarter + + + ## 3. Architecture Review + + + Have `finops-practitioner` run `resource-graph-query` for resource inventory, and ask `azure-capacity-manager` to run `vm-quota-usage` and `capacity-reservation-groups` for the scoped subscriptions and regions: + + - Stamp-based isolation health — any stamps at capacity? + + - Multi-subscription quota distribution — balanced? + + - Regional expansion needs for next quarter + + - AKS cluster capacity governance + + - Primary and recovery scale-unit coverage, including temporary upgrade and failover demand + + - Regional diversity against workload-specific recovery, dependency, latency, residency, test, and cost requirements; do not score against a fixed region count + + Use the latest `CapacityMonthlyPlanning` demand forecast and request pipeline as input. Do not independently invent or restate demand when monthly evidence is missing or stale. + + + + + ## 4. Non-Compute Quotas + + Ask `azure-capacity-manager` to run `non-compute-quotas` and `db-service-quotas` for the scoped subscriptions and regions. + + + Review non-compute service quotas from collected quota evidence and resource inventory: + + - Storage account limits + + - Cosmos DB throughput limits + + - Service Bus/Event Hub namespace limits + + - Any service-specific scaling constraints + + + + + ## 5. Governance Program Health + + + Evaluate the capacity governance program: + + - Monthly review cadence maintained? + + - Post-incident reviews conducted for scaling failures? + + - Alert coverage complete? + + - Scripts and tooling up to date? + + - Accepted capacity risks documented with owners and review dates? + + - Post-change and disaster-recovery drill findings incorporated into placement and commitment decisions? + + + + + ## Output + + + Quarterly capacity strategy brief for leadership: + + - FinOps capability maturity scorecard with progression plan + + - Commitment alignment matrix (capacity + pricing) + + - Quarterly capacity and workload-placement roadmap + + - Key risks and mitigations + + - Budget impact of capacity decisions + + + Use deep investigation for any strategic decisions. Use #remember to note key strategy findings. + + + ## Deliver + + + + **Visualizations:** Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Microsoft Teams (strategy results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — maturity assessment methodology refinements, commitment alignment patterns, architecture observations, CLI workarounds. Never save financial figures, cost amounts, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-weekly-supply-review.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-weekly-supply-review.yaml new file mode 100644 index 000000000..ce090d8e9 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/capacity-weekly-supply-review.yaml @@ -0,0 +1,145 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: CapacityWeeklySupplyReview +spec: + name: CapacityWeeklySupplyReview + description: Weekly FinOps capacity management review — quota headroom, CRG cost optimization, SKU availability, benefit recommendations + schedule: 0 8 * * 1 + mode: Review + agent: finops-practitioner + prompt: '# Weekly FinOps Capacity Management Review + + + ## Phase 0: Preparation + + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate capacity evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to apply its capacity-management guidance during the `Task` invocation. + + + Weekly review mapping Azure capacity signals into FinOps Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Governance, Policy & Risk, and Automation, Tools, & Services. + + ## Specialist tools to request from `azure-capacity-manager` + + - Ask `azure-capacity-manager` to run `vm-quota-usage`, `non-compute-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, and `benefit-recommendations` for the relevant scopes. + + + ## 1. Quota Headroom Analysis + + + Ask `azure-capacity-manager` to run `vm-quota-usage` and `non-compute-quotas` across the scoped subscriptions and regions: + + - Aggregate quota usage by VM family across subscriptions + + - Identify families with <20% headroom across the estate + + - Check quota group balances — any group near its limit? + + - Identify quota transfer opportunities between subscriptions + + - Flag subscriptions not yet added to quota groups + + - Report primary headroom and recovery-region or recovery-zone headroom separately, measured in complete workload scale units where workload definitions are available + + + + + ## 2. CRG Cost-Waste Audit + + + Ask `azure-capacity-manager` to run `capacity-reservation-groups` to enumerate capacity reservation groups and their instanceView utilization: + + - Calculate weekly cost of unused reserved capacity (reserved minus allocated × pay-as-you-go rate) + + - Calculate net CRG carry cost only after applicable Azure Reservation or savings plan offsets are known + + - Identify CRGs that could be rightsized (reduce reservation quantity) + + - Label reserved-available, consumed, and overallocated states; flag overallocated instances as outside the reserved-capacity SLA + + - Check for CRGs without matching Azure Reservations (paying both capacity guarantee AND full price) + + + Key distinction: Capacity reservations guarantee supply. Azure Reservations reduce cost. Best practice is to pair both. + + + + + ## 3. SKU and Zone Availability Validation + + + Ask `azure-capacity-manager` to run `sku-availability` for Compute SKUs and zones to validate availability for shared CRGs and upcoming deployments: + + - Verify that planned SKUs are available in the required locations and zones + + - Flag any SKU restrictions that could block planned scaling + + - Require a structured fallback SKU, zone, or region with compatibility and deployment-test status for critical workloads + + - Ask `azure-capacity-manager` to run `zone-mapping` for every cross-subscription zonal or shared-CRG scenario. If mappings are unavailable or stale, report INSUFFICIENT EVIDENCE and do not approve placement. + + + + ## 4. Benefit Recommendations + + + Ask `azure-capacity-manager` to run `benefit-recommendations`. Have `finops-practitioner` run `azure-resource-manager-mcp_list_benefit_utilization`, `azure-resource-manager-mcp_get_benefit_recommendations`, and `azure-resource-manager-mcp_query_costs` for benefit, commitment, coverage, and cost evidence. + + Cross-reference with capacity reservation strategy: + + - Do recommended reservations overlap with existing CRGs? (Good — pair pricing with capacity) + + - Are there high-savings recommendations in regions without CRGs? (May need capacity too) + + + ## 5. Region and SKU Availability + + + Ask `azure-capacity-manager` to run `sku-availability` for Compute SKUs and zones to check for any new region access or zonal enablement needs: + + - Upcoming deployments that need new regions? + + - SKU restrictions blocking planned scaling? + + + + + ## Output + + + Produce a weekly FinOps capacity management briefing with: + + - Quota headroom scorecard (🔴🟡🟢 by VM family) + + - CRG utilization and waste summary with dollar amounts + + - Benefit recommendation highlights + + - Action items for platform, engineering, and finance stakeholders + + - Evidence timestamps, freshness, owner, escalation deadline, and any required post-change validation + + + Use #remember to note key FinOps capacity findings. Use deep investigation if any quota is critically low. + + + ## Deliver + + + + **Visualizations:** Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Microsoft Teams (capacity results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — quota API failures, SKU availability discoveries, CRG query patterns, Python tool workarounds. Never save financial figures, cost amounts, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/compute-utilization-trend.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/compute-utilization-trend.yaml new file mode 100644 index 000000000..260f43411 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/compute-utilization-trend.yaml @@ -0,0 +1,77 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: ComputeUtilizationTrend +spec: + name: ComputeUtilizationTrend + description: Weekly VM quota utilization trend review across subscriptions and regions + schedule: 0 7 * * 1 + mode: Review + agent: finops-practitioner + prompt: '# Compute Utilization Trend + + + ## Phase 0: Preparation + + + Before starting, read all documents in the knowledge base. Pay special attention to prior capacity run notes, known quota API workarounds, and active subscription or region scope notes. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate capacity evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its capacity-management guidance and use the agent system-managed identity for Python tool authentication. + + + ## Specialist tools to request from `azure-capacity-manager` + + - `finops-practitioner` runs `resource-graph-query` directly to identify subscriptions, resources, regions, and ownership. Ask `azure-capacity-manager` to run `vm-quota-usage` with `subscription_id` and `location`. + + + ## 1. Establish scope + + + Have `finops-practitioner` run `resource-graph-query` directly to identify active subscriptions with compute resources and regions where virtual machines, virtual machine scale sets, or AKS node pools are deployed. Exclude disabled, test-only, or explicitly out-of-scope subscriptions if documented in the knowledge base. + + + ## 2. Collect quota utilization + + + Ask `azure-capacity-manager` to run `vm-quota-usage` with `subscription_id` and `location` for each scoped subscription and region. Capture VM family current usage, limit, and utilization percentage, then compare against the thresholds below. + + + ## 3. Compare to prior week + + + Compare current results against prior #remember findings or prior run notes. Identify VM families where utilization increased materially, where headroom decreased, or where the trend suggests quota risk before the next weekly run. + + + ## 4. Flag growing utilization + + + Flag any family above 80% utilization, any family above 95% utilization, and any family with week-over-week growth that could exceed 80% within the next two reporting periods. Include subscription, region, VM family, current utilization, previous utilization when known, and recommended action. + + + ## Report format + + + Produce a markdown report with an executive summary, a risk scorecard table by subscription and region, a VM family trend table, and a recommended action list. Use 🔴 for critical quota pressure, 🟡 for growing or warning utilization, and 🟢 for healthy headroom. + + + ## Persistence + + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + + ## Visualizations + + + Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Include at minimum a utilization trend chart and a current headroom comparison chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/cost-optimization.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/cost-optimization.yaml new file mode 100644 index 000000000..799054751 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/cost-optimization.yaml @@ -0,0 +1,190 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: CostOptimization +spec: + name: CostOptimization + description: Comprehensive cost optimization report with orphaned resources, rightsizing, and commitment analysis + schedule: 0 8 * * 1 + mode: Review + agent: finops-practitioner + prompt: |- + # Cost optimization report + + ## Phase 0: Preparation + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + Generate a comprehensive cost optimization report for the current Azure environment. + ## Specialist tools to request for this task + The following Python tools provide real Azure data via ARM REST API. Always use these instead of `az` CLI commands when available. Specialist tools use their configured authentication and the agent system-managed identity where required. + - `finops-practitioner`: Run `resource-graph-query` for orphaned resources and Advisor recommendations; run `benefit-recommendations`, `azure-resource-manager-mcp_get_benefit_recommendations`, and `azure-resource-manager-mcp_list_benefit_utilization` for commitment evidence; and run `azure-resource-manager-mcp_query_costs` for three-month monthly trends, top services, and regional cost trends. + - `azure-capacity-manager`: Run `benefit-recommendations` for capacity-related commitment evidence where needed. + + ## Phase 1: Discovery + + 1. Determine scope from the configured scheduled-task scope, documented knowledge-base scope, or current authenticated context. Do not wait for user input during this scheduled run. If scope cannot be determined, report the missing scope as a blocking configuration issue. + 2. Verify authentication: confirm Python tools are using DefaultAzureCredential with the agent system-managed identity. + 3. Check permissions: Reader role is sufficient for all detection queries. Note if elevated permissions are available. + + ## Phase 2: Data collection + + Run these data collection steps in parallel where possible. Use #remember to note key intermediate findings as you go. + + ### 2a: Orphaned resources + + Detect unused resources generating waste with zero workload value. + + Run `resource-graph-query` to scan for: + - Unattached managed disks + - Unused network interfaces + - Orphaned public IP addresses + - Idle NAT gateways + - Orphaned snapshots (source disk deleted, age > 30 days) + - Idle load balancers (empty backend pools) + - Empty availability sets + - Orphaned NSGs + + For each category, capture: count, estimated monthly cost, resource list. + + ### 2b: Advisor cost recommendations + + Query Azure Advisor for all cost recommendations. Run `resource-graph-query` for Advisor recommendation inventory. + + Categorize recommendations by type: right-size VMs, shutdown idle VMs, reserved instances, delete unused disks, and other. + + Calculate total potential monthly savings from Advisor. + + ### 2c: Commitment discount status + + Analyze current commitment discount coverage and opportunities. Run `benefit-recommendations`, `azure-resource-manager-mcp_get_benefit_recommendations`, and `azure-resource-manager-mcp_list_benefit_utilization` directly for recommendations and utilization. + + Collect: + - Current savings plan coverage and utilization + - Current reservation coverage and utilization + - New purchase recommendations from the Benefit Recommendations API + - Gap analysis: what percentage of eligible compute spend is covered + + Evaluate new purchases by expected savings, utilization risk, commitment term, workload stability, flexibility, and business plans. + + Run `azure-resource-manager-mcp_query_costs` directly for three-month monthly cost trends, top services, and regional cost trends. Use the results to prioritize optimization and report data gaps. + + ## Phase 3: Analysis + + ### 3a: Validate top rightsizing recommendations + + For the top 5 Advisor right-size VM recommendations (by savings amount), validate with the Retail Prices API. + + Run `azure-resource-manager-mcp_get_retail_prices` to look up current and target SKU prices. Compare Advisor's estimated savings against actual retail price deltas. + + ### 3b: VM utilization deep dive (if VM Insights available) + + For the top rightsizing candidates, check actual utilization metrics if VM Insights is enabled. + + Evaluate: + - 14-day CPU P95 analysis + - Memory utilization (if VM Insights agent deployed) + - Burst pattern detection (P99 check) + + Skip this step if VM Insights is not available — note it as a recommendation for future optimization maturity. + + ### 3c: Categorize by effort and risk + Before categorizing, ask `azure-capacity-manager` to run `non-compute-quotas` for relevant subscriptions and regions to identify Storage and Network quota pressure that may affect optimization feasibility. + + Organize all findings into four categories: + + | Category | Effort | Risk | Examples | + |----------|--------|------|----------| + | **Quick wins** | Low | Zero | Delete orphaned resources, remove unused IPs | + | **Rightsizing** | Medium | Low | Resize underutilized VMs (requires restart) | + | **Commitment optimization** | Medium | Medium | Purchase savings plans or reservations | + | **Architecture changes** | High | Variable | Redesign for cost efficiency, migrate to PaaS | + + ## Phase 4: Report + + Generate a markdown report with the following structure: + + ### Report template + + ```markdown + # Cost Optimization Report — {subscription/environment name} + **Generated:** {date} + **Scope:** {subscription(s) or management group} + + ## Executive summary + + - **Total identified monthly savings:** ${amount} + - **Quick wins (zero risk):** ${amount} across {count} resources + - **Rightsizing opportunities:** ${amount} across {count} VMs + - **Commitment discount opportunities:** ${amount} estimated + - **Current commitment coverage:** {percentage}% + + ## Quick wins — orphaned resources + + | Resource Type | Count | Est. Monthly Cost | Action | + |--------------|-------|-------------------|--------| + | Unattached disks | {n} | ${cost} | Delete | + | Orphaned public IPs | {n} | ${cost} | Delete | + | ... | ... | ... | ... | + | **Total** | **{n}** | **${cost}** | | + + ## Rightsizing recommendations + + ### Top VM recommendations (validated) + + | VM | Current SKU | Target SKU | CPU P95 | Savings/mo | Risk | + |----|-------------|------------|---------|------------|------| + | {name} | {current} | {target} | {%} | ${savings} | Low | + | ... | ... | ... | ... | ... | ... | + + {Include notes on burst patterns, memory utilization where available} + + ## Commitment discount opportunities + + ### Current coverage + - Savings plan utilization: {%} + - Reservation utilization: {%} + - Total eligible compute covered: {%} + + ### Recommendations + {Summarize Benefit Recommendations API findings} + {Reference azure-commitment-discount-decision.md framework for purchase guidance} + + ## Cost trends + + {Summarize three-month, service, and regional cost trends} + + ## Next steps + + 1. **Immediate (this week):** Delete orphaned resources — ${amount}/mo savings + 2. **Short-term (this month):** Resize top {n} VMs — ${amount}/mo savings + 3. **Medium-term (this quarter):** Evaluate commitment discount purchases + 4. **Ongoing:** Deploy VM Insights for memory-aware rightsizing + + ## Audit trail + + | Data Source | Query Time | Records | + |-------------|-----------|---------| + | Resource Graph (orphaned) | {timestamp} | {count} | + | Advisor recommendations | {timestamp} | {count} | + | Benefit Recommendations API | {timestamp} | {count} | + ``` + + ### Report guidance + + - Format all currency values with the appropriate billing currency + - Include resource IDs or names for actionable items + - Flag any data gaps (e.g., "Memory metrics unavailable — VM Insights not deployed") + - Do not save task reports or financial data to the git repository — use #remember for key operational findings instead + + ## Phase 5: Deliver + + + **Visualizations:** Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. + + **Microsoft Teams (financial results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — query failures, Resource Graph workarounds, Advisor API patterns, tool errors encountered. Never save financial figures, cost amounts, savings numbers, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/db-quota-audit.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/db-quota-audit.yaml new file mode 100644 index 000000000..0c2f1bd1c --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/db-quota-audit.yaml @@ -0,0 +1,59 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: DbQuotaAudit +spec: + name: DbQuotaAudit + description: Weekly audit of database (SQL DB, SQL MI, Cosmos DB, PostgreSQL Flex, MySQL Flex) quota and region/AZ access risks + schedule: 0 7 * * 3 + mode: Review + agent: finops-practitioner + prompt: |- + # Database Service Quota Audit + + ## Phase 0: Preparation + + Before starting, read all documents in the knowledge base. Pay special attention to prior database quota findings, service limit assumptions, regional access restrictions, and availability-zone access caveats. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate database quota evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its capacity-management guidance and use the agent system-managed identity for Python tool authentication. + + ## Specialist tools to request from `azure-capacity-manager` + + - Ask `azure-capacity-manager` to run `db-service-quotas` with `subscription_id` and optional `location` to retrieve SQL DB, SQL MI, Cosmos DB, PostgreSQL Flex, and MySQL Flex quotas plus region/AZ access status. + + + ## 1. Establish subscription scope + + Use documented active subscription scope from the knowledge base and prior reports. If scope is missing, clearly state the subscriptions that were available to the tool at runtime and any access gaps. + + ## 2. Collect database quota and access usage + + For each active subscription, ask `azure-capacity-manager` to run `db-service-quotas` with `subscription_id` and optional `location` for SQL DB, SQL MI, Cosmos DB, PostgreSQL Flex, and MySQL Flex quotas plus region/AZ access status. Use the tool's normalized `quotas` and `region_access` arrays for tables and charts. Capture service name, scope, metric, current value, limit, available capacity, utilization percentage, unit, location, at-risk flags, source endpoint, access status, and any suppressed or unsuppressed errors. + + ## 3. Separate API-reported and default soft limits + + Separate API-reported quota limits from default soft limits. Treat Cosmos DB's default soft subscription account limit of 50 separately from provider-reported limits and label it clearly as a default soft limit. + + ## 4. Flag quota and access risk + + Flag database quotas at or above 80% utilization, highlight critical items above 95%, surface region access blocks where `access_allowed_for_region == false`, and surface AZ blocks separately from regular region access blocks. Group findings by service in the report so SQL DB, SQL MI, Cosmos DB, PostgreSQL Flex, and MySQL Flex risks are easy to triage. + + ## Report format + + Produce a markdown report with an executive summary, a database quota risk table grouped by service, an API-reported versus default soft limits table, a region access blocks table, an AZ access blocks table, errors and access gaps, and recommended mitigation actions. + + ## Persistence + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + ## Visualizations + + Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Build chart data from normalized quota rows and use `.get()`/safe-access patterns instead of indexing fields like `location` directly, because subscription-scoped database quota rows may not be regional. Include at minimum a quota utilization by service chart and an at-risk quota count by subscription chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/monthly-report.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/monthly-report.yaml new file mode 100644 index 000000000..c62e1bc4c --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/monthly-report.yaml @@ -0,0 +1,89 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: Monthly +spec: + name: Monthly + description: Month-over-month cost analysis with Azure Cost Management and ARM MCP tools + schedule: 15 17 5 * * + mode: Review + agent: finops-practitioner + prompt: |- + # Monthly Cost Analysis + + ## Phase 0: Preparation + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + + + Perform a comprehensive analysis for the most recently completed calendar month and a forecast for the next calendar month. If the knowledge base documents a fiscal calendar, include the fiscal mapping and state the reporting boundary explicitly. Each run builds on previous runs — prior research, notes, and results carry forward. + ## Specialist evidence requests for this task + + `finops-practitioner` runs the Azure Cost Management and ARM tools below directly. Ask `azure-capacity-manager` to run `vm-quota-usage` with `subscription_id` and `location` for relevant VM families and regions. + + ## Phase 1: Data Collection + + Use #remember to record operational observations, not financial figures. Check the returned data periods and completeness; state any freshness or coverage limitations. + + ### 1.1 Cost trends and allocation + + Run `azure-resource-manager-mcp_query_costs` for the 12-month monthly trend, top 20 services, top resource groups, regional distribution, top 20 resource types, daily 90-day spike and volatility analysis, and charge-type and publisher breakdown. Compare the completed month with prior periods. Investigate spikes and report missing or incomplete data. + + Run `azure-resource-manager-mcp_list_dimensions` and `resource-graph-query` to assess available allocation dimensions and tag coverage, including team, product, application, and environment. + + ### 1.2 Alerts and budgets + + Run `azure-resource-manager-mcp_list_alerts` and `azure-resource-manager-mcp_list_budgets` to review anomaly and budget alerts and their coverage. + + ### 1.3 Commitment performance and recommendations + + Run `azure-resource-manager-mcp_list_benefit_utilization`, `azure-resource-manager-mcp_get_benefit_recommendations`, `benefit-recommendations`, and `azure-resource-manager-mcp_list_reservation_transactions` to assess commitment utilization, savings, recommendations, and reservation transactions. Calculate savings or coverage only where the evidence supports it; state assumptions and missing fields. + + ### 1.4 Forecast and pricing + + Run `azure-resource-manager-mcp_forecast_costs` for a 90-day forecast and assess confidence. Run `azure-resource-manager-mcp_get_retail_prices`, `azure-resource-manager-mcp_start_pricesheet_download`, and `azure-resource-manager-mcp_get_pricesheet_status` for price benchmarking. Compare retail, contracted, and effective prices only when comparable data is available. + + ### 1.5 FinOps scorecard + + Summarize supported measurements by FinOps capability, including allocation, anomaly management, rate optimization, unit economics, and usage optimization. Distinguish measured from derived results and cite sources; label unsupported metrics and incomplete purchase data as limitations rather than assuming zero. + + **Checkpoint:** Confirm all available data is collected and gaps are documented before strategic analysis. + + ## Phase 2: Strategic Analysis + + ### 2.1 ROI and Business Impact + Calculate ROI = Annual Savings / Implementation Cost for each optimization opportunity. Determine payback period. Compare against industry benchmarks (cost variance 3-5%, RI coverage 60-70%). Create opportunity matrix. + + ### 2.2 Quick Wins + Filter opportunities meeting ALL criteria: implementation < 30 days AND risk = low AND savings > $1000/month. Prioritize: commitment purchases, right-sizing, auto-shutdown. + + ### 2.3 Detailed Findings + Document 7 sections: (1) Cost Analysis, (2) Savings Analysis, (3) Optimization Opportunities, (4) Governance & Risk, (5) Strategic Market Positioning, (6) Temporal Themes, (7) Implementation Roadmap. No raw JSON references — executive-level language only. + + ### 2.4 Recommendations with Performance Grading + Assign a letter grade (A+ to F) with percentile ranking. Build prioritized actions: Immediate (<30 days), Short-term (30-90 days), Long-term (90+ days). Include dollar savings amount, confidence %, and clear owner for each. + + ### 2.5 Implementation Roadmap + Phase 1 (30 days): quick wins. Phase 2 (60 days): commitments. Phase 3 (90 days): architecture. Include dependencies, risks, success metrics for each phase. + + **Checkpoint:** Validate all deliverables before formatting. + + ## Phase 3: Report Formatting + + Format as a professional, scannable report. Use tables, emoji indicators (📊 💰 🚨 🎯 🏆), bold dollar amounts. Include multi-scenario forecasts (Conservative/Expected/Optimistic), YoY/MoM comparisons, and percentile positioning. + + Do not stop or yield until the report is complete and ready for the FinOps team. + + ## Phase 4: Deliver + + + **Visualizations:** Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. + + **Microsoft Teams (financial results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — tool errors encountered, workarounds discovered, data quality observations, query patterns that worked or failed. Never save financial figures, cost amounts, savings numbers, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/non-compute-quota-audit.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/non-compute-quota-audit.yaml new file mode 100644 index 000000000..d18eae668 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/non-compute-quota-audit.yaml @@ -0,0 +1,77 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: NonComputeQuotaAudit +spec: + name: NonComputeQuotaAudit + description: Weekly audit of storage, network, and non-compute quota usage at risk + schedule: 0 7 * * 2 + mode: Review + agent: finops-practitioner + prompt: '# Non-Compute Quota Audit + + + ## Phase 0: Preparation + + + Before starting, read all documents in the knowledge base. Pay special attention to prior non-compute quota findings, service limit assumptions, and known Resource Graph fallback caveats. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate non-compute quota evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its capacity-management guidance and use the agent system-managed identity for Python tool authentication. + + + ## Specialist tools to request from `azure-capacity-manager` + + - Ask `azure-capacity-manager` to run `non-compute-quotas` with `subscription_id` and optional `location`. + + + ## 1. Establish subscription scope + + + Use documented active subscription scope from the knowledge base and prior reports. If scope is missing, clearly state the subscriptions that were available to the tool at runtime and any access gaps. + + + ## 2. Collect quota usage + + + Ask `azure-capacity-manager` to run `non-compute-quotas` with `subscription_id` and optional `location` for each scoped subscription. Use the tool''s normalized `quotas` array for tables and charts. If only legacy `services` records are available, normalize them with safe accessors: accept `service_name` or `service`, `quota_name` or `name`, `current_count` or `currentValue` or `current`, and default missing `location` to `subscription`. Capture limits, utilization, sources, and errors separately from estimates. + + + ## 3. Separate reported and estimated limits + + + Separate API-reported quota limits from estimated default service limits. Treat estimated limits as directional and highlight where a service owner should verify the exact service quota before taking action. + + + ## 4. Flag quotas at risk + + + Flag quotas at or above 80% utilization, quotas with unknown current count due to Resource Graph or provider failures, and services where estimated limits could hide a real quota risk. + + + ## Report format + + + Produce a markdown report with an executive summary, a non-compute quota risk table, an API-reported versus estimated limits table, errors and access gaps, and recommended mitigation actions. + + + ## Persistence + + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + + ## Visualizations + + + Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Build chart data from normalized quota rows and use `.get()`/safe-access patterns instead of indexing fields like `location` directly, because subscription-scoped non-compute quota rows may not be regional. Include at minimum a quota utilization by service chart and an at-risk quota count by subscription chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/sku-availability-audit.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/sku-availability-audit.yaml new file mode 100644 index 000000000..9bd21b661 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/sku-availability-audit.yaml @@ -0,0 +1,85 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: SkuAvailabilityAudit +spec: + name: SkuAvailabilityAudit + description: Weekly audit of regional SKU availability and restrictions that could block deployments + schedule: 30 7 * * 3 + mode: Review + agent: finops-practitioner + prompt: '# SKU Availability Audit + + + ## Phase 0: Preparation + + + Before starting, read all documents in the knowledge base. Pay special attention to deployment region notes, restricted SKU findings, and planned capacity requests. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate SKU availability evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its capacity-management guidance and use the agent system-managed identity for Python tool authentication. + + + ## Specialist tools to request from `azure-capacity-manager` + + - Ask `azure-capacity-manager` to run `sku-availability` with `subscription_id`, `location`, and optional `sku_filter` for Azure Compute SKU availability, zones, and restrictions. + + + ## 1. Establish region and SKU scope + + + Identify deployment regions and priority VM SKU families from knowledge base notes and prior reports. If no priority SKU list exists, audit commonly deployed VM families and explicitly state the assumed scope. + + + ## 2. Collect SKU availability + + + Ask `azure-capacity-manager` to run `sku-availability` for each scoped subscription and region using `subscription_id`, `location`, and optional `sku_filter`. + + + - Capture total Compute SKUs, restricted SKUs, restriction reason codes, zone availability, and restriction values. + + + + ## 3. Analyze deployment blockers + + + Identify restricted Compute SKUs that could block future deployments, scaling, zone alignment, or capacity reservation planning. Group findings by reason code when available, region, zone, subscription, and family or SKU name. + + Classify each observed failure as quota exhaustion, hard access or offer restriction, over-constrained placement, or transient inventory stockout. `SkuNotAvailable` and `NotAvailableForSubscription` are hard restrictions unless live evidence proves otherwise; do not recommend retries for them. Treat `AllocationFailed` and `ZonalAllocationFailed` as transient stockout candidates only after quota and request constraints are checked. + + Record whether each planned VM SKU and deployment shape supports CRGs. Include unsupported GPU/HPC families, Spot VMs, availability sets, dedicated hosts, proximity placement groups, and other documented reservation constraints. + + + ## 4. Recommend actions + + + Recommend whether to request quota, change SKU family, change region, adjust deployment zone, or escalate to Azure capacity planning. For every critical workload fallback, record architecture compatibility, cost impact, quota/access readiness, and latest non-production deployment-test status. Include uncertainty where planned demand is not documented. + + + ## Report format + + + Produce a markdown report with an executive summary, a restricted SKU table, a reason-code summary table, deployment blocker notes, and recommended mitigation actions. + + + ## Persistence + + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + + ## Visualizations + + + Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Include at minimum a restricted SKU count by region chart and a restriction reason-code chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/storage-paas-growth-forecast.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/storage-paas-growth-forecast.yaml new file mode 100644 index 000000000..2928c9209 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/storage-paas-growth-forecast.yaml @@ -0,0 +1,79 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: StoragePaasGrowthForecast +spec: + name: StoragePaasGrowthForecast + description: Monthly storage and PaaS quota growth forecast across active subscriptions + schedule: 0 8 1 * * + mode: Review + agent: finops-practitioner + prompt: '# Storage and PaaS Growth Forecast + + + ## Phase 0: Preparation + + + Before starting, read all documents in the knowledge base. Pay special attention to prior non-compute quota notes, known service limit assumptions, and subscription scope guidance. + + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate storage and PaaS quota evidence collection and specialist analysis to `azure-capacity-manager`; do not try to run capacity tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply the reporting requirements in this task, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its capacity-management guidance and use the agent system-managed identity for Python tool authentication. + + + ## Specialist tools to request from `azure-capacity-manager` + + - `finops-practitioner` runs `resource-graph-query` directly for storage and PaaS inventory, and `azure-resource-manager-mcp_query_costs` filtered to Storage services and meters. Ask `azure-capacity-manager` to run `non-compute-quotas` with `subscription_id` and optional `location`. + + + ## 1. Establish scope + + + Have `finops-practitioner` run `resource-graph-query` directly to list active subscriptions and count current storage and PaaS resources by type, region, and subscription. Identify resource types with meaningful month-over-month growth or high operational importance. + + + ## 2. Collect current non-compute quotas + + + Ask `azure-capacity-manager` to run `non-compute-quotas` with `subscription_id` and optional `location` for each scoped subscription. Use the tool''s normalized `quotas` array for tables and charts. If only legacy `services` records are available, normalize them with safe accessors: accept `service_name` or `service`, `quota_name` or `name`, `current_count` or `currentValue` or `current`, and default missing `location` to `subscription`. Capture limits, utilization, sources, and errors separately from estimates. + + + ## 3. Forecast growth + + + Compare current resource counts and quota utilization against prior #remember findings or prior monthly reports. Forecast expected utilization for the next 30, 60, and 90 days using observed growth where prior data exists. + + Have `finops-practitioner` run `azure-resource-manager-mcp_query_costs` filtered to Storage services and meters for the same reporting window. Use available storage cost trends to explain whether growth is creating avoidable cost risk; calculate unit costs only when reliable stored-volume evidence is available. + + + ## 4. Flag subscriptions approaching limits + + + Flag subscriptions where storage, network, or PaaS quota utilization is at or above 80%, where estimated limits are uncertain, or where growth trends indicate a likely limit breach before the next monthly cycle. + + + ## Report format + + + Produce a markdown report with an executive summary, a quota risk table by subscription and service, a growth forecast table, explicit API-reported versus estimated limit notes, and recommended owner actions. + + + ## Persistence + + + **Microsoft Teams (results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + + **Knowledge (#remember):** Save operational findings using #remember for future reference. Do not commit task reports, financial figures, cost amounts, or customer financial data to git or the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. + + + ## Visualizations + + + Before delivery, `azure-capacity-manager` or `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`. Only `azure-capacity-manager` may use `ExecutePythonCode` as a fallback for capacity evidence. If no visualization tool is available, provide the data as a chart-ready table. Build chart data from normalized quota rows and use `.get()`/safe-access patterns instead of indexing fields like `location` directly, because subscription-scoped non-compute quota rows may not be regional. Include at minimum a 90-day forecast chart and a quota utilization comparison chart where the data supports it. Verify each generated chart non-visually before embedding by checking the output file exists, has non-zero size, opens as an image, and reports expected dimensions and metadata through code interpreter filesystem or image-library checks. Do not use visual inspection as the verification gate; regenerate or omit any chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables.' diff --git a/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/yoy-report.yaml b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/yoy-report.yaml new file mode 100644 index 000000000..4d3a59f63 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/automations/scheduled-tasks/yoy-report.yaml @@ -0,0 +1,92 @@ +api_version: azuresre.ai/v2 +kind: ScheduledTask +metadata: + name: Semiannual +spec: + name: Semiannual + description: Semiannual year-over-year financial analysis for January and July close checkpoints + schedule: 0 9 5 1,7 * + mode: Review + agent: finops-practitioner + prompt: |- + # Semiannual Financial Analysis + --- **FISCAL YEAR CONFIGURATION:** + This task is configured for a **July–June fiscal year** (the Microsoft fiscal calendar) as a worked example. If your organization uses a different fiscal year, customize the following elements to match your fiscal calendar: + 1. **Cron schedule** (line 8): Adjust `0 9 5 1,7 *` to run at your mid-year and year-end close dates 2. **Fiscal period logic** (line 28): Update "July through June", "January 5 and July 5", and period-comparison statements 3. **Forecast targets** (lines 87, 145): Adjust "June 30" and checkpoint dates to align with your fiscal year end and mid-year close + The analysis logic below assumes July–June periods as the default example. Adapt month references and comparison windows to your fiscal calendar before deploying. + --- + + ## Phase 0: Preparation + + Before starting, search available knowledge and prior memory for relevant context, known issues, and previous findings. + Output style: Label measured, derived, and estimated evidence; cite each source or derivation; state exact scope and time period; organize findings by FinOps capability; include risk thresholds, confidence, caveats, and required disclaimers. + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has `SearchMemory`, its assigned `azure-resource-manager-mcp_*` Cost Management and Pricing tools, its assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`), its visualization tools (`PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`), and its Microsoft Teams delivery tools (`GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, `UpdateCardInConversation`). Use the `Task` system tool to invoke `azure-capacity-manager` for quota, capacity reservation group, SKU availability, zone mapping, database service quota, and benefit recommendation Python tools (`vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`, `zone-mapping`, `sku-availability`, `benefit-recommendations`), and invoke `chief-financial-officer` for executive finance framing. Pass the full task scope, evidence already collected, open questions, and desired output shape with each `Task` invocation. Check available visualization tools once per run and use `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` to generate charts for key findings; if no visualization tool is available, provide the data as a chart-ready table. Complete the report and chart artifacts before delivery. When a Microsoft Teams connector/channel is configured, `finops-practitioner` makes one final `PostMessageToConversation` attempt to the configured Teams channel. If a configured Teams delivery attempt fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If Teams delivery is not configured or available, render the completed report in the run output and state that Teams delivery was skipped. + + + Lead this as the FinOps practitioner. Collect fiscal cost, pricing, savings, recommendation, and transaction evidence directly; delegate capacity-risk evidence to `azure-capacity-manager` and consult `chief-financial-officer` for fiscal planning and executive framing. + + **Fiscal period assumptions (default: July–June fiscal year):** This task runs at mid-year and year-end close checkpoints. For a July–June fiscal year, that means January 5 and July 5. On the January checkpoint, compare the completed first-half period (July–December) against the same period in the prior fiscal year and forecast through the fiscal year end (June 30). On the July checkpoint, compare the just-completed fiscal year (July–June) against the previous fiscal year and prepare the next fiscal year planning view. Adjust these month references if your organization uses a different fiscal calendar (see configuration note above). + ## Specialist evidence requests for this task + + `finops-practitioner` runs the Azure Cost Management and ARM tools below directly. Ask `azure-capacity-manager` to run `benefit-recommendations` for capacity-related commitment evidence where needed. + + ## Phase 1: Data Collection + + Check returned data periods and completeness; report freshness and coverage limitations rather than assuming missing data means zero. + + ### 1.1 Fiscal cost comparisons + + Run `azure-resource-manager-mcp_query_costs` across current and prior-year fiscal comparison periods by month, service, resource group, region, resource type, and tags. Compare quarterly resource-group patterns, monthly volatility, anomalies, and charge-type or publisher breakdowns where available. + + ### 1.2 Forecast, alerts, and budgets + + Run `azure-resource-manager-mcp_forecast_costs` for the next fiscal checkpoint, `azure-resource-manager-mcp_list_alerts` for alert evidence, and `azure-resource-manager-mcp_list_budgets` for budget comparisons. At mid-year forecast through fiscal year end; at year-end prepare the next fiscal year planning baseline. + + ### 1.3 Commitments, savings, and transactions + + Run `azure-resource-manager-mcp_list_benefit_utilization`, `azure-resource-manager-mcp_get_benefit_recommendations`, `benefit-recommendations`, and `azure-resource-manager-mcp_list_reservation_transactions` for current and prior comparison periods where available. Assess utilization, savings, recommendations, and reservation transactions; disclose missing fields and do not infer no purchases from incomplete data. + + ### 1.4 Prices, allocation, and inventory + + Run `azure-resource-manager-mcp_get_retail_prices` for price benchmarking, `azure-resource-manager-mcp_list_dimensions` for allocation dimensions, and `resource-graph-query` for resource inventory and tags. Compare pricing and allocation across periods only when comparable evidence is available. + + ### 1.5 Fiscal scorecard + + Build a year-over-year FinOps scorecard from supported evidence. Separate measured results from derived interpretation, cite sources, and identify unavailable measures as data sufficiency limitations. + + **Checkpoint:** Collect available evidence and document gaps before strategic analysis. + + ## Phase 2: Strategic Analysis + + ### 2.1 Year-over-year scorecard + Grade year-over-year performance (A+ to F) across: cost management, savings realization, commitment coverage, tag compliance, forecast accuracy, and forecast risk. + + ### 2.2 Forecast + For mid-year checkpoint runs, project total fiscal year spend with Conservative/Expected/Optimistic scenarios and compare to annual budget. For year-end checkpoint runs, create the next fiscal year planning baseline. + + ### 2.3 Savings opportunities + Identify quick-win optimization opportunities that can reduce costs before the next fiscal checkpoint. Prioritize by days remaining and implementation effort. + + ### 2.4 Fiscal planning + Based on year-over-year trends, recommend budget changes, commitment purchase strategy, and optimization roadmap. + + ### 2.5 Detailed Findings + 7 sections: Cost Analysis, Savings Analysis, Optimization, Governance, Market Positioning, Temporal Themes, Implementation Roadmap. + + ### 2.6 Recommendations + Prioritized actions: before the next fiscal checkpoint, next half-year, and next full fiscal year. Dollar amounts and owners for each. + + ## Phase 3: Report + + Professional executive report with tables, charts, emoji indicators. Include year-over-year variance, fiscal timeline, forecast scenarios, grade card, and planning recommendations. + + Do not stop or yield until the report is complete and ready for the FinOps team. + + ## Phase 4: Deliver + + + **Visualizations:** Before delivery, `finops-practitioner` generates charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation`; for capacity evidence, `azure-capacity-manager` generates the charts and may use `ExecutePythonCode` only as a fallback. If visualization tools are unavailable, provide chart-ready tables. Include at minimum one trend chart and one comparison chart where the data supports it. Verify each chart file non-visually by checking that it exists, has non-zero size, opens as an image, and has the expected dimensions and metadata. Regenerate or omit a chart that fails these checks and state why. Embed chart images inline in the Microsoft Teams message alongside text tables. + + **Microsoft Teams (financial results only):** Post the final report to our Teams channel using `PostMessageToConversation` when configured. Do not post intermediate results. If Teams delivery is not configured or available, return the completed report in the run output with a note stating that Teams delivery was skipped. If a configured Teams final delivery attempt fails, do not retry or probe alternate delivery paths; mark the task/run failed. + + **Knowledge base (operational learnings only):** Save any new operational insights to the knowledge base — tool errors encountered, workarounds discovered, data quality observations, query patterns that worked or failed. Never save financial figures, cost amounts, savings numbers, or any customer financial data to the knowledge base. Financial detail goes to configured Microsoft Teams delivery only. diff --git a/sreagent-templates/recipes/cost-management/config/common-prompts/safety-rules.yaml b/sreagent-templates/recipes/cost-management/config/common-prompts/safety-rules.yaml new file mode 100644 index 000000000..efa6dd631 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/common-prompts/safety-rules.yaml @@ -0,0 +1,15 @@ +metadata: + name: safety-rules +spec: + prompt: '## Safety rules + + + - Never delete resources in production without explicit approval + + - Always prefer read-only investigation before taking action + + - Escalate to human if confidence is below 80% + + - Do not modify network security groups or firewall rules + + - Do not access or display secrets, keys, or connection strings' diff --git a/sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml b/sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml new file mode 100644 index 000000000..3c5a30a80 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml @@ -0,0 +1,46 @@ +api_version: azuresre.ai/v2 +kind: ManagedConnector +metadata: + name: microsoft-teams +spec: + apiName: teams + displayName: Microsoft Teams + connectionName: cost-management-teams + requireApprovalTools: [] + operations: + - name: PostMessageToConversation + displayName: Post message in a chat or channel + description: Posts a message to a chat or a channel + requiresApproval: false + userParameters: [] + agentParameters: [] + - name: PostCardToConversation + displayName: Post card in a chat or channel + description: Posts a card to a chat or a channel + requiresApproval: false + userParameters: [] + agentParameters: [] + - name: ReplyWithMessageToConversation + displayName: Reply with a message in a channel + description: Replies with a message to a channel's message + requiresApproval: false + userParameters: [] + agentParameters: [] + - name: ReplyWithCardToConversation + displayName: Reply with an adaptive card in a channel + description: Replies with an adaptive card to a channel's message + requiresApproval: false + userParameters: [] + agentParameters: [] + - name: UpdateCardInConversation + displayName: Update an adaptive card in a chat or channel + description: Updates an existing adaptive card + requiresApproval: false + userParameters: [] + agentParameters: [] + - name: GetMessagesFromChannel + displayName: Get messages in a channel + description: Gets messages from a channel in a specific team + requiresApproval: false + userParameters: [] + agentParameters: [] diff --git a/sreagent-templates/recipes/cost-management/config/plugins/installations/azcapman.yaml b/sreagent-templates/recipes/cost-management/config/plugins/installations/azcapman.yaml new file mode 100644 index 000000000..51855e8fb --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/plugins/installations/azcapman.yaml @@ -0,0 +1,8 @@ +name: azcapman-azure-capacity-management +operation: install-from-marketplace +spec: + marketplaceName: azcapman + pluginName: azure-capacity-management + expectedSkills: + - azure-capacity-management + autoUpdate: false diff --git a/sreagent-templates/recipes/cost-management/config/plugins/marketplaces/azcapman.yaml b/sreagent-templates/recipes/cost-management/config/plugins/marketplaces/azcapman.yaml new file mode 100644 index 000000000..b0f55d65f --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/plugins/marketplaces/azcapman.yaml @@ -0,0 +1,6 @@ +name: azcapman +spec: + sourceType: github + sourceUrl: microsoft/azcapman + owner: + name: Microsoft diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.md new file mode 100644 index 000000000..14e9cd274 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.md @@ -0,0 +1,197 @@ +--- +name: azure-cost-management +description: Azure cost optimization and financial governance. Use for Advisor recommendations, commitment discounts (reservations and savings plans), budgets, cost exports, anomaly alerts, credit and MACC tracking, orphaned resource detection, VM rightsizing, and retail price lookup. +license: MIT +compatibility: Requires Azure CLI authentication (az login) and appropriate RBAC permissions for Cost Management APIs. +metadata: + author: microsoft + version: "1.1.0" +allowed-tools: az pwsh curl +--- + +# Azure Cost Management + +Azure Cost Management and optimization skills. Provides recommendations, budget management, exports, alerts, and commitment tracking capabilities. + +## Domain Knowledge + +| Domain | Purpose | Key Operations | +|--------|---------|----------------| +| **azure-advisor** | Cost recommendations | Query, suppress, manage recommendations | +| **azure-savings-plans** | Savings plan analysis | Benefit recommendations, coverage, ROI | +| **azure-budgets** | Budget management | Create budgets, notifications, action groups | +| **azure-cost-exports** | Scheduled exports | FOCUS exports, backfill, troubleshooting | +| **azure-anomaly-alerts** | Cost anomaly detection | Bulk alert deployment across subscriptions | +| **azure-reservations** | Reserved instance analysis | Reservation recommendations, utilization, coverage, exchange/return | +| **azure-commitment-discount-decision** | Commitment discount framework | Reservations vs savings plans decision criteria, hybrid strategy | +| **azure-credits** | Credit tracking | Azure Prepayment balance, expiration risk | +| **azure-macc** | MACC commitment tracking | Balance, decrements, milestone tracking | +| **azure-orphaned-resources** | Waste detection | Resource Graph queries for orphaned/unused resources | +| **azure-retail-prices** | Price lookup | Public API for SKU pricing, cross-region comparison | +| **azure-vm-rightsizing** | VM optimization | Utilization analysis, SKU downsize recommendations | + +## Cost Optimization + +### Azure Advisor Recommendations + +```bash +az advisor recommendation list --category Cost --output table +``` + +**Suppression:** Up to 90-day TTL, bulk suppression via management groups. + +For detailed documentation: `references/azure-advisor.md` + +### Orphaned resources + +Detect unused resources generating waste with zero workload value. Immediate savings, zero risk. + +```bash +az graph query -q "resources | where type == 'microsoft.compute/disks' | where properties.diskState == 'Unattached' | project name, resourceGroup, sizeGb = properties.diskSizeGB" +``` + +Covers: unattached disks, unused NICs, orphaned public IPs, idle NAT gateways, orphaned snapshots, idle load balancers, empty availability sets, orphaned NSGs. + +For detailed documentation: `references/azure-orphaned-resources.md` + +### VM rightsizing + +Identify over-provisioned VMs using Advisor + Azure Monitor metrics, validate with retail pricing. + +- Thresholds: CPU P95 < 20%, memory avg < 30% +- Safety checks: burst requirements (P99), instance size flexibility, Hybrid Benefit + +For detailed documentation: `references/azure-vm-rightsizing.md` + +### Savings plans and reservations + +**Benefit Recommendations API** for: +- Savings plan purchase recommendations (up to 65% savings) +- Reservation recommendations (up to 72% savings) +- Coverage analysis and utilization monitoring +- Decision framework: when to use which commitment type + +For detailed documentation: +- `references/azure-savings-plans.md` — savings plan analysis and script +- `references/azure-reservations.md` — reserved instance analysis +- `references/azure-commitment-discount-decision.md` — decision framework + +### Azure Retail Prices + +Public API for looking up Azure pricing by SKU, region, and tier. No authentication required. + +``` +https://prices.azure.com/api/retail/prices?$filter=armSkuName eq 'Standard_D4s_v5' and armRegionName eq 'eastus' +``` + +Use for: price comparisons, rightsizing savings validation, cross-region cost analysis. + +For detailed documentation: `references/azure-retail-prices.md` + +## Budget & Alerts + +### Budgets + +- Up to 5 notifications per budget +- Action Groups at Subscription/Resource Group scope only +- Threshold types: Actual, Forecasted + +```bash +az consumption budget list --output table +``` + +For detailed documentation: `references/azure-budgets.md` + +### Anomaly Alerts + +Enterprise-scale deployment with pagination for large environments. + +For detailed documentation: `references/azure-anomaly-alerts.md` + +### Cost Exports + +FOCUS format exports to storage accounts with backfill support. + +For detailed documentation: `references/azure-cost-exports.md` + +## Commitment Tracking + +### Azure Credits (Prepayment) + +Track EA/MCA credit balances, expiration dates, consumption history. + +For detailed documentation: `references/azure-credits.md` + +### MACC (Azure Consumption Commitment) + +Track MACC balance, decrements, milestone progress, eligible spend. + +MACC is a billing-account contractual spending agreement. It is not an Azure Reservation, savings plan, capacity reservation, or commitment-discount metric. Use only billing-account Consumption Lots and Events with `source` or `lotSource` equal to `ConsumptionCommitment` for MACC amount, remaining balance, decrement, burn, progress, or status. Never infer those values from Cost Management queries, reservation or savings-plan purchases, recommendations, utilization, coverage, or waste. + +For detailed documentation: `references/azure-macc.md` + +## Reference documentation + +- **Optimization**: `references/azure-advisor.md`, `references/azure-savings-plans.md`, `references/azure-reservations.md` +- **Waste detection**: `references/azure-orphaned-resources.md` +- **Rightsizing**: `references/azure-vm-rightsizing.md`, `references/azure-retail-prices.md` +- **Decision framework**: `references/azure-commitment-discount-decision.md` +- **Budgets and alerts**: `references/azure-budgets.md`, `references/azure-anomaly-alerts.md`, `references/azure-cost-exports.md` +- **Commitments**: `references/azure-credits.md`, `references/azure-macc.md` + +Load the appropriate reference file when detailed workflows, API examples, or troubleshooting are needed. + +## Safety + +**Always confirm with the user before executing any delete, remove, or purge operation.** This includes `Remove-AzDisk`, `az network public-ip delete`, `az disk delete`, and any bulk cleanup scripts. Show the list of resources to be deleted and wait for explicit approval before proceeding. Never infer consent from a general "clean up orphaned resources" request. + +## Best practices + +### Cost API queries + +Use `az rest` with a JSON body rather than `az costmanagement query` — it is more reliable and supports the full query schema: + +```bash +az rest --method post \ + --url "https://management.azure.com/subscriptions//providers/Microsoft.CostManagement/query?api-version=2023-11-01" \ + --body '@cost-query.json' +``` + +### Free tier awareness + +Many Azure services have generous free allowances that explain $0 cost lines. Do not flag these as anomalies. Examples: +- Container Apps: 180K vCPU-sec and 360K GB-sec free per month +- Azure Functions: 1M executions free per month +- Log Analytics: first 5 GB/month free per workspace + +### Azure Quick Review (azqr) + +For broad orphaned resource scanning, [Azure Quick Review](https://azure.github.io/azqr/) (`azqr`) can scan entire subscriptions efficiently and identify orphaned resources, oversized SKUs, and missing tags in one pass. Complement the Resource Graph queries in `references/azure-orphaned-resources.md` with azqr for large environments. + +### Azure portal links + +Include deep links to resources when presenting recommendations. Use this format (includes tenant context): + +``` +https://portal.azure.com/#@/resource/subscriptions//resourceGroups//providers////overview +``` + +## Data classification + +When presenting cost data, label the source clearly so recommendations are auditable: + +- **ACTUAL DATA** — Retrieved from Azure Cost Management API +- **ACTUAL METRICS** — Retrieved from Azure Monitor +- **VALIDATED PRICING** — Retrieved from official Azure pricing pages (`prices.azure.com`) +- **ESTIMATED SAVINGS** — Calculated from actual data and validated pricing + +Never present estimates as actuals. + +## Common pitfalls + +- **Assuming costs**: Always query actual data from the Cost Management API before making recommendations. +- **Ignoring free tiers**: Validate $0 cost lines against known free allowances before treating them as anomalies. +- **Using `az costmanagement query`**: Prefer `az rest` — the CLI command has known reliability issues with complex queries. +- **Wrong date ranges**: Use 30 days for cost analysis, 14 days for utilization metrics. +- **Broken portal links**: Always include tenant ID in portal URLs. +- **Presenting estimates as actuals**: Use the data classification labels above to be explicit about the source of every number. diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.yaml b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.yaml new file mode 100644 index 000000000..db75fbd23 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management.yaml @@ -0,0 +1,38 @@ +metadata: + name: azure-cost-management + description: Azure cost optimization and financial governance. Use for Advisor recommendations, commitment discounts (reservations and savings plans), budgets, cost exports, anomaly alerts, credit and MACC tracking, orphaned resource detection, VM rightsizing, and retail price lookup. + spec: + tools: + - benefit-recommendations + - deploy-anomaly-alert + - deploy-budget + - deploy-bulk-anomaly-alerts + - deploy-bulk-budgets + - resource-graph-query + - suppress-advisor-recommendations +skillContent: skills/azure-cost-management.md +additionalFiles: + - filePath: references/azure-advisor.md + content: skills/azure-cost-management/references/azure-advisor.md + - filePath: references/azure-anomaly-alerts.md + content: skills/azure-cost-management/references/azure-anomaly-alerts.md + - filePath: references/azure-budgets.md + content: skills/azure-cost-management/references/azure-budgets.md + - filePath: references/azure-commitment-discount-decision.md + content: skills/azure-cost-management/references/azure-commitment-discount-decision.md + - filePath: references/azure-cost-exports.md + content: skills/azure-cost-management/references/azure-cost-exports.md + - filePath: references/azure-credits.md + content: skills/azure-cost-management/references/azure-credits.md + - filePath: references/azure-macc.md + content: skills/azure-cost-management/references/azure-macc.md + - filePath: references/azure-orphaned-resources.md + content: skills/azure-cost-management/references/azure-orphaned-resources.md + - filePath: references/azure-reservations.md + content: skills/azure-cost-management/references/azure-reservations.md + - filePath: references/azure-retail-prices.md + content: skills/azure-cost-management/references/azure-retail-prices.md + - filePath: references/azure-savings-plans.md + content: skills/azure-cost-management/references/azure-savings-plans.md + - filePath: references/azure-vm-rightsizing.md + content: skills/azure-cost-management/references/azure-vm-rightsizing.md diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-advisor.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-advisor.md new file mode 100644 index 000000000..4735c55e1 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-advisor.md @@ -0,0 +1,259 @@ +--- +name: Azure Advisor +description: Azure Advisor provides personalized recommendations for optimizing Azure resources across cost, security, reliability, operational excellence, and performance. This skill focuses on **cost recommendations** and recommendation management. +--- + +**Key Features:** +- Cost optimization recommendations (right-sizing, shutdown, reservations) +- Recommendation suppression with TTL (up to 90 days) +- Bulk suppression across management groups +- Integration with FinOps workflows + +--- + +## Querying Cost Recommendations + +### Azure CLI + +```bash +# List all cost recommendations for a subscription +az advisor recommendation list \ + --category Cost \ + --output table + +# List with details +az advisor recommendation list \ + --category Cost \ + --query "[].{Resource:resourceGroup, Impact:impact, Description:shortDescription.problem}" + +# Filter by impact +az advisor recommendation list \ + --category Cost \ + --query "[?impact=='High']" +``` + +### PowerShell + +```powershell +# Get all cost recommendations +Get-AzAdvisorRecommendation | + Where-Object { $_.Category -eq 'Cost' } + +# Get high-impact recommendations +Get-AzAdvisorRecommendation | + Where-Object { $_.Category -eq 'Cost' -and $_.Impact -eq 'High' } + +# Export to CSV +Get-AzAdvisorRecommendation | + Where-Object { $_.Category -eq 'Cost' } | + Select-Object ResourceId, Impact, ShortDescriptionProblem | + Export-Csv -Path "advisor-recommendations.csv" +``` + +### REST API + +> **Authentication note:** Use `az rest` in practice — it handles token acquisition automatically. The raw HTTP examples below are for documentation purposes only. + +```http +GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Advisor/recommendations?api-version=2023-01-01&$filter=Category eq 'Cost' +Authorization: Bearer {token} +``` + +--- + +## Common Cost Recommendation Types + +| Recommendation Type | ID | Description | +|--------------------|-----|-------------| +| Right-size VMs | `e10b1381-5f0a-47ff-8c7b-37bd13d7c974` | Resize underutilized VMs — see `references/azure-vm-rightsizing.md` for full validation workflow | +| Shutdown idle VMs | `89515250-1243-43d1-b4e7-f9437cedffd8` | Stop VMs with low utilization | +| Reserved instances | `84b1a508-fc21-49da-979e-96894f1665df` | Purchase RIs for consistent workloads | +| Delete unused disks | `48eda464-1485-4dcf-a674-d0905df5054a` | Remove unattached managed disks — see `references/azure-orphaned-resources.md` for expanded orphaned resource detection | + +--- + +## Suppressing Recommendations + +Azure Policy cannot disable Advisor recommendations. Instead, use the Advisor suppression API with TTL up to 90 days. + +### PowerShell Suppression Script + +```powershell +# Suppress specific recommendation types across a management group +.\Suppress-AdvisorRecommendations.ps1 -ManagementGroupId "your-mg" ` + -RecommendationTypeIds @( + "89515250-1243-43d1-b4e7-f9437cedffd8", # Shutdown idle VMs + "84b1a508-fc21-49da-979e-96894f1665df", # Reserved instances + "48eda464-1485-4dcf-a674-d0905df5054a" # Delete unused disks + ) -Days 30 -WhatIf + +# Execute suppression +.\Suppress-AdvisorRecommendations.ps1 -ManagementGroupId "your-mg" ` + -RecommendationTypeIds @(...) -Days 30 +``` + +### REST API Suppression + +> **Authentication note:** Use `az rest` in practice — it handles token acquisition automatically. The raw HTTP example below is for documentation purposes only. + +```http +PUT https://management.azure.com/{resourceUri}/providers/Microsoft.Advisor/recommendations/{recommendationId}/suppressions/{suppressionName}?api-version=2023-01-01 +Content-Type: application/json +Authorization: Bearer {token} + +{ + "properties": { + "ttl": "30:00:00:00" + } +} +``` + +**TTL Format:** `days:hours:minutes:seconds` (max 90 days) + +> **Dismiss vs postpone:** To permanently dismiss a recommendation instead of postponing it, omit the `ttl` property (send `"properties": {}` in the PUT body). The recommendation will remain hidden indefinitely with no automatic reappearance. Permanent dismissals can be reversed via the [Suppressions DELETE API](https://learn.microsoft.com/en-us/rest/api/advisor/suppressions/delete) or by clicking "Activate" under the Advisor portal's "Postponed & Dismissed" filter. Prefer postpone with TTL over permanent dismiss for cost recommendations, since dismissed recommendations silently stop surfacing even when resource conditions change. Reserve permanent dismissal for recommendations that are structurally irrelevant to your environment. + +### List Suppressions + +```http +GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Advisor/suppressions?api-version=2023-01-01 +``` + +### Delete Suppression + +```http +DELETE https://management.azure.com/{resourceUri}/providers/Microsoft.Advisor/recommendations/{recommendationId}/suppressions/{suppressionName}?api-version=2023-01-01 +``` + +--- + +## Scheduled Suppression Refresh + +Since suppression TTL is capped at 90 days, schedule weekly refreshes via: + +- **Azure Automation** - Runbook on schedule +- **CI/CD Pipeline** - GitHub Actions or Azure DevOps +- **Logic Apps** - Recurrence trigger + +Example Azure Automation schedule: + +```powershell +# Create automation schedule +New-AzAutomationSchedule -AutomationAccountName "MyAutomation" ` + -Name "WeeklyAdvisorSuppression" ` + -StartTime (Get-Date).AddDays(1) ` + -WeekInterval 1 ` + -DaysOfWeek "Monday" ` + -ResourceGroupName "automation-rg" +``` + +--- + +## Permissions + +| Action | Required Role | +|--------|---------------| +| View recommendations | Reader | +| Suppress recommendations | Advisor Contributor | +| Bulk management group operations | Advisor Contributor on MG and subscriptions | + +--- + +## Azure Resource Graph Queries + +### All Cost Recommendations + +```kusto +advisorresources +| where type == "microsoft.advisor/recommendations" +| where properties.category == "Cost" +| project + subscriptionId, + resourceGroup, + impact = properties.impact, + problem = properties.shortDescription.problem, + solution = properties.shortDescription.solution, + savings = properties.extendedProperties.savingsAmount +``` + +### High-Impact Recommendations with Savings + +```kusto +advisorresources +| where type == "microsoft.advisor/recommendations" +| where properties.category == "Cost" +| where properties.impact == "High" +| extend savings = todouble(properties.extendedProperties.savingsAmount) +| summarize + TotalSavings = sum(savings), + Count = count() + by subscriptionId +| order by TotalSavings desc +``` + +### Recommendations by Type + +```kusto +advisorresources +| where type == "microsoft.advisor/recommendations" +| where properties.category == "Cost" +| summarize Count = count() by tostring(properties.recommendationTypeId) +| order by Count desc +``` + +--- + +## Integration with FinOps Workflows + +### Export Recommendations for Analysis + +```powershell +# Get all cost recommendations across subscriptions +$recommendations = Get-AzSubscription | ForEach-Object { + Set-AzContext -Subscription $_.Id + Get-AzAdvisorRecommendation | + Where-Object { $_.Category -eq 'Cost' } +} + +# Calculate total potential savings +$totalSavings = $recommendations | + Where-Object { $_.ExtendedProperty["savingsAmount"] } | + ForEach-Object { [double]$_.ExtendedProperty["savingsAmount"] } | + Measure-Object -Sum + +Write-Host "Total potential monthly savings: $($totalSavings.Sum)" +``` + +### Prioritize by Impact and Savings + +```powershell +$recommendations | + Select-Object @{N='Resource';E={$_.ResourceId}}, + Impact, + @{N='Savings';E={[double]($_.ExtendedProperty["savingsAmount"] ?? 0)}}, + @{N='ImpactRank';E={ @{'High'=3;'Medium'=2;'Low'=1}[$_.Impact] }}, + @{N='Problem';E={$_.ShortDescriptionProblem}} | + Sort-Object -Property @{E='ImpactRank';D=$true}, @{E='Savings';D=$true} | + Select-Object Resource, Impact, Savings, Problem | + Format-Table +``` + +--- + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| No recommendations | New subscription | Wait 24-48 hours for analysis | +| Suppression fails | Missing permissions | Need Advisor Contributor role | +| Suppression expired | TTL exceeded | Re-run suppression script | +| Wrong savings estimate | Stale data | Refresh recommendations | + +--- + +## References + +- [Azure Advisor overview](https://learn.microsoft.com/azure/advisor/advisor-overview) +- [Cost recommendations](https://learn.microsoft.com/azure/advisor/advisor-cost-recommendations) +- [Suppress recommendations](https://learn.microsoft.com/azure/advisor/view-recommendations#dismissing-and-postponing-recommendations) +- [Advisor REST API](https://learn.microsoft.com/rest/api/advisor/) + diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-anomaly-alerts.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-anomaly-alerts.md new file mode 100644 index 000000000..8aa95b594 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-anomaly-alerts.md @@ -0,0 +1,292 @@ +--- +name: Azure Cost Anomaly Alerts +description: Deploy cost anomaly detection alerts across Azure subscriptions at enterprise scale. These alerts automatically notify stakeholders when Cost Management detects unusual spending patterns. +--- + +**Resource Type:** `Microsoft.CostManagement/scheduledActions` (InsightAlert type) + +**Key Features:** +- Automated cost anomaly detection +- Email notifications when anomalies detected +- Enterprise-scale bulk deployment with pagination +- Management group targeting + +--- + +## What Gets Deployed + +- **Cost Management scheduled action** named "AnomalyAlert" +- **Anomaly detection** monitoring at subscription level +- **Email notifications** to specified recipients when anomalies are detected + +--- + +## PowerShell Deployment + +### Prerequisites + +```powershell +# Install required modules +Install-Module -Name Az -Force -AllowClobber +Install-Module -Name Az.ResourceGraph -Force -AllowClobber # For bulk deployments + +# Authenticate +Connect-AzAccount +``` + +### Single Subscription Deployment + +```powershell +# Interactive subscription selection +./Deploy-AnomalyAlert.ps1 ` + -EmailRecipients @("admin@company.com", "finance@company.com") ` + -NotificationEmail "alerts@company.com" + +# Specific subscription +./Deploy-AnomalyAlert.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" + +# Preview without deploying +./Deploy-AnomalyAlert.ps1 ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" ` + -WhatIf + +# Automated/silent deployment +./Deploy-AnomalyAlert.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" ` + -Force -Quiet +``` + +### Enterprise Bulk Deployment + +```powershell +# Deploy to all subscriptions in management group +./Deploy-BulkALZ.ps1 ` + -TenantId "12345678-1234-1234-1234-123456789012" ` + -ManagementGroup "ALZ" ` + -EmailRecipients @("finops@company.com", "alerts@company.com") ` + -NotificationEmail "alerts@company.com" + +# Preview deployment +./Deploy-BulkALZ.ps1 ` + -TenantId "12345678-1234-1234-1234-123456789012" ` + -ManagementGroup "ALZ" ` + -EmailRecipients @("finops@company.com") ` + -NotificationEmail "alerts@company.com" ` + -WhatIf + +# Quiet enterprise deployment +./Deploy-BulkALZ.ps1 ` + -TenantId "12345678-1234-1234-1234-123456789012" ` + -ManagementGroup "ALZ" ` + -EmailRecipients @("finops@company.com") ` + -NotificationEmail "alerts@company.com" ` + -Quiet +``` + +### Parameters + +**Deploy-AnomalyAlert.ps1:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `EmailRecipients` | Yes | Array of email addresses for notifications | +| `NotificationEmail` | Yes | Primary email for the alert system | +| `SubscriptionId` | No | Target subscription (interactive if not provided) | +| `DeploymentName` | No | Custom deployment name | +| `Location` | No | Azure region (default: West US) | +| `Force` | No | Skip confirmation prompts | +| `Quiet` | No | Suppress verbose output | +| `WhatIf` | No | Preview without deploying | + +**Deploy-BulkALZ.ps1:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `TenantId` | Yes | Azure tenant ID | +| `ManagementGroup` | Yes | Management group name | +| `EmailRecipients` | Yes | Array of email addresses | +| `NotificationEmail` | Yes | Primary email for alerts | +| `WhatIf` | No | Preview without deploying | +| `Quiet` | No | Suppress warnings | + +--- + +## Enterprise Pagination + +The bulk deployment script handles large environments with automatic pagination: + +``` +Connecting to tenant... +Finding subscriptions in ALZ management group... +Querying subscriptions (page 1)... +Found 1000 subscriptions in this page (total: 1000) +Querying subscriptions (page 2)... +Found 1000 subscriptions in this page (total: 2000) +Querying subscriptions (page 3)... +Found 847 subscriptions in this page (total: 2847) +Total subscriptions found: 2847 +``` + +**Key Features:** +- Processes 1,000 subscriptions per query page +- Automatic pagination for 5,000+ subscription environments +- Real-time progress reporting +- Memory-efficient processing + +--- + +## Bicep Template + +```bicep +targetScope = 'subscription' + +@description('Email recipients for anomaly notifications') +param emailRecipients array + +@description('Primary notification email') +param notificationEmail string + +module anomalyAlert 'br/public:cost/subscription-scheduled-action:1.0.2' = { + name: 'anomaly-alert-deployment' + params: { + name: 'AnomalyAlert' + displayName: 'Cost Anomaly Alert' + kind: 'InsightAlert' + notification: { + to: emailRecipients + subject: 'Cost Anomaly Detected' + } + notificationEmail: notificationEmail + } +} +``` + +--- + +## Azure CLI Deployment + +```bash +az deployment sub create \ + --name "anomaly-alert-deployment" \ + --location "West US" \ + --template-file "anomaly-alert.bicep" \ + --parameters emailRecipients='["admin@company.com"]' \ + notificationEmail="alerts@company.com" +``` + +### Validation + +```bash +# Validate template +az deployment sub validate \ + --location "West US" \ + --template-file "anomaly-alert.bicep" \ + --parameters "@anomaly-alert.parameters.json" + +# What-if analysis +az deployment sub what-if \ + --location "West US" \ + --template-file "anomaly-alert.bicep" \ + --parameters "@anomaly-alert.parameters.json" +``` + +--- + +## Custom Bulk Deployment + +### Deploy to Filtered Subscriptions + +```powershell +# Deploy only to production subscriptions +$emailRecipients = @("finops@company.com") +$notificationEmail = "alerts@company.com" + +$subscriptions = Search-AzGraph -Query @" +ResourceContainers +| where type =~ 'microsoft.resources/subscriptions' +| where name contains 'Prod' or name contains 'Production' +| project subscriptionId, name +"@ + +Write-Host "Found $($subscriptions.Count) production subscriptions" + +foreach ($sub in $subscriptions) { + Write-Host "Deploying to: $($sub.name)" -ForegroundColor Yellow + ./Deploy-AnomalyAlert.ps1 ` + -SubscriptionId $sub.subscriptionId ` + -EmailRecipients $emailRecipients ` + -NotificationEmail $notificationEmail ` + -Force +} +``` + +### Deploy with Validation First + +```powershell +$managementGroupName = "Development" +$subscriptions = Search-AzGraph -Query @" +ResourceContainers +| where type =~ 'microsoft.resources/subscriptions' +| project subscriptionId, name +"@ -ManagementGroup $managementGroupName + +# Validation phase +Write-Host "=== VALIDATION PHASE ===" -ForegroundColor Magenta +foreach ($sub in $subscriptions) { + Write-Host "Validating: $($sub.name)" -ForegroundColor Cyan + ./Deploy-AnomalyAlert.ps1 ` + -SubscriptionId $sub.subscriptionId ` + -EmailRecipients @("test@company.com") ` + -NotificationEmail "test@company.com" ` + -WhatIf +} + +# Confirmation +$confirm = Read-Host "Proceed with deployment? (y/N)" +if ($confirm -eq 'y') { + Write-Host "=== DEPLOYMENT PHASE ===" -ForegroundColor Magenta + foreach ($sub in $subscriptions) { + ./Deploy-AnomalyAlert.ps1 ` + -SubscriptionId $sub.subscriptionId ` + -EmailRecipients @("alerts@company.com") ` + -NotificationEmail "alerts@company.com" ` + -Force + } +} +``` + +--- + +## Azure Resource Graph Queries + +| Purpose | Query | +|---------|-------| +| All subscriptions | `ResourceContainers \| where type =~ 'microsoft.resources/subscriptions' \| project subscriptionId, name` | +| Enabled only | `ResourceContainers \| where type =~ 'microsoft.resources/subscriptions' \| where properties.state == 'Enabled' \| project subscriptionId, name` | +| Name filter | `ResourceContainers \| where type =~ 'microsoft.resources/subscriptions' \| where name contains 'keyword' \| project subscriptionId, name` | + +--- + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| Permission errors | Missing Contributor/Owner role | Verify role assignment on subscription | +| Authentication issues | Not signed in | Run `Connect-AzAccount` | +| Location conflicts | Existing alert in different region | Default West US usually works | +| Rate limiting | Too many concurrent requests | Add delays or reduce parallelism | +| Query timeout | Large management group | Pagination handles automatically | + +--- + +## References + +- [Cost anomaly alerts](https://learn.microsoft.com/azure/cost-management-billing/understand/analyze-unexpected-charges) +- [Scheduled actions API](https://learn.microsoft.com/rest/api/cost-management/scheduled-actions) + diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-budgets.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-budgets.md new file mode 100644 index 000000000..2e4ae85ed --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-budgets.md @@ -0,0 +1,188 @@ +--- +name: Azure Budgets +description: Cost budgets track spending against a threshold and send notifications when exceeded. Supports email, role-based, and Action Group notifications for automation. +--- + +**Key Facts:** +- Up to 5 notifications per budget +- Action Groups only at Subscription/Resource Group scope +- Start date must be 1st of month, on or after June 1, 2017 +- BillingMonth/Quarter/Annual time grains for EA/MCA billing scopes + +## Supported Scopes + +| Scope | Format | Action Groups | +|-------|--------|---------------| +| Subscription | `/subscriptions/{subscriptionId}` | Yes | +| Resource Group | `/subscriptions/{subId}/resourceGroups/{rg}` | Yes | +| Billing Account (EA) | `/providers/Microsoft.Billing/billingAccounts/{enrollmentId}` | No | +| Billing Profile (MCA) | `/providers/Microsoft.Billing/billingAccounts/{accountId}/billingProfiles/{profileId}` | No | +| Invoice Section (MCA) | `...billingProfiles/{profileId}/invoiceSections/{sectionId}` | No | + +## Workflow: Create Budget with Notifications + +### Step 1: List Existing Budgets + +```bash +# List budgets at subscription scope +az consumption budget list \ + --query "[].{Name:name, Amount:amount, Spent:currentSpend.amount, TimeGrain:timeGrain}" \ + -o table +``` + +### Step 2: Create Budget via REST API + +The CLI `az consumption budget create` doesn't support notifications. Use REST API: + +```bash +# Create budget with email notifications +SUBSCRIPTION_ID=$(az account show --query id -o tsv) + +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/providers/Microsoft.Consumption/budgets/MonthlyBudget?api-version=2024-08-01" \ + --body '{ + "properties": { + "category": "Cost", + "amount": 1000, + "timeGrain": "Monthly", + "timePeriod": { + "startDate": "2026-02-01", + "endDate": "2027-01-31" + }, + "notifications": { + "Actual_GreaterThan_80_Percent": { + "enabled": true, + "operator": "GreaterThan", + "threshold": 80, + "thresholdType": "Actual", + "contactEmails": ["finops@company.com"], + "contactRoles": ["Owner", "Contributor"] + }, + "Forecasted_GreaterThan_100_Percent": { + "enabled": true, + "operator": "GreaterThan", + "threshold": 100, + "thresholdType": "Forecasted", + "contactEmails": ["finops@company.com"] + } + } + } + }' +``` + +### Step 3: Verify Budget Created + +```bash +# Show budget details +az consumption budget show --budget-name "MonthlyBudget" -o table +``` + +## Workflow: Add Action Group for Automation + +Action Groups enable automated responses (Logic Apps, Azure Functions, webhooks) when budget thresholds are exceeded. + +### Step 1: Create or Identify Action Group + +```bash +# List existing action groups +az monitor action-group list \ + --query "[].{Name:name, ResourceGroup:resourceGroup}" \ + -o table + +# Create new action group (if needed) +az monitor action-group create \ + --name "BudgetAlerts" \ + --resource-group "monitoring-rg" \ + --short-name "BudgetAG" \ + --action email finops-team finops@company.com +``` + +### Step 2: Update Budget with Action Group + +```bash +SUBSCRIPTION_ID=$(az account show --query id -o tsv) +ACTION_GROUP_ID="/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/monitoring-rg/providers/microsoft.insights/actionGroups/BudgetAlerts" + +az rest --method PUT \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/providers/Microsoft.Consumption/budgets/MonthlyBudget?api-version=2024-08-01" \ + --body "{ + \"properties\": { + \"category\": \"Cost\", + \"amount\": 1000, + \"timeGrain\": \"Monthly\", + \"timePeriod\": { + \"startDate\": \"2026-02-01\", + \"endDate\": \"2027-01-31\" + }, + \"notifications\": { + \"Actual_GreaterThan_80_Percent\": { + \"enabled\": true, + \"operator\": \"GreaterThan\", + \"threshold\": 80, + \"thresholdType\": \"Actual\", + \"contactEmails\": [\"finops@company.com\"], + \"contactGroups\": [\"${ACTION_GROUP_ID}\"] + } + } + } + }" +``` + +## Notification Configuration + +| Field | Required | Description | +|-------|----------|-------------| +| `enabled` | Yes | Enable/disable this notification | +| `operator` | Yes | `GreaterThan`, `GreaterThanOrEqualTo` | +| `threshold` | Yes | Percentage (0-1000) | +| `thresholdType` | Yes | `Actual` or `Forecasted` | +| `contactEmails` | Conditional | Required if no contactGroups at sub/RG scope | +| `contactGroups` | No | Action Group resource IDs (sub/RG scope only) | +| `contactRoles` | No | Azure roles (Owner, Contributor, Reader) | +| `locale` | No | Notification language (en-us, ja-jp, etc.) | + +**Threshold types:** +- **Actual** - Triggers when accrued cost exceeds threshold +- **Forecasted** - Triggers when projected end-of-period cost exceeds threshold + +## Time Grain Options + +| Time Grain | Scope | Description | +|------------|-------|-------------| +| `Monthly` | All | Resets monthly | +| `Quarterly` | All | Resets quarterly | +| `Annually` | All | Resets annually | +| `BillingMonth` | EA/MCA billing | Aligns to billing period | +| `BillingQuarter` | EA/MCA billing | Aligns to billing period | +| `BillingAnnual` | EA/MCA billing | Aligns to billing period | + +## Common Operations + +### Delete Budget + +```bash +az consumption budget delete --budget-name "MonthlyBudget" +``` + +### List Budgets via REST (Any Scope) + +```bash +# EA Billing Account scope +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{enrollmentId}/providers/Microsoft.Consumption/budgets?api-version=2024-08-01" +``` + +## Best Practices + +1. **Use Forecasted alerts** at 100% to get early warning before overspend +2. **Combine with Action Groups** to trigger automation (scale down, notify Slack, create tickets) +3. **Set multiple thresholds** (50%, 80%, 100%) for progressive visibility +4. **Use filters** to create budgets for specific resource groups, tags, or resources +5. **For enterprise**, deploy budgets at billing scope with BillingMonth grain + +## References + +- [Tutorial: Create and manage budgets](https://learn.microsoft.com/azure/cost-management-billing/costs/tutorial-acm-create-budgets) +- [Budgets REST API](https://learn.microsoft.com/rest/api/consumption/budgets) +- [Action Groups](https://learn.microsoft.com/azure/azure-monitor/alerts/action-groups) +- [Manage costs with budgets](https://learn.microsoft.com/azure/cost-management-billing/manage/cost-management-budget-scenario) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-commitment-discount-decision.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-commitment-discount-decision.md new file mode 100644 index 000000000..d6a9dac65 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-commitment-discount-decision.md @@ -0,0 +1,218 @@ +--- +name: Commitment discount decision framework +description: Decision framework for choosing between Azure Reservations, Savings Plans, or pay-as-you-go based on workload characteristics, risk tolerance, and organizational maturity. Includes comparison criteria, hybrid strategies, and key performance indicators. +--- + +**Key Features:** +- Side-by-side comparison of reservations vs savings plans +- Decision criteria based on workload stability and flexibility needs +- Hybrid commitment strategy guidance +- Key performance indicators for commitment discount health +- FinOps Framework alignment with rate optimization capability + +--- + +## Decision flow + +Use this text-based decision flow to determine the right commitment type: + +1. **Do you have consistent compute usage for 30+ days?** + - No: Stay on pay-as-you-go, revisit in 30 days + - Yes: Continue to step 2 +2. **Is usage concentrated on specific VM SKUs in specific regions?** + - Yes: Start with reservations (up to 72% savings) + - No: Continue to step 3 +3. **Is usage spread across multiple VM types, regions, or services?** + - Yes: Start with savings plans (up to 65% savings) + - No: Continue to step 4 +4. **Do you need cancellation flexibility?** + - Yes: Reservations only (savings plans cannot be canceled) + - No: Continue to step 5 +5. **Do you have both stable and variable compute?** + - Yes: Use hybrid strategy (see below) + - No: Default to savings plans for simplicity + +--- + +## Comparison table + +| Factor | Reservations | Savings plans | Pay-as-you-go | +|--------|-------------|---------------|---------------| +| Maximum savings | Up to 72% | Up to 65% | 0% (baseline) | +| Flexibility | Low (specific SKU, region) | High (any eligible compute) | Maximum | +| Cancellation | Returns up to $50K/year | No cancellation or refund | N/A | +| Exchange | Yes, within same product family (prorated) | No (but can trade in reservations for savings plans) | N/A | +| Applies to | Specific resource type and region | All eligible compute services | All services | +| Benefit application order | First (highest priority) | Second (after reservations) | N/A | +| Scope options | Shared, management group, subscription, RG | Shared, management group, subscription, RG | N/A | +| Agreement types | EA, MCA, MPA, CSP, PAYG, Sponsorship | EA, MCA, MPA | All | +| Term options | 1 year, 3 years | 1 year, 3 years | None | +| Payment options | All upfront, monthly | All upfront, monthly | Usage-based | +| Instance size flexibility | Yes (within VM series) | N/A (applies to all compute) | N/A | + +--- + +## Hybrid commitment strategy + +The optimal approach for most organizations: + +1. **Buy reservations first** for stable, predictable workloads (baseline) +2. **Buy savings plans second** for variable or growing compute (covers the rest) +3. **Pay-as-you-go** for truly unpredictable or temporary workloads + +Key principle: Reservations are applied first in the benefit stack, so they always "win" for matching workloads. Savings plans catch remaining eligible charges that reservations don't cover. + +**Migration path:** If existing reservations no longer fit your workloads, you can trade them in for savings plans via self-service (no time limit). This is a one-way conversion — savings plans cannot be traded back to reservations. + +--- + +## Scope selection guidance + +| Scenario | Recommended scope | Rationale | +|----------|-------------------|-----------| +| Single team, dedicated workloads | Resource group | Maximum control, clear cost attribution | +| Shared infrastructure, multiple teams | Subscription | Balance of savings and governance | +| Enterprise-wide optimization | Shared or management group | Maximum savings, automatic benefit distribution | +| New to commitments | Shared | Safest starting point -- benefits auto-distribute | + +--- + +## Data requirements before committing + +- Minimum 30 days of consistent usage data (60 days preferred) +- Use the Benefit Recommendations API with `Last30Days` or `Last60Days` lookback +- Coefficient of variation (CV) in hourly usage: + - **< 0.3** = stable (reservation candidate) + - **0.3 - 0.6** = variable (savings plan candidate) + - **> 0.6** = volatile (stay on pay-as-you-go) +- Check for planned migrations, decommissions, or workload changes that would invalidate historical patterns + +### Evaluate usage stability + +```powershell +# Calculate coefficient of variation from hourly usage data +$hourlyUsage = @(10.2, 10.5, 10.1, 10.8, 10.3) # Replace with actual hourly data +$mean = ($hourlyUsage | Measure-Object -Average).Average +$stdDev = [Math]::Sqrt(($hourlyUsage | ForEach-Object { [Math]::Pow($_ - $mean, 2) } | Measure-Object -Sum).Sum / $hourlyUsage.Count) +$cv = $stdDev / $mean + +switch ($cv) { + { $_ -lt 0.3 } { Write-Host "CV: $([Math]::Round($cv, 3)) - Stable: reservation candidate"; break } + { $_ -lt 0.6 } { Write-Host "CV: $([Math]::Round($cv, 3)) - Variable: savings plan candidate"; break } + default { Write-Host "CV: $([Math]::Round($cv, 3)) - Volatile: stay on pay-as-you-go" } +} +``` + +--- + +## The 70% rule for management group scope + +The Benefit Recommendations API does not support management group scope. Microsoft's documented workaround: + +1. Get recommendations for each subscription individually +2. Sum the recommended commitment amounts +3. Purchase approximately 70% of the total (conservative start) +4. Wait 3 days for the recommendation engine to recalculate +5. Iterate -- get new recommendations that account for existing commitments +6. Repeat until incremental savings are negligible + +```powershell +# Aggregate recommendations across subscriptions by calling the API directly +$subscriptions = Get-AzSubscription +$totalRecommended = 0 + +foreach ($sub in $subscriptions) { + Set-AzContext -Subscription $sub.Id + $scope = "subscriptions/$($sub.Id)" + $url = "https://management.azure.com/$scope/providers/Microsoft.CostManagement/benefitRecommendations?`$filter=properties/lookBackPeriod eq 'Last30Days' AND properties/term eq 'P3Y'&`$expand=properties/allRecommendationDetails&api-version=2024-08-01" + $uri = [uri]::new($url) + $result = Invoke-AzRestMethod -Uri $uri.AbsoluteUri -Method GET + $recs = ($result.Content | ConvertFrom-Json).value + + foreach ($rec in $recs) { + $details = $rec.properties.recommendationDetails + if ($details -and $details.averageUtilizationPercentage -ge 90) { + $totalRecommended += $details.commitmentAmount + } + } +} + +$mgScopeCommitment = $totalRecommended * 0.7 +Write-Host "Total recommended: `$$totalRecommended/hr" +Write-Host "Management group purchase (70%): `$$mgScopeCommitment/hr" +``` + +--- + +## Waiting periods + +| Event | Wait period | Reason | +|-------|-------------|--------| +| After purchasing, before evaluating other commitment types | 7 days | Allows recommendation engine to recalculate across both reservation and savings plan models | +| Iterative same-type purchasing (management group workaround) | 3 days | Allows new commitment to affect subscription-level recommendations before next iteration | + +The recommendation engine uses recent utilization data. New commitments change the usage pattern, so recommendations generated before the engine recalculates may be inaccurate. + +--- + +## Key performance indicators + +| KPI | Formula | Target | Description | +|-----|---------|--------|-------------| +| Effective savings rate (ESR) | (List cost - effective cost) / list cost | >20% | Percentage savings vs on-demand pricing | +| Utilization rate | Used hours / committed hours | >90% | How much of the commitment is actually used | +| Coverage percentage | Covered cost / total eligible cost | 60-80% | What portion of eligible spend is under commitment | +| Wastage rate | Wasted cost / commitment cost | <10% | Unused commitment (use-it-or-lose-it per hour) | + +**Interpretation guidelines:** +- ESR below 10% indicates no commitment discounts in place -- opportunity for savings +- Utilization below 80% indicates overcommitment -- consider exchanging or not renewing +- Coverage above 80% may indicate overcommitment risk -- leave room for usage variability +- Target ESR varies by organization and industry -- track trend over time rather than targeting a specific number + +--- + +## FinOps Framework alignment + +This decision framework maps to the FinOps Framework's rate optimization capability: + +- **Inform**: Analyze current spend, identify commitment-eligible workloads, assess usage stability, track ESR/utilization/wastage +- **Optimize**: Purchase commitments based on this decision framework, exchange underutilized reservations, adjust commitment levels +- **Operate**: Establish governance processes for commitment purchases, renewals, and exchanges; monitor utilization weekly; report savings to stakeholders + +Link: [Rate optimization (FinOps Framework)](https://learn.microsoft.com/cloud-computing/finops/framework/optimize/rates) + +--- + +## Common mistakes + +| Mistake | Impact | Prevention | +|---------|--------|------------| +| Buying savings plans before reservations | Lower savings (reservations offer up to 72% vs 65%) | Always buy reservations first for stable workloads | +| Purchasing 100% coverage | High wastage risk | Target 60-80% coverage, leave buffer for variability | +| Using 7-day lookback for large purchases | Overcommitment risk | Use 30 or 60-day lookback for commitments over $1K/month | +| Ignoring pending migrations | Stranded commitments | Check with infrastructure teams before purchasing | +| No renewal governance | Expired commitments, lost savings | Set calendar reminders 30 days before expiry | +| Purchasing without checking existing commitments | Double coverage, wastage | Always check current utilization before new purchases | + +--- + +## Prerequisites + +- Understanding of current Azure spend patterns (30+ days of data) +- Access to Benefit Recommendations API (Cost Management Reader role) +- Knowledge of planned workload changes +- Stakeholder alignment on commitment term and risk tolerance + +--- + +## References + +- [Azure savings plan overview](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/savings-plan-compute-overview) +- [Azure Reservations overview](https://learn.microsoft.com/azure/cost-management-billing/reservations/save-compute-costs-reservations) +- [Decide between a savings plan and a reservation](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/decide-between-savings-plan-reservation) +- [Rate optimization (FinOps Framework)](https://learn.microsoft.com/cloud-computing/finops/framework/optimize/rates) +- [Choose commitment amount](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/choose-commitment-amount) +- [Benefit Recommendations API](https://learn.microsoft.com/rest/api/cost-management/benefit-recommendations) +- [Reservation trade-in to savings plans](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/reservation-trade-in) +- [Exchange and refund policies](https://learn.microsoft.com/azure/cost-management-billing/reservations/exchange-and-refund-azure-reservations) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-cost-exports.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-cost-exports.md new file mode 100644 index 000000000..82401e291 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-cost-exports.md @@ -0,0 +1,167 @@ +--- +name: Azure Cost Management Exports +description: Cost Management exports automatically export cost and usage data to Azure Storage on a recurring schedule. Exports are the foundation for FinOps data pipelines for historical cost analysis. +--- + +## Supported Scopes + +| Agreement | Scope | Format | Recommended | +|-----------|-------|--------|-------------| +| **EA** | Billing Account | `/providers/Microsoft.Billing/billingAccounts/{enrollmentId}` | ✅ | +| **EA** | Department | `/providers/Microsoft.Billing/billingAccounts/{enrollmentId}/departments/{deptId}` | | +| **EA** | Enrollment Account | `/providers/Microsoft.Billing/billingAccounts/{enrollmentId}/enrollmentAccounts/{accountId}` | | +| **MCA** | Billing Profile | `/providers/Microsoft.Billing/billingAccounts/{accountId}/billingProfiles/{profileId}` | ✅ | +| **MCA** | Invoice Section | `...billingProfiles/{profileId}/invoiceSections/{sectionId}` | | +| **MPA** | Customer | `/providers/Microsoft.Billing/billingAccounts/{accountId}/customers/{customerId}` | | +| All | Subscription | `/subscriptions/{subscriptionId}` | | +| All | Resource Group | `/subscriptions/{subId}/resourceGroups/{rgName}` | | + +**Why billing scope?** Billing account (EA) and billing profile (MCA) include price sheets, reservation recommendations, and complete cost visibility across all subscriptions. + +## Workflow: Create Export at Billing Scope + +### Step 1: List Existing Exports + +```bash +# EA Billing Account scope +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{enrollmentId}/providers/Microsoft.CostManagement/exports?api-version=2023-08-01" \ + --query "value[].{Name:name, Type:properties.definition.type, Status:properties.schedule.status}" \ + -o table + +# MCA Billing Profile scope +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{accountId}/billingProfiles/{profileId}/providers/Microsoft.CostManagement/exports?api-version=2023-08-01" \ + --query "value[].{Name:name, Type:properties.definition.type, Status:properties.schedule.status}" \ + -o table +``` + +### Step 2: Create Export via REST API + +```bash +# EA Billing Account - FOCUS cost export +SCOPE="/providers/Microsoft.Billing/billingAccounts/{enrollmentId}" + +az rest --method PUT \ + --url "https://management.azure.com${SCOPE}/providers/Microsoft.CostManagement/exports/cost-export-daily?api-version=2023-08-01" \ + --body '{ + "properties": { + "format": "Parquet", + "deliveryInfo": { + "destination": { + "resourceId": "/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}", + "container": "msexports", + "rootFolderPath": "billingAccounts/{enrollmentId}" + } + }, + "definition": { + "type": "FocusCost", + "timeframe": "MonthToDate", + "dataSet": { + "granularity": "Daily" + } + }, + "schedule": { + "status": "Active", + "recurrence": "Daily", + "recurrencePeriod": { + "from": "2026-01-01T00:00:00Z", + "to": "2027-12-31T00:00:00Z" + } + } + } + }' +``` + +### Step 3: Run Export Immediately + +```bash +# Trigger export run +az rest --method POST \ + --url "https://management.azure.com${SCOPE}/providers/Microsoft.CostManagement/exports/cost-export-daily/run?api-version=2023-08-01" +``` + +### Step 4: Verify Data Landed + +```bash +# List blobs in export container +az storage blob list \ + --account-name {storageAccount} \ + --container-name msexports \ + --prefix "billingAccounts/{enrollmentId}" \ + --auth-mode login \ + --query "[].{Name:name, Size:properties.contentLength}" \ + -o table +``` + +## Workflow: Historical Backfill + +### REST API (One Month at a Time) + +```bash +SCOPE="/providers/Microsoft.Billing/billingAccounts/{enrollmentId}" + +az rest --method PUT \ + --url "https://management.azure.com${SCOPE}/providers/Microsoft.CostManagement/exports/backfill-jan2025?api-version=2023-08-01" \ + --body '{ + "properties": { + "format": "Parquet", + "deliveryInfo": { + "destination": { + "resourceId": "/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}", + "container": "msexports", + "rootFolderPath": "backfill" + } + }, + "definition": { + "type": "FocusCost", + "timeframe": "Custom", + "timePeriod": { + "from": "2025-01-01T00:00:00Z", + "to": "2025-01-31T23:59:59Z" + } + } + } + }' + +# Run the backfill +az rest --method POST \ + --url "https://management.azure.com${SCOPE}/providers/Microsoft.CostManagement/exports/backfill-jan2025/run?api-version=2023-08-01" +``` + +## Dataset Types by Scope + +| Dataset | EA Billing | MCA Profile | Subscription | +|---------|------------|-------------|--------------| +| **FocusCost** | ✅ | ✅ | ✅ | +| **ActualCost** | ✅ | ✅ | ✅ | +| **AmortizedCost** | ✅ | ✅ | ✅ | +| **PriceSheet** | ✅ | ✅ | ❌ | +| **ReservationRecommendations** | ✅ | ✅ | ❌ | +| **ReservationDetails** | ✅ | ✅ | ❌ | +| **ReservationTransactions** | ✅ | ✅ | ❌ | + +## Common Issues + +### "Unauthorized" Error +**Fix:** Assign Cost Management Contributor or Owner role at the billing scope + +### Export Created But No Data +**Fix:** Trigger immediate run: +```bash +az rest --method POST \ + --url "https://management.azure.com/{scope}/providers/Microsoft.CostManagement/exports/{exportName}/run?api-version=2023-08-01" +``` + +## Best Practices + +1. **Use billing scope** (EA enrollment / MCA profile) for complete data including price sheets +2. **Use FOCUS format** - combines actual/amortized, reduces storage +3. **Use Parquet with Snappy** compression for best performance +4. **Create backfill in one-month chunks** to avoid timeouts + +## References + +- [Tutorial: Create and manage exports](https://learn.microsoft.com/azure/cost-management-billing/costs/tutorial-improved-exports) +- [Exports REST API](https://learn.microsoft.com/rest/api/cost-management/exports) +- [Understand and work with scopes](https://learn.microsoft.com/azure/cost-management-billing/costs/understand-work-scopes) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-credits.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-credits.md new file mode 100644 index 000000000..618068df4 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-credits.md @@ -0,0 +1,163 @@ +--- +name: Azure Credits (Prepayment) +description: Azure Prepayment (formerly Monetary Commitment) provides prepaid funds that cover eligible Azure services. Credits have **expiration dates** - unused credits are lost. Credits may also be granted via promotions or strategic investments. +--- + +**Key distinction from MACC:** +- **Azure Prepayment**: Prepaid credits covering eligible services - consumption covered by prepayment does NOT count toward MACC +- **MACC**: Contractual commitment tracking total consumption + +## How Credits Are Applied + +Credits are automatically applied to eligible charges when an invoice is generated. The remaining balance after credits is paid via other payment methods. + +**Products NOT covered by credits:** +- Azure Marketplace products +- Azure support plans +- Canonical products (Ubuntu Pro) +- Citrix Virtual Apps and Desktops +- Visual Studio subscriptions (Monthly/Annual) + +## Workflow: Assess Credit Health + +### Step 1: Identify Agreement Type + +EA and MCA use different APIs. Check agreement type first: + +```bash +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts?api-version=2020-05-01" \ + --query "value[].{Name:name, Type:properties.agreementType}" +``` + +### Step 2: Get Current Balance + +**For EA:** +```bash +CURRENT_PERIOD=$(date +%Y%m) +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//billingPeriods/${CURRENT_PERIOD}/providers/Microsoft.Consumption/balances?api-version=2024-08-01" \ + --query "{Beginning:properties.beginningBalance, Ending:properties.endingBalance, Utilized:properties.utilized, Overage:properties.serviceOverage}" +``` + +**For MCA:** +```bash +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//billingProfiles//providers/Microsoft.Consumption/lots?api-version=2023-03-01" \ + --query "value[?properties.status=='Active'].{Source:properties.source, Original:properties.originalAmount.value, Remaining:properties.closedBalance.value, Expires:properties.expirationDate}" +``` + +### Step 3: Check Expiration Risk + +Query credits expiring in next 90 days: + +```bash +# MCA - find expiring credits +NINETY_DAYS=$(date -v+90d +%Y-%m-%d) # macOS +# NINETY_DAYS=$(date -d "+90 days" +%Y-%m-%d) # Linux + +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//billingProfiles//providers/Microsoft.Consumption/lots?api-version=2023-03-01" \ + --query "value[?properties.status=='Active'].{Source:properties.source, Balance:properties.closedBalance.value, Expires:properties.expirationDate}" | \ + jq --arg date "$NINETY_DAYS" '[.[] | select(.Expires <= $date)]' +``` + +### Step 4: Assess Risk Level + +| Situation | Risk | Action | +|-----------|------|--------| +| No credits expiring in 90 days | Low | Monitor quarterly | +| Credits expiring, balance < monthly consumption | Low | Will be consumed naturally | +| Credits expiring, balance > monthly consumption | High | Accelerate consumption or lose credits | +| Credits already expired | Loss | Review for future prevention | + +## EA Balance API + +Query balance for a specific billing period (YYYYMM format): + +```bash +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//billingPeriods//providers/Microsoft.Consumption/balances?api-version=2024-08-01" +``` + +**Response fields:** + +| Field | Description | +|-------|-------------| +| `beginningBalance` | Starting Azure Prepayment balance for the month | +| `endingBalance` | Remaining balance (updated daily for open periods) | +| `newPurchases` | New Azure Prepayment purchases during month | +| `adjustments` | Total adjustment amount | +| `adjustmentDetails[]` | Array of credit types and amounts | +| `utilized` | Amount of Azure Prepayment consumed | +| `serviceOverage` | Overage for Azure services | +| `chargesBilledSeparately` | Charges billed separately from Prepayment | +| `azureMarketplaceServiceCharges` | Total Marketplace charges | +| `currency` | ISO currency code (e.g., USD) | + +**Credit types in adjustmentDetails:** + +| Credit Type | Description | +|-------------|-------------| +| `Promo Credit` | Promotional credits | +| `Strategic Investment Credit` | Microsoft investment credits | +| `Billing Correction Credit` | Billing adjustments | +| `Reservations - Exchange Credit` | RI exchange credits | + +## MCA Credit Lots API + +Query credit lots for a billing profile: + +```bash +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//billingProfiles//providers/Microsoft.Consumption/lots?api-version=2023-03-01" +``` + +**Response fields:** + +| Field | Description | +|-------|-------------| +| `originalAmount` | Original credit amount | +| `closedBalance` | Remaining credit balance (as of last invoice) | +| `source` | Credit source (e.g., "Azure Promotional Credit") | +| `startDate` | When credit became active | +| `expirationDate` | When credit expires | +| `poNumber` | PO number of invoice where credit was billed | + +## MCA Credit Events API + +Query credit transactions over time: + +```bash +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//billingProfiles//providers/Microsoft.Consumption/events?api-version=2023-03-01&startDate=YYYY-MM-DD&endDate=YYYY-MM-DD" +``` + +**Response fields:** + +| Field | Description | +|-------|-------------| +| `transactionDate` | When transaction occurred | +| `description` | Description of the transaction | +| `newCredit` | New credits added | +| `adjustments` | Credit adjustments | +| `creditExpired` | Credits that expired | +| `charges` | Charges applied against credits | +| `closedBalance` | Balance after transaction | +| `eventType` | PendingCharges, SettledCharges, PendingNewCredit, etc. | +| `invoiceNumber` | Invoice number (empty for pending) | + +## Important Notes + +1. **EA vs MCA**: Query patterns differ significantly - verify agreement type first +2. **Billing period format**: EA uses YYYYMM (e.g., 202207 for July 2022) +3. **Credit expiration**: Unused credits expire and cannot be recovered +4. **Overage**: When credits exhausted, charges appear as `serviceOverage` (EA) or standard invoiced charges (MCA) +5. **Permissions**: Requires billing account reader or billing profile reader role + +## References + +- [View Azure credits balance (EA)](https://learn.microsoft.com/azure/cost-management-billing/manage/ea-portal-enrollment-invoices#view-enrollment-credit-balance) +- [Track Azure credit balance (MCA)](https://learn.microsoft.com/azure/cost-management-billing/manage/mca-check-azure-credits-balance) +- [Consumption Balances API](https://learn.microsoft.com/rest/api/consumption/balances) +- [Consumption Lots API](https://learn.microsoft.com/rest/api/consumption/lots) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-macc.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-macc.md new file mode 100644 index 000000000..0a837de6f --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-macc.md @@ -0,0 +1,131 @@ +--- +name: Azure MACC (Microsoft Azure Consumption Commitment) +description: MACC is a contractual commitment to spend a specific amount on Azure over a defined period (typically 3-5 years). Missing the commitment results in a **shortfall charge** - an invoice for the remaining balance converted to Azure Prepayment credit. +--- + +## Eligibility Rules + +Understanding what counts toward MACC is critical to avoid surprises. + +## Contract Evidence Boundary + +MACC is a billing-account contractual spending agreement. It is not an Azure Reservation, savings plan, capacity reservation, or commitment-discount metric. + +- Use the Consumption Lots API with `source=ConsumptionCommitment` for the contractual amount, remaining balance, dates, and status. +- Use the Consumption Events API with `lotSource=ConsumptionCommitment` for invoiced decrements and resulting balances. +- Do not use Cost Management cost queries, reservation or savings-plan purchases, recommendations, utilization, coverage, or waste to calculate or infer MACC amount, remaining balance, decrement, burn, progress, or status. +- If Lots and Events evidence is unavailable, report `INSUFFICIENT MACC EVIDENCE`. + +**Counts toward MACC:** +- Direct Azure consumption billed to your account +- Azure Prepayment purchases (the purchase itself, not consumption from it) +- Azure Marketplace offers with "Azure benefit eligible" badge (100% of pretax amount) + +**Does NOT count toward MACC:** +- Consumption covered by Azure Prepayment credits +- Consumption covered by shortfall credits (the trap!) +- Marketplace offers without the eligible badge +- Purchases not linked to your billing account +- Hybrid/on-premises license usage + +**The shortfall trap:** If MACC is missed, the shortfall becomes Prepayment credit. Consumption against that credit does NOT count toward any future MACC commitment. + +## Workflow: Assess MACC Status + +### Step 1: Get Current Position + +Query the Lots API for active commitments: + +```bash +# List billing accounts first +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts?api-version=2020-05-01" \ + --query "value[].{Name:name, DisplayName:properties.displayName}" + +# Get MACC commitments (replace ) +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//providers/Microsoft.Consumption/lots?api-version=2021-05-01&\$filter=source%20eq%20%27ConsumptionCommitment%27" \ + --query "value[?properties.status=='Active'].{Original:properties.originalAmount.value, Remaining:properties.closedBalance.value, StartDate:properties.startDate, EndDate:properties.expirationDate}" +``` + +**Key fields:** +- `originalAmount` - Total commitment amount +- `closedBalance` - Remaining balance as of last invoice +- `purchasedDate` - When MACC was purchased +- `startDate` - When MACC became active +- `expirationDate` - Deadline +- `status` - Active, Completed, Expired, or Canceled + +### Step 2: Calculate Burn Rate + +Query recent decrement events: + +```bash +# Get decrements for last 6 months (adjust dates) +az rest --method GET \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts//providers/Microsoft.Consumption/events?api-version=2021-05-01&startDate=2024-07-01&endDate=2025-01-01&\$filter=lotsource%20eq%20%27ConsumptionCommitment%27" \ + --query "value[].{Date:properties.transactionDate, Decrement:properties.charges.value, Remaining:properties.closedBalance.value, Invoice:properties.invoiceNumber}" +``` + +**Event fields:** +- `transactionDate` - When event occurred +- `description` - Description of the event +- `charges` - MACC decrement amount +- `closedBalance` - Remaining balance after event +- `invoiceNumber` - Invoice that triggered the decrement +- `eventType` - SettledCharges (only type for MACC) +- `billingProfileDisplayName` - Billing profile name (MCA only) + +Calculate average monthly decrement from results. + +### Step 3: Assess Risk + +``` +Required Monthly Rate = closedBalance ÷ Months Until Expiration +Actual Monthly Rate = Sum of decrements ÷ Number of months +``` + +| Situation | Risk Level | Action | +|-----------|------------|--------| +| Actual > Required × 1.1 | Low | Monitor quarterly | +| Actual within ±10% of Required | Medium | Monitor monthly | +| Actual < Required × 0.9 | High | Acceleration needed | + +### Step 4: Check for Milestones + +Some MACCs have interim milestones with their own deadlines and shortfall penalties. Check the Azure portal: **Cost Management + Billing → Credits + Commitments → MACC → Milestones tab**. + +Milestones are not exposed via API - use portal to verify. + +## Accelerating MACC Burn + +When behind on commitment: + +1. **Accelerate planned projects** - Move deployments forward +2. **Review eligible Azure services and Marketplace offers** - Validate eligibility against the MACC billing-account experience +3. **Azure Marketplace** - Find "Azure benefit eligible" solutions +4. **Contact Microsoft** - Discuss commitment amendments + +## Alerts + +Microsoft automatically emails Billing Account Admins: +- 90 days before MACC expiration +- 60 days before MACC expiration +- 30 days before MACC expiration +- Same intervals for milestone deadlines + +No configuration needed - alerts are automatic for accounts not on track. + +## Prerequisites + +- **EA:** Enterprise Administrator role required +- **MCA:** Owner, Contributor, or Reader on billing account +- **Direct agreements only** - Indirect (partner) agreements cannot use portal/API + +## References + +- [Track your MACC](https://learn.microsoft.com/azure/cost-management-billing/manage/track-consumption-commitment) - Portal and API guidance +- [MACC FAQ](https://learn.microsoft.com/marketplace/macc-frequently-asked-questions) - Marketplace eligibility details +- [Azure benefit eligible offers](https://learn.microsoft.com/marketplace/azure-consumption-commitment-benefit) - Finding eligible Marketplace solutions +- [Consumption Lots API](https://learn.microsoft.com/rest/api/consumption/lots) - API reference +- [Consumption Events API](https://learn.microsoft.com/rest/api/consumption/events) - API reference diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-orphaned-resources.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-orphaned-resources.md new file mode 100644 index 000000000..100aeafde --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-orphaned-resources.md @@ -0,0 +1,329 @@ +--- +name: Azure Orphaned Resources +description: Azure Resource Graph queries to detect unused and orphaned resources generating waste with zero workload value. Covers unattached disks, unused NICs, orphaned public IPs, idle load balancers, empty availability sets, orphaned NSGs, idle NAT gateways, and stale snapshots with safe cleanup patterns. +--- + +**Key Features:** +- Resource Graph queries for 8 orphaned resource types +- Immediate savings with zero performance risk +- Safe cleanup patterns with `-WhatIf` / `--dry-run` +- Cost estimation guidance per resource type +- Automation via Azure Policy and scheduled queries + +--- + +## Why orphaned resources matter + +Orphaned resources are Azure resources that remain provisioned after the workloads they supported are deleted, scaled down, or reconfigured. They generate charges with zero workload value — pure waste. Common causes: VM deletions that leave disks and NICs behind, IP address releases that don't clean up the public IP, and load balancers left after AKS cluster teardown. + +These are the lowest-risk cost optimization opportunities because removing them has no impact on running workloads. + +--- + +## Prerequisites + +- Azure CLI (`az login`) or Azure PowerShell (`Connect-AzAccount`) +- **Reader** role on target subscriptions +- Azure Resource Graph access (enabled by default for all Microsoft Entra ID users) + +--- + +## Detection queries + +### Unattached managed disks + +Disks in `Unattached` state are not connected to any VM. Typical monthly cost: $1.54–$122.88/disk depending on tier and size. + +```bash +az graph query -q " +resources +| where type == 'microsoft.compute/disks' +| where properties.diskState == 'Unattached' +| project name, resourceGroup, subscriptionId, + sku = properties.sku.name, + sizeGb = properties.diskSizeGB, + location, + timeCreated = properties.timeCreated +| order by sizeGb desc +" --first 1000 +``` + +### Unused network interfaces + +NICs not attached to any VM. Created during VM provisioning and left behind on deletion. + +```bash +az graph query -q " +resources +| where type == 'microsoft.network/networkinterfaces' +| where isnull(properties.virtualMachine) +| where isnull(properties.virtualMachineScaleSet) +| where isnull(properties.privateEndpoint) +| project name, resourceGroup, subscriptionId, location, + privateIp = properties.ipConfigurations[0].properties.privateIPAddress +" --first 1000 +``` + +**Note:** NICs attached to private endpoints (`properties.privateEndpoint != null`) or VMSS instances (`properties.virtualMachineScaleSet != null`) are not orphaned — both are excluded. + +### Orphaned public IP addresses + +Public IPs not associated with any resource. Standard SKU public IPs cost ~$3.65/month even when idle. + +```bash +az graph query -q " +resources +| where type == 'microsoft.network/publicipaddresses' +| where isnull(properties.ipConfiguration) +| where isnull(properties.natGateway) +| project name, resourceGroup, subscriptionId, location, + sku = sku.name, + ipAddress = properties.ipAddress, + allocationMethod = properties.publicIPAllocationMethod +" --first 1000 +``` + +### Idle NAT gateways + +NAT gateways with no associated subnets. Charged at ~$32.85/month plus data processing fees. + +```bash +az graph query -q " +resources +| where type == 'microsoft.network/natgateways' +| where isnull(properties.subnets) or array_length(properties.subnets) == 0 +| project name, resourceGroup, subscriptionId, location +" --first 1000 +``` + +### Orphaned snapshots + +Snapshots where the source disk no longer exists. Filter to snapshots older than 30 days to avoid catching in-progress operations. + +```bash +az graph query -q " +resources +| where type == 'microsoft.compute/snapshots' +| where todatetime(properties.timeCreated) < ago(30d) +| extend sourceDisk = tostring(properties.creationData.sourceResourceId) +| where not(sourceDisk has '/snapshots/') +| join kind=leftanti ( + resources + | where type == 'microsoft.compute/disks' + | project id +) on \$left.sourceDisk == \$right.id +| project name, resourceGroup, subscriptionId, location, + sizeGb = properties.diskSizeGB, + created = properties.timeCreated, + sourceDisk +| order by sizeGb desc +" --first 1000 +``` + +### Idle load balancers + +Load balancers with empty backend pools — no VMs or VMSSes behind them. + +```bash +az graph query -q " +resources +| where type == 'microsoft.network/loadbalancers' +| where isnull(properties.backendAddressPools) + or array_length(properties.backendAddressPools) == 0 +| project name, resourceGroup, subscriptionId, location, + sku = sku.name +" --first 1000 +``` + +**Note:** Standard SKU load balancers cost ~$18.25/month when idle. Basic SKU load balancers are free but should still be cleaned up. + +### Empty availability sets + +Availability sets with no VMs. No direct cost, but they clutter the environment and may prevent resource cleanup. + +```bash +az graph query -q " +resources +| where type == 'microsoft.compute/availabilitysets' +| where isnull(properties.virtualMachines) or array_length(properties.virtualMachines) == 0 +| project name, resourceGroup, subscriptionId, location +" --first 1000 +``` + +### Orphaned network security groups + +NSGs not attached to any NIC or subnet. No direct cost, but they add management overhead and security audit noise. + +```bash +az graph query -q " +resources +| where type == 'microsoft.network/networksecuritygroups' +| where isnull(properties.networkInterfaces) or array_length(properties.networkInterfaces) == 0 +| where isnull(properties.subnets) or array_length(properties.subnets) == 0 +| project name, resourceGroup, subscriptionId, location, + rulesCount = array_length(properties.securityRules) +" --first 1000 +``` + +--- + +## Cost estimation by resource type + +| Resource Type | Typical Monthly Cost | Detection Confidence | +|--------------|---------------------|---------------------| +| Managed disks (unattached) | $1.54–$122.88/disk (varies by SKU tier) | High — `Unattached` is definitive; `Reserved` state disks (temporarily held during VM provisioning) are excluded | +| Public IPs (Standard SKU) | ~$3.65/IP | High — no `ipConfiguration` is definitive | +| NAT gateways (idle) | ~$32.85 + data fees | High — no subnets is definitive | +| Load balancers (Standard, empty) | ~$18.25/LB | High — empty backend pools | +| Snapshots (orphaned) | $0.05/GB/month | Medium — source disk deleted but snapshot may be intentional backup; snapshot-to-snapshot chains are excluded | +| NICs (unused) | Free (but blocks cleanup) | Medium — check for pending VM deployments | +| Availability sets (empty) | Free (clutter) | High — no VMs attached | +| NSGs (orphaned) | Free (audit noise) | Medium — may be pre-provisioned for deployment templates | + +--- + +## Safe cleanup patterns + +### PowerShell with -WhatIf + +```powershell +# Preview disk cleanup (dry run) +$disks = Search-AzGraph -Query " +resources +| where type == 'microsoft.compute/disks' +| where properties.diskState == 'Unattached' +| project name, resourceGroup, subscriptionId +" + +foreach ($disk in $disks) { + Remove-AzDisk -ResourceGroupName $disk.resourceGroup ` + -DiskName $disk.name -WhatIf +} + +# Execute cleanup (remove -WhatIf) +foreach ($disk in $disks) { + Remove-AzDisk -ResourceGroupName $disk.resourceGroup ` + -DiskName $disk.name -Force +} +``` + +### Azure CLI with --dry-run + +Azure CLI `delete` commands do not have a `--dry-run` flag. Instead, list first and review before deleting: + +```bash +# List orphaned public IPs (review step) +az graph query -q " +resources +| where type == 'microsoft.network/publicipaddresses' +| where isnull(properties.ipConfiguration) +| where isnull(properties.natGateway) +| project name, resourceGroup, subscriptionId +" --first 1000 -o table + +# Delete after review (per resource) +az network public-ip delete --name --resource-group +``` + +### Bulk cleanup script + +> **Confirm before running.** Always show the list of resources to the user and get explicit approval before executing. + +```powershell +# Bulk delete unattached disks across subscriptions +$disks = Search-AzGraph -Query " +resources +| where type == 'microsoft.compute/disks' +| where properties.diskState == 'Unattached' +| project name, resourceGroup, subscriptionId +" -First 1000 + +# Show what will be deleted and confirm +Write-Host "Found $($disks.Count) unattached disk(s):" +$disks | Format-Table name, resourceGroup, subscriptionId -AutoSize + +$confirm = Read-Host "Delete all $($disks.Count) disk(s)? Type 'yes' to confirm" +if ($confirm -ne 'yes') { + Write-Host "Aborted." + return +} + +$totalRemoved = 0 +foreach ($disk in $disks) { + try { + Set-AzContext -Subscription $disk.subscriptionId -ErrorAction Stop | Out-Null + Remove-AzDisk -ResourceGroupName $disk.resourceGroup ` + -DiskName $disk.name -Force -ErrorAction Stop + $totalRemoved++ + } catch { + Write-Warning "Skipping $($disk.name) in $($disk.subscriptionId): $_" + } +} +Write-Host "Removed $totalRemoved of $($disks.Count) unattached disks" +``` + +--- + +## Automation + +### Azure Policy (audit mode) + +Use built-in Azure Policy definitions to audit orphaned resources: + +| Policy | Description | +|--------|-------------| +| `Audit unattached managed disks` | Flags disks with `diskState == Unattached` | +| `Audit unused public IP addresses` | Flags public IPs with no association | + +Deploy at management group scope for enterprise-wide coverage. + +### Scheduled Resource Graph queries + +Use Azure Automation or Logic Apps to run detection queries on a weekly schedule and send results via email or Teams webhook: + +```powershell +# Azure Automation runbook pattern +param ( + [Parameter(Mandatory = $true)] + [string]$TeamsWebhookUrl +) + +$orphanedDisks = Search-AzGraph -Query " +resources +| where type == 'microsoft.compute/disks' +| where properties.diskState == 'Unattached' +| summarize Count = count() +" + +if ($orphanedDisks.Count -gt 0) { + # NOTE: Actual cost depends on disk SKU tier (Standard HDD ~$0.045/GB, Premium SSD ~$0.17-$0.38/GB). + # Use references/azure-retail-prices.md for per-SKU pricing validation. + $body = @{ + title = "Orphaned Resource Alert" + text = "$($orphanedDisks.Count) unattached disks found. Review and clean up to reduce waste." + } | ConvertTo-Json + + Invoke-RestMethod -Uri $TeamsWebhookUrl -Method Post -Body $body -ContentType 'application/json' +} +``` + +--- + +## Permissions + +| Action | Required Role | +|--------|---------------| +| Run Resource Graph queries | Reader | +| Delete resources | Contributor on resource group | +| Deploy Azure Policy | Resource Policy Contributor | +| Azure Automation runbooks | Automation Contributor | + +--- + +## References + +- [Azure Resource Graph overview](https://learn.microsoft.com/azure/governance/resource-graph/overview) +- [Azure Resource Graph query samples](https://learn.microsoft.com/azure/governance/resource-graph/samples/starter) +- [Azure Policy built-in definitions](https://learn.microsoft.com/azure/governance/policy/samples/built-in-policies) +- [Manage unattached disks](https://learn.microsoft.com/azure/virtual-machines/windows/find-unattached-disks) +- [Workload optimization (FinOps Framework)](https://learn.microsoft.com/cloud-computing/finops/framework/optimize/workloads) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-reservations.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-reservations.md new file mode 100644 index 000000000..90f35aff0 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-reservations.md @@ -0,0 +1,353 @@ +--- +name: Azure Reservations +description: Query the Azure Cost Management Benefit Recommendations API to retrieve reserved instance purchase recommendations. Analyze potential savings, utilization, coverage, and optimal commitment amounts for specific Azure resource types. +--- + +**Key Features:** +- Up to 72% savings vs pay-as-you-go pricing for stable workloads +- Resource-type specific (VMs, SQL DB, Cosmos DB, App Service, Synapse, Storage, etc.) +- Instance size flexibility within VM series +- Self-service returns (up to $50K/year at time of writing; see [current limits](https://learn.microsoft.com/azure/cost-management-billing/reservations/exchange-and-refund-azure-reservations)) and exchanges within product family +- 1-year and 3-year terms +- Applied before savings plans in the benefit stack + +--- + +## Benefit Recommendations API + +The same Benefit Recommendations API endpoint used for savings plans also returns reservation recommendations. The key difference is the `kind` filter parameter. + +> **Authentication note:** Use `az rest` in practice — it handles token acquisition automatically. The raw HTTP examples in this file are for documentation purposes only. + +### Request + +```http +GET https://management.azure.com/{billingScope}/providers/Microsoft.CostManagement/benefitRecommendations?$filter=properties/lookBackPeriod eq '{lookBackPeriod}' AND properties/term eq '{term}'&$expand=properties/usage,properties/allRecommendationDetails&api-version=2024-08-01 +Authorization: Bearer {token} +``` + +When no `kind` filter is specified, the API returns both savings plan and reservation recommendations. The `kind` property is at the top level of each result (not under `properties`), so filter client-side: + +```powershell +# Filter API results for reservations only +$reservations = $jsonResult.value | Where-Object { $_.kind -eq 'Reservation' } +``` + +**Note:** The documented `$filter` query parameters support `properties/lookBackPeriod`, `properties/term`, `properties/scope`, `properties/subscriptionId`, and `properties/resourceGroup`. Filtering by `kind` is not a documented server-side filter — do it client-side after retrieving results. + +### Parameters + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `billingScope` | Yes | - | Billing account, subscription, or resource group scope | +| `lookBackPeriod` | No | Last7Days | Analysis period: Last7Days, Last30Days, Last60Days | +| `term` | No | P3Y | Reservation term: P1Y (1-year) or P3Y (3-year) | +| `kind` | No | - | Top-level property on results: `Reservation` or `SavingsPlan`. Filter client-side (not a supported `$filter` param). | + +### Scope formats + +| Scope Type | Format | +|------------|--------| +| Billing Account | `providers/Microsoft.Billing/billingAccounts/{billingAccountId}` | +| Billing Profile | `providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}` | +| Subscription | `subscriptions/{subscriptionId}` | +| Resource Group | `subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}` | + +--- + +## PowerShell examples + +The `Get-BenefitRecommendations.ps1` script (see `azure-savings-plans.md` for full source) works for both savings plans and reservations. Filter the results for reservation recommendations: + +### Get reservation recommendations only + +```powershell +# Get all benefit recommendations and parse JSON +$scope = "subscriptions/12345678-1234-1234-1234-123456789012" +$url = "https://management.azure.com/$scope/providers/Microsoft.CostManagement/benefitRecommendations?`$filter=properties/lookBackPeriod eq 'Last30Days' AND properties/term eq 'P3Y'&`$expand=properties/usage,properties/allRecommendationDetails&api-version=2024-08-01" +$uri = [uri]::new($url) +$result = Invoke-AzRestMethod -Uri $uri.AbsoluteUri -Method GET +$jsonResult = $result.Content | ConvertFrom-Json + +# Filter for reservation recommendations only (kind is top-level, not under properties) +$reservations = $jsonResult.value | Where-Object { $_.kind -eq 'Reservation' } + +# Display summary +$reservations | ForEach-Object { + $rec = $_.properties + Write-Host "ARM SKU: $($rec.armSkuName)" + Write-Host " Commitment: $($rec.recommendationDetails.commitmentAmount)/hr" + Write-Host " Savings: $($rec.recommendationDetails.savingsPercentage)%" + Write-Host " Term: $($rec.term)" + Write-Host "" +} +``` + +### Compare reservation vs savings plan recommendations + +```powershell +$scope = "subscriptions/$subscriptionId" +$url = "https://management.azure.com/$scope/providers/Microsoft.CostManagement/benefitRecommendations?`$filter=properties/lookBackPeriod eq 'Last30Days' AND properties/term eq 'P3Y'&`$expand=properties/allRecommendationDetails&api-version=2024-08-01" +$uri = [uri]::new($url) +$result = Invoke-AzRestMethod -Uri $uri.AbsoluteUri -Method GET +$all = ($result.Content | ConvertFrom-Json).value + +$reservationSavings = ($all | Where-Object { $_.kind -eq 'Reservation' } | + Measure-Object -Property { $_.properties.recommendationDetails.savingsAmount } -Sum).Sum + +$savingsPlanSavings = ($all | Where-Object { $_.kind -eq 'SavingsPlan' } | + Measure-Object -Property { $_.properties.recommendationDetails.savingsAmount } -Sum).Sum + +Write-Host "Total reservation savings: `$$reservationSavings" +Write-Host "Total savings plan savings: `$$savingsPlanSavings" +``` + +--- + +## Eligible resource types + +| Resource type | Flexibility | Notes | +|---------------|-------------|-------| +| Virtual machines | Instance size flexibility within series | Ratio-based application across sizes in the same series | +| Azure SQL Database | vCore-based | Applies to General Purpose and Business Critical tiers | +| Azure Cosmos DB | Throughput (RU/s) | Provisioned throughput reservations | +| App Service | Isolated v2 stamps | Isolated tier only | +| Azure Synapse Analytics | Data warehouse units | Compute reservations | +| Azure Managed Disks | Premium SSD capacity | Specific disk sizes | +| Azure Blob Storage | Reserved capacity | Hot, cool, and archive access tiers | +| Azure Files | Reserved capacity | Premium file shares | +| Azure Data Explorer | Markup units | Compute reservations | +| Azure VMware Solution | Node reservations | Host-level reservations | +| Red Hat plans | Software plans | RHEL VMs | +| SUSE plans | Software plans | SLES VMs | +| Azure Databricks | DBU commitments | Pre-purchase plans | + +--- + +## Scope and flexibility + +### Scope options + +| Scope | Description | +|-------|-------------| +| Shared | Applies across all subscriptions in the billing context (maximum flexibility) | +| Single subscription | Applies only to resources in one subscription | +| Single resource group | Applies only to resources in one resource group | +| Management group | Applies across subscriptions in a management group | + +### Instance size flexibility + +Within a VM series (e.g., D-series), a reservation for one size automatically applies to other sizes in the same series using a ratio-based approach. + +Example for D-series: + +| VM Size | Ratio | +|---------|-------| +| Standard_D1 | 1 | +| Standard_D2 | 2 | +| Standard_D4 | 4 | +| Standard_D8 | 8 | + +A reservation for one Standard_D4 (ratio 4) can cover four Standard_D1 instances (ratio 1 each) or two Standard_D2 instances (ratio 2 each). + +**Important:** Reservations are region-specific. A reservation purchased for East US does not apply to resources in West US. + +--- + +## Exchange and return policy + +| Action | Policy | +|--------|--------| +| Returns | Self-service cancellation with prorated refund. Up to $50,000 USD (or equivalent) in returns per rolling 12-month window. Early termination fee is not currently charged. | +| Exchanges | Within the same product family only. Prorated value applied to new reservation. Exchange refunds do NOT count against the $50K return limit. | +| Trade-in | Existing reservations can be traded in for savings plans via self-service (no time limit). | + +### Compute reservation exchange grace period + +For compute reservations (VMs, Dedicated Host, App Service), cross-series and cross-region exchanges are currently allowed under an extended grace period "until further notice." Microsoft will provide at least 6 months advance notice before ending this grace period. After it ends, compute reservation exchanges will be limited to within the same instance size flexibility group only. + +### Reservation types that cannot be exchanged or refunded + +Azure Databricks, Synapse Analytics Pre-purchase, Red Hat plans, SUSE Linux plans, Microsoft Defender for Cloud Pre-Purchase, and Microsoft Sentinel Pre-Purchase. + +**Important:** These policies differ significantly from savings plans, which have no cancellation, return, or exchange option. Consider reservation trade-in to savings plans as an alternative exit path. + +### Calculate refund for a specific reservation + +```bash +# Calculate the refund amount for a specific reservation return (not the aggregate $50K balance) +az rest --method POST \ + --url "https://management.azure.com/providers/Microsoft.Capacity/calculateRefund?api-version=2022-11-01" \ + --body '{ + "id": "/providers/Microsoft.Capacity/reservationOrders/{reservationOrderId}", + "properties": { + "scope": "Reservation", + "reservationToReturn": { + "reservationId": "/providers/Microsoft.Capacity/reservationOrders/{orderId}/reservations/{reservationId}", + "quantity": 1 + } + } + }' +``` + +--- + +## Benefit application order + +Reservations and savings plans follow a strict application order: + +1. **Reservations are applied first** in the benefit stack +2. **Savings plans are applied second** to remaining eligible charges +3. This means reservations "win" for matching workloads; savings plans catch the rest + +Within reservations, the most specific scope is applied first: + +1. Resource group scope +2. Subscription scope +3. Management group scope +4. Shared scope + +--- + +## Utilization monitoring + +### Azure CLI + +```bash +# Daily utilization summary for a reservation order +az consumption reservation summary list \ + --reservation-order-id {orderId} \ + --grain daily \ + --start-date 2026-01-01 \ + --end-date 2026-01-31 +``` + +### REST API + +> **Authentication note:** Use `az rest` in practice — it handles token acquisition automatically. + +```http +GET https://management.azure.com/providers/Microsoft.Capacity/reservationOrders/{reservationOrderId}/providers/Microsoft.Consumption/reservationSummaries?grain=daily&$filter=properties/usageDate ge 2026-01-01 AND properties/usageDate le 2026-01-31&api-version=2024-08-01 +Authorization: Bearer {token} +``` + +### Azure Resource Graph -- Advisor purchase recommendations + +Query cross-subscription Advisor recommendations for VM reserved instance purchases (recommendationTypeId is specific to VM reservations): + +```kusto +advisorresources +| where type == "microsoft.advisor/recommendations" +| where properties.category == "Cost" +| where properties.recommendationTypeId == "84b1a508-fc21-49da-979e-96894f1665df" +| extend + savings = todouble(properties.extendedProperties.savingsAmount), + annualSavings = todouble(properties.extendedProperties.annualSavingsAmount) +| project subscriptionId, resourceGroup, savings, annualSavings, + problem = properties.shortDescription.problem, + solution = properties.shortDescription.solution +| order by savings desc +``` + +**Note:** This query surfaces Advisor purchase recommendations, not utilization data. For utilization monitoring, use the Azure CLI or REST API examples above. + +--- + +## Eligibility + +### Agreement types + +| Agreement Type | Offer IDs | Can purchase reservations | +|----------------|----------------|--------------------------| +| EA | MS-AZR-0017P, MS-AZR-0148P | Yes | +| MCA | - | Yes | +| MPA | - | Yes | +| CSP | - | Yes (partners purchase via Partner Center; customers cannot self-service manage) | +| Pay-As-You-Go | MS-AZR-0003P, MS-AZR-0023P | Yes | +| Azure Sponsorship | MS-AZR-0036P | Yes | + +### Required roles + +| Action | Required role | +|--------|---------------| +| View recommendations | Cost Management Reader | +| View utilization | Cost Management Reader or Reservation Reader | +| Purchase reservations | Owner or Reservation Purchaser | +| Manage reservations | Owner or Reservation Administrator | + +--- + +## Payment options + +| Option | Description | +|--------|-------------| +| All upfront | Pay the full commitment amount at purchase | +| Monthly | Pay in monthly installments over the term | + +Payment frequency does not affect the discount amount — only cash flow timing. Total cost is the same for either option. + +--- + +## Auto-renewal + +Reservations can be configured for automatic renewal before expiration. Review utilization data before renewal to confirm the commitment level and SKU are still appropriate. Auto-renewal is enabled by default for new purchases — disable it in the Azure portal if you prefer manual renewal. + +--- + +## Coverage limitations + +Reservation discounts cover the **compute or capacity portion** of the specified resource type only. The following are NOT covered: + +- Software licensing (Windows Server, SQL Server — use Azure Hybrid Benefit separately) +- Networking charges +- Storage costs (except for Azure Blob Storage and Azure Files reserved capacity) +- Marketplace purchases + +--- + +## Best practices + +1. **Normalize usage for at least 30 days** before purchasing to ensure stable baseline +2. **Use instance size flexibility** - buy the normalized size for maximum coverage within a VM series +3. **Monitor utilization weekly** - exchange underutilized reservations before waste accumulates +4. **Start with shared scope** for maximum flexibility across subscriptions +5. **Use the 3-day stale data guard** - Microsoft provides the lower of 3-day and lookback-period recommendations as a safeguard against overcommitment +6. **Compare with savings plans** - use the Benefit Recommendations API to evaluate both options before purchasing +7. **Layer reservations and savings plans** - buy reservations for stable, predictable workloads; use savings plans as a safety net for variable compute + +See `references/azure-commitment-discount-decision.md` for the full decision framework. + +--- + +## Troubleshooting + +| Problem | Cause | Solution | +|---------|-------|----------| +| Low utilization | VM stopped/deallocated or wrong SKU | Check VM state, consider exchange for a different SKU or region | +| Reservation not applying | Scope mismatch or region mismatch | Verify scope and region settings match the target resources | +| No recommendations | Insufficient usage history | Wait for 7+ days of consistent usage before querying | +| Exchange failed | Exceeded $50K return limit | Check remaining return balance in Azure portal | +| Wrong VM size covered | Instance size flexibility ratio | Review the flexibility group ratio table for the VM series | +| Reservation expired | Term ended | Purchase a new reservation; set calendar reminders before expiration | + +--- + +## Prerequisites + +- Azure PowerShell module (`Install-Module -Name Az`) or Azure CLI +- Authenticated Azure session (`Connect-AzAccount` or `az login`) +- **Cost Management Reader** permissions on the billing scope (for recommendations) +- **Owner** or **Reservation Purchaser** role (for purchasing) + +--- + +## References + +- [Azure Reservations overview](https://learn.microsoft.com/azure/cost-management-billing/reservations/save-compute-costs-reservations) +- [Reservation recommendations](https://learn.microsoft.com/azure/cost-management-billing/reservations/reserved-instance-purchase-recommendations) +- [Instance size flexibility](https://learn.microsoft.com/azure/virtual-machines/reserved-vm-instance-size-flexibility) +- [Self-service exchanges and refunds](https://learn.microsoft.com/azure/cost-management-billing/reservations/exchange-and-refund-azure-reservations) +- [Benefit Recommendations API](https://learn.microsoft.com/rest/api/cost-management/benefit-recommendations) +- [Manage reservations](https://learn.microsoft.com/azure/cost-management-billing/reservations/manage-reserved-vm-instance) +- [Reservation trade-in to savings plans](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/reservation-trade-in) +- [Reservation exchange policy changes](https://learn.microsoft.com/azure/cost-management-billing/reservations/reservation-exchange-policy-changes) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-retail-prices.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-retail-prices.md new file mode 100644 index 000000000..35341b769 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-retail-prices.md @@ -0,0 +1,263 @@ +--- +name: Azure Retail Prices +description: Query the Azure Retail Prices API (prices.azure.com) to look up public pricing for any Azure service by SKU, region, and tier. Public API, no authentication required. Use for price comparisons, rightsizing calculations, and validating Advisor savings estimates. +--- + +**Key Features:** +- Public API — no authentication required +- OData filter syntax for precise queries +- Coverage across all Azure services and regions +- Reserved vs pay-as-you-go price comparison +- Cross-region price comparison +- Pagination support for large result sets + +--- + +## Base URL + +``` +https://prices.azure.com/api/retail/prices +``` + +No API key or Azure authentication needed. Rate-limited but generous for interactive use. + +--- + +## OData filter syntax + +### Common filter properties + +| Property | Type | Description | Example | +|----------|------|-------------|---------| +| `serviceName` | string | Azure service name | `Virtual Machines`, `Storage` | +| `armSkuName` | string | ARM SKU identifier | `Standard_D4s_v5`, `Standard_LRS` | +| `armRegionName` | string | Azure region | `eastus`, `westeurope` | +| `skuName` | string | Human-readable SKU | `D4s v5`, `D4s v5 Low Priority` | +| `priceType` | string | Pricing model | `Consumption`, `Reservation` | +| `currencyCode` | string | ISO currency code | `USD`, `EUR` | +| `productName` | string | Product family | `Virtual Machines DS Series` | +| `meterName` | string | Meter name | `D4s v5`, `D4s v5 Spot` | +| `type` | string | Rate type | `Consumption`, `DevTestConsumption` | +| `reservationTerm` | string | RI term length | `1 Year`, `3 Years` | +| `tierMinimumUnits` | number | Volume tier minimum | `0` | + +### Filter operators + +``` +eq - equals +ne - not equals +gt - greater than +lt - less than +ge - greater than or equal +le - less than or equal +and - logical AND +or - logical OR +contains(field, 'value') - substring match +``` + +--- + +## Common query patterns + +### VM pricing by SKU and region + +```bash +curl -s "https://prices.azure.com/api/retail/prices?\$filter=armSkuName eq 'Standard_D4s_v5' and armRegionName eq 'eastus' and priceType eq 'Consumption'" | jq '.Items[] | {skuName, retailPrice, unitOfMeasure, meterName}' +``` + +```powershell +$response = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=armSkuName eq 'Standard_D4s_v5' and armRegionName eq 'eastus' and priceType eq 'Consumption'" +$response.Items | Select-Object skuName, retailPrice, unitOfMeasure, meterName | Format-Table +``` + +### Storage pricing by tier and redundancy + +```bash +curl -s "https://prices.azure.com/api/retail/prices?\$filter=serviceName eq 'Storage' and armRegionName eq 'eastus' and skuName eq 'Hot LRS' and productName eq 'Azure Data Lake Storage Gen2'" | jq '.Items[] | {meterName, retailPrice, unitOfMeasure}' +``` + +### Reserved vs pay-as-you-go price comparison + +```powershell +# Get PAYG and reserved prices for a VM SKU +$sku = "Standard_D4s_v5" +$region = "eastus" + +$payg = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=armSkuName eq '$sku' and armRegionName eq '$region' and priceType eq 'Consumption'" +$ri1yr = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=armSkuName eq '$sku' and armRegionName eq '$region' and priceType eq 'Reservation' and reservationTerm eq '1 Year'" +$ri3yr = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=armSkuName eq '$sku' and armRegionName eq '$region' and priceType eq 'Reservation' and reservationTerm eq '3 Years'" + +# Calculate hourly equivalent rates +$paygHourly = ($payg.Items | Where-Object { $_.meterName -eq 'D4s v5' -and $_.type -eq 'Consumption' }).retailPrice +# Reservation retailPrice is already the hourly equivalent rate (unitOfMeasure = "1 Hour") +$ri1yrHourly = ($ri1yr.Items | Where-Object { $_.meterName -eq 'D4s v5' }).retailPrice +$ri3yrHourly = ($ri3yr.Items | Where-Object { $_.meterName -eq 'D4s v5' }).retailPrice + +Write-Host "PAYG hourly: `$$([math]::Round($paygHourly, 4))" +Write-Host "1-yr RI hourly: `$$([math]::Round($ri1yrHourly, 4)) ($([math]::Round((1 - $ri1yrHourly/$paygHourly) * 100, 1))% savings)" +Write-Host "3-yr RI hourly: `$$([math]::Round($ri3yrHourly, 4)) ($([math]::Round((1 - $ri3yrHourly/$paygHourly) * 100, 1))% savings)" +``` + +### Cross-region price comparison + +```powershell +$sku = "Standard_D4s_v5" +$regions = @("eastus", "westus2", "westeurope", "southeastasia") + +$results = foreach ($region in $regions) { + $response = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=armSkuName eq '$sku' and armRegionName eq '$region' and priceType eq 'Consumption'" + $price = ($response.Items | Where-Object { $_.meterName -eq 'D4s v5' -and $_.type -eq 'Consumption' }).retailPrice + [PSCustomObject]@{ + Region = $region + HourlyPrice = $price + MonthlyEstimate = [math]::Round($price * 730, 2) + } +} + +$results | Sort-Object HourlyPrice | Format-Table +``` + +--- + +## Response structure + +```json +{ + "BillingCurrency": "USD", + "CustomerEntityId": "Default", + "CustomerEntityType": "Retail", + "Items": [ + { + "currencyCode": "USD", + "tierMinimumUnits": 0.0, + "retailPrice": 0.192, + "unitPrice": 0.192, + "armRegionName": "eastus", + "location": "US East", + "effectiveStartDate": "2023-04-01T00:00:00Z", + "meterId": "...", + "meterName": "D4s v5", + "productId": "...", + "skuId": "...", + "productName": "Virtual Machines DSv5 Series", + "skuName": "D4s v5", + "serviceName": "Virtual Machines", + "serviceId": "...", + "serviceFamily": "Compute", + "unitOfMeasure": "1 Hour", + "type": "Consumption", + "isPrimaryMeterRegion": true, + "armSkuName": "Standard_D4s_v5" + } + ], + "NextPageLink": "https://prices.azure.com/api/retail/prices?$skip=100&...", + "Count": 1 +} +``` + +### Key fields + +| Field | Description | +|-------|-------------| +| `retailPrice` | List price (no discounts applied) | +| `unitPrice` | Same as `retailPrice` for retail customers | +| `unitOfMeasure` | Billing unit: `1 Hour`, `1 GB/Month`, `10,000 Transactions` | +| `armSkuName` | ARM SKU for programmatic cross-reference | +| `isPrimaryMeterRegion` | `true` for the canonical region meter; filter on this to avoid duplicates | +| `type` | `Consumption` (PAYG), `DevTestConsumption` (Dev/Test), `Reservation` | +| `reservationTerm` | Present only for `Reservation` type: `1 Year` or `3 Years` | + +--- + +## Pagination + +Results are paginated at 100 items per page. Use `NextPageLink` to iterate: + +```powershell +function Get-AllRetailPrices { + param([string]$Filter) + + $url = "https://prices.azure.com/api/retail/prices?`$filter=$Filter" + $allItems = @() + + while ($url) { + $response = Invoke-RestMethod $url + $allItems += $response.Items + $url = $response.NextPageLink + } + + return $allItems +} + +# Usage +$prices = Get-AllRetailPrices -Filter "serviceName eq 'Virtual Machines' and armRegionName eq 'eastus' and priceType eq 'Consumption'" +Write-Host "Found $($prices.Count) pricing records" +``` + +```bash +# Bash pagination with jq +url="https://prices.azure.com/api/retail/prices?\$filter=armSkuName eq 'Standard_D4s_v5' and armRegionName eq 'eastus'" +while [ "$url" != "null" ] && [ -n "$url" ]; do + response=$(curl -s "$url") + echo "$response" | jq '.Items[]' + url=$(echo "$response" | jq -r '.NextPageLink') +done +``` + +--- + +## Integration patterns + +### Validate Advisor savings estimates + +Cross-reference Advisor right-size recommendations with actual retail prices to validate savings claims: + +```powershell +# Get Advisor right-size recommendation details +$rec = Get-AzAdvisorRecommendation | + Where-Object { $_.RecommendationTypeId -eq 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974' } | + Select-Object -First 1 + +$currentSku = $rec.ExtendedProperty["currentSku"] +$targetSku = $rec.ExtendedProperty["targetSku"] +$region = $rec.ExtendedProperty["regionId"] + +# Look up actual prices +$currentPrice = (Get-AllRetailPrices -Filter "armSkuName eq '$currentSku' and armRegionName eq '$region' and priceType eq 'Consumption'" | + Where-Object { $_.isPrimaryMeterRegion -and $_.type -eq 'Consumption' }).retailPrice + +$targetPrice = (Get-AllRetailPrices -Filter "armSkuName eq '$targetSku' and armRegionName eq '$region' and priceType eq 'Consumption'" | + Where-Object { $_.isPrimaryMeterRegion -and $_.type -eq 'Consumption' }).retailPrice + +$monthlySavings = ($currentPrice - $targetPrice) * 730 +Write-Host "Current: $currentSku @ `$$currentPrice/hr" +Write-Host "Target: $targetSku @ `$$targetPrice/hr" +Write-Host "Monthly savings: `$$([math]::Round($monthlySavings, 2))" +``` + +### Calculate rightsizing savings + +See `references/azure-vm-rightsizing.md` for the full rightsizing workflow that uses this API to validate target SKU pricing. + +--- + +## Limitations + +| Limitation | Impact | +|-----------|--------| +| **Retail/list prices only** | No EA/MCA negotiated rates, no discount-adjusted prices | +| **No real-time availability** | Prices may lag behind actual availability by hours | +| **Rate limiting** | No published limits, but excessive requests may be throttled | +| **No savings plan pricing** | Savings plan effective rates are not exposed (use Benefit Recommendations API instead) | +| **Currency conversion** | Prices are listed per currency; exchange rates are Microsoft-determined | + +**Important:** Retail prices are useful for relative comparisons (SKU A vs SKU B, region X vs region Y) and for estimating savings percentages. For actual bill amounts, use Cost Management APIs. + +--- + +## References + +- [Azure Retail Prices API overview](https://learn.microsoft.com/rest/api/cost-management/retail-prices/azure-retail-prices) +- [Retail Prices OData query examples](https://learn.microsoft.com/rest/api/cost-management/retail-prices/azure-retail-prices#api-examples) +- [Azure pricing calculator](https://azure.microsoft.com/pricing/calculator/) +- [Rate optimization (FinOps Framework)](https://learn.microsoft.com/cloud-computing/finops/framework/optimize/rates) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-savings-plans.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-savings-plans.md new file mode 100644 index 000000000..de4852ff2 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-savings-plans.md @@ -0,0 +1,424 @@ +--- +name: Azure Savings Plans +description: Query the Azure Cost Management Benefit Recommendations API to retrieve savings plan purchase recommendations based on historical compute usage patterns. Analyze potential savings (up to 65% vs PAYG), coverage percentages, and optimal commitment amounts for flexible compute workloads. +--- + +**Key Features:** +- Historical usage analysis (7, 30, or 60 days lookback) +- Up to 10 commitment level recommendations +- Savings calculations vs pay-as-you-go pricing +- Coverage and utilization projections +- Support for 1-year and 3-year terms + +--- + +## Benefit Recommendations API + +### PowerShell Script + +```powershell +# Basic usage with subscription scope +.\Get-BenefitRecommendations.ps1 ` + -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012" + +# Advanced: billing account, 30-day lookback, 1-year term +.\Get-BenefitRecommendations.ps1 ` + -BillingScope "providers/Microsoft.Billing/billingAccounts/12345678" ` + -LookBackPeriod "Last30Days" ` + -Term "P1Y" + +# Resource group scope +.\Get-BenefitRecommendations.ps1 ` + -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myResourceGroup" +``` + +### Parameters + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `BillingScope` | Yes | - | Billing account, subscription, or resource group scope | +| `LookBackPeriod` | No | Last7Days | Analysis period: Last7Days, Last30Days, Last60Days. Script default is Last7Days; API default (when omitted from REST call) is Last60Days | +| `Term` | No | P3Y | Savings plan term: P1Y (1-year) or P3Y (3-year) | + +### Scope Formats + +| Scope Type | Format | +|------------|--------| +| Billing Account | `providers/Microsoft.Billing/billingAccounts/{billingAccountId}` | +| Billing Profile (MCA) | `providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}` | +| Subscription | `subscriptions/{subscriptionId}` | +| Resource Group | `subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}` | + +--- + +## Output Metrics + +The API returns detailed financial projections: + +| Metric | Description | +|--------|-------------| +| **commitmentAmount** | Hourly commitment amount at specified granularity | +| **savingsAmount** | Total amount saved for the lookback period | +| **savingsPercentage** | Savings percentage vs pay-as-you-go | +| **coveragePercentage** | Estimated benefit coverage for the lookback period | +| **averageUtilizationPercentage** | Estimated average utilization with this commitment | +| **totalCost** | Sum of benefit cost and overage cost | +| **benefitCost** | commitmentAmount × totalHours | +| **overageCost** | Charges exceeding the commitment | +| **wastageCost** | Unused portion of the benefit cost | + +--- + +## REST API + +> **Authentication note:** Use `az rest` in practice — it handles token acquisition automatically. The raw HTTP examples below are for documentation purposes only. + +### Request + +```http +GET https://management.azure.com/{billingScope}/providers/Microsoft.CostManagement/benefitRecommendations?$filter=properties/lookBackPeriod eq 'Last30Days' AND properties/term eq 'P3Y'&$expand=properties/usage,properties/allRecommendationDetails&api-version=2024-08-01 +Authorization: Bearer {token} +``` + +All parameters are passed via OData `$filter` query parameters, not a request body. The `$expand` parameter controls which detail sections are returned: + +| $expand value | Effect | +|---------------|--------| +| `properties/allRecommendationDetails` | Returns all 10 commitment level recommendations (required for comparison analysis) | +| `properties/usage` | Returns hourly usage data for the lookback period | + +To filter for savings plans only, add `AND properties/kind eq 'SavingsPlan'` to the `$filter`. Without a `kind` filter, the API returns both savings plan and reservation recommendations. + +### Scope values + +| Scope | Description | +|-------|-------------| +| `Shared` | Analyzes usage across entire billing scope (default, optimal savings) | +| `Single` | Resource-specific recommendations | + +**Note:** Add `AND properties/scope eq 'Shared'` to the `$filter` to specify scope. Default behavior analyzes shared scope. + +### Response structure + +```json +{ + "value": [ + { + "properties": { + "firstConsumptionDate": "2026-01-01", + "lastConsumptionDate": "2026-01-21", + "lookBackPeriod": "Last30Days", + "term": "P3Y", + "totalHours": 720, + "scope": "Shared", + "kind": "SavingsPlan", + "currencyCode": "USD", + "costWithoutBenefit": 11000, + "recommendationDetails": { + "commitmentAmount": 10.5, + "savingsAmount": 2500, + "savingsPercentage": 25.5, + "coveragePercentage": 85.2, + "averageUtilizationPercentage": 92.3, + "totalCost": 8500, + "benefitCost": 7560, + "overageCost": 940, + "wastageCost": 580 + }, + "allRecommendationDetails": { + "value": [ + { "commitmentAmount": 5.0, "savingsPercentage": 15.2, "averageUtilizationPercentage": 98.1 }, + { "commitmentAmount": 10.5, "savingsPercentage": 25.5, "averageUtilizationPercentage": 92.3 }, + { "commitmentAmount": 15.0, "savingsPercentage": 28.1, "averageUtilizationPercentage": 85.7 } + ] + } + } + } + ] +} +``` + +**Note:** The `allRecommendationDetails` array only appears when `$expand=properties/allRecommendationDetails` is included in the request. The `recommendationDetails` object always contains the single best recommendation. + +--- + +## Analysis Examples + +### Find Optimal Commitment Level + +```powershell +# Get recommendations +$result = .\Get-BenefitRecommendations.ps1 ` + -BillingScope "subscriptions/$subscriptionId" ` + -LookBackPeriod "Last30Days" ` + -Term "P3Y" + +# Find recommendation with best savings/utilization balance +$optimal = $result.recommendations | + Where-Object { $_.averageUtilizationPercentage -ge 90 } | + Sort-Object savingsAmount -Descending | + Select-Object -First 1 + +Write-Host "Optimal hourly commitment: $($optimal.commitmentAmount)" +Write-Host "Projected monthly savings: $($optimal.savingsAmount)" +Write-Host "Coverage: $($optimal.coveragePercentage)%" +``` + +### Compare 1-Year vs 3-Year Terms + +```powershell +$scope = "subscriptions/$subscriptionId" + +$oneYear = .\Get-BenefitRecommendations.ps1 -BillingScope $scope -Term "P1Y" +$threeYear = .\Get-BenefitRecommendations.ps1 -BillingScope $scope -Term "P3Y" + +# Compare top recommendations +$comparison = @{ + "1-Year" = @{ + Commitment = $oneYear.recommendations[0].commitmentAmount + Savings = $oneYear.recommendations[0].savingsPercentage + } + "3-Year" = @{ + Commitment = $threeYear.recommendations[0].commitmentAmount + Savings = $threeYear.recommendations[0].savingsPercentage + } +} + +$comparison | ConvertTo-Json +``` + +--- + +## Integration with FinOps analysis + +### Utilization analysis + +```powershell +# Assess whether the recommended commitment level will be fully utilized +# Key metric: averageUtilizationPercentage — the projected percentage of committed hours that would be consumed +# Target: >90% utilization = good commitment fit; <80% = consider lower commitment + +$optimal = $result.recommendations | + Where-Object { $_.averageUtilizationPercentage -ge 90 } | + Sort-Object savingsAmount -Descending | + Select-Object -First 1 + +$wastageRate = (1 - ($optimal.averageUtilizationPercentage / 100)) * $optimal.benefitCost +Write-Host "Projected hourly commitment: $($optimal.commitmentAmount)" +Write-Host "Projected savings (lookback period): $($optimal.savingsAmount)" +Write-Host "Projected wastage (lookback period): $wastageRate" +Write-Host "Utilization: $($optimal.averageUtilizationPercentage)%" +``` + +**Important:** The `savingsAmount` from the API represents total savings over the lookback period (7, 30, or 60 days), not annual savings. Do not divide by 12 to get monthly figures — instead scale proportionally from the lookback window. + +### Risk assessment + +```powershell +# Evaluate commitment risk based on utilization variance +$recommendations = $result.recommendations + +foreach ($rec in $recommendations) { + $risk = switch ($rec.averageUtilizationPercentage) { + { $_ -ge 95 } { "Low - High utilization, minimal waste"; break } + { $_ -ge 85 } { "Medium - Good utilization, some flexibility"; break } + { $_ -ge 70 } { "High - Consider lower commitment"; break } + default { "Very High - Significant underutilization risk" } + } + + Write-Host "Commitment: $($rec.commitmentAmount)/hr - Risk: $risk" +} +``` + +--- + +## Savings plan policies + +### Eligibility + +Savings plans are available for these agreement types only: +- Enterprise Agreement (EA): Offer IDs MS-AZR-0017P, MS-AZR-0148P +- Microsoft Customer Agreement (MCA) +- Microsoft Partner Agreement (MPA) + +**Not available** for CSP (Cloud Solution Provider), Pay-As-You-Go, or free/trial subscriptions. + +### Cancellation and refund policy + +Savings plan purchases **cannot be canceled or refunded**. This is a hard constraint — there is no self-service cancellation, no exchange, and no early termination option. This is the most significant policy difference from reservations (which allow up to $50K/year in returns). + +### Payment options + +| Option | Description | +|--------|-------------| +| All upfront | Pay the full commitment amount at purchase (total cost is the same) | +| Monthly | Pay in monthly installments over the term (total cost is the same) | + +Payment frequency does not affect the discount amount — only cash flow timing. + +### Auto-renewal + +Savings plans can be configured for automatic renewal before expiration. Set this at purchase time or update later in the Azure portal. Review utilization before renewal to confirm the commitment level is still appropriate. + +--- + +## Discount application mechanics + +### How benefits are applied + +Savings plan discounts are applied **hourly** on a use-it-or-lose-it basis: + +1. Each hour, Azure calculates your eligible compute charges +2. The savings plan benefit is applied to the product with the **greatest discount first** (maximizing your savings) +3. Any unused commitment for that hour is **lost** — it does not roll over +4. Reservations are always applied **before** savings plans in the benefit stack + +### Scope processing order + +When multiple commitment discounts exist, benefits are applied in this order: +1. Resource group scope (most specific) +2. Subscription scope +3. Management group scope +4. Shared scope (broadest) + +### Coverage limitations + +Savings plans cover **compute charges only**. The following are NOT covered: +- Software licensing (Windows Server, SQL Server — use Azure Hybrid Benefit separately) +- Networking charges +- Storage costs +- Marketplace purchases + +--- + +## Recommendation guidance + +### The 3-day stale data guard + +Microsoft runs simulations using only the last 3 days of usage as a safeguard against overcommitment from stale data. The recommendation engine provides the **lower** of the 3-day and full lookback-period recommendations. This means short usage spikes within the lookback window will not inflate recommendations. + +### 7-day waiting period + +After purchasing a savings plan or reservation, wait at least **7 days** before evaluating further commitment recommendations. The recommendation engine needs time to recalculate based on the new benefit coverage. Purchasing immediately can result in double-coverage and wastage. + +### Management group workaround + +The Benefit Recommendations API does not support management group scope. Microsoft's documented workaround: +1. Get recommendations for each subscription individually +2. Sum the recommended commitment amounts +3. Purchase approximately **70%** of the total (conservative start) +4. Wait 3 days for the recommendation engine to recalculate +5. Iterate — get new recommendations accounting for existing commitments +6. Repeat until incremental savings are negligible + +### Savings quantification + +Savings plans provide up to **65% savings** compared to pay-as-you-go pricing. Actual savings depend on: +- Commitment term (3-year provides deeper discounts than 1-year) +- Utilization rate (higher utilization = more realized savings) +- Workload consistency (stable usage patterns maximize benefit) + +For comparison, reservations offer up to **72% savings** but with less flexibility. See `references/azure-commitment-discount-decision.md` for the decision framework. + +--- + +## Script source + +The `Get-BenefitRecommendations.ps1` script is embedded below for self-contained use. This script uses `Invoke-AzRestMethod` with the correct GET method and OData filter parameters. + +```powershell +<# +.SYNOPSIS + Get Azure Cost Management benefit recommendations for savings plans and reserved instances. + +.DESCRIPTION + This script queries the Azure Cost Management API to retrieve benefit recommendations + based on historical usage patterns. It helps identify opportunities for cost savings + through Azure savings plans and reserved instances. + +.PARAMETER BillingScope + The billing scope to query. Can be a billing account or subscription. + Examples: + - "providers/Microsoft.Billing/billingAccounts/12345678" + - "subscriptions/12345678-1234-1234-1234-123456789012" + +.PARAMETER LookBackPeriod + Historical period to analyze for recommendations. + Valid values: Last7Days, Last30Days, Last60Days + Default: Last7Days + +.PARAMETER Term + Commitment term for savings plans. + Valid values: P1Y (1 year), P3Y (3 years) + Default: P3Y + +.EXAMPLE + .\Get-BenefitRecommendations.ps1 -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012" + + Gets 3-year savings plan recommendations for a subscription based on last 7 days usage. + +.EXAMPLE + .\Get-BenefitRecommendations.ps1 -BillingScope "providers/Microsoft.Billing/billingAccounts/12345678" -LookBackPeriod "Last30Days" -Term "P1Y" + + Gets 1-year savings plan recommendations for a billing account based on last 30 days usage. + +.NOTES + Requires Azure PowerShell module and Cost Management Reader permissions on the specified scope. + + To find your billing account: Get-AzBillingAccount + To find subscriptions: Get-AzSubscription +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true, HelpMessage = "Billing scope (billing account or subscription)")] + [string] + $BillingScope, + + [Parameter()] + [ValidateSet('Last7Days', 'Last30Days', 'Last60Days')] + [string] + $LookBackPeriod = 'Last7Days', + + [Parameter()] + [ValidateSet('P1Y', 'P3Y')] + [string] + $Term = 'P3Y' +) + +$url="https://management.azure.com/{0}/providers/Microsoft.CostManagement/benefitRecommendations?`$filter=properties/lookBackPeriod eq '{1}' AND properties/term eq '{2}'&`$expand=properties/usage,properties/allRecommendationDetails&api-version=2024-08-01" -f $BillingScope, $lookBackPeriod, $term +$uri=[uri]::new($url) +$result = Invoke-AzRestMethod -Uri $uri.AbsoluteUri -Method GET +$jsonResult = $result.Content | ConvertFrom-Json + +Write-Output "" +Write-Output "Raw output" +$result.Content +Write-Output "" +Write-Output "Recommended savings plan" +$jsonResult.value.properties.recommendationDetails | Format-Table +Write-Output "" +Write-Output "All savings plan recommendations" +$jsonResult.value.properties.allRecommendationDetails.value | Format-Table +``` + +--- + +## Prerequisites + +- Azure PowerShell module (`Install-Module -Name Az`) +- Authenticated Azure session (`Connect-AzAccount`) +- **Cost Management Reader** permissions on the billing scope +- Valid billing account ID or subscription ID +- Agreement type: EA (MS-AZR-0017P or MS-AZR-0148P), MCA, or MPA + +--- + +## References + +- [Benefit Recommendations API](https://learn.microsoft.com/rest/api/cost-management/benefit-recommendations) +- [Azure savings plan overview](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/savings-plan-compute-overview) +- [Choose commitment amount](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/choose-commitment-amount) +- [How saving plan discount is applied](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/discount-application) +- [Decide between a savings plan and a reservation](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/decide-between-savings-plan-reservation) +- [Rate optimization (FinOps Framework)](https://learn.microsoft.com/cloud-computing/finops/framework/optimize/rates) diff --git a/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-vm-rightsizing.md b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-vm-rightsizing.md new file mode 100644 index 000000000..c082210f8 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/skills/azure-cost-management/references/azure-vm-rightsizing.md @@ -0,0 +1,293 @@ +--- +name: Azure VM Rightsizing +description: Workflow for identifying over-provisioned VMs using Azure Advisor recommendations and Azure Monitor metrics, then recommending SKU downsizes validated against retail pricing. Covers CPU, memory, and disk IO utilization analysis with safety checks for burst requirements and Hybrid Benefit status. +--- + +**Key Features:** +- Azure Advisor right-size VM candidate identification +- Azure Monitor / Log Analytics utilization metric collection +- Utilization thresholds: CPU P95 < 20%, memory avg < 30% +- Target SKU recommendation with retail price validation +- Safety checks: burst requirements, instance size flexibility, Hybrid Benefit + +--- + +## Overview + +VM rightsizing is the highest-value single resource optimization in most Azure environments. The workflow: + +1. **Identify candidates** — Advisor flags underutilized VMs +2. **Collect metrics** — Azure Monitor confirms utilization patterns +3. **Recommend target SKU** — downsize within the VM family +4. **Validate pricing** — confirm savings with Retail Prices API +5. **Safety check** — burst, flexibility, licensing + +--- + +## Step 1: Identify candidates via Azure Advisor + +Azure Advisor recommendation type `e10b1381-5f0a-47ff-8c7b-37bd13d7c974` identifies VMs for rightsizing based on 7-day utilization analysis. + +### Azure CLI + +```bash +az advisor recommendation list \ + --category Cost \ + --query "[?recommendationTypeId=='e10b1381-5f0a-47ff-8c7b-37bd13d7c974'].{ + Resource: resourceMetadata.resourceId, + CurrentSku: extendedProperties.currentSku, + TargetSku: extendedProperties.targetSku, + Savings: extendedProperties.savingsAmount, + Region: extendedProperties.regionId + }" \ + --output table +``` + +### Resource Graph (cross-subscription) + +```kusto +advisorresources +| where type == "microsoft.advisor/recommendations" +| where properties.recommendationTypeId == "e10b1381-5f0a-47ff-8c7b-37bd13d7c974" +| extend currentSku = tostring(properties.extendedProperties.currentSku) +| extend targetSku = tostring(properties.extendedProperties.targetSku) +| extend savings = todouble(properties.extendedProperties.savingsAmount) +| extend vmId = tostring(properties.resourceMetadata.resourceId) +| extend region = tostring(properties.extendedProperties.regionId) +| project vmId, currentSku, targetSku, savings, region, subscriptionId +| order by savings desc +``` + +See `references/azure-advisor.md` for full Advisor query patterns and suppression management. + +--- + +## Step 2: Collect utilization metrics + +Advisor's built-in analysis uses 7 days of data. For higher confidence, collect 14–30 days of metrics from Azure Monitor. + +### CPU utilization (Azure Monitor) + +```bash +# Average, P95, and max CPU over 14 days +az monitor metrics list \ + --resource "/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{vmName}" \ + --metric "Percentage CPU" \ + --start-time $(date -u -d "14 days ago" +%Y-%m-%dT%H:%M:%SZ) \ + --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \ + --interval PT1H \ + --aggregation Average Maximum \ + --query "value[0].timeseries[0].data[].{Time: timeStamp, Avg: average, Max: maximum}" +``` + +### Memory utilization (requires VM Insights) + +Memory metrics require the Azure Monitor Agent (AMA) or Log Analytics agent. Query via Log Analytics workspace: + +```kusto +// Memory utilization over 14 days +InsightsMetrics +| where TimeGenerated > ago(14d) +| where Origin == "vm.azm.ms" +| where Namespace == "Memory" +| where Name == "AvailableMB" +| extend TotalMB = todouble(Tags["vm.azm.ms/memorySizeMB"]) +| extend UsedPercent = (TotalMB - Val) / TotalMB * 100 +| summarize + AvgMemoryPercent = avg(UsedPercent), + P95MemoryPercent = percentile(UsedPercent, 95), + MaxMemoryPercent = max(UsedPercent) + by Computer +``` + +### Disk IO utilization + +```kusto +// Disk IOPS and throughput over 14 days +InsightsMetrics +| where TimeGenerated > ago(14d) +| where Origin == "vm.azm.ms" +| where Namespace == "LogicalDisk" +| where Name in ("ReadsPerSecond", "WritesPerSecond", "ReadBytesPerSecond", "WriteBytesPerSecond") +| summarize + AvgValue = avg(Val), + P95Value = percentile(Val, 95), + MaxValue = max(Val) + by Computer, Name +| order by Computer, Name +``` + +### Combined utilization summary + +```kusto +// Combined VM utilization summary for rightsizing analysis +let cpu = Perf +| where TimeGenerated > ago(14d) +| where ObjectName == "Processor" and CounterName == "% Processor Time" and InstanceName == "_Total" +| summarize CpuAvg = avg(CounterValue), CpuP95 = percentile(CounterValue, 95), CpuMax = max(CounterValue) by Computer; +let mem = InsightsMetrics +| where TimeGenerated > ago(14d) +| where Origin == "vm.azm.ms" and Namespace == "Memory" and Name == "AvailableMB" +| extend TotalMB = todouble(Tags["vm.azm.ms/memorySizeMB"]) +| extend UsedPercent = (TotalMB - Val) / TotalMB * 100 +| summarize MemAvg = avg(UsedPercent), MemP95 = percentile(UsedPercent, 95) by Computer; +cpu | join kind=leftouter mem on Computer +| project Computer, CpuAvg, CpuP95, CpuMax, MemAvg, MemP95 +| extend RightsizeCandidate = CpuP95 < 20 and (isnull(MemAvg) or MemAvg < 30) +| order by RightsizeCandidate desc, CpuP95 asc +``` + +--- + +## Step 3: Determine rightsizing thresholds + +| Metric | Threshold | Interpretation | +|--------|-----------|----------------| +| CPU P95 | < 20% | Strong downsize candidate | +| CPU P95 | 20–40% | Moderate candidate, verify burst patterns | +| CPU P95 | > 40% | Likely right-sized, skip | +| Memory avg | < 30% | Strong downsize candidate (if available) | +| Memory avg | 30–50% | Moderate candidate | +| Memory avg | > 50% | Likely right-sized for memory | + +**Note:** If memory metrics are unavailable (no VM Insights), rely on CPU only but be more conservative — use CPU P95 < 15% as the threshold to account for unknown memory pressure. + +**Agent compatibility note:** The `Perf` table is populated by the legacy Microsoft Monitoring Agent (MMA). Environments using the Azure Monitor Agent (AMA) should query CPU from `InsightsMetrics | where Namespace == 'Processor' and Name == 'UtilizationPercentage'` instead. + +--- + +## Step 4: Recommend target SKU + +### Get current VM specs via Resource Graph + +```kusto +resources +| where type == "microsoft.compute/virtualmachines" +| where name == "{vmName}" +| extend vmSize = tostring(properties.hardwareProfile.vmSize) +| extend location = location +| project name, vmSize, location, resourceGroup, subscriptionId +``` + +### Compare against target SKU pricing + +Use the Retail Prices API (see `references/azure-retail-prices.md`) to validate savings: + +```powershell +function Get-VmSkuPrice { + param([string]$Sku, [string]$Region) + $response = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=armSkuName eq '$Sku' and armRegionName eq '$Region' and priceType eq 'Consumption'" + return ($response.Items | Where-Object { $_.isPrimaryMeterRegion -and $_.type -eq 'Consumption' -and $_.meterName -notmatch 'Spot|Low Priority' } | Select-Object -First 1).retailPrice +} + +# Example: D4s_v5 -> D2s_v5 in eastus +$currentPrice = Get-VmSkuPrice -Sku "Standard_D4s_v5" -Region "eastus" +$targetPrice = Get-VmSkuPrice -Sku "Standard_D2s_v5" -Region "eastus" +$monthlySavings = ($currentPrice - $targetPrice) * 730 + +Write-Host "Current: Standard_D4s_v5 @ `$$currentPrice/hr" +Write-Host "Target: Standard_D2s_v5 @ `$$targetPrice/hr" +Write-Host "Monthly savings: `$$([math]::Round($monthlySavings, 2))" +Write-Host "Annual savings: `$$([math]::Round($monthlySavings * 12, 2))" +``` + +### Common downsize paths + +| Current Family | Typical Downsize | Notes | +|---------------|-----------------|-------| +| D-series (general purpose) | Halve vCPU count | D4s_v5 -> D2s_v5 | +| E-series (memory optimized) | Halve vCPU count | E8s_v5 -> E4s_v5 | +| F-series (compute optimized) | Halve vCPU count | F8s_v2 -> F4s_v2 | +| B-series (burstable) | Already burstable — verify CPU credits | Often right-sized | +| Cross-family | D-series -> B-series | For consistently low utilization | + +--- + +## Step 5: Safety checks + +### Burst requirements + +Check P99 CPU to ensure burst capacity is preserved: + +```kusto +Perf +| where TimeGenerated > ago(14d) +| where ObjectName == "Processor" and CounterName == "% Processor Time" and InstanceName == "_Total" +| where Computer == "{vmName}" +| summarize P99 = percentile(CounterValue, 99), Max = max(CounterValue) by Computer +``` + +If P99 > 80%, the VM has burst patterns that a smaller SKU may not handle. Consider B-series (burstable) instead of downsizing within the same family. + +### Instance size flexibility + +Azure Reservations support [instance size flexibility](https://learn.microsoft.com/azure/virtual-machines/reserved-vm-instance-size-flexibility) within a VM series. Downsizing within the same series (e.g., D4s_v5 -> D2s_v5) preserves reservation coverage with an adjusted ratio. + +Check if the VM is covered by a reservation: + +```kusto +advisorresources +| where type == "microsoft.advisor/recommendations" +| where properties.category == "Cost" +| where properties.recommendationTypeId == "e10b1381-5f0a-47ff-8c7b-37bd13d7c974" +| extend vmId = tostring(properties.resourceMetadata.resourceId) +| extend currentSku = tostring(properties.extendedProperties.currentSku) +| extend targetSku = tostring(properties.extendedProperties.targetSku) +| project vmId, currentSku, targetSku +``` + +If the VM is covered by a reservation and the downsize stays within the same series, the reservation automatically adjusts. Cross-series moves forfeit reservation coverage. + +### Azure Hybrid Benefit status + +Check if the VM uses Azure Hybrid Benefit (AHUB) for Windows or SQL licensing: + +```bash +az vm show --name {vmName} --resource-group {rg} \ + --query "{licenseType: licenseType, osType: storageProfile.osDisk.osType}" +``` + +| `licenseType` value | Meaning | +|--------------------|---------| +| `Windows_Server` | AHUB for Windows Server | +| `Windows_Client` | AHUB for Windows Client | +| `RHEL_BYOS` | BYOS for Red Hat | +| `SLES_BYOS` | BYOS for SUSE | +| `null` | No AHUB — paying full license cost | + +Ensure the target SKU preserves the same `licenseType` setting during resize. + +--- + +## Limitations + +| Limitation | Impact | Mitigation | +|-----------|--------|------------| +| Memory metrics require VM Insights | No memory data without agent | Deploy AMA agent, use CPU-only with conservative thresholds | +| Advisor uses 7-day window | May miss weekly patterns | Supplement with 14–30 day Azure Monitor analysis | +| No application-level metrics | CPU/memory don't capture app performance | Coordinate with app owners before resize | +| Resize requires VM restart | Brief downtime | Schedule during maintenance window | +| Cross-series resize loses reservation | Reservation coverage forfeited | Stay within same series when possible | + +--- + +## Permissions + +| Action | Required Role | +|--------|---------------| +| Query Advisor recommendations | Reader | +| Query Azure Monitor metrics | Monitoring Reader | +| Query Log Analytics workspace | Log Analytics Reader | +| Resize VM | Virtual Machine Contributor | +| Query Resource Graph | Reader | + +--- + +## References + +- [Right-size VMs (Azure Advisor)](https://learn.microsoft.com/azure/advisor/advisor-cost-recommendations#right-size-or-shutdown-underutilized-virtual-machines) +- [VM Insights overview](https://learn.microsoft.com/azure/azure-monitor/vm/vminsights-overview) +- [Instance size flexibility](https://learn.microsoft.com/azure/virtual-machines/reserved-vm-instance-size-flexibility) +- [Azure Monitor metrics](https://learn.microsoft.com/azure/azure-monitor/essentials/data-platform-metrics) +- [Azure Retail Prices API](https://learn.microsoft.com/rest/api/cost-management/retail-prices/azure-retail-prices) +- [Workload optimization (FinOps Framework)](https://learn.microsoft.com/cloud-computing/finops/framework/optimize/workloads) diff --git a/sreagent-templates/recipes/cost-management/config/subagents/azure-capacity-manager.yaml b/sreagent-templates/recipes/cost-management/config/subagents/azure-capacity-manager.yaml new file mode 100644 index 000000000..7653bd0a4 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/subagents/azure-capacity-manager.yaml @@ -0,0 +1,145 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgent +metadata: + name: azure-capacity-manager +spec: + instructions: 'You are azure-capacity-manager, an Azure capacity-management specialist for SaaS ISVs operating workloads in ISV-owned Azure subscriptions under EA or MCA billing. + + + Scope: + + - Handle Azure capacity management as part of the canonical FinOps Framework: Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization, Governance, Policy & Risk, and Automation, Tools, & Services. + + - Cover quota analysis and increases, quota groups and transfers, capacity reservation groups, sharing and overallocation, region access, zonal enablement, AKS capacity readiness, non-compute quota + constraints, and monitoring or governance alerts. + + - Treat capacity resilience as workload-specific evidence: model complete primary and recovery scale units or deployment stamps, including normal demand, temporary upgrade or replacement surge, and failover demand. + + - Use assigned Python capacity tools, knowledge files, and uploaded references to inspect current state, compare scenarios, and recommend next actions. + + - Do not call Azure Resource Graph, Azure CLI, deployment, data freshness, or `Task`. Use Teams and visualization tools only when the practitioner asks you to render or deliver the final evidence package. If Resource Graph or infrastructure remediation evidence is needed, return a precise evidence request to `finops-practitioner`. + + + Grounding rules: + + - Don''t rely on unstated internal knowledge for Azure behavior. + + - Ground claims in uploaded knowledge, Microsoft Learn, or live command output gathered in the session. + + - Cite the source path, URL, or command evidence you used. + + - If a claim can''t be grounded, say so instead of guessing. + + + Key distinctions: + + - Capacity reservations guarantee compute supply. Azure Reservations and savings plans reduce cost. They aren''t substitutes. + + - MACC is a separate billing-account contractual spending agreement. It is not a capacity reservation, Azure Reservation, savings plan, benefit recommendation, or commitment-discount metric, and MACC status is outside this agent''s scope. Never infer MACC amount, balance, decrement, burn, progress, or status from capacity or rate-optimization evidence. Require authoritative billing-account Consumption Lots and Events with `source` or `lotSource` equal to `ConsumptionCommitment`; otherwise return `INSUFFICIENT MACC EVIDENCE` to the practitioner. + + - Quota groups aggregate compute quota under management group scope, but they aren''t management groups and don''t inherit management group RBAC or policy. + + - Region access, quota increases, and zonal enablement are separate controls with separate approval paths. + + - Logical availability-zone mappings are subscription-specific, so verify physical zone alignment before cross-subscription sharing decisions. + + - VM quota is regional total-vCPU and VM-family-vCPU entitlement, not an exact-SKU capacity guarantee. Compute Fleet still consumes applicable subscription, region, and VM-family quota. + + - Classify failures before recommending action: quota exhaustion; hard access or offer restriction (`SkuNotAvailable`, `NotAvailableForSubscription`); over-constrained placement; or transient inventory stockout (`AllocationFailed`, `ZonalAllocationFailed`). Do not spend a retry budget or recommend quota for a hard restriction or stockout. + + - Do not prescribe fixed region counts or availability percentages. Base region and zone placement on workload criticality, recovery objectives, dependencies, latency, data residency, tested failover behavior, and cost. + + - Service Health is supporting evidence, not proof that a specific allocation will succeed or that every capacity issue has a published alternative or resolution time. + + + Python tool inventory: + + + - Use the database service quota tool for SQL DB, SQL MI, Cosmos DB, PostgreSQL Flex, and MySQL Flex quota, region access, and AZ-access risk checks. + + - Use the assigned Azure Resource Manager MCP tools for live commitment-benefit utilization, benefit recommendations, reservation transactions, retail pricing, and pricesheet workflows. Require the exact billing scope and agreement type before starting a pricesheet download. + + - Use `zone-mapping` before cross-subscription zonal placement or shared-CRG decisions. If mapping evidence is missing, stale, or inaccessible, fail the decision closed as insufficient evidence. + + + Decision framework: + + 1. Gather state for quota usage, reservation coverage, access constraints, billing scope, and workload demand. + + 2. Identify the limiting constraint before recommending action. + + 3. Compare options with numbers whenever you can. + + 4. Make a clear recommendation, document assumptions, and list exact next steps. + + 5. For capacity-resilience decisions, report workload tier and scope; primary and recovery scale units; surge and failover demand; demand horizon, provisioning lead time, and days of runway; subscription, region, zone, SKU/family, service, quota, and inventory constraints; CRG eligibility, reserved/consumed/overallocated state, and discount-adjusted carry cost; explicit fallback SKU, zone, and region options with compatibility and test status; evidence timestamp and freshness; confidence; owner; escalation deadline; and post-change validation. + + + Safety rules: + + - Stay review-oriented and propose write actions before executing them. + + - Don''t delete capacity reservations, reservation groups, or quota-group configuration without explicit confirmation. + + - Run what-if analysis before recommending reservation sizing, sharing, or reduction changes. + + - Call out zone-mismatch and cost risks before recommending zonal reservations or unused reserved capacity.' + handoffDescription: 'Route Azure capacity-management work here: quota analysis or increases, quota groups and transfers, capacity reservation group planning or sharing, region access, zonal enablement, + AKS capacity readiness, and monitoring or governance recommendations for Azure SaaS estates.' + handoffs: [] + tools: + - SearchMemory + - execute_python + - ExecutePythonCode + - PlotBarChart + - PlotPieChart + - PlotAreaChartWithCorrelation + - benefit-recommendations + - sku-availability + - vm-quota-usage + - non-compute-quotas + - db-service-quotas + - capacity-reservation-groups + - zone-mapping + - PostMessageToConversation + - PostCardToConversation + - ReplyWithMessageToConversation + - ReplyWithCardToConversation + - UpdateCardInConversation + - GetMessagesFromChannel + mcpTools: + - azure-resource-manager-mcp_list_benefit_utilization + - azure-resource-manager-mcp_get_benefit_recommendations + - azure-resource-manager-mcp_list_reservation_transactions + - azure-resource-manager-mcp_get_retail_prices + - azure-resource-manager-mcp_start_pricesheet_download + - azure-resource-manager-mcp_get_pricesheet_status + hooks: + PostToolUse: + - type: command + matcher: Bash|ExecuteShellCommand|Terminal|RunAzCliReadCommands|RunAzCliWriteCommands|execute_python|ExecutePythonCode + timeout: 30 + failMode: block + script: | + #!/usr/bin/env python3 + import json + import re + import sys + + context = json.load(sys.stdin) + tool_payload = json.dumps({ + "tool_input": context.get("tool_input", {}), + "tool_result": context.get("tool_result", ""), + }) + blocked = [ + (r"https://[^\s\"']+@github\.com", "Do not use authenticated GitHub remote URLs in shell commands. Use the configured credential provider or GitHub connector."), + (r"gh[pousr]_[A-Za-z0-9_]{20,}", "Do not pass GitHub tokens to shell tools or transcripts."), + (r"github_pat_[A-Za-z0-9_]{20,}", "GitHub PATs are forbidden. Do not pass personal access tokens to shell tools or transcripts."), + ] + + for pattern, reason in blocked: + if re.search(pattern, tool_payload): + print(json.dumps({"decision": "block", "reason": reason})) + sys.exit(0) + + print(json.dumps({"decision": "allow"})) diff --git a/sreagent-templates/recipes/cost-management/config/subagents/chief-financial-officer.yaml b/sreagent-templates/recipes/cost-management/config/subagents/chief-financial-officer.yaml new file mode 100644 index 000000000..fbba959b5 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/subagents/chief-financial-officer.yaml @@ -0,0 +1,47 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgent +metadata: + name: chief-financial-officer +spec: + instructions: 'You are the chief-financial-officer agent for FinOps and executive financial leadership scenarios. + + + Focus areas: + + - Lead strategic financial analysis tied to business outcomes, capital allocation, liquidity, and value creation. + + - Guide budgeting, rolling forecasts, scenario planning, variance analysis, and KPI-driven performance management. + + - Assess financial risk, controls, compliance exposure, and trade-offs across short-term execution and long-term sustainability. + + - Evaluate cloud and technology spending through a FinOps lens, including cost optimization, unit economics, commitment decisions, and investment prioritization. + + - Produce executive-ready recommendations for boards, CEOs, finance leaders, and FinOps stakeholders. + + + Response expectations: + + - Start with an executive summary. + + - Provide structured analysis with explicit assumptions, key drivers, and quantified reasoning when data is available. + + - Recommend prioritized actions, expected impact, risks, and mitigations. + + - Call out missing data, uncertainty, and follow-up steps needed for a decision. + + - Keep MACC contract analysis separate from reservation, savings-plan, capacity-reservation, and commitment-discount analysis. Do not use purchases, recommendations, utilization, coverage, or waste for those instruments as a proxy for MACC amount, remaining balance, decrement, burn, progress, or status. Accept MACC status only from authoritative billing-account Consumption Lots and Events with `source` or `lotSource` equal to `ConsumptionCommitment`; otherwise state `INSUFFICIENT MACC EVIDENCE`. + + - For month-level, fiscal-period, or scheduled financial reports, consume evidence packages from the FinOps practitioner and capacity specialist. You have only the Knowledge Base search tool (`SearchMemory`) so you can read uploaded recipe knowledge and prior memory. You have no operational tools; do not run raw data collection, delivery, or visualization yourself. + + - Keep the analysis strategic, financially rigorous, and actionable. + - Do not own autonomous scheduled tasks. Act as a consulted Finance and Leadership persona for budgeting, forecasting, unit economics, investment tradeoffs, commitment risk, and executive packaging.' + handoffDescription: Executive financial strategy and FinOps leadership for budgeting, forecasting, risk, capital allocation, and cost optimization. + handoffs: [] + tools: + - SearchMemory + systemTools: [] + mcpTools: [] + maxReflectionCount: 0 + customReflectionNote: '' + commonPrompts: [] + enableVanillaMode: true diff --git a/sreagent-templates/recipes/cost-management/config/subagents/finops-practitioner.yaml b/sreagent-templates/recipes/cost-management/config/subagents/finops-practitioner.yaml new file mode 100644 index 000000000..03ede2200 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/subagents/finops-practitioner.yaml @@ -0,0 +1,109 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgent +metadata: + name: finops-practitioner +spec: + instructions: 'You are a FinOps practitioner specializing in Azure cloud financial management and native Azure Cost Management operations. + + + Mission: + + - Guide teams on implementing FinOps practices using the six FinOps principles. + + - Help with cost allocation, shared-cost management, showback and chargeback, commitment optimization, budgeting, forecasting, governance, anomaly management, and FinOps operating-model design. + + - Translate FinOps concepts into practical implementation guidance using Azure Cost Management, Azure Advisor, and Azure Resource Graph capabilities. + + + Working approach: + + 1. Assess context first: business objectives, stakeholders, cloud footprint, current pain points, and the organization''s Crawl-Walk-Run maturity. + + 2. Align recommendations to the FinOps principles: collaboration, business value, ownership, timely and accurate data, central enablement, and effective use of the cloud variable-cost model. + + 3. Evaluate trade-offs clearly: expected value, implementation effort, risk, and organizational impact. + + 4. Deliver an action plan: prioritized next steps, owners, metrics, and sequencing appropriate to the team''s maturity. + + 5. State assumptions explicitly when required data is missing. + + + Focus areas: + + - Understand Usage & Cost: data quality, FOCUS-aligned reporting, tagging, allocation models, and shared-cost transparency. + + - Quantify Business Value: budgeting, forecasting, benchmarking, and unit economics. + + - Optimize Usage & Cost: rightsizing, commitment strategy, workload efficiency, pricing optimization, and automation opportunities. + + - Manage the FinOps Practice: enablement, governance, accountability, alerts, anomalies, and sustainable operating rhythms. + + + Guardrails: + + - Never recommend blind cost cutting without explaining business impact, trade-offs, and risk. + + - Never centralize all cloud decisions away from engineering teams. + + - Never hide cost data from the stakeholders who need visibility and ownership. + + - Never recommend commitments without considering utilization risk, flexibility, and long-term business plans. + + - Keep MACC separate from all discount and capacity constructs. A MACC is a billing-account contractual spending agreement; it is not an Azure Reservation, savings plan, capacity reservation, or commitment-discount metric. Never use Cost Management cost query results, reservation or savings-plan purchases, recommendations, utilization, coverage, or waste as evidence of MACC amount, balance, decrement, burn, progress, or status. Require authoritative billing-account Consumption Lots and Events with `source` or `lotSource` equal to `ConsumptionCommitment`; otherwise report `INSUFFICIENT MACC EVIDENCE`. + + - Always provide maturity-appropriate guidance. Keep Crawl-stage recommendations focused on foundational visibility and governance, Walk-stage guidance on process and optimization, and Run-stage guidance + on automation, policy, and advanced financial analytics. + + + Collaboration guidance: + + - Use Azure Cost Management, Azure Advisor, and Azure Resource Graph capabilities when they improve implementation speed, data quality, or governance rigor. + + - Own every scheduled task run and the FinOps operating rhythm. Use `SearchMemory` for available knowledge and prior memory, your assigned `azure-resource-manager-mcp_*` tools for Cost Management, forecast, AKS cost, dimension, budget, alert, benefit utilization, benefit recommendation, reservation transaction, retail price, and pricesheet queries, your assigned Python ARM tools (`resource-graph-query`, `benefit-recommendations`, `deploy-budget`, `deploy-bulk-budgets`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, and `suppress-advisor-recommendations`) for resource inventory, Advisor recommendations, and governance actions, your `PlotBarChart`, `PlotPieChart`, and `PlotAreaChartWithCorrelation` tools for charts, plus `GetMessagesFromChannel`, `PostMessageToConversation`, `PostCardToConversation`, `ReplyWithMessageToConversation`, `ReplyWithCardToConversation`, and `UpdateCardInConversation` for final Teams delivery. + + - Invoke `azure-capacity-manager` with `Task` for quota, capacity reservation group, regional SKU, zonal availability, AI/GPU capacity, database service quota, and benefit recommendation questions. + + - Invoke `chief-financial-officer` with `Task` for executive finance narratives, portfolio prioritization, and board-level recommendations. The CFO consumes evidence and provides decision framing; it does not gather data or own schedules. + + - Each `Task` invocation must include the exact task scope, evidence already collected, open questions, and requested output shape.' + handoffDescription: 'Use this agent for FinOps practice guidance: cost allocation, shared-cost strategy, showback or chargeback, commitment optimization, budgeting, forecasting, governance, anomaly response, + maturity assessment, and operating-model implementation aligned to FinOps principles and the Crawl-Walk-Run maturity model.' + handoffs: [] + tools: + - SearchMemory + - GetMessagesFromChannel + - PostMessageToConversation + - PostCardToConversation + - ReplyWithMessageToConversation + - ReplyWithCardToConversation + - UpdateCardInConversation + - PlotBarChart + - PlotPieChart + - PlotAreaChartWithCorrelation + - benefit-recommendations + - deploy-anomaly-alert + - deploy-budget + - deploy-bulk-anomaly-alerts + - deploy-bulk-budgets + - resource-graph-query + - suppress-advisor-recommendations + systemTools: [] + mcpTools: + - azure-resource-manager-mcp_query_costs + - azure-resource-manager-mcp_query_aks_costs + - azure-resource-manager-mcp_forecast_costs + - azure-resource-manager-mcp_list_dimensions + - azure-resource-manager-mcp_list_budgets + - azure-resource-manager-mcp_get_budget + - azure-resource-manager-mcp_create_budget + - azure-resource-manager-mcp_list_alerts + - azure-resource-manager-mcp_list_benefit_utilization + - azure-resource-manager-mcp_get_benefit_recommendations + - azure-resource-manager-mcp_list_reservation_transactions + - azure-resource-manager-mcp_get_retail_prices + - azure-resource-manager-mcp_start_pricesheet_download + - azure-resource-manager-mcp_get_pricesheet_status + maxReflectionCount: 0 + customReflectionNote: '' + commonPrompts: [] + enableVanillaMode: true diff --git a/sreagent-templates/recipes/cost-management/config/tools/benefit-recommendations.yaml b/sreagent-templates/recipes/cost-management/config/tools/benefit-recommendations.yaml new file mode 100644 index 000000000..b4349dbe4 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/benefit-recommendations.yaml @@ -0,0 +1,186 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: benefit-recommendations +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Gets Azure Cost Management benefit recommendations for savings plans and reserved instances + at a billing scope. Use it to identify commitment discount recommendation savings, cost, + term, and break-even details. Available parameters: billing_scope, lookback_period, term. + functionCode: |- + def main(**kwargs): + from urllib.parse import quote + + import requests + + billing_scope = kwargs.get("billing_scope") + lookback_period = kwargs.get("lookback_period") or "Last7Days" + term = kwargs.get("term") or "P3Y" + + result = { + "billing_scope": billing_scope, + "lookback_period": lookback_period, + "term": term, + "recommendations": [], + } + + if not billing_scope: + result["error"] = "billing_scope is required" + return result + + scope = str(billing_scope).strip().strip("/") + filter_expression = ( + f"properties/lookBackPeriod eq '{lookback_period}' " + f"and properties/term eq '{term}'" + ) + url = ( + f"https://management.azure.com/{scope}" + "/providers/Microsoft.CostManagement/benefitRecommendations" + f"?api-version=2025-03-01&$filter={quote(filter_expression, safe='')}" + ) + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token("https://management.azure.com/.default") + payload_items = [] + next_url = url + while next_url: + response = requests.get( + next_url, + headers={"Authorization": f"Bearer {token.token}"}, + timeout=300, + ) + response.raise_for_status() + payload = response.json() + payload_items.extend(payload.get("value", [])) + next_url = payload.get("nextLink") + except Exception as exc: + result["error"] = f"Failed to get Cost Management benefit recommendations: {exc}" + return result + + def _first(*values): + for value in values: + if value is not None: + return value + return None + + def _nested(source, *path): + current = source + for key in path: + if not isinstance(current, dict): + return None + current = current.get(key) + return current + + def _number(value): + if isinstance(value, (int, float)): + return value + try: + return float(value) + except (TypeError, ValueError): + return None + + def _difference(left, right): + left_number = _number(left) + right_number = _number(right) + if left_number is None or right_number is None: + return None + return left_number - right_number + + def _recommendation_type(item, properties): + raw_type = str( + _first( + item.get("kind"), + properties.get("benefitType"), + properties.get("recommendationType"), + "", + ) + ) + normalized = raw_type.lower() + if "reservation" in normalized or "reserved" in normalized: + return "RI" + if "savings" in normalized or "benefit" in normalized: + return "SP" + return raw_type or None + + for item in payload_items: + properties = item.get("properties", {}) if isinstance(item, dict) else {} + recommendation_details = properties.get("recommendationDetails") or {} + if not isinstance(recommendation_details, dict): + recommendation_details = {} + + total_cost = _first( + properties.get("totalCost"), + _nested(recommendation_details, "totalCost"), + ) + cost_without_benefit = properties.get("costWithoutBenefit") + savings = _first( + properties.get("estimatedSavingsAmount"), + properties.get("totalSavings"), + properties.get("savingsAmount"), + properties.get("annualSavingsAmount"), + _nested(recommendation_details, "averageSavingsAmount"), + _nested(recommendation_details, "savingsAmount"), + _difference(cost_without_benefit, total_cost), + ) + cost = _first( + total_cost, + properties.get("costWithBenefit"), + properties.get("recommendedQuantityCost"), + properties.get("purchaseAmount"), + _nested(recommendation_details, "cost"), + _nested(recommendation_details, "costWithBenefit"), + ) + recommendation_term = _first( + properties.get("term"), + _nested(recommendation_details, "term"), + term, + ) + break_even = _first( + properties.get("breakEvenTime"), + properties.get("breakEvenDate"), + properties.get("breakEvenPeriod"), + _nested(recommendation_details, "breakEvenTime"), + _nested(recommendation_details, "breakEvenDate"), + _nested(recommendation_details, "breakEvenPeriod"), + ) + + result["recommendations"].append( + { + "type": _recommendation_type(item, properties), + "savings": savings, + "cost": cost, + "total_cost": total_cost, + "cost_without_benefit": cost_without_benefit, + "term": recommendation_term, + "break_even": break_even, + "id": item.get("id"), + "name": item.get("name"), + } + ) + + result["count"] = len(result["recommendations"]) + return result + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: billing_scope + type: string + description: Azure billing scope or subscription scope to query for benefit recommendations. + required: true + - name: lookback_period + type: string + description: Optional lookback period for benefit recommendations. Defaults to Last7Days. + required: false + - name: term + type: string + description: Optional commitment term for benefit recommendations. Defaults to P3Y. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/capacity-reservation-groups.yaml b/sreagent-templates/recipes/cost-management/config/tools/capacity-reservation-groups.yaml new file mode 100644 index 000000000..6ab6cb305 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/capacity-reservation-groups.yaml @@ -0,0 +1,199 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: capacity-reservation-groups +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Lists Azure capacity reservation groups in a subscription and reports utilization by comparing reserved capacity against allocated virtual machines. Use it to identify unused reserved capacity, overallocated groups, and zone-specific capacity reservation waste. + functionCode: |- + def main(**kwargs): + import requests + + ARM_ENDPOINT = "https://management.azure.com" + ARM_SCOPE = "https://management.azure.com/.default" + API_VERSION = "2024-03-01" + + subscription_id = kwargs.get("subscription_id") + + if not subscription_id: + return { + "error": "subscription_id is required", + "capacity_reservation_groups": [], + } + + def get_attr(obj, *names, default=None): + for name in names: + if isinstance(obj, dict): + if name in obj and obj[name] is not None: + return obj[name] + properties = obj.get("properties") + if isinstance(properties, dict) and name in properties and properties[name] is not None: + return properties[name] + continue + value = getattr(obj, name, None) + if value is not None: + return value + return default + + def as_list(value): + if value is None: + return [] + if isinstance(value, list): + return value + if isinstance(value, (tuple, set)): + return list(value) + return [value] + + def as_int(value): + try: + return int(value or 0) + except (TypeError, ValueError): + return 0 + + def resource_group_from_id(resource_id): + if not resource_id: + return None + parts = [part for part in resource_id.split("/") if part] + for index, part in enumerate(parts): + if part.lower() == "resourcegroups" and index + 1 < len(parts): + return parts[index + 1] + return None + + def subresource_id(item): + return get_attr(item, "id", default=str(item)) + + result = { + "subscription_id": subscription_id, + "capacity_reservation_groups": [], + } + errors = [] + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token(ARM_SCOPE).token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + except Exception as exc: + result["error"] = f"Failed to initialize Azure ARM credential: {exc}" + return result + + try: + list_url = ( + f"{ARM_ENDPOINT}/subscriptions/{subscription_id}" + "/providers/Microsoft.Compute/capacityReservationGroups" + f"?api-version={API_VERSION}&$expand=virtualMachinesAssociated" + ) + crgs = [] + while list_url: + response = requests.get(list_url, headers=headers, timeout=120) + response.raise_for_status() + payload = response.json() + crgs.extend(payload.get("value", [])) + list_url = payload.get("nextLink") + + for crg in crgs: + name = get_attr(crg, "name", default="") + resource_id = get_attr(crg, "id", default="") + resource_group = resource_group_from_id(resource_id) + location = get_attr(crg, "location", default=None) + zones = as_list(get_attr(crg, "zones", default=[])) + detail = crg + + if resource_group and name: + try: + detail_url = ( + f"{ARM_ENDPOINT}/subscriptions/{subscription_id}/resourceGroups/{resource_group}" + f"/providers/Microsoft.Compute/capacityReservationGroups/{name}" + f"?api-version={API_VERSION}&$expand=instanceView" + ) + detail_response = requests.get(detail_url, headers=headers, timeout=60) + detail_response.raise_for_status() + detail = detail_response.json() + except Exception as exc: + errors.append({ + "name": name, + "resource_group": resource_group, + "error": f"Failed to get instance_view: {exc}", + }) + + instance_view = get_attr(detail, "instance_view", "instanceView") + reservations = as_list(get_attr(instance_view, "capacity_reservations", "capacityReservations", default=[])) + + reserved_count = 0 + allocated_ids = set() + reservation_names = [] + + for reservation in reservations: + reservation_name = get_attr(reservation, "name") + if reservation_name: + reservation_names.append(reservation_name) + + utilization_info = get_attr(reservation, "utilization_info", "utilizationInfo") + reserved_count += as_int(get_attr(utilization_info, "current_capacity", "currentCapacity", default=0)) + + allocated_vms = as_list(get_attr( + utilization_info, + "virtual_machines_allocated", + "virtualMachinesAllocated", + default=[], + )) + for vm in allocated_vms: + vm_id = subresource_id(vm) + if vm_id: + allocated_ids.add(vm_id) + + associated_vms = as_list(get_attr( + detail, + "virtual_machines_associated", + "virtualMachinesAssociated", + default=get_attr(crg, "virtual_machines_associated", "virtualMachinesAssociated", default=[]), + )) + + associated_ids = [subresource_id(vm) for vm in associated_vms if subresource_id(vm)] + allocated_count = len(allocated_ids) + utilization_pct = None + if reserved_count > 0: + utilization_pct = round((allocated_count / reserved_count) * 100, 2) + + result["capacity_reservation_groups"].append({ + "name": name, + "id": resource_id, + "resource_group": resource_group, + "location": location, + "zones": zones, + "zone": ",".join(str(zone) for zone in zones) if zones else None, + "reserved_count": reserved_count, + "allocated_count": allocated_count, + "utilization_pct": utilization_pct, + "waste": reserved_count > allocated_count, + "waste_count": max(reserved_count - allocated_count, 0), + "overallocated": allocated_count > reserved_count if reserved_count > 0 else False, + "capacity_reservations": reservation_names, + "virtual_machines_allocated": sorted(allocated_ids), + "virtual_machines_associated": associated_ids, + }) + except Exception as exc: + result["error"] = f"Failed to list capacity reservation groups: {exc}" + return result + + if errors: + result["errors"] = errors + + return result + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: subscription_id + type: string + description: Azure subscription ID to query for capacity reservation group utilization. + required: true diff --git a/sreagent-templates/recipes/cost-management/config/tools/db-service-quotas.yaml b/sreagent-templates/recipes/cost-management/config/tools/db-service-quotas.yaml new file mode 100644 index 000000000..73a2c823f --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/db-service-quotas.yaml @@ -0,0 +1,562 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: db-service-quotas +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Reports Azure database service quota usage and regional access signals through ARM REST. + Covers SQL DB regional vCore quota for Azure SQL Database and Data Warehouse. + Covers SQL MI managed instance vCore, per-family, and subnet quota usage. + Covers Cosmos DB subscription account count, per-region account count, and region/AZ access. + Covers PostgreSQL Flexible Server regional access, HA capability, and per-SKU vCore quota. + Covers MySQL Flexible Server regional access and zone-redundant HA capability. + Use include_capabilities for detailed SQL MI hardware family, PostgreSQL, and MySQL capability rows. + Results normalize locations, utilization percentages, at-risk flags, provider errors, and soft skips. + The tool uses Azure ARM REST with DefaultAzureCredential and the agent system-managed identity by default. + Inspired by https://github.com/naspinall-MS/az-quota-helper (MIT). + functionCode: |- + def main(**kwargs): + import re + import requests + + ARM_ENDPOINT = "https://management.azure.com" + ARM_SCOPE = "https://management.azure.com/.default" + LOCATIONS_API_VERSION = "2022-12-01" + SQL_API_VERSION = "2025-01-01" + COSMOS_API_VERSION = "2024-11-15" + PG_CAPABILITIES_API_VERSION = "2024-08-01" + PG_USAGES_API_VERSION = "2025-06-01-preview" + MYSQL_CAPABILITIES_API_VERSION = "2023-12-30" + SERVICE_DISPLAY = { + "SQLDB": "SQL DB", + "SQLMI": "SQL MI", + "COSMOSDB": "Cosmos DB", + "POSTGRESQL": "PostgreSQL Flex", + "MYSQL": "MySQL Flex", + } + ALL_SERVICE_KEYS = ["SQLDB", "SQLMI", "COSMOSDB", "POSTGRESQL", "MYSQL"] + + subscription_id = kwargs.get("subscription_id") + include_capabilities = bool(kwargs.get("include_capabilities")) + + result = { + "subscription_id": subscription_id, + "services": [], + "locations": [], + "quotas": [], + "region_access": [], + "at_risk_count": 0, + "warning_count": 0, + "critical_count": 0, + "errors": [], + "suppressed_error_count": 0, + } + if include_capabilities: + result["capabilities"] = [] + + if not subscription_id: + result["error"] = "subscription_id is required" + result["errors"].append({"kind": "validation", "message": "subscription_id is required"}) + return result + + def normalize_location(value): + return "".join(character for character in str(value or "").lower() if character.isalnum()) + + def parse_locations(value): + if not value: + return [] + if isinstance(value, str): + raw = value.split(",") + elif isinstance(value, (list, tuple, set)): + raw = value + else: + raw = [value] + return [normalize_location(item) for item in raw if normalize_location(item)] + + def parse_services(value): + if value is None or value == "": + return set(ALL_SERVICE_KEYS), [] + if isinstance(value, str): + raw = [item.strip() for item in value.split(",")] + elif isinstance(value, (list, tuple, set)): + raw = [str(item).strip() for item in value] + else: + raw = [str(value).strip()] + tokens = [item.upper().replace(" ", "") for item in raw if item] + if not tokens or "ALL" in tokens: + invalid = [token for token in tokens if token not in set(ALL_SERVICE_KEYS + ["ALL"])] + return set(ALL_SERVICE_KEYS), invalid + invalid = [token for token in tokens if token not in set(ALL_SERVICE_KEYS + ["ALL"])] + return set(tokens) - {"ALL"}, invalid + + selected_services, invalid_services = parse_services(kwargs.get("services", "All")) + if invalid_services: + result["error"] = "Invalid services parameter" + result["errors"].append({ + "kind": "validation", + "message": "Invalid services parameter", + "invalid_services": invalid_services, + "allowed_services": ALL_SERVICE_KEYS + ["ALL"], + }) + return result + + result["services"] = [SERVICE_DISPLAY[key] for key in ALL_SERVICE_KEYS if key in selected_services] + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token(ARM_SCOPE).token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + except Exception as exc: + result["error"] = f"Failed to initialize Azure ARM credential: {exc}" + result["errors"].append({"kind": "auth", "message": str(exc)}) + return result + + def endpoint(path): + if str(path).startswith("http"): + return path + return f"{ARM_ENDPOINT}{path}" + + def add_error(kind, service, location, url, message, status_code=None, reason=None, suppressed=False): + row = { + "kind": kind, + "service": service, + "location": normalize_location(location) if location else None, + "source_endpoint": url, + "message": message, + } + if status_code is not None: + row["status_code"] = status_code + if reason: + row["reason"] = reason + if suppressed: + row["suppressed"] = True + result["suppressed_error_count"] += 1 + result["errors"].append(row) + + def arm_get(path, service, location=None, suppress_cosmos_locations_5xx=False): + url = endpoint(path) + try: + response = requests.get(url, headers=headers, timeout=60) + except Exception as exc: + add_error("request_failed", service, location, url, str(exc)) + return None + + if response.status_code >= 400: + body = "" + try: + body = response.text or "" + except Exception: + body = "" + if response.status_code == 400 and "NoRegisteredProviderFound" in body: + add_error("provider_not_registered", service, location, url, body[:1000], response.status_code, response.reason, True) + return None + if response.status_code == 404: + add_error("service_not_in_region", service, location, url, body[:1000], response.status_code, response.reason, True) + return None + if suppress_cosmos_locations_5xx and response.status_code >= 500: + add_error("service_not_in_region", service, location, url, body[:1000], response.status_code, response.reason, True) + return None + add_error("http_error", service, location, url, body[:1000], response.status_code, response.reason) + return None + + try: + return response.json() + except Exception as exc: + add_error("json_parse_failed", service, location, url, str(exc)) + return None + + def arm_get_paged(path, service, location=None): + rows = [] + next_url = endpoint(path) + while next_url: + payload = arm_get(next_url, service, location) + if payload is None: + return rows + rows.extend(payload.get("value", [])) + next_url = payload.get("nextLink") + return rows + + def to_float(value): + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + def to_int_or_none(value): + number = to_float(value) + if number is None: + return None + return int(number) + + def first_present(record, keys, default=None): + if not isinstance(record, dict): + return default + for key in keys: + value = record.get(key) + if value is not None: + return value + return default + + def name_value(value): + if isinstance(value, dict): + return value.get("localizedValue") or value.get("localized_value") or value.get("value") or value.get("name") or str(value) + return str(value) if value is not None else "" + + def clean_metric(value): + metric = name_value(value).strip() + metric = re.sub(r"\s+for\s+.*$", "", metric, flags=re.IGNORECASE).strip() + return metric.rstrip(".").strip() + + def add_quota(service, scope, metric, current_value, limit, unit, location, source_endpoint): + current_number = to_float(current_value) + limit_number = to_float(limit) + normalized_limit = int(limit_number) if limit_number is not None else None + available = None + utilization_pct = None + if current_number is not None and limit_number is not None: + available = int(limit_number - current_number) + if limit_number > 0: + utilization_pct = round((current_number / limit_number) * 100, 2) + at_risk_80 = bool(utilization_pct is not None and utilization_pct > 80) + at_risk_95 = bool(utilization_pct is not None and utilization_pct > 95) + if at_risk_80: + result["at_risk_count"] += 1 + if at_risk_80 and not at_risk_95: + result["warning_count"] += 1 + if at_risk_95: + result["critical_count"] += 1 + result["quotas"].append({ + "service": service, + "scope": scope, + "metric": metric, + "current_value": current_value, + "limit": normalized_limit, + "available": available, + "utilization_pct": utilization_pct, + "unit": unit, + "location": normalize_location(location) if location else None, + "at_risk_80": at_risk_80, + "at_risk_95": at_risk_95, + "source_endpoint": source_endpoint, + }) + + def add_region_access(service, location, region_allowed, az_allowed, notes, supports_availability_zone, source_endpoint): + if isinstance(notes, (list, tuple)): + note_text = "; ".join([str(note) for note in notes if note]) + else: + note_text = str(notes or "") + result["region_access"].append({ + "service": service, + "location": normalize_location(location), + "access_allowed_for_region": bool(region_allowed), + "access_allowed_for_az": az_allowed, + "notes": note_text, + "supports_availability_zone": supports_availability_zone, + "source_endpoint": source_endpoint, + }) + + locations_payload = arm_get( + f"/subscriptions/{subscription_id}/locations?api-version={LOCATIONS_API_VERSION}", + "Azure Resource Manager", + ) + region_has_az = {} + subscription_locations = [] + if locations_payload: + for item in locations_payload.get("value", []): + loc_name = normalize_location(item.get("name") or item.get("displayName")) + if not loc_name: + continue + subscription_locations.append(loc_name) + mappings = item.get("availabilityZoneMappings") or [] + region_has_az[loc_name] = bool(mappings) + + requested_locations = parse_locations(kwargs.get("location")) + if requested_locations: + locations = requested_locations + else: + locations = subscription_locations + result["locations"] = locations + if not locations: + result["error"] = "location did not contain any valid region names and subscription locations could not be enumerated" + result["errors"].append({"kind": "validation", "message": result["error"]}) + return result + + def sql_region_capabilities(location): + path = f"/subscriptions/{subscription_id}/providers/Microsoft.Sql/locations/{location}/capabilities?api-version={SQL_API_VERSION}" + url = endpoint(path) + payload = arm_get(path, "SQL", location) + if payload is None: + return + root_properties = payload.get("properties", payload) + region_available = root_properties.get("status") == "Available" or payload.get("status") == "Available" + mi_families = [] + versions = root_properties.get("supportedManagedInstanceVersions") or payload.get("supportedManagedInstanceVersions") or [] + for version in versions: + for edition in version.get("supportedEditions", []) or []: + for family in edition.get("supportedFamilies", []) or []: + family_name = family.get("name") or family.get("family") or family.get("hardwareFamily") or "Unknown" + zone_redundant = bool(family.get("zoneRedundant")) + mi_families.append({"family": family_name, "zone_redundant": zone_redundant}) + if include_capabilities: + result.setdefault("capabilities", []).append({ + "service": "SQL MI", + "location": normalize_location(location), + "hardware_family": family_name, + "zone_redundant": zone_redundant, + "edition": edition.get("name"), + "version": version.get("name"), + "source_endpoint": url, + }) + if "SQLDB" in selected_services: + notes = [] if region_available else ["SQL region status is not Available"] + notes.append("AZ access data not yet available for SQL DB") + add_region_access("SQL DB", location, region_available, "N/A", notes, None, url) + if "SQLMI" in selected_services: + zr_count = sum(1 for family in mi_families if family["zone_redundant"]) + family_count = len(mi_families) + notes = [] + if not region_available: + az_flag = False + notes.append("SQL region status is not Available") + elif not region_has_az.get(normalize_location(location), False): + az_flag = "AZNotSupported" + notes.append("Region does not advertise availability zones") + elif family_count > 0 and zr_count == family_count: + az_flag = True + notes.append("All SQL MI hardware families report zone redundancy") + elif zr_count > 0: + az_flag = "Partial" + notes.append("Some SQL MI hardware families report zone redundancy") + else: + az_flag = False + notes.append("No SQL MI hardware families report zone redundancy") + add_region_access("SQL MI", location, region_available, az_flag, notes, region_has_az.get(normalize_location(location), None), url) + + def collect_sql_db_quota(location): + path = f"/subscriptions/{subscription_id}/providers/Microsoft.Sql/locations/{location}/usages/RegionalVCoreQuotaForSQLDBAndDW?api-version={SQL_API_VERSION}" + url = endpoint(path) + payload = arm_get(path, "SQL DB", location) + if payload is None: + return + items = payload.get("value") if isinstance(payload.get("value"), list) else [payload] + for item in items: + props = item.get("properties", item) + metric = clean_metric(props.get("displayName") or item.get("displayName") or item.get("name")) + add_quota("SQL DB", f"Region ({normalize_location(location)})", metric, props.get("currentValue"), props.get("limit"), props.get("unit"), location, url) + + def collect_sql_mi_quotas(location): + path = f"/subscriptions/{subscription_id}/providers/Microsoft.Sql/locations/{location}/usages?api-version={SQL_API_VERSION}" + url = endpoint(path) + payload = arm_get(path, "SQL MI", location) + if payload is None: + return + for item in payload.get("value", []): + props = item.get("properties", item) + raw_name = name_value(item.get("name") or props.get("name")) + display_name = clean_metric(props.get("displayName") or raw_name) + include = bool(re.search(r"ManagedInstance|SqlMI|SubnetFor", raw_name, flags=re.IGNORECASE)) or display_name in ["VCore Quota", "Subnet Quota"] + if not include or re.search(r"Free", raw_name, flags=re.IGNORECASE): + continue + add_quota("SQL MI", f"Region ({normalize_location(location)})", display_name, props.get("currentValue"), props.get("limit"), props.get("unit"), location, url) + + cosmos_locations_cache = None + cosmos_accounts_cache = None + + def get_cosmos_locations(): + nonlocal cosmos_locations_cache + if cosmos_locations_cache is not None: + return cosmos_locations_cache + path = f"/subscriptions/{subscription_id}/providers/Microsoft.DocumentDB/locations?api-version={COSMOS_API_VERSION}" + payload = arm_get(path, "Cosmos DB", suppress_cosmos_locations_5xx=True) + cosmos_locations_cache = payload.get("value", []) if payload else [] + return cosmos_locations_cache + + def get_cosmos_accounts(): + nonlocal cosmos_accounts_cache + if cosmos_accounts_cache is not None: + return cosmos_accounts_cache + path = f"/subscriptions/{subscription_id}/providers/Microsoft.DocumentDB/databaseAccounts?api-version={COSMOS_API_VERSION}" + cosmos_accounts_cache = arm_get_paged(path, "Cosmos DB") + return cosmos_accounts_cache + + def collect_cosmos(location): + loc_endpoint = endpoint(f"/subscriptions/{subscription_id}/providers/Microsoft.DocumentDB/locations?api-version={COSMOS_API_VERSION}") + matched = None + for item in get_cosmos_locations(): + item_id = item.get("id", "") + item_loc = item_id.split("/locations/")[-1] if "/locations/" in item_id else item.get("name") + if normalize_location(item_loc) == normalize_location(location): + matched = item + break + if matched: + props = matched.get("properties", {}) + supports_az = props.get("supportsAvailabilityZone") + region_allowed = bool(props.get("isSubscriptionRegionAccessAllowedForRegular")) + az_allowed_raw = props.get("isSubscriptionRegionAccessAllowedForAz") + notes = [] + if not region_allowed: + notes.append("Subscription regular region access is blocked") + if supports_az is False: + az_allowed = "AZNotSupported" + notes.append("Cosmos DB does not support availability zones in this region") + else: + az_allowed = bool(az_allowed_raw) + if not az_allowed: + notes.append("Subscription AZ access is blocked") + add_region_access("Cosmos DB", location, region_allowed, az_allowed, notes, supports_az, loc_endpoint) + else: + add_error("service_not_in_region", "Cosmos DB", location, loc_endpoint, "Cosmos DB location metadata was not found", suppressed=True) + + def collect_cosmos_quotas(): + accounts = get_cosmos_accounts() + accounts_endpoint = endpoint(f"/subscriptions/{subscription_id}/providers/Microsoft.DocumentDB/databaseAccounts?api-version={COSMOS_API_VERSION}") + add_quota("Cosmos DB", "Subscription", "Total Database Accounts (default soft limit: 50)", len(accounts), 50, "Count", None, accounts_endpoint) + for location in locations: + count = 0 + for account in accounts: + props = account.get("properties", {}) + account_locations = props.get("locations") or [] + if any(normalize_location(item.get("locationName")) == normalize_location(location) for item in account_locations): + count += 1 + elif normalize_location(account.get("location")) == normalize_location(location): + count += 1 + add_quota("Cosmos DB", f"Region ({normalize_location(location)})", "Database Accounts in Region", count, None, "Count", location, accounts_endpoint) + + def collect_postgresql_capabilities(location): + path = f"/subscriptions/{subscription_id}/providers/Microsoft.DBforPostgreSQL/locations/{location}/capabilities?api-version={PG_CAPABILITIES_API_VERSION}" + url = endpoint(path) + payload = arm_get(path, "PostgreSQL Flex", location) + if payload is None: + return + caps = payload.get("value", []) + flex = next((item for item in caps if item.get("name") == "FlexibleServerCapabilities"), None) + if not flex: + add_region_access("PostgreSQL Flex", location, False, False, "FlexibleServerCapabilities not found", region_has_az.get(normalize_location(location), None), url) + return + props = flex.get("properties", flex) + restricted = props.get("restricted") == "Enabled" or flex.get("restricted") == "Enabled" + zr_supported = props.get("zoneRedundantHaSupported") == "Enabled" or flex.get("zoneRedundantHaSupported") == "Enabled" + notes = [] + if restricted: + notes.append("Region is restricted") + az_flag = False + elif not region_has_az.get(normalize_location(location), False): + notes.append("Region does not advertise availability zones") + az_flag = "AZNotSupported" + else: + az_flag = bool(zr_supported) + if not zr_supported: + notes.append("Zone redundant HA is not enabled") + add_region_access("PostgreSQL Flex", location, not restricted, az_flag, notes, region_has_az.get(normalize_location(location), None), url) + if include_capabilities: + result.setdefault("capabilities", []).append({ + "service": "PostgreSQL Flex", + "location": normalize_location(location), + "geoBackupSupported": first_present(props, ["geoBackupSupported", "geoBackupSupportedByDefault"]), + "zoneRedundantHaSupported": first_present(props, ["zoneRedundantHaSupported"]), + "zoneRedundantHaAndGeoBackupSupported": first_present(props, ["zoneRedundantHaAndGeoBackupSupported"]), + "onlineResizeSupported": first_present(props, ["onlineResizeSupported"]), + "storageAutoGrowthSupported": first_present(props, ["storageAutoGrowthSupported"]), + "source_endpoint": url, + }) + + def collect_postgresql_quotas(location): + path = f"/subscriptions/{subscription_id}/providers/Microsoft.DBforPostgreSQL/locations/{location}/resourceType/flexibleServers/usages?api-version={PG_USAGES_API_VERSION}" + url = endpoint(path) + for item in arm_get_paged(path, "PostgreSQL Flex", location): + metric = name_value(item.get("name")) + add_quota("PostgreSQL Flex", f"Region ({normalize_location(location)})", metric, item.get("currentValue"), item.get("limit"), item.get("unit"), location, url) + + def collect_mysql_capabilities(location): + path = f"/subscriptions/{subscription_id}/providers/Microsoft.DBforMySQL/locations/{location}/capabilities?api-version={MYSQL_CAPABILITIES_API_VERSION}" + url = endpoint(path) + payload = arm_get(path, "MySQL Flex", location) + if payload is None: + return + caps = payload.get("value", []) + if not caps: + add_region_access("MySQL Flex", location, False, False, "No MySQL Flexible Server capabilities returned; region may be restricted", region_has_az.get(normalize_location(location), None), url) + return + zr_supported = False + for item in caps: + props = item.get("properties", item) + ha_modes = props.get("supportedHAMode") or item.get("supportedHAMode") or [] + if isinstance(ha_modes, str): + ha_modes = [ha_modes] + if any("ZoneRedundant" in str(mode) for mode in ha_modes): + zr_supported = True + if include_capabilities: + zones = props.get("supportedZones") or props.get("supportedAvailabilityZones") or [props.get("zone")] + zones = [zone for zone in zones if zone] + if not zones: + zones = [None] + for zone in zones: + result.setdefault("capabilities", []).append({ + "service": "MySQL Flex", + "location": normalize_location(location), + "zone": zone, + "supportedHAMode": ha_modes, + "geoBackupSupported": first_present(props, ["geoBackupSupported", "geoBackupSupport"]), + "source_endpoint": url, + }) + notes = [] + if not region_has_az.get(normalize_location(location), False): + az_flag = "AZNotSupported" + notes.append("Region does not advertise availability zones") + else: + az_flag = bool(zr_supported) + if not zr_supported: + notes.append("ZoneRedundant HA mode is not advertised") + add_region_access("MySQL Flex", location, True, az_flag, notes, region_has_az.get(normalize_location(location), None), url) + + for location in locations: + if "SQLDB" in selected_services or "SQLMI" in selected_services: + sql_region_capabilities(location) + if "SQLDB" in selected_services: + collect_sql_db_quota(location) + if "SQLMI" in selected_services: + collect_sql_mi_quotas(location) + if "COSMOSDB" in selected_services: + collect_cosmos(location) + if "POSTGRESQL" in selected_services: + collect_postgresql_capabilities(location) + collect_postgresql_quotas(location) + if "MYSQL" in selected_services: + collect_mysql_capabilities(location) + + if "COSMOSDB" in selected_services: + collect_cosmos_quotas() + + return result + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: subscription_id + type: string + description: Azure subscription ID to query for database service quotas and access. + required: true + - name: location + type: string + description: Optional comma-separated or single Azure region. If omitted, all ARM subscription locations are enumerated. + required: false + - name: services + type: string + description: Optional comma-separated subset of SqlDB, SqlMI, CosmosDB, PostgreSQL, MySQL, or All. Defaults to All. + required: false + - name: include_capabilities + type: boolean + description: Include SQL MI hardware-family ZR detail, PostgreSQL/MySQL HA modes, and geo-backup capability rows. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/deploy-anomaly-alert.yaml b/sreagent-templates/recipes/cost-management/config/tools/deploy-anomaly-alert.yaml new file mode 100644 index 000000000..bc6e677e2 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/deploy-anomaly-alert.yaml @@ -0,0 +1,114 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: deploy-anomaly-alert +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Creates or updates a Cost Management scheduled action for anomaly detection + in a subscription and sends anomaly alert notifications to email recipients. + functionCode: |- + def main(**kwargs): + import re + import requests + + subscription_id = kwargs.get("subscription_id") + email_recipients = kwargs.get("email_recipients") + scheduled_action_name = kwargs.get("scheduled_action_name") or "cost-anomaly-alert" + guid_pattern = r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + email_pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + + if not subscription_id: + return {"status": "failed", "error": "subscription_id is required"} + if not isinstance(subscription_id, str) or not re.match(guid_pattern, subscription_id): + return {"status": "failed", "error": "subscription_id must be a valid GUID"} + if not email_recipients: + return {"status": "failed", "error": "email_recipients is required"} + + if isinstance(email_recipients, str): + recipients = [item.strip() for item in email_recipients.split(",") if item.strip()] + else: + recipients = [str(item).strip() for item in email_recipients if str(item).strip()] + if not recipients: + return {"status": "failed", "error": "email_recipients must contain at least one email"} + invalid_emails = [email for email in recipients if not re.match(email_pattern, email)] + if invalid_emails: + return {"status": "failed", "error": f"invalid email address: {invalid_emails[0]}"} + + body = { + "kind": "InsightAlert", + "properties": { + "displayName": "Cost anomaly alert", + "status": "Enabled", + "viewId": ( + f"/subscriptions/{subscription_id}/providers/Microsoft.CostManagement/" + "views/ms:DailyAnomalyByResourceGroup" + ), + "schedule": { + "frequency": "Daily", + "hourOfDay": 8, + "daysOfWeek": [], + "weeksOfMonth": [], + }, + "notification": { + "subject": "Azure cost anomaly detected", + "to": recipients, + "language": "en-us", + }, + }, + } + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token("https://management.azure.com/.default").token + except Exception as exc: + return {"status": "failed", "error": f"failed to acquire Azure credential: {exc}"} + url = ( + f"https://management.azure.com/subscriptions/{subscription_id}" + f"/providers/Microsoft.CostManagement/scheduledActions/{scheduled_action_name}" + "?api-version=2022-10-01" + ) + try: + response = requests.put( + url, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + json=body, + timeout=120, + ) + except Exception as exc: + return {"status": "failed", "error": f"anomaly alert PUT request failed: {exc}"} + + result = { + "subscription_id": subscription_id, + "scheduled_action_name": scheduled_action_name, + "email_recipients": recipients, + "status_code": response.status_code, + } + try: + result["anomaly_alert"] = response.json() + except ValueError: + result["response_text"] = response.text + if response.status_code in (200, 201): + result["status"] = "created" if response.status_code == 201 else "updated" + else: + result["status"] = "failed" + result["error"] = response.text + return result + timeoutSeconds: 240 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: subscription_id + type: string + description: Azure subscription ID where the anomaly alert scheduled action will be created. + required: true + - name: email_recipients + type: string + description: Comma-separated email recipients for anomaly notifications. + required: true diff --git a/sreagent-templates/recipes/cost-management/config/tools/deploy-budget.yaml b/sreagent-templates/recipes/cost-management/config/tools/deploy-budget.yaml new file mode 100644 index 000000000..e015d25e1 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/deploy-budget.yaml @@ -0,0 +1,176 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: deploy-budget +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Creates or updates a subscription-level Azure Cost Management budget. + Uses ARM REST to PUT Microsoft.Consumption/budgets with notification + contact emails for budget coverage remediation. + functionCode: |- + def main(**kwargs): + from datetime import date + import re + import requests + + subscription_id = kwargs.get("subscription_id") + budget_name = kwargs.get("budget_name") or "SubscriptionBudget" + amount = kwargs.get("amount") + time_grain = kwargs.get("time_grain") or "Monthly" + contact_emails = kwargs.get("contact_emails") + guid_pattern = r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + email_pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + + if not subscription_id: + return {"status": "failed", "error": "subscription_id is required"} + if not isinstance(subscription_id, str) or not re.match(guid_pattern, subscription_id): + return {"status": "failed", "error": "subscription_id must be a valid GUID"} + if amount is None or str(amount).strip() == "": + return {"status": "failed", "error": "amount is required"} + if time_grain not in ["Monthly", "Quarterly", "Annually"]: + return {"status": "failed", "error": "time_grain must be one of Monthly, Quarterly, or Annually"} + if not contact_emails: + return {"status": "failed", "error": "contact_emails is required"} + + if isinstance(contact_emails, str): + emails = [item.strip() for item in contact_emails.split(",") if item.strip()] + else: + emails = [str(item).strip() for item in contact_emails if str(item).strip()] + if not emails: + return {"status": "failed", "error": "contact_emails must contain at least one email"} + invalid_emails = [email for email in emails if not re.match(email_pattern, email)] + if invalid_emails: + return {"status": "failed", "error": f"invalid email address: {invalid_emails[0]}"} + + try: + amount_number = float(amount) + except (TypeError, ValueError): + return {"status": "failed", "error": "amount must be numeric"} + if amount_number <= 0: + return {"status": "failed", "error": "amount must be greater than 0"} + + today = date.today() + next_month = today.month + 1 + year = today.year + (1 if next_month == 13 else 0) + month = 1 if next_month == 13 else next_month + start_date = date(year, month, 1) + end_date = date(year + 1, month, 1) + + body = { + "properties": { + "category": "Cost", + "amount": amount_number, + "timeGrain": time_grain, + "timePeriod": { + "startDate": start_date.isoformat(), + "endDate": end_date.isoformat(), + }, + "notifications": { + "Actual_GreaterThan_50_Percent": { + "enabled": True, + "operator": "GreaterThan", + "threshold": 50, + "thresholdType": "Actual", + "contactEmails": emails, + "contactRoles": ["Owner", "Contributor"], + }, + "Actual_GreaterThan_75_Percent": { + "enabled": True, + "operator": "GreaterThan", + "threshold": 75, + "thresholdType": "Actual", + "contactEmails": emails, + "contactRoles": ["Owner", "Contributor"], + }, + "Actual_GreaterThan_90_Percent": { + "enabled": True, + "operator": "GreaterThan", + "threshold": 90, + "thresholdType": "Actual", + "contactEmails": emails, + "contactRoles": ["Owner", "Contributor"], + }, + "Forecasted_GreaterThan_100_Percent": { + "enabled": True, + "operator": "GreaterThan", + "threshold": 100, + "thresholdType": "Forecasted", + "contactEmails": emails, + "contactRoles": ["Owner", "Contributor"], + }, + }, + } + } + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token("https://management.azure.com/.default").token + except Exception as exc: + return {"status": "failed", "error": f"failed to acquire Azure credential: {exc}"} + url = ( + f"https://management.azure.com/subscriptions/{subscription_id}" + f"/providers/Microsoft.Consumption/budgets/{budget_name}" + "?api-version=2023-11-01" + ) + try: + response = requests.put( + url, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + json=body, + timeout=120, + ) + except Exception as exc: + return {"status": "failed", "error": f"budget PUT request failed: {exc}"} + + result = { + "subscription_id": subscription_id, + "budget_name": budget_name, + "amount": amount_number, + "time_grain": time_grain, + "contact_emails": emails, + "status_code": response.status_code, + } + try: + result["budget"] = response.json() + except ValueError: + result["response_text"] = response.text + + if response.status_code in (200, 201): + result["status"] = "created" if response.status_code == 201 else "updated" + else: + result["status"] = "failed" + result["error"] = response.text + return result + timeoutSeconds: 240 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: subscription_id + type: string + description: Azure subscription ID where the budget will be created or updated. + required: true + - name: budget_name + type: string + description: |- + Budget name. Default: SubscriptionBudget. + required: false + - name: amount + type: string + description: Budget amount in the subscription billing currency. + required: true + - name: time_grain + type: string + description: |- + Budget reset period, such as Monthly, Quarterly, or Annually. Default: Monthly. + required: false + - name: contact_emails + type: string + description: Comma-separated email addresses that receive budget notifications. + required: true diff --git a/sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-anomaly-alerts.yaml b/sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-anomaly-alerts.yaml new file mode 100644 index 000000000..1cceae3a6 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-anomaly-alerts.yaml @@ -0,0 +1,153 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: deploy-bulk-anomaly-alerts +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Discovers enabled subscriptions in a management group with Azure Resource Graph + and creates or updates Cost Management anomaly alert scheduled actions per subscription. + functionCode: |- + def main(**kwargs): + import re + import requests + + ARM_ENDPOINT = "https://management.azure.com" + GRAPH_API_VERSION = "2022-10-01" + + management_group = kwargs.get("management_group") + email_recipients = kwargs.get("email_recipients") + scheduled_action_name = kwargs.get("scheduled_action_name") or "cost-anomaly-alert" + guid_pattern = r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + email_pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + + if not isinstance(management_group, str) or not management_group.strip(): + return {"status": "failed", "error": "management_group is required", "deployments": []} + if not email_recipients: + return {"status": "failed", "error": "email_recipients is required", "deployments": []} + management_group = management_group.strip() + + if isinstance(email_recipients, str): + recipients = [item.strip() for item in email_recipients.split(",") if item.strip()] + else: + recipients = [str(item).strip() for item in email_recipients if str(item).strip()] + if not recipients: + return {"status": "failed", "error": "email_recipients must contain at least one email", "deployments": []} + invalid_emails = [email for email in recipients if not re.match(email_pattern, email)] + if invalid_emails: + return {"status": "failed", "error": f"invalid email address: {invalid_emails[0]}", "deployments": []} + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token("https://management.azure.com/.default").token + except Exception as exc: + return {"status": "failed", "error": f"failed to acquire Azure credential: {exc}", "deployments": []} + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + query = ( + "ResourceContainers " + "| where type =~ 'microsoft.resources/subscriptions' " + "| where properties.state =~ 'Enabled' " + "| project subscriptionId, name" + ) + graph_url = f"{ARM_ENDPOINT}/providers/Microsoft.ResourceGraph/resources?api-version={GRAPH_API_VERSION}" + subscriptions = [] + skip = 0 + page_size = 1000 + while True: + body = { + "managementGroups": [management_group], + "query": query, + "options": {"$top": page_size, "$skip": skip}, + } + try: + response = requests.post(graph_url, headers=headers, json=body, timeout=60) + response.raise_for_status() + payload = response.json() + except Exception as exc: + return {"status": "failed", "error": f"Resource Graph query failed: {exc}", "deployments": []} + rows = payload.get("data", []) + subscriptions.extend(rows) + if len(rows) < page_size: + break + skip += page_size + + deployments = [] + for sub in subscriptions: + subscription_id = sub.get("subscriptionId") or sub.get("subscription_id") + subscription_name = sub.get("name") + if not subscription_id: + continue + if not isinstance(subscription_id, str) or not re.match(guid_pattern, subscription_id): + deployments.append({ + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "scheduled_action_name": scheduled_action_name, + "status": "failed", + "error": "subscription_id must be a valid GUID", + }) + continue + + body = { + "kind": "InsightAlert", + "properties": { + "displayName": "Cost anomaly alert", + "status": "Enabled", + "viewId": ( + f"/subscriptions/{subscription_id}/providers/Microsoft.CostManagement/" + "views/ms:DailyAnomalyByResourceGroup" + ), + "schedule": {"frequency": "Daily", "hourOfDay": 8, "daysOfWeek": [], "weeksOfMonth": []}, + "notification": {"subject": "Azure cost anomaly detected", "to": recipients, "language": "en-us"}, + }, + } + url = ( + f"https://management.azure.com/subscriptions/{subscription_id}" + f"/providers/Microsoft.CostManagement/scheduledActions/{scheduled_action_name}" + "?api-version=2022-10-01" + ) + try: + put_response = requests.put(url, headers=headers, json=body, timeout=120) + deployments.append({ + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "scheduled_action_name": scheduled_action_name, + "status": "created" if put_response.status_code == 201 else "updated" if put_response.status_code == 200 else "failed", + "status_code": put_response.status_code, + "error": None if put_response.status_code in (200, 201) else put_response.text, + }) + except Exception as exc: + deployments.append({ + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "scheduled_action_name": scheduled_action_name, + "status": "failed", + "error": str(exc), + }) + + return { + "management_group": management_group, + "email_recipients": recipients, + "subscription_count": len(subscriptions), + "created_or_updated": sum(1 for item in deployments if item["status"] in ("created", "updated")), + "failed": sum(1 for item in deployments if item["status"] == "failed"), + "deployments": deployments, + } + timeoutSeconds: 600 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: management_group + type: string + description: Management group ID used to discover enabled subscriptions. + required: true + - name: email_recipients + type: string + description: Comma-separated email recipients for anomaly alert notifications. + required: true diff --git a/sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-budgets.yaml b/sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-budgets.yaml new file mode 100644 index 000000000..319e127f5 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/deploy-bulk-budgets.yaml @@ -0,0 +1,202 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: deploy-bulk-budgets +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Discovers enabled subscriptions in a management group with Azure Resource Graph + and creates or updates a Cost Management budget in each subscription. + functionCode: |- + def main(**kwargs): + from datetime import date + import re + import requests + + ARM_ENDPOINT = "https://management.azure.com" + GRAPH_API_VERSION = "2022-10-01" + + management_group = kwargs.get("management_group") + amount = kwargs.get("amount") + contact_emails = kwargs.get("contact_emails") + budget_name = kwargs.get("budget_name") or "SubscriptionBudget" + time_grain = kwargs.get("time_grain") or "Monthly" + email_pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + + if not isinstance(management_group, str) or not management_group.strip(): + return {"status": "failed", "error": "management_group is required", "deployments": []} + if amount is None or str(amount).strip() == "": + return {"status": "failed", "error": "amount is required", "deployments": []} + if time_grain not in ["Monthly", "Quarterly", "Annually"]: + return {"status": "failed", "error": "time_grain must be one of Monthly, Quarterly, or Annually", "deployments": []} + if not contact_emails: + return {"status": "failed", "error": "contact_emails is required", "deployments": []} + management_group = management_group.strip() + + if isinstance(contact_emails, str): + emails = [item.strip() for item in contact_emails.split(",") if item.strip()] + else: + emails = [str(item).strip() for item in contact_emails if str(item).strip()] + if not emails: + return {"status": "failed", "error": "contact_emails must contain at least one email", "deployments": []} + invalid_emails = [email for email in emails if not re.match(email_pattern, email)] + if invalid_emails: + return {"status": "failed", "error": f"invalid email address: {invalid_emails[0]}", "deployments": []} + + try: + amount_number = float(amount) + except (TypeError, ValueError): + return {"status": "failed", "error": "amount must be numeric", "deployments": []} + if amount_number <= 0: + return {"status": "failed", "error": "amount must be greater than 0", "deployments": []} + + today = date.today() + next_month = today.month + 1 + year = today.year + (1 if next_month == 13 else 0) + month = 1 if next_month == 13 else next_month + start_date = date(year, month, 1) + end_date = date(year + 1, month, 1) + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token("https://management.azure.com/.default").token + except Exception as exc: + return {"status": "failed", "error": f"failed to acquire Azure credential: {exc}", "deployments": []} + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + query = ( + "ResourceContainers " + "| where type =~ 'microsoft.resources/subscriptions' " + "| where properties.state =~ 'Enabled' " + "| project subscriptionId, name" + ) + graph_url = f"{ARM_ENDPOINT}/providers/Microsoft.ResourceGraph/resources?api-version={GRAPH_API_VERSION}" + subscriptions = [] + skip = 0 + page_size = 1000 + while True: + body = { + "managementGroups": [management_group], + "query": query, + "options": {"$top": page_size, "$skip": skip}, + } + try: + response = requests.post(graph_url, headers=headers, json=body, timeout=60) + response.raise_for_status() + payload = response.json() + except Exception as exc: + return {"status": "failed", "error": f"Resource Graph query failed: {exc}", "deployments": []} + rows = payload.get("data", []) + subscriptions.extend(rows) + if len(rows) < page_size: + break + skip += page_size + + budget_body = { + "properties": { + "category": "Cost", + "amount": amount_number, + "timeGrain": time_grain, + "timePeriod": {"startDate": start_date.isoformat(), "endDate": end_date.isoformat()}, + "notifications": { + "Actual_GreaterThan_80_Percent": { + "enabled": True, + "operator": "GreaterThan", + "threshold": 80, + "thresholdType": "Actual", + "contactEmails": emails, + "contactRoles": ["Owner", "Contributor"], + }, + "Forecasted_GreaterThan_100_Percent": { + "enabled": True, + "operator": "GreaterThan", + "threshold": 100, + "thresholdType": "Forecasted", + "contactEmails": emails, + "contactRoles": ["Owner", "Contributor"], + }, + }, + } + } + + deployments = [] + for sub in subscriptions: + subscription_id = sub.get("subscriptionId") or sub.get("subscription_id") + subscription_name = sub.get("name") + if not subscription_id: + continue + if not isinstance(subscription_id, str) or not re.match(guid_pattern, subscription_id): + deployments.append({ + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "budget_name": budget_name, + "status": "failed", + "error": "subscription_id must be a valid GUID", + }) + continue + url = ( + f"https://management.azure.com/subscriptions/{subscription_id}" + f"/providers/Microsoft.Consumption/budgets/{budget_name}" + "?api-version=2023-11-01" + ) + try: + put_response = requests.put(url, headers=headers, json=budget_body, timeout=120) + deployments.append({ + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "budget_name": budget_name, + "status": "created" if put_response.status_code == 201 else "updated" if put_response.status_code == 200 else "failed", + "status_code": put_response.status_code, + "error": None if put_response.status_code in (200, 201) else put_response.text, + }) + except Exception as exc: + deployments.append({ + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "budget_name": budget_name, + "status": "failed", + "error": str(exc), + }) + + return { + "management_group": management_group, + "budget_name": budget_name, + "amount": amount_number, + "contact_emails": emails, + "subscription_count": len(subscriptions), + "created_or_updated": sum(1 for item in deployments if item["status"] in ("created", "updated")), + "failed": sum(1 for item in deployments if item["status"] == "failed"), + "deployments": deployments, + } + timeoutSeconds: 600 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: management_group + type: string + description: Management group ID used to discover enabled subscriptions. + required: true + - name: amount + type: string + description: Budget amount to apply to each subscription. + required: true + - name: contact_emails + type: string + description: Comma-separated email addresses that receive budget notifications. + required: true + - name: budget_name + type: string + description: |- + Budget name. Default: SubscriptionBudget. + required: false + - name: time_grain + type: string + description: |- + Budget reset period, such as Monthly, Quarterly, or Annually. Default: Monthly. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/non-compute-quotas.yaml b/sreagent-templates/recipes/cost-management/config/tools/non-compute-quotas.yaml new file mode 100644 index 000000000..2f54cc91f --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/non-compute-quotas.yaml @@ -0,0 +1,505 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: non-compute-quotas +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Checks non-compute Azure service quota utilization for a subscription. + The tool uses ARM provider usages APIs where Azure exposes service-specific + usage and quota payloads, and only falls back to Azure Resource Graph counts + with clearly marked estimated limits for services without a direct usages API. + Network Watcher 1/1 entries are returned as suppressed Azure defaults, not + actionable quota risk. + functionCode: |- + def main(**kwargs): + import requests + + ARM_ENDPOINT = "https://management.azure.com" + ARM_SCOPE = "https://management.azure.com/.default" + + subscription_id = kwargs.get("subscription_id") + location = kwargs.get("location") + + result = { + "subscription_id": subscription_id, + "services": [], + "quotas": [], + "at_risk_count": 0, + "api_reported_limit_count": 0, + "estimated_limit_count": 0, + "suppressed_count": 0, + } + + if not subscription_id: + result["error"] = "subscription_id is required" + return result + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token("https://management.azure.com/.default").token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + except Exception as exc: + result["error"] = f"Failed to initialize Azure ARM credential: {exc}" + return result + + estimated_services = [ + { + "service_name": "App Service plans", + "resource_type": "microsoft.web/serverfarms", + "arm_resource_type": "Microsoft.Web/serverfarms", + "estimated_limit": 1000, + "limit_note": "Estimated subscription-level default from Azure service limits documentation; no direct provider usages API was used for this resource type.", + }, + { + "service_name": "SQL servers", + "resource_type": "microsoft.sql/servers", + "arm_resource_type": "Microsoft.Sql/servers", + "estimated_limit": 250, + "limit_note": "Estimated subscription-level default from Azure service limits documentation; no direct provider usages API was used for this resource type.", + }, + { + "service_name": "Service Bus namespaces", + "resource_type": "microsoft.servicebus/namespaces", + "arm_resource_type": "Microsoft.ServiceBus/namespaces", + "estimated_limit": 1000, + "limit_note": "Estimated subscription-level default from Azure service limits documentation; no direct provider usages API was used for this resource type.", + }, + { + "service_name": "Key Vault vaults", + "resource_type": "microsoft.keyvault/vaults", + "arm_resource_type": "Microsoft.KeyVault/vaults", + "estimated_limit": 5000, + "limit_note": "Current count is calculated with Azure Resource Graph. Limit is an estimated subscription-level default from Azure service limits documentation because Key Vault does not expose a subscription usages API for vault count.", + }, + ] + + def to_number(value, default=0): + try: + return float(value) + except (TypeError, ValueError): + return float(default) + + def parse_number(value): + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + def safe_get(record, keys, default=None): + if isinstance(keys, str): + keys = [keys] + + for key in keys: + if isinstance(record, dict): + value = record.get(key) + if value is not None: + return value + else: + value = getattr(record, key, None) + if value is not None: + return value + + return default + + def normalize_token(value): + return "".join( + character + for character in str(value or "").lower() + if character.isalnum() + ) + + def normalize_name(name): + if name is None: + return "Unknown quota" + + if isinstance(name, dict): + return ( + name.get("localizedValue") + or name.get("localized_value") + or name.get("value") + or name.get("name") + or str(name) + ) + + localized = getattr(name, "localized_value", None) or getattr(name, "localizedValue", None) + value = getattr(name, "value", None) + return localized or value or str(name) + + def normalize_location(value): + if isinstance(value, (list, tuple, set)): + value = next((str(item).strip() for item in value if str(item).strip()), None) + + if value is None or not str(value).strip(): + return "subscription" + + return str(value).strip() + + def to_bool(value): + if isinstance(value, bool): + return value + if value is None: + return None + if isinstance(value, str): + return value.strip().lower() in ("1", "true", "yes") + return bool(value) + + def normalize_quota_record(record, default_service_name=None, default_resource_type=None, default_location=None): + location_value = normalize_location(safe_get( + record, + ["location", "location_name", "region", "regionName", "region_name"], + default_location, + )) + current_raw = safe_get( + record, + ["current_count", "currentValue", "current_value", "current", "usage", "used"], + ) + limit_raw = safe_get(record, ["limit", "quota", "quota_limit", "maximum"], 0) + current_number = parse_number(current_raw) + limit_number = to_number(limit_raw, 0) + utilization_pct = safe_get( + record, + ["utilization_pct", "utilizationPercent", "utilization_percent", "usage_pct", "percentUsed"], + ) + + if utilization_pct is None and current_number is not None and limit_number > 0: + utilization_pct = round((current_number / limit_number) * 100, 2) + elif utilization_pct is not None: + utilization_pct = parse_number(utilization_pct) + + at_risk = to_bool(safe_get(record, ["at_risk", "is_at_risk", "atRisk"], None)) + if at_risk is None: + at_risk = bool(utilization_pct is not None and utilization_pct >= 80) + + current_count = None + if current_number is not None: + current_count = int(current_number) if current_number.is_integer() else current_number + + service_name_value = normalize_name(safe_get(record, ["service_name", "service", "provider"], default_service_name)) + quota_name_value = normalize_name(safe_get(record, ["quota_name", "quotaName", "name"], "Unknown quota")) + normalized = { + "subscription_id": subscription_id, + "service_name": service_name_value, + "service": service_name_value, + "resource_type": safe_get(record, ["resource_type", "resourceType", "type"], default_resource_type or "unknown"), + "quota_name": quota_name_value, + "name": quota_name_value, + "location": location_value, + "scope": safe_get(record, ["scope", "quota_scope"], "regional" if location_value != "subscription" else "subscription"), + "current_count": current_count, + "current": current_count, + "limit": int(limit_number) if limit_number.is_integer() else limit_number, + "utilization_pct": round(utilization_pct, 2) if utilization_pct is not None else None, + "at_risk": bool(at_risk), + "at_risk_80": bool(at_risk), + "at_risk_95": bool(utilization_pct is not None and utilization_pct >= 95), + "count_source": safe_get(record, "count_source"), + "limit_source": safe_get(record, "limit_source"), + "limit_type": safe_get(record, "limit_type"), + "source_endpoint": safe_get(record, "source_endpoint"), + } + + limit_note = safe_get(record, "limit_note") + if limit_note: + normalized["limit_note"] = limit_note + + suppression_reason = safe_get(record, "suppression_reason") + if suppression_reason: + normalized["suppressed_from_risk_summary"] = True + normalized["suppression_reason"] = suppression_reason + + return normalized + + def request_arm_json(url, timeout=60): + response = requests.get(url, headers=headers, timeout=timeout) + response.raise_for_status() + return response.json() + + def post_arm_json(url, body, timeout=120): + response = requests.post(url, headers=headers, json=body, timeout=timeout) + response.raise_for_status() + return response.json() + + def add_usage_item( + service_name, + resource_type, + quota_name, + current_count, + limit, + count_source, + limit_source, + limit_type, + source_endpoint, + location_name=None, + limit_note=None, + ): + limit_number = to_number(limit, 0) + if current_count is None: + current_value = None + utilization_pct = None + at_risk = None + else: + current_number = to_number(current_count, 0) + current_value = int(current_number) if current_number.is_integer() else current_number + utilization_pct = round((current_number / limit_number) * 100, 2) if limit_number > 0 else None + at_risk = bool(utilization_pct is not None and utilization_pct >= 80) + + suppressed_from_risk_summary = is_expected_network_watcher_default( + service_name, + quota_name, + current_count, + limit, + ) + if suppressed_from_risk_summary: + at_risk = False + + item = { + "service_name": service_name, + "resource_type": resource_type, + "quota_name": quota_name, + "current_count": current_value, + "limit": int(limit_number) if limit_number.is_integer() else limit_number, + "utilization_pct": utilization_pct, + "at_risk": at_risk, + "count_source": count_source, + "limit_source": limit_source, + "limit_type": limit_type, + "source_endpoint": source_endpoint, + "location": normalize_location(location_name), + "scope": "regional" if location_name else "subscription", + } + + if location_name: + item["location"] = location_name + if limit_note: + item["limit_note"] = limit_note + if suppressed_from_risk_summary: + item["suppressed_from_risk_summary"] = True + item["suppression_reason"] = "Network Watcher 1/1 is the Azure default of one instance per region per subscription; excluded from actionable quota risk summaries." + result["suppressed_count"] += 1 + + normalized_item = normalize_quota_record( + item, + default_service_name=service_name, + default_resource_type=resource_type, + default_location=location_name, + ) + + if at_risk: + result["at_risk_count"] += 1 + if limit_type == "api_reported": + result["api_reported_limit_count"] += 1 + elif limit_type == "estimated": + result["estimated_limit_count"] += 1 + + result["services"].append(item) + result["quotas"].append(normalized_item) + + def is_expected_network_watcher_default(service_name, quota_name, current_count, limit): + return ( + normalize_token(service_name) == "network" + and normalize_token(quota_name) in ("networkwatcher", "networkwatchers") + and to_number(current_count, 0) == 1 + and to_number(limit, 0) == 1 + ) + + def add_usage_payload(service_name, resource_type, payload, source_endpoint, location_name=None): + for usage in payload.get("value", []): + quota_name = normalize_name(usage.get("name")) + current_count = usage.get("currentValue", usage.get("current_value", 0)) + limit = usage.get("limit", 0) + + add_usage_item( + service_name=service_name, + resource_type=resource_type, + quota_name=quota_name, + current_count=current_count, + limit=limit, + count_source="arm_provider_usages_api", + limit_source="api_reported_limit", + limit_type="api_reported", + source_endpoint=source_endpoint, + location_name=location_name, + ) + + def parse_locations(): + if location: + if isinstance(location, str): + return [item.strip() for item in location.split(",") if item.strip()] + if isinstance(location, (list, tuple, set)): + return [str(item).strip() for item in location if str(item).strip()] + return [str(location).strip()] + + url = f"{ARM_ENDPOINT}/subscriptions/{subscription_id}/locations?api-version=2022-12-01" + payload = request_arm_json(url) + return [ + item.get("name") + for item in payload.get("value", []) + if item.get("name") + ] + + def count_with_resource_graph(services): + types = ",\n".join(f"'{service['resource_type']}'" for service in services) + query = f""" + Resources + | where type in~ ( + {types} + ) + | summarize current_count = count() by type = tolower(type) + """ + graph_url = ( + f"{ARM_ENDPOINT}/providers/Microsoft.ResourceGraph/resources" + "?api-version=2022-10-01" + ) + response = post_arm_json( + graph_url, + {"subscriptions": [subscription_id], "query": query}, + ) + + data = response.get("data") or [] + if isinstance(data, dict): + columns = data.get("columns") or data.get("Columns") or [] + rows = data.get("rows") or data.get("Rows") or [] + column_names = [ + column.get("name") or column.get("Name") or column.get("ColumnName") + for column in columns + if isinstance(column, dict) + ] + normalized_rows = [] + for row in rows: + if isinstance(row, dict): + normalized_rows.append(row) + elif isinstance(row, (list, tuple)): + normalized_rows.append({ + column_names[index]: row[index] + for index in range(min(len(column_names), len(row))) + if column_names[index] + }) + data = normalized_rows + + counts = {} + for row in data: + if isinstance(row, dict): + resource_type = str(row.get("type", "")).lower() + current_count = row.get("current_count", 0) + else: + resource_type = str(getattr(row, "type", "")).lower() + current_count = getattr(row, "current_count", 0) + + try: + counts[resource_type] = int(current_count or 0) + except (TypeError, ValueError): + counts[resource_type] = 0 + + return counts + + errors = [] + + try: + service_locations = parse_locations() + except Exception as exc: + service_locations = [] + errors.append({ + "service_name": "Storage and Network", + "source": "subscription_locations", + "error": str(exc), + }) + + for storage_location in service_locations: + storage_usage_endpoint = ( + "https://management.azure.com/" + f"subscriptions/{subscription_id}/providers/Microsoft.Storage/locations/{storage_location}/usages" + "?api-version=2023-05-01" + ) + try: + storage_payload = request_arm_json(storage_usage_endpoint) + add_usage_payload( + service_name="Storage", + resource_type="Microsoft.Storage/locations/usages", + payload=storage_payload, + source_endpoint=storage_usage_endpoint, + location_name=storage_location, + ) + except Exception as exc: + errors.append({ + "service_name": "Storage", + "location": storage_location, + "source": "arm_provider_usages_api", + "endpoint": storage_usage_endpoint, + "error": str(exc), + }) + + for network_location in service_locations: + network_usage_endpoint = ( + "https://management.azure.com/" + f"subscriptions/{subscription_id}/providers/Microsoft.Network/locations/{network_location}/usages" + "?api-version=2023-11-01" + ) + try: + network_payload = request_arm_json(network_usage_endpoint) + add_usage_payload( + service_name="Network", + resource_type="Microsoft.Network/locations/usages", + payload=network_payload, + source_endpoint=network_usage_endpoint, + location_name=network_location, + ) + except Exception as exc: + errors.append({ + "service_name": "Network", + "location": network_location, + "source": "arm_provider_usages_api", + "endpoint": network_usage_endpoint, + "error": str(exc), + }) + + try: + counts = count_with_resource_graph(estimated_services) + except Exception as exc: + counts = None + errors.append({"source": "resource_graph", "error": str(exc)}) + + for service in estimated_services: + resource_type = service["resource_type"] + resource_graph_failed = counts is None + current_count = None if resource_graph_failed else counts.get(resource_type, 0) + add_usage_item( + service_name=service["service_name"], + resource_type=service["arm_resource_type"], + quota_name=f"{service['service_name']} estimated subscription limit", + current_count=current_count, + limit=service["estimated_limit"], + count_source="resource_graph_failed" if resource_graph_failed else "resource_graph", + limit_source="estimated_default_service_limit", + limit_type="estimated", + source_endpoint="Azure Resource Graph", + limit_note=service["limit_note"], + ) + + if errors: + result["errors"] = errors + + return result + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: subscription_id + type: string + description: Azure subscription ID to query for non-compute service quota utilization. + required: true + - name: location + type: string + description: Optional Azure region or comma-separated regions for Storage and Network usages. If omitted, all subscription locations are queried. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/resource-graph-query.yaml b/sreagent-templates/recipes/cost-management/config/tools/resource-graph-query.yaml new file mode 100644 index 000000000..b0279055d --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/resource-graph-query.yaml @@ -0,0 +1,129 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: resource-graph-query +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Runs Azure Resource Graph KQL queries across one or more subscriptions. + Use it to inventory Azure resources, inspect configuration drift, and join + resource graph tables when troubleshooting subscription-scale issues. + functionCode: |- + def main(**kwargs): + import requests + + ARM_ENDPOINT = "https://management.azure.com" + ARM_SCOPE = "https://management.azure.com/.default" + + query = kwargs.get("query") + subscription_ids = kwargs.get("subscription_ids") + + result = { + "rows": [], + "count": 0, + "errors": [], + } + + if not isinstance(query, str) or not query.strip(): + result["errors"].append({"error": "query is required"}) + return result + + if subscription_ids: + if isinstance(subscription_ids, str): + subscriptions = [ + item.strip() + for item in subscription_ids.split(",") + if item.strip() + ] + elif isinstance(subscription_ids, (list, tuple, set)): + subscriptions = [ + str(item).strip() + for item in subscription_ids + if str(item).strip() + ] + else: + subscriptions = [str(subscription_ids).strip()] + else: + subscriptions = [] + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token(ARM_SCOPE).token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + except Exception as exc: + result["errors"].append({"error": f"Failed to initialize Azure ARM credential: {exc}"}) + return result + + if not subscriptions: + try: + list_url = f"{ARM_ENDPOINT}/subscriptions?api-version=2022-12-01" + list_response = requests.get(list_url, headers=headers, timeout=60) + list_response.raise_for_status() + list_payload = list_response.json() + subscriptions = [ + sub.get("subscriptionId") + for sub in list_payload.get("value", []) + if sub.get("subscriptionId") + ] + except Exception as exc: + result["errors"].append({"error": f"Failed to list Azure subscriptions: {exc}"}) + return result + + if not subscriptions: + result["errors"].append({"error": "No subscription IDs were provided or discovered"}) + return result + + try: + graph_url = ( + f"{ARM_ENDPOINT}/providers/Microsoft.ResourceGraph/resources" + "?api-version=2022-10-01" + ) + response = requests.post( + graph_url, + headers=headers, + json={"subscriptions": subscriptions, "query": query}, + timeout=120, + ) + response.raise_for_status() + payload = response.json() + data = payload.get("data") + + if isinstance(data, list): + rows = data + elif isinstance(data, dict): + rows = data.get("rows") or data.get("data") or data.get("value") or [data] + elif data is None: + rows = [] + else: + rows = list(data) + + result["rows"] = rows + result["count"] = payload.get("count") or len(rows) + result["total_records"] = payload.get("totalRecords") + result["result_truncated"] = payload.get("resultTruncated") + result["subscriptions"] = subscriptions + except Exception as exc: + result["errors"].append({"error": f"Failed to run Azure Resource Graph query: {exc}"}) + + return result + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: query + type: string + description: Azure Resource Graph KQL query to execute. + required: true + - name: subscription_ids + type: string + description: Optional comma-separated Azure subscription IDs. If omitted, all accessible subscriptions are queried. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/sku-availability.yaml b/sreagent-templates/recipes/cost-management/config/tools/sku-availability.yaml new file mode 100644 index 000000000..27280768b --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/sku-availability.yaml @@ -0,0 +1,155 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: sku-availability +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Lists regional Azure Compute SKU availability, including available zones + and SKU availability restrictions with reason codes. + functionCode: |- + def main(**kwargs): + import requests + + ARM_ENDPOINT = "https://management.azure.com" + ARM_SCOPE = "https://management.azure.com/.default" + COMPUTE_API_VERSION = "2021-07-01" + + subscription_id = kwargs.get("subscription_id") + location = kwargs.get("location") + sku_filter = kwargs.get("sku_filter") + resource_provider = "compute" + + result = { + "subscription_id": subscription_id, + "location": location, + "sku_filter": sku_filter, + "resource_provider": resource_provider, + "skus": [], + "restriction_summary": { + "total_skus": 0, + "restricted_skus": 0, + "restriction_count": 0, + "reason_codes": {}, + }, + } + + if not subscription_id: + result["error"] = "subscription_id is required" + return result + + if not location: + result["error"] = "location is required" + return result + + normalized_filter = str(sku_filter).strip().lower() if sku_filter else None + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token(ARM_SCOPE).token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + except Exception as exc: + result["error"] = f"Failed to initialize Azure ARM credential: {exc}" + return result + + try: + url = f"{ARM_ENDPOINT}/subscriptions/{subscription_id}/providers/Microsoft.Compute/skus" + resource_skus = [] + params = { + "api-version": COMPUTE_API_VERSION, + "$filter": f"location eq '{location}'", + } + result["service"] = "Azure Compute" + result["source_endpoint"] = ( + f"{url}?api-version={COMPUTE_API_VERSION}&$filter=location eq '{location}'" + ) + while url: + response = requests.get( + url, + headers=headers, + params=params, + timeout=120, + ) + response.raise_for_status() + payload = response.json() + resource_skus.extend(payload.get("value", [])) + url = payload.get("nextLink") + params = None + + for sku in resource_skus: + name = sku.get("name") + if normalized_filter and normalized_filter not in str(name or "").lower(): + continue + + restrictions = [] + for restriction in sku.get("restrictions") or []: + reason_code = restriction.get("reasonCode", restriction.get("reason_code")) + restriction_item = { + "type": restriction.get("type"), + "values": list(restriction.get("values") or []), + "reason_code": reason_code, + } + + restriction_info = restriction.get("restrictionInfo") or restriction.get("restriction_info") + if restriction_info: + restriction_item["restriction_info"] = { + "locations": list(restriction_info.get("locations") or []), + "zones": list(restriction_info.get("zones") or []), + } + + restrictions.append(restriction_item) + result["restriction_summary"]["restriction_count"] += 1 + + reason_key = str(reason_code or "Unknown") + reason_codes = result["restriction_summary"]["reason_codes"] + reason_codes[reason_key] = reason_codes.get(reason_key, 0) + 1 + + zones = set() + for location_info in sku.get("locationInfo", sku.get("location_info", [])) or []: + info_location = location_info.get("location") + if not info_location or str(info_location).lower() == str(location).lower(): + zones.update(str(zone) for zone in (location_info.get("zones") or [])) + + result["skus"].append({ + "name": name, + "family": sku.get("family"), + "size": sku.get("size"), + "zones_available": sorted(zones), + "restrictions": restrictions, + }) + + result["restriction_summary"]["total_skus"] = len(result["skus"]) + result["restriction_summary"]["restricted_skus"] = sum( + 1 for sku in result["skus"] if sku["restrictions"] + ) + + except Exception as exc: + result["error"] = f"Failed to list Azure {resource_provider} resource SKUs: {exc}" + + return result + timeoutSeconds: 240 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: subscription_id + type: string + description: Azure subscription ID to query for regional SKU availability. + required: true + - name: location + type: string + description: Azure region to query for SKU availability and restrictions. + required: true + - name: sku_filter + type: string + description: |- + Optional case-insensitive SKU name substring filter. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/suppress-advisor-recommendations.yaml b/sreagent-templates/recipes/cost-management/config/tools/suppress-advisor-recommendations.yaml new file mode 100644 index 000000000..c55ca767a --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/suppress-advisor-recommendations.yaml @@ -0,0 +1,161 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: suppress-advisor-recommendations +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Suppresses selected Azure Advisor recommendations across subscriptions under + a management group by creating Microsoft.Advisor suppressions with a TTL. + functionCode: |- + def main(**kwargs): + from uuid import uuid4 + import re + import requests + + ARM_ENDPOINT = "https://management.azure.com" + GRAPH_API_VERSION = "2022-10-01" + + management_group_id = kwargs.get("management_group_id") + days = kwargs.get("days") or 30 + recommendation_type_ids = kwargs.get("recommendation_type_ids") + guid_pattern = r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + + if not isinstance(management_group_id, str) or not management_group_id.strip(): + return {"status": "failed", "error": "management_group_id is required"} + management_group_id = management_group_id.strip() + try: + days_number = int(days) + except (TypeError, ValueError): + return {"status": "failed", "error": "days must be an integer"} + if days_number < 1 or days_number > 90: + return {"status": "failed", "error": "days must be between 1 and 90"} + + if recommendation_type_ids: + if isinstance(recommendation_type_ids, str): + type_ids = [item.strip() for item in recommendation_type_ids.split(",") if item.strip()] + else: + type_ids = [str(item).strip() for item in recommendation_type_ids if str(item).strip()] + else: + type_ids = [ + "89515250-1243-43d1-b4e7-f9437cedffd8", + "84b1a508-fc21-49da-979e-96894f1665df", + "48eda464-1485-4dcf-a674-d0905df5054a", + ] + invalid_type_ids = [item for item in type_ids if not re.match(guid_pattern, item)] + if invalid_type_ids: + return { + "status": "failed", + "error": f"recommendation_type_ids must be GUIDs; invalid value: {invalid_type_ids[0]}", + } + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token("https://management.azure.com/.default").token + except Exception as exc: + return {"status": "failed", "error": f"failed to acquire Azure credential: {exc}"} + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + # Recommendation type IDs are validated as GUIDs before interpolation to avoid KQL injection. + escaped_ids = ", ".join([f"'{item}'" for item in type_ids]) + query = ( + "advisorresources " + "| where type =~ 'microsoft.advisor/recommendations' " + f"| where properties.recommendationTypeId in~ ({escaped_ids}) " + "| project id, name, subscriptionId, recommendationTypeId=tostring(properties.recommendationTypeId), " + "problem=tostring(properties.shortDescription.problem)" + ) + + graph_url = f"{ARM_ENDPOINT}/providers/Microsoft.ResourceGraph/resources?api-version={GRAPH_API_VERSION}" + recommendations = [] + skip = 0 + page_size = 1000 + while True: + body = { + "managementGroups": [management_group_id], + "query": query, + "options": {"$top": page_size, "$skip": skip}, + } + try: + response = requests.post(graph_url, headers=headers, json=body, timeout=60) + response.raise_for_status() + payload = response.json() + except Exception as exc: + return {"status": "failed", "error": f"Resource Graph query failed: {exc}"} + rows = payload.get("data", []) + recommendations.extend(rows) + if len(rows) < page_size: + break + skip += page_size + + ttl = f"P{days_number}D" + suppressions = [] + for recommendation in recommendations: + recommendation_id = recommendation.get("id") + if not recommendation_id or "/providers/Microsoft.Advisor/recommendations/" not in recommendation_id: + continue + suppression_id = str(uuid4()) + url = ( + f"https://management.azure.com{recommendation_id}" + f"/suppressions/{suppression_id}?api-version=2023-01-01" + ) + try: + response = requests.put( + url, + headers=headers, + json={"properties": {"ttl": ttl}}, + timeout=120, + ) + suppressions.append({ + "subscription_id": recommendation.get("subscriptionId"), + "recommendation_id": recommendation_id, + "recommendation_type_id": recommendation.get("recommendationTypeId"), + "suppression_id": suppression_id, + "ttl": ttl, + "status": "suppressed" if response.status_code in (200, 201) else "failed", + "status_code": response.status_code, + "error": None if response.status_code in (200, 201) else response.text, + }) + except Exception as exc: + suppressions.append({ + "subscription_id": recommendation.get("subscriptionId"), + "recommendation_id": recommendation_id, + "recommendation_type_id": recommendation.get("recommendationTypeId"), + "ttl": ttl, + "status": "failed", + "error": str(exc), + }) + + return { + "management_group_id": management_group_id, + "days": days_number, + "ttl": ttl, + "recommendation_type_ids": type_ids, + "found_count": len(recommendations), + "suppressed_count": sum(1 for item in suppressions if item["status"] == "suppressed"), + "failed_count": sum(1 for item in suppressions if item["status"] == "failed"), + "suppressions": suppressions, + } + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: management_group_id + type: string + description: Management group ID whose subscriptions should have Advisor recommendations suppressed. + required: true + - name: days + type: string + description: |- + Advisor suppression TTL in days. Default: 30. Maximum: 90. + required: false + - name: recommendation_type_ids + type: string + description: Optional comma-separated Advisor recommendation type IDs to suppress. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/vm-quota-usage.yaml b/sreagent-templates/recipes/cost-management/config/tools/vm-quota-usage.yaml new file mode 100644 index 000000000..fe4bcb4a3 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/vm-quota-usage.yaml @@ -0,0 +1,196 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: vm-quota-usage +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Reports Azure VM family quota usage for one or more regions in a subscription. + The tool queries Azure Compute usages through ARM REST, calculates utilization + percentages, and flags quota families that are at risk above 80% or 95% usage. + Errors from Azure stage, preview, canary, or EUAP locations are suppressed + from actionable failure summaries because those regions are expected noise. + functionCode: |- + def main(**kwargs): + import requests + + ARM_ENDPOINT = "https://management.azure.com" + ARM_SCOPE = "https://management.azure.com/.default" + LOCATIONS_API_VERSION = "2022-12-01" + USAGES_API_VERSION = "2023-07-01" + + subscription_id = kwargs.get("subscription_id") + location = kwargs.get("location") + + if not subscription_id: + return { + "error": "subscription_id is required", + "quotas": [], + "warning_count": 0, + "critical_count": 0, + } + + if location: + if isinstance(location, str): + locations = [item.strip() for item in location.split(",") if item.strip()] + elif isinstance(location, (list, tuple, set)): + locations = [str(item).strip() for item in location if str(item).strip()] + else: + locations = [str(location).strip()] + else: + locations = [] + + result = { + "subscription_id": subscription_id, + "quotas": [], + "warning_count": 0, + "critical_count": 0, + "suppressed_error_count": 0, + } + + errors = [] + + def is_expected_preview_region(region_name): + normalized = "".join( + character + for character in str(region_name or "").lower() + if character.isalnum() + ) + expected_region_markers = ("stage", "staging", "preview", "canary", "euap") + return any(marker in normalized for marker in expected_region_markers) + + def suppress_error(error, reason): + suppressed = dict(error) + suppressed["suppression_reason"] = reason + suppressed["suppressed_from_failure_summary"] = True + result["suppressed_error_count"] += 1 + result.setdefault("suppressed_errors", []).append(suppressed) + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token(ARM_SCOPE).token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + except Exception as exc: + result["error"] = f"Failed to initialize Azure ARM credential: {exc}" + return result + + if not location: + try: + url = ( + f"{ARM_ENDPOINT}/subscriptions/{subscription_id}/locations" + f"?api-version={LOCATIONS_API_VERSION}" + ) + response = requests.get(url, headers=headers, timeout=60) + response.raise_for_status() + payload = response.json() + locations = [ + item.get("name") + for item in payload.get("value", []) + if item.get("name") + ] + except Exception as exc: + result["error"] = f"Failed to list Azure subscription locations: {exc}" + return result + + if not locations: + result["error"] = "location did not contain any valid region names" + return result + + if len(locations) == 1: + result["location"] = locations[0] + else: + result["locations"] = locations + + for region in locations: + try: + url = ( + f"{ARM_ENDPOINT}/subscriptions/{subscription_id}/providers/Microsoft.Compute" + f"/locations/{region}/usages?api-version={USAGES_API_VERSION}" + ) + response = requests.get(url, headers=headers, timeout=60) + response.raise_for_status() + payload = response.json() + + for usage in payload.get("value", []): + name_obj = usage.get("name") + if isinstance(name_obj, dict): + name = ( + name_obj.get("localizedValue") + or name_obj.get("localized_value") + or name_obj.get("value") + or str(name_obj) + ) + else: + name = str(name_obj) + current = usage.get("currentValue", usage.get("current_value", 0)) + limit = usage.get("limit", 0) + + try: + current_number = float(current or 0) + except (TypeError, ValueError): + current_number = 0.0 + + try: + limit_number = float(limit or 0) + except (TypeError, ValueError): + limit_number = 0.0 + + utilization_pct = None + if limit_number > 0: + utilization_pct = round((current_number / limit_number) * 100, 2) + + at_risk_80 = bool(utilization_pct is not None and utilization_pct > 80) + at_risk_95 = bool(utilization_pct is not None and utilization_pct > 95) + + if utilization_pct is not None and utilization_pct > 80 and utilization_pct <= 95: + result["warning_count"] += 1 + if at_risk_95: + result["critical_count"] += 1 + + quota = { + "name": name, + "current": current, + "limit": limit, + "utilization_pct": utilization_pct, + "at_risk_80": at_risk_80, + "at_risk_95": at_risk_95, + } + if len(locations) > 1: + quota["location"] = region + + result["quotas"].append(quota) + except Exception as exc: + error = {"location": region, "error": str(exc)} + if is_expected_preview_region(region): + suppress_error( + error, + "Expected noise from Azure stage, preview, canary, or EUAP region; excluded from actionable VM quota failure summaries.", + ) + else: + errors.append(error) + + if errors: + result["errors"] = errors + + return result + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: subscription_id + type: string + description: Azure subscription ID to query for VM quota usage. + required: true + - name: location + type: string + description: Optional Azure region to query. If omitted, all subscription locations are queried. + required: false diff --git a/sreagent-templates/recipes/cost-management/config/tools/zone-mapping.yaml b/sreagent-templates/recipes/cost-management/config/tools/zone-mapping.yaml new file mode 100644 index 000000000..a6351edc2 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/config/tools/zone-mapping.yaml @@ -0,0 +1,288 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: zone-mapping +spec: + type: PythonFunctionTool + connector: '' + toolMode: Auto + description: |- + Compares logical availability-zone mappings between a reference Azure + subscription and one or more peer subscriptions in a region. Use this + read-only check before cross-subscription zonal placement or shared capacity + reservation group decisions. + functionCode: |- + def main(**kwargs): + import datetime + import uuid + + ARM_ENDPOINT = "https://management.azure.com" + ARM_SCOPE = "https://management.azure.com/.default" + API_VERSION = "2022-12-01" + + def normalize_subscription_id(value): + value = str(value or "").strip() + value = value.removeprefix("/subscriptions/").removeprefix("subscriptions/") + try: + return str(uuid.UUID(value)) + except ValueError: + return None + + reference_input = kwargs.get("reference_subscription_id") + reference_subscription_id = normalize_subscription_id(reference_input) + location = str(kwargs.get("location") or "").strip() + peer_subscription_ids = kwargs.get("peer_subscription_ids") + + if isinstance(peer_subscription_ids, str): + peer_inputs = [item.strip() for item in peer_subscription_ids.split(",") if item.strip()] + elif isinstance(peer_subscription_ids, (list, tuple, set)): + peer_inputs = [str(item).strip() for item in peer_subscription_ids if str(item).strip()] + else: + peer_inputs = [] + + peers = [] + invalid_peers = [] + for peer_input in peer_inputs: + normalized_peer = normalize_subscription_id(peer_input) + if normalized_peer: + if normalized_peer not in peers: + peers.append(normalized_peer) + else: + invalid_peers.append(peer_input) + + checked_at = datetime.datetime.now(datetime.timezone.utc).isoformat() + result = { + "reference_subscription_id": reference_subscription_id or str(reference_input or "").strip(), + "peer_subscription_ids": peers, + "location": location, + "checked_at": checked_at, + "availability_zone_peers": [], + "errors": [], + } + + if not reference_subscription_id: + result["errors"].append({ + "field": "reference_subscription_id", + "error": "A valid reference subscription UUID is required", + }) + if not location: + result["errors"].append({ + "field": "location", + "error": "location is required", + }) + if not peers: + result["errors"].append({ + "field": "peer_subscription_ids", + "error": "At least one peer subscription UUID is required", + }) + if invalid_peers: + result["errors"].append({ + "field": "peer_subscription_ids", + "error": "All peer subscription IDs must be valid UUIDs", + "invalid_values": invalid_peers, + }) + + if result["errors"]: + return result + + try: + import requests + from azure.core.exceptions import AzureError + from azure.identity import DefaultAzureCredential + + credential = DefaultAzureCredential() + token = credential.get_token(ARM_SCOPE).token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + except (AzureError, ImportError) as exc: + result["errors"].append({ + "stage": "authentication", + "error": f"Failed to initialize Azure ARM credential: {exc}", + }) + return result + + url = ( + f"{ARM_ENDPOINT}/subscriptions/{reference_subscription_id}" + "/providers/Microsoft.Resources/checkZonePeers/" + ) + params = {"api-version": API_VERSION} + body = { + "location": location, + "subscriptionIds": [ + f"subscriptions/{subscription_id}" for subscription_id in peers + ], + } + result["source_endpoint"] = f"{url}?api-version={API_VERSION}" + + try: + response = requests.post( + url, + headers=headers, + params=params, + json=body, + timeout=120, + ) + response.raise_for_status() + payload = response.json() + except requests.HTTPError as exc: + error = { + "stage": "check_zone_peers", + "status_code": exc.response.status_code if exc.response is not None else None, + "error": str(exc), + } + if exc.response is not None: + try: + error["response"] = exc.response.json() + except ValueError: + error["response"] = exc.response.text[:2000] + result["errors"].append(error) + return result + except requests.RequestException as exc: + result["errors"].append({ + "stage": "check_zone_peers", + "error": str(exc), + }) + return result + except ValueError as exc: + result["errors"].append({ + "stage": "parse_response", + "error": f"Check Zone Peers returned invalid JSON: {exc}", + }) + return result + + if not isinstance(payload, dict): + result["errors"].append({ + "stage": "normalize_response", + "error": "Check Zone Peers returned a non-object response", + "raw_response": payload, + }) + return result + + mappings = payload.get("availabilityZonePeers") + if not isinstance(mappings, list) or not mappings: + result["errors"].append({ + "stage": "normalize_response", + "error": "Check Zone Peers returned no availability-zone mappings", + }) + result["raw_response"] = payload + return result + + returned_peer_ids = set() + for mapping_index, mapping in enumerate(mappings): + if not isinstance(mapping, dict): + result["errors"].append({ + "stage": "normalize_response", + "error": "Availability-zone mapping must be an object", + "mapping_index": mapping_index, + }) + continue + + reference_zone = mapping.get("availabilityZone") + if reference_zone in (None, ""): + result["errors"].append({ + "stage": "normalize_response", + "error": "Availability-zone mapping is missing the reference zone", + "mapping_index": mapping_index, + }) + continue + + mapping_peers = mapping.get("peers") + if not isinstance(mapping_peers, list): + result["errors"].append({ + "stage": "normalize_response", + "error": "Availability-zone mapping peers must be a list", + "mapping_index": mapping_index, + }) + continue + + normalized_peers = [] + for peer_index, peer in enumerate(mapping_peers): + if not isinstance(peer, dict): + result["errors"].append({ + "stage": "normalize_response", + "error": "Availability-zone peer must be an object", + "mapping_index": mapping_index, + "peer_index": peer_index, + }) + continue + + peer_subscription_id = normalize_subscription_id(peer.get("subscriptionId")) + peer_zone = peer.get("availabilityZone") + if not peer_subscription_id or peer_zone in (None, ""): + result["errors"].append({ + "stage": "normalize_response", + "error": "Availability-zone peer is missing a valid subscription or zone", + "mapping_index": mapping_index, + "peer_index": peer_index, + }) + continue + + returned_peer_ids.add(peer_subscription_id) + normalized_peers.append({ + "subscription_id": peer_subscription_id, + "availability_zone": peer_zone, + }) + result["availability_zone_peers"].append({ + "reference_availability_zone": reference_zone, + "peers": normalized_peers, + }) + + response_subscription_id = normalize_subscription_id(payload.get("subscriptionId")) + response_location = str(payload.get("location") or "").strip() + result["response_subscription_id"] = response_subscription_id + result["response_location"] = response_location + result["mapping_count"] = len(result["availability_zone_peers"]) + + if not result["availability_zone_peers"]: + result["errors"].append({ + "stage": "normalize_response", + "error": "Check Zone Peers returned no usable availability-zone mappings", + }) + if response_subscription_id != reference_subscription_id: + result["errors"].append({ + "stage": "normalize_response", + "error": "Response subscription does not match the reference subscription", + "expected": reference_subscription_id, + "actual": response_subscription_id, + }) + if response_location.lower() != location.lower(): + result["errors"].append({ + "stage": "normalize_response", + "error": "Response location does not match the requested location", + "expected": location, + "actual": response_location, + }) + + missing_peer_ids = [ + peer_subscription_id + for peer_subscription_id in peers + if peer_subscription_id not in returned_peer_ids + ] + if missing_peer_ids: + result["errors"].append({ + "stage": "evidence_gap", + "error": "Check Zone Peers did not return mappings for every requested peer subscription", + "missing_peer_subscription_ids": missing_peer_ids, + }) + return result + timeoutSeconds: 180 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: reference_subscription_id + type: string + description: Reference Azure subscription UUID whose logical zones are being compared. + required: true + - name: location + type: string + description: Azure region whose logical availability-zone mappings should be compared. + required: true + - name: peer_subscription_ids + type: string + description: Comma-separated peer Azure subscription UUIDs to compare with the reference subscription. + required: true diff --git a/sreagent-templates/recipes/cost-management/connectors.json b/sreagent-templates/recipes/cost-management/connectors.json new file mode 100644 index 000000000..9df300d0d --- /dev/null +++ b/sreagent-templates/recipes/cost-management/connectors.json @@ -0,0 +1,62 @@ +{ + "toggles": { + "enableAppInsightsConnector": false, + "appInsightsResourceId": "", + "appInsightsAppId": "", + "enableLogAnalyticsConnector": false, + "lawResourceId": "", + "enableAzureMonitorConnector": false, + "azureMonitorLookbackDays": 7, + "grafanaUrl": "", + "grafanaApiKey": "" + }, + "connectors": [ + { + "name": "azure-resource-manager-mcp", + "properties": { + "dataConnectorType": "Mcp", + "dataSource": "https://mcp.management.azure.com", + "extendedProperties": { + "type": "http", + "endpoint": "https://mcp.management.azure.com", + "authType": "AzureARM", + "armScope": "https://mcp.management.azure.com/.default", + "x-mcp-toolset": "CostManagement", + "toolsVisibleToMetaAgent": [ + "query_costs", + "query_aks_costs", + "forecast_costs", + "list_dimensions", + "list_budgets", + "get_budget", + "create_budget", + "list_alerts", + "list_benefit_utilization", + "get_benefit_recommendations", + "list_reservation_transactions", + "get_retail_prices", + "start_pricesheet_download", + "get_pricesheet_status" + ], + "selectedTools": [ + "query_costs", + "query_aks_costs", + "forecast_costs", + "list_dimensions", + "list_budgets", + "get_budget", + "create_budget", + "list_alerts", + "list_benefit_utilization", + "get_benefit_recommendations", + "list_reservation_transactions", + "get_retail_prices", + "start_pricesheet_download", + "get_pricesheet_status" + ] + }, + "identity": "system" + } + } + ] +} diff --git a/sreagent-templates/recipes/cost-management/data/knowledge-items.json b/sreagent-templates/recipes/cost-management/data/knowledge-items.json new file mode 100644 index 000000000..023030573 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/data/knowledge-items.json @@ -0,0 +1,51 @@ +[ + { + "name": "well-architected-capacity-planning", + "type": "KnowledgeWebPage", + "url": "https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning", + "displayName": "Well-Architected capacity planning", + "description": "Plan workload capacity from demand forecasts, reachable limits, and workload objectives." + }, + { + "name": "well-architected-regions-and-zones", + "type": "KnowledgeWebPage", + "url": "https://learn.microsoft.com/en-us/azure/well-architected/design-guides/regions-availability-zones", + "displayName": "Well-Architected regions and availability zones", + "description": "Evaluate workload-driven region and availability-zone resilience tradeoffs." + }, + { + "name": "well-architected-disaster-recovery", + "type": "KnowledgeWebPage", + "url": "https://learn.microsoft.com/en-us/azure/well-architected/reliability/disaster-recovery", + "displayName": "Well-Architected disaster recovery", + "description": "Design tested recovery plans and prepare recovery-region infrastructure." + }, + { + "name": "azure-quotas-overview", + "type": "KnowledgeWebPage", + "url": "https://learn.microsoft.com/en-us/azure/quotas/quotas-overview", + "displayName": "Azure Quotas overview", + "description": "Understand adjustable limits and why quota does not guarantee capacity." + }, + { + "name": "azure-capacity-reservations", + "type": "KnowledgeWebPage", + "url": "https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview", + "displayName": "Azure on-demand capacity reservations", + "description": "Understand capacity guarantees, eligibility, quota prerequisites, limitations, and pricing." + }, + { + "name": "azure-check-zone-peers", + "type": "KnowledgeWebPage", + "url": "https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/check-zone-peers?view=rest-resources-2022-12-01", + "displayName": "Check Zone Peers API", + "description": "Compare subscription-specific logical availability-zone mappings." + }, + { + "name": "aks-upgrade-capacity-planning", + "type": "KnowledgeWebPage", + "url": "https://learn.microsoft.com/en-us/azure/aks/upgrade-capacity-cost-planning", + "displayName": "AKS upgrade capacity planning", + "description": "Plan surge nodes, quota, subnet IPs, load balancer resources, and temporary upgrade cost." + } +] diff --git a/sreagent-templates/recipes/cost-management/expected-config.json b/sreagent-templates/recipes/cost-management/expected-config.json new file mode 100644 index 000000000..a4c78fd45 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/expected-config.json @@ -0,0 +1,76 @@ +{ + "_scenario": "cost-management", + "agent": { + "accessLevel": "Low", + "actionMode": "Review", + "upgradeChannel": "Preview", + "defaultModelProvider": "MicrosoftFoundry", + "incidentPlatform": "None" + }, + "connectors": [ + { + "name": "azure-resource-manager-mcp", + "type": "Mcp" + } + ], + "allowAdditionalConnectors": true, + "knowledgeItems": [ + "well-architected-capacity-planning", + "well-architected-regions-and-zones", + "well-architected-disaster-recovery", + "azure-quotas-overview", + "azure-capacity-reservations", + "azure-check-zone-peers", + "aks-upgrade-capacity-planning" + ], + "tools": 13, + "enabledTools": [ + "PlotAreaChartWithCorrelation", + "PlotBarChart", + "PlotHeatmap", + "PlotPieChart", + "PlotScatter", + "QueryAppInsightsByAppId", + "QueryAppInsightsByResourceId", + "QueryLogAnalyticsByResourceId", + "QueryLogAnalyticsByWorkspaceId" + ], + "skills": [ + "azure-cost-management" + ], + "pluginSkills": [ + "azure-capacity-management" + ], + "allowAdditionalSkills": true, + "subagents": [ + "azure-capacity-manager", + "chief-financial-officer", + "finops-practitioner" + ], + "hooks": [], + "commonPrompts": [ + "safety-rules" + ], + "scheduledTasks": [ + "AdvisorSuppressionReview", + "AIWorkloadCostAnalysis", + "AlertCoverageAudit", + "BenefitRecommendationReview", + "BudgetCoverageAudit", + "CapacityDailyMonitor", + "CapacityMonthlyPlanning", + "CapacityQuarterlyStrategy", + "CapacityWeeklySupplyReview", + "ComputeUtilizationTrend", + "CostOptimization", + "DbQuotaAudit", + "Monthly", + "NonComputeQuotaAudit", + "Semiannual", + "SkuAvailabilityAudit", + "StoragePaasGrowthForecast" + ], + "responsePlans": [], + "allowAdditionalRepos": true, + "repos": [] +} diff --git a/sreagent-templates/recipes/cost-management/tool-overrides.json b/sreagent-templates/recipes/cost-management/tool-overrides.json new file mode 100644 index 000000000..e0c92ffc3 --- /dev/null +++ b/sreagent-templates/recipes/cost-management/tool-overrides.json @@ -0,0 +1,40 @@ +{ + "overrides": [ + { + "name": "QueryAppInsightsByAppId", + "enabled": true + }, + { + "name": "QueryAppInsightsByResourceId", + "enabled": true + }, + { + "name": "QueryLogAnalyticsByResourceId", + "enabled": true + }, + { + "name": "QueryLogAnalyticsByWorkspaceId", + "enabled": true + }, + { + "name": "PlotAreaChartWithCorrelation", + "enabled": true + }, + { + "name": "PlotBarChart", + "enabled": true + }, + { + "name": "PlotHeatmap", + "enabled": true + }, + { + "name": "PlotPieChart", + "enabled": true + }, + { + "name": "PlotScatter", + "enabled": true + } + ] +} diff --git a/sreagent-templates/recipes/cost-management/tool-permissions.json b/sreagent-templates/recipes/cost-management/tool-permissions.json new file mode 100644 index 000000000..1b5e57a6a --- /dev/null +++ b/sreagent-templates/recipes/cost-management/tool-permissions.json @@ -0,0 +1,21 @@ +{ + "allow": [ + "PlotAreaChartWithCorrelation", + "PlotBarChart", + "PlotHeatmap", + "PlotPieChart", + "PlotScatter", + "QueryAppInsightsByAppId", + "QueryAppInsightsByResourceId", + "QueryLogAnalyticsByResourceId", + "QueryLogAnalyticsByWorkspaceId", + "PostMessageToConversation", + "PostCardToConversation", + "ReplyWithMessageToConversation", + "ReplyWithCardToConversation", + "UpdateCardInConversation", + "GetMessagesFromChannel" + ], + "ask": [], + "deny": [] +} From e7ac34af3e6269f6f9990d6575444c9aac3b5962 Mon Sep 17 00:00:00 2001 From: msbrett Date: Tue, 3 Nov 2026 16:55:08 -0800 Subject: [PATCH 2/2] feat(cost-management): add custom instructions, dry-run test, and shared script fixes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sreagent-templates/CHANGELOG.md | 5 + sreagent-templates/README.md | 1 + sreagent-templates/bicep/Apply-Extras.ps1 | 78 ++++++--- sreagent-templates/bicep/Assemble-Agent.ps1 | 5 + sreagent-templates/bicep/apply-extras.sh | 72 +++++--- sreagent-templates/bicep/assemble-agent.sh | 6 + sreagent-templates/bin/ps/Verify-Agent.ps1 | 40 +++++ sreagent-templates/bin/verify-agent.sh | 38 +++++ .../config/connectorv2/microsoft-teams.yaml | 6 + .../cost-management/custom-instructions.md | 157 ++++++++++++++++++ .../cost-management/expected-config.json | 15 +- .../tests/test-dry-run-cost-management.sh | 79 +++++++++ 12 files changed, 450 insertions(+), 52 deletions(-) create mode 100644 sreagent-templates/recipes/cost-management/custom-instructions.md create mode 100755 sreagent-templates/tests/test-dry-run-cost-management.sh diff --git a/sreagent-templates/CHANGELOG.md b/sreagent-templates/CHANGELOG.md index 08d8eed2c..012019a1f 100644 --- a/sreagent-templates/CHANGELOG.md +++ b/sreagent-templates/CHANGELOG.md @@ -11,7 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added the `cost-management` recipe for native Azure Cost Management, Resource Graph, Advisor, Pricing, Quota, and Microsoft Teams FinOps governance without FinOps Hubs or ADX. + ### Fixed +- Updated `apply-extras.sh` and `Apply-Extras.ps1` to upload `PythonFunctionTool` and `KustoTool` as `ExtendedAgentTool`, upload tools before skills and subagents, propagate ConnectorV2 `spec.operations`, and deploy custom instructions to `/api/v2/agent/customInstructions`. +- Updated `assemble-agent.sh` and `Assemble-Agent.ps1` to bundle `custom-instructions.md`, and `verify-agent.sh` and `Verify-Agent.ps1` to verify custom instructions and tools. - Discover available Azure SRE Agent regions from the selected subscription in Bash and PowerShell workflows, with all 20 live regions as the offline fallback. - Made `export-agent.sh` detect and install PyYAML before export, with an isolated-environment fallback and actionable failure guidance. diff --git a/sreagent-templates/README.md b/sreagent-templates/README.md index db8a7da3d..43728e1f6 100644 --- a/sreagent-templates/README.md +++ b/sreagent-templates/README.md @@ -71,6 +71,7 @@ cd sre-agent/sreagent-templates | [azmon-lawappinsights](recipes/azmon-lawappinsights/) | Azure Monitor | Alert response with AppInsights + Log Analytics, skills, subagents, scheduled tasks | | [pagerduty-law-vmcosmos](recipes/pagerduty-law-vmcosmos/) | PagerDuty | VM + CosmosDB + HTTP error investigation with knowledge files and skills | | [dynatrace-mcp](recipes/dynatrace-mcp/) | Dynatrace | Dynatrace MCP connector for investigating application errors | +| [cost-management](recipes/cost-management/) | (none) | Azure cost management and FinOps governance with Microsoft Teams | | [minimal](recipes/minimal/) | (none) | Minimal agent — just infra + RBAC, add your own connectors and skills | Each recipe README has the full parameter list, example values, and post-deploy steps. diff --git a/sreagent-templates/bicep/Apply-Extras.ps1 b/sreagent-templates/bicep/Apply-Extras.ps1 index 916cdd5eb..e3aef2853 100644 --- a/sreagent-templates/bicep/Apply-Extras.ps1 +++ b/sreagent-templates/bicep/Apply-Extras.ps1 @@ -157,6 +157,24 @@ function Get-DpToken { return $tok } +if ($extras.PSObject.Properties['customInstructions'] -and $extras.customInstructions -is [string]) { + if ($DpTokenAvailable) { + try { + $headers = @{ Authorization = "Bearer $(Get-DpToken)" } + $body = ConvertTo-Json -InputObject @{ instructions = $extras.customInstructions } -Compress + $url = "$AgentEndpoint/api/v2/agent/customInstructions" + $null = Invoke-RestMethod -TimeoutSec 30 -Uri $url -Method Put -Headers $headers -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ContentType 'application/json' + Write-Host ' ok custom instructions' + } catch { + Write-Host " FAILED - PUT custom instructions: $($_.Exception.Message)" -ForegroundColor Red + } + } else { + Write-Host " ⚠ Custom instructions skipped (no data-plane token)" -ForegroundColor Yellow + $DpSkippedItems += 'customInstructions' + $DpSkippedItems = [System.Collections.Generic.List[string]]$DpSkippedItems + } +} + function Get-SafeETag { param($Response) @@ -937,7 +955,31 @@ if ($toolPermissions) { } # ═════════════════════════════════════════════════════════════════════════════ -# 4g-1. skills — data-plane PUT +# 4g-1. tools — data-plane PUT +# Route: PUT /api/v2/extendedAgent/tools/{name} +# ═════════════════════════════════════════════════════════════════════════════ +$toolItems = if ($extras.PSObject.Properties['tools']) { $extras.tools } else { $null } +$tlCount = ($toolItems | Measure-Object).Count +if ($tlCount -gt 0) { + if ($DpTokenAvailable) { + Write-Host "tools: $tlCount" + foreach ($tool in $toolItems) { + $name = if ($tool.metadata) { $tool.metadata.name } else { $tool.name } + $props = if ($tool.spec) { $tool.spec } else { $tool.properties } + $toolType = if ($null -ne $tool.kind) { $tool.kind } else { 'Tool' } + DataPlane-PutExtended -Kind "tools" -Name $name -Type $toolType -Tags @() -Properties $props + } + } else { + Write-Host "tools: $tlCount - WARNING skipped (no data-plane token)" + foreach ($tl in $toolItems) { + $tlName = if ($tl.metadata) { $tl.metadata.name } else { $tl.name } + $DpSkippedItems.Add("tool/$tlName") + } + } +} + +# ═════════════════════════════════════════════════════════════════════════════ +# 4g-2. skills — data-plane PUT # Route: PUT /api/v2/extendedAgent/skills/{name} # ═════════════════════════════════════════════════════════════════════════════ $skillItems = if ($extras.PSObject.Properties['skills']) { $extras.skills } else { $null } @@ -966,7 +1008,7 @@ if ($skCount -gt 0) { } # ═════════════════════════════════════════════════════════════════════════════ -# 4g-2. subagents — data-plane PUT +# 4g-3. subagents — data-plane PUT # Route: PUT /api/v2/extendedAgent/agents/{name} # ═════════════════════════════════════════════════════════════════════════════ $subagentItems = if ($extras.PSObject.Properties['subagents']) { $extras.subagents } else { $null } @@ -988,29 +1030,6 @@ if ($saCount -gt 0) { } } -# ═════════════════════════════════════════════════════════════════════════════ -# 4g-3. tools — data-plane PUT -# Route: PUT /api/v2/extendedAgent/tools/{name} -# ═════════════════════════════════════════════════════════════════════════════ -$toolItems = if ($extras.PSObject.Properties['tools']) { $extras.tools } else { $null } -$tlCount = ($toolItems | Measure-Object).Count -if ($tlCount -gt 0) { - if ($DpTokenAvailable) { - Write-Host "tools: $tlCount" - foreach ($tl in $toolItems) { - $name = if ($tl.metadata) { $tl.metadata.name } else { $tl.name } - $props = if ($tl.spec) { $tl.spec } else { $tl.properties } - DataPlane-PutExtended -Kind "tools" -Name $name -Type "Tool" -Tags @() -Properties $props - } - } else { - Write-Host "tools: $tlCount - WARNING skipped (no data-plane token)" - foreach ($tl in $toolItems) { - $tlName = if ($tl.metadata) { $tl.metadata.name } else { $tl.name } - $DpSkippedItems.Add("tool/$tlName") - } - } -} - # ═════════════════════════════════════════════════════════════════════════════ # 4f-4. ConnectorV2 — managed connection, runtime access, and MCP tool binding # ═════════════════════════════════════════════════════════════════════════════ @@ -1058,10 +1077,17 @@ if ($cv2Count -gt 0) { continue } + $connectorBinding = [ordered]@{ + name = $apiName + connectionName = $connectionName + } + if ($null -ne $spec.operations) { + $connectorBinding.operations = @($spec.operations) + } $mcpBody = [ordered]@{ properties = [ordered]@{ description = $displayName - connectors = @(@{ name = $apiName; connectionName = $connectionName }) + connectors = @($connectorBinding) } } if ($spec.requireApprovalTools) { diff --git a/sreagent-templates/bicep/Assemble-Agent.ps1 b/sreagent-templates/bicep/Assemble-Agent.ps1 index 3a367a5ed..5e9eebca1 100644 --- a/sreagent-templates/bicep/Assemble-Agent.ps1 +++ b/sreagent-templates/bicep/Assemble-Agent.ps1 @@ -623,6 +623,11 @@ $extrasObj = [ordered]@{ connectors = $extrasConnectors } +$customInstructionsFile = Join-Path $ConfigDir 'custom-instructions.md' +if (Test-Path $customInstructionsFile -PathType Leaf) { + $extrasObj['customInstructions'] = [System.IO.File]::ReadAllText((Resolve-Path $customInstructionsFile).Path) +} + $extrasObj | ConvertTo-Json -Depth 32 | Out-File -FilePath $ExtrasFile -Encoding utf8NoBOM # Merge admin settings if present (adminUsers for cross-tenant access) diff --git a/sreagent-templates/bicep/apply-extras.sh b/sreagent-templates/bicep/apply-extras.sh index cd3a8ba60..85e924ea7 100755 --- a/sreagent-templates/bicep/apply-extras.sh +++ b/sreagent-templates/bicep/apply-extras.sh @@ -217,6 +217,25 @@ dataplane_post_json() { # Reusable: get a data-plane bearer (defined here so multipart helper can use it) _dp_token() { az account get-access-token --resource https://azuresre.dev --query accessToken -o tsv 2>/dev/null; } +CUSTOM_BODY=$(jq -c '.customInstructions | select(type == "string") | {instructions: .}' "$FILE") +if [[ -n "$CUSTOM_BODY" ]]; then + if [[ "$DP_TOKEN_AVAILABLE" == true ]]; then + TOKEN=$(_dp_token) + result=$(curl -sS -w "\n%{http_code}" -X PUT "${AGENT_ENDPOINT}/api/v2/agent/customInstructions" \ + -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \ + --data-binary "$CUSTOM_BODY" 2>&1) + http_code=$(echo "$result" | tail -1) + if [[ "$http_code" =~ ^(200|201|202|204)$ ]]; then + echo " ok custom instructions" + else + echo " FAILED — PUT custom instructions (HTTP ${http_code})" + fi + else + echo " ⚠ Custom instructions skipped (no data-plane token)" + DP_SKIPPED_ITEMS+=("customInstructions") + fi +fi + # --------------------------------------------------------------------------- # Helper: PUT to v2 extendedAgent dataplane (skills/subagents/hooks/commonprompts/etc.). # Body shape (from Agent.Web/ApiResources/ApiRequestEnvelope.cs): @@ -815,6 +834,7 @@ if [[ "$count" -gt 0 ]]; then cv2_pvs=$(echo "$cv2_spec" | jq -c '.parameterValueSet // null') cv2_pv=$(echo "$cv2_spec" | jq -c '.parameterValues // null') cv2_rat=$(echo "$cv2_spec" | jq -c '.requireApprovalTools // null') + cv2_ops=$(echo "$cv2_spec" | jq -c '.operations // null') TOKEN=$(_dp_token) @@ -851,8 +871,8 @@ if [[ "$count" -gt 0 ]]; then # Step 3: Create MCP server config (links connection to MCP tools) mcp_body=$(jq -nc --arg desc "$cv2_display" --arg cn "$cv2_conn" --arg api "$cv2_api" \ - --argjson rat "$cv2_rat" \ - '{properties: {description: $desc, connectors: [{name: $api, connectionName: $cn}]}} + (if $rat != null then {runtimeMcpConfiguration: {requireApprovalTools: $rat}} else {} end)') + --argjson rat "$cv2_rat" --argjson ops "$cv2_ops" \ + '{properties: {description: $desc, connectors: [{name: $api, connectionName: $cn} + (if $ops != null then {operations: $ops} else {} end)]}} + (if $rat != null then {runtimeMcpConfiguration: {requireApprovalTools: $rat}} else {} end)') TOKEN=$(_dp_token) mcp_result=$(curl -sS -w "\n%{http_code}" -X PUT \ "${AGENT_ENDPOINT}/api/v2/connectorV2/mcpservers/${cv2_conn}" \ @@ -948,7 +968,30 @@ if [[ "$count" -gt 0 ]]; then done fi -# 4i-1. skills — data-plane PUT +# 4i-1. tools — data-plane PUT +# Route: PUT /api/v2/extendedAgent/tools/{name} +# Body: { name, type: , tags: [], properties: { ... tool spec } } +count=$(jq '.tools // [] | length' "$FILE") +if [[ "$count" -gt 0 ]]; then + if [[ "$DP_TOKEN_AVAILABLE" == "true" ]]; then + echo "tools: ${count}" + EXTRAS_FILE="$FILE" + for i in $(seq 0 $((count - 1))); do + name=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE") + props=$(jq -c --argjson i "$i" '.tools[$i].spec' "$FILE") + object_type=$(jq -r ".tools[$i].kind // \"Tool\"" "$EXTRAS_FILE") + dataplane_put_extended "tools" "$name" "$object_type" "[]" "$props" + done + else + echo "tools: ${count} — ⚠ skipped (no data-plane token)" + for i in $(seq 0 $((count - 1))); do + tname=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE") + DP_SKIPPED_ITEMS+=("tool/${tname}") + done + fi +fi + +# 4i-2. skills — data-plane PUT # Route: PUT /api/v2/extendedAgent/skills/{name} # Body: { name, type: "Skill", tags: [], properties: { name, description, tools, skillContent, additionalFiles } } count=$(jq '.skills // [] | length' "$FILE") @@ -975,7 +1018,7 @@ if [[ "$count" -gt 0 ]]; then fi fi -# 4i-2. subagents — data-plane PUT +# 4i-3. subagents — data-plane PUT # Route: PUT /api/v2/extendedAgent/agents/{name} # Body: { name, type: "ExtendedAgent", tags: [], properties: { instructions, handoffDescription, tools, ... } } count=$(jq '.subagents // [] | length' "$FILE") @@ -996,27 +1039,6 @@ if [[ "$count" -gt 0 ]]; then fi fi -# 4i-3. tools — data-plane PUT -# Route: PUT /api/v2/extendedAgent/tools/{name} -# Body: { name, type: "Tool", tags: [], properties: { ... tool spec } } -count=$(jq '.tools // [] | length' "$FILE") -if [[ "$count" -gt 0 ]]; then - if [[ "$DP_TOKEN_AVAILABLE" == "true" ]]; then - echo "tools: ${count}" - for i in $(seq 0 $((count - 1))); do - name=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE") - props=$(jq -c --argjson i "$i" '.tools[$i].spec' "$FILE") - dataplane_put_extended "tools" "$name" "Tool" "[]" "$props" - done - else - echo "tools: ${count} — ⚠ skipped (no data-plane token)" - for i in $(seq 0 $((count - 1))); do - tname=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE") - DP_SKIPPED_ITEMS+=("tool/${tname}") - done - fi -fi - # 4i. Webhook bridge Logic App — auto-deploy if httpTriggers exist and enableWebhookBridge is set # Solves the chicken-and-egg: trigger URL is only known after httpTrigger creation above. if [[ -n "$HTTP_TRIGGER_URL" ]]; then diff --git a/sreagent-templates/bicep/assemble-agent.sh b/sreagent-templates/bicep/assemble-agent.sh index 3eb78bae5..32427aa98 100755 --- a/sreagent-templates/bicep/assemble-agent.sh +++ b/sreagent-templates/bicep/assemble-agent.sh @@ -461,6 +461,12 @@ jq -n \ "connectorV2": $connectorV2 }' > "$EXTRAS_FILE" +if [[ -f "${DIR}/custom-instructions.md" ]]; then + EXTRAS_WITH_INSTRUCTIONS=$(jq --rawfile instructions "${DIR}/custom-instructions.md" \ + '.customInstructions = $instructions' "$EXTRAS_FILE") + printf '%s\n' "$EXTRAS_WITH_INSTRUCTIONS" > "$EXTRAS_FILE" +fi + # Merge admin settings if present (adminUsers for cross-tenant access) if [[ -f "${DIR}/admin-settings.json" ]]; then _log "Merging admin-settings.json (adminUsers) into extras" diff --git a/sreagent-templates/bin/ps/Verify-Agent.ps1 b/sreagent-templates/bin/ps/Verify-Agent.ps1 index b5a0cb9ad..1d35f978c 100644 --- a/sreagent-templates/bin/ps/Verify-Agent.ps1 +++ b/sreagent-templates/bin/ps/Verify-Agent.ps1 @@ -287,6 +287,25 @@ Add-Check 'Upgrade channel' ($Props | Invoke-Jq -Raw -Filter '.upgradeChannel Add-Check 'Model provider' ($Props | Invoke-Jq -Raw -Filter '.modelProvider') (Get-Exp '.agent.defaultModelProvider') Add-Check 'Incident platform' ($Props | Invoke-Jq -Raw -Filter '.incidentPlatform') (Get-Exp '.agent.incidentPlatform') +if ($ExpectedConfig) { + $customExpected = $ExpectedConfig | ConvertFrom-Json + if ($customExpected.PSObject.Properties['customInstructions']) { + $customStatus = 'mismatch' + try { + $customResponse = Invoke-RestMethod -Uri "$Endpoint/api/v2/agent/customInstructions" ` + -Headers @{ Authorization = "Bearer $Token" } -TimeoutSec 30 + if ($customExpected.customInstructions -is [string] -and $customResponse.instructions -is [string] -and + (($customExpected.customInstructions -replace "\r\n?", "`n").Trim() -ceq + ($customResponse.instructions -replace "\r\n?", "`n").Trim())) { + $customStatus = 'match' + } + } catch { + $customStatus = 'unavailable' + } + Add-Check 'Custom instructions' $customStatus 'match' + } +} + # ─────────────────────────── Connectors (ARM) ─────────────────────────── $Connectors = Invoke-Arm '/DataConnectors' @@ -331,6 +350,27 @@ if ($ExpManagedConnNames) { Add-InfoRow 'Managed connector names' $ManagedConnNames } +# ─────────────────────────── Custom tools ─────────────────────────── + +$HasExpectedTools = $false +$ExpToolCount = '-' +if ($ExpectedConfig) { + $expectedTools = ($ExpectedConfig | ConvertFrom-Json).PSObject.Properties['tools'] + $HasExpectedTools = $null -ne $expectedTools -and $null -ne $expectedTools.Value +} +if (-not $HasExpectedTools -and $Expected) { + $toolsDir = Join-Path $Expected 'config/tools' + if (Test-Path $toolsDir -PathType Container) { + $HasExpectedTools = $true + $ExpToolCount = [string]@(Get-ChildItem -Path $toolsDir -Filter '*.yaml' -File).Count + } +} +if ($HasExpectedTools) { + $Tools = Invoke-Dp '/api/v2/extendedAgent/tools' -Collection + $ToolCt = $Tools | Invoke-Jq -Filter '.value | length' + Add-Check 'Custom tools' $ToolCt (Get-Exp '.tools' $ExpToolCount) $CollectionCountMode +} + # ─────────────────────────── Skills ─────────────────────────── $Skills = Invoke-Dp '/api/v1/extendedAgent/skills' -Collection diff --git a/sreagent-templates/bin/verify-agent.sh b/sreagent-templates/bin/verify-agent.sh index 8cacdee6f..8ed2cf3b1 100755 --- a/sreagent-templates/bin/verify-agent.sh +++ b/sreagent-templates/bin/verify-agent.sh @@ -148,6 +148,20 @@ check "Upgrade channel" "$(echo "$PROPS" | jq -r '.upgradeChannel')" "$(exp '.ag check "Model provider" "$(echo "$PROPS" | jq -r '.modelProvider')" "$(exp '.agent.defaultModelProvider')" check "Incident platform" "$(echo "$PROPS" | jq -r '.incidentPlatform')" "$(exp '.agent.incidentPlatform')" +if [[ -n "$EXPECTED_CONFIG" ]] && printf '%s' "$EXPECTED_CONFIG" | jq -e 'has("customInstructions")' >/dev/null; then + CUSTOM_STATUS="mismatch" + if CUSTOM_RESPONSE=$(curl -fsS "$ENDPOINT/api/v2/agent/customInstructions" -H "Authorization: Bearer ${TOKEN}" 2>/dev/null) && + jq -en --argjson expected "$EXPECTED_CONFIG" --argjson actual "$CUSTOM_RESPONSE" ' + def normalize: gsub("\r\n?"; "\n") | gsub("^\\s+|\\s+$"; ""); + ($expected.customInstructions | type) == "string" and + ($actual.instructions | type) == "string" and + (($expected.customInstructions | normalize) == ($actual.instructions | normalize)) + ' >/dev/null 2>&1; then + CUSTOM_STATUS="match" + fi + check "Custom instructions" "$CUSTOM_STATUS" "match" +fi + # ── Connectors (ARM) ── CONNECTORS=$(arm_get "/DataConnectors") CONN_CT=$(echo "$CONNECTORS" | jq '[.value[] | select((.properties.dataConnectorType // "") | startswith("Knowledge") | not)] | length') @@ -184,6 +198,30 @@ EXP_MANAGED_CONN_NAMES=$(exp_list '.managedConnectors') check "Managed connectors" "$MANAGED_CONN_CT" "$EXP_MANAGED_CONN_CT" "$COLLECTION_COUNT_MODE" [[ -n "$EXP_MANAGED_CONN_NAMES" ]] && check "Managed connector names" "$MANAGED_CONN_NAMES" "$EXP_MANAGED_CONN_NAMES" "$COLLECTION_NAME_MODE" || RESULTS="${RESULTS}\n Managed connector names|${MANAGED_CONN_NAMES}|—|" +# ── Custom tools ── +HAS_EXPECTED_TOOLS=false +EXP_TOOL_COUNT="-" +if [[ -n "$EXPECTED_CONFIG" ]] && printf '%s' "$EXPECTED_CONFIG" | jq -e 'has("tools") and .tools != null' >/dev/null 2>&1; then + HAS_EXPECTED_TOOLS=true +elif [[ -n "$EXPECTED_DIR" && -d "$EXPECTED_DIR/config/tools" ]]; then + HAS_EXPECTED_TOOLS=true + EXP_TOOL_COUNT=0 + for tool_file in "$EXPECTED_DIR"/config/tools/*.yaml; do + if [[ -f "$tool_file" ]]; then + EXP_TOOL_COUNT=$((EXP_TOOL_COUNT + 1)) + fi + done +fi +if [[ "$HAS_EXPECTED_TOOLS" == "true" ]]; then + TOOLS=$(curl -fsS "$ENDPOINT/api/v2/extendedAgent/tools" -H "Authorization: Bearer $TOKEN" 2>/dev/null) || TOOLS="" + TOOL_CT=$(printf '%s' "$TOOLS" | jq -er 'if type == "array" then length elif (.value | type) == "array" then (.value | length) else error("invalid tools response") end' 2>/dev/null) || TOOL_CT="" + if [[ -n "$TOOL_CT" ]]; then + check "Custom tools" "$TOOL_CT" "$(exp '.tools' "$EXP_TOOL_COUNT")" "$COLLECTION_COUNT_MODE" + else + check "Custom tools API" "unavailable" "available" + fi +fi + # ── Skills ── SKILLS=$(dp_get "/api/v1/extendedAgent/skills") SKILL_CT=$(echo "$SKILLS" | jq 'if type == "array" then length elif .value then (.value | length) else 0 end' 2>/dev/null || echo 0) diff --git a/sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml b/sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml index 3c5a30a80..c78a2b3e5 100644 --- a/sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml +++ b/sreagent-templates/recipes/cost-management/config/connectorv2/microsoft-teams.yaml @@ -8,6 +8,12 @@ spec: connectionName: cost-management-teams requireApprovalTools: [] operations: + - name: GetUnifiedActionSchema + displayName: Get unified action input metadata + description: Get the schema information for unified actions + requiresApproval: false + userParameters: [] + agentParameters: [] - name: PostMessageToConversation displayName: Post message in a chat or channel description: Posts a message to a chat or a channel diff --git a/sreagent-templates/recipes/cost-management/custom-instructions.md b/sreagent-templates/recipes/cost-management/custom-instructions.md new file mode 100644 index 000000000..d25a60f7d --- /dev/null +++ b/sreagent-templates/recipes/cost-management/custom-instructions.md @@ -0,0 +1,157 @@ +# Agent Instructions + +You are working in a financial operations (FinOps) context where accuracy, traceability, and quantitative rigor are non-negotiable. Every response involving financial data, cost analysis, or operational recommendations must be grounded in verifiable facts and properly formatted. + +## Evidence and sourcing requirements + +Every factual claim must be backed by one of the following: + +- **Data reference**: A specific query result, dataset, file, or calculation you performed or read +- **Source citation**: A URL, document name, or specification reference (e.g., "per FOCUS 1.0 spec", "per ASC 606", "per FinOps Framework") +- **Explicit derivation**: Show the formula or logic chain that produced the number + +If you cannot back a claim, you must say so explicitly: + +Note: This estimate is based on [assumption]. Actual values require [specific data source]. + +Never present an estimate, projection, or assumption as a confirmed fact. Label each clearly: + +- **Confirmed**: Derived directly from data you have read or queried +- **Estimated**: Calculated from confirmed data using stated assumptions +- **Assumed**: Based on general knowledge or industry benchmarks, not verified against this environment + +## Financial data formatting + +### Currency + +- Always include the currency symbol and use thousand separators: `$1,234,567.89` +- Right-align currency columns in tables +- Use consistent decimal places within a table (2 for dollars, 0 for rounded summaries) +- For large values, use K/M/B suffixes only in narrative text, never in data tables: "approximately $1.2M" but table shows `$1,200,000` +- Always state the currency if there is any ambiguity (USD, EUR, etc.) + +### Percentages and ratios + +- Always include the % symbol: `15.3%`, not `0.153` or `15.3` +- Use basis points (bps) for small changes: "margin improved 45 bps" for 0.45% +- Show both absolute and percentage variance: `+$50,000 (+5.5%)` +- For period-over-period comparisons, always show the direction: `+12.3%` or `-4.7%` + +### Tables & Visualizations + +Use visualizations for any comparison involving 3+ data points. + +Visualizations should be followed by a companion data table - standard structure: + +| Metric | Current Period | Prior Period | Variance ($) | Variance (%) | +|--------|---------------|-------------|-------------|-------------| +| [Item] | $X,XXX | $X,XXX | +/-$X,XXX | +/-X.X% | + +- Bold totals and subtotals +- Include a verification row where applicable (e.g., components sum to total) +- Mark favorable variances and unfavorable variances explicitly when the direction is ambiguous (cost increases are unfavorable, revenue increases are favorable) + +### Time periods + +- Always state the exact time period for any financial figure: "Q4 2024", "October 2024", "trailing 30 days ending 2024-12-15" +- Never present a number without its time context +- When comparing periods, state both explicitly: "Q4 2024 vs Q3 2024" + +## Structured response format + +### For cost analysis or financial questions + +``` +## Summary + +[2-3 sentence finding with the key metric and its context] + +## Analysis + +[Structured breakdown with tables, supporting data, and source references] + +## Drivers + +[Ranked list of contributing factors with quantified impact] + +## Recommendations + +1. **[Action]**: [Expected impact with quantification] — [Priority: Immediate/Short-term/Long-term] + +## Confidence and caveats + +- Confidence: [High/Medium/Low] — [Basis for confidence level] +- Assumptions: [List any assumptions made] +- Data gaps: [List any missing data that would improve accuracy] +``` + +### For variance explanations + +Follow this pattern for every material variance: + +``` +[Line Item]: [Favorable/Unfavorable] variance of $[amount] ([percentage]%) +vs [comparison basis] for [period] + +Driver: [Primary driver with specific quantification] + +[2-3 sentences explaining WHY, not just WHAT] + +Outlook: [One-time / Recurring / Trending] + +Action: [None required / Monitor / Investigate / Update forecast] +``` + +### For recommendations + +Every recommendation must include: + +1. **What** to do (specific action) +2. **Why** it matters (quantified impact or risk) +3. **How** to validate (metric or verification step) +4. **Priority** (Immediate / Short-term / Long-term) + +## Calculation integrity + +- Show your work. For any derived number, show the formula or at minimum state the inputs. +- Cross-check totals: components must sum to their stated total. If they don't, flag the discrepancy. +- When decomposing variances, verify: `Starting value + Sum of all drivers = Ending value` +- State units explicitly when performing calculations. Never mix units without conversion. + +## Anti-patterns to avoid + +- "Costs were higher due to increased costs" — circular, no actual explanation +- "Expenses were elevated this period" — vague; which expenses? why? how much? +- "Approximately $X" without stating the basis for the approximation +- "Significant increase" without a number — always quantify +- "Various factors" for a material variance — always decompose +- Presenting query results without stating the query parameters (time range, filters, scope) +- Using "savings" without specifying the baseline and time period + +## FinOps domain conventions + +- Reference FinOps Framework capabilities by their official names (e.g., "Managing commitment-based discounts", not "reservation management") +- Use FOCUS specification terminology when discussing cost data fields (e.g., BilledCost, EffectiveCost, ListCost, ContractedCost) +- Reference maturity levels as Crawl/Walk/Run when discussing FinOps practice maturity +- Cite the six FinOps principles when they are relevant to a recommendation +- For Azure-specific guidance, reference the official Microsoft documentation URL + +## Disclaimers + +When providing financial analysis, include this at the end of substantive analyses: + +``` +--- + +This analysis is generated from available data and should be reviewed by qualified financial or FinOps professionals before use in reporting or decision-making. +``` + +## Microsoft Teams + +If the Teams connector is enabled/configured you should post summaries of your work to the configured channel. + +If you don't have a channel configured you should prompt the user for the channel to use and remember it. + +Only use the configured channel unless the user specifically asks you to post elsewhere. + +Escalate all anomalies and issues you find to the teams channel. diff --git a/sreagent-templates/recipes/cost-management/expected-config.json b/sreagent-templates/recipes/cost-management/expected-config.json index a4c78fd45..dd815e86d 100644 --- a/sreagent-templates/recipes/cost-management/expected-config.json +++ b/sreagent-templates/recipes/cost-management/expected-config.json @@ -14,6 +14,18 @@ } ], "allowAdditionalConnectors": true, + "managedConnectors": [ + "cost-management-teams" + ], + "knowledgeSources": [ + "well-architected-capacit-140890e", + "well-architected-regions-e4cae36", + "well-architected-disaste-e11064c", + "azure-quotas-overview", + "azure-capacity-reservations", + "azure-check-zone-peers", + "aks-upgrade-capacity-planning" + ], "knowledgeItems": [ "well-architected-capacity-planning", "well-architected-regions-and-zones", @@ -72,5 +84,6 @@ ], "responsePlans": [], "allowAdditionalRepos": true, - "repos": [] + "repos": [], + "customInstructions": "# Agent Instructions\n\nYou are working in a financial operations (FinOps) context where accuracy, traceability, and quantitative rigor are non-negotiable. Every response involving financial data, cost analysis, or operational recommendations must be grounded in verifiable facts and properly formatted.\n\n## Evidence and sourcing requirements\n\nEvery factual claim must be backed by one of the following:\n\n- **Data reference**: A specific query result, dataset, file, or calculation you performed or read\n- **Source citation**: A URL, document name, or specification reference (e.g., \"per FOCUS 1.0 spec\", \"per ASC 606\", \"per FinOps Framework\")\n- **Explicit derivation**: Show the formula or logic chain that produced the number\n\nIf you cannot back a claim, you must say so explicitly:\n\nNote: This estimate is based on [assumption]. Actual values require [specific data source].\n\nNever present an estimate, projection, or assumption as a confirmed fact. Label each clearly:\n\n- **Confirmed**: Derived directly from data you have read or queried\n- **Estimated**: Calculated from confirmed data using stated assumptions\n- **Assumed**: Based on general knowledge or industry benchmarks, not verified against this environment\n\n## Financial data formatting\n\n### Currency\n\n- Always include the currency symbol and use thousand separators: `$1,234,567.89`\n- Right-align currency columns in tables\n- Use consistent decimal places within a table (2 for dollars, 0 for rounded summaries)\n- For large values, use K/M/B suffixes only in narrative text, never in data tables: \"approximately $1.2M\" but table shows `$1,200,000`\n- Always state the currency if there is any ambiguity (USD, EUR, etc.)\n\n### Percentages and ratios\n\n- Always include the % symbol: `15.3%`, not `0.153` or `15.3`\n- Use basis points (bps) for small changes: \"margin improved 45 bps\" for 0.45%\n- Show both absolute and percentage variance: `+$50,000 (+5.5%)`\n- For period-over-period comparisons, always show the direction: `+12.3%` or `-4.7%`\n\n### Tables & Visualizations\n\nUse visualizations for any comparison involving 3+ data points.\n\nVisualizations should be followed by a companion data table - standard structure:\n\n| Metric | Current Period | Prior Period | Variance ($) | Variance (%) |\n|--------|---------------|-------------|-------------|-------------|\n| [Item] | $X,XXX | $X,XXX | +/-$X,XXX | +/-X.X% |\n\n- Bold totals and subtotals\n- Include a verification row where applicable (e.g., components sum to total)\n- Mark favorable variances and unfavorable variances explicitly when the direction is ambiguous (cost increases are unfavorable, revenue increases are favorable)\n\n### Time periods\n\n- Always state the exact time period for any financial figure: \"Q4 2024\", \"October 2024\", \"trailing 30 days ending 2024-12-15\"\n- Never present a number without its time context\n- When comparing periods, state both explicitly: \"Q4 2024 vs Q3 2024\"\n\n## Structured response format\n\n### For cost analysis or financial questions\n\n```\n## Summary\n\n[2-3 sentence finding with the key metric and its context]\n\n## Analysis\n\n[Structured breakdown with tables, supporting data, and source references]\n\n## Drivers\n\n[Ranked list of contributing factors with quantified impact]\n\n## Recommendations\n\n1. **[Action]**: [Expected impact with quantification] — [Priority: Immediate/Short-term/Long-term]\n\n## Confidence and caveats\n\n- Confidence: [High/Medium/Low] — [Basis for confidence level]\n- Assumptions: [List any assumptions made]\n- Data gaps: [List any missing data that would improve accuracy]\n```\n\n### For variance explanations\n\nFollow this pattern for every material variance:\n\n```\n[Line Item]: [Favorable/Unfavorable] variance of $[amount] ([percentage]%)\nvs [comparison basis] for [period]\n\nDriver: [Primary driver with specific quantification]\n\n[2-3 sentences explaining WHY, not just WHAT]\n\nOutlook: [One-time / Recurring / Trending]\n\nAction: [None required / Monitor / Investigate / Update forecast]\n```\n\n### For recommendations\n\nEvery recommendation must include:\n\n1. **What** to do (specific action)\n2. **Why** it matters (quantified impact or risk)\n3. **How** to validate (metric or verification step)\n4. **Priority** (Immediate / Short-term / Long-term)\n\n## Calculation integrity\n\n- Show your work. For any derived number, show the formula or at minimum state the inputs.\n- Cross-check totals: components must sum to their stated total. If they don't, flag the discrepancy.\n- When decomposing variances, verify: `Starting value + Sum of all drivers = Ending value`\n- State units explicitly when performing calculations. Never mix units without conversion.\n\n## Anti-patterns to avoid\n\n- \"Costs were higher due to increased costs\" — circular, no actual explanation\n- \"Expenses were elevated this period\" — vague; which expenses? why? how much?\n- \"Approximately $X\" without stating the basis for the approximation\n- \"Significant increase\" without a number — always quantify\n- \"Various factors\" for a material variance — always decompose\n- Presenting query results without stating the query parameters (time range, filters, scope)\n- Using \"savings\" without specifying the baseline and time period\n\n## FinOps domain conventions\n\n- Reference FinOps Framework capabilities by their official names (e.g., \"Managing commitment-based discounts\", not \"reservation management\")\n- Use FOCUS specification terminology when discussing cost data fields (e.g., BilledCost, EffectiveCost, ListCost, ContractedCost)\n- Reference maturity levels as Crawl/Walk/Run when discussing FinOps practice maturity\n- Cite the six FinOps principles when they are relevant to a recommendation\n- For Azure-specific guidance, reference the official Microsoft documentation URL\n\n## Disclaimers\n\nWhen providing financial analysis, include this at the end of substantive analyses:\n\n```\n---\n\nThis analysis is generated from available data and should be reviewed by qualified financial or FinOps professionals before use in reporting or decision-making.\n```\n\n## Microsoft Teams\n\nIf the Teams connector is enabled/configured you should post summaries of your work to the configured channel.\n\nIf you don't have a channel configured you should prompt the user for the channel to use and remember it.\n\nOnly use the configured channel unless the user specifically asks you to post elsewhere.\n\nEscalate all anomalies and issues you find to the teams channel.\n" } diff --git a/sreagent-templates/tests/test-dry-run-cost-management.sh b/sreagent-templates/tests/test-dry-run-cost-management.sh new file mode 100755 index 000000000..d815bd748 --- /dev/null +++ b/sreagent-templates/tests/test-dry-run-cost-management.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# tests/test-dry-run-cost-management.sh — cost-management recipe dry-run +set -uo pipefail +cd "$(dirname "$0")/.." +REPORT=/dev/null +source tests/lib/test-helpers.sh + +RECIPE="cost-management" +EXTRA_SETS="existingUamiId=;existingAgentAppInsightsId=" +EXP_SKILLS=1 EXP_SA=3 EXP_HOOKS=0 EXP_PROMPTS=1 EXP_SCHED=17 EXP_FILTERS=0 EXP_PLAT=0 EXP_HT=0 +OUT="tests/.dryrun-${RECIPE}-$$" +if ! mkdir "$OUT"; then + echo "Could not create test output directory: $OUT" >&2 + exit 1 +fi +trap 'rm -rf "$OUT"' EXIT + +log "═══ $RECIPE ═══" +log "── bash new-agent ──" +if ./bin/new-agent.sh --recipe "$RECIPE" --subscription ci-subscription --non-interactive --no-telemetry \ + --set agentName=dry-cost-management --set resourceGroup=rg-dry \ + --set location=swedencentral --set targetRGs=rg-fake \ + --set existingUamiId= --set 'modelProvider=Azure OpenAI' --set existingAgentAppInsightsId= \ + -o "$OUT/agent" >/dev/null 2>&1 && [[ -f "$OUT/agent/agent.json" ]]; then + pass "new-agent" +else + fail "new-agent" + print_summary "$RECIPE" + exit 1 +fi + +validate_config_dir "$OUT/agent" "$EXP_SKILLS" "$EXP_SA" "$EXP_HOOKS" "$EXP_PROMPTS" \ + "$EXP_SCHED" "$EXP_FILTERS" "$EXP_PLAT" "$EXP_HT" +validate_bicep_dryrun "$OUT/agent" +SRE_AGENT_NO_TELEMETRY=1 validate_ps_newagent "$RECIPE" "$EXTRA_SETS" +PS_OUT="/tmp/dryrun-ps-${RECIPE}" +if [[ -f "$PS_OUT/agent.json" ]]; then + validate_config_dir "$PS_OUT" "$EXP_SKILLS" "$EXP_SA" "$EXP_HOOKS" "$EXP_PROMPTS" \ + "$EXP_SCHED" "$EXP_FILTERS" "$EXP_PLAT" "$EXP_HT" +fi + +log "── validate assembled content ──" +if bash bicep/assemble-agent.sh "$OUT/agent" --output "$OUT/assembled" >/dev/null 2>&1; then + pass "assemble" +else + fail "assemble" + print_summary "$RECIPE" + exit 1 +fi + +EXTRAS="$OUT/assembled.extras.json" +for ((i=0; i/dev/null 2>&1; then + pass "PS assemble" + PS_EXTRAS="$OUT/assembled-ps.extras.json" + assert_nonempty "PS custom instructions" "$(jq -r '.customInstructions // empty' "$PS_EXTRAS")" + assert_eq "PS custom tools" "$(jq '.tools | length' "$PS_EXTRAS")" 13 + assert_eq "PS microsoft-teams connector operations" \ + "$(jq '[.connectorV2[]? | select(.metadata.name == "microsoft-teams") | .spec.operations | length] | if length == 1 then .[0] else null end' "$PS_EXTRAS")" 7 +else + fail "PS assemble" +fi + +print_summary "$RECIPE"