diff --git a/.gitignore b/.gitignore index 198952770..5417aab06 100644 --- a/.gitignore +++ b/.gitignore @@ -387,3 +387,6 @@ src/templates/finops-hub-copilot-studio/knowledge/query-catalog.md todo/ done/ release/scloud-occurrence-report.md + +# Generated SRE Agent recipe packages +docs/deploy/sre-agent/*/sre-agent-recipe.zip diff --git a/docs-mslearn/.openpublishing.redirection.finops.json b/docs-mslearn/.openpublishing.redirection.finops.json index ab20d5dbd..b2caf8eba 100644 --- a/docs-mslearn/.openpublishing.redirection.finops.json +++ b/docs-mslearn/.openpublishing.redirection.finops.json @@ -130,6 +130,11 @@ "redirect_url": "/cloud-computing/finops/toolkit/workbooks/customize-workbooks", "redirect_document_id": false }, + { + "source_path_from_root": "/finops/finops/toolkit/hubs/configure-sre.md", + "redirect_url": "/cloud-computing/finops/toolkit/sre-agent/overview", + "redirect_document_id": false + }, { "source_path_from_root": "/finops/finops/framework/manage/culture.md", "redirect_url": "/cloud-computing/finops/framework/manage/education", diff --git a/docs-mslearn/TOC.yml b/docs-mslearn/TOC.yml index 4d48e0aff..4edc706b0 100644 --- a/docs-mslearn/TOC.yml +++ b/docs-mslearn/TOC.yml @@ -54,7 +54,7 @@ href: framework/optimize/optimize-cloud-usage-cost.md - name: Architecting for cloud href: framework/optimize/architecting.md - - name: Workload optimization + - name: Usage optimization href: framework/optimize/workloads.md - name: Rate optimization href: framework/optimize/rates.md @@ -154,6 +154,26 @@ href: toolkit/hubs/upgrade.md - name: Compatibility guide href: toolkit/hubs/compatibility.md + - name: FinOps toolkit SRE Agent + items: + - name: Overview + href: toolkit/sre-agent/overview.md + - name: Deploy + href: toolkit/sre-agent/deploy.md + - name: Agents and skills + href: toolkit/sre-agent/agents.md + - name: Tools + href: toolkit/sre-agent/tools.md + - name: Scheduled tasks + href: toolkit/sre-agent/scheduled-tasks.md + - name: Knowledge and memory + href: toolkit/sre-agent/knowledge.md + - name: Security and permissions + href: toolkit/sre-agent/security.md + - name: Troubleshooting + href: toolkit/sre-agent/troubleshooting.md + - name: Template reference + href: toolkit/sre-agent/template.md - name: Power BI items: - name: Overview @@ -170,7 +190,7 @@ href: toolkit/power-bi/invoicing.md - name: Rate optimization report href: toolkit/power-bi/rate-optimization.md - - name: Workload optimization report + - name: Usage optimization report href: toolkit/power-bi/workload-optimization.md - name: Data ingestion report href: toolkit/power-bi/data-ingestion.md diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 1de8c58e3..d293afd09 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 05/26/2026 +ms.date: 06/17/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -25,28 +25,16 @@ This article summarizes the features and enhancements in each release of the Fin The following section lists features and enhancements that are currently in development. ---> - -
- -## v15 - -_Released June 2026_ - -### [Implementing FinOps guide](../implementing-finops-guide.md) v15 - -- **Fixed** - - Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` in the [data dictionary](help/data-dictionary.md) to align with FOCUS 1.2 ([#2112](https://github.com/microsoft/finops-toolkit/pull/2112)). - -### Claude Code plugin v15 +### Claude Code plugin v15.0.0 - **Added** - Added Claude Code plugin with skills for FinOps hubs and Azure Cost Management ([#2043](https://github.com/microsoft/finops-toolkit/pull/2043)). - Added 4 agents (CFO, FinOps practitioner, database query, hubs agent), 5 commands (`/ftk-hubs-connect`, `/ftk-hubs-healthCheck`, `/ftk-mom-report`, `/ftk-ytd-report`, `/ftk-cost-optimization`), and an output style. - Linked to the existing KQL query catalog in `src/queries/` from the plugin. -### [FinOps hubs](hubs/finops-hubs-overview.md) v15 +### Bicep Registry module pending updates +- Cost Management export modules for subscriptions and resource groups. - **Changed** - Added a callout to the `config_RunBackfillJob` backfill option clarifying that it isn't supported on Microsoft Customer Agreement (MCA) billing accounts or billing profiles ([#2113](https://github.com/microsoft/finops-toolkit/issues/2113)). - **Fixed** @@ -57,26 +45,34 @@ _Released June 2026_ - **Fixed** - Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` to align with FOCUS 1.2 ([#2112](https://github.com/microsoft/finops-toolkit/pull/2112)). - -### [Optimization engine](optimization-engine/overview.md) updates - -- **Fixed** - - Removed call to Azure Classic administrators endpoint (deprecated on May 1, 2026) from Azure RBAC assignments exports ([#2142](https://github.com/microsoft/finops-toolkit/issues/2142)). - --> - -> [!div class="nextstepaction"] -> [Download](https://github.com/microsoft/finops-toolkit/releases/tag/v15) -> [!div class="nextstepaction"] -> [Full changelog](https://github.com/microsoft/finops-toolkit/compare/v14...v15) - -
+
## v14 _Released April 2026_ +### Claude Code plugin v13.0.0 + +- **Added** + - Added Claude Code plugin with skills for FinOps hubs and Azure Cost Management. + - Added 4 agents (CFO, FinOps practitioner, database query, hubs agent), 5 commands (`/ftk-hubs-connect`, `/ftk-hubs-healthCheck`, `/ftk-mom-report`, `/ftk-ytd-report`, `/ftk-cost-optimization`), and an output style. + - Linked to the existing KQL query catalog in `src/queries/` from the plugin. + +### [SRE agent](sre-agent/overview.md) + +- **Added** + - Added Azure SRE Agent template with a packaged Deploy to Azure path, `deploy.sh` local CLI automation, and Bicep infrastructure. + - Added 5 subagents (`finops-practitioner`, `azure-capacity-manager`, `chief-financial-officer`, `ftk-database-query`, `ftk-hubs-agent`), 3 skills, 50 tools, and 1 Kusto MCP connector. + - Added 19 scheduled tasks (daily, weekly, monthly, semiannual, and quarterly cadences) with Teams channel delivery. + - Added 6 knowledge documents for agent onboarding, artifact verification, Teams notification patterns, known issues, document index guidance, and FinOps Toolkit output style. + - Added FinOps toolkit SRE Agent documentation pages for Microsoft Learn. +- **Changed** + - Set agent action mode to Autonomous so scheduled tasks can deliver reports without human approval. + - Switched scheduled task persistence from legacy CLI creation to manifest apply for idempotent re-runs. + - Replaced "save to knowledge base" instructions with `#remember` for operational notes across all scheduled tasks. + ### [Implementing FinOps guide](../implementing-finops-guide.md) v14 - **Added** diff --git a/docs-mslearn/toolkit/hubs/configure-sre.md b/docs-mslearn/toolkit/hubs/configure-sre.md new file mode 100644 index 000000000..309dc7bb9 --- /dev/null +++ b/docs-mslearn/toolkit/hubs/configure-sre.md @@ -0,0 +1,249 @@ +--- +title: Configure an SRE agent for FinOps hubs +description: Learn how to configure an Azure SRE agent to connect to your FinOps hub for scheduled cost analysis, capacity monitoring, and reporting. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: how-to +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: micflan +# customer intent: As a FinOps hub admin, I want to connect an Azure SRE agent to my hub so that I can receive scheduled cost reports, anomaly detection, and capacity monitoring in Teams. +--- + +# Configure an SRE agent for FinOps hubs + +[Azure SRE Agent](/azure/sre-agent/overview) supports agent-based operational workflows. This article shows how to connect Azure SRE Agent to a [FinOps hub](finops-hubs-overview.md), configure scheduled cost analysis and capacity checks from the [SRE agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent), and send results to Teams with the [Teams notification connector](/azure/sre-agent/send-notifications). + +
+ +## Prerequisites + +- [Deployed a FinOps hub instance](finops-hubs-overview.md#create-a-new-hub) with Data Explorer. +- [Configured scopes](configure-scopes.md) and ingested data successfully. +- Permissions to create deployed resources, such as **Contributor** on the subscription when the template creates the agent resource group. You also need permissions to assign roles at subscription, target resource group, and agent scopes, such as **Role Based Access Control Administrator**, **User Access Administrator**, or **Owner** on those scopes. [Learn more](/azure/role-based-access-control/built-in-roles). +- The `Microsoft.App` resource provider [registered](/azure/azure-resource-manager/management/resource-providers-and-types#register-resource-provider) on the subscription. +- For local CLI deployments only: [Azure CLI](/cli/azure/install-azure-cli) 2.60 or later, `curl`, `jq`, `python3`, and `bash` available locally for the [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent). + +
+ +## Review deployed resources + +The [SRE agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) deploys a single Azure SRE agent with these resources and configuration objects: + +| Component | Count | Description | +|-----------|-------|-------------| +| SRE agent | 1 | Stable [`Microsoft.App/agents`](/azure/sre-agent/overview) resource in [autonomous mode](/azure/sre-agent/run-modes) | +| Managed identities | 1-2 | System-assigned managed identity for the agent; portal deployments also create a user-assigned identity for the deployment script | +| Log Analytics | 1 | Workspace for agent telemetry | +| Application Insights | 1 | Linked to Log Analytics for monitoring | +| Target resource group RBAC | 3-4 per target group | Reader, Monitoring Reader, Log Analytics Reader, and Contributor when `accessLevel` is `High` | +| Azure Data Explorer role (optional) | 1 | `AllDatabasesViewer` when Azure Data Explorer parameters are provided | +| Subagents | 5 | `finops-practitioner`, `azure-capacity-manager`, `chief-financial-officer`, `ftk-database-query`, `ftk-hubs-agent` | +| Skills | 3 | `azure-capacity-management`, `azure-cost-management`, `finops-toolkit` | +| Tools | 50 | Kusto and Python tools for cost, capacity, governance, and reporting workflows | +| Connector | 1 | Kusto MCP connector to the FinOps hub Azure Data Explorer cluster | +| Scheduled tasks | 19 | Reports at daily, weekly, monthly, semiannual, and quarterly cadences | +| Knowledge docs | 6 | Onboarding, artifact verification, Teams notification patterns, known issues, document index guidance, and the FinOps Toolkit output style | + +
+ +## Deploy the SRE agent + +Use the Azure portal deployment for the same one-click experience as other FinOps toolkit templates. The template creates the Azure resources and runs an embedded deployment script to apply the FinOps hub recipe assets. + + +> [!div class="nextstepaction"] +> [Deploy to Azure](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2FcreateUiDefinition.json) + + +You can also use the [local deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent). It is copied from the Microsoft SRE Agent starter lab and updated for the FinOps toolkit. It uses Azure CLI + Bicep directly and doesn't use `azd`: + +```bash +cd src/templates/sre-agent + +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + --resource-group \ + --name \ + --location \ + --cluster-uri https://..kusto.windows.net/Hub \ + [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/] +``` + +The portal deployment is the customer-facing template entry point. `bin/deploy.sh` remains available for local CLI automation. + +The local [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent): + +1. Sets the `az` CLI context to the target subscription. This step is required for [B2B tenant environments](#troubleshoot-b2b-tenant-environments). +2. Runs a subscription-scoped Azure CLI + Bicep deployment from `infra/main.bicep`. +3. Runs `bin/apply-extras.sh`, which applies the recipe assets that are not deployed by Bicep. + +The extras step follows the Microsoft SRE Agent template pattern: connectors and KnowledgeFile sources are applied as SRE Agent child resources, while built-in tool configuration, 3 skills, 5 subagents, 50 tools, and 19 scheduled tasks are applied through the SRE Agent data plane. + +### Grant the optional Azure Data Explorer viewer role + +To grant the agent's managed identity the `AllDatabasesViewer` role on your Azure Data Explorer cluster, pass `--cluster-uri`. The deployment resolves the cluster ARM resource ID from the URI when the cluster is in the target subscription. If the cluster is in another subscription or can't be resolved, pass `--cluster-resource-id` explicitly. The deployment uses the [Azure Data Explorer role module](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep): + +```bash +--cluster-uri https://..kusto.windows.net/Hub \ +--cluster-resource-id /subscriptions/.../resourceGroups//providers/Microsoft.Kusto/clusters/ +``` + +If your Azure Data Explorer cluster has `publicNetworkAccess` set to `Disabled`, the deployment still creates all SRE Agent resources, assigns `AllDatabasesViewer`, and creates the `finops-hub-kusto` connector. The script also prints a warning because hosted Azure SRE Agent can't run direct KQL queries against private endpoint ADX clusters. Review the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations), then decide whether to enable public query access for the cluster. Until public query access is enabled or Azure SRE Agent adds private endpoint ADX query support, the connector is expected to remain unhealthy. + +### Redeploy an existing agent + +To update an existing agent, rerun the [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) with the same resource group and agent name: + +```bash +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + --resource-group \ + --name \ + --location \ + --cluster-uri https://..kusto.windows.net/Hub \ + [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/] +``` + +### Delete the deployment + +To delete Azure resources, delete the resource group that contains the SRE Agent resources after confirming no other resources share it: + +```bash +az group delete --subscription --name +``` + +
+ +## Verify the deployment + +After `bin/deploy.sh` completes, use the template's [post-deployment verification guidance](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent#verify): + +1. Confirm `bin/apply-extras.sh` completed without errors. +2. Open [sre.azure.com](https://sre.azure.com), switch to the directory that contains your subscription, and select your agent. +3. Confirm 5 subagents, 3 skills, and 50 tools appear in **Builder**. +4. Go to **Scheduled tasks** and confirm 19 tasks are listed and active. +5. Ask the agent: `What knowledge documents do you have?` and confirm the shipped knowledge is available. + +
+ +## Configure Teams notifications + +Scheduled tasks can send reports to a Teams channel through the [Teams notification connector](/azure/sre-agent/send-notifications). The connector requires interactive OAuth setup in the portal. + +1. Open [sre.azure.com](https://sre.azure.com), open your agent, then go to **Builder** > **Connectors**. +2. Select **Add connector** > **Send notification (Microsoft Teams)**. +3. Sign in with your Microsoft 365 account and paste the channel URL from **Get link to channel** in Teams. +4. Select the agent's managed identity and save. +5. Test from chat: `Post a test message to our Teams channel saying "FinOps SRE agent connected."` + +Use the built-in `PostTeamsMessage` tool from the [Teams notification guidance](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/recipes/finops-hub/knowledge/teams-notification-guide.md). Don't call the Microsoft Graph API or the connection's `dynamicInvoke` endpoint directly because that path returns a 403 error for this connector configuration. + +For Outlook notifications, follow the same pattern with the **Outlook Tools (Office 365 Outlook)** connector. See [Send notifications](/azure/sre-agent/send-notifications) for details. + +
+ +## Review scheduled tasks + +The template deploys 19 scheduled tasks from the [`recipes/finops-hub/automations/scheduled-tasks`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks) folder. When the Teams connector is configured, each task posts its final report to the connected channel: + +| Task | Agent | Schedule | What it reports | +|------|-------|----------|-----------------| +| HubsHealthCheck | finops-practitioner | Daily 6:00 AM | Hub version, data freshness, and pipeline status | +| CapacityDailyMonitor | finops-practitioner | Daily 6:30 AM | Quota usage, CRG utilization, and alert status | +| ComputeUtilizationTrend | finops-practitioner | Weekly Monday 7:00 AM | VM quota utilization trends across subscriptions and regions | +| CostOptimization | finops-practitioner | Weekly Monday 8:00 AM | Orphaned resources, rightsizing, and commitment opportunities | +| CapacityWeeklySupplyReview | finops-practitioner | Weekly Monday 8:00 AM | Quota headroom, CRG cost-waste audit, and benefit recommendations | +| NonComputeQuotaAudit | finops-practitioner | Weekly Tuesday 7:00 AM | Storage, network, and non-compute quota risks | +| DbQuotaAudit | finops-practitioner | Weekly Wednesday 7:00 AM | Database quota and region or zone access risks | +| SkuAvailabilityAudit | finops-practitioner | Weekly Wednesday 7:30 AM | Regional SKU availability and deployment blockers | +| MonitoringScopeValidation | finops-practitioner | Weekly Thursday 9:00 AM | Subscription monitoring coverage and hub freshness | +| BenefitRecommendationReview | finops-practitioner | Weekly Friday 8:00 AM | Reservation and savings plan recommendations with finance framing | +| StoragePaasGrowthForecast | finops-practitioner | Monthly 1st 8:00 AM | Storage and PaaS quota growth forecast | +| AdvisorSuppressionReview | finops-practitioner | Monthly 1st 9:00 AM | Active Advisor suppression age, expiration, and risk | +| CapacityMonthlyPlanning | finops-practitioner | Monthly 1st 9:00 AM | Demand forecast, capacity request pipeline, and governance review | +| AIWorkloadCostAnalysis | finops-practitioner | Monthly 1st 10:00 AM | AI token economics, model efficiency, and cost allocation | +| Monthly | finops-practitioner | Monthly on the 5th at 5:15 PM | Month-over-month cost analysis using Kusto evidence delegated to ftk-database-query | +| BudgetCoverageAudit | finops-practitioner | Monthly 15th 8:00 AM | Subscription budget coverage and missing guardrails | +| AlertCoverageAudit | finops-practitioner | Monthly 16th 8:00 AM | Cost anomaly alert coverage across active subscriptions | +| Semiannual | finops-practitioner | January 5 and July 5 at 9:00 AM | Semiannual year-over-year finance analysis with forecast and CFO consultation | +| CapacityQuarterlyStrategy | finops-practitioner | Quarterly 9:00 AM | FinOps capability maturity, commitment alignment, and architecture review | + +Each scheduled task reads the uploaded knowledge documents before it starts and applies `ftk-output-style.md` for evidence, formatting, FinOps capability mapping, confidence, and caveat conventions. Send financial results to Teams through the configured [notification connector](/azure/sre-agent/send-notifications). Save only operational notes, such as tool errors, workarounds, and patterns, to agent [memory](/azure/sre-agent/memory) with `#remember`; don't save financial data. + +
+ +## Troubleshoot B2B tenant environments + +In B2B environments, the Azure subscription and Azure SRE Agent resource can live in a different Microsoft Entra tenant than your Microsoft 365 home tenant. The deployment script sets the active subscription before deployment to align the CLI context with the resource tenant. + +If [sre.azure.com](https://sre.azure.com) shows the agent correctly but `bin/apply-extras.sh` cannot get an SRE Agent data-plane token, use these B2B tenant checks: + +1. Confirm the active Azure CLI context points at the subscription that owns the SRE agent resource. +2. Re-authenticate against the tenant that owns the subscription. +3. Run `az login --scope https://azuresre.dev/.default`, then rerun `bin/deploy.sh` or `bin/apply-extras.sh`. + +Browser success with CLI failure indicates that the CLI token was issued for the wrong tenant. The [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) runs `az account set --subscription` before deployment to set the target subscription context. + +
+ +## Review built-in capabilities + +Azure SRE Agent includes platform capabilities that are on by default in this template: + +- **Workspace tools**: Azure SRE Agent can run file, terminal, and Python operations in a sandboxed environment for data analysis, chart generation, and report formatting. The [Bicep template](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/infra/modules/sre-agent.bicep) deploys the stable agent API and sets `experimentalSettings.EnableSandboxGroup` and `experimentalSettings.EnableWorkspaceTools`. See [Deep context](/azure/sre-agent/workspace-tools). +- **DocsGuide**: DocsGuide provides Azure documentation grounding for agent responses. See [Use DocsGuide](/azure/sre-agent/use-docsguide). +- **Visualization**: Built-in chart and table rendering for investigation results. See [Tools](/azure/sre-agent/tools). +- **Memory**: Memory stores operational knowledge across sessions. See [Memory and knowledge](/azure/sre-agent/memory). + +The analytical subagents include `execute_python` where they need calculations, charts, tables, and downloadable artifacts. `finops-practitioner` orchestrates scheduled analysis, `ftk-database-query` owns Kusto evidence, `azure-capacity-manager` owns Azure capacity evidence, and `chief-financial-officer` provides finance and leadership consultation. + +
+ +## Review supported regions + +The Azure SRE Agent deployment supports `australiaeast`, `canadacentral`, `eastus2`, `francecentral`, `koreacentral`, `swedencentral`, and `uksouth`. The [Bicep template](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/main.bicep) restricts the `location` parameter with `@allowed`. + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20FinOps%20hubs%3F/cvaQuestion/How%20valuable%20are%20FinOps%20hubs%3F/surveyId/FTK/bladeName/Hubs/featureName/ConfigureSRE) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20FinOps%20hubs%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Anomaly management](../../framework/understand/anomalies.md) +- [Rate optimization](../../framework/optimize/rates.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) + +Related solutions: + +- [Configure AI agents for FinOps hubs](configure-ai.md) +- [FinOps hubs](finops-hubs-overview.md) +- [FinOps toolkit Power BI reports](../power-bi/reports.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/agents.md b/docs-mslearn/toolkit/sre-agent/agents.md new file mode 100644 index 000000000..d677010ae --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/agents.md @@ -0,0 +1,363 @@ +--- +title: Specialist agents and skills (Azure SRE Agent in the FinOps toolkit) +description: Understand how the FinOps toolkit configures Azure SRE Agent with specialist agents, tools, skills, and knowledge to automate FinOps and capacity management work. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: concept-article +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to understand how the FinOps toolkit's specialist agents and skills work so that I can route work to the right specialist. +--- + +# Specialist agents and skills (Azure SRE Agent in the FinOps toolkit) + +The FinOps toolkit configures [Azure SRE Agent](/azure/sre-agent/overview) with a multi-agent architecture aligned to the canonical FinOps Framework. One orchestrator receives prompts and scheduled tasks, then delegates work to specialist subagents with focused FinOps, finance, capacity, database, and hub operations expertise. + +The template configures 5 subagents, 3 skills, 50 tools (37 generated Kusto tools and 13 Python tools), and a FinOps hub connector. The orchestrator keeps the experience simple. The specialist agents keep answers grounded in the right domain. + +
+ +## finops-practitioner + +Domain: FinOps practice guidance, cost allocation, optimization, anomaly response, AI cost management, governance, and operating-model design. + +The `finops-practitioner` agent helps teams apply FinOps principles to real Azure cost and usage questions. It assesses business context, maturity, stakeholders, and trade-offs before recommending actions. It leads scheduled FinOps workflows and delegates evidence collection to specialists. + +What it does: + +- Guides cost allocation, shared-cost strategy, showback, and chargeback. +- Orchestrates cost anomaly and cost-driver investigations using evidence from `ftk-database-query`. +- Plans Usage Optimization, Rate Optimization, Governance, Policy & Risk, and practice health improvements. +- Translates FinOps concepts into practical Microsoft Cost Management and FinOps toolkit steps. +- Keeps recommendations maturity-aware across Crawl, Walk, and Run stages. + +Key tool routing: + +- The orchestrator has no direct operational tools. +- Delegation to `ftk-database-query` for every Kusto, FOCUS, `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()` evidence request. +- Delegation to `azure-capacity-manager` for quota, capacity reservation, SKU, region, zone, and AI/GPU capacity evidence. +- Delegation to `ftk-hubs-agent` for Resource Graph, data freshness, hub infrastructure, budget deployment, anomaly alert deployment, and Advisor suppression tools. + +When it's invoked: + +- A user asks for FinOps practice guidance, anomaly triage, allocation, governance, reporting, optimization, or maturity planning. +- A scheduled task needs recurring cost visibility, optimization review, AI cost analysis, or FinOps practice health checks. +- The orchestrator needs a general FinOps owner before handing deeper Kusto or finance work to another specialist. + +Example prompts: + +- "Help me design a showback model for shared platform costs across engineering teams." +- "Investigate why our AI costs increased this month and recommend the first three actions." +- "Create a Crawl-Walk-Run plan for improving tagging, budgets, anomaly response, and optimization." +- "Review our reservation and savings plan opportunities and explain which FinOps capabilities we should mature first." + +Expected output: + +- A practical FinOps recommendation with business context, maturity assumptions, trade-offs, and next steps. +- Evidence-backed cost drivers, optimization opportunities, governance actions, and stakeholder guidance. +- Clear separation between what the agent knows from tool evidence and what needs more data. + +Use this agent versus another: + +- Use `finops-practitioner` first when the question mixes cost analysis, FinOps process, governance, allocation, optimization, and stakeholder planning. +- Use `ftk-database-query` instead when the main deliverable is a specific KQL query, schema explanation, or detailed Hub database result. +- Use `chief-financial-officer` instead when the main deliverable is an executive finance narrative, budget decision, board-ready summary, or capital-allocation recommendation. +- Use `azure-capacity-manager` instead when the constraint is quota, capacity reservation supply, region access, zones, or AKS capacity readiness. +- Use `ftk-hubs-agent` instead when the work is deploying, upgrading, configuring, or troubleshooting a FinOps hub. + +
+ +## azure-capacity-manager + +Domain: Azure capacity evidence for Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Governance, Policy & Risk, and Automation, Tools & Services. + +The `azure-capacity-manager` agent maps Azure quota, region, SKU, zone, capacity reservation, and AKS capacity evidence into the FinOps Framework. It separates capacity guarantees from pricing commitments so teams can make better quota, reservation, and savings decisions. + +What it does: + +- Reviews quota usage, quota increases, quota groups, transfers, and headroom. +- Plans capacity reservation groups, sharing, overallocation, and utilization checks. +- Evaluates region access, zonal enablement, and logical-to-physical zone alignment. +- Checks AKS node pool capacity readiness and non-compute quota constraints. +- Aligns capacity planning with reservation and savings plan recommendations. + +Key tools it uses: + +- Azure CLI tools, such as `RunAzCliReadCommands`, `RunAzCliWriteCommands`, and `GetAzCliHelp` +- Python analysis through `execute_python` +- Capacity tools such as `vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `sku-availability`, and `capacity-reservation-groups` +- Azure Resource Graph and benefit recommendation tools where capacity, inventory, and rate-optimization context intersect +- Delegation to `ftk-database-query` for Kusto-backed forecast, commitment utilization, savings, pricing, recommendation, and transaction evidence + +When it's invoked: + +- A user asks about quota, capacity reservations, region access, availability zones, AKS capacity, or capacity governance. +- A scheduled capacity task checks daily quota usage, weekly capacity readiness, monthly planning, or quarterly strategy. +- Another agent needs capacity context before recommending commitments, scaling changes, or regional moves. + +Example prompts: + +- "Check whether we have enough quota and regional capacity for next month's AKS node pool expansion." +- "Plan a capacity reservation strategy for a production workload that needs guaranteed VM supply in two regions." +- "Compare our capacity reservation utilization with savings plan recommendations before we buy more commitments." +- "Explain the zone mapping risks before we move this workload to a multi-zone architecture." + +Expected output: + +- A capacity-readiness assessment with quota, region, zone, reservation, and utilization findings. +- Recommended capacity actions, such as quota requests, reservation group changes, region alternatives, or monitoring tasks. +- Explicit distinction between capacity guarantees and pricing commitments so teams don't treat reservations, savings plans, and quota as interchangeable. + +Use this agent versus another: + +- Use `azure-capacity-manager` when supply availability is the main question: quota, capacity reservations, region access, zones, AKS node pools, or capacity governance. +- Use `finops-practitioner` instead when the question is primarily about FinOps operating model, allocation, anomaly response, or broad cost optimization. +- Use `chief-financial-officer` instead when capacity decisions need executive budget framing, investment trade-offs, or board-ready risk language. +- Use `ftk-database-query` instead when you need exact Hub Kusto results about cost, utilization, recommendations, or pricing before making a capacity decision. +- Use `ftk-hubs-agent` instead when the capacity issue is blocking a hub deployment, upgrade, or configuration workflow. + +
+ +## chief-financial-officer + +Domain: Executive finance, budgeting, forecasting, risk, capital allocation, cloud economics, and board-ready FinOps narratives. + +The `chief-financial-officer` agent turns evidence packages into financial guidance. It focuses on business outcomes, executive summaries, quantified assumptions, and decision-ready recommendations. It is a consultative persona and doesn't own autonomous scheduled tasks or raw data collection. + +What it does: + +- Prepares budgeting, rolling forecasts, variance analysis, and KPI-driven performance views. +- Evaluates investment priorities, capital allocation, risk, controls, and compliance exposure. +- Assesses cloud spend through unit economics, commitment decisions, and value creation. +- Produces recommendations for boards, CEOs, finance leaders, and FinOps stakeholders. +- Calls out missing data, uncertainty, risks, and follow-up steps. + +Key tools it uses: + +- Python analysis through `execute_python` for scenario calculations and executive packaging +- Evidence packages from `finops-practitioner`, `ftk-database-query`, and `azure-capacity-manager` +- Teams tools for final communication when a configured workflow asks for executive delivery + +When it's invoked: + +- A user asks for executive summaries, budget variance, forecast drift, board narratives, or portfolio trade-offs. +- The `finops-practitioner` agent needs finance leadership for executive framing, prioritization, commitment risk, or investment tradeoffs. + +Example prompts: + +- "Summarize cloud spend performance for the CFO with risks, opportunities, and decisions needed this week." +- "Explain the budget variance for the quarter and separate timing issues from structural run-rate changes." +- "Create a board-ready narrative for our AI investment, including unit economics and governance risks." +- "Prioritize these three optimization programs by financial impact, execution risk, and business value." + +Expected output: + +- An executive-ready finance view with quantified assumptions, variance explanations, risks, options, and recommended decisions. +- Budget, forecast, portfolio, or investment guidance that connects cloud spend to business outcomes. +- Clear caveats for missing evidence, confidence level, and follow-up data needed before committing to a decision. + +Use this agent versus another: + +- Use `chief-financial-officer` when the audience is executives, finance leaders, boards, or business owners making budget, forecast, risk, or investment decisions. +- Use `finops-practitioner` instead when the work needs broader FinOps process guidance, practitioner playbooks, or operating-model design. +- Use `ftk-database-query` instead when the deliverable is detailed KQL, data extraction, schema validation, pricing lookup, or recommendation analysis. +- Use `azure-capacity-manager` instead when the core issue is capacity supply, quota, zones, reservation groups, or regional access. +- Use `ftk-hubs-agent` instead when financial reporting is blocked by hub deployment, export, connectivity, version, or health issues. + +
+ +## ftk-database-query + +Domain: FinOps hub database analysis, FOCUS-aligned KQL, schema validation, query diagnostics, pricing, recommendations, and transactions. + +The `ftk-database-query` agent is the KQL specialist. It queries the Hub database, explains schema choices, and adapts catalog queries before writing custom KQL. + +What it does: + +- Uses the Hub database for analytics and avoids the Ingestion database for end-user analysis. +- Starts with the query catalog and adapts the closest existing query when possible. +- Builds scoped custom drill-downs from `costs-enriched-base` when no catalog query fits and uses aggregate tools for full-month or scheduled reports. +- Uses `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()` for FOCUS-aligned analysis. +- Explains filters, functions, metrics, missing data, and data freshness concerns. + +Key tools it uses: + +- Memory and Azure discovery through `SearchMemory`, `RunAzCliReadCommands`, and `GetAzCliHelp` +- Python analysis through `execute_python` +- The full FinOps hub Kusto catalog, including `costs-enriched-base`, `monthly-cost-trend`, `top-services-by-cost`, `cost-by-region-trend`, `cost-anomaly-detection`, `cost-forecasting-model`, `commitment-discount-utilization`, `savings-summary-report`, `top-other-transactions`, and AI cost tools + +When it's invoked: + +- A user asks for a KQL query, schema explanation, pricing lookup, recommendation analysis, transaction analysis, or cost drill-down. +- A scheduled task needs exact FinOps hub data, a focused diagnostic, or FOCUS-aligned query execution. +- Another agent needs live cost data or schema-aware analysis before making a recommendation. + +Example prompts: + +- "Write a FOCUS-aligned KQL query that shows month-over-month cost by service and billing account." +- "Use the Hub database to find the top resources driving last week's cost anomaly." +- "Explain which columns from `Costs()` I should use for amortized cost, list cost, and chargeback reporting." +- "Compare prices for this SKU across regions and show the assumptions in the query." + +Expected output: + +- A query-first answer with the selected catalog query or custom KQL, filters, assumptions, and expected columns. +- Data interpretation that explains schema choices, functions, time windows, aggregation grain, and freshness limitations. +- Diagnostics for query failures or missing results, including what to validate in the Hub database or source exports. + +Use this agent versus another: + +- Use `ftk-database-query` when the question depends on exact FinOps hub data, KQL, schema details, pricing data, recommendations, or transaction records. +- Use `finops-practitioner` instead when you need to turn query results into a broader FinOps plan, operating-model recommendation, or governance approach. +- Use `chief-financial-officer` instead when data results need to become an executive budget, forecast, risk, or portfolio decision narrative. +- Use `azure-capacity-manager` instead when the query is only supporting quota, capacity reservation, regional supply, zone, or AKS readiness decisions. +- Use `ftk-hubs-agent` instead when the problem is that hub data is missing, stale, inaccessible, or caused by deployment or export health issues. + +
+ +## ftk-hubs-agent + +Domain: FinOps hubs deployment, upgrades, configuration, exports, dashboards, connectivity, and health. + +The `ftk-hubs-agent` agent keeps FinOps hubs running. It uses validation-first workflows for deployment, upgrade, maintenance, and troubleshooting. + +What it does: + +- Deploys and upgrades FinOps hubs. +- Maintains hub configuration, exports, dashboards, and connectivity. +- Troubleshoots deployment failures and unhealthy hub environments. +- Checks prerequisites, required resource providers, RBAC, regions, quotas, naming conflicts, and existing resources. +- Runs template validation and what-if checks before deploy or upgrade changes. + +Key tools it uses: + +- Knowledge search through `SearchMemory` +- Azure CLI tools, such as `RunAzCliReadCommands`, `RunAzCliWriteCommands`, and `GetAzCliHelp` +- Direct REST freshness checks through `data-freshness-check`, which treats `Costs()` as the source of truth for hub data freshness + +When it's invoked: + +- A user asks to deploy, upgrade, configure, maintain, or troubleshoot FinOps hubs. +- A scheduled health task checks FinOps hub readiness, export freshness, version status, or connectivity. +- Another agent needs hub health context before trusting query results or scheduled reports. + +Example prompts: + +- "Validate whether my environment is ready to deploy FinOps hubs and list any blocking prerequisites." +- "Troubleshoot why my Cost Management exports are not showing up in the Hub database." +- "Plan a safe FinOps hubs upgrade and include validation, what-if, and rollback checks." +- "Check hub data freshness and explain whether reports should trust the current `Costs()` results." + +Expected output: + +- A deployment, upgrade, configuration, or troubleshooting plan that starts with validation and identifies blockers before changes. +- Hub health findings for prerequisites, RBAC, providers, exports, connectivity, version status, and data freshness. +- Recommended next actions, including safe validation commands, what-if checks, and escalation points when write operations are required. + +Use this agent versus another: + +- Use `ftk-hubs-agent` when the work changes or diagnoses the FinOps hub platform itself: deployment, upgrade, configuration, exports, connectivity, dashboards, or health. +- Use `ftk-database-query` instead when the hub is healthy and the question is about KQL, schema, pricing, recommendations, transactions, or cost drill-downs. +- Use `finops-practitioner` instead when hub data needs to become a FinOps process, optimization, allocation, governance, or anomaly response recommendation. +- Use `chief-financial-officer` instead when hub findings need executive finance framing, budget guidance, or portfolio prioritization. +- Use `azure-capacity-manager` instead when a deployment or scaling issue is primarily about Azure quota, capacity reservation supply, region access, or zones. + +
+ +## Handoff model + +The orchestrator starts with the user's prompt or scheduled task instructions. It selects the agent whose `handoffDescription` best matches the task, then passes the conversation to that specialist. + +Agents can also recommend handoffs when the work crosses domain boundaries: + +- `finops-practitioner` hands executive finance narratives, portfolio prioritization, and board-level recommendations to `chief-financial-officer`. +- `finops-practitioner` hands deep FinOps hub database and KQL work to `ftk-database-query`. +- `finops-practitioner` hands capacity evidence requests to `azure-capacity-manager` when the task involves quota, capacity reservations, region access, zones, AKS capacity, or capacity governance. +- Hub deployment, upgrade, and troubleshooting work routes to `ftk-hubs-agent`. + +Handoffs keep each response focused. The FinOps specialist can frame the business problem, the KQL specialist can gather evidence, the capacity specialist can validate supply constraints, and the finance specialist can turn the findings into an executive decision. + +
+ +## Skills + +Skills give agents a domain reference map. The template applies all three skills to Azure SRE Agent. When a prompt or scheduled task enters one of these domains, the matching skill can load before the agent responds. + +### azure-capacity-management + +Provides Azure capacity management guidance for SaaS ISVs running workloads in their own Azure subscriptions under Enterprise Agreement or Microsoft Customer Agreement billing. It covers quota operations, quota groups, capacity reservation groups, region access, zonal enablement, AKS capacity governance, non-compute quotas, and capacity alerts. In this recipe, those references are used as implementation detail under the canonical FinOps Framework capabilities. + +The `azure-capacity-manager` agent is instructed to load this skill before capacity work. Capacity scheduled tasks also tell the agent to load it before checking quota, capacity reservation, SKU, or supply-readiness signals. + +### azure-cost-management + +Provides Azure cost optimization and financial governance guidance. It covers Advisor recommendations, commitment discounts, budgets, exports, anomaly alerts, credits, Microsoft Azure Consumption Commitment tracking, orphaned resources, VM rightsizing, and Azure retail price lookup. + +FinOps and reporting scheduled tasks load this skill with the `finops-toolkit` skill when they need Microsoft Cost Management context beyond FinOps hub Kusto data. + +### finops-toolkit + +Provides FinOps toolkit and FinOps hubs guidance. It maps tasks to the right references, explains Hub database functions, lists the Kusto query catalog, and covers hub deployment, upgrade, health checks, FOCUS mapping, Power BI, alerts, workbooks, and the FinOps toolkit for PowerShell. + +FinOps hub query, reporting, and AI cost tasks load this skill so agents can use the Hub database, `Costs()`, `Prices()`, `Recommendations()`, `Transactions()`, and the predefined Kusto query catalog correctly. + +
+ +## How agents, tools, skills, and knowledge work together + +The agent combines four layers: + +1. **Agents** route the work to the right specialist. +2. **Skills** load domain guidance and reference maps. +3. **Tools** gather evidence from Azure, FinOps hubs, Python analysis, and Kusto queries. +4. **Knowledge** supplies onboarding guidance, notification patterns, and known issue context. + +Together, these layers help the agent move from a question to an evidence-backed recommendation. The orchestrator delegates, the specialist loads the right skill, tools collect the data, and knowledge keeps the answer aligned with the deployed environment. The FinOps practitioner coordinates the operating model, the database specialist gathers Kusto evidence, the capacity specialist gathers Azure capacity evidence, and the CFO supplies finance and leadership framing. + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Anomaly management](../../framework/understand/anomalies.md) +- [Cost allocation](../../framework/understand/allocation.md) +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Architecting for cloud](../../framework/optimize/architecting.md) +- [Rate optimization](../../framework/optimize/rates.md) +- [Usage optimization](../../framework/optimize/workloads.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) +- [Azure Monitor](/azure/azure-monitor/) + +Related solutions: + +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) +- [FinOps hubs](../hubs/finops-hubs-overview.md) +- [Azure SRE Agent template reference (FinOps toolkit)](template.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/deploy.md b/docs-mslearn/toolkit/sre-agent/deploy.md new file mode 100644 index 000000000..86269d484 --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/deploy.md @@ -0,0 +1,215 @@ +--- +title: Deploy Azure SRE Agent with the FinOps toolkit +description: Deploy the FinOps toolkit Azure SRE Agent template from the Azure portal or CLI, connect it to a FinOps hub Data Explorer cluster, and validate the deployment. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: tutorial +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps hub admin, I want to deploy and configure the FinOps toolkit's Azure SRE Agent so that I can receive scheduled cost reports, anomaly detection, and capacity monitoring. +--- + + + +# Deploy Azure SRE Agent with the FinOps toolkit + +In this tutorial, you learn how to deploy the [FinOps toolkit Azure SRE Agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent), connect it to a [FinOps hub](../hubs/finops-hubs-overview.md), and validate the deployment. + +The deployment flow is copied from the Microsoft SRE Agent starter lab and updated for the FinOps toolkit. The Azure portal path uses the toolkit's packaged ARM template and an embedded deployment script to apply the SRE Agent recipe. The local CLI path uses Azure CLI + Bicep for infrastructure and the supported SRE Agent ARM and data-plane surfaces for recipe configuration. It doesn't use `azd`. + +## What gets deployed + +The FinOps hub recipe (`src/templates/sre-agent/recipes/finops-hub/`) deploys: + +| Component | Count | Notes | +|-----------|-------|-------| +| SRE Agent | 1 | `Microsoft.App/agents` | +| Model provider | 1 | Azure OpenAI provider-level routing (`MicrosoftFoundry` ARM value, `Automatic` model routing) | +| Managed identities | 1-2 | Agent system-assigned managed identity; portal deployments also create a user-assigned identity for the deployment script | +| Log Analytics workspace | 1 | Linked to the agent for telemetry | +| Application Insights | 1 | Linked to Log Analytics | +| Custom agents | 5 | FinOps practitioner orchestrator plus CFO, capacity, database-query, and hubs specialists | +| Skills | 3 | Capacity, cost management, and FinOps Toolkit | +| Tools | 50 | Kusto, capacity, and Hub infrastructure tools | +| Tool overrides | 9 | Enables SRE Agent Log Query and Visualization tools | +| Scheduled tasks | 19 | FinOps, governance, and reporting automations | +| Kusto connector | 0 or 1 | Included when you pass `--cluster-uri` | +| Knowledge docs | 6 | Five recipe knowledge docs plus the FinOps Toolkit output style | + +## Prerequisites + +- A deployed FinOps hub with Data Explorer. +- Permissions to create deployed resources, such as **Contributor** on the subscription when the template creates the agent resource group. +- Permissions to assign roles at subscription, target resource group, and agent scopes, such as **Role Based Access Control Administrator**, **User Access Administrator**, or **Owner** on those scopes. +- The `Microsoft.App` resource provider registered in the subscription. +- For local CLI deployments only: [Azure CLI](/cli/azure/install-azure-cli), `curl`, `jq`, `python3` with `PyYAML`, and Bash 3.2 or newer. + +Run: + +```bash +cd src/templates/sre-agent +bash bin/check-prerequisites.sh --subscription +``` + +## Deploy the FinOps hub recipe + +Use the Azure portal deployment when you want the same one-click experience as other FinOps toolkit templates. The template creates the Azure resources, grants the deployment script identity SRE Agent Administrator on the agent, downloads the packaged recipe assets, and applies connectors, tools, skills, subagents, knowledge, and scheduled tasks. + + +> [!div class="nextstepaction"] +> [Deploy to Azure](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2FcreateUiDefinition.json) + + +### Deploy from local CLI + +Run one script with explicit parameters: + +```bash +cd src/templates/sre-agent + +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + --resource-group \ + --name \ + --location \ + --cluster-uri https://..kusto.windows.net/Hub \ + [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/] \ + [--target-resource-group ...] \ + [--dry-run] \ + [--force] +``` + +`bin/deploy.sh --help` is the CLI contract: + +```text +Usage: bash bin/deploy.sh --recipe [options] + +Required: + --recipe Recipe directory + --subscription Azure subscription + -g, --resource-group Resource group for the agent + -n, --name Agent name + -l, --location Azure region + +Optional: + --target-resource-group Repeatable target resource group. Defaults to --resource-group. + --cluster-uri Kusto connector URI, including database name. + Example: https://..kusto.windows.net/Hub + --cluster-resource-id Optional Kusto cluster ARM resource ID. Real deployments resolve this from --cluster-uri when possible; dry-run requires it. + --no-subscription-reader Do not assign Reader at subscription scope. Default: no subscription Reader (opt-in with --subscription-reader). + --deploy-name Deployment name override. Defaults to a deterministic name. + --dry-run Validate inputs and write parameters without Azure calls. + --force Accepted for compatibility. + -h, --help Show this help. +``` + +## Validation modes + +Dry-run is hermetic and skips live Azure calls: + +```bash +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + -g \ + -n \ + -l \ + --cluster-uri https://..kusto.windows.net/Hub \ + --cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/ \ + --dry-run +``` + +When deploying from the portal, the template runs a subscription-scoped ARM deployment and then runs a `Microsoft.Resources/deploymentScripts` resource to configure the Kusto connector and remaining recipe assets through the supported SRE Agent ARM and data-plane surfaces. When deploying locally, `deploy.sh` runs the same subscription-scoped infrastructure deployment and then runs `bin/apply-extras.sh` for the recipe configuration step. + +The recipe defaults the parent SRE Agent to Azure OpenAI provider-level routing by setting `defaultModel.provider` to `MicrosoftFoundry` and `defaultModel.name` to `Automatic`. Azure SRE Agent automatically selects the model within the configured provider for each task. The template doesn't pin different models for individual custom agents or scheduled tasks because the documented SRE Agent configuration surface is provider-level. + +Resource names are deterministic for the subscription ID, agent resource group ID, and agent name. Use the same values to update an existing deployment. Post-provisioning deletes existing scheduled tasks with the recipe's task names before applying manifests so redeployments don't create duplicate automations. `--deploy-name` only changes the ARM deployment record and local build directory. + +The agent defaults to Low (read-only) access level for reporting and analysis without modification risk. Subscription Reader is opt-in (`--subscription-reader`) and only required for subscription-wide ARM-backed reports; otherwise the agent scopes reads via target resource groups. This posture allows the 19 scheduled reports to run autonomously while eliminating write blast radius. + +If `--cluster-uri` points to an Azure Data Explorer cluster with `publicNetworkAccess` set to `Disabled`, the script still deploys all resources, assigns the agent identity `AllDatabasesViewer`, and creates the `finops-hub-kusto` connector. It also prints a warning with the SRE Agent known-limitations URL because private endpoint ADX blocks direct KQL queries from the hosted agent. The customer can decide whether to enable public query access for the connector after reviewing . + +## Recipe identity defaults + +- Shipped recipes in this repo omit the `identity` block. +- Customer-authored recipes can keep identity defaults in `agent.json`. +- CLI flags always override recipe defaults. + +If you omit `--target-resource-group`, the deploy flow uses the recipe default when present; otherwise it falls back to the agent resource group. + +## Verify the deployment + +```bash +bash bin/verify-agent.sh \ + $(az account show --query id -o tsv) \ + \ + \ + --expected recipes/finops-hub +``` + +Then confirm the agent in [sre.azure.com](https://sre.azure.com). If you passed `--cluster-uri`, verify the `finops-hub-kusto` connector. If the deployment warned that the cluster denies public query access, the connector is expected to remain unhealthy until the customer enables public query access or Microsoft adds private endpoint ADX query support for SRE Agent. + +## Configure notifications + +Scheduled tasks deliver reports to Microsoft Teams and Outlook through Azure SRE Agent notification connectors. Connectors require interactive OAuth setup in [sre.azure.com](https://sre.azure.com), so the portal template and `bin/deploy.sh` don't create them. + +### Configure Teams + +1. Open [sre.azure.com](https://sre.azure.com), open your agent, then go to **Builder** > **Connectors**. +2. Select **Add connector** > **Send notification (Microsoft Teams)**. +3. Sign in with your Microsoft 365 account. +4. Paste the channel URL from **Get link to channel** in Teams. +5. Select the agent's managed identity and save. +6. Test from chat: `Post a test message to our Teams channel saying "Azure SRE Agent connected via the FinOps toolkit."` + +Use the built-in `PostTeamsMessage` tool from the [Teams notification guidance](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/recipes/finops-hub/knowledge/teams-notification-guide.md). Don't call the Microsoft Graph API or the connection's `dynamicInvoke` endpoint directly because that path returns a 403 error for this connector configuration. + +### Configure Outlook + +1. Open [sre.azure.com](https://sre.azure.com), open your agent, then go to **Builder** > **Connectors**. +2. Select **Add connector** > **Outlook Tools (Office 365 Outlook)**. +3. Sign in with a Microsoft 365 account that has mailbox access. +4. Select the agent's managed identity and save. +5. Test from chat: `Send an email to with subject "SRE Agent test" and body "Outlook connector is working."` + +For more information, see [Send notifications in Azure SRE Agent](/azure/sre-agent/send-notifications). + +## GitHub Actions example + +The included GitHub Actions example passes the cluster URI as a deploy flag while leaving secret-only inputs such as `GITHUB_PAT` and `ADO_PAT` in the environment. + +## Migrating from env-var-driven deploys + +The old deploy flow used configuration inputs such as `FINOPS_HUB_CLUSTER_URI`, `FINOPS_HUB_CLUSTER_RESOURCE_ID`, and `connectors.secrets.env`. Those inputs are no longer supported for identity or config. + +Before: + +```bash +export FINOPS_HUB_CLUSTER_URI="https://..kusto.windows.net/hub" +export FINOPS_HUB_CLUSTER_RESOURCE_ID="/subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/" +bash bin/deploy.sh recipes/finops-hub +``` + +After: + +```bash +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + --resource-group \ + --name \ + --location \ + --cluster-uri https://..kusto.windows.net/Hub \ + [--cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/] +``` + +The only supported environment-variable inputs are secrets: + +- `GITHUB_PAT` +- `ADO_PAT` +- `ADO_USE_AAD` +- `ADO_USE_MI` +- `ADO_ORG` diff --git a/docs-mslearn/toolkit/sre-agent/get-started.md b/docs-mslearn/toolkit/sre-agent/get-started.md new file mode 100644 index 000000000..1da3e0191 --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/get-started.md @@ -0,0 +1,135 @@ +--- +title: Get started with the FinOps toolkit on Azure SRE Agent +description: Learn what to do after deploying Azure SRE Agent with the FinOps toolkit — first queries, scheduled tasks, and specialized subagents. +author: flanakin +ms.author: micflan +ms.date: 06/17/2026 +ms.topic: quickstart +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: arclares +# customer intent: As a FinOps practitioner, I want to know what to do after deploying the FinOps toolkit's Azure SRE Agent so I can start getting value from it immediately. +--- + +# Get started with the FinOps toolkit on Azure SRE Agent + +You've deployed [Azure SRE Agent with the FinOps toolkit](overview.md). Here's how to start using it after the [deployment workflow](deploy.md) finishes. + +The [deployment guide](deploy.md) covers how to deploy and configure the agent. This guide focuses on what to do next: post-deployment prompts, [scheduled tasks](scheduled-tasks.md), [specialist agents](agents.md), and customization. + +
+ +## Talk to the agent + +Azure SRE Agent responds to natural language questions about your Azure environment. The [tool catalog](tools.md) describes how Kusto tools query [FinOps hubs](../hubs/finops-hubs-overview.md) data, while Python tools call Azure APIs. + +Ask questions the same way you'd ask another FinOps or platform engineer. The [agent and skills reference](agents.md) explains how the orchestrator routes work to specialist agents that use tools, skills, and knowledge to ground recommendations. + +
+ +## Automation map + +Use this map to connect each post-deployment activity to the agent, task, tool, output, and decision pattern. The [Azure SRE Agent overview](overview.md), [agents reference](agents.md), [tools reference](tools.md), and [scheduled tasks reference](scheduled-tasks.md) document the full catalog. + +| Capability | Agent | Tasks | Tools | Output | You decide | +|------------|-------|-------|-------|--------|------------| +| Cost visibility and anomaly review | [`finops-practitioner`](agents.md#finops-practitioner) | [`Monthly`](scheduled-tasks.md#monthly), [`CostOptimization`](scheduled-tasks.md#costoptimization), and [`AlertCoverageAudit`](scheduled-tasks.md#alertcoverageaudit) | [Cost analysis](tools.md#cost-analysis-and-reporting), [anomaly detection](tools.md#anomaly-detection-and-forecasting), [forecasting](tools.md#anomaly-detection-and-forecasting), and [rate optimization](tools.md#rate-optimization) tools through [`ftk-database-query`](agents.md#ftk-database-query) where Kusto evidence is required | Cost drivers, anomaly findings, forecasts, and prioritized actions from the scheduled task outputs | Which owners investigate, which anomalies are expected, and which optimization actions should move first | +| Capacity and quota posture | [`azure-capacity-manager`](agents.md#azure-capacity-manager) | [`CapacityDailyMonitor`](scheduled-tasks.md#capacitydailymonitor), [`ComputeUtilizationTrend`](scheduled-tasks.md#computeutilizationtrend), [`CapacityWeeklySupplyReview`](scheduled-tasks.md#capacityweeklysupplyreview), and [`NonComputeQuotaAudit`](scheduled-tasks.md#noncomputequotaaudit) | [`vm-quota-usage`](tools.md#capacity-management), [`capacity-reservation-groups`](tools.md#capacity-management), [`non-compute-quotas`](tools.md#capacity-management), and [`sku-availability`](tools.md#capacity-management) | Quota pressure, capacity reservation waste, SKU restrictions, and capacity blockers from the capacity task outputs | Which quota requests, region changes, reservation changes, or workload moves need action | +| Finance and commitment decisions | [`finops-practitioner`](agents.md#finops-practitioner), consulting [`chief-financial-officer`](agents.md#chief-financial-officer) | [`BenefitRecommendationReview`](scheduled-tasks.md#benefitrecommendationreview), [`Semiannual`](scheduled-tasks.md#semiannual), and [`AIWorkloadCostAnalysis`](scheduled-tasks.md#aiworkloadcostanalysis) | [Benefit, savings, commitment, forecasting, and AI cost tools](tools.md#rate-optimization) through [`ftk-database-query`](agents.md#ftk-database-query) where Kusto evidence is required | Executive summaries, savings opportunities, forecast risk, and decision-ready commitment context from FinOps task outputs with CFO framing | Which purchases, deferrals, budget changes, or executive escalations are approved | +| Hub health and data trust | [`ftk-hubs-agent`](agents.md#ftk-hubs-agent) | [`HubsHealthCheck`](scheduled-tasks.md#hubshealthcheck) and [`MonitoringScopeValidation`](scheduled-tasks.md#monitoringscopevalidation) | [`data-freshness-check`](tools.md#data-ingestion-and-health), Azure discovery, and hub configuration tools | Hub version, connectivity, data freshness, and monitoring coverage findings from hub health task outputs | Whether reports are trustworthy, which exports need repair, and when to pause analysis that depends on stale data | + +
+ +## Getting started: first things to try + +Start with focused questions that validate your data, summarize spend, and find common optimization opportunities that map to the documented [tool catalog](tools.md) and [scheduled-task coverage](scheduled-tasks.md#task-details). + +1. Check your data pipeline and scheduled outputs: "Is my FinOps hubs data fresh, which [scheduled tasks](scheduled-tasks.md#task-details) ran recently, and what reports or Teams notifications did they produce?" +2. Review your spending and waste: "What did we spend last month by subscription, and which idle VMs across subscriptions should we investigate first?" +3. Review anomalies: "Show me this week's cost anomalies, explain the likely drivers, and recommend who should investigate each one." +4. Find savings: "What reservation recommendations do I have?" +5. Check capacity posture: "What's my quota utilization in eastus?" + +If a response doesn't have enough context, narrow the question by subscription, management group, billing scope, region, resource type, or reporting period so the agent can select focused tools and filters from the [tool catalog](tools.md). + +
+ +## Scheduled tasks run automatically + +The agent runs [19 scheduled tasks](scheduled-tasks.md) on daily, weekly, monthly, semiannual, and quarterly cadences. The [scheduled tasks reference](scheduled-tasks.md) lists daily, weekly, monthly, semiannual, and quarterly tasks for health, optimization, capacity, planning, finance, and strategy reviews. + +You don't need to trigger scheduled tasks because each task has a cron expression in its task definition, and [task details](scheduled-tasks.md#task-details) describe how reports are formatted for Microsoft Teams when notifications are configured. + +Customize the defaults before you rely on the automation in production. Review each scheduled task's `cron_expression` and prompt thresholds in the [task details](scheduled-tasks.md#task-details), then adjust schedules and thresholds to match your operating rhythm, time zone, reporting calendar, and risk tolerance. + +For example, move the month-over-month report to run after billing data settles. Lower quota headroom thresholds for regions with known capacity pressure. Raise anomaly review thresholds for subscriptions with expected seasonal spikes. The [customization options](scheduled-tasks.md#task-details) cover each task in detail. + +
+ +## Build on the basics + +The agent includes [5 specialized subagents](agents.md), and you can ask the orchestrator to route the work or mention the specialist when you know which domain you need. + +- [`finops-practitioner`](agents.md#finops-practitioner) — cost analysis, optimization, budgets, alerts, and FinOps practice guidance +- [`azure-capacity-manager`](agents.md#azure-capacity-manager) — quota, capacity reservations, SKU availability, and capacity governance +- [`chief-financial-officer`](agents.md#chief-financial-officer) — consultative financial strategy, commitment decisions, budgeting, forecasting, and executive finance narratives +- [`ftk-database-query`](agents.md#ftk-database-query) — direct Kusto queries, schema validation, pricing, recommendations, and transactions against FinOps hubs +- [`ftk-hubs-agent`](agents.md#ftk-hubs-agent) — hub health, data freshness, exports, connectivity, deployment, and upgrade troubleshooting + +Use specialist names when you want a specific lens. For example, ask the [`chief-financial-officer`](agents.md#chief-financial-officer) agent to frame a commitment decision for leadership, or ask the [`ftk-database-query`](agents.md#ftk-database-query) agent to explain the KQL behind a cost trend. + +
+ +## Learn more + +Use these pages as your next steps: + +- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md) +- [Specialist agents and skills](agents.md) +- [Scheduled tasks (Azure SRE Agent in the FinOps toolkit)](scheduled-tasks.md) +- [FinOps toolkit best practices](../../best-practices/library.md) + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/GetStarted) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Architecting for cloud](../../framework/optimize/architecting.md) +- [Rate optimization](../../framework/optimize/rates.md) +- [Usage optimization](../../framework/optimize/workloads.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) +- [Microsoft Cost Management](/azure/cost-management-billing/costs/) + +Related solutions: + +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md) +- [Specialist agents and skills](agents.md) +- [Scheduled tasks (Azure SRE Agent in the FinOps toolkit)](scheduled-tasks.md) +- [FinOps toolkit best practices](../../best-practices/library.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/knowledge.md b/docs-mslearn/toolkit/sre-agent/knowledge.md new file mode 100644 index 000000000..2525342e7 --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/knowledge.md @@ -0,0 +1,172 @@ +--- +title: Manage knowledge and memory (Azure SRE Agent in the FinOps toolkit) +description: Learn how knowledge grounds the FinOps toolkit's Azure SRE Agent in your team's context and how memory keeps operational learnings available across sessions and redeployments. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: concept-article +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to manage knowledge and memory for Azure SRE Agent so that the agent can answer with my team's operational context. +--- + +# Manage knowledge and memory (Azure SRE Agent in the FinOps toolkit) + +Knowledge grounds the agent in your team's context. It helps the agent answer with your runbooks, known issues, notification patterns, and deployment details instead of relying on general guidance alone. + +Use knowledge and memory together: + +- **Knowledge** gives the agent trusted reference material, such as runbooks, architecture notes, and connector setup guidance. +- **Memory** keeps useful learnings from investigations, user instructions, and recurring operational patterns. +- **Session insights** help the agent learn from previous work, including what fixed an issue and what did not. + +
+ +## Shipped knowledge docs + +The [Azure SRE Agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) uploads six knowledge documents during extras configuration. Five are under `recipes/finops-hub/knowledge/`; the shared FinOps Toolkit output style is uploaded from `src/templates/claude-plugin/output-styles/ftk-output-style.md`. The script uploads them as portal-visible Knowledge Sources and verifies the KnowledgeFile sources are indexed before continuing so missing or unindexed documents fail deployment instead of becoming a silent runtime issue. + +| Knowledge doc | What it provides | +|---|---| +| `chart-artifact-verification.md` | Guidance for validating generated charts, tables, and downloadable artifacts before sending reports to stakeholders. | +| `document-index.md` | Index of the shipped FinOps, capacity, and hub-operation documents the agent can use for grounding. | +| `ftk-output-style.md` | Shared report style for evidence-backed financial and capacity-management output, including FinOps capability sections, thresholds, confidence, caveats, and disclaimers. | +| `onboarding-recommendations.md` | First-run guidance for team onboarding, `/learn`, and "What should I do next?" prompts. It reminds the agent to validate Azure access, enable visualization tools, configure FinOps hub data sources, and recommend Outlook and Microsoft Teams connectors when needed. | +| `teams-notification-guide.md` | Delivery guidance for scheduled reports and notifications. It tells the agent to use the built-in `PostTeamsMessage` and `ReplyToTeamsThread` tools, format Teams messages as HTML, and avoid unsupported direct calls to Microsoft Graph or connector endpoints. | +| `known-issues-and-workarounds.md` | Operational workarounds found during scheduled task validation. It covers stale data detection, Resource Graph fallbacks, quota command issues, JMESPath escaping, memory write conflicts, Kusto query errors, and the split between financial data in Teams and operational learnings in memory. | + +> [!TIP] +> Keep these docs short and practical. The agent uses them best when each file gives clear instructions, constraints, and examples. + +
+ +## Memory system + +Azure SRE Agent uses memory to carry context forward between threads. The agent extends that with these memory layers to improve recommendations over time. + +### Session insights + +Session insights are extracted automatically after a thread goes quiet. They capture useful details from the conversation, such as: + +- Symptoms observed +- Resolution steps +- Root cause +- Pitfalls to avoid + +For FinOps operations, this helps the agent remember patterns like stale export data, commands that failed, fallbacks that worked, and report delivery issues. Session insights are most useful when users correct or rate responses after an investigation. + +### User memories + +User memories are explicit facts that you ask the agent to save. Use them for stable details the agent should remember, such as team preferences, environment constraints, or recurring operational notes. + +Use these commands in chat: + +- `#remember` to save a fact +- `#retrieve` to find saved facts +- `#forget` to delete a saved fact + +> [!IMPORTANT] +> Save operational context, not sensitive financial figures. Send costs, savings, forecasts, and grade results to Teams reports instead of persisting them in memory. + +### Proactive knowledge persistence + +The agent also maintains synthesized knowledge files under `memories/synthesizedKnowledge/`. + +- `overview.md` is loaded when a conversation starts and acts as an at-a-glance summary. +- Topic files store deeper notes, such as team context, architecture, deployment patterns, logs, auth, debugging notes, and reusable queries. + +The agent updates these files by merging new information and removing outdated details. This gives the agent a durable, compact summary of what your team has learned. + +
+ +## Knowledge sources in the portal + +You can add more knowledge in [sre.azure.com](https://sre.azure.com) from **Builder** > **Knowledge sources**. + +Knowledge sources can include: + +- **Files**: Upload runbooks, architecture docs, escalation guides, API docs, and team procedures. +- **Web pages**: Connect stable documentation pages that the agent should cite. +- **Repositories**: Connect source repositories when code, configuration, or markdown docs should ground agent responses. + +Use uploaded files for stable content. Use connected sources for content that changes often, such as a wiki, repository, or live documentation site. + +The template upload path uses the same SRE Agent data-plane contract as the portal file upload flow: `PUT /api/v2/extendedAgent/connectors/{name}` with `type: KnowledgeItem` and `dataConnectorType: KnowledgeFile`. Validate the shipped knowledge with `bin/verify-agent.sh`, the `bin/apply-extras.sh` output, or **Builder** > **Knowledge sources**. + +
+ +## Knowledge across redeployments + +Knowledge from the template persists across redeployments through the extras configuration step. + +When you run `bin/deploy.sh`, the template provisions Azure resources and then runs `bin/apply-extras.sh`. The extras script uploads everything under `recipes/finops-hub/knowledge/`, uploads the shared output style, and waits for the expected KnowledgeFile sources to appear as indexed: + +```bash +for file in "$REPO_ROOT"/src/templates/sre-agent/recipes/finops-hub/knowledge/*.md; do + PUT "$SRE_AGENT_ENDPOINT/api/v2/extendedAgent/connectors/$(basename "$file")" +done +PUT "$SRE_AGENT_ENDPOINT/api/v2/extendedAgent/connectors/ftk-output-style.md" +curl -H "Authorization: Bearer $TOKEN" "$SRE_AGENT_ENDPOINT/api/v2/extendedAgent/connectors" +``` + +This keeps the shipped onboarding, artifact verification, Teams notification, document index, output-style, and known-issues guidance available after the agent is redeployed. If you add your own files to `recipes/finops-hub/knowledge/`, they are uploaded by the same step. + +
+ +## Best practices + +Use knowledge for information the agent should trust and cite. + +- Upload runbooks, architecture guides, escalation paths, known issues, connector setup notes, and service-specific troubleshooting guides. +- Name files by task or topic, such as `aks-scale-out-runbook.md`, `finops-hub-kusto-queries.md`, or `teams-report-delivery.md`. +- Keep one purpose per file so the agent can retrieve the right source quickly. +- Include owner, date, scope, prerequisites, and last review notes in operational docs. +- Avoid secrets, credentials, personal data, and raw financial figures. +- Review knowledge quarterly to remove stale workarounds, retire old commands, and add new operational learnings. + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent) + + +
+ +## Vote on or suggest ideas + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Anomaly management](../../framework/understand/anomalies.md) +- [Workload management](../../framework/optimize/workloads.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) +- [Microsoft Teams](/microsoftteams/) + +Related solutions: + +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [FinOps hubs](../hubs/finops-hubs-overview.md) +- [FinOps toolkit Power BI reports](../power-bi/reports.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/kusto-tools.md b/docs-mslearn/toolkit/sre-agent/kusto-tools.md new file mode 100644 index 000000000..1ae2520f9 --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/kusto-tools.md @@ -0,0 +1,497 @@ +--- +title: Kusto tools +description: Review the FinOps hub Kusto tools the FinOps toolkit ships for Azure SRE Agent and learn when to use each tool for cost, commitment discount, anomaly, forecast, AI, and price analysis. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: reference +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to understand each Kusto tool the FinOps toolkit ships for Azure SRE Agent so that I can choose the right query for cost and optimization analysis. +--- + +# Kusto tools + +The FinOps toolkit deployment configures Azure SRE Agent with 37 Kusto tools that query your FinOps hub Azure Data Explorer database through the `finops-hub-kusto` connector. Each tool is configured as a `KustoTool` and generated from the FinOps hub query catalog to ground agent responses in cost, price, recommendation, transaction, KPI, and AI usage data. Query source lives at [`src/queries/catalog`](../../../src/queries/catalog/). The SRE Agent recipe rejects explicit Kusto YAML files so the query catalog stays the only source for Kusto tool definitions. + +This reference also calls out related optimization tools that appear in scheduled-task requirements when they affect the same analysis path. Those tools aren't Kusto tools unless explicitly marked as `KustoTool`. + +> [!IMPORTANT] +> Kusto tools require the hosted Azure SRE Agent to reach the Azure Data Explorer query endpoint. If the FinOps hub cluster has `publicNetworkAccess` set to `Disabled`, the toolkit still deploys the agent, assigns `AllDatabasesViewer`, and creates the connector, but the connector is expected to remain unhealthy. Review the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations), then decide whether to enable public query access for the cluster. + +Use this reference when you want to understand which tool fits a prompt, scheduled task, or custom agent workflow. For a summary of all the agent's tools, see [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md). + +
+ +## Source validation + +The template generates every FinOps hub Kusto tool from the 37 `.kql` files in [`src/queries/catalog`](../../../src/queries/catalog/). The recipe keeps 13 `PythonTool` YAML files in [`src/templates/sre-agent/recipes/finops-hub/config/tools`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/). Build validation fails if an explicit Kusto YAML file is added, if a catalog query doesn't generate exactly one Kusto tool, if a KPI-linked query isn't generated as a Kusto tool, or if a KPI-linked query isn't requested by any scheduled task. + +| Source category | Count | Evidence | +|-----------------|------:|----------| +| FinOps hub Kusto tools | 37 | Generated from [`src/queries/catalog/*.kql`](../../../src/queries/catalog/) by [`build-extras.py`](../../../src/templates/sre-agent/bin/build-extras.py). The catalog is indexed in [`src/queries/INDEX.md`](../../../src/queries/INDEX.md), and KPI-linked tools are mapped in [`src/queries/KPI.md`](../../../src/queries/KPI.md). | +| Related Python tools | 13 | [`benefit-recommendations`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/benefit-recommendations.yaml), [`capacity-reservation-groups`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/capacity-reservation-groups.yaml), [`data-freshness-check`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/data-freshness-check.yaml), [`db-service-quotas`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/db-service-quotas.yaml), [`deploy-anomaly-alert`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-anomaly-alert.yaml), [`deploy-budget`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-budget.yaml), [`deploy-bulk-anomaly-alerts`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-anomaly-alerts.yaml), [`deploy-bulk-budgets`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-budgets.yaml), [`non-compute-quotas`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/non-compute-quotas.yaml), [`resource-graph-query`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/resource-graph-query.yaml), [`sku-availability`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/sku-availability.yaml), [`suppress-advisor-recommendations`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/suppress-advisor-recommendations.yaml), and [`vm-quota-usage`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/vm-quota-usage.yaml) | + +
+ +## Cost analysis + +Use cost analysis tools to review cost and usage from different reporting angles, including time, service, resource group, region, billing hierarchy, and resource type. + +### costs-enriched-base + +Source query: [`costs-enriched-base.kql`](../../../src/queries/catalog/costs-enriched-base.kql). + +Queries a guarded, enriched row-level cost and usage sample with tags, resource details, savings fields, commitment fields, and FinOps toolkit metadata. + +Use it only for narrow detail drill-downs after an aggregate result identifies the scope to inspect. The tool rejects windows greater than one day to avoid Azure Data Explorer result truncation. For full-month, fiscal-period, scheduled report, dashboard export, allocation, showback, chargeback, tag-coverage, or broad custom analysis, use aggregate tools first, such as `monthly-cost-trend`, `cost-by-financial-hierarchy`, `top-services-by-cost`, `top-resource-groups-by-cost`, or `top-resource-types-by-cost`. + +Example prompt: "Show me enriched cost details for yesterday's top resource group, including tags and resource group." + +Sample output shape: One row per cost record with fields such as `ChargePeriodStart`, `SubAccountName`, `ResourceId`, `ResourceName`, `ResourceType`, `ServiceName`, `x_ResourceGroupName`, `BilledCost`, `EffectiveCost`, `ContractedCost`, `ListCost`, `PricingQuantity`, `Tags`, `CommitmentDiscountType`, `CommitmentDiscountStatus`, `x_TotalSavings`, and `x_FreeReason`. + +### monthly-cost-trend + +Source query: [`monthly-cost-trend.kql`](../../../src/queries/catalog/monthly-cost-trend.kql). + +Queries monthly billed and effective cost totals to show cost trends over time. + +Use it when you need a month-by-month view for budget reviews, executive summaries, or recurring FinOps reporting. It helps identify whether spend is increasing, decreasing, or stabilizing. + +Example prompt: "Show the monthly cost trend for the last six months." + +Sample output shape: One row per month with `x_ChargeMonth`, `BilledCost`, and `EffectiveCost`. + +### monthly-cost-change-percentage + +Source query: [`monthly-cost-change-percentage.kql`](../../../src/queries/catalog/monthly-cost-change-percentage.kql). + +Queries month-over-month billed and effective cost changes as percentages. + +Use it when stakeholders ask how much costs changed between months or when you need to find volatility quickly. It works well for variance reviews and cost spike triage. + +Example prompt: "Which months had the largest percentage increase in effective cost?" + +Sample output shape: One row per month with `ChargePeriodStart`, `BilledCost`, `EffectiveCost`, `PreviousBilledCost`, `PreviousEffectiveCost`, billed-cost change percentage, and effective-cost change percentage. + +### quarterly-cost-by-resource-group + +Source query: [`quarterly-cost-by-resource-group.kql`](../../../src/queries/catalog/quarterly-cost-by-resource-group.kql). + +Queries quarterly cost by resource group, subscription, and month for a reporting window. + +Use it when preparing quarterly business reviews or resource-group-level accountability reports. It helps teams connect quarterly cost movement to resource ownership and subscription context. + +Example prompt: "Break down quarterly costs by resource group for the last quarter." + +Sample output shape: One row per subscription, resource group, and month with `SubAccountName`, `x_ResourceGroupName`, `x_ChargeMonth`, and `EffectiveCost`. + +### cost-by-region-trend + +Source query: [`cost-by-region-trend.kql`](../../../src/queries/catalog/cost-by-region-trend.kql). + +Queries effective cost trends by Azure region. + +Use it when you need to understand regional cost distribution, investigate regional growth, or evaluate whether workloads are shifting between regions. It can also support capacity and placement discussions. + +Example prompt: "Show cost trends by Azure region over the last three months." + +Sample output shape: One row per region with `RegionName` and `EffectiveCost`. + +### cost-by-financial-hierarchy + +Source query: [`cost-by-financial-hierarchy.kql`](../../../src/queries/catalog/cost-by-financial-hierarchy.kql). + +Queries costs organized by billing profile, invoice section, team, product, application, environment, and other financial hierarchy fields. + +Use it when you need a finance-aligned view for allocation, showback, chargeback, or executive reporting. It helps translate resource-level usage into business ownership. + +Example prompt: "Summarize costs by billing profile, team, product, and application." + +Sample output shape: One row per financial hierarchy combination with `x_BillingProfileName`, `x_InvoiceSectionName`, `x_Team`, `x_Product`, `x_Application`, `x_Environment`, `EffectiveCost`, and `PercentOfTotal`. + +### top-services-by-cost + +Source query: [`top-services-by-cost.kql`](../../../src/queries/catalog/top-services-by-cost.kql). + +Queries the Azure services with the highest effective cost. + +Use it when you need to prioritize service-level optimization or identify which services drive overall spend. Start here for broad cost reviews before drilling into resources or resource groups. + +Example prompt: "What are the top 10 Azure services by cost this month?" + +Sample output shape: One row per service with `ServiceName` and `EffectiveCost`. + +### top-resource-types-by-cost + +Source query: [`top-resource-types-by-cost.kql`](../../../src/queries/catalog/top-resource-types-by-cost.kql). + +Queries the resource types with the highest effective cost and resource counts. + +Use it when you need to understand which resource categories are driving spend, such as virtual machines, disks, databases, or networking resources. It helps identify optimization themes across many resources. + +Example prompt: "List the top resource types by effective cost and resource count." + +Sample output shape: One row per resource type with `ResourceType`, `ResourceCount`, and `EffectiveCost`. + +### top-resource-groups-by-cost + +Source query: [`top-resource-groups-by-cost.kql`](../../../src/queries/catalog/top-resource-groups-by-cost.kql). + +Queries the resource groups with the highest effective cost. + +Use it when you need an owner-friendly cost view or want to focus optimization work on the most expensive resource groups. It is useful for team accountability and workload-level reviews. + +Example prompt: "Which resource groups had the highest costs in the last 30 days?" + +Sample output shape: One row per subscription and resource group with `SubAccountName`, `x_ResourceGroupName`, and `EffectiveCost`. + +
+ +## Commitment discounts + +Use commitment discount tools to review reservation and savings plan utilization, recommendations, realized savings, and purchase transactions. + +### commitment-discount-utilization + +Source query: [`commitment-discount-utilization.kql`](../../../src/queries/catalog/commitment-discount-utilization.kql). + +Queries consumed core hours by commitment discount type, including reservation, savings plan, and on-demand usage. + +Use it when you need to understand how well commitments are being used and whether uncovered on-demand usage remains. It helps compare capacity planning, usage patterns, and commitment coverage. + +Example prompt: "Show reservation and savings plan utilization for the last month." + +Sample output shape: One row per commitment category with `CommitmentDiscountType`, `TotalConsumedCoreHours`, and `PercentOfTotal`. Empty commitment types are reported as `On Demand`. + +### reservation-recommendation-breakdown + +Source query: [`reservation-recommendation-breakdown.kql`](../../../src/queries/catalog/reservation-recommendation-breakdown.kql). + +Queries detailed reservation recommendations, including savings, break-even dates, normalized sizes, scope, and term details. + +Use it when evaluating whether to buy reservations or when preparing a recommendation package for finance and workload owners. It helps compare potential savings with commitment risk. + +Example prompt: "Break down reservation purchase recommendations by service, term, and expected savings." + +Sample output shape: One row per reservation recommendation with fields such as `RegionId`, normalized size or group details, `x_SkuMeterId`, `x_SkuTerm`, expected before and after effective cost, `x_BreakEvenMonths`, and `x_BreakEvenDate`. + +### benefit-recommendations + +Source query: [`benefit-recommendations.kql`](../../../src/queries/catalog/benefit-recommendations.kql). + +Gets Microsoft Cost Management benefit recommendations for savings plans and reserved instances at a billing scope. + +This is a related optimization tool, but it's a `PythonTool`, not a Kusto tool. Use it when the agent needs current Cost Management recommendation API results by billing scope, lookback period, and term. Use `reservation-recommendation-breakdown` when you need FinOps hub recommendation data from `Recommendations()`. + +Example prompt: "Get current savings plan and reservation benefit recommendations for this billing profile." + +Sample output shape: A JSON object with `billing_scope`, `lookback_period`, `term`, `count`, and `recommendations`. Each recommendation includes fields such as `type`, `savings`, `cost`, `total_cost`, `cost_without_benefit`, `term`, `break_even`, `id`, and `name`. If the request can't run, the object includes an `error` field. + +### savings-summary-report + +Source query: [`savings-summary-report.kql`](../../../src/queries/catalog/savings-summary-report.kql). + +Queries list cost, effective cost, negotiated savings, commitment savings, total savings, and savings rate. + +Use it when you need a summary of savings from discounts and commitments compared with pay-as-you-go pricing. It works well for executive reporting and rate optimization reviews. + +Example prompt: "Summarize total savings from negotiated rates and commitment discounts this month." + +Sample output shape: One row per billing currency with `BillingCurrency`, `ListCost`, `ContractedCost`, `EffectiveCost`, `x_NegotiatedDiscountSavings`, `x_CommitmentDiscountSavings`, `x_TotalSavings`, and `x_EffectiveSavingsRate`. + +### top-commitment-transactions + +Source query: [`top-commitment-transactions.kql`](../../../src/queries/catalog/top-commitment-transactions.kql). + +Queries the largest non-usage commitment discount purchase transactions, including reservations and savings plans. + +Use it when investigating major commitment-related charges, purchase timing, or renewal activity. It helps separate commitment purchases from normal usage costs. + +Example prompt: "Show the largest reservation and savings plan transactions this quarter." + +Sample output shape: One row per commitment purchase transaction with `ChargePeriodStart`, `ChargeCategory`, `BilledCost`, `BillingCurrency`, `CommitmentDiscountName`, `CommitmentDiscountNameUnique`, `CommitmentDiscountType`, `SubAccountName`, `SubAccountNameUnique`, `ResourceNameUnique`, and `x_ResourceGroupNameUnique`. + +
+ +## Anomaly detection + +Use anomaly detection tools to identify unusual cost patterns that need investigation. + +### cost-anomaly-detection + +Source query: [`cost-anomaly-detection.kql`](../../../src/queries/catalog/cost-anomaly-detection.kql). + +Queries cost time series and detects unusual spikes or drops across a configurable history window. + +Use it when you need to triage unexpected spend changes, monitor recurring cost health, or explain why costs moved outside the normal pattern. Pair it with cost drill-down tools to find the affected service, region, or resource group. + +Example prompt: "Detect cost anomalies over the last 90 days and explain the biggest spikes." + +Sample output shape: One time-series row with `ChargePeriodStart`, `CostSeries`, and `anomalies`, where the series arrays represent the analyzed interval and the anomaly markers. + +
+ +## Forecasting + +Use forecasting tools to project future cost based on historical patterns. + +### cost-forecasting-model + +Source query: [`cost-forecasting-model.kql`](../../../src/queries/catalog/cost-forecasting-model.kql). + +Queries historical cost trends and projects future effective cost. + +Use it when preparing budgets, rolling forecasts, or expected run-rate discussions. It is most helpful when recent historical usage is representative of near-term demand. + +Example prompt: "Forecast effective cost for the next three months based on recent trends." + +Sample output shape: One time-series row with `ChargePeriodStart`, `EffectiveCostSeries`, and `forecast`, where the forecast array extends the historical cost series for the requested future periods. + +
+ +## AI/ML costs + +Use AI/ML cost tools to analyze Azure OpenAI and related AI service costs, token usage, model efficiency, and application ownership. + +### ai-cost-by-application + +Source query: [`ai-cost-by-application.kql`](../../../src/queries/catalog/ai-cost-by-application.kql). + +Queries AI and machine learning costs by application, team, and environment tags. + +Use it when you need showback, chargeback, or ownership analysis for AI workloads. It helps map AI spend to the applications and teams consuming it. + +Example prompt: "Break down AI costs by application and environment for this month." + +Sample output shape: One row per application, team, environment, and cost center with fields such as `Application`, `Team`, `Environment`, `CostCenter`, `EffectiveCost`, `TokenCount`, and `CostPer1KTokens`. + +### ai-daily-trend + +Source query: [`ai-daily-trend.kql`](../../../src/queries/catalog/ai-daily-trend.kql). + +Queries daily AI and machine learning cost trends. + +Use it when you need to monitor day-to-day AI spend, spot sudden changes, or prepare daily operating reports. It can also provide context before investigating token or model-level drivers. + +Example prompt: "Show the daily AI cost trend for the last 30 days." + +Sample output shape: One row per day with `ChargePeriodStart`, daily AI cost, daily token count, and cost per 1,000 tokens. + +### ai-model-cost-comparison + +Source query: [`ai-model-cost-comparison.kql`](../../../src/queries/catalog/ai-model-cost-comparison.kql). + +Queries AI model costs so the agent can compare costs across models. + +Use it when evaluating model efficiency, unit economics, or opportunities to shift workloads to lower-cost models. It is useful for comparing model choices before changing application behavior. + +Example prompt: "Compare costs across AI models and identify the most expensive models per 1,000 tokens." + +Sample output shape: One row per model with `Model`, `TokenCount`, `EffectiveCost`, `ListCost`, `CostPer1KTokens`, `ListPer1KTokens`, and `DiscountPercent`. + +### ai-token-usage-breakdown + +Source query: [`ai-token-usage-breakdown.kql`](../../../src/queries/catalog/ai-token-usage-breakdown.kql). + +Queries AI token usage by model, model version, and input or output direction. + +Use it when token consumption is the suspected cost driver or when you need to separate prompt and completion usage. It helps connect AI cost changes to usage behavior. + +Example prompt: "Break down token usage by model and input versus output tokens for the last week." + +Sample output shape: One row per model and token direction with `Model`, `Direction`, `TokenCount`, `EffectiveCost`, `UnitCostPerToken`, and `CostPer1KTokens`. + +
+ +## FinOps KPI tools + +FinOps KPI tools are generated from the query catalog and map to query links in [`src/queries/KPI.md`](../../../src/queries/KPI.md). Scheduled tasks request these tools through `ftk-database-query` for monthly, semiannual, health, anomaly, capacity, storage, monitoring, AI, and benefit-review scorecards. + +### percentage-unallocated-costs + +Source query: [`percentage-unallocated-costs.kql`](../../../src/queries/catalog/percentage-unallocated-costs.kql). + +Measures the share of effective cost that lacks required allocation evidence for the reporting window. + +### percentage-untagged-costs + +Source query: [`percentage-untagged-costs.kql`](../../../src/queries/catalog/percentage-untagged-costs.kql). + +Measures the share of effective cost on resources that have no tags. + +### tagging-policy-compliance + +Source query: [`tagging-policy-compliance.kql`](../../../src/queries/catalog/tagging-policy-compliance.kql). + +Measures cost-weighted compliance with required tag keys. + +### allocation-accuracy-index + +Source query: [`allocation-accuracy-index.kql`](../../../src/queries/catalog/allocation-accuracy-index.kql). + +Measures directly attributed cost as a share of total effective cost. + +### anomaly-detection-rate + +Source query: [`anomaly-detection-rate.kql`](../../../src/queries/catalog/anomaly-detection-rate.kql). + +Measures the share of effective spend in anomaly-flagged daily buckets. + +### anomaly-variance-total + +Source query: [`anomaly-variance-total.kql`](../../../src/queries/catalog/anomaly-variance-total.kql). + +Quantifies signed and absolute unpredicted spend variance for detected anomaly events. + +### cost-visibility-delay + +Source query: [`cost-visibility-delay.kql`](../../../src/queries/catalog/cost-visibility-delay.kql). + +Measures cost data visibility delay from charge period end to Hub ingestion. + +### data-update-frequency + +Source query: [`data-update-frequency.kql`](../../../src/queries/catalog/data-update-frequency.kql). + +Measures FinOps hub ingestion update cadence from distinct ingestion timestamps. + +### commitment-utilization-score + +Source query: [`commitment-utilization-score.kql`](../../../src/queries/catalog/commitment-utilization-score.kql). + +Computes commitment utilization amount, potential, and score by commitment and currency. + +### commitment-discount-waste + +Source query: [`commitment-discount-waste.kql`](../../../src/queries/catalog/commitment-discount-waste.kql). + +Measures unused commitment value as a share of total commitment cost. + +### compute-spend-commitment-coverage + +Source query: [`compute-spend-commitment-coverage.kql`](../../../src/queries/catalog/compute-spend-commitment-coverage.kql). + +Measures compute spend covered by commitment discounts. + +### compute-cost-per-core + +Source query: [`compute-cost-per-core.kql`](../../../src/queries/catalog/compute-cost-per-core.kql). + +Computes hourly and effective average compute cost per consumed vCPU core hour. + +### cost-optimization-index + +Source query: [`cost-optimization-index.kql`](../../../src/queries/catalog/cost-optimization-index.kql). + +Computes the Hub-wide cost optimization index from current recommendations and cost context. + +### cost-per-gb-stored + +Source query: [`cost-per-gb-stored.kql`](../../../src/queries/catalog/cost-per-gb-stored.kql). + +Calculates storage cost per normalized GB-month. + +### macc-consumption-vs-commitment + +Source query: [`macc-consumption-vs-commitment.kql`](../../../src/queries/catalog/macc-consumption-vs-commitment.kql). + +Measures Microsoft Azure Consumption Commitment drawdown by billing profile and month. + +### storage-tier-distribution + +Source query: [`storage-tier-distribution.kql`](../../../src/queries/catalog/storage-tier-distribution.kql). + +Summarizes storage cost and GB-month distribution by access-tier bucket. + +
+ +## Usage optimization + +Use Usage Optimization tools to identify idle, orphaned, or wasteful resources before starting cleanup work. + +### idle-resource-sweep + +Source status: No `idle-resource-sweep.yaml` file appears in the source inventory for [`src/templates/sre-agent/recipes/finops-hub/config/tools`](../../../src/templates/sre-agent/recipes/finops-hub/config/tools/). + +Reviews idle or orphaned resource candidates for Usage Optimization. + +This Gate-named tool is a required analysis path, but the current template doesn't include an `idle-resource-sweep` Kusto tool YAML file. Until a dedicated tool is added, ground idle-resource reviews with `top-resource-types-by-cost`, `top-resource-groups-by-cost`, and narrow `costs-enriched-base` drill-downs. Then correlate candidates with Azure Resource Graph or Azure Advisor data through the relevant Python or built-in Azure tools before recommending cleanup. + +Example prompt: "Find likely idle or orphaned resources and group them by resource type, subscription, and estimated monthly cost." + +Sample output shape: A candidate list with fields such as `SubAccountName`, `ResourceId`, `ResourceName`, `ResourceType`, `x_ResourceGroupName`, `RegionName`, `ServiceName`, `EffectiveCost`, `LastUsageSignal`, `IdleReason`, `RecommendedAction`, and `Confidence`. When using the current template, this shape is assembled from existing cost aggregates, enriched cost rows, and external Azure inventory or recommendation signals rather than returned by a single shipped Kusto tool. + +
+ +## Price analysis + +Use price analysis tools to compare prices, savings, and non-commitment transactions that affect total cost. + +### service-price-benchmarking + +Source query: [`service-price-benchmarking.kql`](../../../src/queries/catalog/service-price-benchmarking.kql). + +Queries service price benchmarks, including list cost, contracted cost, effective cost, negotiated savings, commitment savings, and total savings. + +Use it when you need to compare prices across services or understand how negotiated and commitment discounts affect effective rates. It helps identify services with the largest rate optimization opportunity. + +Example prompt: "Benchmark service prices and show where negotiated or commitment savings are highest." + +Sample output shape: One row per service with `ServiceName`, `ListCost`, `ContractedCost`, `EffectiveCost`, negotiated savings, commitment discount savings, total savings, and savings percentages. + +### top-other-transactions + +Source query: [`top-other-transactions.kql`](../../../src/queries/catalog/top-other-transactions.kql). + +Queries the largest non-usage and non-commitment transactions, such as Marketplace or miscellaneous charges. + +Use it when costs don't reconcile to normal usage or commitment purchases. It helps isolate large one-time or nonstandard transactions that can distort monthly cost trends. + +Example prompt: "Show the largest non-usage and non-commitment transactions this month." + +Sample output shape: One row per non-usage, non-commitment transaction with `ChargePeriodStart`, `ChargeCategory`, `BilledCost`, `BillingCurrency`, `SubAccountName`, `x_InvoiceSectionName`, `PricingCategory`, `PricingQuantity`, `PricingUnit`, `ProviderName`, and `PublisherName`. + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/KustoTools) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Anomaly management](../../framework/understand/anomalies.md) +- [Cost allocation](../../framework/understand/allocation.md) +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Rate optimization](../../framework/optimize/rates.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) +- [Microsoft Cost Management](/azure/cost-management-billing/costs/) + +Related solutions: + +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md) +- [Python tools](python-tools.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/overview.md b/docs-mslearn/toolkit/sre-agent/overview.md new file mode 100644 index 000000000..902803cac --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/overview.md @@ -0,0 +1,151 @@ +--- +title: Azure SRE Agent in the FinOps toolkit +description: Learn how the FinOps toolkit deploys Azure SRE Agent with FinOps and capacity management automation on top of FinOps hubs. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: concept-article +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to understand what the FinOps toolkit's Azure SRE Agent deployment provides so that I can automate cost, capacity, and operations workflows. +--- + +# Azure SRE Agent in the FinOps toolkit + +The FinOps toolkit ships a Deploy to Azure template and a local Azure CLI + Bicep wrapper that deploy [Azure SRE Agent](/azure/sre-agent/overview) and configure it for FinOps and capacity management workflows on top of [FinOps hubs](../hubs/finops-hubs-overview.md). The deployment includes specialist subagents, FOCUS-aligned Kusto and Python tools, scheduled tasks, and grounded knowledge so the agent can investigate cost changes, monitor quota and capacity signals, prepare executive summaries, and deliver scheduled updates. The deployment focuses on three core design principles: + +- **Automate the rhythm**
_Run daily, weekly, monthly, and quarterly FinOps workflows without waiting for manual report requests._ +- **Ground every answer**
_Use FinOps hub data, FOCUS-aligned Kusto tools, and Azure platform context to keep recommendations tied to evidence._ +- **Act with experts**
_Route work to specialized FinOps, finance, capacity, database, and hubs agents instead of a single generic assistant._ + +The FinOps toolkit deployment helps teams move from dashboards and alerts to an operating model where the agent investigates cost changes, monitors quota and capacity signals, prepares executive summaries, and delivers scheduled updates through Azure SRE Agent. + + +> [!NOTE] +> Estimated cost: Varies by Azure SRE Agent preview pricing, telemetry ingestion, and your existing FinOps hub footprint. +> +> The template provisions an Azure SRE Agent with a system-assigned managed identity, Log Analytics workspace, and Application Insights resource. Costs depend on the selected region, Azure SRE Agent pricing, Log Analytics and Application Insights ingestion and retention, and the Azure Data Explorer footprint you connect to. The managed identity and RBAC assignments don't typically add direct cost. + + + +> [!IMPORTANT] +> The FinOps toolkit deploys all SRE Agent resources even when the connected Azure Data Explorer cluster uses private endpoints. If the cluster has `publicNetworkAccess` set to `Disabled`, hosted Azure SRE Agent can't run direct KQL queries against the cluster. The deployment warns and links to the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations) so the customer can decide whether to enable public query access. + + +
+ + +> [!div class="nextstepaction"] +> [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) + + +
+ +## What you get + +The FinOps toolkit's Azure SRE Agent template deploys and configures these resources and Azure SRE Agent objects: + +| Component | Count | Description | +|-----------|-------|-------------| +| Azure SRE Agent | 1 | `Microsoft.App/agents` resource in autonomous mode | +| Model provider | 1 | Azure OpenAI provider-level routing (`MicrosoftFoundry` ARM value, `Automatic` model routing) | +| Managed identities | 1-2 | System-assigned managed identity for the agent; portal deployments also create a user-assigned identity for the deployment script | +| Log Analytics | 1 | Workspace for agent telemetry | +| Application Insights | 1 | Linked to Log Analytics for monitoring | +| Target resource group RBAC | 3-4 per target group | Reader, Monitoring Reader, Log Analytics Reader, and Contributor when `accessLevel` is `High` | +| Azure Data Explorer role | Optional | `AllDatabasesViewer` when Azure Data Explorer parameters are provided | +| Custom agents | 5 | `azure-capacity-manager`, `chief-financial-officer`, `finops-practitioner`, `ftk-database-query`, and `ftk-hubs-agent` | +| Skills | 3 | `azure-capacity-management`, `azure-cost-management`, and `finops-toolkit` | +| Tools | 50 | Kusto, capacity, and Hub infrastructure tools | +| Tool overrides | 9 | Enables SRE Agent Log Query and Visualization tools | +| Scheduled tasks | 19 | Daily, weekly, monthly, semiannual, and quarterly recurring workflows | +| Connectors | 1 | Optional Kusto connector to your FinOps hub Azure Data Explorer cluster | +| Knowledge docs | 6 | Five recipe knowledge docs plus the FinOps Toolkit output style | +| Notification connectors | 0 by default | Outlook and Teams can be added after deployment in the Azure SRE Agent portal | + +
+ +## Architecture overview + +The FinOps toolkit deployment is copied from the Microsoft SRE Agent starter-lab pattern and updated to use the toolkit's packaged Deploy to Azure flow or Azure CLI + Bicep directly. It doesn't use `azd`. The deployment runs in this order: + +1. The Azure portal or `bin/deploy.sh` starts a subscription-scoped ARM deployment. +2. The deployment creates the agent resource group when needed. +3. Bicep creates monitoring resources and Azure SRE Agent. +4. Bicep assigns target resource group RBAC and can optionally assign `AllDatabasesViewer` on your FinOps hub Azure Data Explorer cluster. +5. A deployment script in the portal path, or `bin/apply-extras.sh` in the local CLI path, creates the Kusto connector and applies skills, custom agents, tools, knowledge, and scheduled tasks through the supported SRE Agent ARM and data-plane surfaces. +6. You optionally add Outlook and Teams connectors in [sre.azure.com](https://sre.azure.com) when you want scheduled reports delivered outside the agent chat. + +The result is a single Azure SRE Agent with a FinOps Framework-aligned operating model layered on top. The `finops-practitioner` agent owns the FinOps operating rhythm and scheduled analysis, has no direct tools, and delegates evidence collection to specialists. The `ftk-database-query` agent owns all Kusto and FOCUS evidence collection. The `azure-capacity-manager` agent maps Azure capacity signals into Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Governance, Policy & Risk, and Automation, Tools & Services. The `chief-financial-officer` agent provides finance and leadership consultation, and the `ftk-hubs-agent` monitors FinOps hub health and data freshness. + +The template defaults the parent SRE Agent to Azure OpenAI provider-level routing (`MicrosoftFoundry` in ARM) with automatic model selection inside that provider. Individual custom agents don't pin model names; they inherit the parent agent provider and use SRE Agent's automatic model routing. + +
+ +## When to use the agent + +FinOps teams often have the data they need but not the time to run every investigation on schedule. The agent gives the team an automated operating rhythm that can review cost changes, check commitment coverage, monitor capacity headroom, and summarize action items before the next stakeholder meeting. + +Use the agent when you want to: + +- Review FinOps hub data through conversational and scheduled agent workflows. +- Monitor cost anomalies, budget variance, and forecast drift. +- Track quota usage, capacity reservation utilization, region access, and zone readiness. +- Prepare daily, weekly, monthly, and quarterly cost and capacity summaries. +- Route questions to specialized agents with FinOps, finance, capacity, database, and hubs context. + +
+ +## Get started + +Deploy the agent when you have a FinOps hub with an Azure Data Explorer cluster and an Azure subscription where you can deploy Azure SRE Agent resources. After deployment, open the agent in [sre.azure.com](https://sre.azure.com), verify the subagents, skills, tools, and scheduled tasks, and add notification connectors if you want reports sent to Teams or Outlook. + + +> [!div class="nextstepaction"] +> [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) + + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Anomaly management](../../framework/understand/anomalies.md) +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Architecting for cloud](../../framework/optimize/architecting.md) +- [Rate optimization](../../framework/optimize/rates.md) +- [Usage optimization](../../framework/optimize/workloads.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) +- [Azure Monitor](/azure/azure-monitor/) + +Related solutions: + +- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) +- [FinOps hubs](../hubs/finops-hubs-overview.md) +- [Azure SRE Agent template reference (FinOps toolkit)](template.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/python-tools.md b/docs-mslearn/toolkit/sre-agent/python-tools.md new file mode 100644 index 000000000..f1b4e654e --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/python-tools.md @@ -0,0 +1,514 @@ +--- +title: Python tools +description: Review the Python tools the FinOps toolkit ships for Azure SRE Agent for Azure quota, capacity, budgets, anomaly alerts, Resource Graph, FinOps hub health, and Advisor suppressions. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: reference +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to understand which Python tools the FinOps toolkit ships for Azure SRE Agent so that I can use the right Azure API-backed tool for capacity, cost governance, and operations work. +--- + +# Python tools + +The FinOps toolkit deployment configures Azure SRE Agent with 13 Python tools that call Azure APIs through the agent's managed identity. These tools complement the Kusto tools by checking Azure platform state, deploying Cost Management controls, and automating governance tasks that aren't stored in FinOps hub data. + +Use Python tools when the agent needs live Azure Resource Manager, Resource Graph, Cost Management, Azure Data Explorer, or Advisor data instead of historical cost and usage data from the FinOps hub. + +> [!NOTE] +> The agent managed identity must have enough permission at the target scope to read or update the resources each tool touches. Read tools typically need Reader permissions. Deployment and suppression tools need permissions to create or update the target Cost Management or Advisor resources. + +## Source inventory + +The following table maps the 13 Python tools documented on this page to their source YAML files. Use this inventory to audit this reference page against the template source. It doesn't assert that no other YAML files exist under `src/templates/sre-agent/recipes/finops-hub/config/tools/`. + +| Tool | Source file | +|------|-------------| +| `vm-quota-usage` | `src/templates/sre-agent/recipes/finops-hub/config/tools/vm-quota-usage.yaml` | +| `capacity-reservation-groups` | `src/templates/sre-agent/recipes/finops-hub/config/tools/capacity-reservation-groups.yaml` | +| `sku-availability` | `src/templates/sre-agent/recipes/finops-hub/config/tools/sku-availability.yaml` | +| `non-compute-quotas` | `src/templates/sre-agent/recipes/finops-hub/config/tools/non-compute-quotas.yaml` | +| `deploy-budget` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-budget.yaml` | +| `deploy-bulk-budgets` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-budgets.yaml` | +| `deploy-anomaly-alert` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-anomaly-alert.yaml` | +| `deploy-bulk-anomaly-alerts` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-anomaly-alerts.yaml` | +| `resource-graph-query` | `src/templates/sre-agent/recipes/finops-hub/config/tools/resource-graph-query.yaml` | +| `benefit-recommendations` | `src/templates/sre-agent/recipes/finops-hub/config/tools/benefit-recommendations.yaml` | +| `data-freshness-check` | `src/templates/sre-agent/recipes/finops-hub/config/tools/data-freshness-check.yaml` | +| `db-service-quotas` | `src/templates/sre-agent/recipes/finops-hub/config/tools/db-service-quotas.yaml` | +| `suppress-advisor-recommendations` | `src/templates/sre-agent/recipes/finops-hub/config/tools/suppress-advisor-recommendations.yaml` | + +
+ +## Capacity and quota + +Use capacity and quota tools to check VM family quota, capacity reservation group utilization, SKU restrictions, and non-compute service limits before deployment or during recurring capacity reviews. + +### `vm-quota-usage` + +- **Azure API:** Azure Resource Manager Compute usages API: `Microsoft.Compute/locations/usages`. +- **When to use it:** Use this tool to query VM family quota usage across one or more regions in a subscription. It calculates utilization percentages and flags VM families above warning or critical thresholds. +- **Example prompt:** "Check VM quota usage for subscription `` in eastus and westus2, and call out any families above 80% utilization." +- **Sample output shape:** + + ```json + { + "subscription_id": "", + "locations": ["eastus", "westus2"], + "quotas": [ + { + "location": "eastus", + "name": "Standard DSv5 Family vCPUs", + "current": 80, + "limit": 100, + "utilization_pct": 80, + "at_risk_80": false, + "at_risk_95": false + } + ], + "warning_count": 0, + "critical_count": 0, + "suppressed_error_count": 0, + "suppressed_errors": [], + "errors": [] + } + ``` + +### `capacity-reservation-groups` + +- **Azure API:** Azure Resource Manager Compute capacity reservation group API: `Microsoft.Compute/capacityReservationGroups`, including capacity reservation instance view and associated virtual machines. +- **When to use it:** Use this tool to list capacity reservation groups in a subscription and compare reserved capacity with allocated virtual machines. It helps find unused capacity, overallocated groups, and zone-specific capacity reservation waste. +- **Example prompt:** "List capacity reservation groups in subscription `` and identify groups with unused reserved capacity." +- **Sample output shape:** + + ```json + { + "subscription_id": "", + "capacity_reservation_groups": [ + { + "name": "crg-prod-eastus", + "id": "/subscriptions//resourceGroups//providers/Microsoft.Compute/capacityReservationGroups/crg-prod-eastus", + "resource_group": "", + "location": "eastus", + "zones": ["1"], + "zone": "1", + "reserved_count": 10, + "allocated_count": 8, + "utilization_pct": 80, + "waste": true, + "waste_count": 2, + "overallocated": false, + "capacity_reservations": ["crg-prod-eastus/Standard_D8s_v5"], + "virtual_machines_allocated": ["/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/vm01"], + "virtual_machines_associated": [] + } + ], + "errors": [] + } + ``` + +### `sku-availability` + +- **Azure API:** Azure Resource Manager SKU APIs for Azure Compute and Azure Data Explorer: `Microsoft.Compute/skus` and `Microsoft.Kusto/locations/skus`. +- **When to use it:** Use this tool to verify whether a VM or Azure Data Explorer SKU is available in a region and whether zone or subscription restrictions could block deployment. +- **Example prompt:** "Check whether Standard_D8s_v5 is available in eastus2, including zone support and any restriction reasons." +- **Sample output shape:** + + ```json + { + "subscription_id": "", + "location": "eastus2", + "sku_filter": "Standard_D8s_v5", + "resource_provider": "compute", + "service": "Azure Compute", + "source_endpoint": "https://management.azure.com/subscriptions//providers/Microsoft.Compute/skus?api-version=2021-07-01&$filter=location eq 'eastus2'", + "skus": [ + { + "name": "Standard_D8s_v5", + "family": "standardDSv5Family", + "size": "D8s_v5", + "zones_available": ["1", "2", "3"], + "restrictions": [] + } + ], + "restriction_summary": { + "total_skus": 1, + "restricted_skus": 0, + "restriction_count": 0, + "reason_codes": {} + } + } + ``` + +### `non-compute-quotas` + +- **Azure API:** Azure Resource Manager provider usage APIs for supported services, with Azure Resource Graph fallbacks for services that don't expose a direct usage API. +- **When to use it:** Use this tool to review storage, networking, and PaaS quota usage that can block growth even when compute quota is healthy. It marks whether each limit came from an API-reported quota or an estimated fallback. +- **Example prompt:** "Audit non-compute quota usage for subscription `` in eastus and summarize storage or network limits above 80% utilization." +- **Sample output shape:** + + ```json + { + "subscription_id": "", + "services": [ + { + "service_name": "Storage", + "resource_type": "Microsoft.Storage/locations/usages", + "quota_name": "StorageAccounts", + "current_count": 100, + "limit": 250, + "utilization_pct": 40, + "at_risk": false, + "count_source": "arm_provider_usages_api", + "limit_source": "api_reported_limit", + "limit_type": "api_reported", + "source_endpoint": "https://management.azure.com/subscriptions//providers/Microsoft.Storage/locations/eastus/usages?api-version=2023-05-01", + "location": "eastus", + "scope": "regional" + } + ], + "quotas": [ + { + "subscription_id": "", + "service_name": "Storage", + "service": "Storage", + "resource_type": "Microsoft.Storage/locations/usages", + "quota_name": "StorageAccounts", + "name": "StorageAccounts", + "location": "eastus", + "scope": "regional", + "current_count": 100, + "current": 100, + "limit": 250, + "utilization_pct": 40, + "at_risk": false, + "at_risk_80": false, + "at_risk_95": false, + "count_source": "arm_provider_usages_api", + "limit_source": "api_reported_limit", + "limit_type": "api_reported", + "source_endpoint": "https://management.azure.com/subscriptions//providers/Microsoft.Storage/locations/eastus/usages?api-version=2023-05-01" + } + ], + "at_risk_count": 0, + "api_reported_limit_count": 1, + "estimated_limit_count": 0, + "suppressed_count": 0, + "errors": [] + } + ``` + +
+ +## Budget and alert deployment + +Use budget and alert deployment tools to create Cost Management controls for a single subscription or apply the same control across enabled subscriptions in a management group. + +### `deploy-budget` + +- **Azure API:** Azure Resource Manager Consumption budgets API: `Microsoft.Consumption/budgets`. +- **When to use it:** Use this tool to create or update a subscription-level Cost Management budget with notification thresholds and contact emails. +- **Example prompt:** "Create a monthly budget named SubscriptionBudget for subscription `` with an amount of 50000 and notify finops@example.com." +- **Sample output shape:** + + ```json + { + "subscription_id": "", + "budget_name": "SubscriptionBudget", + "amount": 50000, + "time_grain": "Monthly", + "contact_emails": ["finops@example.com"], + "status_code": 201, + "budget": { + "id": "/subscriptions//providers/Microsoft.Consumption/budgets/SubscriptionBudget", + "name": "SubscriptionBudget", + "type": "Microsoft.Consumption/budgets" + }, + "status": "created" + } + ``` + +### `deploy-bulk-budgets` + +- **Azure API:** Azure Resource Graph to discover enabled subscriptions, then Azure Resource Manager Consumption budgets API: `Microsoft.Consumption/budgets`. +- **When to use it:** Use this tool to deploy the same Cost Management budget across all enabled subscriptions under a management group. +- **Example prompt:** "Deploy a monthly 50000 budget named SubscriptionBudget to every enabled subscription in management group `` and notify finops@example.com." +- **Sample output shape:** + + ```json + { + "management_group": "", + "budget_name": "SubscriptionBudget", + "amount": 50000, + "contact_emails": ["finops@example.com"], + "subscription_count": 2, + "created_or_updated": 2, + "failed": 0, + "deployments": [ + { + "subscription_id": "", + "subscription_name": "Production", + "budget_name": "SubscriptionBudget", + "status": "created", + "status_code": 201, + "error": null + } + ] + } + ``` + +### `deploy-anomaly-alert` + +- **Azure API:** Azure Resource Manager Cost Management scheduled actions API: `Microsoft.CostManagement/scheduledActions`. +- **When to use it:** Use this tool to create or update a daily Cost Management anomaly alert scheduled action for one subscription. +- **Example prompt:** "Create a cost anomaly alert for subscription `` that sends daily anomaly notifications to finops@example.com." +- **Sample output shape:** + + ```json + { + "subscription_id": "", + "scheduled_action_name": "cost-anomaly-alert", + "email_recipients": ["finops@example.com"], + "status_code": 201, + "anomaly_alert": { + "id": "/subscriptions//providers/Microsoft.CostManagement/scheduledActions/cost-anomaly-alert", + "name": "cost-anomaly-alert", + "kind": "InsightAlert" + }, + "status": "created" + } + ``` + +### `deploy-bulk-anomaly-alerts` + +- **Azure API:** Azure Resource Graph to discover enabled subscriptions, then Azure Resource Manager Cost Management scheduled actions API: `Microsoft.CostManagement/scheduledActions`. +- **When to use it:** Use this tool to deploy the same cost anomaly alert configuration across enabled subscriptions under a management group. +- **Example prompt:** "Deploy cost anomaly alerts to all enabled subscriptions in management group `` and send notifications to finops@example.com." +- **Sample output shape:** + + ```json + { + "management_group": "", + "email_recipients": ["finops@example.com"], + "subscription_count": 2, + "created_or_updated": 2, + "failed": 0, + "deployments": [ + { + "subscription_id": "", + "subscription_name": "Production", + "scheduled_action_name": "cost-anomaly-alert", + "status": "updated", + "status_code": 200, + "error": null + } + ] + } + ``` + +
+ +## Resource analysis + +Use resource analysis tools when the agent needs live Azure inventory, configuration, or commitment discount recommendation data outside the FinOps hub. + +### `resource-graph-query` + +- **Azure API:** Azure Resource Graph resources API: `Microsoft.ResourceGraph/resources`. +- **When to use it:** Use this tool to run Azure Resource Graph KQL across one or more subscriptions for inventory, configuration drift, governance checks, and subscription-scale troubleshooting. +- **Example prompt:** "Run a Resource Graph query across my subscriptions to list unattached managed disks by subscription, resource group, size, and age." +- **Sample output shape:** + + ```json + { + "rows": [ + { + "subscriptionId": "", + "resourceGroup": "", + "name": "disk01", + "type": "microsoft.compute/disks" + } + ], + "count": 1, + "errors": [], + "total_records": 1, + "result_truncated": false, + "subscriptions": [""] + } + ``` + +### `benefit-recommendations` + +- **Azure API:** Azure Resource Manager Cost Management benefit recommendations API: `Microsoft.CostManagement/benefitRecommendations`. +- **When to use it:** Use this tool to retrieve reservation and savings plan recommendations at a billing scope, including recommendation type, term, savings, cost, and break-even details. +- **Example prompt:** "Get three-year benefit recommendations for billing account scope `` using a Last30Days lookback and summarize the largest savings opportunities." +- **Sample output shape:** + + ```json + { + "billing_scope": "", + "lookback_period": "Last30Days", + "term": "P3Y", + "recommendations": [ + { + "type": "SP", + "savings": 12000, + "cost": 30000, + "total_cost": 30000, + "cost_without_benefit": 42000, + "term": "P3Y", + "break_even": "P8M", + "id": "/providers/Microsoft.Billing/billingAccounts//providers/Microsoft.CostManagement/benefitRecommendations/", + "name": "" + } + ], + "count": 1 + } + ``` + +
+ +## Hub management + +Use hub management tools to verify whether FinOps hub data is current enough for reliable scheduled reports and agent answers. Use `data-freshness-check` as the source of truth for hub freshness before relying on stale memory, raw KQL rollups, or ingestion timestamp checks. + +### `data-freshness-check` + +- **Azure API:** Azure Data Explorer REST query API (`/v1/rest/query` or `/v2/rest/query`) on the configured FinOps hub cluster. +- **When to use it:** Use this tool to check data freshness for FinOps hub functions such as `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()`. It reports latest data dates, stale functions, and functions without data. Treat `Costs()` as the authoritative freshness signal. If `Costs()` is 3 days old or newer, don't report the hub as stale even if older memory, raw KQL, or ingestion timestamp checks disagree. +- **Example prompt:** "Check data freshness for the FinOps hub at `` in the hub database and tell me which functions are stale." +- **Sample output shape:** + + ```json + { + "cluster_uri": "https://.kusto.windows.net", + "database": "hub", + "functions": [ + { + "function_name": "Costs()", + "source_dataset": "Costs", + "expected_export_dataset": "focuscost", + "row_count": 100000, + "latest_data_date": "2026-05-01T00:00:00Z", + "latest_ingestion_time": null, + "staleness_days": 1, + "is_stale": false, + "has_data": true, + "schema_status": "ok", + "schema_column_count": 120, + "required_for_hub_health": true, + "diagnostic_code": "OK", + "diagnostic_message": null + } + ], + "total_functions": 4, + "stale_count": 0, + "empty_count": 0, + "error_count": 0, + "hub_data_stale": false, + "attention_required": false, + "status": "healthy", + "diagnostics": [], + "freshest_function": {}, + "stalest_function": {}, + "authoritative_freshness_signal": { + "function_name": "Costs()", + "latest_data_date": "2026-05-01T00:00:00Z", + "staleness_days": 1, + "is_stale": false, + "has_data": true, + "row_count": 100000 + }, + "source_of_truth": { + "tool": "data-freshness-check", + "method": "direct_adx_rest_query", + "endpoint": "/v2/rest/query", + "authoritative_function": "Costs()", + "freshness_threshold_days": 3, + "supersedes": ["stale memory conclusions", "raw KQL freshness rollups", "Kusto ingestion timestamp checks"] + } + } + ``` + +
+ +## Advisor + +Use Advisor tools when the agent needs to apply governance decisions to Azure Advisor recommendations. + +### `suppress-advisor-recommendations` + +- **Azure API:** Azure Resource Graph to find Advisor recommendations, then Azure Resource Manager Advisor suppressions API: `Microsoft.Advisor/recommendations/suppressions`. +- **When to use it:** Use this tool to suppress selected Azure Advisor recommendation types across subscriptions under a management group for a limited time to live. +- **Example prompt:** "Suppress the default Advisor recommendation types for management group `` for 30 days and report how many recommendations were suppressed." +- **Sample output shape:** + + ```json + { + "management_group_id": "", + "days": 30, + "ttl": "P30D", + "recommendation_type_ids": ["89515250-1243-43d1-b4e7-f9437cedffd8"], + "found_count": 10, + "suppressed_count": 10, + "failed_count": 0, + "suppressions": [ + { + "subscription_id": "", + "recommendation_id": "/subscriptions//providers/Microsoft.Advisor/recommendations/", + "recommendation_type_id": "89515250-1243-43d1-b4e7-f9437cedffd8", + "suppression_id": "", + "ttl": "P30D", + "status": "suppressed", + "status_code": 201, + "error": null + } + ] + } + ``` + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/PythonTools) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Anomaly management](../../framework/understand/anomalies.md) +- [Budgeting](../../framework/quantify/budgeting.md) +- [Rate optimization](../../framework/optimize/rates.md) +- [Usage optimization](../../framework/optimize/workloads.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Resource Graph](/azure/governance/resource-graph/) +- [Microsoft Cost Management](/azure/cost-management-billing/costs/) +- [Azure Advisor](/azure/advisor/) + +Related solutions: + +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md) +- [Kusto tools](kusto-tools.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/scheduled-tasks.md b/docs-mslearn/toolkit/sre-agent/scheduled-tasks.md new file mode 100644 index 000000000..9c9c08a95 --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/scheduled-tasks.md @@ -0,0 +1,277 @@ +--- +title: Scheduled tasks (Azure SRE Agent in the FinOps toolkit) +description: Learn how the FinOps toolkit's scheduled tasks automate daily, weekly, monthly, and quarterly FinOps operating rhythms on Azure SRE Agent. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: concept-article +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to understand the scheduled tasks the FinOps toolkit deploys to Azure SRE Agent so that I can plan recurring cost, capacity, and finance reviews. +--- + +# Scheduled tasks (Azure SRE Agent in the FinOps toolkit) + +The FinOps toolkit ships scheduled tasks that run recurring FinOps operating-rhythm workflows on Azure SRE Agent. They turn common reviews into autonomous checks that gather data, route work to the right specialist agent, generate charts where the data supports them, and post completed reports to Microsoft Teams when a Teams notification connector is configured. + +The template deploys 19 scheduled tasks from `src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/`. These tasks cover daily health checks, weekly optimization and capacity reviews, monthly planning and finance reports, semiannual year-over-year analysis, and quarterly strategy. + +
+ +## Daily tasks + +Daily tasks run every morning to validate FinOps hub health and monitor Azure capacity signals that feed Planning & Estimating, Architecting & Workload Placement, Usage Optimization, Governance, Policy & Risk, and Automation, Tools & Services. They keep cost and capacity surprises visible before each business day. + +| Task | Agent | Schedule | Description | +|------|-------|----------|-------------| +| `HubsHealthCheck` | `ftk-hubs-agent` | Daily at 6:00 AM
`0 6 * * *` | FinOps hub version and data freshness validation | +| `CapacityDailyMonitor` | `azure-capacity-manager` | Daily at 6:30 AM
`30 6 * * *` | Daily capacity health check — quota usage, CRG utilization, zone capacity | + +
+ +## Weekly tasks + +Weekly tasks summarize cost optimization, Azure capacity evidence, and benefit recommendation activity for the previous week. They give the team a recurring rhythm for deeper analysis without manual report requests. + +| Task | Agent | Schedule | Description | +|------|-------|----------|-------------| +| `ComputeUtilizationTrend` | `azure-capacity-manager` | Weekly on Monday at 7:00 AM
`0 7 * * 1` | Weekly VM quota utilization trend review across subscriptions and regions | +| `CostOptimization` | `finops-practitioner` | Weekly on Monday at 8:00 AM
`0 8 * * 1` | Comprehensive cost optimization report with orphaned resources, rightsizing, and commitment analysis | +| `CapacityWeeklySupplyReview` | `azure-capacity-manager` | Weekly on Monday at 8:00 AM
`0 8 * * 1` | Weekly capacity evidence review — quota headroom, CRG cost optimization, SKU availability, benefit recommendations | +| `NonComputeQuotaAudit` | `azure-capacity-manager` | Weekly on Tuesday at 7:00 AM
`0 7 * * 2` | Weekly audit of storage, network, and non-compute quota usage at risk | +| `DbQuotaAudit` | `azure-capacity-manager` | Weekly on Wednesday at 7:00 AM
`0 7 * * 3` | Weekly audit of database quota and region or zone access risks | +| `SkuAvailabilityAudit` | `azure-capacity-manager` | Weekly on Wednesday at 7:30 AM
`30 7 * * 3` | Weekly audit of regional SKU availability and restrictions that could block deployments | +| `MonitoringScopeValidation` | `ftk-hubs-agent` | Weekly on Thursday at 9:00 AM
`0 9 * * 4` | Weekly validation that FinOps hub monitoring covers all active subscriptions | +| `BenefitRecommendationReview` | `finops-practitioner` | Weekly on Friday at 8:00 AM
`0 8 * * 5` | Weekly review of reservation and savings plan recommendations, with CFO consultation for decision framing | + +
+ +## Monthly tasks + +Monthly tasks run after billing data finalizes to produce year-to-date analysis, capacity planning forecasts, AI workload cost reviews, and audit reports for budget and alert coverage. Use them to anchor monthly business reviews. + +| Task | Agent | Schedule | Description | +|------|-------|----------|-------------| +| `StoragePaasGrowthForecast` | `azure-capacity-manager` | Monthly on the 1st at 8:00 AM
`0 8 1 * *` | Monthly storage and PaaS quota growth forecast across active subscriptions | +| `AdvisorSuppressionReview` | `finops-practitioner` | Monthly on the 1st at 9:00 AM
`0 9 1 * *` | Monthly review of active Advisor recommendation suppressions for stale or expired decisions | +| `CapacityMonthlyPlanning` | `azure-capacity-manager` | Monthly on the 1st at 9:00 AM
`0 9 1 * *` | Monthly capacity planning cycle — demand forecast, capacity request pipeline, governance review | +| `Monthly` | `finops-practitioner` | Monthly on the 5th at 5:15 PM
`15 17 5 * *` | Autonomous month-over-month cost analysis with FinOps hub tools | +| `AIWorkloadCostAnalysis` | `finops-practitioner` | Monthly on the 1st at 10:00 AM
`0 10 1 * *` | Monthly AI workload cost analysis — token economics, model efficiency, and cost allocation for Azure OpenAI | +| `Semiannual` | `finops-practitioner` | January 5 and July 5 at 9:00 AM
`0 9 5 1,7 *` | Semiannual year-over-year finance analysis with forecast and CFO consultation | +| `BudgetCoverageAudit` | `finops-practitioner` | Monthly on the 15th at 8:00 AM
`0 8 15 * *` | Monthly audit of subscription budget coverage and missing budget controls | +| `AlertCoverageAudit` | `finops-practitioner` | Monthly on the 16th at 8:00 AM
`0 8 16 * *` | Monthly audit of cost anomaly alert coverage across active subscriptions | + +
+ +## Quarterly tasks + +Quarterly tasks run at the start of each calendar quarter to summarize capacity-related FinOps capability maturity, commitment alignment, and architecture evolution. Use them to feed leadership reviews and the next quarter's planning cycle. + +| Task | Agent | Schedule | Description | +|------|-------|----------|-------------| +| `CapacityQuarterlyStrategy` | `azure-capacity-manager` | Quarterly on January 1, April 1, July 1, and October 1 at 9:00 AM
`0 9 1 1,4,7,10 *` | Quarterly capacity strategy review — FinOps capability maturity, commitment alignment, architecture evolution | + +
+ +## Task details + +Each scheduled task is defined in YAML under `src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/`. You can customize the schedule by changing the `cron_expression` in the task definition before deployment. You can also tune the task prompt to change thresholds, scope, report sections, and recommended actions for your operating model. + +### HubsHealthCheck + +- **Data sources queried:** GitHub release metadata for the latest FinOps hub version, Azure resource discovery for deployed hub resources, and the `data-freshness-check` tool for the latest `Costs()` update date. The task also validates hub connectivity before trusting downstream scheduled reports. +- **Output format and content:** A daily health summary that reports deployed version status, latest available version, data refresh status, connectivity findings, and any blocking hub issues. The report is formatted for Teams when notifications are configured. +- **Recommended actions generated:** Upgrade FinOps hubs when the deployed version is behind, investigate stale exports or ingestion failures, restore connectivity, and delay cost analysis that depends on stale hub data. +- **Customization options:** Change the daily cron from `0 6 * * *`, adjust how many days of data freshness lag is acceptable, add organization-specific hub resource filters, and add required escalation steps for stale data or version drift. + +### CapacityDailyMonitor + +- **Data sources queried:** VM quota usage through `vm-quota-usage`, capacity reservation group data through `capacity-reservation-groups`, non-compute quota context through `non-compute-quotas`, Azure CLI discovery for AKS and regional resources, and hub freshness checks where cost context is needed. +- **Output format and content:** A daily capacity health report with quota pressure, capacity reservation utilization, AKS node pool readiness, alert status, and urgent capacity blockers mapped to FinOps capability impact. +- **Recommended actions generated:** File quota increases, redistribute workload demand, adjust capacity reservation groups, investigate underutilized reservations, validate AKS node pool capacity, and escalate imminent deployment blockers. +- **Customization options:** Change the daily cron from `30 6 * * *`, tune warning and critical quota-utilization thresholds, define target capacity reservation utilization bands, scope monitored subscriptions or regions, and add workload-specific AKS node pool checks. + +### Monthly + +- **Data sources queried:** `finops-practitioner` delegates FinOps hub Kusto evidence to `ftk-database-query`, including `data-freshness-check`, `monthly-cost-trend`, `monthly-cost-change-percentage`, `top-services-by-cost`, `top-resource-groups-by-cost`, `cost-by-region-trend`, `cost-anomaly-detection`, `savings-summary-report`, `commitment-discount-utilization`, `cost-forecasting-model`, `reservation-recommendation-breakdown`, `top-resource-types-by-cost`, `service-price-benchmarking`, and `top-other-transactions`. Capacity-risk evidence is delegated to `azure-capacity-manager`. +- **Output format and content:** A daily month-over-month cost analysis with executive summary, service and resource group drivers, anomalies, forecasts, savings and commitment signals, regional distribution, tag coverage, marketplace or other purchases, charts where data supports them, and action items. +- **Recommended actions generated:** Investigate anomalies, correct cost allocation gaps, prioritize cost drivers, act on savings opportunities, review commitment utilization, address forecast risk, and validate tags or financial hierarchy gaps. +- **Customization options:** Change the monthly cron from `15 17 5 * *`, adjust anomaly and variance thresholds, change the comparison window, scope the analysis to selected subscriptions or billing entities, and add or remove Kusto sections from the report. + +### ComputeUtilizationTrend + +- **Data sources queried:** VM quota utilization through `vm-quota-usage`, Azure Resource Graph subscription and VM inventory, and prior-period utilization signals from the scheduled analysis prompt. +- **Output format and content:** A weekly trend report that lists quota families, subscriptions, regions, current utilization, week-over-week movement, and quota lines approaching defined risk thresholds. +- **Recommended actions generated:** Request quota increases for growing usage, move demand to lower-pressure regions or quota families, validate planned deployments against headroom, and close stale quota allocations that are no longer needed. +- **Customization options:** Change the weekly cron from `0 7 * * 1`, tune warning and critical utilization thresholds, choose the trend lookback window, filter to production subscriptions or priority regions, and add business-unit routing for quota owners. + +### CostOptimization + +- **Data sources queried:** Azure Resource Graph for orphaned and idle resources, Advisor cost recommendations, commitment and benefit recommendation tools, Azure CLI resource validation, optional VM utilization data, and FinOps hub data when available. +- **Output format and content:** A weekly cost optimization report with executive summary, quick wins, orphaned resources, rightsizing candidates, commitment discount status, effort and risk categories, estimated savings, and charts for major opportunities. +- **Recommended actions generated:** Delete or remediate orphaned resources, resize underutilized workloads, buy or adjust reservations and savings plans, validate Advisor recommendations, and assign owners for high-value optimization actions. +- **Customization options:** Change the weekly cron from `0 8 * * 1`, tune savings and utilization thresholds, exclude protected resource groups or tags, change effort and risk categories, and modify recommendation ranking or minimum savings filters. + +### CapacityWeeklySupplyReview + +- **Data sources queried:** VM quota usage, `capacity-reservation-groups`, `non-compute-quotas`, `sku-availability`, benefit and reservation recommendations, Azure CLI regional discovery, and commitment-related cost signals. +- **Output format and content:** A weekly capacity evidence review covering quota headroom, capacity reservation waste, SKU and zone availability, region access, non-compute constraints, and capacity-to-rate optimization opportunities mapped to FinOps capabilities. +- **Recommended actions generated:** Increase quota, rebalance capacity reservations, release or resize underused CRGs, validate alternate SKUs or regions, align capacity reservations with reservation or savings opportunities, and escalate SKU restrictions. +- **Customization options:** Change the weekly cron from `0 8 * * 1`, tune quota and CRG utilization thresholds, set approved regions and SKU allowlists, scope the review to critical workloads, and adjust how benefit recommendations are ranked. + +### NonComputeQuotaAudit + +- **Data sources queried:** `non-compute-quotas` for storage, network, and other service quota usage; subscription scope from Azure discovery; and service-specific quota values returned by Azure APIs when available. +- **Output format and content:** A weekly audit that separates reported and estimated limits, lists services and regions near limits, highlights quotas with missing limit telemetry, and summarizes risk by subscription. +- **Recommended actions generated:** Request quota increases, reduce or redistribute usage, add missing quota monitoring, validate estimated limits with service owners, and escalate high-risk storage or network quotas before deployments fail. +- **Customization options:** Change the weekly cron from `0 7 * * 2`, tune at-risk utilization thresholds, choose monitored services, scope subscriptions or regions, and define how to handle quotas that report current usage but not limits. + +### DbQuotaAudit + +- **Data sources queried:** `db-service-quotas` for SQL DB, SQL Managed Instance, Cosmos DB, PostgreSQL Flexible Server, and MySQL Flexible Server quota, region, and availability-zone access signals. +- **Output format and content:** A weekly database quota report that separates service limits, regional access, zone access, estimated or missing evidence, and workload impact by subscription and region. +- **Recommended actions generated:** Request database quota increases, validate region or zone access, adjust database placement plans, add owner routing for capacity risks, and escalate blockers before deployment or scale events. +- **Customization options:** Change the weekly cron from `0 7 * * 3`, choose database services or regions to inspect, tune risk thresholds, and define owner metadata requirements. + +### SkuAvailabilityAudit + +- **Data sources queried:** `sku-availability` for regional SKU availability and restrictions, Azure subscription and region discovery, and workload-provided SKU filters when present. +- **Output format and content:** A weekly availability report that lists requested SKUs by region, availability status, restrictions, deployment blockers, and recommended fallback regions or SKU families. +- **Recommended actions generated:** Change deployment region or SKU, request regional access where possible, update landing-zone allowed SKU lists, revise deployment plans, and escalate blockers that affect production capacity. +- **Customization options:** Change the weekly cron from `30 7 * * 3`, tune the SKU and region scope, add required workload SKUs, define preferred fallback regions, and change the severity threshold for restricted or unavailable SKUs. + +### MonitoringScopeValidation + +- **Data sources queried:** Azure Resource Graph for active subscription inventory, FinOps hub `data-freshness-check`, hub database and cluster configuration, and subscription coverage comparisons between Azure and hub data. +- **Output format and content:** A weekly monitoring coverage report showing active subscriptions, subscriptions with fresh hub data, subscriptions missing from monitoring, stale coverage, and evidence for scope gaps. +- **Recommended actions generated:** Add missing subscriptions to exports or hub monitoring, fix stale export configuration, validate hub permissions, update subscription onboarding processes, and pause unsupported financial analysis for missing scopes. +- **Customization options:** Change the weekly cron from `0 9 * * 4`, tune data freshness thresholds, filter excluded or retired subscriptions, change the required coverage percentage, and add owner routing for missing subscription remediation. + +### BenefitRecommendationReview + +- **Data sources queried:** `benefit-recommendations` for reservation and savings plan opportunities, billing-scope context, plus Kusto-backed commitment utilization and savings evidence delegated to `ftk-database-query`. +- **Output format and content:** A weekly executive report with total savings opportunity, recommendation categories, term and scope assumptions, purchase candidates, risk notes, and decision-ready next steps. +- **Recommended actions generated:** Approve high-confidence purchases, reject or defer recommendations that conflict with demand forecasts, adjust commitment coverage, request validation from workload owners, and document finance approval decisions. +- **Customization options:** Change the weekly cron from `0 8 * * 5`, tune minimum savings and confidence thresholds, set preferred commitment terms, scope recommendations by billing profile or subscription, and add finance approval rules. + +### StoragePaasGrowthForecast + +- **Data sources queried:** `non-compute-quotas` for current storage and PaaS quota usage, Azure Resource Graph for service inventory, subscription scope from Azure discovery, and historical growth signals derived from current usage where available. +- **Output format and content:** A monthly forecast that identifies storage and PaaS services approaching limits, current usage and limit signals, projected growth risk, and subscriptions needing quota planning. +- **Recommended actions generated:** Request quota increases, archive or clean up unused capacity, move demand to less constrained subscriptions or regions, add service-specific monitoring, and validate growth assumptions with workload owners. +- **Customization options:** Change the monthly cron from `0 8 1 * *`, tune forecast horizon and risk thresholds, select included PaaS services, define acceptable headroom, and add workload-specific growth assumptions. + +### AdvisorSuppressionReview + +- **Data sources queried:** Azure Resource Graph for Advisor suppression resources and metadata, subscription inventory, suppression age and expiration fields, and related recommendation context where available. +- **Output format and content:** A monthly suppression inventory with active, stale, expired, and undocumented suppressions; affected recommendations; age; owner information; and remediation status. +- **Recommended actions generated:** Remove expired suppressions, renew justified suppressions with documentation, restore important Advisor recommendations, assign owners for stale decisions, and create follow-up work for risky suppressed savings. +- **Customization options:** Change the monthly cron from `0 9 1 * *`, tune stale-age and expiration thresholds, scope suppression review by subscription or owner tag, add required justification fields, and define exception approval rules. + +### CapacityMonthlyPlanning + +- **Data sources queried:** `azure-capacity-manager` uses `vm-quota-usage`, `capacity-reservation-groups`, `resource-graph-query`, and benefit recommendations directly, then asks `ftk-database-query` for Kusto-backed forecast, commitment utilization, and savings evidence. +- **Output format and content:** A monthly capacity planning packet with demand forecast, capacity request pipeline, quota and CRG allocation review, cost impact, governance findings, and planning decisions. +- **Recommended actions generated:** Submit quota and capacity requests, adjust capacity reservations, align capacity requests with forecasted demand, rebalance allocation across subscriptions, and update governance controls for capacity planning. +- **Customization options:** Change the monthly cron from `0 9 1 * *`, tune forecast horizon and growth thresholds, choose capacity planning regions and SKUs, set acceptable CRG utilization bands, and add capacity request workflow fields. + +### Semiannual + +- **Data sources queried:** `finops-practitioner` delegates FinOps hub Kusto evidence to `ftk-database-query`, including `monthly-cost-trend`, `quarterly-cost-by-resource-group`, `cost-anomaly-detection`, `savings-summary-report`, `commitment-discount-utilization`, `cost-by-financial-hierarchy`, `cost-forecasting-model`, `reservation-recommendation-breakdown`, `cost-by-region-trend`, `top-resource-types-by-cost`, `service-price-benchmarking`, `monthly-cost-change-percentage`, `top-commitment-transactions`, and `top-other-transactions`. +- **Output format and content:** A semiannual year-over-year finance report with spend trend, forward forecast, quarterly trends, service portfolio analysis, anomaly narrative, savings realization, commitment performance, hierarchy views, and executive actions. +- **Recommended actions generated:** Reforecast budgets, address variance drivers, approve savings actions, adjust commitments, escalate financial hierarchy gaps, and document finance risks before fiscal reviews. +- **Customization options:** Change the semiannual cron from `0 9 5 1,7 *`, align fiscal calendar assumptions, tune variance and forecast thresholds, scope the report by billing or management group hierarchy, and add finance-specific KPI sections. + +### AIWorkloadCostAnalysis + +- **Data sources queried:** `finops-practitioner` delegates AI cost Kusto evidence to `ftk-database-query`, including `ai-token-usage-breakdown`, `ai-model-cost-comparison`, `ai-daily-trend`, and `ai-cost-by-application`, plus cost and capacity context from Azure discovery and `azure-capacity-manager` when needed. +- **Output format and content:** A monthly AI cost report covering token economics, model mix, application allocation, daily trends, input and output ratios, month-over-month AI cost movement, optimization opportunities, and charts. +- **Recommended actions generated:** Substitute models where quality allows, optimize prompts, rightsize environments, improve application cost allocation, assess commitment coverage, and prioritize workloads with high token cost growth. +- **Customization options:** Change the monthly cron from `0 10 1 * *`, tune token-cost and growth thresholds, choose model families or applications in scope, add quality or latency guardrails for model substitution, and change chargeback allocation rules. + +### BudgetCoverageAudit + +- **Data sources queried:** Azure Resource Graph for subscription inventory and budget resources, subscription metadata, and budget configuration details such as amount, period, contact, and scope. +- **Output format and content:** A monthly budget coverage report with active subscriptions, configured budgets, missing or incomplete budgets, coverage gaps, and budget-control remediation items. +- **Recommended actions generated:** Create missing budgets, update budget contacts and thresholds, align budget scopes to subscription ownership, retire budgets for inactive subscriptions, and escalate subscriptions without financial guardrails. +- **Customization options:** Change the monthly cron from `0 8 15 * *`, define required budget coverage thresholds, exclude sandbox or retired subscriptions, set minimum alert percentages, and add owner or cost-center routing. + +### AlertCoverageAudit + +- **Data sources queried:** Azure Resource Graph for active subscriptions and anomaly alert resources, cost alert configuration, and subscription metadata for ownership and scope validation. +- **Output format and content:** A monthly alert coverage report that lists subscriptions with and without cost anomaly alerts, stale or incomplete alert configuration, missing contacts, and remediation priority. +- **Recommended actions generated:** Create missing anomaly alerts, repair alert scopes or contacts, standardize alert thresholds, remove obsolete alerts, and assign owners for unmonitored high-spend subscriptions. +- **Customization options:** Change the monthly cron from `0 8 16 * *`, tune required alert coverage and severity thresholds, exclude low-risk subscriptions, set required notification contacts, and customize anomaly alert policy standards. + +### CapacityQuarterlyStrategy + +- **Data sources queried:** VM quota usage, reservation and savings recommendations, Kusto-backed commitment and savings evidence delegated to `ftk-database-query`, Azure Resource Graph, Azure CLI architecture signals, and non-compute quota context. +- **Output format and content:** A quarterly strategy report covering FinOps capability maturity, commitment alignment, architecture evolution, region and quota strategy, governance health, risk register, and executive recommendations. +- **Recommended actions generated:** Mature capacity governance, align reservations and savings plans with demand, update regional architecture, address non-compute quota constraints, revise capacity request processes, and set quarterly capacity objectives. +- **Customization options:** Change the quarterly cron from `0 9 1 1,4,7,10 *`, tune maturity scoring and risk thresholds, set strategic regions and SKU families, align with quarterly business review dates, and add organization-specific governance criteria. + +
+ +## Notification behavior + +Scheduled tasks deliver final reports through Azure SRE Agent notification connectors. When you configure the Microsoft Teams connector, each task posts only the completed summary to the connected Teams channel. The task prompts explicitly avoid posting intermediate findings so the channel stays focused on ready-to-use results. + +The prompts also split what goes to Teams and what goes to the agent knowledge base: + +- **Teams** receives financial, status, capacity, and strategy results. This includes charts, executive summaries, action items, savings opportunities, quota risks, and capacity recommendations. +- **Knowledge base** receives operational learnings only. Examples include tool errors, query patterns, API workarounds, pipeline failure modes, and troubleshooting steps that worked. +- **Financial detail stays out of memory.** Prompts tell the agents not to save customer financial data, cost amounts, savings numbers, token costs, or similar financial details to the knowledge base. + +For connector setup, see [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md#configure-notifications). + +
+ +## Roadmap + +The deployed template includes the 19 scheduled tasks listed on this page. The repository also includes the detailed current task catalog in [CATALOG.md](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/CATALOG.md). + +The catalog is a roadmap for future automation ideas. It includes more than 74 potential daily, weekly, monthly, quarterly, and annual tasks across FinOps, capacity management, FinOps for AI, governance, optimization, benchmarking, and executive reporting. Those catalog entries are planned ideas, not deployed scheduled tasks, unless they are listed in the deployed task catalog above. + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/ScheduledTasks) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Anomaly management](../../framework/understand/anomalies.md) +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Rate optimization](../../framework/optimize/rates.md) +- [Usage optimization](../../framework/optimize/workloads.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) +- [Azure Monitor](/azure/azure-monitor/) + +Related solutions: + +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) +- [Azure SRE Agent template reference (FinOps toolkit)](template.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/security.md b/docs-mslearn/toolkit/sre-agent/security.md new file mode 100644 index 000000000..f8e5afb90 --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/security.md @@ -0,0 +1,200 @@ +--- +title: Security and permissions for Azure SRE Agent in the FinOps toolkit +description: Review the permissions, identities, run modes, and data flows the FinOps toolkit configures on Azure SRE Agent before you deploy it in your environment. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: concept-article +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps hub admin, I want to understand the security and permissions the FinOps toolkit configures on Azure SRE Agent so that I can deploy it with least privilege. +--- + +# Security and permissions for Azure SRE Agent in the FinOps toolkit + +The FinOps toolkit deployment configures Azure SRE Agent with managed identity, Azure RBAC, Azure Data Explorer permissions, and optional Microsoft Teams or Outlook connectors to run FinOps and capacity workflows. Review these permissions before deployment so you can keep the agent scoped to the data and actions it needs. + +> [!IMPORTANT] +> The agent is designed for least privilege. The template grants read, monitoring, Kusto viewer, and zone mapping permissions. It doesn't grant broad write access to Azure resources. + +
+ +## Deployment permissions + +To deploy the template, the deploying user or service principal needs one of these permission sets on the target subscription: + +- **Owner** +- **User Access Administrator** and **Contributor** + +These permissions are needed because deployment creates a resource group, creates Azure resources, assigns Azure RBAC roles, and assigns the deploying principal the Azure SRE Agent Administrator role on the agent resource. + +> [!TIP] +> Use a dedicated deployment identity for production environments. After deployment, review role assignments and remove any temporary permissions the deployment identity no longer needs. + +
+ +## Managed identity permissions + +The template creates an Azure SRE Agent with a system-assigned managed identity. + +The system-assigned managed identity is the primary identity the template manages. The template uses it for Azure resource operations, action execution, the knowledge graph configuration, and connector setup. The deployment grants the system-assigned managed identity these permissions: + +| Role | Scope | What it grants | Why it's needed | +|------|-------|----------------|-----------------| +| Reader | Subscription | Read access to Azure resources and metadata | Lets the agent inspect resource configuration, cost scopes, quota context, and related Azure inventory | +| Reader | Target resource groups | Read access to scoped resource groups | Lets the agent inspect explicitly targeted resources | +| Monitoring Reader | Target resource groups | Read access to Azure Monitor settings and telemetry | Lets the agent use monitoring context for investigations and health checks | +| Log Analytics Reader | Target resource groups | Read access to Log Analytics resources | Lets the agent query scoped workspace telemetry when configured | +| Contributor | Target resource groups when `accessLevel` is `High` | Resource-group write access | Lets remediation tools create or update approved alerts and budgets in targeted scopes | +| AllDatabasesViewer | Azure Data Explorer cluster | Viewer access across databases in the cluster | Lets the Kusto connector query FinOps hub cost data | +| SRE Agent Administrator | Agent resource | Administer the Azure SRE Agent resource | Lets the deploying principal manage the agent after deployment | + +With this default scope, the managed identity can read Azure resource metadata, monitoring context, and FinOps hub data. It can write approved remediation resources only in explicitly targeted resource groups when `accessLevel` is `High`, and it can send messages or email only through connectors you configure. + +
+ +## Data Explorer permissions + +The agent reads FinOps hub data through an Azure Data Explorer connector. When you provide the optional cluster name and cluster resource group parameters, the deployment assigns `AllDatabasesViewer` on the target Azure Data Explorer cluster to the system-assigned managed identity. + +This permission lets the agent query hub data, including cost, usage, commitment, allocation, anomaly, and forecast datasets exposed through the FinOps toolkit Kusto tools. It doesn't grant database administration permissions. + +> [!NOTE] +> If you don't grant `AllDatabasesViewer` during deployment, a cluster administrator can grant the equivalent viewer permission later. The Kusto connector won't return FinOps hub results until the managed identity has permission on the cluster. + +
+ +## Connector permissions + +The agent doesn't create Teams or Outlook notification connectors during deployment. You add them interactively in [sre.azure.com](https://sre.azure.com) after deployment. + +To configure a Teams or Outlook connector, the configuring user needs **Contributor** on the agent resource group, including these actions: + +| Permission | Why it's needed | +|------------|-----------------| +| `Microsoft.Web/connections/write` | Creates or updates the connector resource that stores the connection configuration | +| `Microsoft.Authorization/roleAssignments/write` | Assigns the selected managed identity permission to use the connector resource | + +Communication connectors use both OAuth and managed identity: + +1. You sign in with a Microsoft 365 account through OAuth. +2. You select the agent's managed identity. +3. The connector stores the OAuth token securely in the connector resource. +4. The agent uses the managed identity at runtime to access the connector resource and send messages or email. + +For Teams, the account that signs in must have access to the target channel and the channel's **Get link to channel** URL. The connector can post messages to the configured channel, reply to threads, and read channel messages for context. + +
+ +## Run modes + +Azure SRE Agent supports two run modes: + +| Mode | Security behavior | Use with the FinOps toolkit deployment | +|------|-------------------|---------------------------| +| Review | The agent proposes Azure infrastructure write actions, and an SRE Agent Administrator approves or denies them | Best for production response plans and any workflow that might change resources | +| Autonomous | The agent runs allowed actions without waiting for approval | Best for trusted recurring reports, health checks, and scheduled FinOps summaries | + +The template sets the agent action configuration to **Autonomous** so scheduled cost and capacity reports can run and post results without manual approval. + +> [!IMPORTANT] +> Run mode doesn't replace permissions. The agent can act only when its managed identity, connector, and data source permissions allow the action. Review mode adds an approval step for Azure infrastructure write actions, but other actions, such as querying data or posting to Teams, run based on available tools and permissions. + +Start with Review for workflows that could affect production resources. Use Autonomous for read-only reporting tasks after you confirm the prompts, tools, and destination channels are correct. + +
+ +## B2B tenant considerations + +In B2B environments, the Azure subscription and Azure SRE Agent resource can be in a different Microsoft Entra tenant than your Microsoft 365 home tenant. + +Use these checks when deployment, Azure MCP Server, or connector configuration fails: + +1. Confirm the active Azure CLI context points to the subscription that owns the SRE Agent resource. +2. Re-authenticate against the tenant that owns the subscription. +3. When using Azure MCP Server `sreagent` tools, pass the resource tenant, subscription, resource group, and agent name explicitly. +4. If Azure MCP Server returns `403` from its SRE Agent path, verify the same identity can read the ARM resource and data-plane endpoint before changing agent permissions. + +Browser access can succeed while a local tool returns `401`, `403`, or `Forbidden` if the token was issued for the wrong tenant. The deployment script sets the active subscription before deployment to reduce this risk. + +Connector setup can also cross identity boundaries. The Azure resource permissions come from the resource tenant, while Teams or Outlook OAuth uses the Microsoft 365 account that signs in to the connector. + +
+ +## Data flow + +The agent reads operational and cost data from these sources: + +- FinOps hub cost and usage data in Azure Data Explorer. +- Azure resource metadata through Reader permissions. +- Azure Monitor and Log Analytics context through target resource group reader permissions. +- Uploaded knowledge documents that describe FinOps toolkit tools, Teams notification patterns, report output style, and known issues. + +Scheduled reports and investigation summaries can be sent to: + +- The Azure SRE Agent chat experience. +- A configured Microsoft Teams channel. +- Outlook, if you add the Outlook connector. + +Don't save financial data to agent memory. Save only operational notes, such as tool errors, workarounds, and repeatable patterns. + +Data residency follows the Azure resources and Microsoft 365 services you connect. FinOps hub data stays in your Azure Data Explorer cluster until queried. Agent telemetry is written to the deployed Log Analytics and Application Insights resources. Teams or Outlook reports are delivered to the channel or mailbox you configure, and OAuth tokens are stored in the connector resource rather than in the agent prompt or knowledge files. + +
+ +## Least privilege checklist + +Use this checklist before you move from test to production: + +- Grant the deployment identity only the permissions needed for deployment. +- Keep the agent managed identity scoped to subscription Reader, target resource group reader/remediation permissions, and Azure Data Explorer viewer permissions unless your own workflows require more. +- Use Review mode for production workflows that can change Azure resources. +- Use Autonomous mode only for trusted reports, health checks, and read-only tasks. +- Configure Teams and Outlook connectors with accounts that are allowed to send to the target audience. +- Send reports only to channels or mailboxes approved for cost and capacity information. +- Review Data Explorer access when hub databases or clusters change. + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Anomaly management](../../framework/understand/anomalies.md) +- [Rate optimization](../../framework/optimize/rates.md) +- [Usage optimization](../../framework/optimize/workloads.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Run modes in Azure SRE Agent](/azure/sre-agent/run-modes) +- [Azure Data Explorer](/azure/data-explorer/) +- [Azure Monitor](/azure/azure-monitor/) + +Related solutions: + +- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [Azure SRE Agent template reference (FinOps toolkit)](template.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/template.md b/docs-mslearn/toolkit/sre-agent/template.md new file mode 100644 index 000000000..a36aa9b7f --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/template.md @@ -0,0 +1,209 @@ +--- +title: Azure SRE Agent template reference (FinOps toolkit) +description: Review the FinOps toolkit's Azure SRE Agent deployment template, parameters, outputs, script flags, and Bicep module structure. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: reference +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps hub admin, I want to understand the FinOps toolkit's Azure SRE Agent template so that I can deploy and customize it safely. +--- + +# Azure SRE Agent template reference (FinOps toolkit) + +This reference summarizes the [FinOps toolkit's Azure SRE Agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent). Use it to review deployment prerequisites, Bicep parameters, script options, and module structure before you deploy or customize the template. + +
+ +## Prerequisites + +Ensure the following prerequisites are met before you deploy the template: + + +- You must have permissions to create the deployed resources and assign roles. + + | Task | Minimum permission | + | ---- | ------------------ | + | Deploy the subscription-scoped Bicep template and create the target resource group | [Contributor](/azure/role-based-access-control/built-in-roles#contributor) on the subscription | + | Assign subscription and target resource group roles to the agent managed identity | [Role Based Access Control Administrator](/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator), [User Access Administrator](/azure/role-based-access-control/built-in-roles#user-access-administrator), or [Owner](/azure/role-based-access-control/built-in-roles#owner) on the assignment scopes | + | Assign Azure Data Explorer access when cluster parameters are set | Permission to create `Microsoft.Kusto/clusters/principalAssignments` on the target cluster | + | Apply Azure SRE Agent objects with the portal deployment script or `bin/apply-extras.sh` | SRE Agent Administrator on the deployed Azure SRE Agent resource | + +- The `Microsoft.App` resource provider must be registered in the subscription. +- For local CLI deployments only: [Azure CLI](/cli/azure/install-azure-cli), `curl`, `jq`, Bash 3.2 or newer, and `python3` with PyYAML must be available locally. +- A FinOps hub with Azure Data Explorer is required when you want the agent to query hub data. + + +
+ +## Parameters + +Here are the parameters you can use to customize the deployment: + +| Parameter | Type | Default value | Allowed values | Description | +| --------- | ---- | ------------- | -------------- | ----------- | +| **resourceGroupName** | String | None | Any string | Required. Resource group that contains the SRE Agent resources. | +| **agentName** | String | None | Any string | Required. Azure SRE Agent name. | +| **location** | String | `eastus2` | `australiaeast`, `canadacentral`, `eastus2`, `francecentral`, `koreacentral`, `swedencentral`, `uksouth` | Optional. Primary location for all resources. | +| **targetResourceGroups** | Array | `[]` | Resource group names | Optional. Resource groups the agent can observe or act on. Defaults to the agent resource group. | +| **targetResourceGroupNames** | String | `""` | Comma-separated resource group names | Optional. Portal form input for target resource groups. Defaults to the agent resource group. | +| **accessLevel** | String | `High` | `Low`, `High` | Optional. Agent access level. | +| **actionMode** | String | `autonomous` | `review`, `autonomous`, `readOnly` | Optional. Agent action mode. | +| **upgradeChannel** | String | `Preview` | `Stable`, `Preview` | Optional. Agent upgrade channel. | +| **defaultModelProvider** | String | `MicrosoftFoundry` | `MicrosoftFoundry`, `Anthropic` | Optional. Parent SRE Agent model provider. `MicrosoftFoundry` maps to Azure OpenAI in the SRE Agent portal. | +| **defaultModelName** | String | `Automatic` | Any supported model name | Optional. Parent SRE Agent model name. Use `Automatic` so SRE Agent routes to the appropriate model inside the selected provider. | +| **monthlyAgentUnitLimit** | Int | `10000` | 1 or higher | Optional. Monthly agent unit limit. | +| **experimentalSettings** | Object | `{ "EnableSandboxGroup": true, "EnableWorkspaceTools": true }` | Object | Optional. Agent sandbox and workspace tool settings for the stable SRE Agent runtime. | +| **finopsHubKustoConnectorUri** | String | `""` | Database-qualified Kusto URI | Optional. Kusto connector URI for the FinOps hub database, such as `https://cluster.region.kusto.windows.net/Hub`. | +| **finopsHubKustoClusterResourceId** | String | `""` | Azure resource ID | Optional. Azure Data Explorer cluster resource ID for the FinOps hub role assignment. | +| **enableSubscriptionReaderRole** | Boolean | `true` | `true`, `false` | Optional. Assigns Reader at subscription scope to the agent managed identity. | +| **recipePackageUri** | String | Derived from the template URI | Public URI | Optional. URI for the packaged recipe assets used by the portal deployment script. | +| **forceUpdateTag** | String | Current deployment timestamp | Any string | Optional. Forces the portal deployment script to rerun during redeployment. | + +
+ +## Deployment values + +The portal template receives values from `createUiDefinition.json`, then runs a subscription-scoped ARM deployment directly from the published toolkit deploy artifacts. `bin/deploy.sh` writes a deployment parameter file under the local SRE Agent deployment cache, then runs `az deployment sub create` directly. Neither path uses Azure Developer CLI environments. + +| Value | Source | Description | +| ----- | ------ | ----------- | +| **subscription** | `--subscription` | Azure subscription ID. | +| **resourceGroupName** | `--resource-group` | Resource group that contains the SRE Agent resources. | +| **agentName** | `--name` | Azure SRE Agent name. | +| **location** | `--location` | Azure location for the deployment. | +| **targetResourceGroups** | `--target-resource-group` | Target resource group names. Defaults to the agent resource group. | +| **enableSubscriptionReaderRole** | Default, unless `--no-subscription-reader` is passed | Whether the deployment assigns subscription-scope Reader to the agent managed identity. | +| **defaultModelProvider** | `agent.json` | Parent SRE Agent model provider. The FinOps hub recipe defaults to Azure OpenAI provider-level routing with `MicrosoftFoundry`. | +| **defaultModelName** | `agent.json` | Parent SRE Agent model name. The FinOps hub recipe defaults to `Automatic` model routing. | +| **finopsHubKustoClusterResourceId** | `--cluster-resource-id` | Optional cluster resource ID used to assign `AllDatabasesViewer`. | +| **FinOps Hub Kusto connector URI** | `--cluster-uri` | Database-qualified Kusto URI, such as `https://cluster.region.kusto.windows.net/Hub`. | + +
+ +## Portal recipe configuration + +The Deploy to Azure path packages `azuredeploy.json`, `createUiDefinition.json`, and `sre-agent-recipe.zip` under `docs/deploy/sre-agent//`. The `recipePackageUri` parameter defaults to a file beside the linked template by using the ARM deployment template-link URI. The deployment script downloads that package, reads `extras.json`, and applies the same recipe objects as `bin/apply-extras.sh`. + +The deployment script uses a user-assigned managed identity. Bicep grants that identity SRE Agent Administrator on the agent resource before the script runs, then the script uses ARM for connectors and the SRE Agent data plane for built-in tools, knowledge, tools, skills, subagents, and scheduled tasks. + +
+ +## Azure Data Explorer network access + +When `--cluster-uri` points to an Azure Data Explorer cluster, the deployment inspects the cluster's network access setting before creating the agent. If `publicNetworkAccess` is `Disabled`, the deployment doesn't stop. It still deploys all resources, assigns the agent managed identity `AllDatabasesViewer`, and creates the `finops-hub-kusto` connector. + +The deployment prints a warning with the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations) because hosted Azure SRE Agent can't run direct KQL queries against private endpoint ADX clusters. The customer must decide whether to enable public query access for the cluster. Until public query access is enabled or Azure SRE Agent adds private endpoint ADX query support, the connector is expected to remain unhealthy. + +
+ +## Outputs + +Here are the outputs generated by the deployment: + +| Output | Type | Description | +| ------ | ---- | ----------- | +| **AZURE_RESOURCE_GROUP** | String | Name of the Azure resource group that contains the SRE Agent resources. | +| **AZURE_LOCATION** | String | Azure location used for the SRE Agent resources. | +| **SRE_AGENT_NAME** | String | Name of the deployed Azure SRE Agent resource. | +| **SRE_AGENT_ENDPOINT** | String | Endpoint of the deployed Azure SRE Agent resource. | + +
+ +## Script flags + +The template includes Bash scripts for one-shot deployment and extras configuration. + +### Deployment wrapper + +Use `bin/deploy.sh` to write deterministic ARM parameters, deploy `infra/main.bicep` with Azure CLI, and run `bin/apply-extras.sh`. + +Supporting resource names are deterministic for the subscription ID, agent resource group ID, and agent name. Rerunning the script with the same tuple updates the same Log Analytics workspace, Application Insights component, system-assigned managed identity RBAC assignments, and SRE Agent instead of creating timestamped duplicates. The optional `--deploy-name` only names the ARM deployment record and local build directory. + +| Bash flag | Required | Description | +| --------- | -------- | ----------- | +| `--recipe ` | Yes | Recipe directory. Use `recipes/finops-hub`. | +| `--subscription ` | Yes | Azure subscription ID. | +| `--resource-group ` | Yes | Resource group for SRE Agent resources. | +| `--name ` | Yes | Azure SRE Agent name. | +| `--location ` | Yes | Azure region. | +| `--target-resource-group ` | No | Repeatable target resource group. Defaults to `--resource-group`. | +| `--cluster-uri ` | No | Database-qualified FinOps hub Kusto URI, such as `https://cluster.region.kusto.windows.net/Hub`. | +| `--cluster-resource-id ` | Optional with `--cluster-uri` for real deployments; required for `--dry-run` | Kusto cluster ARM resource ID for `AllDatabasesViewer`. Real deployments resolve it from `--cluster-uri` when the cluster is in the target subscription. | +| `--no-subscription-reader` | No | Skips the default subscription-scope Reader assignment. Use only when equivalent read access is granted another way. | +| `--deploy-name ` | No | Deployment name override. Defaults to a deterministic name from subscription ID, resource group, and agent name. | +| `--dry-run` | No | Validate inputs and write parameters without Azure calls. | +| `--force`, `--no-telemetry` | No | Compatibility flags accepted by the wrapper. | +| `-h`, `--help` | No | Show script help. | + +### Extras configuration + +Use `bin/apply-extras.sh` to configure the Kusto connector through ARM when requested, upload KnowledgeFile sources, and apply custom agents, skills, tools, and scheduled tasks through the SRE Agent data plane. `bin/post-provision.sh` remains as a deprecated compatibility wrapper that forwards to `bin/apply-extras.sh`. + +| Bash flag | Required | Description | +| --------- | -------- | ----------- | +| `--endpoint ` | Yes | SRE Agent endpoint returned by deployment. | +| `--recipe ` | Yes | Recipe directory. | +| `--subscription ` | Yes | Azure subscription that contains the SRE Agent. | +| `--resource-group ` | Yes | Resource group that contains the SRE Agent. | +| `--name ` | Yes | SRE Agent name. | +| `--build-dir ` | Yes | Working directory for generated extras request and response files. | +| `--kusto-connector-uri ` | No | Database-qualified Kusto connector URI. | + +
+ +## Module structure + +The template uses a subscription-scoped entry point and resource group modules: + +| File | Scope | Deploys or configures | +| ---- | ----- | --------------------- | +| `infra/main.bicep` | Subscription | Creates the target resource group, calls the resource group deployment, assigns subscription and target resource group RBAC, and optionally assigns Azure Data Explorer roles. | +| `infra/resources.bicep` | Resource group | Orchestrates identity, monitoring, and Azure SRE Agent modules, then surfaces outputs to the subscription deployment. | +| `infra/modules/monitoring.bicep` | Resource group | Creates the Log Analytics workspace and workspace-based Application Insights component for telemetry. | +| `infra/modules/sre-agent.bicep` | Resource group | Creates the `Microsoft.App/agents@2026-01-01` resource, configures action mode, model provider, sandbox, and workspace tool settings, assigns SRE Agent Administrator to the deployer, and exposes the agent endpoint for `bin/apply-extras.sh`. | +| `infra/modules/resource-group-rbac.bicep` | Resource group | Assigns Reader, Monitoring Reader, Log Analytics Reader, and optionally Contributor to the agent system-assigned managed identity. | +| `infra/modules/kusto-all-databases-viewer-rbac.bicep` | Resource group | Assigns `AllDatabasesViewer` on an existing Azure Data Explorer cluster to the system-assigned managed identity when cluster parameters are provided. | + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Anomaly management](../../framework/understand/anomalies.md) +- [Rate optimization](../../framework/optimize/rates.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) + +Related solutions: + +- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [FinOps hubs](../hubs/finops-hubs-overview.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/tools.md b/docs-mslearn/toolkit/sre-agent/tools.md new file mode 100644 index 000000000..e10481581 --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/tools.md @@ -0,0 +1,186 @@ +--- +title: Tools shipped for Azure SRE Agent in the FinOps toolkit +description: Review the Kusto and Python tools the FinOps toolkit ships for Azure SRE Agent for cost analysis, anomaly detection, rate optimization, capacity management, and operations. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: reference +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to understand which tools the FinOps toolkit ships for Azure SRE Agent so that I can choose the right agent workflow for cost, capacity, and operations analysis. +--- + +# Tools shipped for Azure SRE Agent in the FinOps toolkit + +The FinOps toolkit deployment configures Azure SRE Agent with tools that ground agent responses in live Azure and FinOps hub data. [Kusto tools](kusto-tools.md) query the FinOps hub Azure Data Explorer database, and [Python tools](python-tools.md) call Azure APIs through the agent's managed identity. + +Use this article as a catalog of the tools included with the template. For deeper implementation details, review the [Kusto tools](kusto-tools.md) and [Python tools](python-tools.md) references. + +The template configures 50 tools: 37 Kusto query tools generated from the FinOps hub query catalog and 13 Python tools, as documented in the [Kusto tools](kusto-tools.md) and [Python tools](python-tools.md) references. All Kusto tools are assigned only to `ftk-database-query`; other agents request Kusto evidence from that specialist instead of querying FinOps hub directly. + +> [!NOTE] +> The agent list shows subagents that reference each tool in `recipes/finops-hub/config/subagents`. Tools marked "Not assigned" are included in the tool catalog, but aren't referenced by a subagent configuration. For agent roles and tool usage, see [agents and skills](agents.md). + +
+ +## AI cost management + +AI cost management tools help agents analyze Azure OpenAI spend, token usage, model efficiency, and application-level allocation, as described in [AI/ML costs](kusto-tools.md#aiml-costs). + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `ai-cost-by-application` | `Kusto` | Breaks down Azure OpenAI costs by application, team, and environment tags for chargeback, showback, and allocation; see [ai-cost-by-application](kusto-tools.md#ai-cost-by-application). | `ftk-database-query` | +| `ai-daily-trend` | `Kusto` | Shows daily Azure OpenAI cost and token trends for AI cost anomaly detection and forecasting; see [ai-daily-trend](kusto-tools.md#ai-daily-trend). | `ftk-database-query` | +| `ai-model-cost-comparison` | `Kusto` | Compares Azure OpenAI model cost per 1,000 tokens to identify model efficiency opportunities; see [ai-model-cost-comparison](kusto-tools.md#ai-model-cost-comparison). | `ftk-database-query` | +| `ai-token-usage-breakdown` | `Kusto` | Breaks down Azure OpenAI token consumption by model version and input or output direction; see [ai-token-usage-breakdown](kusto-tools.md#ai-token-usage-breakdown). | `ftk-database-query` | + +
+ +## Cost analysis and reporting + +Cost analysis and reporting tools summarize FinOps hub cost data by service, region, resource group, financial hierarchy, transaction type, and time period, as described in [Cost analysis](kusto-tools.md#cost-analysis) and [Price analysis](kusto-tools.md#price-analysis). + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `costs-enriched-base` | `Kusto` | Returns a guarded row-level enriched cost and usage sample for narrow drill-downs; see [costs-enriched-base](kusto-tools.md#costs-enriched-base). Use aggregate tools for full-month, fiscal-period, scheduled report, and tag-coverage rollup requests. | `ftk-database-query` | +| `cost-by-financial-hierarchy` | `Kusto` | Reports top costs across billing profile, invoice section, team, product, application, and environment; see [cost-by-financial-hierarchy](kusto-tools.md#cost-by-financial-hierarchy). | `ftk-database-query` | +| `cost-by-region-trend` | `Kusto` | Summarizes effective cost by Azure region to identify regional spend distribution and optimization opportunities; see [cost-by-region-trend](kusto-tools.md#cost-by-region-trend). | `ftk-database-query` | +| `monthly-cost-change-percentage` | `Kusto` | Calculates month-over-month billed and effective cost changes to spot spikes, drops, and volatility; see [monthly-cost-change-percentage](kusto-tools.md#monthly-cost-change-percentage). | `ftk-database-query` | +| `monthly-cost-trend` | `Kusto` | Returns billed and effective cost totals by month for trend review and budget analysis; see [monthly-cost-trend](kusto-tools.md#monthly-cost-trend). | `ftk-database-query` | +| `quarterly-cost-by-resource-group` | `Kusto` | Returns top resource-group cost rows by subscription and month for quarterly reporting windows; see [quarterly-cost-by-resource-group](kusto-tools.md#quarterly-cost-by-resource-group). | `ftk-database-query` | +| `top-other-transactions` | `Kusto` | Lists top non-usage, non-commitment purchase transactions, such as Marketplace or miscellaneous charges; see [top-other-transactions](kusto-tools.md#top-other-transactions). | `ftk-database-query` | +| `top-resource-groups-by-cost` | `Kusto` | Returns top resource groups by effective cost for focused reporting and optimization; see [top-resource-groups-by-cost](kusto-tools.md#top-resource-groups-by-cost). | `ftk-database-query` | +| `top-resource-types-by-cost` | `Kusto` | Returns top resource types by resource count and effective cost to identify costly categories; see [top-resource-types-by-cost](kusto-tools.md#top-resource-types-by-cost). | `ftk-database-query` | +| `top-services-by-cost` | `Kusto` | Returns top Azure services by effective cost to prioritize service-level optimization; see [top-services-by-cost](kusto-tools.md#top-services-by-cost). | `ftk-database-query` | + +
+ +## FinOps KPI scorecard + +FinOps KPI scorecard tools are generated from [`src/queries/catalog`](../../../src/queries/catalog/) and map directly to query links in [`src/queries/KPI.md`](../../../src/queries/KPI.md). Scheduled monthly, semiannual, health, anomaly, capacity, storage, monitoring, and benefit-review tasks request these tools through `ftk-database-query`. + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `allocation-accuracy-index` | `Kusto` | Measures directly attributed cost as a share of total effective cost. | `ftk-database-query` | +| `anomaly-detection-rate` | `Kusto` | Measures the share of spend in anomaly-flagged daily buckets. | `ftk-database-query` | +| `anomaly-variance-total` | `Kusto` | Quantifies unpredicted spend variance for detected anomaly events. | `ftk-database-query` | +| `commitment-discount-waste` | `Kusto` | Measures unused commitment value as a share of total commitment cost. | `ftk-database-query` | +| `commitment-utilization-score` | `Kusto` | Computes commitment utilization score by commitment and currency. | `ftk-database-query` | +| `compute-cost-per-core` | `Kusto` | Calculates effective and hourly compute cost per consumed vCPU core hour. | `ftk-database-query` | +| `compute-spend-commitment-coverage` | `Kusto` | Measures compute spend covered by commitment discounts. | `ftk-database-query` | +| `cost-optimization-index` | `Kusto` | Computes a cost optimization index from current recommendations and cost context. | `ftk-database-query` | +| `cost-per-gb-stored` | `Kusto` | Calculates storage cost per normalized GB-month. | `ftk-database-query` | +| `cost-visibility-delay` | `Kusto` | Measures cost data visibility delay for data-ingestion KPI reporting. | `ftk-database-query` | +| `data-update-frequency` | `Kusto` | Measures FinOps hub ingestion update cadence. | `ftk-database-query` | +| `macc-consumption-vs-commitment` | `Kusto` | Measures Microsoft Azure Consumption Commitment drawdown against commitment. | `ftk-database-query` | +| `percentage-unallocated-costs` | `Kusto` | Measures the share of cost missing required allocation evidence. | `ftk-database-query` | +| `percentage-untagged-costs` | `Kusto` | Measures the share of cost on resources with no tags. | `ftk-database-query` | +| `storage-tier-distribution` | `Kusto` | Summarizes storage cost and GB-month distribution by access-tier bucket. | `ftk-database-query` | +| `tagging-policy-compliance` | `Kusto` | Measures cost-weighted compliance with required tag keys. | `ftk-database-query` | + +
+ +## Anomaly detection and forecasting + +Anomaly detection and forecasting tools help agents identify unexpected cost changes, project future spend, and deploy alerting automation, as described in [Anomaly detection](kusto-tools.md#anomaly-detection), [Forecasting](kusto-tools.md#forecasting), and [Budget and alert deployment](python-tools.md#budget-and-alert-deployment). + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `cost-anomaly-detection` | `Kusto` | Detects cost spikes and drops with time-series anomaly detection over a configurable history window; see [cost-anomaly-detection](kusto-tools.md#cost-anomaly-detection). | `ftk-database-query` | +| `cost-forecasting-model` | `Kusto` | Forecasts future effective cost from historical cost data for budgeting and trend projection; see [cost-forecasting-model](kusto-tools.md#cost-forecasting-model). | `ftk-database-query` | +| `deploy-anomaly-alert` | `Python` | Creates or updates a Cost Management scheduled action for anomaly detection in a subscription after explicit remediation approval; see [deploy-anomaly-alert](python-tools.md#deploy-anomaly-alert). | `finops-practitioner` | +| `deploy-bulk-anomaly-alerts` | `Python` | Discovers enabled subscriptions in a management group and deploys anomaly alert scheduled actions per subscription after explicit remediation approval; see [deploy-bulk-anomaly-alerts](python-tools.md#deploy-bulk-anomaly-alerts). | `finops-practitioner` | + +
+ +## Rate optimization + +Rate optimization tools help agents review reservations, savings plans, pricing benchmarks, commitment transactions, and Advisor recommendation suppression workflows, as described in [Commitment discounts](kusto-tools.md#commitment-discounts), [Price analysis](kusto-tools.md#price-analysis), [Resource analysis](python-tools.md#resource-analysis), and [Advisor](python-tools.md#advisor). + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `benefit-recommendations` | `Python` | Gets Microsoft Cost Management benefit recommendations for savings plans and reserved instances at a billing scope; see [benefit-recommendations](python-tools.md#benefit-recommendations). | `finops-practitioner`, `azure-capacity-manager` | +| `commitment-discount-utilization` | `Kusto` | Analyzes consumed core hours by commitment discount type, including on-demand usage, for a reporting window; see [commitment-discount-utilization](kusto-tools.md#commitment-discount-utilization). | `ftk-database-query` | +| `reservation-recommendation-breakdown` | `Kusto` | Analyzes reservation recommendations, savings, break-even dates, normalized sizes, scope, and term details; see [reservation-recommendation-breakdown](kusto-tools.md#reservation-recommendation-breakdown). | `ftk-database-query` | +| `savings-summary-report` | `Kusto` | Summarizes list cost, effective cost, negotiated savings, commitment savings, total savings, and savings rate; see [savings-summary-report](kusto-tools.md#savings-summary-report). | `ftk-database-query` | +| `service-price-benchmarking` | `Kusto` | Benchmarks services by list cost, contracted cost, effective cost, negotiated savings, commitment savings, and total savings; see [service-price-benchmarking](kusto-tools.md#service-price-benchmarking). | `ftk-database-query` | +| `suppress-advisor-recommendations` | `Python` | Suppresses selected Azure Advisor recommendations across subscriptions under a management group with a time to live after explicit remediation approval; see [suppress-advisor-recommendations](python-tools.md#suppress-advisor-recommendations). | `finops-practitioner` | +| `top-commitment-transactions` | `Kusto` | Returns top non-usage commitment discount purchase transactions for reservation and savings plan review; see [top-commitment-transactions](kusto-tools.md#top-commitment-transactions). | `ftk-database-query` | + +
+ +## Capacity management + +Capacity management tools help agents inspect quota, capacity reservations, SKU availability, and non-compute service limits before planning or deployment, as described in [Capacity and quota](python-tools.md#capacity-and-quota) and the [azure-capacity-manager](agents.md#azure-capacity-manager) agent reference. + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `capacity-reservation-groups` | `Python` | Lists capacity reservation groups and compares reserved capacity with allocated virtual machines; see [capacity-reservation-groups](python-tools.md#capacity-reservation-groups). | `azure-capacity-manager` | +| `non-compute-quotas` | `Python` | Checks non-compute service quota usage with provider usage APIs and estimated Resource Graph fallbacks; see [non-compute-quotas](python-tools.md#non-compute-quotas). | `azure-capacity-manager` | +| `sku-availability` | `Python` | Lists Azure Compute or Azure Data Explorer SKU availability, zones, and regional restriction reasons before planning or deployment; see [sku-availability](python-tools.md#sku-availability). | `azure-capacity-manager`, `ftk-hubs-agent` | +| `vm-quota-usage` | `Python` | Reports VM family quota usage by region and flags families above 80% or 95% utilization; see [vm-quota-usage](python-tools.md#vm-quota-usage). | `azure-capacity-manager` | + +
+ +## Governance and automation + +Governance and automation tools help agents deploy budget guardrails and run Resource Graph queries for inventory, configuration, and operational checks, as described in [Budget and alert deployment](python-tools.md#budget-and-alert-deployment) and [Resource analysis](python-tools.md#resource-analysis). + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `deploy-budget` | `Python` | Creates or updates a subscription-level Cost Management budget with notification contacts after explicit remediation approval; see [deploy-budget](python-tools.md#deploy-budget). | `finops-practitioner` | +| `deploy-bulk-budgets` | `Python` | Discovers enabled subscriptions in a management group and creates or updates a Cost Management budget in each subscription after explicit remediation approval; see [deploy-bulk-budgets](python-tools.md#deploy-bulk-budgets). | `finops-practitioner` | +| `resource-graph-query` | `Python` | Runs Azure Resource Graph KQL queries across subscriptions for inventory and configuration troubleshooting; see [resource-graph-query](python-tools.md#resource-graph-query). | `finops-practitioner`, `azure-capacity-manager`, `ftk-hubs-agent` | + +
+ +## Data ingestion and health + +Data ingestion and health tools help agents validate whether FinOps hub data is fresh enough to trust for analysis and reporting, as described in [Hub management](python-tools.md#hub-management). + +| Tool | Type | Description | Agents | +|------|------|-------------|--------| +| `data-freshness-check` | `Python` | Checks FinOps hub function data freshness through direct Azure Data Explorer REST queries. Treats `Costs()` as the authoritative freshness signal with a three-day threshold and supersedes conflicting stale-memory or raw-KQL conclusions; see [data-freshness-check](python-tools.md#data-freshness-check). | `ftk-hubs-agent` | + +
+ +## Give feedback + +Let us know how we're doing with a [quick review](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent). We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent) + + +If you're looking for something specific, [vote for an existing or create a new idea](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc). Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Anomaly management](../../framework/understand/anomalies.md) +- [Rate optimization](../../framework/optimize/rates.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) +- [Microsoft Cost Management](/azure/cost-management-billing/costs/) + +Related solutions: + +- [Azure SRE Agent in the FinOps toolkit](overview.md) +- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) +- [FinOps hubs](../hubs/finops-hubs-overview.md) + +
diff --git a/docs-mslearn/toolkit/sre-agent/troubleshooting.md b/docs-mslearn/toolkit/sre-agent/troubleshooting.md new file mode 100644 index 000000000..32a7811ec --- /dev/null +++ b/docs-mslearn/toolkit/sre-agent/troubleshooting.md @@ -0,0 +1,252 @@ +--- +title: Troubleshoot Azure SRE Agent deployments from the FinOps toolkit +description: Resolve common deployment, tenant, connector, data, and query issues for Azure SRE Agent deployments from the FinOps toolkit. +author: msbrett +ms.author: brettwil +ms.date: 06/17/2026 +ms.topic: how-to +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: brettwil +#customer intent: As a FinOps practitioner, I want to troubleshoot the FinOps toolkit's Azure SRE Agent deployment so that I can restore scheduled cost, capacity, and operations workflows. +--- + +# Troubleshoot Azure SRE Agent deployments from the FinOps toolkit + +Use this guide when the agent deploys, but Azure MCP Server `sreagent` commands, scheduled tasks, connectors, or data queries don't behave as expected. Start with tenant and deployment checks, then use the known issue sections to match the symptom, cause, and workaround. + +
+ +## Troubleshoot B2B tenant environments + +In B2B environments, the Azure subscription and Azure SRE Agent resource can live in a different Microsoft Entra tenant than your Microsoft 365 home tenant. If [sre.azure.com](https://sre.azure.com) shows the agent correctly but Azure MCP Server `sreagent` commands return `401`, `403`, `AccessDenied`, or `Forbidden`, treat the issue as tenant selection first. + +**Symptom:** Browser access works, but Azure MCP Server `sreagent_agents_list`, `sreagent_agents_get`, `sreagent_agents_tools_list`, or related SRE Agent commands fail with `401`, `403`, `AccessDenied`, or `Forbidden`. + +**Cause:** The tool token can be issued for the wrong tenant or the tool can route the SRE Agent discovery call through a tenant-scoped Resource Graph path. The browser session can use your Microsoft 365 home tenant, while the Azure SRE Agent resource belongs to a different tenant. + +**Workaround:** + +1. Confirm the active Azure CLI context points at the subscription that owns the Azure SRE Agent resource. +1. Re-authenticate Azure CLI against the tenant that owns the subscription and resource. +1. Pass the resource tenant, subscription, resource group, and agent name explicitly to Azure MCP Server `sreagent` commands. +1. Verify the same identity can read the ARM resource with `az resource show` and can call the SRE Agent data-plane endpoint with a token for `https://azuresre.dev`. +1. If ARM and direct data-plane calls work but Azure MCP Server `sreagent` commands still return `AccessDenied`, capture the timestamp and correlation ID from the tool response and route it as an Azure MCP Server or Azure SRE Agent B2B access issue. + +> [!TIP] +> Browser success with Azure MCP Server failure usually means the agent might be healthy, but the tool credential or SRE Agent discovery path needs tenant-specific verification. + +
+ +## Fix common deployment failures + +Use these checks after `bin/deploy.sh` or post-provisioning fails. + +### Unsupported region + +**Symptom:** The Bicep deployment fails during validation or resource creation. + +**Cause:** The template only supports `australiaeast`, `canadacentral`, `eastus2`, `francecentral`, `koreacentral`, `swedencentral`, and `uksouth`. + +**Workaround:** Redeploy in a supported region. + +### Resource provider not registered + +**Symptom:** Azure Resource Manager fails to create the Azure SRE Agent resource. + +**Cause:** The `Microsoft.App` resource provider isn't registered on the target subscription. + +**Workaround:** Register the provider, wait for registration to complete, and rerun deployment. + +```bash +az provider register --namespace Microsoft.App +``` + +### Missing deployment permissions + +**Symptom:** Deployment or role assignment steps fail with authorization errors. + +**Cause:** The deploying user doesn't have enough permissions to create resources or assign roles. + +**Workaround:** Use an account with permissions to create resources and assign roles at the affected scopes. Contributor can create the deployed resources; Role Based Access Control Administrator, User Access Administrator, or Owner can assign the required roles. If you configure notification connectors later, make sure the configuring user can write connections and role assignments in the agent resource group. + +### Zone mapping API returns 404 + +**Symptom:** Capacity or zone-mapping checks fail when the agent calls `checkZonePeers`. + +**Cause:** The `AvailabilityZonePeering` feature isn't registered for the subscription or management group scope. + +**Workaround:** Register the feature and re-register the resource provider. + +```bash +az feature register --namespace Microsoft.Resources --name AvailabilityZonePeering +az provider register --namespace Microsoft.Resources +``` + +### Post-provision hook fails + +**Symptom:** Azure resources deploy, but skills, agents, tools, scheduled tasks, knowledge documents, or the Kusto connector don't appear in [sre.azure.com](https://sre.azure.com). + +**Cause:** The post-provision step couldn't resolve the endpoint, get an SRE Agent data-plane token, configure the Kusto connector, or apply the SRE configuration through the supported ARM and data-plane APIs. + +**Workaround:** Check that Azure CLI, `python3`, `jq`, `curl`, and `bash` are available locally. Then rerun `bin/apply-extras.sh` from `src/templates/sre-agent`. + +### Connector setup is missing + +**Symptom:** Scheduled tasks run, but Teams or Outlook delivery doesn't work. + +**Cause:** `bin/deploy.sh` does not create notification connectors because Teams and Outlook require interactive OAuth setup. + +**Workaround:** Add the Teams or Outlook connector in [sre.azure.com](https://sre.azure.com), select the agent managed identity, and send a test message. + +### Kusto connector is unhealthy for a private endpoint cluster + +**Symptom:** The SRE Agent deployment succeeds and the `finops-hub-kusto` connector is created, but the connector doesn't become healthy. The deployment output may warn that the Azure Data Explorer cluster denies public query access. + +**Cause:** Hosted Azure SRE Agent runs outside your VNET. Azure SRE Agent can use ARM for resource discovery, health, metrics, and management operations, but direct KQL queries to private endpoint Azure Data Explorer clusters are a documented limitation when `publicNetworkAccess` is `Disabled`. + +**Workaround:** Review the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations), then decide whether to enable public query access for the Azure Data Explorer cluster. The FinOps toolkit deployment doesn't change customer cluster networking automatically. Until public query access is enabled or Azure SRE Agent adds private endpoint ADX query support, Kusto tools that depend on `finops-hub-kusto` are expected to fail while ARM-based health and metrics operations continue to work. + +
+ +## Review known issues + +The following issues were observed during scheduled task testing. They don't always indicate a broken deployment. + +### Teams tool discovery + +**Symptom:** A subagent reports that `PostTeamsChannelMessage` isn't available or that it couldn't find the Teams posting function. + +**Cause:** Subagents invoked directly for manual testing might not inherit Teams connector tools. Connector tools are available to the base agent or when the platform triggers a scheduled task. + +**Workaround:** + +- Use the platform cron schedule for production scheduled tasks. +- For manual testing, invoke the base agent without the `--agent` flag, then delegate with `@subagent` in the prompt. +- Use the built-in `PostTeamsMessage` tool. Don't call Microsoft Graph or dynamic invoke endpoints directly because that path can return `403`. + +### Data pipeline staleness + +**Symptom:** Reports show incomplete or missing recent cost data, and forecasts look distorted. + +**Cause:** The FinOps hub data pipeline or Cost Management export may be stale, so the Azure Data Explorer cluster might not have current data. Older memory notes, raw KQL checks, or ingestion timestamp checks can also report stale data incorrectly after the pipeline has recovered. + +**Workaround:** + +- Run `data-freshness-check` or the hubs health check task to confirm freshness before escalating. Treat the direct REST `Costs()` result as the source of truth. +- If `Costs()` is 3 days old or newer, mark conflicting stale-memory, raw-KQL, or ingestion timestamp conclusions as superseded and don't report the hub as stale. +- If `Costs()` has no rows, has a query error, or is more than 3 days old, let reports call out stale data clearly and check Cost Management exports in the Azure portal and pipeline runs in Azure Data Factory. + +### Resource Graph failures + +**Symptom:** `az graph query` returns an unknown error, and Resource Graph-based analysis fails. + +**Cause:** The managed identity may not have Reader permissions at the right scope, or complex query expressions may fail in the code interpreter shell environment. + +**Workaround:** + +- Fall back to scoped `az resource list` queries against specific subscriptions. +- Confirm the agent managed identity has Reader at the management group or subscription scope. +- Simplify query expressions. + +### Quota CLI failures + +**Symptom:** `az quota usage list` or `az vm list-usage` fails in the agent execution environment. + +**Cause:** The `az quota` extension might be missing, or the managed identity might not have permission to read quota data. + +**Workaround:** + +- Use `az vm list-usage --location ` as a compute quota fallback. +- For broader quota checks, use Azure Resource Manager REST calls from code interpreter. +- Track persistent CLI failures so the quota tool can be updated. + +### JMESPath escaping + +**Symptom:** Azure CLI commands fail when `--query` uses backticks, brackets, or property names with dots. + +**Cause:** Shell escaping conflicts with JMESPath syntax in the code interpreter environment. + +**Workaround:** + +- Prefer `--output json`, then parse the result with Python. +- Use only simple JMESPath selections when you need `--query`. +- Avoid nested expressions that rely on backtick-escaped property names. + +### Memory file conflicts + +**Symptom:** A scheduled task returns `File write failed` and says a memory file already exists. + +**Cause:** A repeated task run tried to create the same memory file again. The memory system requires an edit operation for existing files. + +**Workaround:** + +- Use an edit operation for subsequent writes to the same memory file. +- Treat the first failure as recoverable. Agents can usually switch from create to edit and continue. + +### Kusto query errors + +**Symptom:** A tool returns `Error executing query on cluster`. + +**Cause:** A Kusto tool may reference a function, table, or column that doesn't exist in your FinOps hub version, or the query syntax may need a version-specific update. + +**Workaround:** + +- Try a simpler query. +- Check the FinOps hub version. +- Capture the failing tool name and query so the tool YAML can be fixed. + +
+ +## Get support + +If the workaround doesn't resolve the issue, [open a GitHub issue](https://github.com/microsoft/finops-toolkit/issues/new/choose) and include: + +- The deployment region and target subscription tenant +- The failing command, task, or tool name +- The exact error message +- Whether [sre.azure.com](https://sre.azure.com) can open the agent successfully +- Whether the issue affects deployment, Azure MCP Server `sreagent` commands, scheduled tasks, connectors, or FinOps hub data + +For product ideas or known gaps, [vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc). + +
+ +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgentTroubleshooting) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +Related FinOps capabilities: + +- [Reporting and analytics](../../framework/understand/reporting.md) +- [Anomaly management](../../framework/understand/anomalies.md) +- [Rate optimization](../../framework/optimize/rates.md) + +Related products: + +- [Azure SRE Agent](/azure/sre-agent/overview) +- [Azure Data Explorer](/azure/data-explorer/) + +Related solutions: + +- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md) +- [FinOps hubs](../hubs/finops-hubs-overview.md) +- [Azure SRE Agent template reference (FinOps toolkit)](template.md) + +
diff --git a/docs/deploy/sre-agent/14.0/azuredeploy.json b/docs/deploy/sre-agent/14.0/azuredeploy.json new file mode 100644 index 000000000..62832fff6 --- /dev/null +++ b/docs/deploy/sre-agent/14.0/azuredeploy.json @@ -0,0 +1,1178 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "9242331632702473420" + } + }, + "parameters": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource group that holds the SRE Agent resources." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "location": { + "type": "string", + "defaultValue": "eastus2", + "allowedValues": [ + "australiaeast", + "canadacentral", + "eastus2", + "francecentral", + "koreacentral", + "swedencentral", + "uksouth" + ], + "metadata": { + "description": "Primary location for all resources." + } + }, + "targetResourceGroups": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Resource groups the agent can observe or act on. The agent resource group is always included." + } + }, + "targetResourceGroupNames": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Comma-separated resource groups the agent can observe or act on. Used by the Azure portal form." + } + }, + "finopsHubKustoConnectorUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional database-qualified FinOps Hub Kusto connector URI. Example: https://..kusto.windows.net/Hub" + } + }, + "finopsHubKustoClusterResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment." + } + }, + "accessLevel": { + "type": "string", + "defaultValue": "Low", + "allowedValues": [ + "Low", + "High" + ], + "metadata": { + "description": "Agent access level. Low (read-only) is recommended for reporting and analysis without modification risk." + } + }, + "actionMode": { + "type": "string", + "defaultValue": "review", + "allowedValues": [ + "review", + "autonomous", + "readOnly" + ], + "metadata": { + "description": "Agent action mode." + } + }, + "upgradeChannel": { + "type": "string", + "defaultValue": "Preview", + "allowedValues": [ + "Stable", + "Preview" + ], + "metadata": { + "description": "Agent upgrade channel." + } + }, + "defaultModelProvider": { + "type": "string", + "defaultValue": "MicrosoftFoundry", + "allowedValues": [ + "MicrosoftFoundry", + "Anthropic" + ], + "metadata": { + "description": "Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal." + } + }, + "defaultModelName": { + "type": "string", + "defaultValue": "Automatic", + "metadata": { + "description": "Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider." + } + }, + "monthlyAgentUnitLimit": { + "type": "int", + "defaultValue": 10000, + "minValue": 1, + "metadata": { + "description": "Monthly agent unit limit." + } + }, + "experimentalSettings": { + "type": "object", + "defaultValue": { + "EnableSandboxGroup": true, + "EnableWorkspaceTools": true + }, + "metadata": { + "description": "Agent experimental settings." + } + }, + "enableSubscriptionReaderRole": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Assign Reader on the deployment subscription to the agent managed identity. Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups." + } + }, + "recipePackageUri": { + "type": "string", + "defaultValue": "[uri(deployment().properties.templateLink.uri, 'sre-agent-recipe.zip')]", + "metadata": { + "description": "Public URI for the generated SRE Agent recipe package. Deploy-to-Azure links derive this from the template URI." + } + }, + "recipePackageSha256": { + "type": "string", + "defaultValue": "bc87901a7090d36feb7d80664d57fc9bfa09056eed316fbf5458855b9de97662", + "metadata": { + "description": "SHA256 hash of the recipe package for integrity verification." + } + }, + "forceUpdateTag": { + "type": "string", + "defaultValue": "[utcNow()]", + "metadata": { + "description": "Forces the recipe deployment script to run when the template is redeployed." + } + }, + "tags": { + "type": "object", + "defaultValue": { + "finops-toolkit": "sre-agent", + "source": "microsoft-finops-toolkit" + }, + "metadata": { + "description": "Azure resource tags." + } + }, + "deployerPrincipalType": { + "type": "string", + "defaultValue": "User", + "allowedValues": [ + "User", + "ServicePrincipal" + ], + "metadata": { + "description": "Principal type of the deployer." + } + } + }, + "variables": { + "copy": [ + { + "name": "targetRgIds", + "count": "[length(variables('targetRgs'))]", + "input": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex('targetRgIds')])]" + } + ], + "rawTargetResourceGroups": "[split(replace(parameters('targetResourceGroupNames'), ' ', ''), ',')]", + "parsedTargetResourceGroups": "[filter(variables('rawTargetResourceGroups'), lambda('rgName', not(empty(lambdaVariables('rgName')))))]", + "targetRgs": "[union(createArray(parameters('resourceGroupName')), parameters('targetResourceGroups'), variables('parsedTargetResourceGroups'))]", + "agentResourceGroupId": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]", + "namingSeed": "[toLower(format('{0}|{1}|{2}', subscription().subscriptionId, variables('agentResourceGroupId'), parameters('agentName')))]", + "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7", + "hasKustoCluster": "[not(empty(parameters('finopsHubKustoClusterResourceId')))]", + "kustoClusterSubscriptionId": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[2], '')]", + "kustoClusterResourceGroupName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[4], '')]", + "kustoClusterName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2024-03-01", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableSubscriptionReaderRole')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(subscription().id, variables('namingSeed'), variables('readerRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('readerRoleId'))]", + "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "resources-deployment", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "agentName": { + "value": "[parameters('agentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "namingSeed": { + "value": "[variables('namingSeed')]" + }, + "targetResourceGroupIds": { + "value": "[variables('targetRgIds')]" + }, + "accessLevel": { + "value": "[parameters('accessLevel')]" + }, + "actionMode": { + "value": "[parameters('actionMode')]" + }, + "upgradeChannel": { + "value": "[parameters('upgradeChannel')]" + }, + "defaultModelProvider": { + "value": "[parameters('defaultModelProvider')]" + }, + "defaultModelName": { + "value": "[parameters('defaultModelName')]" + }, + "monthlyAgentUnitLimit": { + "value": "[parameters('monthlyAgentUnitLimit')]" + }, + "experimentalSettings": { + "value": "[parameters('experimentalSettings')]" + }, + "deployerPrincipalType": { + "value": "[parameters('deployerPrincipalType')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "400180081712866601" + } + }, + "parameters": { + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for all resources." + } + }, + "namingSeed": { + "type": "string", + "metadata": { + "description": "Deterministic naming seed built from subscription ID, agent resource group ID, and agent name." + } + }, + "targetResourceGroupIds": { + "type": "array", + "metadata": { + "description": "Resource group IDs shown as managed resources in the SRE Agent." + } + }, + "accessLevel": { + "type": "string", + "metadata": { + "description": "Agent access level." + } + }, + "actionMode": { + "type": "string", + "metadata": { + "description": "Agent action mode." + } + }, + "upgradeChannel": { + "type": "string", + "metadata": { + "description": "Agent upgrade channel." + } + }, + "defaultModelProvider": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model provider." + } + }, + "defaultModelName": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model name." + } + }, + "monthlyAgentUnitLimit": { + "type": "int", + "metadata": { + "description": "Monthly agent unit limit." + } + }, + "experimentalSettings": { + "type": "object", + "metadata": { + "description": "Agent experimental settings." + } + }, + "deployerPrincipalType": { + "type": "string", + "defaultValue": "User", + "allowedValues": [ + "User", + "ServicePrincipal" + ], + "metadata": { + "description": "Principal type of the deployer." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Azure resource tags." + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(parameters('namingSeed'))]", + "logAnalyticsName": "[format('law-{0}', variables('uniqueSuffix'))]", + "appInsightsName": "[format('appi-{0}', variables('uniqueSuffix'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitoring", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "logAnalyticsName": { + "value": "[variables('logAnalyticsName')]" + }, + "appInsightsName": { + "value": "[variables('appInsightsName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "9164683275614055037" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for resources" + } + }, + "logAnalyticsName": { + "type": "string", + "metadata": { + "description": "Log Analytics Workspace name" + } + }, + "appInsightsName": { + "type": "string", + "metadata": { + "description": "Application Insights name" + } + } + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[parameters('logAnalyticsName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30 + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[parameters('appInsightsName')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "Request_Source": "IbizaAIExtension", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]" + ] + } + ], + "outputs": { + "logAnalyticsWorkspaceId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]" + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "value": "[parameters('logAnalyticsName')]" + }, + "appInsightsId": { + "type": "string", + "value": "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]" + }, + "appInsightsAppId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').AppId]" + }, + "appInsightsConnectionString": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').ConnectionString]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "sre-agent", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "agentName": { + "value": "[parameters('agentName')]" + }, + "appInsightsAppId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsAppId.value]" + }, + "appInsightsConnectionString": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" + }, + "appInsightsId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsId.value]" + }, + "managedResourceGroupIds": { + "value": "[parameters('targetResourceGroupIds')]" + }, + "accessLevel": { + "value": "[parameters('accessLevel')]" + }, + "actionMode": { + "value": "[parameters('actionMode')]" + }, + "upgradeChannel": { + "value": "[parameters('upgradeChannel')]" + }, + "defaultModelProvider": { + "value": "[parameters('defaultModelProvider')]" + }, + "defaultModelName": { + "value": "[parameters('defaultModelName')]" + }, + "monthlyAgentUnitLimit": { + "value": "[parameters('monthlyAgentUnitLimit')]" + }, + "experimentalSettings": { + "value": "[parameters('experimentalSettings')]" + }, + "deployerPrincipalType": { + "value": "[parameters('deployerPrincipalType')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "2881433815087416423" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for resources." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "appInsightsAppId": { + "type": "string", + "metadata": { + "description": "Application Insights App ID." + } + }, + "appInsightsConnectionString": { + "type": "securestring", + "metadata": { + "description": "Application Insights connection string." + } + }, + "appInsightsId": { + "type": "string", + "metadata": { + "description": "Application Insights resource ID." + } + }, + "managedResourceGroupIds": { + "type": "array", + "metadata": { + "description": "Resource group IDs to add as managed resources." + } + }, + "accessLevel": { + "type": "string", + "metadata": { + "description": "Agent access level." + } + }, + "actionMode": { + "type": "string", + "metadata": { + "description": "Agent action mode." + } + }, + "upgradeChannel": { + "type": "string", + "metadata": { + "description": "Agent upgrade channel." + } + }, + "defaultModelProvider": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model provider." + } + }, + "defaultModelName": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model name." + } + }, + "monthlyAgentUnitLimit": { + "type": "int", + "metadata": { + "description": "Monthly agent unit limit." + } + }, + "experimentalSettings": { + "type": "object", + "metadata": { + "description": "Agent experimental settings." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Azure resource tags." + } + }, + "deployerPrincipalType": { + "type": "string", + "defaultValue": "User", + "allowedValues": [ + "User", + "ServicePrincipal" + ], + "metadata": { + "description": "Principal type of the deployer." + } + } + }, + "variables": { + "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55" + }, + "resources": [ + { + "type": "Microsoft.App/agents", + "apiVersion": "2026-01-01", + "name": "[parameters('agentName')]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), createObject('hidden-link: /app-insights-resource-id', parameters('appInsightsId'), 'source', 'microsoft-sre-agent-starter-lab', 'finops-toolkit', 'sre-agent'))]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "knowledgeGraphConfiguration": { + "managedResources": "[parameters('managedResourceGroupIds')]", + "identity": "system" + }, + "actionConfiguration": { + "mode": "[parameters('actionMode')]", + "identity": "system", + "accessLevel": "[parameters('accessLevel')]" + }, + "upgradeChannel": "[parameters('upgradeChannel')]", + "defaultModel": { + "provider": "[parameters('defaultModelProvider')]", + "name": "[parameters('defaultModelName')]" + }, + "monthlyAgentUnitLimit": "[parameters('monthlyAgentUnitLimit')]", + "experimentalSettings": "[parameters('experimentalSettings')]", + "mcpServers": [], + "logConfiguration": { + "applicationInsightsConfiguration": { + "appId": "[parameters('appInsightsAppId')]", + "connectionString": "[parameters('appInsightsConnectionString')]" + } + } + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]", + "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), deployer().objectId, variables('sreAgentAdminRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]", + "principalId": "[deployer().objectId]", + "principalType": "[parameters('deployerPrincipalType')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/agents', parameters('agentName'))]" + ] + } + ], + "outputs": { + "agentName": { + "type": "string", + "value": "[parameters('agentName')]" + }, + "agentId": { + "type": "string", + "value": "[resourceId('Microsoft.App/agents', parameters('agentName'))]" + }, + "agentEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01').agentEndpoint]" + }, + "agentPortalUrl": { + "type": "string", + "value": "[format('https://sre.azure.com/#/agent/{0}/{1}/{2}', subscription().subscriptionId, resourceGroup().name, parameters('agentName'))]" + }, + "agentPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.principalId]" + }, + "agentTenantId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.tenantId]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" + ] + } + ], + "outputs": { + "agentName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentName.value]" + }, + "agentEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentEndpoint.value]" + }, + "agentPortalUrl": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPortalUrl.value]" + }, + "agentPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "agentTenantId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentTenantId.value]" + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]" + ] + }, + { + "copy": { + "name": "targetRbac", + "count": "[length(variables('targetRgs'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('target-rbac-{0}', uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex()])), variables('namingSeed')))]", + "resourceGroup": "[variables('targetRgs')[copyIndex()]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "accessLevel": { + "value": "[parameters('accessLevel')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "6305641259472431615" + } + }, + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the managed identity to assign roles to." + } + }, + "accessLevel": { + "type": "string", + "allowedValues": [ + "Low", + "High" + ], + "metadata": { + "description": "Agent access level." + } + } + }, + "variables": { + "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7", + "monitoringReaderRoleId": "43d0d8ad-25c7-4714-9337-8ba259a9fe05", + "logAnalyticsReaderRoleId": "73c42c96-874c-492b-b04d-ab87d138a893", + "contributorRoleId": "b24988ac-6180-42a0-ab88-20f7382dd24c", + "roleIds": "[if(equals(parameters('accessLevel'), 'High'), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId'), variables('contributorRoleId')), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId')))]" + }, + "resources": [ + { + "copy": { + "name": "roleAssignments", + "count": "[length(variables('roleIds'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, parameters('principalId'), variables('roleIds')[copyIndex()])]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleIds')[copyIndex()])]", + "principalId": "[parameters('principalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]" + ] + }, + { + "condition": "[variables('hasKustoCluster')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('kusto-rbac-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed')))]", + "subscriptionId": "[variables('kustoClusterSubscriptionId')]", + "resourceGroup": "[variables('kustoClusterResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "clusterName": { + "value": "[variables('kustoClusterName')]" + }, + "principalApplicationId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "principalTenantId": { + "value": "[tenant().tenantId]" + }, + "principalAssignmentName": { + "value": "[format('sre-agent-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed'), 'all-db-viewer'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "15341408879505554256" + } + }, + "parameters": { + "clusterName": { + "type": "string", + "metadata": { + "description": "Kusto cluster name." + } + }, + "principalApplicationId": { + "type": "string", + "metadata": { + "description": "Microsoft Entra application/client ID to assign to the Kusto cluster." + } + }, + "principalTenantId": { + "type": "string", + "metadata": { + "description": "Principal tenant ID." + } + }, + "principalAssignmentName": { + "type": "string", + "metadata": { + "description": "Stable principal assignment name." + } + } + }, + "resources": [ + { + "type": "Microsoft.Kusto/clusters/principalAssignments", + "apiVersion": "2024-04-13", + "name": "[format('{0}/{1}', parameters('clusterName'), parameters('principalAssignmentName'))]", + "properties": { + "principalId": "[parameters('principalApplicationId')]", + "principalType": "App", + "role": "AllDatabasesViewer", + "tenantId": "[parameters('principalTenantId')]" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "apply-extras", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "agentName": { + "value": "[parameters('agentName')]" + }, + "agentEndpoint": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]" + }, + "subscriptionId": { + "value": "[subscription().subscriptionId]" + }, + "recipePackageUri": { + "value": "[parameters('recipePackageUri')]" + }, + "recipePackageSha256": { + "value": "[parameters('recipePackageSha256')]" + }, + "kustoConnectorUri": { + "value": "[parameters('finopsHubKustoConnectorUri')]" + }, + "forceUpdateTag": { + "value": "[parameters('forceUpdateTag')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "5025769957758856658" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for the deployment script resources." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "agentEndpoint": { + "type": "string", + "metadata": { + "description": "SRE Agent data-plane endpoint." + } + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription that contains the SRE Agent." + } + }, + "recipePackageUri": { + "type": "string", + "metadata": { + "description": "Public URI for the generated SRE Agent recipe package." + } + }, + "recipePackageSha256": { + "type": "string", + "metadata": { + "description": "SHA256 hash of the recipe package for integrity verification." + } + }, + "kustoConnectorUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional database-qualified Kusto connector URI." + } + }, + "forceUpdateTag": { + "type": "string", + "metadata": { + "description": "Forces the deployment script to run when the template is redeployed." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Azure resource tags." + } + } + }, + "variables": { + "$fxv#0": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\n$ErrorActionPreference = 'Stop'\r\n\r\nfunction Get-RequiredEnv($Name) {\r\n $value = [Environment]::GetEnvironmentVariable($Name)\r\n if ([string]::IsNullOrWhiteSpace($value)) {\r\n throw \"Missing required environment variable: $Name\"\r\n }\r\n return $value\r\n}\r\n\r\nfunction Get-OptionalEnv($Name) {\r\n return [Environment]::GetEnvironmentVariable($Name)\r\n}\r\n\r\nfunction Get-TempRoot() {\r\n $tempRoot = [System.IO.Path]::GetTempPath()\r\n if ([string]::IsNullOrWhiteSpace($tempRoot)) {\r\n $tempRoot = Get-OptionalEnv 'AZ_SCRIPTS_PATH_OUTPUT_DIRECTORY'\r\n }\r\n if ([string]::IsNullOrWhiteSpace($tempRoot)) {\r\n $tempRoot = (Get-Location).Path\r\n }\r\n return $tempRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)\r\n}\r\n\r\nfunction ConvertTo-BodyJson($Value) {\r\n return ($Value | ConvertTo-Json -Depth 100 -Compress)\r\n}\r\n\r\nfunction Get-PropertyValue($Object, [string[]]$Names, $Default = '') {\r\n foreach ($name in $Names) {\r\n if ($null -ne $Object -and $Object.PSObject.Properties[$name] -and $null -ne $Object.$name) {\r\n return $Object.$name\r\n }\r\n }\r\n return $Default\r\n}\r\n\r\nfunction Get-HttpStatusCode($Exception) {\r\n if ($Exception.Response -and $Exception.Response.StatusCode) {\r\n return [int]$Exception.Response.StatusCode\r\n }\r\n return $null\r\n}\r\n\r\nfunction Invoke-WithRetry([scriptblock]$Action, [string]$Label, [int]$MaxAttempts = 5, [int]$DelaySeconds = 15) {\r\n for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {\r\n try {\r\n return & $Action\r\n }\r\n catch {\r\n $statusCode = Get-HttpStatusCode $_.Exception\r\n if ($statusCode -and $statusCode -notin @(401, 403, 408, 429, 500, 502, 503, 504)) {\r\n throw\r\n }\r\n if ($attempt -eq $MaxAttempts) {\r\n throw\r\n }\r\n Write-Output \"$Label attempt $attempt/$MaxAttempts failed: $($_.Exception.Message)\"\r\n Start-Sleep -Seconds $DelaySeconds\r\n }\r\n }\r\n}\r\n\r\nfunction Invoke-JsonRest([string]$Method, [string]$Uri, [string]$Token, $Body = $null) {\r\n $headers = @{\r\n Authorization = \"Bearer $Token\"\r\n 'Content-Type' = 'application/json'\r\n }\r\n $parameters = @{\r\n Method = $Method\r\n Uri = $Uri\r\n Headers = $headers\r\n }\r\n if ($null -ne $Body) {\r\n $parameters.Body = ConvertTo-BodyJson $Body\r\n }\r\n return Invoke-RestMethod @parameters\r\n}\r\n\r\nfunction Get-KnowledgeSourceName([string]$FileName) {\r\n $name = [IO.Path]::GetFileName($FileName).ToLowerInvariant() -replace '[^a-z0-9-]+', '-'\r\n $name = $name -replace '-+', '-'\r\n return $name.Trim('-')\r\n}\r\n\r\nfunction Get-Collection($Value) {\r\n if ($null -eq $Value) {\r\n return @()\r\n }\r\n return @($Value)\r\n}\r\n\r\nfunction Convert-TokenToString($Token) {\r\n if ($Token -is [Security.SecureString]) {\r\n $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token)\r\n try {\r\n return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)\r\n }\r\n finally {\r\n [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)\r\n }\r\n }\r\n return [string]$Token\r\n}\r\n\r\nfunction Get-ArmAccessTokenString() {\r\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://management.azure.com/').Token\r\n}\r\n\r\nfunction Get-SreAccessTokenString() {\r\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://azuresre.dev').Token\r\n}\r\n\r\n$subscriptionId = Get-RequiredEnv 'subscriptionId'\r\n$resourceGroupName = Get-RequiredEnv 'resourceGroupName'\r\n$agentName = Get-RequiredEnv 'agentName'\r\n$agentEndpoint = (Get-RequiredEnv 'agentEndpoint').TrimEnd('/')\r\n$recipePackageUri = Get-RequiredEnv 'recipePackageUri'\r\n$recipePackageSha256 = Get-RequiredEnv 'recipePackageSha256'\r\n$kustoConnectorUri = Get-OptionalEnv 'kustoConnectorUri'\r\n$armApiVersion = '2025-05-01-preview'\r\n$armBase = \"https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.App/agents/$agentName\"\r\n\r\nWrite-Output 'Connecting with deployment script managed identity...'\r\nConnect-AzAccount -Identity | Out-Null\r\nSet-AzContext -Subscription $subscriptionId | Out-Null\r\n\r\n$tempRoot = Get-TempRoot\r\n$workRoot = Join-Path $tempRoot 'sre-agent-recipe'\r\n$zipPath = Join-Path $tempRoot 'sre-agent-recipe.zip'\r\nRemove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue\r\nNew-Item -Path $workRoot -ItemType Directory -Force | Out-Null\r\n\r\nWrite-Output \"Validating SRE Agent recipe package URI: $recipePackageUri\"\r\n[uri]$parsedUri = $recipePackageUri\r\nif ($parsedUri.Scheme -ne 'https') {\r\n throw \"Recipe package URI must use https scheme, got: $($parsedUri.Scheme)\"\r\n}\r\n\r\n$allowedHosts = @(\r\n 'raw.githubusercontent.com',\r\n 'github.com',\r\n 'aka.ms'\r\n)\r\n\r\nif ($allowedHosts -notcontains $parsedUri.Host -and $parsedUri.Host -notlike '*.blob.core.windows.net') {\r\n throw \"Recipe package host '$($parsedUri.Host)' is not in the allowlist. Allowed: raw.githubusercontent.com, github.com, aka.ms, *.blob.core.windows.net\"\r\n}\r\n\r\nWrite-Output \"Downloading SRE Agent recipe package: $recipePackageUri\"\r\nInvoke-WithRetry -Label 'download recipe package' -Action {\r\n Invoke-WebRequest -Uri $recipePackageUri -OutFile $zipPath\r\n} | Out-Null\r\n\r\nWrite-Output \"Verifying recipe package integrity...\"\r\n$actualHash = (Get-FileHash -Algorithm SHA256 -Path $zipPath).Hash\r\nif ($actualHash -ne $recipePackageSha256) {\r\n throw \"Recipe package hash mismatch: expected $recipePackageSha256 got $actualHash\"\r\n}\r\n\r\nExpand-Archive -Path $zipPath -DestinationPath $workRoot -Force\r\n\r\n$extrasPath = Join-Path $workRoot 'extras.json'\r\nif (-not (Test-Path $extrasPath)) {\r\n throw \"Recipe package did not contain extras.json\"\r\n}\r\n\r\n$extras = Get-Content $extrasPath -Raw | ConvertFrom-Json -Depth 100\r\nWrite-Output \"Loaded recipe package from $extrasPath\"\r\n\r\nif ([string]::IsNullOrWhiteSpace($kustoConnectorUri)) {\r\n $extras.connectors = @(\r\n Get-Collection $extras.connectors | Where-Object {\r\n $type = Get-PropertyValue $_.properties @('dataConnectorType', 'type')\r\n $type -ine 'Kusto'\r\n }\r\n )\r\n}\r\nelse {\r\n foreach ($connector in Get-Collection $extras.connectors) {\r\n $type = Get-PropertyValue $connector.properties @('dataConnectorType', 'type')\r\n if ($type -ieq 'Kusto') {\r\n $connector.properties.dataSource = $kustoConnectorUri\r\n if (-not $connector.properties.PSObject.Properties['identity']) {\r\n $connector.properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\r\n }\r\n }\r\n }\r\n}\r\n\r\nfunction Invoke-ArmPutConnector([string]$Name, $Body) {\r\n $uri = \"$armBase/connectors/$Name`?api-version=$armApiVersion\"\r\n Invoke-WithRetry -Label \"ARM PUT connector $Name\" -Action {\r\n Invoke-JsonRest -Method PUT -Uri $uri -Token (Get-ArmAccessTokenString) -Body $Body\r\n } | Out-Null\r\n Write-Output \" ARM PUT connectors/${Name}: ok\"\r\n}\r\n\r\nfunction Invoke-SreApi([string]$Method, [string]$Path, $Body = $null) {\r\n $uri = \"$agentEndpoint$Path\"\r\n return Invoke-WithRetry -Label \"$Method $Path\" -MaxAttempts 21 -DelaySeconds 30 -Action {\r\n Invoke-JsonRest -Method $Method -Uri $uri -Token (Get-SreAccessTokenString) -Body $Body\r\n }\r\n}\r\n\r\nfunction Invoke-ExtendedPut([string]$Kind, [string]$Name, [string]$Type, $Properties) {\r\n $encodedName = [Uri]::EscapeDataString($Name)\r\n $body = @{\r\n name = $Name\r\n type = $Type\r\n tags = @()\r\n properties = $Properties\r\n }\r\n Invoke-SreApi -Method PUT -Path \"/api/v2/extendedAgent/$Kind/$encodedName\" -Body $body | Out-Null\r\n Write-Output \" PUT $Kind/${Name}: ok\"\r\n}\r\n\r\nWrite-Output 'Step 1/7: Applying connectors...'\r\nforeach ($connector in Get-Collection $extras.connectors) {\r\n $name = [string]$connector.name\r\n $properties = $connector.properties\r\n if (-not $properties.PSObject.Properties['identity']) {\r\n $properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\r\n }\r\n Invoke-ArmPutConnector -Name $name -Body @{ properties = $properties }\r\n}\r\n\r\nWrite-Output 'Step 2/7: Configuring built-in tools...'\r\n$overrides = Get-Collection $extras.builtInTools.overrides\r\nif ($overrides.Count -gt 0) {\r\n $body = @{\r\n overrides = @($overrides | ForEach-Object {\r\n @{\r\n name = $_.name\r\n enabled = [bool]$_.enabled\r\n }\r\n })\r\n }\r\n Invoke-SreApi -Method POST -Path '/api/v2/agent/tools/configure' -Body $body | Out-Null\r\n Write-Output \" built-in tools configured: $($overrides.Count) overrides\"\r\n}\r\n\r\nWrite-Output 'Step 3/7: Uploading knowledge sources...'\r\nforeach ($item in Get-Collection $extras.knowledgeItems) {\r\n $sourceName = Get-KnowledgeSourceName ([string]$item.name)\r\n $bytes = [Text.Encoding]::UTF8.GetBytes([string]$item.content)\r\n $body = @{\r\n properties = @{\r\n dataConnectorType = 'KnowledgeFile'\r\n dataSource = $sourceName\r\n extendedProperties = @{\r\n displayName = [string]$item.name\r\n fileName = [string]$item.name\r\n fileContent = [Convert]::ToBase64String($bytes)\r\n contentType = [string](Get-PropertyValue $item @('contentType') 'application/octet-stream')\r\n }\r\n }\r\n }\r\n Invoke-ArmPutConnector -Name $sourceName -Body $body\r\n Start-Sleep -Seconds 5\r\n}\r\n\r\nWrite-Output 'Step 4/7: Applying tools...'\r\nforeach ($tool in Get-Collection $extras.tools) {\r\n $name = [string]$tool.metadata.name\r\n $properties = $tool.spec\r\n if ($properties.type -eq 'PythonTool') {\r\n $properties.type = 'PythonFunctionTool'\r\n }\r\n Invoke-ExtendedPut -Kind 'tools' -Name $name -Type 'ExtendedAgentTool' -Properties $properties\r\n}\r\n\r\nWrite-Output 'Step 5/7: Applying skills...'\r\nforeach ($skill in Get-Collection $extras.skills) {\r\n $name = [string]$skill.metadata.name\r\n $tools = @()\r\n if ($skill.metadata.spec -and $skill.metadata.spec.tools) {\r\n $tools = Get-Collection $skill.metadata.spec.tools\r\n }\r\n $properties = @{\r\n name = $name\r\n description = [string](Get-PropertyValue $skill.metadata @('description'))\r\n tools = $tools\r\n skillContent = [string](Get-PropertyValue $skill @('skillContent'))\r\n additionalFiles = @()\r\n }\r\n Invoke-ExtendedPut -Kind 'skills' -Name $name -Type 'Skill' -Properties $properties\r\n}\r\n\r\nWrite-Output 'Step 6/7: Applying subagents...'\r\nforeach ($subagent in Get-Collection $extras.subagents) {\r\n $name = [string]$subagent.metadata.name\r\n Invoke-ExtendedPut -Kind 'agents' -Name $name -Type 'ExtendedAgent' -Properties $subagent.spec\r\n}\r\n\r\nWrite-Output 'Step 7/7: Applying scheduled tasks...'\r\n$scheduledTasks = Get-Collection $extras.scheduledTasks\r\nif ($scheduledTasks.Count -gt 0) {\r\n $existing = Invoke-SreApi -Method GET -Path '/api/v1/scheduledtasks'\r\n foreach ($task in $scheduledTasks) {\r\n $name = [string]$task.metadata.name\r\n foreach ($match in (Get-Collection $existing | Where-Object { $_.name -eq $name })) {\r\n if ($match.id) {\r\n Invoke-SreApi -Method DELETE -Path \"/api/v1/scheduledtasks/$($match.id)\" | Out-Null\r\n Write-Output \" deleted existing scheduledtasks/$name\"\r\n }\r\n }\r\n }\r\n\r\n foreach ($task in $scheduledTasks) {\r\n $name = [string]$task.metadata.name\r\n $spec = $task.spec\r\n $properties = @{\r\n name = [string](Get-PropertyValue $spec @('name') $name)\r\n description = [string](Get-PropertyValue $spec @('description'))\r\n cronExpression = [string](Get-PropertyValue $spec @('schedule', 'cronExpression', 'cron_expression'))\r\n agentPrompt = [string](Get-PropertyValue $spec @('prompt', 'agentPrompt', 'agent_prompt'))\r\n agentMode = [string](Get-PropertyValue $spec @('mode', 'agentMode', 'agent_mode') 'Review')\r\n isEnabled = [bool](Get-PropertyValue $spec @('enabled') $true)\r\n agent = [string](Get-PropertyValue $spec @('agent'))\r\n }\r\n Invoke-ExtendedPut -Kind 'scheduledtasks' -Name $name -Type 'ScheduledTask' -Properties $properties\r\n }\r\n}\r\n\r\nWrite-Output 'SRE Agent extras complete.'\r\n", + "identityName": "[format('id-sre-apply-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]", + "scriptName": "[format('apply-sre-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]", + "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55" + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[variables('identityName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]", + "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2023-08-01", + "name": "[variables('scriptName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "AzurePowerShell", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')))]": {} + } + }, + "properties": { + "azPowerShellVersion": "11.0", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "timeout": "PT2H", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "scriptContent": "[variables('$fxv#0')]", + "environmentVariables": [ + { + "name": "subscriptionId", + "value": "[parameters('subscriptionId')]" + }, + { + "name": "resourceGroupName", + "value": "[resourceGroup().name]" + }, + { + "name": "agentName", + "value": "[parameters('agentName')]" + }, + { + "name": "agentEndpoint", + "value": "[parameters('agentEndpoint')]" + }, + { + "name": "recipePackageUri", + "value": "[parameters('recipePackageUri')]" + }, + { + "name": "recipePackageSha256", + "value": "[parameters('recipePackageSha256')]" + }, + { + "name": "kustoConnectorUri", + "value": "[parameters('kustoConnectorUri')]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]", + "[extensionResourceId(resourceId('Microsoft.App/agents', parameters('agentName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId')))]" + ] + } + ], + "outputs": { + "identityName": { + "type": "string", + "value": "[variables('identityName')]" + }, + "scriptName": { + "type": "string", + "value": "[variables('scriptName')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]", + "targetRbac" + ] + } + ], + "outputs": { + "AZURE_RESOURCE_GROUP": { + "type": "string", + "value": "[parameters('resourceGroupName')]" + }, + "AZURE_LOCATION": { + "type": "string", + "value": "[parameters('location')]" + }, + "SRE_AGENT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentName.value]" + }, + "SRE_AGENT_ENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]" + }, + "AGENT_PORTAL_URL": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPortalUrl.value]" + }, + "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "SYSTEM_MANAGED_IDENTITY_TENANT_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentTenantId.value]" + }, + "LOG_ANALYTICS_WORKSPACE_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" + }, + "APPLY_EXTRAS_SCRIPT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'apply-extras'), '2025-04-01').outputs.scriptName.value]" + } + } +} \ No newline at end of file diff --git a/docs/deploy/sre-agent/14.0/createUiDefinition.json b/docs/deploy/sre-agent/14.0/createUiDefinition.json new file mode 100644 index 000000000..3b17beb78 --- /dev/null +++ b/docs/deploy/sre-agent/14.0/createUiDefinition.json @@ -0,0 +1,169 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "basics": { + "description": "Deploy Azure SRE Agent with the FinOps toolkit recipe for FinOps hub analysis, capacity monitoring, scheduled tasks, and specialist agents.", + "location": { + "label": "Location", + "resourceTypes": [ + "Microsoft.App/agents", + "Microsoft.Insights/components", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.OperationalInsights/workspaces", + "Microsoft.Resources/deploymentScripts" + ] + } + } + }, + "resourceTypes": [ + "Microsoft.App/agents", + "Microsoft.Insights/components", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.OperationalInsights/workspaces", + "Microsoft.Resources/deploymentScripts" + ], + "basics": [ + { + "name": "resourceGroupName", + "type": "Microsoft.Common.TextBox", + "label": "Agent resource group", + "defaultValue": "finops-hub-sre", + "toolTip": "Resource group that will contain the Azure SRE Agent, monitoring resources, and deployment script identity.", + "constraints": { + "required": true, + "regex": "^[a-zA-Z0-9._()\\-]{1,90}$", + "validationMessage": "Enter a valid Azure resource group name." + }, + "visible": true + }, + { + "name": "agentName", + "type": "Microsoft.Common.TextBox", + "label": "Agent name", + "defaultValue": "finops-hub-sre", + "toolTip": "Name of the Azure SRE Agent resource.", + "constraints": { + "required": true, + "regex": "^[a-zA-Z0-9][a-zA-Z0-9\\-]{1,61}[a-zA-Z0-9]$", + "validationMessage": "Name must be 3-63 characters and can contain letters, numbers, and hyphens. The first and last characters must be alphanumeric." + }, + "visible": true + } + ], + "steps": [ + { + "name": "configuration", + "label": "Configuration", + "elements": [ + { + "name": "finopsHubKustoConnectorUri", + "type": "Microsoft.Common.TextBox", + "label": "FinOps hub Kusto URI", + "defaultValue": "", + "toolTip": "Optional database-qualified Azure Data Explorer URI for the FinOps hub. Example: https://cluster.region.kusto.windows.net/Hub", + "constraints": { + "required": false, + "regex": "^$|^https://.+\\.kusto\\.windows\\.net/.+", + "validationMessage": "Enter a database-qualified Kusto URI such as https://cluster.region.kusto.windows.net/Hub, or leave blank." + }, + "visible": true + }, + { + "name": "finopsHubKustoClusterResourceId", + "type": "Microsoft.Common.TextBox", + "label": "FinOps hub Kusto cluster resource ID", + "defaultValue": "", + "toolTip": "Optional Azure Data Explorer cluster resource ID. Provide this to assign the agent managed identity AllDatabasesViewer on the cluster.", + "constraints": { + "required": false, + "regex": "^$|^/subscriptions/.+/resourceGroups/.+/providers/Microsoft\\.Kusto/clusters/.+", + "validationMessage": "Enter a valid Microsoft.Kusto/clusters resource ID, or leave blank." + }, + "visible": true + }, + { + "name": "targetResourceGroups", + "type": "Microsoft.Common.TextBox", + "label": "Additional target resource groups", + "defaultValue": "", + "toolTip": "Optional comma-separated resource group names the agent can observe or act on. The agent resource group is always included.", + "constraints": { + "required": false, + "regex": "^$|^[A-Za-z0-9_.()\\-]+(\\s*,\\s*[A-Za-z0-9_.()\\-]+)*$", + "validationMessage": "Enter a comma-separated list of resource group names without blank entries or trailing commas." + }, + "visible": true + }, + { + "name": "accessLevel", + "type": "Microsoft.Common.OptionsGroup", + "label": "Access level", + "defaultValue": "Low", + "toolTip": "Low grants read-only monitoring roles (recommended for reporting and analysis). High adds Contributor on target resource groups for autonomous remediation workflows.", + "constraints": { + "allowedValues": [ + { + "label": "High", + "value": "High" + }, + { + "label": "Low", + "value": "Low" + } + ], + "required": true + }, + "visible": true + }, + { + "name": "actionMode", + "type": "Microsoft.Common.OptionsGroup", + "label": "Action mode", + "defaultValue": "review", + "toolTip": "Review requires approval before write actions. Autonomous lets the agent run enabled actions within its assigned access.", + "constraints": { + "allowedValues": [ + { + "label": "Autonomous", + "value": "autonomous" + }, + { + "label": "Review", + "value": "review" + }, + { + "label": "Read only", + "value": "readOnly" + } + ], + "required": true + }, + "visible": true + }, + { + "name": "enableSubscriptionReaderRole", + "type": "Microsoft.Common.CheckBox", + "label": "Grant subscription Reader to the agent", + "toolTip": "Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.", + "defaultValue": false, + "visible": true + } + ] + } + ], + "outputs": { + "resourceGroupName": "[basics('resourceGroupName')]", + "agentName": "[basics('agentName')]", + "location": "[location()]", + "targetResourceGroupNames": "[steps('configuration').targetResourceGroups]", + "finopsHubKustoConnectorUri": "[steps('configuration').finopsHubKustoConnectorUri]", + "finopsHubKustoClusterResourceId": "[steps('configuration').finopsHubKustoClusterResourceId]", + "accessLevel": "[steps('configuration').accessLevel]", + "actionMode": "[steps('configuration').actionMode]", + "enableSubscriptionReaderRole": "[steps('configuration').enableSubscriptionReaderRole]" + } + } +} diff --git a/docs/deploy/sre-agent/latest/azuredeploy.json b/docs/deploy/sre-agent/latest/azuredeploy.json new file mode 100644 index 000000000..62832fff6 --- /dev/null +++ b/docs/deploy/sre-agent/latest/azuredeploy.json @@ -0,0 +1,1178 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "9242331632702473420" + } + }, + "parameters": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource group that holds the SRE Agent resources." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "location": { + "type": "string", + "defaultValue": "eastus2", + "allowedValues": [ + "australiaeast", + "canadacentral", + "eastus2", + "francecentral", + "koreacentral", + "swedencentral", + "uksouth" + ], + "metadata": { + "description": "Primary location for all resources." + } + }, + "targetResourceGroups": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Resource groups the agent can observe or act on. The agent resource group is always included." + } + }, + "targetResourceGroupNames": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Comma-separated resource groups the agent can observe or act on. Used by the Azure portal form." + } + }, + "finopsHubKustoConnectorUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional database-qualified FinOps Hub Kusto connector URI. Example: https://..kusto.windows.net/Hub" + } + }, + "finopsHubKustoClusterResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment." + } + }, + "accessLevel": { + "type": "string", + "defaultValue": "Low", + "allowedValues": [ + "Low", + "High" + ], + "metadata": { + "description": "Agent access level. Low (read-only) is recommended for reporting and analysis without modification risk." + } + }, + "actionMode": { + "type": "string", + "defaultValue": "review", + "allowedValues": [ + "review", + "autonomous", + "readOnly" + ], + "metadata": { + "description": "Agent action mode." + } + }, + "upgradeChannel": { + "type": "string", + "defaultValue": "Preview", + "allowedValues": [ + "Stable", + "Preview" + ], + "metadata": { + "description": "Agent upgrade channel." + } + }, + "defaultModelProvider": { + "type": "string", + "defaultValue": "MicrosoftFoundry", + "allowedValues": [ + "MicrosoftFoundry", + "Anthropic" + ], + "metadata": { + "description": "Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal." + } + }, + "defaultModelName": { + "type": "string", + "defaultValue": "Automatic", + "metadata": { + "description": "Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider." + } + }, + "monthlyAgentUnitLimit": { + "type": "int", + "defaultValue": 10000, + "minValue": 1, + "metadata": { + "description": "Monthly agent unit limit." + } + }, + "experimentalSettings": { + "type": "object", + "defaultValue": { + "EnableSandboxGroup": true, + "EnableWorkspaceTools": true + }, + "metadata": { + "description": "Agent experimental settings." + } + }, + "enableSubscriptionReaderRole": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Assign Reader on the deployment subscription to the agent managed identity. Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups." + } + }, + "recipePackageUri": { + "type": "string", + "defaultValue": "[uri(deployment().properties.templateLink.uri, 'sre-agent-recipe.zip')]", + "metadata": { + "description": "Public URI for the generated SRE Agent recipe package. Deploy-to-Azure links derive this from the template URI." + } + }, + "recipePackageSha256": { + "type": "string", + "defaultValue": "bc87901a7090d36feb7d80664d57fc9bfa09056eed316fbf5458855b9de97662", + "metadata": { + "description": "SHA256 hash of the recipe package for integrity verification." + } + }, + "forceUpdateTag": { + "type": "string", + "defaultValue": "[utcNow()]", + "metadata": { + "description": "Forces the recipe deployment script to run when the template is redeployed." + } + }, + "tags": { + "type": "object", + "defaultValue": { + "finops-toolkit": "sre-agent", + "source": "microsoft-finops-toolkit" + }, + "metadata": { + "description": "Azure resource tags." + } + }, + "deployerPrincipalType": { + "type": "string", + "defaultValue": "User", + "allowedValues": [ + "User", + "ServicePrincipal" + ], + "metadata": { + "description": "Principal type of the deployer." + } + } + }, + "variables": { + "copy": [ + { + "name": "targetRgIds", + "count": "[length(variables('targetRgs'))]", + "input": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex('targetRgIds')])]" + } + ], + "rawTargetResourceGroups": "[split(replace(parameters('targetResourceGroupNames'), ' ', ''), ',')]", + "parsedTargetResourceGroups": "[filter(variables('rawTargetResourceGroups'), lambda('rgName', not(empty(lambdaVariables('rgName')))))]", + "targetRgs": "[union(createArray(parameters('resourceGroupName')), parameters('targetResourceGroups'), variables('parsedTargetResourceGroups'))]", + "agentResourceGroupId": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]", + "namingSeed": "[toLower(format('{0}|{1}|{2}', subscription().subscriptionId, variables('agentResourceGroupId'), parameters('agentName')))]", + "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7", + "hasKustoCluster": "[not(empty(parameters('finopsHubKustoClusterResourceId')))]", + "kustoClusterSubscriptionId": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[2], '')]", + "kustoClusterResourceGroupName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[4], '')]", + "kustoClusterName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2024-03-01", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableSubscriptionReaderRole')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(subscription().id, variables('namingSeed'), variables('readerRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('readerRoleId'))]", + "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "resources-deployment", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "agentName": { + "value": "[parameters('agentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "namingSeed": { + "value": "[variables('namingSeed')]" + }, + "targetResourceGroupIds": { + "value": "[variables('targetRgIds')]" + }, + "accessLevel": { + "value": "[parameters('accessLevel')]" + }, + "actionMode": { + "value": "[parameters('actionMode')]" + }, + "upgradeChannel": { + "value": "[parameters('upgradeChannel')]" + }, + "defaultModelProvider": { + "value": "[parameters('defaultModelProvider')]" + }, + "defaultModelName": { + "value": "[parameters('defaultModelName')]" + }, + "monthlyAgentUnitLimit": { + "value": "[parameters('monthlyAgentUnitLimit')]" + }, + "experimentalSettings": { + "value": "[parameters('experimentalSettings')]" + }, + "deployerPrincipalType": { + "value": "[parameters('deployerPrincipalType')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "400180081712866601" + } + }, + "parameters": { + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for all resources." + } + }, + "namingSeed": { + "type": "string", + "metadata": { + "description": "Deterministic naming seed built from subscription ID, agent resource group ID, and agent name." + } + }, + "targetResourceGroupIds": { + "type": "array", + "metadata": { + "description": "Resource group IDs shown as managed resources in the SRE Agent." + } + }, + "accessLevel": { + "type": "string", + "metadata": { + "description": "Agent access level." + } + }, + "actionMode": { + "type": "string", + "metadata": { + "description": "Agent action mode." + } + }, + "upgradeChannel": { + "type": "string", + "metadata": { + "description": "Agent upgrade channel." + } + }, + "defaultModelProvider": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model provider." + } + }, + "defaultModelName": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model name." + } + }, + "monthlyAgentUnitLimit": { + "type": "int", + "metadata": { + "description": "Monthly agent unit limit." + } + }, + "experimentalSettings": { + "type": "object", + "metadata": { + "description": "Agent experimental settings." + } + }, + "deployerPrincipalType": { + "type": "string", + "defaultValue": "User", + "allowedValues": [ + "User", + "ServicePrincipal" + ], + "metadata": { + "description": "Principal type of the deployer." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Azure resource tags." + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(parameters('namingSeed'))]", + "logAnalyticsName": "[format('law-{0}', variables('uniqueSuffix'))]", + "appInsightsName": "[format('appi-{0}', variables('uniqueSuffix'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitoring", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "logAnalyticsName": { + "value": "[variables('logAnalyticsName')]" + }, + "appInsightsName": { + "value": "[variables('appInsightsName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "9164683275614055037" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for resources" + } + }, + "logAnalyticsName": { + "type": "string", + "metadata": { + "description": "Log Analytics Workspace name" + } + }, + "appInsightsName": { + "type": "string", + "metadata": { + "description": "Application Insights name" + } + } + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[parameters('logAnalyticsName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30 + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[parameters('appInsightsName')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "Request_Source": "IbizaAIExtension", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]" + ] + } + ], + "outputs": { + "logAnalyticsWorkspaceId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]" + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "value": "[parameters('logAnalyticsName')]" + }, + "appInsightsId": { + "type": "string", + "value": "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]" + }, + "appInsightsAppId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').AppId]" + }, + "appInsightsConnectionString": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').ConnectionString]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "sre-agent", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "agentName": { + "value": "[parameters('agentName')]" + }, + "appInsightsAppId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsAppId.value]" + }, + "appInsightsConnectionString": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" + }, + "appInsightsId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsId.value]" + }, + "managedResourceGroupIds": { + "value": "[parameters('targetResourceGroupIds')]" + }, + "accessLevel": { + "value": "[parameters('accessLevel')]" + }, + "actionMode": { + "value": "[parameters('actionMode')]" + }, + "upgradeChannel": { + "value": "[parameters('upgradeChannel')]" + }, + "defaultModelProvider": { + "value": "[parameters('defaultModelProvider')]" + }, + "defaultModelName": { + "value": "[parameters('defaultModelName')]" + }, + "monthlyAgentUnitLimit": { + "value": "[parameters('monthlyAgentUnitLimit')]" + }, + "experimentalSettings": { + "value": "[parameters('experimentalSettings')]" + }, + "deployerPrincipalType": { + "value": "[parameters('deployerPrincipalType')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "2881433815087416423" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for resources." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "appInsightsAppId": { + "type": "string", + "metadata": { + "description": "Application Insights App ID." + } + }, + "appInsightsConnectionString": { + "type": "securestring", + "metadata": { + "description": "Application Insights connection string." + } + }, + "appInsightsId": { + "type": "string", + "metadata": { + "description": "Application Insights resource ID." + } + }, + "managedResourceGroupIds": { + "type": "array", + "metadata": { + "description": "Resource group IDs to add as managed resources." + } + }, + "accessLevel": { + "type": "string", + "metadata": { + "description": "Agent access level." + } + }, + "actionMode": { + "type": "string", + "metadata": { + "description": "Agent action mode." + } + }, + "upgradeChannel": { + "type": "string", + "metadata": { + "description": "Agent upgrade channel." + } + }, + "defaultModelProvider": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model provider." + } + }, + "defaultModelName": { + "type": "string", + "metadata": { + "description": "Default SRE Agent model name." + } + }, + "monthlyAgentUnitLimit": { + "type": "int", + "metadata": { + "description": "Monthly agent unit limit." + } + }, + "experimentalSettings": { + "type": "object", + "metadata": { + "description": "Agent experimental settings." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Azure resource tags." + } + }, + "deployerPrincipalType": { + "type": "string", + "defaultValue": "User", + "allowedValues": [ + "User", + "ServicePrincipal" + ], + "metadata": { + "description": "Principal type of the deployer." + } + } + }, + "variables": { + "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55" + }, + "resources": [ + { + "type": "Microsoft.App/agents", + "apiVersion": "2026-01-01", + "name": "[parameters('agentName')]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), createObject('hidden-link: /app-insights-resource-id', parameters('appInsightsId'), 'source', 'microsoft-sre-agent-starter-lab', 'finops-toolkit', 'sre-agent'))]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "knowledgeGraphConfiguration": { + "managedResources": "[parameters('managedResourceGroupIds')]", + "identity": "system" + }, + "actionConfiguration": { + "mode": "[parameters('actionMode')]", + "identity": "system", + "accessLevel": "[parameters('accessLevel')]" + }, + "upgradeChannel": "[parameters('upgradeChannel')]", + "defaultModel": { + "provider": "[parameters('defaultModelProvider')]", + "name": "[parameters('defaultModelName')]" + }, + "monthlyAgentUnitLimit": "[parameters('monthlyAgentUnitLimit')]", + "experimentalSettings": "[parameters('experimentalSettings')]", + "mcpServers": [], + "logConfiguration": { + "applicationInsightsConfiguration": { + "appId": "[parameters('appInsightsAppId')]", + "connectionString": "[parameters('appInsightsConnectionString')]" + } + } + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]", + "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), deployer().objectId, variables('sreAgentAdminRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]", + "principalId": "[deployer().objectId]", + "principalType": "[parameters('deployerPrincipalType')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/agents', parameters('agentName'))]" + ] + } + ], + "outputs": { + "agentName": { + "type": "string", + "value": "[parameters('agentName')]" + }, + "agentId": { + "type": "string", + "value": "[resourceId('Microsoft.App/agents', parameters('agentName'))]" + }, + "agentEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01').agentEndpoint]" + }, + "agentPortalUrl": { + "type": "string", + "value": "[format('https://sre.azure.com/#/agent/{0}/{1}/{2}', subscription().subscriptionId, resourceGroup().name, parameters('agentName'))]" + }, + "agentPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.principalId]" + }, + "agentTenantId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.tenantId]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" + ] + } + ], + "outputs": { + "agentName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentName.value]" + }, + "agentEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentEndpoint.value]" + }, + "agentPortalUrl": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPortalUrl.value]" + }, + "agentPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "agentTenantId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentTenantId.value]" + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]" + ] + }, + { + "copy": { + "name": "targetRbac", + "count": "[length(variables('targetRgs'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('target-rbac-{0}', uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex()])), variables('namingSeed')))]", + "resourceGroup": "[variables('targetRgs')[copyIndex()]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "accessLevel": { + "value": "[parameters('accessLevel')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "6305641259472431615" + } + }, + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the managed identity to assign roles to." + } + }, + "accessLevel": { + "type": "string", + "allowedValues": [ + "Low", + "High" + ], + "metadata": { + "description": "Agent access level." + } + } + }, + "variables": { + "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7", + "monitoringReaderRoleId": "43d0d8ad-25c7-4714-9337-8ba259a9fe05", + "logAnalyticsReaderRoleId": "73c42c96-874c-492b-b04d-ab87d138a893", + "contributorRoleId": "b24988ac-6180-42a0-ab88-20f7382dd24c", + "roleIds": "[if(equals(parameters('accessLevel'), 'High'), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId'), variables('contributorRoleId')), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId')))]" + }, + "resources": [ + { + "copy": { + "name": "roleAssignments", + "count": "[length(variables('roleIds'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, parameters('principalId'), variables('roleIds')[copyIndex()])]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleIds')[copyIndex()])]", + "principalId": "[parameters('principalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]" + ] + }, + { + "condition": "[variables('hasKustoCluster')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('kusto-rbac-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed')))]", + "subscriptionId": "[variables('kustoClusterSubscriptionId')]", + "resourceGroup": "[variables('kustoClusterResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "clusterName": { + "value": "[variables('kustoClusterName')]" + }, + "principalApplicationId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "principalTenantId": { + "value": "[tenant().tenantId]" + }, + "principalAssignmentName": { + "value": "[format('sre-agent-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed'), 'all-db-viewer'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "15341408879505554256" + } + }, + "parameters": { + "clusterName": { + "type": "string", + "metadata": { + "description": "Kusto cluster name." + } + }, + "principalApplicationId": { + "type": "string", + "metadata": { + "description": "Microsoft Entra application/client ID to assign to the Kusto cluster." + } + }, + "principalTenantId": { + "type": "string", + "metadata": { + "description": "Principal tenant ID." + } + }, + "principalAssignmentName": { + "type": "string", + "metadata": { + "description": "Stable principal assignment name." + } + } + }, + "resources": [ + { + "type": "Microsoft.Kusto/clusters/principalAssignments", + "apiVersion": "2024-04-13", + "name": "[format('{0}/{1}', parameters('clusterName'), parameters('principalAssignmentName'))]", + "properties": { + "principalId": "[parameters('principalApplicationId')]", + "principalType": "App", + "role": "AllDatabasesViewer", + "tenantId": "[parameters('principalTenantId')]" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "apply-extras", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "agentName": { + "value": "[parameters('agentName')]" + }, + "agentEndpoint": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]" + }, + "subscriptionId": { + "value": "[subscription().subscriptionId]" + }, + "recipePackageUri": { + "value": "[parameters('recipePackageUri')]" + }, + "recipePackageSha256": { + "value": "[parameters('recipePackageSha256')]" + }, + "kustoConnectorUri": { + "value": "[parameters('finopsHubKustoConnectorUri')]" + }, + "forceUpdateTag": { + "value": "[parameters('forceUpdateTag')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "5025769957758856658" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for the deployment script resources." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "SRE Agent name." + } + }, + "agentEndpoint": { + "type": "string", + "metadata": { + "description": "SRE Agent data-plane endpoint." + } + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription that contains the SRE Agent." + } + }, + "recipePackageUri": { + "type": "string", + "metadata": { + "description": "Public URI for the generated SRE Agent recipe package." + } + }, + "recipePackageSha256": { + "type": "string", + "metadata": { + "description": "SHA256 hash of the recipe package for integrity verification." + } + }, + "kustoConnectorUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional database-qualified Kusto connector URI." + } + }, + "forceUpdateTag": { + "type": "string", + "metadata": { + "description": "Forces the deployment script to run when the template is redeployed." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Azure resource tags." + } + } + }, + "variables": { + "$fxv#0": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\n$ErrorActionPreference = 'Stop'\r\n\r\nfunction Get-RequiredEnv($Name) {\r\n $value = [Environment]::GetEnvironmentVariable($Name)\r\n if ([string]::IsNullOrWhiteSpace($value)) {\r\n throw \"Missing required environment variable: $Name\"\r\n }\r\n return $value\r\n}\r\n\r\nfunction Get-OptionalEnv($Name) {\r\n return [Environment]::GetEnvironmentVariable($Name)\r\n}\r\n\r\nfunction Get-TempRoot() {\r\n $tempRoot = [System.IO.Path]::GetTempPath()\r\n if ([string]::IsNullOrWhiteSpace($tempRoot)) {\r\n $tempRoot = Get-OptionalEnv 'AZ_SCRIPTS_PATH_OUTPUT_DIRECTORY'\r\n }\r\n if ([string]::IsNullOrWhiteSpace($tempRoot)) {\r\n $tempRoot = (Get-Location).Path\r\n }\r\n return $tempRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)\r\n}\r\n\r\nfunction ConvertTo-BodyJson($Value) {\r\n return ($Value | ConvertTo-Json -Depth 100 -Compress)\r\n}\r\n\r\nfunction Get-PropertyValue($Object, [string[]]$Names, $Default = '') {\r\n foreach ($name in $Names) {\r\n if ($null -ne $Object -and $Object.PSObject.Properties[$name] -and $null -ne $Object.$name) {\r\n return $Object.$name\r\n }\r\n }\r\n return $Default\r\n}\r\n\r\nfunction Get-HttpStatusCode($Exception) {\r\n if ($Exception.Response -and $Exception.Response.StatusCode) {\r\n return [int]$Exception.Response.StatusCode\r\n }\r\n return $null\r\n}\r\n\r\nfunction Invoke-WithRetry([scriptblock]$Action, [string]$Label, [int]$MaxAttempts = 5, [int]$DelaySeconds = 15) {\r\n for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {\r\n try {\r\n return & $Action\r\n }\r\n catch {\r\n $statusCode = Get-HttpStatusCode $_.Exception\r\n if ($statusCode -and $statusCode -notin @(401, 403, 408, 429, 500, 502, 503, 504)) {\r\n throw\r\n }\r\n if ($attempt -eq $MaxAttempts) {\r\n throw\r\n }\r\n Write-Output \"$Label attempt $attempt/$MaxAttempts failed: $($_.Exception.Message)\"\r\n Start-Sleep -Seconds $DelaySeconds\r\n }\r\n }\r\n}\r\n\r\nfunction Invoke-JsonRest([string]$Method, [string]$Uri, [string]$Token, $Body = $null) {\r\n $headers = @{\r\n Authorization = \"Bearer $Token\"\r\n 'Content-Type' = 'application/json'\r\n }\r\n $parameters = @{\r\n Method = $Method\r\n Uri = $Uri\r\n Headers = $headers\r\n }\r\n if ($null -ne $Body) {\r\n $parameters.Body = ConvertTo-BodyJson $Body\r\n }\r\n return Invoke-RestMethod @parameters\r\n}\r\n\r\nfunction Get-KnowledgeSourceName([string]$FileName) {\r\n $name = [IO.Path]::GetFileName($FileName).ToLowerInvariant() -replace '[^a-z0-9-]+', '-'\r\n $name = $name -replace '-+', '-'\r\n return $name.Trim('-')\r\n}\r\n\r\nfunction Get-Collection($Value) {\r\n if ($null -eq $Value) {\r\n return @()\r\n }\r\n return @($Value)\r\n}\r\n\r\nfunction Convert-TokenToString($Token) {\r\n if ($Token -is [Security.SecureString]) {\r\n $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token)\r\n try {\r\n return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)\r\n }\r\n finally {\r\n [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)\r\n }\r\n }\r\n return [string]$Token\r\n}\r\n\r\nfunction Get-ArmAccessTokenString() {\r\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://management.azure.com/').Token\r\n}\r\n\r\nfunction Get-SreAccessTokenString() {\r\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://azuresre.dev').Token\r\n}\r\n\r\n$subscriptionId = Get-RequiredEnv 'subscriptionId'\r\n$resourceGroupName = Get-RequiredEnv 'resourceGroupName'\r\n$agentName = Get-RequiredEnv 'agentName'\r\n$agentEndpoint = (Get-RequiredEnv 'agentEndpoint').TrimEnd('/')\r\n$recipePackageUri = Get-RequiredEnv 'recipePackageUri'\r\n$recipePackageSha256 = Get-RequiredEnv 'recipePackageSha256'\r\n$kustoConnectorUri = Get-OptionalEnv 'kustoConnectorUri'\r\n$armApiVersion = '2025-05-01-preview'\r\n$armBase = \"https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.App/agents/$agentName\"\r\n\r\nWrite-Output 'Connecting with deployment script managed identity...'\r\nConnect-AzAccount -Identity | Out-Null\r\nSet-AzContext -Subscription $subscriptionId | Out-Null\r\n\r\n$tempRoot = Get-TempRoot\r\n$workRoot = Join-Path $tempRoot 'sre-agent-recipe'\r\n$zipPath = Join-Path $tempRoot 'sre-agent-recipe.zip'\r\nRemove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue\r\nNew-Item -Path $workRoot -ItemType Directory -Force | Out-Null\r\n\r\nWrite-Output \"Validating SRE Agent recipe package URI: $recipePackageUri\"\r\n[uri]$parsedUri = $recipePackageUri\r\nif ($parsedUri.Scheme -ne 'https') {\r\n throw \"Recipe package URI must use https scheme, got: $($parsedUri.Scheme)\"\r\n}\r\n\r\n$allowedHosts = @(\r\n 'raw.githubusercontent.com',\r\n 'github.com',\r\n 'aka.ms'\r\n)\r\n\r\nif ($allowedHosts -notcontains $parsedUri.Host -and $parsedUri.Host -notlike '*.blob.core.windows.net') {\r\n throw \"Recipe package host '$($parsedUri.Host)' is not in the allowlist. Allowed: raw.githubusercontent.com, github.com, aka.ms, *.blob.core.windows.net\"\r\n}\r\n\r\nWrite-Output \"Downloading SRE Agent recipe package: $recipePackageUri\"\r\nInvoke-WithRetry -Label 'download recipe package' -Action {\r\n Invoke-WebRequest -Uri $recipePackageUri -OutFile $zipPath\r\n} | Out-Null\r\n\r\nWrite-Output \"Verifying recipe package integrity...\"\r\n$actualHash = (Get-FileHash -Algorithm SHA256 -Path $zipPath).Hash\r\nif ($actualHash -ne $recipePackageSha256) {\r\n throw \"Recipe package hash mismatch: expected $recipePackageSha256 got $actualHash\"\r\n}\r\n\r\nExpand-Archive -Path $zipPath -DestinationPath $workRoot -Force\r\n\r\n$extrasPath = Join-Path $workRoot 'extras.json'\r\nif (-not (Test-Path $extrasPath)) {\r\n throw \"Recipe package did not contain extras.json\"\r\n}\r\n\r\n$extras = Get-Content $extrasPath -Raw | ConvertFrom-Json -Depth 100\r\nWrite-Output \"Loaded recipe package from $extrasPath\"\r\n\r\nif ([string]::IsNullOrWhiteSpace($kustoConnectorUri)) {\r\n $extras.connectors = @(\r\n Get-Collection $extras.connectors | Where-Object {\r\n $type = Get-PropertyValue $_.properties @('dataConnectorType', 'type')\r\n $type -ine 'Kusto'\r\n }\r\n )\r\n}\r\nelse {\r\n foreach ($connector in Get-Collection $extras.connectors) {\r\n $type = Get-PropertyValue $connector.properties @('dataConnectorType', 'type')\r\n if ($type -ieq 'Kusto') {\r\n $connector.properties.dataSource = $kustoConnectorUri\r\n if (-not $connector.properties.PSObject.Properties['identity']) {\r\n $connector.properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\r\n }\r\n }\r\n }\r\n}\r\n\r\nfunction Invoke-ArmPutConnector([string]$Name, $Body) {\r\n $uri = \"$armBase/connectors/$Name`?api-version=$armApiVersion\"\r\n Invoke-WithRetry -Label \"ARM PUT connector $Name\" -Action {\r\n Invoke-JsonRest -Method PUT -Uri $uri -Token (Get-ArmAccessTokenString) -Body $Body\r\n } | Out-Null\r\n Write-Output \" ARM PUT connectors/${Name}: ok\"\r\n}\r\n\r\nfunction Invoke-SreApi([string]$Method, [string]$Path, $Body = $null) {\r\n $uri = \"$agentEndpoint$Path\"\r\n return Invoke-WithRetry -Label \"$Method $Path\" -MaxAttempts 21 -DelaySeconds 30 -Action {\r\n Invoke-JsonRest -Method $Method -Uri $uri -Token (Get-SreAccessTokenString) -Body $Body\r\n }\r\n}\r\n\r\nfunction Invoke-ExtendedPut([string]$Kind, [string]$Name, [string]$Type, $Properties) {\r\n $encodedName = [Uri]::EscapeDataString($Name)\r\n $body = @{\r\n name = $Name\r\n type = $Type\r\n tags = @()\r\n properties = $Properties\r\n }\r\n Invoke-SreApi -Method PUT -Path \"/api/v2/extendedAgent/$Kind/$encodedName\" -Body $body | Out-Null\r\n Write-Output \" PUT $Kind/${Name}: ok\"\r\n}\r\n\r\nWrite-Output 'Step 1/7: Applying connectors...'\r\nforeach ($connector in Get-Collection $extras.connectors) {\r\n $name = [string]$connector.name\r\n $properties = $connector.properties\r\n if (-not $properties.PSObject.Properties['identity']) {\r\n $properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\r\n }\r\n Invoke-ArmPutConnector -Name $name -Body @{ properties = $properties }\r\n}\r\n\r\nWrite-Output 'Step 2/7: Configuring built-in tools...'\r\n$overrides = Get-Collection $extras.builtInTools.overrides\r\nif ($overrides.Count -gt 0) {\r\n $body = @{\r\n overrides = @($overrides | ForEach-Object {\r\n @{\r\n name = $_.name\r\n enabled = [bool]$_.enabled\r\n }\r\n })\r\n }\r\n Invoke-SreApi -Method POST -Path '/api/v2/agent/tools/configure' -Body $body | Out-Null\r\n Write-Output \" built-in tools configured: $($overrides.Count) overrides\"\r\n}\r\n\r\nWrite-Output 'Step 3/7: Uploading knowledge sources...'\r\nforeach ($item in Get-Collection $extras.knowledgeItems) {\r\n $sourceName = Get-KnowledgeSourceName ([string]$item.name)\r\n $bytes = [Text.Encoding]::UTF8.GetBytes([string]$item.content)\r\n $body = @{\r\n properties = @{\r\n dataConnectorType = 'KnowledgeFile'\r\n dataSource = $sourceName\r\n extendedProperties = @{\r\n displayName = [string]$item.name\r\n fileName = [string]$item.name\r\n fileContent = [Convert]::ToBase64String($bytes)\r\n contentType = [string](Get-PropertyValue $item @('contentType') 'application/octet-stream')\r\n }\r\n }\r\n }\r\n Invoke-ArmPutConnector -Name $sourceName -Body $body\r\n Start-Sleep -Seconds 5\r\n}\r\n\r\nWrite-Output 'Step 4/7: Applying tools...'\r\nforeach ($tool in Get-Collection $extras.tools) {\r\n $name = [string]$tool.metadata.name\r\n $properties = $tool.spec\r\n if ($properties.type -eq 'PythonTool') {\r\n $properties.type = 'PythonFunctionTool'\r\n }\r\n Invoke-ExtendedPut -Kind 'tools' -Name $name -Type 'ExtendedAgentTool' -Properties $properties\r\n}\r\n\r\nWrite-Output 'Step 5/7: Applying skills...'\r\nforeach ($skill in Get-Collection $extras.skills) {\r\n $name = [string]$skill.metadata.name\r\n $tools = @()\r\n if ($skill.metadata.spec -and $skill.metadata.spec.tools) {\r\n $tools = Get-Collection $skill.metadata.spec.tools\r\n }\r\n $properties = @{\r\n name = $name\r\n description = [string](Get-PropertyValue $skill.metadata @('description'))\r\n tools = $tools\r\n skillContent = [string](Get-PropertyValue $skill @('skillContent'))\r\n additionalFiles = @()\r\n }\r\n Invoke-ExtendedPut -Kind 'skills' -Name $name -Type 'Skill' -Properties $properties\r\n}\r\n\r\nWrite-Output 'Step 6/7: Applying subagents...'\r\nforeach ($subagent in Get-Collection $extras.subagents) {\r\n $name = [string]$subagent.metadata.name\r\n Invoke-ExtendedPut -Kind 'agents' -Name $name -Type 'ExtendedAgent' -Properties $subagent.spec\r\n}\r\n\r\nWrite-Output 'Step 7/7: Applying scheduled tasks...'\r\n$scheduledTasks = Get-Collection $extras.scheduledTasks\r\nif ($scheduledTasks.Count -gt 0) {\r\n $existing = Invoke-SreApi -Method GET -Path '/api/v1/scheduledtasks'\r\n foreach ($task in $scheduledTasks) {\r\n $name = [string]$task.metadata.name\r\n foreach ($match in (Get-Collection $existing | Where-Object { $_.name -eq $name })) {\r\n if ($match.id) {\r\n Invoke-SreApi -Method DELETE -Path \"/api/v1/scheduledtasks/$($match.id)\" | Out-Null\r\n Write-Output \" deleted existing scheduledtasks/$name\"\r\n }\r\n }\r\n }\r\n\r\n foreach ($task in $scheduledTasks) {\r\n $name = [string]$task.metadata.name\r\n $spec = $task.spec\r\n $properties = @{\r\n name = [string](Get-PropertyValue $spec @('name') $name)\r\n description = [string](Get-PropertyValue $spec @('description'))\r\n cronExpression = [string](Get-PropertyValue $spec @('schedule', 'cronExpression', 'cron_expression'))\r\n agentPrompt = [string](Get-PropertyValue $spec @('prompt', 'agentPrompt', 'agent_prompt'))\r\n agentMode = [string](Get-PropertyValue $spec @('mode', 'agentMode', 'agent_mode') 'Review')\r\n isEnabled = [bool](Get-PropertyValue $spec @('enabled') $true)\r\n agent = [string](Get-PropertyValue $spec @('agent'))\r\n }\r\n Invoke-ExtendedPut -Kind 'scheduledtasks' -Name $name -Type 'ScheduledTask' -Properties $properties\r\n }\r\n}\r\n\r\nWrite-Output 'SRE Agent extras complete.'\r\n", + "identityName": "[format('id-sre-apply-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]", + "scriptName": "[format('apply-sre-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]", + "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55" + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[variables('identityName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]", + "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2023-08-01", + "name": "[variables('scriptName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "AzurePowerShell", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')))]": {} + } + }, + "properties": { + "azPowerShellVersion": "11.0", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "timeout": "PT2H", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "scriptContent": "[variables('$fxv#0')]", + "environmentVariables": [ + { + "name": "subscriptionId", + "value": "[parameters('subscriptionId')]" + }, + { + "name": "resourceGroupName", + "value": "[resourceGroup().name]" + }, + { + "name": "agentName", + "value": "[parameters('agentName')]" + }, + { + "name": "agentEndpoint", + "value": "[parameters('agentEndpoint')]" + }, + { + "name": "recipePackageUri", + "value": "[parameters('recipePackageUri')]" + }, + { + "name": "recipePackageSha256", + "value": "[parameters('recipePackageSha256')]" + }, + { + "name": "kustoConnectorUri", + "value": "[parameters('kustoConnectorUri')]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]", + "[extensionResourceId(resourceId('Microsoft.App/agents', parameters('agentName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId')))]" + ] + } + ], + "outputs": { + "identityName": { + "type": "string", + "value": "[variables('identityName')]" + }, + "scriptName": { + "type": "string", + "value": "[variables('scriptName')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]", + "targetRbac" + ] + } + ], + "outputs": { + "AZURE_RESOURCE_GROUP": { + "type": "string", + "value": "[parameters('resourceGroupName')]" + }, + "AZURE_LOCATION": { + "type": "string", + "value": "[parameters('location')]" + }, + "SRE_AGENT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentName.value]" + }, + "SRE_AGENT_ENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]" + }, + "AGENT_PORTAL_URL": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPortalUrl.value]" + }, + "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]" + }, + "SYSTEM_MANAGED_IDENTITY_TENANT_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentTenantId.value]" + }, + "LOG_ANALYTICS_WORKSPACE_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" + }, + "APPLY_EXTRAS_SCRIPT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'apply-extras'), '2025-04-01').outputs.scriptName.value]" + } + } +} \ No newline at end of file diff --git a/docs/deploy/sre-agent/latest/createUiDefinition.json b/docs/deploy/sre-agent/latest/createUiDefinition.json new file mode 100644 index 000000000..3b17beb78 --- /dev/null +++ b/docs/deploy/sre-agent/latest/createUiDefinition.json @@ -0,0 +1,169 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "basics": { + "description": "Deploy Azure SRE Agent with the FinOps toolkit recipe for FinOps hub analysis, capacity monitoring, scheduled tasks, and specialist agents.", + "location": { + "label": "Location", + "resourceTypes": [ + "Microsoft.App/agents", + "Microsoft.Insights/components", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.OperationalInsights/workspaces", + "Microsoft.Resources/deploymentScripts" + ] + } + } + }, + "resourceTypes": [ + "Microsoft.App/agents", + "Microsoft.Insights/components", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.OperationalInsights/workspaces", + "Microsoft.Resources/deploymentScripts" + ], + "basics": [ + { + "name": "resourceGroupName", + "type": "Microsoft.Common.TextBox", + "label": "Agent resource group", + "defaultValue": "finops-hub-sre", + "toolTip": "Resource group that will contain the Azure SRE Agent, monitoring resources, and deployment script identity.", + "constraints": { + "required": true, + "regex": "^[a-zA-Z0-9._()\\-]{1,90}$", + "validationMessage": "Enter a valid Azure resource group name." + }, + "visible": true + }, + { + "name": "agentName", + "type": "Microsoft.Common.TextBox", + "label": "Agent name", + "defaultValue": "finops-hub-sre", + "toolTip": "Name of the Azure SRE Agent resource.", + "constraints": { + "required": true, + "regex": "^[a-zA-Z0-9][a-zA-Z0-9\\-]{1,61}[a-zA-Z0-9]$", + "validationMessage": "Name must be 3-63 characters and can contain letters, numbers, and hyphens. The first and last characters must be alphanumeric." + }, + "visible": true + } + ], + "steps": [ + { + "name": "configuration", + "label": "Configuration", + "elements": [ + { + "name": "finopsHubKustoConnectorUri", + "type": "Microsoft.Common.TextBox", + "label": "FinOps hub Kusto URI", + "defaultValue": "", + "toolTip": "Optional database-qualified Azure Data Explorer URI for the FinOps hub. Example: https://cluster.region.kusto.windows.net/Hub", + "constraints": { + "required": false, + "regex": "^$|^https://.+\\.kusto\\.windows\\.net/.+", + "validationMessage": "Enter a database-qualified Kusto URI such as https://cluster.region.kusto.windows.net/Hub, or leave blank." + }, + "visible": true + }, + { + "name": "finopsHubKustoClusterResourceId", + "type": "Microsoft.Common.TextBox", + "label": "FinOps hub Kusto cluster resource ID", + "defaultValue": "", + "toolTip": "Optional Azure Data Explorer cluster resource ID. Provide this to assign the agent managed identity AllDatabasesViewer on the cluster.", + "constraints": { + "required": false, + "regex": "^$|^/subscriptions/.+/resourceGroups/.+/providers/Microsoft\\.Kusto/clusters/.+", + "validationMessage": "Enter a valid Microsoft.Kusto/clusters resource ID, or leave blank." + }, + "visible": true + }, + { + "name": "targetResourceGroups", + "type": "Microsoft.Common.TextBox", + "label": "Additional target resource groups", + "defaultValue": "", + "toolTip": "Optional comma-separated resource group names the agent can observe or act on. The agent resource group is always included.", + "constraints": { + "required": false, + "regex": "^$|^[A-Za-z0-9_.()\\-]+(\\s*,\\s*[A-Za-z0-9_.()\\-]+)*$", + "validationMessage": "Enter a comma-separated list of resource group names without blank entries or trailing commas." + }, + "visible": true + }, + { + "name": "accessLevel", + "type": "Microsoft.Common.OptionsGroup", + "label": "Access level", + "defaultValue": "Low", + "toolTip": "Low grants read-only monitoring roles (recommended for reporting and analysis). High adds Contributor on target resource groups for autonomous remediation workflows.", + "constraints": { + "allowedValues": [ + { + "label": "High", + "value": "High" + }, + { + "label": "Low", + "value": "Low" + } + ], + "required": true + }, + "visible": true + }, + { + "name": "actionMode", + "type": "Microsoft.Common.OptionsGroup", + "label": "Action mode", + "defaultValue": "review", + "toolTip": "Review requires approval before write actions. Autonomous lets the agent run enabled actions within its assigned access.", + "constraints": { + "allowedValues": [ + { + "label": "Autonomous", + "value": "autonomous" + }, + { + "label": "Review", + "value": "review" + }, + { + "label": "Read only", + "value": "readOnly" + } + ], + "required": true + }, + "visible": true + }, + { + "name": "enableSubscriptionReaderRole", + "type": "Microsoft.Common.CheckBox", + "label": "Grant subscription Reader to the agent", + "toolTip": "Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.", + "defaultValue": false, + "visible": true + } + ] + } + ], + "outputs": { + "resourceGroupName": "[basics('resourceGroupName')]", + "agentName": "[basics('agentName')]", + "location": "[location()]", + "targetResourceGroupNames": "[steps('configuration').targetResourceGroups]", + "finopsHubKustoConnectorUri": "[steps('configuration').finopsHubKustoConnectorUri]", + "finopsHubKustoClusterResourceId": "[steps('configuration').finopsHubKustoClusterResourceId]", + "accessLevel": "[steps('configuration').accessLevel]", + "actionMode": "[steps('configuration').actionMode]", + "enableSubscriptionReaderRole": "[steps('configuration').enableSubscriptionReaderRole]" + } + } +} diff --git a/src/powershell/Tests/Unit/Templates.SreAgent.Deploy.Tests.ps1 b/src/powershell/Tests/Unit/Templates.SreAgent.Deploy.Tests.ps1 new file mode 100644 index 000000000..4122e4d98 --- /dev/null +++ b/src/powershell/Tests/Unit/Templates.SreAgent.Deploy.Tests.ps1 @@ -0,0 +1,1286 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'SRE Agent deploy template' { + BeforeAll { + $script:RepoRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + $script:DeployScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/deploy.sh' + $script:ApplyExtrasScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/apply-extras.sh' + $script:PostProvisionScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/post-provision.sh' + $script:VerifyScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh' + $script:RecipeDir = Join-Path $script:RepoRoot 'src/templates/sre-agent/recipes/finops-hub' + $script:ScheduledTaskDir = Join-Path $script:RecipeDir 'automations/scheduled-tasks' + $script:OutputStylePath = Join-Path $script:RepoRoot 'src/templates/claude-plugin/output-styles/ftk-output-style.md' + $script:ReadmePath = Join-Path $script:RepoRoot 'src/templates/sre-agent/README.md' + $script:DocsPath = Join-Path $script:RepoRoot 'docs-mslearn/toolkit/sre-agent/deploy.md' + $script:AgentJsonPath = Join-Path $script:RecipeDir 'agent.json' + $script:SkipBash = -not (Get-Command bash -ErrorAction SilentlyContinue) + + function Invoke-BashCommand { + param( + [Parameter(Mandatory)] + [string] $Command + ) + + Push-Location $script:RepoRoot + try { + $output = & bash -lc "MSYS2_ARG_CONV_EXCL='/subscriptions;/providers' $Command" 2>&1 + [pscustomobject]@{ + ExitCode = $LASTEXITCODE + Output = ($output -join "`n") + } + } + finally { + Pop-Location + } + } + + function Invoke-BashCommandWithPath { + param( + [Parameter(Mandatory)] + [string] $Command, + + [Parameter(Mandatory)] + [string] $PathPrefix + ) + + Push-Location $script:RepoRoot + try { + $originalPath = $env:PATH + $bashPathPrefix = ConvertTo-BashPath $PathPrefix + $quotedPathPrefix = ConvertTo-BashSingleQuoted $bashPathPrefix + $output = & bash -c "MSYS2_ARG_CONV_EXCL='/subscriptions;/providers' PATH=${quotedPathPrefix}:`$PATH $Command" 2>&1 + [pscustomobject]@{ + ExitCode = $LASTEXITCODE + Output = ($output -join "`n") + } + } + finally { + $env:PATH = $originalPath + Pop-Location + } + } + + function ConvertTo-BashPath { + param( + [Parameter(Mandatory)] + [string] $Path + ) + + $quotedPath = ConvertTo-BashSingleQuoted $Path + $converted = & bash -lc "command -v cygpath >/dev/null 2>&1 && cygpath -u $quotedPath" 2>$null + if ($LASTEXITCODE -eq 0 -and $converted) { + return ($converted -join '') + } + + return $Path + } + + function ConvertTo-BashSingleQuoted { + param( + [Parameter(Mandatory)] + [string] $Value + ) + + return "'" + ($Value -replace "'", "'\''") + "'" + } + + function Set-BashStub { + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [string] $Content + ) + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($Path, ($Content -replace "`r`n", "`n"), $utf8NoBom) + $bashPath = ConvertTo-BashPath $Path + $quotedPath = ConvertTo-BashSingleQuoted $bashPath + & bash -c "chmod +x $quotedPath" + if ($LASTEXITCODE -ne 0) { + throw "Failed to mark bash stub executable: $Path" + } + } + + function Set-PythonExtrasStub { + param( + [Parameter(Mandatory)] + [string] $BinDir + ) + + $realPython = (Get-Command python3 -ErrorAction SilentlyContinue).Source + if (-not $realPython) { + $realPython = (Get-Command python -ErrorAction Stop).Source + } + $realPython = ConvertTo-BashPath $realPython + $realPythonLiteral = ConvertTo-BashSingleQuoted $realPython + + $stubContent = @' +#!/usr/bin/env bash +set -euo pipefail +real_python=__REAL_PYTHON__ + +if [[ "${1:-}" == "-c" && "${2:-}" == "import yaml" ]]; then + exit 0 +fi + +if [[ "${1:-}" == */build-extras.py ]]; then + output="" + while [[ "$#" -gt 0 ]]; do + case "$1" in + --output) + output="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + [[ -n "$output" ]] || exit 2 + mkdir -p "$(dirname "$output")" + cat > "$output" <<'JSON' +{ + "connectors": [], + "builtInTools": { "overrides": [] }, + "knowledgeItems": [], + "tools": [], + "skills": [], + "subagents": [ + { "metadata": { "name": "chief-financial-officer" }, "spec": { "description": "test" } }, + { "metadata": { "name": "ftk-database-query" }, "spec": { "description": "test" } }, + { "metadata": { "name": "ftk-hubs-agent" }, "spec": { "description": "test" } }, + { "metadata": { "name": "finops-practitioner" }, "spec": { "description": "test" } } + ], + "scheduledTasks": [] +} +JSON + printf '{"connectors":0,"knowledgeItems":0,"tools":0,"skills":0,"subagents":4,"scheduledTasks":0}\n' + exit 0 +fi + +exec "$real_python" "$@" +'@ + $stubContent = $stubContent.Replace('__REAL_PYTHON__', $realPythonLiteral) + + Set-BashStub -Path (Join-Path $BinDir 'python3') -Content $stubContent + Set-BashStub -Path (Join-Path $BinDir 'python') -Content $stubContent + } + } + + Context 'bash availability' { + It 'has bash available for hermetic tests' { + if ($script:SkipBash) { + Set-ItResult -Skipped -Because 'bash is unavailable' + } + $true | Should -BeTrue + } + } + + Context 'help and parsing' { + It 'prints help with portal labels and required flags' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:DeployScript' --help" + $result.ExitCode | Should -Be 0 + $result.Output | Should -Match 'Resource group' + $result.Output | Should -Match 'Agent name' + $result.Output | Should -Match 'Region' + $result.Output | Should -Match 'Subscription' + $result.Output | Should -Match '--cluster-uri' + } + + It 'documents the same help lines in README and docs' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $help = Invoke-BashCommand "bash '$script:DeployScript' --help" + $help.ExitCode | Should -Be 0 + $readme = Get-Content -Path $script:ReadmePath -Raw + $docs = Get-Content -Path $script:DocsPath -Raw + + @( + '--recipe ', + '--resource-group ', + '--name ', + '--location ', + '--subscription ', + '--cluster-uri ', + '--cluster-resource-id ', + '--deploy-name ' + ) | ForEach-Object { + $escaped = [regex]::Escape($_) + $help.Output | Should -Match $escaped + $readme | Should -Match $escaped + $docs | Should -Match $escaped + } + } + + It 'errors for each missing required recipe flag' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $cases = @( + @{ + Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --dry-run" + Match = 'subscription' + } + @{ + Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 --dry-run" + Match = 'resource-group' + } + @{ + Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-x --dry-run" + Match = 'name' + } + @{ + Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-x -n a --dry-run" + Match = 'location' + } + ) + + foreach ($case in $cases) { + $result = Invoke-BashCommand $case.Command + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match $case.Match + } + } + + It 'errors on unknown flags' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:DeployScript' --recipes '$script:RecipeDir' --dry-run" + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match 'unknown flag' + } + + It 'errors when a value-taking flag is missing its value' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' -g -n foo --dry-run" + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match 'requires a value' + } + + It 'errors when verify-agent expected config is missing its value' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:VerifyScript' 00000000-0000-0000-0000-000000000000 rg-test test-agent --expected" + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match 'flag --expected requires a value' + $result.Output | Should -Not -Match 'unbound variable' + } + + It 'rejects unknown verify-agent arguments before Azure calls' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:VerifyScript' 00000000-0000-0000-0000-000000000000 rg-test test-agent --bogus" + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match "unknown argument '--bogus'" + $result.Output | Should -Not -Match 'Could not resolve agent endpoint' + } + + It 'rejects the positional footgun' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:DeployScript' --dry-run rg-test" + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match 'unknown argument' + } + + It 'shows customer values and not maintainer defaults on happy-path dry-run' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l westus3 --cluster-uri https://example.westus3.kusto.windows.net/Hub --cluster-resource-id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Kusto/clusters/fake --dry-run" + $result.ExitCode | Should -Be 0 + $result.Output | Should -Match 'rg-test-customer' + $result.Output | Should -Match 'customer-sre-agent' + $result.Output | Should -Match 'westus3' + $result.Output | Should -Not -Match 'rg-finops-sre-agent|finops-sre-agent|eastus2' + } + + It 'uses a deterministic default deployment name across dry-runs' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $deployRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-deploy-name-" + [guid]::NewGuid().ToString('N')) + try { + $command = "SRE_AGENT_DEPLOY_DIR='$deployRoot' bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l westus3 --cluster-uri https://example.westus3.kusto.windows.net/Hub --cluster-resource-id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Kusto/clusters/fake --dry-run" + $first = Invoke-BashCommand $command + $second = Invoke-BashCommand $command + + $first.ExitCode | Should -Be 0 + $second.ExitCode | Should -Be 0 + $firstPath = [regex]::Match($first.Output, 'Parameters:\s+(.+)').Groups[1].Value.Trim() + $secondPath = [regex]::Match($second.Output, 'Parameters:\s+(.+)').Groups[1].Value.Trim() + + $firstPath | Should -Be $secondPath + $firstPath | Should -Match 'sre-agent-customer-sre-agent-[a-f0-9]{12}' + + $parameters = Get-Content -Path $firstPath -Raw | ConvertFrom-Json + $parameters.parameters.upgradeChannel.value | Should -Be 'Preview' + $parameters.parameters.experimentalSettings.value.EnableSandboxGroup | Should -BeTrue + $parameters.parameters.experimentalSettings.value.EnableWorkspaceTools | Should -BeTrue + $parameters.parameters.monthlyAgentUnitLimit.value | Should -Be 10000 + } + finally { + Remove-Item -Path $deployRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'requires cluster resource ID for dry-run when a Kusto URI is provided' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l westus3 --cluster-uri https://example.westus3.kusto.windows.net/Hub --dry-run" + + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match '--cluster-resource-id is required with --cluster-uri for --dry-run' + } + + It 'auto-resolves the Kusto cluster resource ID before deployment' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-kusto-resolve-" + [guid]::NewGuid().ToString('N')) + $binDir = Join-Path $tempRoot 'bin' + $deployRoot = Join-Path $tempRoot 'deploy' + New-Item -ItemType Directory -Force -Path $binDir, $deployRoot | Out-Null + + $fakeAz = Join-Path $binDir 'az' + Set-BashStub -Path $fakeAz -Content @' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$*" == *"account get-access-token"* ]]; then + echo "fake-token" + exit 0 +fi + +if [[ "${1:-}" == "rest" ]]; then + echo "{}" + exit 0 +fi + +if [[ "$*" == *"resource list"* ]]; then + exit 0 +fi + +if [[ "$*" == *"graph query"* ]]; then + cat <<'JSON' +{ + "data": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/example" + } + ] +} +JSON + exit 0 +fi + +if [[ "$*" == *"deployment sub show"* ]]; then + cat <<'JSON' +{ + "properties": { + "provisioningState": "Succeeded", + "outputs": { + "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" }, + "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" }, + "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" } + } + } +} +JSON + exit 0 +fi + +if [[ "$*" == *"deployment sub create"* ]]; then + cat <<'JSON' +{ + "properties": { + "provisioningState": "Succeeded", + "outputs": { + "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" }, + "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" }, + "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" } + } + } +} +JSON + exit 0 +fi + +case "$*" in + *"account show"*|*"account set"*|*"provider register"*|*"version"*) + exit 0 + ;; +esac + +exit 0 +'@ + + $fakeCurl = Join-Path $binDir 'curl' + Set-BashStub -Path $fakeCurl -Content @' +#!/usr/bin/env bash +set -euo pipefail + +out="" +url="" +method="GET" +while [[ $# -gt 0 ]]; do + case "$1" in + -o) + out="$2" + shift 2 + ;; + -w) + shift 2 + ;; + -X) + method="$2" + shift 2 + ;; + -H|--data-binary) + shift 2 + ;; + http*) + url="$1" + shift + ;; + *) + shift + ;; + esac +done + +if [[ "$url" == */api/v2/extendedAgent/connectors/finops-hub-kusto ]]; then + cat > "$out" <<'JSON' +{"name":"finops-hub-kusto","properties":{"dataConnectorType":"Kusto"}} +JSON + printf "201" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/connectors ]]; then + cat > "$out" <<'JSON' +{ + "value": [ + { "name": "chart-artifact-verification-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "chart-artifact-verification.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "document-index-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "document-index.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "ftk-output-style-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "ftk-output-style.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "known-issues-and-workarounds-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "known-issues-and-workarounds.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "onboarding-recommendations-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "onboarding-recommendations.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "teams-notification-guide-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "teams-notification-guide.md", "createdAt": "2026-05-25T19:57:31Z" } } } + ] +} +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/connectors/* ]]; then + cat > "$out" <<'JSON' +{ + "properties": { + "dataConnectorType": "KnowledgeFile", + "extendedProperties": { + "createdAt": "2026-05-25T19:57:31Z", + "lastModifiedAt": "2026-05-25T19:57:31Z" + } + } +} +JSON + if [[ "$method" == "PUT" ]]; then printf "201"; else printf "200"; fi + exit 0 +fi + +if [[ "$url" == */api/v2/agent/tools/configure ]]; then + cat > "$out" <<'JSON' +{} +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v1/scheduledtasks ]]; then + cat > "$out" <<'JSON' +[] +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/agents/* ]]; then + [[ -n "${AGENT_APPLY_LOG:-}" ]] && basename "$url" >> "$AGENT_APPLY_LOG" + cat > "$out" <<'JSON' +{ + "type": "ExtendedAgent", + "properties": { + "provisioningState": "Succeeded" + } +} +JSON + printf "201" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/tools/* || "$url" == */api/v2/extendedAgent/skills/* || "$url" == */api/v2/extendedAgent/scheduledtasks/* ]]; then + cat > "$out" <<'JSON' +{ + "properties": { + "provisioningState": "Succeeded" + } +} +JSON + printf "201" + exit 0 +fi + +cat > "$out" <<'JSON' +{ + "files": [ + { "name": "chart-artifact-verification-md", "isIndexed": true, "errorReason": null }, + { "name": "document-index-md", "isIndexed": true, "errorReason": null }, + { "name": "ftk-output-style-md", "isIndexed": true, "errorReason": null }, + { "name": "known-issues-and-workarounds-md", "isIndexed": true, "errorReason": null }, + { "name": "onboarding-recommendations-md", "isIndexed": true, "errorReason": null }, + { "name": "teams-notification-guide-md", "isIndexed": true, "errorReason": null } + ], + "continuationToken": "" +} +JSON +printf "200" +'@ + + Set-PythonExtrasStub -BinDir $binDir + + Set-BashStub -Path (Join-Path $binDir 'sleep') -Content @' +#!/usr/bin/env bash +exit 0 +'@ + + try { + $command = "SRE_AGENT_DEPLOY_DIR='$deployRoot' SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS=0 SRE_AGENT_APPLY_RETRY_DELAY_SECONDS=0 bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l eastus2 --cluster-uri https://example.westus3.kusto.windows.net/Hub" + $result = Invoke-BashCommandWithPath $command $binDir + $result.ExitCode | Should -Be 0 -Because $result.Output + $result.Output | Should -Match 'Resolving Kusto cluster resource ID from --cluster-uri' + $result.Output | Should -Match '/providers/Microsoft\.Kusto/clusters/example' + + $parametersFile = (Get-ChildItem -Path $deployRoot -Recurse -Filter 'deploy.parameters.json' | Select-Object -First 1).FullName + $parametersFile | Should -Not -BeNullOrEmpty + $parameters = Get-Content -Path $parametersFile -Raw | ConvertFrom-Json + $parameters.parameters.finopsHubKustoClusterResourceId.value | Should -Be '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/example' + } + finally { + Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'warns but continues when the Kusto cluster denies public query access' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-kusto-private-" + [guid]::NewGuid().ToString('N')) + $binDir = Join-Path $tempRoot 'bin' + $deployRoot = Join-Path $tempRoot 'deploy' + New-Item -ItemType Directory -Force -Path $binDir, $deployRoot | Out-Null + + $fakeAz = Join-Path $binDir 'az' + Set-BashStub -Path $fakeAz -Content @' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$*" == *"account get-access-token"* ]]; then + echo "fake-token" + exit 0 +fi + +if [[ "${1:-}" == "rest" ]]; then + echo "{}" + exit 0 +fi + +if [[ "$*" == *"resource show"* ]]; then + cat <<'JSON' +{ + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/privateadx", + "properties": { + "uri": "https://privateadx.westus.kusto.windows.net", + "publicNetworkAccess": "Disabled", + "privateEndpointConnections": [ + { + "name": "privateadx-ep", + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved" + } + } + } + ] + } +} +JSON + exit 0 +fi + +if [[ "$*" == *"deployment sub show"* ]]; then + cat <<'JSON' +{ + "properties": { + "provisioningState": "Succeeded", + "outputs": { + "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" }, + "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" }, + "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" } + } + } +} +JSON + exit 0 +fi + +if [[ "$*" == *"deployment sub create"* ]]; then + cat <<'JSON' +{ + "properties": { + "provisioningState": "Succeeded", + "outputs": { + "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" }, + "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" }, + "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" } + } + } +} +JSON + exit 0 +fi + +case "$*" in + *"account show"*|*"account set"*|*"provider register"*|*"version"*) + exit 0 + ;; +esac + +exit 0 +'@ + + $fakeCurl = Join-Path $binDir 'curl' + Set-BashStub -Path $fakeCurl -Content @' +#!/usr/bin/env bash +set -euo pipefail + +out="" +url="" +method="GET" +while [[ $# -gt 0 ]]; do + case "$1" in + -o) + out="$2" + shift 2 + ;; + -w) + shift 2 + ;; + -X) + method="$2" + shift 2 + ;; + -H|--data-binary) + shift 2 + ;; + http*) + url="$1" + shift + ;; + *) + shift + ;; + esac +done + +if [[ "$url" == */api/v2/extendedAgent/connectors/finops-hub-kusto ]]; then + cat > "$out" <<'JSON' +{"name":"finops-hub-kusto","properties":{"dataConnectorType":"Kusto"}} +JSON + printf "201" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/connectors ]]; then + cat > "$out" <<'JSON' +{ + "value": [ + { "name": "chart-artifact-verification-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "chart-artifact-verification.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "document-index-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "document-index.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "ftk-output-style-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "ftk-output-style.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "known-issues-and-workarounds-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "known-issues-and-workarounds.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "onboarding-recommendations-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "onboarding-recommendations.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "teams-notification-guide-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "teams-notification-guide.md", "createdAt": "2026-05-25T19:57:31Z" } } } + ] +} +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/connectors/* ]]; then + cat > "$out" <<'JSON' +{ + "properties": { + "dataConnectorType": "KnowledgeFile", + "extendedProperties": { + "createdAt": "2026-05-25T19:57:31Z", + "lastModifiedAt": "2026-05-25T19:57:31Z" + } + } +} +JSON + if [[ "$method" == "PUT" ]]; then printf "201"; else printf "200"; fi + exit 0 +fi + +if [[ "$url" == */api/v1/scheduledtasks ]]; then + cat > "$out" <<'JSON' +[] +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v2/agent/tools/configure ]]; then + cat > "$out" <<'JSON' +{} +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/agents/* || "$url" == */api/v2/extendedAgent/tools/* || "$url" == */api/v2/extendedAgent/skills/* || "$url" == */api/v2/extendedAgent/scheduledtasks/* ]]; then + cat > "$out" <<'JSON' +{ + "properties": { + "provisioningState": "Succeeded" + } +} +JSON + printf "201" + exit 0 +fi + +cat > "$out" <<'JSON' +{} +JSON +printf "200" +'@ + + Set-PythonExtrasStub -BinDir $binDir + + Set-BashStub -Path (Join-Path $binDir 'sleep') -Content @' +#!/usr/bin/env bash +exit 0 +'@ + + try { + $clusterId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/privateadx' + $command = "SRE_AGENT_DEPLOY_DIR='$deployRoot' SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS=0 SRE_AGENT_APPLY_RETRY_DELAY_SECONDS=0 bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l eastus2 --cluster-uri https://privateadx.westus.kusto.windows.net/Hub --cluster-resource-id $clusterId" + $result = Invoke-BashCommandWithPath $command $binDir + $result.ExitCode | Should -Be 0 -Because $result.Output + $result.Output | Should -Match 'Warning: The Kusto cluster denies public query access' + $result.Output | Should -Match 'private endpoint ADX blocks direct KQL queries' + $result.Output | Should -Match 'https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations' + $result.Output | Should -Match 'SRE Agent ready' + } + finally { + Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'routes deploy through the copied starter-lab infra and apply-extras path' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $deployContent = Get-Content -Path $script:DeployScript -Raw + $deployContent | Should -Match 'INFRA_DIR=.*infra' + $deployContent | Should -Match '\$\{INFRA_DIR\}/main\.bicep' + $deployContent | Should -Match 'apply-extras\.sh' + $deployContent | Should -Not -Match 'bicep/assemble-agent|hydrate-extensions' + + Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') | Should -BeTrue + Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/apply-extras.sh') | Should -BeTrue + } + } + + Context 'repo invariants' { + It 'removes shipped recipe identity defaults' { + $agentJson = Get-Content -Path $script:AgentJsonPath -Raw + $agentJson | Should -Not -Match '"identity"' + } + + It 'uses the current SRE Agent API, model provider, and sandbox configuration' { + $agentJson = Get-Content -Path $script:AgentJsonPath -Raw | ConvertFrom-Json + $mainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') -Raw + $sreAgentBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/modules/sre-agent.bicep') -Raw + $verifyScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh') -Raw + + $agentJson.upgradeChannel | Should -Be 'Preview' + $agentJson.defaultModelProvider | Should -Be 'MicrosoftFoundry' + $agentJson.defaultModelName | Should -Be 'Automatic' + $agentJson.experimentalSettings.EnableSandboxGroup | Should -BeTrue + $agentJson.experimentalSettings.EnableWorkspaceTools | Should -BeTrue + $sreAgentBicep | Should -Match 'Microsoft\.App/agents@2026-01-01' + $sreAgentBicep | Should -Not -Match 'Microsoft\.App/agents@2025-05-01-preview' + $sreAgentBicep | Should -Match 'defaultModel:' + $sreAgentBicep | Should -Match 'provider: defaultModelProvider' + $sreAgentBicep | Should -Match 'name: defaultModelName' + $sreAgentBicep | Should -Match 'upgradeChannel: upgradeChannel' + $mainBicep | Should -Match 'defaultModelProvider' + $mainBicep | Should -Match 'defaultModelName' + $mainBicep | Should -Match 'EnableSandboxGroup' + $mainBicep | Should -Match 'EnableWorkspaceTools' + $verifyScript | Should -Match 'API_VERSION="2026-01-01"' + } + + It 'limits legacy config env-var references to the allowlist' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "git grep -nE 'FINOPS_HUB_CLUSTER_URI|FINOPS_HUB_CLUSTER_RESOURCE_ID' -- docs-mslearn/toolkit/sre-agent/deploy.md src/templates/sre-agent" + $result.ExitCode | Should -Be 0 + + $paths = $result.Output -split "`n" | + Where-Object { $_ } | + ForEach-Object { ($_ -split ':', 2)[0] } | + Sort-Object -Unique + + $paths | Should -Be @( + 'docs-mslearn/toolkit/sre-agent/deploy.md', + 'src/templates/sre-agent/bin/build-extras.py', + 'src/templates/sre-agent/README.md', + 'src/templates/sre-agent/recipes/finops-hub/connectors.json' + ) + } + + It 'requires recipe package hash validation before extraction' { + $applyExtrasScriptPath = Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1' + $applyExtrasLines = Get-Content -Path $applyExtrasScriptPath + $applyExtrasScript = $applyExtrasLines -join "`n" + + $applyExtrasScript | Should -Match "\$recipePackageSha256 = Get-RequiredEnv 'recipePackageSha256'" + $applyExtrasScript | Should -Match 'Get-FileHash\s+-Algorithm\s+SHA256' + $applyExtrasScript | Should -Match '(?s)recipePackageSha256.*throw' + + $hashLine = for ($i = 0; $i -lt $applyExtrasLines.Count; $i++) { + if ($applyExtrasLines[$i] -match 'Get-FileHash' -and $applyExtrasLines[$i] -match 'SHA256') { + $i + 1 + break + } + } + $throwLine = for ($i = 0; $i -lt $applyExtrasLines.Count; $i++) { + if ($applyExtrasLines[$i] -match 'throw' -and $applyExtrasLines[$i] -match 'hash|SHA256|recipePackageSha256') { + $i + 1 + break + } + } + $expandArchiveLine = for ($i = 0; $i -lt $applyExtrasLines.Count; $i++) { + if ($applyExtrasLines[$i] -match 'Expand-Archive') { + $i + 1 + break + } + } + + $hashLine | Should -BeGreaterThan 0 + $throwLine | Should -BeGreaterThan 0 + $expandArchiveLine | Should -BeGreaterThan 0 + $hashLine | Should -BeLessThan $expandArchiveLine + $throwLine | Should -BeLessThan $expandArchiveLine + } + + It 'declares recipePackageSha256 in the compiled deploy templates' { + foreach ($compiledPath in @( + (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/latest/azuredeploy.json'), + (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/14.0/azuredeploy.json') + )) { + $compiledTemplate = Get-Content -Path $compiledPath -Raw | ConvertFrom-Json -Depth 100 + $compiledTemplate.parameters.PSObject.Properties.Name | Should -Contain 'recipePackageSha256' + } + } + + It 'requires an https allowlisted recipe package origin before download' { + $applyExtrasScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1') -Raw + + $applyExtrasScript | Should -Match '(?is)\[uri\]\$[A-Za-z0-9_]+\s*=\s*\$recipePackageUri' + $applyExtrasScript | Should -Match "(?is)\.Scheme\s*-ne\s*'https'.*throw" + $applyExtrasScript | Should -Match '(?is)\.Host.*(-notcontains|-notin|-notmatch|-notlike).*(throw|fail)' + } + + It 'keeps connectors.secrets.env out of scripts' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "git grep -nE 'connectors\.secrets\.env' -- src/templates/sre-agent/bin src/templates/sre-agent/infra" + $result.ExitCode | Should -Be 1 + } + + It 'removes the legacy custom bicep deployment surface' { + Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bicep') | Should -BeFalse + Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/hydrate-extensions.sh') | Should -BeFalse + } + + It 'removes dead telemetry scripts and source references from the SRE Agent template' { + Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/telemetry.sh') | Should -BeFalse + + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "git grep -nE 'f10eff7f-b995-4c41-8347-90f0f55d5969|send_telemetry' -- src/templates/sre-agent" + $result.ExitCode | Should -Be 1 -Because $result.Output + } + + It 'removes legacy telemetry flags and env vars from deploy surfaces' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $deployContent = Get-Content -Path $script:DeployScript -Raw + $deployContent | Should -Not -Match '--no-telemetry' + + $result = Invoke-BashCommand "git grep -nE '\-\-no-telemetry|SRE_AGENT_NO_TELEMETRY' -- docs-mslearn/toolkit/sre-agent/deploy.md src/templates/sre-agent" + $result.ExitCode | Should -Be 1 -Because $result.Output + } + + It 'parameterizes the deployer principal type in the SRE Agent module' { + $sreAgentBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/modules/sre-agent.bicep') -Raw + + $sreAgentBicep | Should -Match "(?s)@allowed\(\s*\[\s*'User'\s*,\s*'ServicePrincipal'\s*\]\s*\)\s*param\s+deployerPrincipalType\s+string" + $sreAgentBicep | Should -Match 'principalType:\s*deployerPrincipalType' + $sreAgentBicep | Should -Not -Match "principalType:\s*'User'" + } + + It 'passes ServicePrincipal deployer principal type in the CI/CD example' { + $githubActionsDeploy = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml') -Raw + + $githubActionsDeploy | Should -Match 'ServicePrincipal' + $githubActionsDeploy | Should -Match 'deployerPrincipalType|--deployer-principal-type' + } + + It 'uses deterministic subscription and resource group identity for support resource names' { + $mainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') -Raw + $resourcesBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/resources.bicep') -Raw + + $mainBicep | Should -Match "agentResourceGroupId = subscriptionResourceId\('Microsoft\.Resources/resourceGroups', resourceGroupName\)" + $mainBicep | Should -Match 'namingSeed = toLower' + $mainBicep | Should -Match 'subscription\(\)\.subscriptionId' + $mainBicep | Should -Match 'agentResourceGroupId' + $mainBicep | Should -Match 'agentName' + + $resourcesBicep | Should -Match 'param namingSeed string' + $resourcesBicep | Should -Match 'uniqueSuffix = uniqueString\(namingSeed\)' + $resourcesBicep | Should -Not -Match 'resourceGroup\(\)\.id|deployment\(\)\.name' + } + + It 'defaults to read-only identity with review-mode portal and autonomous recipe reporting' { + $portalMainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/main.bicep') -Raw + $cliMainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') -Raw + $createUiDef = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/createUiDefinition.json') -Raw | ConvertFrom-Json -Depth 100 + $deployScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/deploy.sh') -Raw + $recipeAgentJson = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/recipes/finops-hub/agent.json') -Raw | ConvertFrom-Json -Depth 100 + $expectedConfigJson = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/recipes/finops-hub/expected-config.json') -Raw | ConvertFrom-Json -Depth 100 + + $portalMainBicep | Should -Match "param accessLevel string = 'Low'" + $portalMainBicep | Should -Match 'param enableSubscriptionReaderRole bool = false' + $portalMainBicep | Should -Match "param actionMode string = 'review'" + + $cliMainBicep | Should -Match "param accessLevel string = 'Low'" + $cliMainBicep | Should -Match 'param enableSubscriptionReaderRole bool = false' + $cliMainBicep | Should -Match "param actionMode string = 'autonomous'" + + $accessLevelControl = $createUiDef.parameters.steps | + Where-Object { $_.name -eq 'configuration' } | + Select-Object -First 1 -ExpandProperty elements | + Where-Object { $_.name -eq 'accessLevel' } + $accessLevelControl.defaultValue | Should -Be 'Low' + + $subscriptionReaderControl = $createUiDef.parameters.steps | + Where-Object { $_.name -eq 'configuration' } | + Select-Object -First 1 -ExpandProperty elements | + Where-Object { $_.name -eq 'enableSubscriptionReaderRole' } + $subscriptionReaderControl.defaultValue | Should -Be $false + + $actionModeControl = $createUiDef.parameters.steps | + Where-Object { $_.name -eq 'configuration' } | + Select-Object -First 1 -ExpandProperty elements | + Where-Object { $_.name -eq 'actionMode' } + $actionModeControl.defaultValue | Should -Be 'review' + + $deployScript | Should -Match 'ENABLE_SUBSCRIPTION_READER="false"' + + $recipeAgentJson.access.accessLevel | Should -Be 'Low' + $recipeAgentJson.access.actionMode | Should -Be 'autonomous' + + $expectedConfigJson.agent.accessLevel | Should -Be 'Low' + $expectedConfigJson.agent.actionMode | Should -Be 'autonomous' + $expectedConfigJson.subscriptionRoleAssignments | Should -Be @() + } + + It 'declares Low accessLevel and disabled subscription Reader in the compiled deploy templates' { + foreach ($compiledPath in @( + (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/latest/azuredeploy.json'), + (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/14.0/azuredeploy.json') + )) { + $compiledTemplate = Get-Content -Path $compiledPath -Raw | ConvertFrom-Json -Depth 100 + $compiledTemplate.parameters.accessLevel.defaultValue | Should -Be 'Low' + $compiledTemplate.parameters.enableSubscriptionReaderRole.defaultValue | Should -Be $false + } + } + + It 'applies subagents after their local handoff targets' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + + $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-post-provision-" + [guid]::NewGuid().ToString('N')) + $binDir = Join-Path $tempRoot 'bin' + $buildDir = Join-Path $tempRoot 'build' + $logPath = Join-Path $tempRoot 'agent-apply.log' + New-Item -ItemType Directory -Force -Path $binDir, $buildDir | Out-Null + + $fakeAz = Join-Path $binDir 'az' + Set-BashStub -Path $fakeAz -Content @' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$*" == *"account get-access-token"* ]]; then + echo "fake-token" + exit 0 +fi + +if [[ "${1:-}" == "rest" ]]; then + echo "{}" + exit 0 +fi + +case "$*" in + *"account show"*|*"account set"*|*"version"*) + exit 0 + ;; +esac + +exit 1 +'@ + + $fakeCurl = Join-Path $binDir 'curl' + Set-BashStub -Path $fakeCurl -Content @' +#!/usr/bin/env bash +set -euo pipefail + +out="" +url="" +method="GET" +while [[ $# -gt 0 ]]; do + case "$1" in + -o) + out="$2" + shift 2 + ;; + -w) + shift 2 + ;; + -X) + method="$2" + shift 2 + ;; + -H|--data-binary) + shift 2 + ;; + http*) + url="$1" + shift + ;; + *) + shift + ;; + esac +done + +if [[ "$url" == */api/v2/extendedAgent/connectors ]]; then + cat > "$out" <<'JSON' +{ + "value": [ + { "name": "chart-artifact-verification-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "chart-artifact-verification.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "document-index-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "document-index.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "ftk-output-style-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "ftk-output-style.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "known-issues-and-workarounds-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "known-issues-and-workarounds.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "onboarding-recommendations-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "onboarding-recommendations.md", "createdAt": "2026-05-25T19:57:31Z" } } }, + { "name": "teams-notification-guide-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "teams-notification-guide.md", "createdAt": "2026-05-25T19:57:31Z" } } } + ] +} +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/connectors/* ]]; then + cat > "$out" <<'JSON' +{ + "type": "KnowledgeItem", + "properties": { + "dataConnectorType": "KnowledgeFile", + "extendedProperties": { + "createdAt": "2026-05-25T19:57:31Z", + "lastModifiedAt": "2026-05-25T19:57:31Z" + } + } +} +JSON + if [[ "$method" == "PUT" ]]; then printf "201"; else printf "200"; fi + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/agents/* ]]; then + [[ -n "${AGENT_APPLY_LOG:-}" ]] && basename "$url" >> "$AGENT_APPLY_LOG" + cat > "$out" <<'JSON' +{ + "type": "ExtendedAgent", + "properties": { + "provisioningState": "Succeeded" + } +} +JSON + printf "201" + exit 0 +fi + +if [[ "$url" == */api/v2/extendedAgent/tools/* || "$url" == */api/v2/extendedAgent/skills/* || "$url" == */api/v2/extendedAgent/scheduledtasks/* ]]; then + cat > "$out" <<'JSON' +{ + "properties": { + "provisioningState": "Succeeded" + } +} +JSON + printf "201" + exit 0 +fi + +if [[ "$url" == */api/v2/agent/tools/configure ]]; then + cat > "$out" <<'JSON' +{} +JSON + printf "200" + exit 0 +fi + +if [[ "$url" == */api/v1/scheduledtasks ]]; then + cat > "$out" <<'JSON' +[] +JSON + printf "200" + exit 0 +fi + +cat > "$out" <<'JSON' +{ + "files": [ + { "name": "chart-artifact-verification-md", "isIndexed": true, "errorReason": null }, + { "name": "document-index-md", "isIndexed": true, "errorReason": null }, + { "name": "ftk-output-style-md", "isIndexed": true, "errorReason": null }, + { "name": "known-issues-and-workarounds-md", "isIndexed": true, "errorReason": null }, + { "name": "onboarding-recommendations-md", "isIndexed": true, "errorReason": null }, + { "name": "teams-notification-guide-md", "isIndexed": true, "errorReason": null } + ], + "continuationToken": "" +} +JSON +printf "200" +'@ + + Set-PythonExtrasStub -BinDir $binDir + + Set-BashStub -Path (Join-Path $binDir 'sleep') -Content @' +#!/usr/bin/env bash +exit 0 +'@ + + try { + $command = "AGENT_APPLY_LOG='$logPath' SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS=0 SRE_AGENT_APPLY_RETRY_DELAY_SECONDS=0 bash '$script:PostProvisionScript' --endpoint https://example.azuresre.ai --subscription 00000000-0000-0000-0000-000000000000 --resource-group rg-test-customer --name customer-sre-agent --recipe '$script:RecipeDir' --build-dir '$buildDir'" + $result = Invoke-BashCommandWithPath $command $binDir + $result.ExitCode | Should -Be 0 -Because $result.Output + + $order = @(Get-Content -Path $logPath) + [array]::IndexOf($order, 'chief-financial-officer') | Should -BeLessThan ([array]::IndexOf($order, 'finops-practitioner')) + [array]::IndexOf($order, 'ftk-database-query') | Should -BeLessThan ([array]::IndexOf($order, 'finops-practitioner')) + [array]::IndexOf($order, 'ftk-hubs-agent') | Should -BeLessThan ([array]::IndexOf($order, 'finops-practitioner')) + } + finally { + Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'derives expected recipe connectors for verification' { + $verifyScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh') -Raw + + $verifyScript | Should -Match 'connectors\.json' + $verifyScript | Should -Match 'EXPECTED_CONNECTORS' + $verifyScript | Should -Match 'EXP_CONN_CT=.*EXPECTED_CONNECTORS' + $verifyScript | Should -Match 'EXP_CONN_NAMES=.*EXPECTED_CONNECTORS' + } + + It 'uploads the shared FinOps output style as a portal knowledge source' { + $applyExtrasScript = Get-Content -Path $script:ApplyExtrasScript -Raw + $buildExtrasScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/build-extras.py') -Raw + + Test-Path $script:OutputStylePath | Should -BeTrue + $buildExtrasScript | Should -Match 'claude-plugin/output-styles/ftk-output-style\.md' + $buildExtrasScript | Should -Match 'Output style knowledge document not found' + $applyExtrasScript | Should -Match 'knowledgeItems' + $applyExtrasScript | Should -Match '/api/v2/extendedAgent/connectors' + $applyExtrasScript | Should -Match 'Knowledge sources failed to index' + $applyExtrasScript | Should -Match 'KnowledgeFile' + $applyExtrasScript | Should -Not -Match ('sr' + 'ectl') + $applyExtrasScript | Should -Not -Match '/api/v1/agentmemory/files' + } + + It 'requires expected knowledge sources and verifies indexing' { + $expectedConfig = Get-Content -Path (Join-Path $script:RecipeDir 'expected-config.json') -Raw | ConvertFrom-Json + $verifyScript = Get-Content -Path $script:VerifyScript -Raw + + $expectedConfig.knowledgeSources.Count | Should -Be 6 + $expectedConfig.knowledgeSources | Should -Contain 'ftk-output-style.md' + $expectedConfig.PSObject.Properties.Name | Should -Not -Contain 'knowledgeDocs' + $verifyScript | Should -Match '/api/v2/extendedAgent/connectors' + $verifyScript | Should -Match '\$expected\[\] \| \. as \$name \| select\(\$actual \| index\(\$name\)\)' + $verifyScript | Should -Match 'Knowledge sources expected' + $verifyScript | Should -Match 'Knowledge sources indexed' + $verifyScript | Should -Match 'Unindexed knowledge sources' + $verifyScript | Should -Not -Match '/api/v1/agentmemory' + $verifyScript | Should -Not -Match '"knowledge_" \+' + $verifyScript | Should -Not -Match 'Knowledge connector rows' + } + + It 'does not duplicate response plan verification with dead incident-filter scan state' { + $verifyScript = Get-Content -Path $script:VerifyScript -Raw + + ([regex]::Matches($verifyScript, 'check "Filter names"')).Count | Should -Be 1 + $verifyScript | Should -Not -Match 'EXP_FILTERS' + $verifyScript | Should -Not -Match 'automations/incident-filters' + } + + It 'requires every scheduled task to apply the shared output style' { + $taskFiles = @(Get-ChildItem -Path $script:ScheduledTaskDir -Filter '*.yaml') + $taskFiles.Count | Should -Be 19 + + $expectedInstruction = [regex]::Escape('Output style: Apply `ftk-output-style.md`') + foreach ($file in $taskFiles) { + $content = Get-Content -Path $file.FullName -Raw + $content | Should -Match $expectedInstruction + } + } + + It 'deletes existing scheduled tasks before applying manifests' { + $applyExtrasScript = Get-Content -Path $script:ApplyExtrasScript -Raw + $verifyScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh') -Raw + + $applyExtrasScript | Should -Match 'delete_existing_scheduled_tasks' + $applyExtrasScript | Should -Match '/api/v1/scheduledtasks' + $applyExtrasScript | Should -Match 'dataplane_put_extended "scheduledtasks"' + $verifyScript | Should -Match 'Scheduled task duplicates' + $verifyScript | Should -Match '0 duplicates' + } + + } + + Context 'copy-and-update deployment contract' { + It 'rejects what-if because the copied starter-lab flow deploys directly' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test -n test-agent -l westus3 --what-if" + $result.ExitCode | Should -Be 2 + $result.Output | Should -Match 'unknown flag' + } + + It 'documents that azd is not used' { + if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' } + $result = Invoke-BashCommand "git grep -nF 'azd' -- src/templates/sre-agent/bin src/templates/sre-agent/infra docs-mslearn/toolkit/sre-agent/deploy.md src/templates/sre-agent/README.md" + $result.Output | Should -Match 'azd' + $result.Output | Should -Match 'not used|doesn''t use' + } + } +} diff --git a/src/scripts/Build-SreAgentTemplate.ps1 b/src/scripts/Build-SreAgentTemplate.ps1 new file mode 100644 index 000000000..8c2c2a5e2 --- /dev/null +++ b/src/scripts/Build-SreAgentTemplate.ps1 @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Builds the SRE Agent recipe package used by the portal deployment template. + + .PARAMETER DestDir + Release directory for the copied sre-agent template. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$DestDir +) + +$templateRoot = Resolve-Path "$PSScriptRoot/../templates/sre-agent" +$recipeDir = Join-Path $templateRoot "recipes/finops-hub" +$builder = Join-Path $templateRoot "bin/build-extras.py" +$assetsDir = Join-Path $DestDir "assets" +$workDir = Join-Path $assetsDir "sre-agent-recipe" +$extrasPath = Join-Path $workDir "extras.json" +$packagePath = Join-Path $assetsDir "sre-agent-recipe.zip" +$placeholderKustoUri = "https://placeholder.eastus2.kusto.windows.net/Hub" + +if (-not (Get-Command python3 -ErrorAction SilentlyContinue)) +{ + throw "python3 is required to build the SRE Agent recipe package." +} + +& "$PSScriptRoot/New-Directory.ps1" $assetsDir +Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue +& "$PSScriptRoot/New-Directory.ps1" $workDir + +Write-Host " Building SRE Agent recipe package..." +$summary = python3 $builder --recipe $recipeDir --output $extrasPath --kusto-connector-uri $placeholderKustoUri +if (-not $?) +{ + throw "Failed to build SRE Agent extras manifest." +} + +Write-Verbose " SRE Agent extras summary: $summary" +Remove-Item $packagePath -Force -ErrorAction SilentlyContinue +Compress-Archive -Path "$workDir/*" -DestinationPath $packagePath -Force +Remove-Item $workDir -Recurse -Force + +Write-Host " Created assets/sre-agent-recipe.zip" diff --git a/src/scripts/Build-Toolkit.ps1 b/src/scripts/Build-Toolkit.ps1 index cfa992eb6..dfd7e336e 100644 --- a/src/scripts/Build-Toolkit.ps1 +++ b/src/scripts/Build-Toolkit.ps1 @@ -193,17 +193,15 @@ $templates | ForEach-Object { # Create target directory $destDir = "$outdir/$templateName" Write-Verbose " Creating target directory: $destDir" - Remove-Item $destDir -Recurse -ErrorAction SilentlyContinue + Remove-Item $destDir -Recurse -Force -ErrorAction SilentlyContinue & "$PSScriptRoot/New-Directory.ps1" $destDir # Copy required files Write-Host " Copying files..." - $sourceFiles = Get-ChildItem $srcDir -Force | Where-Object { $_.Name -notin @(".build.config", ".buildignore", "scaffold.json") } + $sourceFiles = Get-ChildItem $srcDir | Where-Object { $_.Name -notin @(".build.config", ".buildignore", "scaffold.json") } Write-Verbose " Copying $($sourceFiles.Count) items from source to destination" $sourceFiles | Copy-Item -Destination $destDir -Recurse - Get-ChildItem $destDir -Force -Recurse -Filter ".DS_Store" | Remove-Item -Force - # Run custom build scripts after copying files (operates on release copy, not source) if ($buildConfig.scripts.Count -gt 0) { diff --git a/src/scripts/Package-Toolkit.ps1 b/src/scripts/Package-Toolkit.ps1 index 5b6301a92..8943edecd 100644 --- a/src/scripts/Package-Toolkit.ps1 +++ b/src/scripts/Package-Toolkit.ps1 @@ -209,6 +209,23 @@ function Copy-TemplateFiles() } Copy-FlatDeploymentFiles + + # Inject the recipe package SHA-256 into azuredeploy.json so the deployment + # script can verify package integrity before extraction. Runs only when a + # template ships both the compiled template and a recipe zip; no-op otherwise. + $deployJson = "$targetDir/azuredeploy.json" + $recipeZip = "$targetDir/sre-agent-recipe.zip" + if ((Test-Path $deployJson) -and (Test-Path $recipeZip)) + { + $deployContent = Get-Content $deployJson -Raw + if ($deployContent -match 'PLACEHOLDER_RECIPE_PACKAGE_SHA256') + { + $recipeSha = (Get-FileHash -Algorithm SHA256 -Path $recipeZip).Hash.ToLower() + $deployContent = $deployContent -replace 'PLACEHOLDER_RECIPE_PACKAGE_SHA256', $recipeSha + Set-Content -Path $deployJson -Value $deployContent -NoNewline + Write-Verbose " Injected recipe package SHA-256 ($recipeSha) into $deployJson" + } + } } else { diff --git a/src/templates/sre-agent/.build.config b/src/templates/sre-agent/.build.config new file mode 100644 index 000000000..ac067ed9b --- /dev/null +++ b/src/templates/sre-agent/.build.config @@ -0,0 +1,9 @@ +{ + "scripts": [ + "Build-SreAgentTemplate.ps1" + ], + "ignore": [ + "bin/__pycache__", + "submodules" + ] +} diff --git a/src/templates/sre-agent/.gitattributes b/src/templates/sre-agent/.gitattributes new file mode 100644 index 000000000..1bbd6959a --- /dev/null +++ b/src/templates/sre-agent/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +*.py text eol=lf diff --git a/src/templates/sre-agent/.gitignore b/src/templates/sre-agent/.gitignore new file mode 100644 index 000000000..c60cffcaa --- /dev/null +++ b/src/templates/sre-agent/.gitignore @@ -0,0 +1,3 @@ + +.gate/ +!bin/ diff --git a/src/templates/sre-agent/.upstream-pin b/src/templates/sre-agent/.upstream-pin new file mode 100644 index 000000000..e3b8a9b46 --- /dev/null +++ b/src/templates/sre-agent/.upstream-pin @@ -0,0 +1,5 @@ +upstream_repo=https://github.com/microsoft/sre-agent +upstream_path=labs/starter-lab/ +pinned_sha=a401e003a3874c374f5fff5278123a1f603fe270 +pinned_date=2026-05-25 +pinned_version=1.0.0 diff --git a/src/templates/sre-agent/AGENTS.md b/src/templates/sre-agent/AGENTS.md new file mode 100644 index 000000000..1bdb1778e --- /dev/null +++ b/src/templates/sre-agent/AGENTS.md @@ -0,0 +1,31 @@ +# Agent instructions + +**⛔ PROHIBITED: Manual Azure resource intervention.** Do not run Azure control-plane or data-plane commands against live SRE Agent resources outside the canonical entry point `bin/deploy.sh` and its owned helper scripts in `infra/`, `bin/apply-extras.sh`, and the deprecated compatibility wrapper `bin/post-provision.sh`. All Azure changes go through the release process. No exceptions. + +Load the `azure-sre-agent` skill at session start and after every compaction or summarization. + +## Template inventory + +| Component | Count | Details | +|-----------|-------|---------| +| Custom agents | 5 | `finops-practitioner` orchestrator plus four delegated subagents: `azure-capacity-manager`, `chief-financial-officer`, `ftk-database-query`, `ftk-hubs-agent` | +| Tool-bearing delegated subagents | 3 | `azure-capacity-manager`, `ftk-database-query`, `ftk-hubs-agent`; `finops-practitioner` and `chief-financial-officer` have no tools | +| Skills | 3 | `azure-capacity-management`, `azure-cost-management`, `finops-toolkit` | +| Tools | 50 | 37 generated Kusto tools from `src/queries/catalog/` plus 13 Python tools under `recipes/finops-hub/config/tools/` | +| Scheduled tasks | 19 | All owned by `finops-practitioner`; specialist agents are delegated by prompt, not scheduled directly | +| Connectors | 1 | Kusto connector to FinOps Hub ADX cluster | +| Knowledge docs | 6 | Five recipe knowledge files plus `ftk-output-style.md` from the Claude plugin output styles | + +## Key references + +- [README.md](README.md) — Deployment guide and architecture +- [CATALOG.md](CATALOG.md) — Authoritative FinOps Framework alignment, inventory, tool ownership, and scheduled-task catalog +- `bin/deploy.sh` — Canonical deployment entry point copied from the Microsoft starter-lab setup flow and updated for no-azd FinOps deployment +- `infra/` — Copied-and-updated Microsoft starter-lab Bicep baseline +- `recipes/finops-hub/` — Recipe content +- `../claude-plugin/output-styles/ftk-output-style.md` — Uploaded as SRE Agent knowledge and referenced by every scheduled task for report formatting +- `.upstream-pin` — Upstream canonical template pin + +## Scheduled task Teams delivery + +Scheduled-task entrypoint agents that deliver results must have explicit access to the Teams connector tools: `PostTeamsMessage`, `ReplyToTeamsMessage`, and `GetTeamsMessages`. When a Teams connector/channel is configured, results must be delivered through that configured Teams channel. diff --git a/src/templates/sre-agent/CATALOG.md b/src/templates/sre-agent/CATALOG.md new file mode 100644 index 000000000..ee224e796 --- /dev/null +++ b/src/templates/sre-agent/CATALOG.md @@ -0,0 +1,256 @@ +# FinOps toolkit SRE Agent - framework alignment catalog + +This catalog is the controlling reference for aligning the `recipes/finops-hub/` SRE Agent template to the FinOps Framework. Azure capacity-management content is included as implementation guidance under the relevant FinOps capabilities, not as a separate operating model. Framework names were checked against the FinOps Foundation references below on 2026-05-26. Keep the recipe content aligned to this file: + +- Agent defaults: `recipes/finops-hub/agent.json` +- Expected inventory: `recipes/finops-hub/expected-config.json` +- Custom agents: `recipes/finops-hub/config/subagents/*.yaml` +- Skills: `recipes/finops-hub/config/skills/*/` +- Tools: generated Kusto tools from `src/queries/catalog/*.kql` plus Python tools from `recipes/finops-hub/config/tools/*.yaml` +- Scheduled tasks: `recipes/finops-hub/automations/scheduled-tasks/*.yaml` +- Output style knowledge: `../claude-plugin/output-styles/ftk-output-style.md` + +## Implemented inventory + +| Component | Count | Source | +|-----------|------:|--------| +| Custom agents | 5 | Practitioner orchestrator plus four delegated subagents in `config/subagents/*.yaml` | +| Skills | 3 | `config/skills/*/` | +| Tools | 50 | 37 generated Kusto tools from `src/queries/catalog/*.kql` plus 13 Python tools from `config/tools/*.yaml` | +| Tool overrides | 9 | `config/built-in-tools.json` | +| Scheduled tasks | 19 | All set `spec.agent: finops-practitioner` in `automations/scheduled-tasks/*.yaml` | +| Connectors | 1 | `connectors.json`: FinOps Hub Kusto | +| Knowledge docs | 6 | Five recipe knowledge files plus `../claude-plugin/output-styles/ftk-output-style.md` | + +Tool mix: + +| Tool type | Count | +|-----------|------:| +| KustoTool | 37 | +| PythonTool | 13 | +| MCP tool namespaces | 0 | + +## Framework anchors + +The recipe maps to these external operating models: + +- FinOps Framework domains: Understand Usage & Cost, Quantify Business Value, Optimize Usage & Cost, and Manage the FinOps Practice. +- FinOps Framework capabilities: Data Ingestion, Allocation, Reporting & Analytics, Anomaly Management, Planning & Estimating, Forecasting, Budgeting, KPIs & Benchmarking, Unit Economics, Architecting & Workload Placement, Rate Optimization, Usage Optimization, Sustainability, Licensing & SaaS, FinOps Practice Operations, Governance, Policy & Risk, FinOps Assessment, Automation, Tools, & Services, FinOps Education & Enablement, Invoicing & Chargeback, and Intersecting Disciplines. +- Capabilities directly automated by this recipe: Data Ingestion, Allocation, Reporting & Analytics, Anomaly Management, Planning & Estimating, Forecasting, Budgeting, KPIs & Benchmarking, Unit Economics, Architecting & Workload Placement, Usage Optimization, Rate Optimization, FinOps Practice Operations, Governance, Policy & Risk, Automation, Tools, & Services, and Invoicing & Chargeback. +- Capabilities present as guidance or consultative context only: Sustainability, Licensing & SaaS, FinOps Assessment, FinOps Education & Enablement, and Intersecting Disciplines. The recipe does not ship scheduled tasks or specialist tools that directly implement these capabilities. +- Executive strategy alignment is treated as a leadership reporting outcome, not as a FinOps Framework capability. +- FinOps Framework principles: Teams need to collaborate, Business value drives technology decisions, Everyone takes ownership for their technology usage, FinOps data should be accessible, timely, and accurate, FinOps should be enabled centrally, and Take advantage of the variable cost model of the cloud. +- FinOps Framework personas: FinOps Practitioner coordinates the practice; Engineering owns technical usage and implementation; Finance and Leadership provide planning, decision rights, and business context; Product and Procurement contribute demand, value, and commitment context. +- FinOps Framework phases: Inform, Optimize, and Operate are iterative lenses applied to capabilities. They are not a replacement taxonomy and not a serial deployment order. +- Azure capacity management: capacity tooling is mapped into Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization, Governance, Policy & Risk, and Automation, Tools, & Services. Capacity reservations guarantee supply; reservations and savings plans reduce rate. These are coordinated but not interchangeable. + +Primary references: + +- FinOps Framework domains: +- FinOps Framework capabilities: +- FinOps Framework principles: +- FinOps Framework personas: +- FinOps Framework phases: +- Azure SRE Agent workflow automation: +- Azure SRE Agent scheduled tasks: +- Azure SRE Agent custom agents: +- Azure SRE Agent connectors: +- Microsoft SRE Agent repository and templates: +- Azure capacity governance for ISVs: + +## Agent operating model + +Azure SRE Agent automation is modeled as connector-provided tools, custom agents with scoped tool access, and scheduled tasks that trigger the selected custom agent. Knowledge files are reference material; they do not grant tools. This recipe follows that model by assigning custom tools to the specialist that owns the capability, then routing every scheduled task to `finops-practitioner` so the practitioner owns the operating rhythm and delegates evidence collection. + +The recipe defaults the parent Azure SRE Agent to the Azure OpenAI provider (`MicrosoftFoundry` in ARM) with `Automatic` model routing. SRE Agent selects the model within the provider for each task. Custom agents in this recipe do not declare their own model providers or pinned model names because the documented SRE Agent configuration surface is provider-level on the parent agent. + +All tool-bearing specialist agents include `SearchMemory` so they can read uploaded Knowledge Sources, including `ftk-output-style.md`, known issues, and prior operational notes. Specialist ownership still controls action scope: Kusto, capacity, and hubs responsibilities remain separated. The practitioner has no direct tools and delegates all evidence, visualization, delivery, and remediation work. `chief-financial-officer` has no tools and receives packaged evidence through practitioner handoff. + +Scheduled reports must classify gaps before recommending action: product or deployment defects, data sufficiency limits, or customer-owned delegation. Sparse UAT Hub history, empty transaction diagnostics, or missing multi-period trigger evidence reduce confidence and should not be reported as product failure by themselves. The deployment owns Reader access for the deployment subscription by default. Broader management group, billing, quota, or cross-subscription access is customer-owned delegation unless this template explicitly owns that role assignment. + +| Custom agent | Role in the operating model | Direct tool policy | FinOps / capacity alignment | +|----------|-----------------------------|--------------------|----------------------------| +| `finops-practitioner` | Practice lead and scheduled-report orchestrator. Owns the FinOps operating rhythm, applies the output style, frames business questions, routes evidence requests, and turns specialist outputs into actions. | No direct tools. Delegates Kusto evidence to `ftk-database-query`, capacity evidence to `azure-capacity-manager`, Hub platform and infrastructure checks to `ftk-hubs-agent`, and executive decision framing to `chief-financial-officer`. | FinOps Practitioner persona; central enablement; collaboration; Reporting & Analytics, Governance, Policy & Risk, Usage Optimization, Rate Optimization, and leadership strategy alignment. | +| `ftk-database-query` | FinOps Hub evidence specialist. Owns all Kusto/FOCUS query execution, Kusto result interpretation, schema guidance, and query diagnostics. | Owns every generated `KustoTool` from `src/queries/catalog/*.kql`. Does not own Azure CLI, Python capacity, infrastructure health, or remediation tools. No scheduled-task ownership. | Understand Usage & Cost; Data Ingestion, Allocation, Reporting & Analytics, Anomaly Management, pricing, recommendation, KPI, unit economics, and commitment evidence. | +| `azure-capacity-manager` | Engineering and platform capacity specialist. Owns quota, region, zone, SKU, CRG, non-compute quota, database quota, AKS readiness, and capacity-to-rate coordination. | Owns capacity Python tools only. Does not own Kusto, Resource Graph, Hub freshness, Azure CLI, or remediation deployment tools. | Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Governance, Policy & Risk, and Automation, Tools, & Services. | +| `chief-financial-officer` | Consultative finance and leadership persona. Frames budget, forecast, commitment, risk, and investment tradeoffs for executive audiences. | Should not own scheduled tasks or raw data collection. Consumes evidence packages from `finops-practitioner` and specialists. | Finance and Leadership personas; Quantify Business Value; leadership strategy alignment; Budgeting, Forecasting, Unit Economics, investment decisions. | +| `ftk-hubs-agent` | FinOps Hub platform specialist. Owns hub health, deployment, upgrade, connector, export freshness, monitoring scope, and analytics-backend readiness. | Owns platform discovery, Resource Graph inventory, `data-freshness-check`, ADX SKU preflight, hub troubleshooting tools, and explicit remediation deployment tools. Does not own business cost-analysis reports. | Data Ingestion and Reporting & Analytics foundation; Automation, Tools, & Services; Governance, Policy & Risk. | + +Hard boundaries: + +- `finops-practitioner` must never query Kusto directly. Any prompt that needs `Costs()`, `Prices()`, `Recommendations()`, `Transactions()`, or a Kusto tool must delegate that evidence request to `ftk-database-query`. +- `finops-practitioner` has no direct tools. It must use handoffs for Kusto, capacity Python, Resource Graph, Hub infrastructure checks, visualization, delivery, and remediation. +- Every scheduled task must set `spec.agent: finops-practitioner`. Specialist agents are delegated subagents, not scheduled-task entrypoints. +- `chief-financial-officer` must not run autonomous scheduled tasks. Finance is a consulted persona for decisions and executive packaging, not the worker that gathers evidence. +- Capacity tasks must keep pricing commitments and capacity guarantees distinct. Use `azure-capacity-manager` for quota/CRG/access/SKU evidence and `ftk-database-query` for Kusto-backed rate and cost evidence. +- Remediation tools such as budget deployment, anomaly alert deployment, and Advisor suppression are interactive or explicitly approved remediation actions. Scheduled audits may recommend them but must not silently execute them. + +## Source lineage + +The SRE Agent recipe is an orchestration layer over the toolkit and azcapman assets, not a separate FinOps model. + +| Source | Role in this recipe | +|--------|---------------------| +| `src/templates/claude-plugin/agents/*.md` and `src/templates/copilot-plugin/agents/*.agent.md` | Canonical toolkit agent personas for FinOps practitioner, FinOps Hub database, CFO, capacity, and hubs specialists. | +| `src/templates/agent-skills/finops-toolkit/` | Canonical FinOps Toolkit skill content and query catalog guidance used by the SRE recipe. | +| `src/templates/agent-skills/azure-cost-management/` | Canonical Azure Cost Management skill content for Advisor, budgets, anomaly alerts, savings plans, reservations, and related cost workflows. | +| `src/templates/sre-agent/submodules/azcapman/agents/azure-capacity-manager.md` | Canonical azcapman capacity specialist persona. | +| `src/templates/sre-agent/submodules/azcapman/skills/azure-capacity-management/` | Canonical azcapman capacity skill and references. The recipe uses symlinks; do not duplicate this content. | +| `src/queries/` | Canonical FinOps Hub Kusto query catalog. All custom Kusto tools are generated from this catalog; explicit recipe YAML Kusto definitions are rejected so `src/queries/catalog/*.kql` remains the single Kusto source. | +| `src/templates/claude-plugin/output-styles/ftk-output-style.md` | Shared FinOps Toolkit report style uploaded as SRE Agent knowledge and referenced by every scheduled task. | + +## Custom agent capability matrix + +This matrix is the reference for which custom agent owns each FinOps capability surface, which tools it can call directly, and which scheduled tasks it owns. It is derived from `src/templates/claude-plugin/agents/*.md`, `src/templates/copilot-plugin/agents/*.agent.md`, `src/templates/agent-skills/`, `src/templates/sre-agent/recipes/finops-hub/config/subagents/*.yaml`, `src/templates/sre-agent/submodules/azcapman/agents/azure-capacity-manager.md`, `src/templates/sre-agent/submodules/azcapman/skills/azure-capacity-management/SKILL.md`, and `src/queries/`. + +| Custom agent | Persona / function | FinOps capabilities owned or directly implemented | Direct custom tools | Scheduled tasks owned | Required handoffs and consults | +|----------|--------------------|----------------------------------------------------|---------------------|---------------------|-------------------------------| +| `finops-practitioner` | FinOps Practitioner and operating-rhythm orchestrator. Leads practice cadence, report assembly, output style, and remediation approval flow. | Allocation, Reporting & Analytics orchestration, Anomaly Management orchestration, Budgeting, KPIs & Benchmarking, Unit Economics, Usage Optimization orchestration, Rate Optimization orchestration, FinOps Practice Operations, Governance, Policy & Risk, Automation, Tools, & Services, and Invoicing & Chargeback orchestration. | None. | All 19 scheduled tasks. | Must delegate all Kusto/FOCUS evidence to `ftk-database-query`; capacity, quota, SKU, zone, region, and CRG evidence to `azure-capacity-manager`; executive finance framing to `chief-financial-officer`; FinOps Hub platform readiness, Resource Graph, visualization, delivery, and infrastructure/remediation tooling to `ftk-hubs-agent`. | +| `ftk-database-query` | FinOps Hub evidence specialist. Owns Kusto, FOCUS, `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()` evidence packages. | Data Ingestion evidence, Allocation evidence, Reporting & Analytics evidence, Anomaly Management evidence, Forecasting evidence, KPIs & Benchmarking evidence, Unit Economics evidence, Rate Optimization evidence, Usage Optimization evidence, Invoicing & Chargeback evidence. | All 37 generated FinOps Hub `KustoTool` definitions listed in the Tool inventory from `src/queries/catalog/*.kql`. | None. This specialist is invoked by other scheduled tasks for evidence collection. | Returns evidence packages to `finops-practitioner`; does not own business recommendations, infrastructure checks, capacity evidence, or remediation decisions. | +| `azure-capacity-manager` | Engineering and platform capacity specialist sourced from the toolkit and azcapman. Owns Azure capacity, quota, SKU, region, zone, CRG, AKS, and non-compute limit evidence. | Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Governance, Policy & Risk, Automation, Tools, & Services. | `benefit-recommendations`, `sku-availability`, `vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `capacity-reservation-groups`. | None. Invoked by `finops-practitioner` scheduled tasks for capacity evidence. | Returns capacity evidence to `finops-practitioner`. It does not query Kusto, run Resource Graph, check Hub freshness, or hand off to other agents. | +| `chief-financial-officer` | Finance and Leadership consultative persona. Frames budget, forecast, capital allocation, commitment risk, and executive tradeoffs. | Budgeting, Forecasting, KPIs & Benchmarking, Unit Economics, Rate Optimization decision framing, Invoicing & Chargeback framing, and leadership strategy alignment. | None. | None. Finance is consulted; it does not run autonomous scheduled tasks. | Consumes packaged evidence from `finops-practitioner`, `ftk-database-query`, `azure-capacity-manager`, and `ftk-hubs-agent`. Must not query Kusto, collect raw telemetry, investigate capacity directly, or deliver reports. | +| `ftk-hubs-agent` | FinOps Hub platform specialist. Owns hub deployment, upgrade, health, connector, export, monitoring scope, analytics-backend readiness, Resource Graph inventory, and explicit remediation deployment tooling. | Data Ingestion platform readiness, Reporting & Analytics platform readiness, Automation, Tools, & Services, Governance, Policy & Risk. | `data-freshness-check`, `resource-graph-query`, `sku-availability`, `deploy-anomaly-alert`, `deploy-budget`, `deploy-bulk-anomaly-alerts`, `deploy-bulk-budgets`, `suppress-advisor-recommendations`. | None. Invoked by `finops-practitioner` scheduled tasks for Hub platform and infrastructure evidence. | Returns infrastructure evidence to `finops-practitioner`. It does not own business cost analysis, capacity evidence, or executive framing. | + +## Capability coverage matrix + +The phase column is a primary operating lens, not a workflow gate. Scheduled tasks can produce Inform evidence, Optimize recommendations, and Operate actions in a single run when that matches the task intent. + +| FinOps capability | Domain | Primary phase lens | Recipe owner | Scheduled tasks | Primary tool families | +|-------------------|--------|--------------------|--------------|-----------------|-----------------------| +| Data Ingestion | Understand Usage & Cost | Inform | `ftk-hubs-agent`, `ftk-database-query` | `HubsHealthCheck`, `MonitoringScopeValidation`; evidence support for `Monthly` and `Semiannual` | `data-freshness-check`, `cost-visibility-delay`, `data-update-frequency`, FinOps Hub Kusto tools, Hub platform checks | +| Allocation | Understand Usage & Cost | Inform | `finops-practitioner`, `ftk-database-query` | `Monthly`, `Semiannual`, `AIWorkloadCostAnalysis` | `cost-by-financial-hierarchy`, `ai-cost-by-application`, `allocation-accuracy-index`, `percentage-unallocated-costs`, `percentage-untagged-costs`, `tagging-policy-compliance`, resource inventory evidence | +| Reporting & Analytics | Understand Usage & Cost | Inform | `finops-practitioner`, `ftk-database-query` | `Monthly`, `Semiannual`, `CostOptimization`, `AIWorkloadCostAnalysis` | Cost trend, top-N, savings, pricing, AI, FinOps Hub Kusto evidence, visualization, and notification tools | +| Anomaly Management | Understand Usage & Cost | Inform / Operate | `finops-practitioner`, `ftk-database-query` | `AlertCoverageAudit`, `CostOptimization`, `Monthly`, `Semiannual` | `cost-anomaly-detection`, `anomaly-detection-rate`, `anomaly-variance-total`, `deploy-anomaly-alert`, `deploy-bulk-anomaly-alerts`, `resource-graph-query` | +| Planning & Estimating | Quantify Business Value | Inform | `azure-capacity-manager`, `finops-practitioner` | `CapacityMonthlyPlanning`, `CapacityQuarterlyStrategy`, `StoragePaasGrowthForecast`, `Semiannual` | `vm-quota-usage`, `non-compute-quotas`, `capacity-reservation-groups`, `cost-forecasting-model` | +| Forecasting | Quantify Business Value | Inform | `azure-capacity-manager`, `ftk-database-query`, `finops-practitioner` | `CapacityMonthlyPlanning`, `StoragePaasGrowthForecast`, `Monthly`, `Semiannual` | `cost-forecasting-model`, `monthly-cost-trend`, quota and capacity trend tools | +| Budgeting | Quantify Business Value | Inform / Operate | `finops-practitioner`, `chief-financial-officer` | `BudgetCoverageAudit`, `Monthly`, `Semiannual` | `deploy-budget`, `deploy-bulk-budgets`, `monthly-cost-trend`, `cost-forecasting-model` | +| KPIs & Benchmarking | Quantify Business Value | Inform | `finops-practitioner`, `ftk-database-query`, `chief-financial-officer` | `Monthly`, `Semiannual`, `AIWorkloadCostAnalysis` | `service-price-benchmarking`, `savings-summary-report`, FinOps KPI Kusto tools, AI model comparison, visualization tools | +| Unit Economics | Quantify Business Value | Inform | `finops-practitioner`, `ftk-database-query`, `chief-financial-officer` | `AIWorkloadCostAnalysis`, `Monthly`, `Semiannual` | AI token and model cost tools, `compute-cost-per-core`, `cost-per-gb-stored`, `cost-by-financial-hierarchy`, `service-price-benchmarking` | +| Architecting & Workload Placement | Optimize Usage & Cost | Optimize | `azure-capacity-manager`, `finops-practitioner` | `CapacityWeeklySupplyReview`, `SkuAvailabilityAudit`, `ComputeUtilizationTrend`, `CapacityQuarterlyStrategy` | `sku-availability`, `resource-graph-query`, `vm-quota-usage`, `capacity-reservation-groups` | +| Rate Optimization | Optimize Usage & Cost | Optimize | `finops-practitioner`, `ftk-database-query`, `azure-capacity-manager`, `chief-financial-officer` | `BenefitRecommendationReview`, `CapacityMonthlyPlanning`, `CapacityQuarterlyStrategy`, `Monthly`, `Semiannual` | `benefit-recommendations`, `commitment-discount-utilization`, `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, `cost-optimization-index`, `macc-consumption-vs-commitment`, `reservation-recommendation-breakdown`, `savings-summary-report`, `service-price-benchmarking` | +| Usage Optimization | Optimize Usage & Cost | Optimize | `finops-practitioner`, `azure-capacity-manager`, `ftk-database-query` | `CostOptimization`, `ComputeUtilizationTrend`, `CapacityDailyMonitor`, `CapacityWeeklySupplyReview` | `top-services-by-cost`, `top-resource-types-by-cost`, `compute-cost-per-core`, `cost-per-gb-stored`, `storage-tier-distribution`, `resource-graph-query`, quota and CRG tools | +| Sustainability | Optimize Usage & Cost | Optimize | Guidance only through `finops-practitioner` | None | No direct recipe tool. Consider only when optimization decisions materially affect sustainability goals. | +| Licensing & SaaS | Optimize Usage & Cost | Optimize | Guidance only through `finops-practitioner` and `chief-financial-officer` | None | No direct recipe tool. Azure Hybrid Benefit evidence can appear in FinOps Hub cost data, but this recipe does not ship a Licensing & SaaS workflow. | +| FinOps Practice Operations | Manage the FinOps Practice | Operate | `finops-practitioner` | `Monthly`, `Semiannual`, all governance review tasks | Scheduled task cadence, output style, memory, Teams/Outlook delivery pattern | +| Governance, Policy & Risk | Manage the FinOps Practice | Operate | `finops-practitioner`, `azure-capacity-manager`, `ftk-hubs-agent` | `AdvisorSuppressionReview`, `AlertCoverageAudit`, `BudgetCoverageAudit`, `CapacityDailyMonitor`, `MonitoringScopeValidation` | `resource-graph-query`, budget/anomaly deployment tools, suppression tools, quota tools | +| FinOps Assessment | Manage the FinOps Practice | Operate | Guidance only through `finops-practitioner` | None | No direct recipe tool. Assessment is advisory unless a future task is added. | +| Automation, Tools, & Services | Manage the FinOps Practice | Operate | `finops-practitioner`, `azure-capacity-manager`, `ftk-hubs-agent` | All scheduled tasks | Scheduled tasks, custom tools, built-in visualization tools, Teams and Outlook connector tools | +| FinOps Education & Enablement | Manage the FinOps Practice | Operate | Guidance only through `finops-practitioner` | None | No direct recipe tool. Knowledge files support agent context but are not a training workflow. | +| Invoicing & Chargeback | Manage the FinOps Practice | Operate | `finops-practitioner`, `ftk-database-query`, `chief-financial-officer` | `Monthly`, `Semiannual` | `cost-by-financial-hierarchy`, transaction tools, monthly trend and allocation evidence | +| Intersecting Disciplines | Manage the FinOps Practice | Operate | Guidance only through `finops-practitioner` | None | No direct recipe tool. Security, ITSM, ITFM, ITAM, and sustainability intersections are consultative context. | + +## Skills + +| Skill | Path | +|-------|------| +| `azure-capacity-management` | `config/skills/azure-capacity-management/` | +| `azure-cost-management` | `config/skills/azure-cost-management/` | +| `finops-toolkit` | `config/skills/finops-toolkit/` | + +## Scheduled task ownership map + +| Task | Owning agent | Schedule | Framework alignment | Required delegates and consults | +|------|------------|----------|---------------------|----------------------------------| +| `HubsHealthCheck` | `finops-practitioner` | `0 6 * * *` | Understand Usage & Cost: Data Ingestion; Manage the FinOps Practice: Automation, Tools, & Services | Delegates Hub platform evidence to `ftk-hubs-agent`. | +| `CapacityDailyMonitor` | `finops-practitioner` | `30 6 * * *` | Automation, Tools, & Services; Usage Optimization; Governance, Policy & Risk | Delegates capacity evidence to `azure-capacity-manager`; no CFO consult. | +| `ComputeUtilizationTrend` | `finops-practitioner` | `0 7 * * 1` | Usage Optimization; Planning & Estimating; Forecasting | Delegates capacity and inventory evidence to `azure-capacity-manager`. | +| `CostOptimization` | `finops-practitioner` | `0 8 * * 1` | Optimize Usage & Cost: Usage Optimization and Rate Optimization; Manage the FinOps Practice: Governance, Policy & Risk | Delegates Kusto evidence to `ftk-database-query`; delegates quota/capacity feasibility to `azure-capacity-manager`; consults `chief-financial-officer` for commitment and priority framing when decisions are material. | +| `CapacityWeeklySupplyReview` | `finops-practitioner` | `0 8 * * 1` | Planning & Estimating; Forecasting; Architecting & Workload Placement; Usage Optimization; Rate Optimization support | Delegates capacity evidence to `azure-capacity-manager`; asks `ftk-database-query` for Kusto-backed commitment, savings, and forecast evidence. | +| `NonComputeQuotaAudit` | `finops-practitioner` | `0 7 * * 2` | Automation, Tools, & Services; Governance, Policy & Risk | Delegates non-compute quota evidence to `azure-capacity-manager`. | +| `DbQuotaAudit` | `finops-practitioner` | `0 7 * * 3` | Automation, Tools, & Services; Governance, Policy & Risk | Delegates database quota evidence to `azure-capacity-manager`. | +| `SkuAvailabilityAudit` | `finops-practitioner` | `30 7 * * 3` | Planning & Estimating; Architecting & Workload Placement; region and SKU readiness | Delegates SKU availability evidence to `azure-capacity-manager`; consults `ftk-hubs-agent` only for FinOps Hub ADX backend deployment readiness. | +| `MonitoringScopeValidation` | `finops-practitioner` | `0 9 * * 4` | Understand Usage & Cost: Data Ingestion and Reporting & Analytics foundation | Delegates Hub platform and inventory evidence to `ftk-hubs-agent`. | +| `BenefitRecommendationReview` | `finops-practitioner` | `0 8 * * 5` | Optimize Usage & Cost: Rate Optimization; Quantify Business Value | Delegates Kusto recommendation/utilization evidence to `ftk-database-query`; consults `chief-financial-officer` for approval framing and commitment risk. | +| `AdvisorSuppressionReview` | `finops-practitioner` | `0 9 1 * *` | Optimize Usage & Cost; Governance, Policy & Risk | Uses inventory/governance evidence; remediation requires explicit approval. | +| `AIWorkloadCostAnalysis` | `finops-practitioner` | `0 10 1 * *` | Quantify Business Value: Unit Economics; Understand Usage & Cost: Reporting & Analytics; leadership strategy alignment for AI | Delegates AI cost Kusto evidence to `ftk-database-query`; delegates GPU/capacity evidence to `azure-capacity-manager`; consults `chief-financial-officer` for investment and unit-economics framing. | +| `CapacityMonthlyPlanning` | `finops-practitioner` | `0 9 1 * *` | Planning & Estimating; Forecasting; Budgeting support; Architecting & Workload Placement | Delegates capacity evidence to `azure-capacity-manager`; asks `ftk-database-query` for Kusto-backed forecast/rate context; may consult `chief-financial-officer` for budget guardrails. | +| `StoragePaasGrowthForecast` | `finops-practitioner` | `0 8 1 * *` | Planning & Estimating; Forecasting; Automation, Tools, & Services | Delegates non-compute and inventory evidence to `azure-capacity-manager`. | +| `Monthly` | `finops-practitioner` | `15 17 5 * *` | Understand Usage & Cost: Reporting & Analytics and Anomaly Management; Optimize Usage & Cost: Usage Optimization and Rate Optimization | Delegates all Kusto evidence to `ftk-database-query`; delegates capacity risk evidence to `azure-capacity-manager`; consults `chief-financial-officer` for material commitment or forecast decisions. | +| `Semiannual` | `finops-practitioner` | `0 9 5 1,7 *` | Quantify Business Value: Forecasting, Budgeting, KPIs & Benchmarking; Manage the FinOps Practice leadership reporting | Delegates fiscal Kusto evidence to `ftk-database-query`; delegates capacity risk evidence to `azure-capacity-manager` where needed; consults `chief-financial-officer` for executive report framing. | +| `BudgetCoverageAudit` | `finops-practitioner` | `0 8 15 * *` | Quantify Business Value: Budgeting; Manage the FinOps Practice: Governance, Policy & Risk | Uses governance inventory; budget deployment requires explicit approval. | +| `AlertCoverageAudit` | `finops-practitioner` | `0 8 16 * *` | Understand Usage & Cost: Anomaly Management; Manage the FinOps Practice: Governance, Policy & Risk and Automation, Tools, & Services | Uses governance inventory; anomaly alert deployment requires explicit approval. | +| `CapacityQuarterlyStrategy` | `finops-practitioner` | `0 9 1 1,4,7,10 *` | Leadership strategy alignment; Planning & Estimating; Forecasting; Architecting & Workload Placement; Rate Optimization support | Delegates strategic capacity evidence to `azure-capacity-manager`; asks `ftk-database-query` for Kusto-backed cost/rate evidence; consults `chief-financial-officer` for portfolio tradeoff framing. | + +## Tool inventory + +### Kusto tools + +All Kusto tools are owned by `ftk-database-query`. Other agents request Kusto evidence from that specialist instead of calling these tools directly. + +| Tool | Purpose | +|------|---------| +| `ai-cost-by-application` | Breaks down Azure OpenAI costs by application, team, and environment tags. | +| `ai-daily-trend` | Reports daily Azure OpenAI cost and token consumption trends. | +| `ai-model-cost-comparison` | Compares cost per 1K tokens across Azure OpenAI model versions. | +| `ai-token-usage-breakdown` | Breaks Azure OpenAI token consumption down by model version and input/output direction. | +| `allocation-accuracy-index` | Measures directly attributed cost as a share of total effective cost. | +| `anomaly-detection-rate` | Measures the share of effective spend on anomaly-flagged daily buckets. | +| `anomaly-variance-total` | Quantifies signed and absolute unpredicted spend variance for anomaly events. | +| `commitment-discount-waste` | Measures unused commitment value as a share of total commitment cost. | +| `commitment-discount-utilization` | Analyzes consumed core hours by commitment discount type. | +| `commitment-utilization-score` | Computes commitment utilization amount, potential, and score by commitment and currency. | +| `compute-cost-per-core` | Computes hourly and effective average compute cost per consumed vCPU core hour. | +| `compute-spend-commitment-coverage` | Measures compute spend covered by commitment discounts. | +| `cost-anomaly-detection` | Detects cost spikes and drops over a configurable history window. | +| `cost-by-financial-hierarchy` | Summarizes costs across billing profile, invoice section, team, product, application, and environment. | +| `cost-by-region-trend` | Summarizes effective cost by Azure region. | +| `cost-forecasting-model` | Forecasts future effective cost from historical cost data. | +| `cost-optimization-index` | Computes the hub-wide Cost Optimization Index from current recommendations and windowed cost. | +| `cost-per-gb-stored` | Calculates storage cost per normalized GB-month. | +| `cost-visibility-delay` | Measures cost data visibility delay from charge period end to Hub ingestion. | +| `costs-enriched-base` | Returns enriched row-level cost and usage samples for drill-down analysis. | +| `data-update-frequency` | Measures Hub ingestion update cadence from distinct ingestion timestamps. | +| `macc-consumption-vs-commitment` | Measures MACC consumption versus commitment drawdown by billing profile and month. | +| `monthly-cost-change-percentage` | Calculates month-over-month billed and effective cost change. | +| `monthly-cost-trend` | Returns billed and effective cost totals by month. | +| `percentage-unallocated-costs` | Measures the share of effective cost without allocation evidence. | +| `percentage-untagged-costs` | Measures the share of effective cost associated with resources that have no tags. | +| `quarterly-cost-by-resource-group` | Returns top resource-group cost rows by subscription and month. | +| `reservation-recommendation-breakdown` | Analyzes Microsoft reservation recommendations from `Recommendations()`. | +| `savings-summary-report` | Summarizes list cost, effective cost, negotiated discount savings, commitment savings, and effective savings rate. | +| `service-price-benchmarking` | Benchmarks services by list, contracted, and effective cost. | +| `storage-tier-distribution` | Summarizes storage cost and GB-month distribution by access-tier bucket. | +| `tagging-policy-compliance` | Measures cost-weighted compliance with required tag keys. | +| `top-commitment-transactions` | Returns top non-usage commitment discount purchase transactions. | +| `top-other-transactions` | Returns top non-usage, non-commitment purchase transactions. | +| `top-resource-groups-by-cost` | Returns top resource groups by effective cost. | +| `top-resource-types-by-cost` | Returns top resource types by count and effective cost. | +| `top-services-by-cost` | Returns top Azure services by effective cost. | + +### Built-in tools + +The recipe enables SRE Agent platform tools through `config/built-in-tools.json` during the apply-extras step. These are platform tools, not custom recipe tools. FinOps Hub database access remains owned by the custom Kusto tools assigned to `ftk-database-query`. + +| Category | Enabled tools | +|----------|---------------| +| Log Query | `QueryLogAnalyticsByWorkspaceId`, `QueryAppInsightsByAppId`, `QueryAppInsightsByResourceId`, `QueryLogAnalyticsByResourceId` | +| Visualization | `PlotScatter`, `PlotHeatmap`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation` | + +### Custom Azure and Hub tools + +| Tool | Assigned agents | Purpose | +|------|-----------------|---------| +| `benefit-recommendations` | `azure-capacity-manager` | Gets Cost Management benefit recommendations for savings plans and reserved instances. | +| `capacity-reservation-groups` | `azure-capacity-manager` | Lists capacity reservation groups and reports reserved versus allocated capacity. | +| `data-freshness-check` | `ftk-hubs-agent` | Checks FinOps Hub function data freshness through Azure Data Explorer REST queries. | +| `db-service-quotas` | `azure-capacity-manager` | Reports SQL DB, SQL MI, Cosmos DB, PostgreSQL Flex, and MySQL Flex quota and region access signals. | +| `deploy-anomaly-alert` | `ftk-hubs-agent` | Creates or updates a Cost Management scheduled action for anomaly detection after explicit remediation approval. | +| `deploy-budget` | `ftk-hubs-agent` | Creates or updates a subscription-level Cost Management budget after explicit remediation approval. | +| `deploy-bulk-anomaly-alerts` | `ftk-hubs-agent` | Creates or updates anomaly alert scheduled actions across subscriptions discovered from a management group after explicit remediation approval. | +| `deploy-bulk-budgets` | `ftk-hubs-agent` | Creates or updates Cost Management budgets across subscriptions discovered from a management group after explicit remediation approval. | +| `non-compute-quotas` | `azure-capacity-manager` | Checks non-compute Azure service quota utilization. | +| `resource-graph-query` | `ftk-hubs-agent` | Runs Azure Resource Graph KQL across one or more subscriptions. | +| `sku-availability` | `azure-capacity-manager`, `ftk-hubs-agent` | Lists regional SKU availability for Azure Compute or Azure Data Explorer. | +| `suppress-advisor-recommendations` | `ftk-hubs-agent` | Suppresses selected Azure Advisor recommendations across subscriptions under a management group after explicit remediation approval. | +| `vm-quota-usage` | `azure-capacity-manager` | Reports Azure VM family quota usage and quota risk by subscription and region. | + +## Operating notes + +- The Kusto connector is applied by `bin/apply-extras.sh` as an SRE Agent connector child resource when a Kusto URI is provided. The Bicep deployment grants the agent managed identity `AllDatabasesViewer` on the Azure Data Explorer cluster before the Kusto connector is created. This is required because the Hub functions can resolve cross-database references such as `Ingestion`. `finops-hub-kusto` supports the recipe's custom query tools. Scheduled tasks are applied by the same helper through the SRE Agent data plane. +- The portal Team onboarding wizard is part of the expected customer flow and must not be bypassed. The deployment supports it by assigning deployment-subscription Reader by default; adding the agent resource group, explicit target resource groups, and same-subscription FinOps Hub resource group to the agent managed-resource scope; and assigning the agent identity target-scope RBAC for Azure Resource Graph and Azure CLI discovery. +- When the target Azure Data Explorer cluster denies public query access, `bin/deploy.sh` still deploys all resources, grants cluster `AllDatabasesViewer`, and creates the connector, then warns that private endpoint ADX blocks direct KQL queries from the hosted SRE Agent and links to the SRE Agent known limitations. +- The SRE Agent resource is deployed with `Microsoft.App/agents@2026-01-01`, `upgradeChannel: Preview`, `experimentalSettings.EnableSandboxGroup`, and `experimentalSettings.EnableWorkspaceTools`. +- Skills are applied with their `SKILL.md` content and tool references. Supporting reference material that must be available to the agent should be promoted to KnowledgeFile sources because the current SRE Agent data plane rejects skill payloads with non-empty `additionalFiles`. +- `bin/apply-extras.sh` uploads `ftk-output-style.md` from the Claude plugin output styles as a portal-visible `KnowledgeFile` source and verifies the expected KnowledgeFile sources are indexed. Every scheduled task explicitly applies that style for report, Teams-message, and Outlook-email output. +- `README.md` carries the deployment workflow; this file carries the detailed implemented recipe inventory. +- Add or remove scheduled task YAML, tool YAML, subagent YAML, or skill directories first, then update this catalog and `README.md` in the same change. diff --git a/src/templates/sre-agent/README.md b/src/templates/sre-agent/README.md new file mode 100644 index 000000000..00bc6a6e5 --- /dev/null +++ b/src/templates/sre-agent/README.md @@ -0,0 +1,240 @@ +# FinOps toolkit SRE Agent + +Deploy and configure an Azure SRE Agent with the FinOps toolkit recipe under `recipes/finops-hub/`. + +The deployment flow is copied from the Microsoft SRE Agent starter lab and updated for the FinOps toolkit: + +- Azure CLI + Bicep deploy the SRE Agent infrastructure. +- `bin/apply-extras.sh` applies recipe assets that are not deployed by Bicep: connectors, KnowledgeFile sources, built-in tool configuration, tools, skills, subagents, and scheduled tasks. +- `azd` is not used. + +## What you get + +| Component | Count | Description | +|-----------|-------|-------------| +| SRE Agent | 1 | `Microsoft.App/agents` resource | +| Model provider | 1 | Azure OpenAI provider-level routing (`MicrosoftFoundry` ARM value, `Automatic` model routing) | +| Managed identity | 1 | Agent system-assigned managed identity | +| Log Analytics | 1 | Workspace for agent telemetry | +| Application Insights | 1 | Linked to Log Analytics | +| Custom agents | 5 | FinOps practitioner orchestrator plus CFO, capacity, database-query, and hubs specialists | +| Skills | 3 | Azure capacity management, Azure cost management, and FinOps Toolkit | +| Tools | 50 | Kusto, capacity, and Hub infrastructure tools | +| Tool overrides | 9 | Enables SRE Agent Log Query and Visualization tools | +| Scheduled tasks | 19 | Recurring FinOps, capacity, governance, and reporting tasks, all owned by `finops-practitioner` | +| Connectors | 1 | Optional FinOps Hub Kusto connector when `--cluster-uri` is provided | +| Knowledge docs | 6 | Five recipe knowledge docs plus the FinOps Toolkit output style | + +## Current recipe inventory + +The current implementation is the `recipes/finops-hub/` recipe. [CATALOG.md](CATALOG.md) is the authoritative inventory and FinOps Framework alignment reference, including the subagent-to-capability, tool, and scheduled-task matrix; this section summarizes what ships. + +| Agent | Scheduled tasks owned | Focus | +|-------|----------------------:|-------| +| `finops-practitioner` | 19 | Sole scheduled-task owner. Runs the operating rhythm, delegates specialist evidence collection, applies the output style, assembles reports, and delivers results. | +| `azure-capacity-manager` | 0 | Tool-bearing delegated subagent for quota, capacity reservation groups, region and zone access, AKS capacity, and capacity evidence. Owns the capacity Python tools; does not own Kusto, Resource Graph, Hub freshness, CLI, or remediation deployment tools. | +| `chief-financial-officer` | 0 | Consultative subagent with no tools. Provides finance and leadership framing for budgeting, forecasting, AI unit economics, commitment risk, and executive decisions from evidence supplied by the practitioner. | +| `ftk-database-query` | 0 | Tool-bearing delegated subagent for schema-aware FinOps Hub KQL and database evidence. Owns the Kusto tools; does not own Python, Azure CLI, Resource Graph, Hub freshness, or remediation deployment tools. | +| `ftk-hubs-agent` | 0 | Tool-bearing delegated subagent for Hub health, data freshness, monitoring scope validation, deployment, upgrade, connector readiness, Resource Graph inventory, and explicit remediation deployment tooling. | + +| Tool type | Count | Examples | +|-----------|------:|----------| +| KustoTool | 37 | `cost-anomaly-detection`, `allocation-accuracy-index`, `cost-optimization-index`, `reservation-recommendation-breakdown` | +| PythonTool | 13 | `vm-quota-usage`, `data-freshness-check`, `db-service-quotas`, `sku-availability` | + +The infrastructure deploys `Microsoft.App/agents@2026-01-01` on the `Preview` upgrade channel with `EnableSandboxGroup` and `EnableWorkspaceTools` enabled. This matches the upstream SRE Agent template default and enables the extended-agent skill, tool, subagent, and schedule APIs used by the recipe. The recipe defaults the parent SRE Agent to Azure OpenAI provider-level routing (`defaultModel.provider = MicrosoftFoundry`, `defaultModel.name = Automatic`). Azure SRE Agent selects the model within the configured provider; this template does not pin different models per custom agent or scheduled task. `bin/apply-extras.sh` enables the SRE Agent built-in Log Query and Visualization tools. It uploads the recipe knowledge files and the shared FinOps Toolkit output style from `../claude-plugin/output-styles/ftk-output-style.md` as portal-visible `KnowledgeFile` sources, then verifies the expected sources are indexed before tools, subagents, and scheduled tasks are applied. Scheduled tasks reference `ftk-output-style.md` so recurring reports use the same evidence, formatting, FinOps capability, confidence, and disclaimer conventions. + +Skills are applied with their `SKILL.md` content and tool references. The FinOps Toolkit query references resolve through the canonical `src/queries` catalog so the SRE Agent recipe does not carry a stale copy. Supporting reference material that must be available to the agent should be uploaded as KnowledgeFile sources; the current SRE Agent data plane rejects skill payloads with non-empty `additionalFiles`. + +Scheduled reports must distinguish product or deployment defects from expected evaluation-data limits and customer-owned delegation. Limited Hub history, empty transaction diagnostics, and missing multi-period trigger evidence reduce confidence; they are not by themselves deployment failures. Broader management group, billing, quota, or subscription visibility should be reported as a required customer delegation step unless this template explicitly owns the role assignment. + +The recipe is aligned to the FinOps Framework references listed in [CATALOG.md](CATALOG.md). `finops-practitioner` owns all scheduled tasks, has no direct tools, and orchestrates four delegated subagents. Three delegated subagents have tools: `ftk-database-query` owns Kusto and FOCUS evidence collection, `azure-capacity-manager` owns capacity Python evidence, and `ftk-hubs-agent` owns Hub platform and infrastructure health tooling. `chief-financial-officer` is consulted for finance and leadership framing and has no tools or scheduled tasks. Capacity and quota management is mapped into the canonical domains, capabilities, personas, principles, and phases, not a separate azcapman operating model. + +The phase model is iterative: + +| Phase | How the recipe uses it | +|-------|------------------------| +| Inform | Build trusted cost, usage, allocation, forecast, unit-economics, data freshness, and capacity-headroom evidence. | +| Optimize | Identify rate, usage, workload placement, SKU, quota, CRG, and architecture opportunities with business-value tradeoffs. | +| Operate | Run scheduled governance, budget, alert, reporting, connector, and continuous-improvement workflows. | + +| Scheduled task | Owning agent | Schedule | +|----------------|-------|----------| +| `CapacityDailyMonitor` | `finops-practitioner` | `30 6 * * *` | +| `HubsHealthCheck` | `finops-practitioner` | `0 6 * * *` | +| `Monthly` | `finops-practitioner` | `15 17 5 * *` | +| `CapacityWeeklySupplyReview` | `finops-practitioner` | `0 8 * * 1` | +| `ComputeUtilizationTrend` | `finops-practitioner` | `0 7 * * 1` | +| `CostOptimization` | `finops-practitioner` | `0 8 * * 1` | +| `NonComputeQuotaAudit` | `finops-practitioner` | `0 7 * * 2` | +| `DbQuotaAudit` | `finops-practitioner` | `0 7 * * 3` | +| `SkuAvailabilityAudit` | `finops-practitioner` | `30 7 * * 3` | +| `MonitoringScopeValidation` | `finops-practitioner` | `0 9 * * 4` | +| `BenefitRecommendationReview` | `finops-practitioner` | `0 8 * * 5` | +| `AdvisorSuppressionReview` | `finops-practitioner` | `0 9 1 * *` | +| `AIWorkloadCostAnalysis` | `finops-practitioner` | `0 10 1 * *` | +| `CapacityMonthlyPlanning` | `finops-practitioner` | `0 9 1 * *` | +| `StoragePaasGrowthForecast` | `finops-practitioner` | `0 8 1 * *` | +| `Semiannual` | `finops-practitioner` | `0 9 5 1,7 *` | +| `BudgetCoverageAudit` | `finops-practitioner` | `0 8 15 * *` | +| `AlertCoverageAudit` | `finops-practitioner` | `0 8 16 * *` | +| `CapacityQuarterlyStrategy` | `finops-practitioner` | `0 9 1 1,4,7,10 *` | + +## Prerequisites + +- Azure CLI (`az`) +- `curl` +- `jq` +- `python3` with PyYAML +- Bash 3.2 or newer +- `Microsoft.App` registered in the target subscription + +Run: + +```bash +bash bin/check-prerequisites.sh --subscription +``` + +## Deploy + +Run one script with explicit parameters: + +```bash +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + --resource-group \ + --name \ + --location \ + --cluster-uri https://..kusto.windows.net/Hub \ + [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/] \ + [--target-resource-group ...] \ + [--dry-run] \ + [--force] +``` + +`bin/deploy.sh --help` is the CLI contract: + +```text +Usage: bash bin/deploy.sh --recipe [options] + +Required: + --recipe Recipe directory + --subscription Azure subscription + -g, --resource-group Resource group for the agent + -n, --name Agent name + -l, --location Azure region + +Optional: + --target-resource-group Repeatable target resource group. The agent resource group is always included. + --cluster-uri Kusto connector URI, including database name. + Example: https://..kusto.windows.net/Hub + --cluster-resource-id Optional Kusto cluster ARM resource ID. Real deployments resolve this from --cluster-uri when possible; dry-run requires it. + --no-subscription-reader Do not assign Reader at subscription scope. Default: assign Reader. + --deployer-principal-type Principal type of the deployer (User or ServicePrincipal). Default: User. + --deploy-name Deployment name override. Defaults to a deterministic name. + --dry-run Validate inputs and write parameters without Azure calls. + --force Accepted for compatibility. + -h, --help Show this help. +``` + +### Modes + +Dry-run is hermetic and does not call Azure: + +```bash +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + -g \ + -n \ + -l \ + --cluster-uri https://..kusto.windows.net/Hub \ + [--cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/] \ + --dry-run +``` + +You can also validate the local extras assembly without Azure calls: + +```bash +bash bin/apply-extras.sh \ + --endpoint https://example.sre.azure.com \ + --subscription \ + --resource-group \ + --name \ + --recipe recipes/finops-hub \ + --build-dir /tmp/ftk-sre-agent-extras \ + --kusto-connector-uri https://..kusto.windows.net/Hub \ + --dry-run +``` + +When deploying, `deploy.sh` runs a subscription-scoped ARM deployment and then runs `bin/apply-extras.sh` to apply the recipe extras. The helper follows the upstream SRE Agent template pattern: connectors and KnowledgeFile sources use ARM child resources, while built-in tool configuration, custom tools, skills, subagents, and scheduled tasks use the SRE Agent data plane. + +Supporting resource names are deterministic for the subscription ID, agent resource group ID, and agent name. Rerunning the script with the same values updates the same Log Analytics workspace, Application Insights component, system-managed identity RBAC assignments, and SRE Agent. The apply-extras step deletes existing scheduled tasks with the recipe's task names before applying manifests so redeployments don't create duplicate automations. `--deploy-name` only changes the ARM deployment record and local build directory. + +The deployment intentionally keeps the SRE Agent onboarding wizard in the portal. The wizard uses the agent managed identity first to discover managed Azure resources and may show a **Grant permissions** OBO prompt if the identity cannot read a scope yet. Do not bypass that flow. Instead, confirm the agent has the expected managed-resource scopes and RBAC: + +- The deployment assigns `Reader` at subscription scope by default so subscription inventory, Resource Graph, capacity, quota, and coverage tools can inspect the deployment subscription. Pass `--no-subscription-reader` only when you grant equivalent read access another way. +- The agent resource group is always added to `knowledgeGraphConfiguration.managedResources` and receives the recipe's target-scope RBAC. +- Each `--target-resource-group` value is added to managed resources and receives the same target-scope RBAC. +- When `--cluster-uri` resolves to a same-subscription FinOps Hub Kusto cluster, the cluster's resource group is also added to managed resources and receives target-scope RBAC. +- With the default `High` recipe, target-scope RBAC is `Reader`, `Monitoring Reader`, `Log Analytics Reader`, and `Contributor`. `Low` omits `Contributor`. + +If `--cluster-uri` points to an Azure Data Explorer cluster with `publicNetworkAccess` set to `Disabled`, the script still deploys all resources, assigns the agent identity `AllDatabasesViewer` on the cluster, and creates the `finops-hub-kusto` connector. It also prints a warning with the SRE Agent known-limitations URL because private endpoint ADX blocks direct KQL queries from the hosted agent. The customer can decide whether to enable public query access for the connector after reviewing . + +## Recipe identity policy + +- Shipped recipes in this repo omit the `identity` block. +- Customer-authored recipes may include `identity` defaults for reproducible deployments. +- CLI flags always win over recipe defaults. + +If you omit `--target-resource-group`, the deploy flow still scopes the agent to its own resource group. If a same-subscription `--cluster-uri` is provided, the FinOps Hub resource group is also included automatically. + +## Verify + +```bash +bash bin/verify-agent.sh \ + $(az account show --query id -o tsv) \ + \ + \ + --expected recipes/finops-hub +``` + +If you passed `--cluster-uri`, confirm the `finops-hub-kusto` connector in `https://sre.azure.com`. If the deployment warned that the cluster denies public query access, the connector is expected to remain unhealthy until the customer enables public query access or Microsoft adds private endpoint ADX query support for SRE Agent. + +## CI/CD example + +The GitHub Actions example passes the cluster URI as a script flag while keeping secrets such as `GITHUB_PAT` or `ADO_PAT` in the environment. + +## Migrating from env-var-driven deploys + +The old deploy path accepted config through environment variables such as `FINOPS_HUB_CLUSTER_URI`, `FINOPS_HUB_CLUSTER_RESOURCE_ID`, and `connectors.secrets.env`. Those inputs are no longer supported for config or identity. + +Before: + +```bash +export FINOPS_HUB_CLUSTER_URI="https://..kusto.windows.net/hub" +export FINOPS_HUB_CLUSTER_RESOURCE_ID="/subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/" +bash bin/deploy.sh recipes/finops-hub +``` + +After: + +```bash +bash bin/deploy.sh \ + --recipe recipes/finops-hub \ + --subscription \ + --resource-group \ + --name \ + --location \ + --cluster-uri https://..kusto.windows.net/Hub \ + --cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/ +``` + +The only supported environment-variable inputs are secrets: + +- `GITHUB_PAT` +- `ADO_PAT` +- `ADO_USE_AAD` +- `ADO_USE_MI` +- `ADO_ORG` diff --git a/src/templates/sre-agent/bin/apply-extras.sh b/src/templates/sre-agent/bin/apply-extras.sh new file mode 100755 index 000000000..c90365b0c --- /dev/null +++ b/src/templates/sre-agent/bin/apply-extras.sh @@ -0,0 +1,581 @@ +#!/usr/bin/env bash +# ============================================================================= +# apply-extras.sh - Apply non-Bicep SRE Agent recipe assets +# +# Follows the microsoft/sre-agent template pattern: Bicep deploys the ARM +# resource, then this script applies connectors, KnowledgeFile sources, skills, +# subagents, tools, and scheduled tasks through the supported ARM/data-plane +# surfaces. +# ============================================================================= + +set -euo pipefail + +usage() { + cat < --subscription --resource-group --name --recipe --build-dir [options] + +Required: + --endpoint SRE Agent endpoint + --subscription Azure subscription that contains the SRE Agent + --resource-group Resource group that contains the SRE Agent + --name SRE Agent name + --recipe Recipe directory + --build-dir Working directory for generated extras and request files + +Optional: + --kusto-connector-uri Database-qualified Kusto connector URI + --dry-run Build extras and request payloads without Azure calls + -h, --help Show this help +EOF + exit "${1:-0}" +} + +fail() { + echo "$1" >&2 + exit "${2:-1}" +} + +require_value() { + local flag="$1" + local value="${2:-}" + if [[ -z "$value" || "$value" == -* ]]; then + fail "Error: flag ${flag} requires a value" 2 + fi +} + +safe_name() { + printf '%s' "$1" | tr '/:' '__' | tr -cd 'A-Za-z0-9._-' +} + +urlencode() { + printf '%s' "$1" | jq -sRr @uri +} + +knowledge_source_name() { + basename "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/-+/-/g; s/^-//; s/-$//' +} + +get_sre_token() { + az account get-access-token --resource https://azuresre.dev --query accessToken -o tsv 2>/dev/null +} + +ENDPOINT="" +SUBSCRIPTION_ID="" +RESOURCE_GROUP="" +AGENT_NAME="" +RECIPE_DIR="" +BUILD_DIR="" +KUSTO_CONNECTOR_URI="" +DRY_RUN="" +ARM_API_VERSION="2025-05-01-preview" +REQUEST_DELAY_SECONDS="${SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS:-15}" +RETRY_DELAY_SECONDS="${SRE_AGENT_APPLY_RETRY_DELAY_SECONDS:-15}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --endpoint) + require_value "--endpoint" "${2:-}" + ENDPOINT="$2" + shift 2 + ;; + --subscription) + require_value "--subscription" "${2:-}" + SUBSCRIPTION_ID="$2" + shift 2 + ;; + --resource-group) + require_value "--resource-group" "${2:-}" + RESOURCE_GROUP="$2" + shift 2 + ;; + --name) + require_value "--name" "${2:-}" + AGENT_NAME="$2" + shift 2 + ;; + --recipe) + require_value "--recipe" "${2:-}" + RECIPE_DIR="$2" + shift 2 + ;; + --build-dir) + require_value "--build-dir" "${2:-}" + BUILD_DIR="$2" + shift 2 + ;; + --kusto-connector-uri) + require_value "--kusto-connector-uri" "${2:-}" + KUSTO_CONNECTOR_URI="$2" + shift 2 + ;; + --dry-run) + DRY_RUN="true" + shift + ;; + -h|--help) + usage 0 + ;; + *) + fail "Error: unknown argument '$1'" 2 + ;; + esac +done + +[[ -n "$ENDPOINT" ]] || fail "Error: --endpoint is required" 2 +[[ -n "$SUBSCRIPTION_ID" ]] || fail "Error: --subscription is required" 2 +[[ -n "$RESOURCE_GROUP" ]] || fail "Error: --resource-group is required" 2 +[[ -n "$AGENT_NAME" ]] || fail "Error: --name is required" 2 +[[ -n "$RECIPE_DIR" ]] || fail "Error: --recipe is required" 2 +[[ -n "$BUILD_DIR" ]] || fail "Error: --build-dir is required" 2 +[[ -d "$RECIPE_DIR" ]] || fail "Error: recipe directory not found: $RECIPE_DIR" 1 + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +RECIPE_DIR="$(cd "$RECIPE_DIR" && pwd)" +mkdir -p "$BUILD_DIR" +BUILD_DIR="$(cd "$BUILD_DIR" && pwd)" +EXTRAS_FILE="${BUILD_DIR}/extras.json" +REQUEST_DIR="${BUILD_DIR}/requests" +RESPONSE_DIR="${BUILD_DIR}/responses" +ARM_BASE="https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.App/agents/${AGENT_NAME}" + +command -v jq >/dev/null || fail "jq is required" 1 +command -v base64 >/dev/null || fail "base64 is required" 1 + +PYTHON_CMD="" +if command -v python3 >/dev/null; then + PYTHON_CMD="python3" +elif command -v python >/dev/null && python --version 2>&1 | grep -q "Python 3"; then + PYTHON_CMD="python" +else + fail "Python 3 is required" 1 +fi + +"$PYTHON_CMD" -c "import yaml" 2>/dev/null || fail "PyYAML is required to build extras. Install with: pip install pyyaml" 1 + +mkdir -p "$REQUEST_DIR" "$RESPONSE_DIR" + +echo "" +echo "=============================================" +echo " SRE Agent - Apply Extras" +echo "=============================================" +echo "" +echo "Agent endpoint: $ENDPOINT" +echo "Recipe: $RECIPE_DIR" +echo "Build dir: $BUILD_DIR" +echo "" + +echo "Step 1/8: Building extras manifest..." +SUMMARY_JSON="$("$PYTHON_CMD" "${SCRIPT_DIR}/build-extras.py" \ + --recipe "$RECIPE_DIR" \ + --output "$EXTRAS_FILE" \ + --kusto-connector-uri "$KUSTO_CONNECTOR_URI")" +echo " extras: $EXTRAS_FILE" +echo " summary: $SUMMARY_JSON" +echo "" + +if [[ -z "$DRY_RUN" ]]; then + command -v az >/dev/null || fail "az is required to apply extras" 1 + command -v curl >/dev/null || fail "curl is required to apply extras" 1 + az account set --subscription "$SUBSCRIPTION_ID" >/dev/null + DP_TOKEN="$(get_sre_token)" + [[ -n "$DP_TOKEN" ]] || fail "Error: failed to get Azure SRE Agent bearer token. Run: az login --scope https://azuresre.dev/.default" 1 +else + DP_TOKEN="dry-run" + echo "Dry run enabled. Azure control-plane and data-plane calls will be skipped." + echo "" +fi + +arm_put_connector() { + local name="$1" + local body_json="$2" + local safe + local body_file + local response_file + local result + + safe="$(safe_name "$name")" + body_file="${REQUEST_DIR}/arm-connectors/${safe}.json" + response_file="${RESPONSE_DIR}/arm-connectors/${safe}.json" + mkdir -p "$(dirname "$body_file")" "$(dirname "$response_file")" + printf '%s\n' "$body_json" > "$body_file" + + if [[ -n "$DRY_RUN" ]]; then + echo " dry-run ARM PUT connectors/${name} -> $body_file" + return 0 + fi + + for attempt in 1 2 3 4 5; do + if result="$(az rest -m PUT \ + --url "${ARM_BASE}/connectors/${name}?api-version=${ARM_API_VERSION}" \ + --body "@${body_file}" \ + --headers "Content-Type=application/json" \ + -o json 2>&1)"; then + printf '%s\n' "$result" > "$response_file" + echo " ARM PUT connectors/${name}: ok" + return 0 + fi + + printf '%s\n' "$result" > "$response_file" + echo " ARM PUT connectors/${name}: attempt ${attempt}/5 failed" + [[ "$attempt" != "5" ]] && sleep "$RETRY_DELAY_SECONDS" + done + + sed -n '1,120p' "$response_file" >&2 || true + fail "Failed to apply connector ${name}" 1 +} + +dataplane_put_extended() { + local kind="$1" + local name="$2" + local type="$3" + local tags_json="$4" + local props_json="$5" + local safe + local body_file + local response_file + local body + local http_code + local encoded_name + + safe="$(safe_name "$name")" + body_file="${REQUEST_DIR}/dataplane-${kind}/${safe}.json" + response_file="${RESPONSE_DIR}/dataplane-${kind}/${safe}.json" + mkdir -p "$(dirname "$body_file")" "$(dirname "$response_file")" + body="$(jq -nc --arg name "$name" --arg type "$type" --argjson tags "$tags_json" --argjson properties "$props_json" \ + '{name: $name, type: $type, tags: $tags, properties: $properties}')" + printf '%s\n' "$body" > "$body_file" + + if [[ -n "$DRY_RUN" ]]; then + echo " dry-run PUT ${kind}/${name} -> $body_file" + return 0 + fi + + encoded_name="$(urlencode "$name")" + for attempt in 1 2 3 4 5; do + if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \ + -X PUT "${ENDPOINT}/api/v2/extendedAgent/${kind}/${encoded_name}" \ + -H "Authorization: Bearer ${DP_TOKEN}" \ + -H "Content-Type: application/json" \ + --data-binary "@${body_file}")"; then + : + else + http_code="000" + fi + + case "$http_code" in + 200|201|202|204) + echo " PUT ${kind}/${name}: ok" + return 0 + ;; + esac + + echo " PUT ${kind}/${name}: attempt ${attempt}/5 returned HTTP ${http_code}" + [[ "$attempt" != "5" ]] && sleep "$RETRY_DELAY_SECONDS" + done + + sed -n '1,120p' "$response_file" >&2 || true + fail "Failed to apply ${kind}/${name}" 1 +} + +apply_built_in_tools_config() { + local config_json + local override_count + local body_file="${REQUEST_DIR}/built-in-tools/configure.json" + local response_file="${RESPONSE_DIR}/built-in-tools/configure.json" + local http_code + + config_json="$(jq -c '.builtInTools // {"overrides":[]}' "$EXTRAS_FILE")" + override_count="$(jq -r '.overrides | length' <<< "$config_json")" + [[ "$override_count" != "0" ]] || { + echo " built-in tools: no overrides" + return 0 + } + + mkdir -p "$(dirname "$body_file")" "$(dirname "$response_file")" + jq '{overrides: [.overrides[] | {name, enabled}]}' <<< "$config_json" > "$body_file" + + if [[ -n "$DRY_RUN" ]]; then + echo " dry-run POST agent/tools/configure -> $body_file" + return 0 + fi + + for attempt in 1 2 3 4 5; do + if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \ + -X POST "${ENDPOINT}/api/v2/agent/tools/configure" \ + -H "Authorization: Bearer ${DP_TOKEN}" \ + -H "Content-Type: application/json" \ + --data-binary "@${body_file}")"; then + : + else + http_code="000" + fi + + case "$http_code" in + 200|201|202|204) + echo " built-in tools configured: ${override_count} overrides" + return 0 + ;; + esac + + echo " built-in tools: attempt ${attempt}/5 returned HTTP ${http_code}" + [[ "$attempt" != "5" ]] && sleep "$RETRY_DELAY_SECONDS" + done + + sed -n '1,120p' "$response_file" >&2 || true + fail "Failed to configure built-in tools" 1 +} + +delete_existing_scheduled_tasks() { + local count + local response_file="${RESPONSE_DIR}/scheduledtasks.existing.json" + local name + local ids + local id + local http_code + local deleted=0 + + count="$(jq '.scheduledTasks // [] | length' "$EXTRAS_FILE")" + [[ "$count" -gt 0 ]] || return 0 + + if [[ -n "$DRY_RUN" ]]; then + echo " dry-run scheduled-task cleanup: ${count} task name(s)" + return 0 + fi + + if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \ + "${ENDPOINT}/api/v1/scheduledtasks" \ + -H "Authorization: Bearer ${DP_TOKEN}")"; then + : + else + http_code="000" + fi + [[ "$http_code" == "200" ]] || fail "Failed to list existing scheduled tasks before apply (HTTP ${http_code})" 1 + + for i in $(seq 0 $((count - 1))); do + name="$(jq -r --argjson index "$i" '.scheduledTasks[$index].metadata.name' "$EXTRAS_FILE")" + ids="$(jq -r --arg name "$name" '.[]? | select(.name == $name) | .id // empty' "$response_file" 2>/dev/null || true)" + while IFS= read -r id; do + [[ -n "$id" ]] || continue + if curl -sS -o /dev/null -X DELETE "${ENDPOINT}/api/v1/scheduledtasks/${id}" -H "Authorization: Bearer ${DP_TOKEN}"; then + echo " deleted existing scheduled-task ${name} (${id})" + deleted=$((deleted + 1)) + else + fail "Failed to delete existing scheduled-task ${name} (${id})" 1 + fi + done <<< "$ids" + done + + [[ "$deleted" -eq 0 ]] || echo " scheduled-task cleanup: ${deleted} existing deleted" +} + +verify_knowledge_docs() { + local expected_docs=("$@") + local response_file="${RESPONSE_DIR}/knowledge-sources/list.json" + local detail_file + local doc + local source_name + local entry + local indexed + local reason + local missing + local unindexed + local http_code + + [[ "${#expected_docs[@]}" -gt 0 ]] || return 0 + [[ -z "$DRY_RUN" ]] || { + echo " dry-run knowledge verification skipped" + return 0 + } + + mkdir -p "$(dirname "$response_file")" + echo " waiting for knowledge sources to index..." + for attempt in $(seq 1 20); do + if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \ + "${ENDPOINT}/api/v2/extendedAgent/connectors" \ + -H "Authorization: Bearer ${DP_TOKEN}")"; then + : + else + http_code="000" + fi + + missing=0 + unindexed=0 + if [[ "$http_code" == "200" ]]; then + for doc in "${expected_docs[@]}"; do + source_name="$(knowledge_source_name "$doc")" + entry="$(jq -c --arg name "$source_name" '[.value[]? | select(.name == $name and .properties.dataConnectorType == "KnowledgeFile")][0] // empty' "$response_file" 2>/dev/null || true)" + if [[ -z "$entry" ]]; then + missing=$((missing + 1)) + continue + fi + + detail_file="${RESPONSE_DIR}/knowledge-sources/${source_name}.json" + if http_code="$(curl -sS -o "$detail_file" -w "%{http_code}" \ + "${ENDPOINT}/api/v2/extendedAgent/connectors/${source_name}" \ + -H "Authorization: Bearer ${DP_TOKEN}")"; then + : + else + http_code="000" + fi + + indexed="$(jq -r '.properties.extendedProperties.createdAt // empty' "$detail_file" 2>/dev/null || true)" + [[ -n "$indexed" ]] || unindexed=$((unindexed + 1)) + done + + if [[ "$missing" -eq 0 && "$unindexed" -eq 0 ]]; then + echo " knowledge sources indexed: ${#expected_docs[@]} docs" + return 0 + fi + + echo " knowledge indexing attempt ${attempt}/20: ${missing} missing, ${unindexed} not indexed" + else + echo " knowledge indexing attempt ${attempt}/20: connectors returned HTTP ${http_code}" + fi + + [[ "$attempt" -lt 20 ]] && sleep "$RETRY_DELAY_SECONDS" + done + + echo " knowledge source status:" >&2 + for doc in "${expected_docs[@]}"; do + source_name="$(knowledge_source_name "$doc")" + detail_file="${RESPONSE_DIR}/knowledge-sources/${source_name}.json" + entry="$(jq -c --arg name "$source_name" '[.value[]? | select(.name == $name and .properties.dataConnectorType == "KnowledgeFile")][0] // empty' "$response_file" 2>/dev/null || true)" + if [[ -z "$entry" ]]; then + echo " ${doc}: missing" >&2 + else + indexed="$(jq -r '.properties.extendedProperties.createdAt // empty' "$detail_file" 2>/dev/null || true)" + reason="$(jq -r '.properties.extendedProperties.errorReason // ""' "$detail_file" 2>/dev/null || true)" + echo " ${doc}: indexed=${indexed}${reason:+ reason=${reason}}" >&2 + fi + done + fail "Knowledge sources failed to index" 1 +} + +echo "Step 2/8: Applying connectors..." +CONNECTOR_COUNT="$(jq '.connectors // [] | length' "$EXTRAS_FILE")" +if [[ "$CONNECTOR_COUNT" -gt 0 ]]; then + for i in $(seq 0 $((CONNECTOR_COUNT - 1))); do + name="$(jq -r --argjson index "$i" '.connectors[$index].name' "$EXTRAS_FILE")" + connector_type="$(jq -r --argjson index "$i" '.connectors[$index].properties.dataConnectorType // .connectors[$index].properties.type // ""' "$EXTRAS_FILE")" + connector_type_lower="$(printf '%s' "$connector_type" | tr '[:upper:]' '[:lower:]')" + if [[ "$connector_type_lower" == "mcp" ]]; then + jq -e --argjson index "$i" '.connectors[$index].properties.extendedProperties.type == "http" and (.connectors[$index].properties.extendedProperties.endpoint // "") != ""' "$EXTRAS_FILE" >/dev/null \ + || fail "Error: MCP connector ${name} must use properties.extendedProperties.type and endpoint" 1 + jq -e --argjson index "$i" '(.connectors[$index].properties.extendedProperties.headers? | type) != "object"' "$EXTRAS_FILE" >/dev/null \ + || fail "Error: MCP connector ${name} must flatten custom headers into properties.extendedProperties" 1 + fi + body="$(jq -c --argjson index "$i" '{properties: (.connectors[$index].properties | if (.identity // "") == "" then . + {identity: "system"} else . end)}' "$EXTRAS_FILE")" + arm_put_connector "$name" "$body" + done +else + echo " connectors: none" +fi +echo "" + +echo "Step 3/8: Configuring built-in tools..." +apply_built_in_tools_config +echo "" + +echo "Step 4/8: Uploading knowledge sources..." +KNOWLEDGE_COUNT="$(jq '.knowledgeItems // [] | length' "$EXTRAS_FILE")" +KNOWLEDGE_DOC_NAMES=() +if [[ "$KNOWLEDGE_COUNT" -gt 0 ]]; then + for i in $(seq 0 $((KNOWLEDGE_COUNT - 1))); do + fname="$(jq -r --argjson index "$i" '.knowledgeItems[$index].name' "$EXTRAS_FILE")" + content_type="$(jq -r --argjson index "$i" '.knowledgeItems[$index].contentType // "application/octet-stream"' "$EXTRAS_FILE")" + source_name="$(knowledge_source_name "$fname")" + content_b64="$(jq -rj --argjson index "$i" '.knowledgeItems[$index].content' "$EXTRAS_FILE" | base64 | tr -d '\n')" + KNOWLEDGE_DOC_NAMES+=("$fname") + body="$(jq -nc \ + --arg dataSource "$source_name" \ + --arg displayName "$fname" \ + --arg fileName "$fname" \ + --arg fileContent "$content_b64" \ + --arg contentType "$content_type" \ + '{properties:{dataConnectorType:"KnowledgeFile",dataSource:$dataSource,extendedProperties:{displayName:$displayName,fileName:$fileName,fileContent:$fileContent,contentType:$contentType}}}')" + arm_put_connector "$source_name" "$body" + [[ -z "$DRY_RUN" && "$i" -lt $((KNOWLEDGE_COUNT - 1)) ]] && sleep "$REQUEST_DELAY_SECONDS" + done + verify_knowledge_docs "${KNOWLEDGE_DOC_NAMES[@]}" +else + echo " knowledge sources: none" +fi +echo "" + +echo "Step 5/8: Applying tools..." +TOOL_COUNT="$(jq '.tools // [] | length' "$EXTRAS_FILE")" +if [[ "$TOOL_COUNT" -gt 0 ]]; then + for i in $(seq 0 $((TOOL_COUNT - 1))); do + name="$(jq -r --argjson index "$i" '.tools[$index].metadata.name' "$EXTRAS_FILE")" + props="$(jq -c --argjson index "$i" ' + .tools[$index].spec + | if (.type // "") == "PythonTool" then .type = "PythonFunctionTool" else . end + ' "$EXTRAS_FILE")" + dataplane_put_extended "tools" "$name" "ExtendedAgentTool" "[]" "$props" + done +else + echo " tools: none" +fi +echo "" + +echo "Step 6/8: Applying skills..." +SKILL_COUNT="$(jq '.skills // [] | length' "$EXTRAS_FILE")" +if [[ "$SKILL_COUNT" -gt 0 ]]; then + for i in $(seq 0 $((SKILL_COUNT - 1))); do + name="$(jq -r --argjson index "$i" '.skills[$index].metadata.name' "$EXTRAS_FILE")" + props="$(jq -c --argjson index "$i" ' + { + name: .skills[$index].metadata.name, + description: (.skills[$index].metadata.description // ""), + tools: (.skills[$index].metadata.spec.tools // []), + skillContent: (.skills[$index].skillContent // ""), + additionalFiles: [] + }' "$EXTRAS_FILE")" + dataplane_put_extended "skills" "$name" "Skill" "[]" "$props" + done +else + echo " skills: none" +fi +echo "" + +echo "Step 7/8: Applying subagents..." +SUBAGENT_COUNT="$(jq '.subagents // [] | length' "$EXTRAS_FILE")" +if [[ "$SUBAGENT_COUNT" -gt 0 ]]; then + for i in $(seq 0 $((SUBAGENT_COUNT - 1))); do + name="$(jq -r --argjson index "$i" '.subagents[$index].metadata.name' "$EXTRAS_FILE")" + props="$(jq -c --argjson index "$i" '.subagents[$index].spec' "$EXTRAS_FILE")" + dataplane_put_extended "agents" "$name" "ExtendedAgent" "[]" "$props" + done +else + echo " subagents: none" +fi +echo "" + +echo "Step 8/8: Applying scheduled tasks..." +TASK_COUNT="$(jq '.scheduledTasks // [] | length' "$EXTRAS_FILE")" +if [[ "$TASK_COUNT" -gt 0 ]]; then + delete_existing_scheduled_tasks + for i in $(seq 0 $((TASK_COUNT - 1))); do + name="$(jq -r --argjson index "$i" '.scheduledTasks[$index].metadata.name' "$EXTRAS_FILE")" + props="$(jq -c --argjson index "$i" ' + .scheduledTasks[$index].spec as $spec | + { + name: ($spec.name // .scheduledTasks[$index].metadata.name // ""), + description: ($spec.description // ""), + cronExpression: ($spec.schedule // $spec.cronExpression // $spec.cron_expression // ""), + agentPrompt: ($spec.prompt // $spec.agentPrompt // $spec.agent_prompt // ""), + agentMode: ($spec.mode // $spec.agentMode // $spec.agent_mode // "Review"), + isEnabled: ($spec.enabled // true), + agent: ($spec.agent // "") + }' "$EXTRAS_FILE")" + dataplane_put_extended "scheduledtasks" "$name" "ScheduledTask" "[]" "$props" + done +else + echo " scheduled tasks: none" +fi +echo "" + +echo "=============================================" +echo " SRE Agent extras complete" +echo "=============================================" +echo "" diff --git a/src/templates/sre-agent/bin/build-extras.py b/src/templates/sre-agent/bin/build-extras.py new file mode 100755 index 000000000..5b81f7f69 --- /dev/null +++ b/src/templates/sre-agent/bin/build-extras.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python3 +"""Build SRE Agent extras from a FinOps Toolkit recipe. + +The output follows the microsoft/sre-agent apply-extras contract: infrastructure +stays in Bicep/ARM, while data-plane and connector extras are assembled into one +JSON document for the local apply step. +""" + +from __future__ import annotations + +import argparse +import json +import os +import pathlib +import re +import sys +from typing import Any + +try: + import yaml +except ImportError as exc: # pragma: no cover - exercised by shell prereq checks + raise SystemExit("PyYAML is required. Install with: pip install pyyaml") from exc + + +JsonObject = dict[str, Any] + + +def load_json(path: pathlib.Path, default: Any) -> Any: + if not path.is_file(): + return default + with path.open("r", encoding="utf-8") as handle: + return json.load(handle) + + +def load_yaml(path: pathlib.Path) -> JsonObject: + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) or {} + if not isinstance(data, dict): + raise SystemExit(f"YAML manifest must be an object: {path}") + return data + + +def yaml_files(root: pathlib.Path) -> list[pathlib.Path]: + if not root.is_dir(): + return [] + paths: list[pathlib.Path] = [] + for dirpath, _, filenames in os.walk(root, followlinks=True): + for filename in filenames: + path = pathlib.Path(dirpath) / filename + if path.suffix.lower() in {".yaml", ".yml"}: + paths.append(path) + return sorted(paths) + + +def collect_yaml(root: pathlib.Path) -> list[JsonObject]: + return [load_yaml(path) for path in yaml_files(root)] + + +def read_kql_body(path: pathlib.Path) -> str: + text = read_text(path) + lines = text.splitlines() + start = 0 + for index, line in enumerate(lines): + stripped = line.strip() + if stripped == "" or stripped.startswith("//"): + continue + start = index + break + return "\n".join(lines[start:]).rstrip() + + +def collect_kql_comment_block(path: pathlib.Path) -> list[str]: + comments: list[str] = [] + for line in read_text(path).splitlines(): + stripped = line.strip() + if stripped == "": + continue + if not stripped.startswith("//"): + break + comments.append(stripped[2:].strip()) + return comments + + +def kql_description(path: pathlib.Path) -> str: + comments = collect_kql_comment_block(path) + query_name = path.stem.replace("-", " ") + description_lines: list[str] = [] + in_description = False + for comment in comments: + if comment == "Description:": + in_description = True + continue + if comment == "Parameters:" or comment.startswith("Query:"): + if in_description: + break + if comment.startswith("Query:"): + query_name = comment.split(":", 1)[1].strip() or query_name + continue + if in_description: + description_lines.append(comment) + description = " ".join(part for part in description_lines if part) + if not description: + description = f"Runs the {query_name} FinOps Hub catalog query." + return description + + +def kql_parameters(path: pathlib.Path) -> list[JsonObject]: + comments = collect_kql_comment_block(path) + query = read_kql_body(path) + parameters: list[JsonObject] = [] + in_parameters = False + for comment in comments: + if comment == "Parameters:": + in_parameters = True + continue + if not in_parameters: + continue + if comment.startswith("Each row") or comment.startswith("Use this query"): + break + if comment.lower().startswith("none"): + break + if ":" not in comment: + continue + name, raw_description = comment.split(":", 1) + name = name.strip() + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name): + continue + if not re.search(rf"^let\s+{re.escape(name)}\s*=", query, flags=re.MULTILINE): + continue + if any(parameter["name"] == name for parameter in parameters): + continue + parameters.append( + { + "name": name, + "type": "string", + "description": raw_description.strip(), + "required": True, + } + ) + return parameters + + +def replace_kql_parameter_defaults(query: str, parameters: list[JsonObject]) -> str: + for parameter in parameters: + name = re.escape(str(parameter["name"])) + query = re.sub( + rf"^let\s+{name}\s*=\s*[^;\n]+;", + f"let {parameter['name']} = ##{parameter['name']}##;", + query, + count=1, + flags=re.MULTILINE, + ) + return query + + +def collect_catalog_kusto_tools(recipe_dir: pathlib.Path) -> list[JsonObject]: + catalog_dir = (recipe_dir / "../../../../queries/catalog").resolve() + if not catalog_dir.is_dir(): + raise SystemExit(f"FinOps Hub query catalog not found: {catalog_dir}") + + generated: list[JsonObject] = [] + for path in sorted(catalog_dir.glob("*.kql")): + name = path.stem + parameters = kql_parameters(path) + query = replace_kql_parameter_defaults(read_kql_body(path), parameters) + spec: JsonObject = { + "type": "KustoTool", + "connector": "finops-hub-kusto", + "toolMode": "Auto", + "description": kql_description(path), + "database": "Hub", + "query": query, + } + if parameters: + spec["parameters"] = parameters + generated.append( + { + "api_version": "azuresre.ai/v2", + "kind": "ExtendedAgentTool", + "metadata": {"name": name}, + "spec": spec, + } + ) + return generated + + +def validate_explicit_tools(tools: list[JsonObject]) -> None: + explicit_kusto_names: list[str] = [] + for tool in tools: + name = metadata_name(tool) + spec = tool.get("spec") or {} + if spec.get("type") == "KustoTool": + explicit_kusto_names.append(name) + if explicit_kusto_names: + raise SystemExit( + "Kusto tools must be generated from src/queries/catalog/*.kql; " + "remove explicit Kusto YAML file(s): " + + ", ".join(sorted(explicit_kusto_names)) + ) + + +def catalog_kpi_tool_names(recipe_dir: pathlib.Path) -> set[str]: + kpi_path = (recipe_dir / "../../../../queries/KPI.md").resolve() + if not kpi_path.is_file(): + raise SystemExit(f"FinOps KPI catalog not found: {kpi_path}") + text = read_text(kpi_path) + return set(re.findall(r"\[([a-z0-9-]+)\]\(catalog/\1\.kql\)", text)) + + +def validate_tool_and_task_coverage(recipe_dir: pathlib.Path, extras: JsonObject) -> None: + expected_config = load_json(recipe_dir / "expected-config.json", {}) + if not isinstance(expected_config, dict): + raise SystemExit(f"Expected config must be an object: {recipe_dir / 'expected-config.json'}") + + expected_skills = sorted(str(name) for name in expected_config.get("skills") or []) + if expected_skills: + skill_names = {metadata_name(skill) for skill in extras.get("skills") or []} + missing_skills = [name for name in expected_skills if name not in skill_names] + if missing_skills: + raise SystemExit( + "Expected skill directory missing SKILL.md or has a broken symlink: " + + ", ".join(missing_skills) + ) + + catalog_dir = (recipe_dir / "../../../../queries/catalog").resolve() + catalog_names = {path.stem for path in catalog_dir.glob("*.kql")} + tools = extras.get("tools") or [] + tool_names = [metadata_name(tool) for tool in tools] + duplicate_names = sorted({name for name in tool_names if tool_names.count(name) > 1}) + if duplicate_names: + raise SystemExit("Duplicate tool definitions found: " + ", ".join(duplicate_names)) + + kusto_names = { + metadata_name(tool) + for tool in tools + if (tool.get("spec") or {}).get("type") == "KustoTool" + } + missing_kusto = sorted(catalog_names - kusto_names) + if missing_kusto: + raise SystemExit( + "Catalog query file(s) missing generated Kusto tools: " + + ", ".join(missing_kusto) + ) + + kpi_names = catalog_kpi_tool_names(recipe_dir) + missing_kpi_tools = sorted(kpi_names - kusto_names) + if missing_kpi_tools: + raise SystemExit( + "KPI catalog query file(s) missing generated Kusto tools: " + + ", ".join(missing_kpi_tools) + ) + + scheduled_text = "\n".join(json.dumps(task, sort_keys=True) for task in extras.get("scheduledTasks") or []) + missing_scheduled_kpis = [ + name + for name in sorted(kpi_names) + if not re.search(rf"(? str: + metadata = item.get("metadata") or {} + spec = item.get("spec") or {} + name = metadata.get("name") or spec.get("name") or item.get("name") + if not name: + raise SystemExit(f"Manifest missing metadata.name: {item}") + return str(name) + + +def ordered_subagents(root: pathlib.Path) -> list[JsonObject]: + items = collect_yaml(root) + by_name = {metadata_name(item): item for item in items} + dependencies: dict[str, list[str]] = {} + for name, item in by_name.items(): + spec = item.get("spec") or {} + handoffs = spec.get("handoffs") or [] + dependencies[name] = [str(handoff) for handoff in handoffs if str(handoff) in by_name] + + ordered: list[JsonObject] = [] + visiting: set[str] = set() + visited: set[str] = set() + + def visit(name: str) -> None: + if name in visited: + return + if name in visiting: + cycle = " -> ".join([*visiting, name]) + raise SystemExit(f"Subagent handoff cycle detected: {cycle}") + visiting.add(name) + for dependency in dependencies[name]: + visit(dependency) + visiting.remove(name) + visited.add(name) + ordered.append(by_name[name]) + + for name in sorted(by_name): + visit(name) + return ordered + + +def parse_frontmatter(text: str) -> JsonObject: + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + return {} + for index, line in enumerate(lines[1:], start=1): + if line.strip() == "---": + raw = "\n".join(lines[1:index]) + data = yaml.safe_load(raw) or {} + if not isinstance(data, dict): + return {} + return data + return {} + + +def as_list(value: Any) -> list[str]: + if value is None: + return [] + if isinstance(value, list): + return [str(item) for item in value] + if isinstance(value, str): + return [part for part in re.split(r"[\s,]+", value.strip()) if part] + return [str(value)] + + +def read_text(path: pathlib.Path) -> str: + return path.read_text(encoding="utf-8", errors="replace") + + +def collect_skill_directories(root: pathlib.Path) -> list[JsonObject]: + if not root.is_dir(): + return [] + + skills: list[JsonObject] = [] + for skill_dir in sorted(path for path in root.iterdir() if path.is_dir()): + skill_file = skill_dir / "SKILL.md" + if not skill_file.is_file(): + continue + + skill_content = read_text(skill_file) + frontmatter = parse_frontmatter(skill_content) + name = str(frontmatter.get("name") or skill_dir.name) + description = str(frontmatter.get("description") or "") + tools = as_list(frontmatter.get("tools")) + + additional_files: list[JsonObject] = [] + for dirpath, _, filenames in os.walk(skill_dir, followlinks=True): + for filename in filenames: + path = pathlib.Path(dirpath) / filename + relative_path = path.relative_to(skill_dir).as_posix() + if relative_path == "SKILL.md": + continue + additional_files.append( + { + "name": relative_path, + "path": relative_path, + "content": read_text(path), + } + ) + + skills.append( + { + "metadata": { + "name": name, + "description": description, + "spec": {"tools": tools}, + }, + "skillContent": skill_content, + "additionalFiles": sorted(additional_files, key=lambda item: item["name"]), + } + ) + + return skills + + +def replace_env_refs(value: Any, replacements: dict[str, str]) -> Any: + if isinstance(value, str): + pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") + + def repl(match: re.Match[str]) -> str: + key = match.group(1) + return replacements.get(key) or os.environ.get(key) or match.group(0) + + return pattern.sub(repl, value) + if isinstance(value, list): + return [replace_env_refs(item, replacements) for item in value] + if isinstance(value, dict): + return {key: replace_env_refs(item, replacements) for key, item in value.items()} + return value + + +def collect_connectors(recipe_dir: pathlib.Path, kusto_connector_uri: str) -> list[JsonObject]: + config = load_json(recipe_dir / "connectors.json", {"connectors": []}) + raw_connectors = config if isinstance(config, list) else config.get("connectors", []) + replacements = {} + if kusto_connector_uri: + replacements["FINOPS_HUB_CLUSTER_URI"] = kusto_connector_uri + + connectors: list[JsonObject] = [] + for raw in raw_connectors: + connector = replace_env_refs(raw, replacements) + properties = connector.get("properties") or {} + connector_type = str(properties.get("dataConnectorType") or properties.get("type") or "") + if connector_type.lower() == "kusto" and not kusto_connector_uri: + continue + if connector_type.lower() == "kusto": + properties["dataSource"] = kusto_connector_uri + connector["properties"] = properties + connectors.append(connector) + return connectors + + +def knowledge_content_type(path: pathlib.Path) -> str: + suffix = path.suffix.lower() + return { + ".md": "text/markdown", + ".txt": "text/plain", + ".json": "application/json", + ".csv": "text/csv", + ".yaml": "application/yaml", + ".yml": "application/yaml", + }.get(suffix, "application/octet-stream") + + +def collect_knowledge_items(recipe_dir: pathlib.Path) -> list[JsonObject]: + knowledge_paths: list[pathlib.Path] = [] + knowledge_dir = recipe_dir / "knowledge" + if knowledge_dir.is_dir(): + knowledge_paths.extend(sorted(path for path in knowledge_dir.iterdir() if path.is_file())) + + output_style = (recipe_dir / "../../../claude-plugin/output-styles/ftk-output-style.md").resolve() + if not output_style.is_file(): + raise SystemExit(f"Output style knowledge document not found: {output_style}") + knowledge_paths.append(output_style) + + items: list[JsonObject] = [] + for path in knowledge_paths: + items.append( + { + "name": path.name, + "content": read_text(path), + "contentType": knowledge_content_type(path), + "sourcePath": str(path), + } + ) + return items + + +def main() -> int: + parser = argparse.ArgumentParser(description="Build FinOps Toolkit SRE Agent extras JSON.") + parser.add_argument("--recipe", required=True, type=pathlib.Path) + parser.add_argument("--output", required=True, type=pathlib.Path) + parser.add_argument("--kusto-connector-uri", default="") + args = parser.parse_args() + + recipe_dir = args.recipe.resolve() + if not recipe_dir.is_dir(): + raise SystemExit(f"Recipe directory not found: {recipe_dir}") + + explicit_tools = collect_yaml(recipe_dir / "config/tools") + validate_explicit_tools(explicit_tools) + generated_tools = collect_catalog_kusto_tools(recipe_dir) + + extras = { + "connectors": collect_connectors(recipe_dir, args.kusto_connector_uri), + "builtInTools": load_json(recipe_dir / "config/built-in-tools.json", {"overrides": []}), + "knowledgeItems": collect_knowledge_items(recipe_dir), + "skills": collect_skill_directories(recipe_dir / "config/skills"), + "subagents": ordered_subagents(recipe_dir / "config/subagents"), + "tools": [*explicit_tools, *generated_tools], + "scheduledTasks": collect_yaml(recipe_dir / "automations/scheduled-tasks"), + } + validate_tool_and_task_coverage(recipe_dir, extras) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open("w", encoding="utf-8") as handle: + json.dump(extras, handle, indent=2, ensure_ascii=False) + handle.write("\n") + + summary = {key: len(value) for key, value in extras.items() if isinstance(value, list)} + summary["builtInToolOverrides"] = len((extras["builtInTools"] or {}).get("overrides", [])) + print(json.dumps(summary, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/templates/sre-agent/bin/check-prerequisites.sh b/src/templates/sre-agent/bin/check-prerequisites.sh new file mode 100755 index 000000000..2dd6b2566 --- /dev/null +++ b/src/templates/sre-agent/bin/check-prerequisites.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# check-prerequisites.sh — verify required tools are installed + +usage() { + cat <] + +Options: + --subscription Subscription to scope Azure CLI checks + -h, --help Show this help +EOF + exit "${1:-0}" +} + +check_prerequisites() { + local missing=0 + + for cmd in az curl git jq; do + if ! command -v "$cmd" &>/dev/null; then + echo " ❌ Missing: $cmd" >&2 + missing=$((missing + 1)) + fi + done + + if ! command -v python3 &>/dev/null && ! command -v python &>/dev/null; then + echo " ❌ Missing: python3 (needed for YAML processing)" >&2 + missing=$((missing + 1)) + else + local py=$(command -v python3 || command -v python) + if ! "$py" -c "import yaml" 2>/dev/null; then + echo " ❌ Missing: PyYAML — install: pip install pyyaml" >&2 + missing=$((missing + 1)) + fi + fi + + if [[ $missing -gt 0 ]]; then + echo " $missing prerequisite(s) missing. See README for install guide." >&2 + return 1 + fi + return 0 +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + SUBSCRIPTION_ID="" + while [[ $# -gt 0 ]]; do + case "$1" in + --subscription) + [[ -n "${2:-}" && "${2:-}" != -* ]] || { echo "Error: flag --subscription requires a value" >&2; exit 2; } + SUBSCRIPTION_ID="$2" + shift 2 + ;; + -h|--help) + usage 0 + ;; + *) + echo "Error: unexpected argument '$1'" >&2 + usage 2 + ;; + esac + done + + if [[ -n "$SUBSCRIPTION_ID" ]]; then + export AZURE_SUBSCRIPTION_ID="$SUBSCRIPTION_ID" + fi + + check_prerequisites +fi diff --git a/src/templates/sre-agent/bin/deploy.sh b/src/templates/sre-agent/bin/deploy.sh new file mode 100755 index 000000000..e40373733 --- /dev/null +++ b/src/templates/sre-agent/bin/deploy.sh @@ -0,0 +1,566 @@ +#!/usr/bin/env bash +# ============================================================================= +# deploy.sh - FinOps Toolkit SRE Agent setup +# +# Copied from microsoft/sre-agent labs/starter-lab/scripts/setup.sh and updated +# for this template: +# - uses Azure CLI + Bicep directly, not azd +# - deploys the FinOps SRE Agent infrastructure only, not the Grubify lab app +# - applies non-Bicep recipe assets with apply-extras after ARM succeeds +# ============================================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +INFRA_DIR="${PROJECT_DIR}/infra" + +usage() { + cat < [options] + +Required: + --recipe Recipe directory + --subscription Azure subscription + -g, --resource-group Resource group for the agent + -n, --name Agent name + -l, --location Azure region + +Optional: + --target-resource-group Repeatable target resource group. The agent resource group is always included. + --cluster-uri Kusto connector URI, including database name. + Example: https://..kusto.windows.net/Hub + --cluster-resource-id Optional Kusto cluster ARM resource ID. Real deployments resolve this from --cluster-uri when possible; dry-run requires it. + --no-subscription-reader Do not assign Reader at subscription scope. Default: assign Reader. + --deployer-principal-type Principal type of the deployer (User or ServicePrincipal). Default: User. + --deploy-name Deployment name override. Defaults to a deterministic name. + --dry-run Validate inputs and write parameters without Azure calls. + --force Accepted for compatibility. + -h, --help Show this help. +EOF + exit "${1:-0}" +} + +fail() { + echo -e "${RED}$1${NC}" >&2 + exit "${2:-1}" +} + +require_value() { + local flag="$1" + local value="${2:-}" + if [[ -z "$value" || "$value" == -* ]]; then + fail "Error: flag ${flag} requires a value" 2 + fi +} + +recipe_value() { + local file="$1" + local path="$2" + jq -r "$path // empty | if . == null or . == \"null\" then \"\" else . end" "$file" +} + +deterministic_deploy_name() { + "$PYTHON_CMD" - "$SUBSCRIPTION_ID" "$RESOURCE_GROUP" "$AGENT_NAME" <<'PY' +import hashlib +import re +import sys + +subscription_id, resource_group, agent_name = sys.argv[1:4] +resource_group_id = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}" +seed = f"{subscription_id}|{resource_group_id}|{agent_name}".lower() +slug = re.sub(r"[^a-z0-9-]+", "-", agent_name.lower()).strip("-") or "agent" +digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:12] +name = f"sre-agent-{slug}-{digest}" +print(name[:64].rstrip("-")) +PY +} + +deployment_output_value() { + local file="$1" + local key="$2" + local default="${3:-}" + jq -r \ + --arg key "$key" \ + --arg default "$default" \ + '(.properties.outputs // {}) + | to_entries + | map(select((.key | ascii_upcase) == ($key | ascii_upcase)) | .value.value) + | first // $default' \ + "$file" +} + +normalize_action_mode() { + case "$1" in + Autonomous|autonomous|Automatic|automatic) printf 'autonomous' ;; + Review|review) printf 'review' ;; + ReadOnly|readOnly|readonly) printf 'readOnly' ;; + *) fail "Error: unsupported action mode '$1'" 2 ;; + esac +} + +validate_kusto_uri() { + local uri="$1" + [[ -z "$uri" ]] && return 0 + if [[ "$uri" != https://*.kusto.windows.net/* ]]; then + fail "Error: --cluster-uri must be a database-qualified Kusto URI. Example: https://..kusto.windows.net/Hub" 2 + fi + local path="${uri#https://}" + if [[ "$path" != */* || -z "${path#*/}" ]]; then + fail "Error: --cluster-uri must include the Kusto database name. Example: https://..kusto.windows.net/Hub" 2 + fi +} + +parse_kusto_cluster_name() { + local uri="$1" + local host + host="${uri#https://}" + host="${host%%/*}" + printf '%s\n' "${host%%.*}" +} + +parse_kusto_database_name() { + local uri="$1" + local path + path="${uri#https://}" + path="${path#*/}" + path="${path%%\?*}" + path="${path%%#*}" + printf '%s\n' "${path%%/*}" +} + +to_lower() { + printf '%s\n' "$1" | tr '[:upper:]' '[:lower:]' +} + +append_target_rg() { + local rg="$1" + local existing + [[ -n "$rg" ]] || return 0 + if [[ "${#TARGET_RGS[@]}" -gt 0 ]]; then + for existing in "${TARGET_RGS[@]}"; do + if [[ "$(to_lower "$existing")" == "$(to_lower "$rg")" ]]; then + return 0 + fi + done + fi + TARGET_RGS+=("$rg") +} + +resource_id_subscription() { + local resource_id="$1" + printf '%s\n' "$resource_id" | awk -F/ '{print $3}' +} + +resource_id_resource_group() { + local resource_id="$1" + printf '%s\n' "$resource_id" | awk -F/ '{print $5}' +} + +resolve_kusto_cluster_resource_id() { + local uri="$1" + local cluster_name + local cluster_uri + local resource_id + local graph_result_count + + cluster_name="$(parse_kusto_cluster_name "$uri")" + [[ -n "$cluster_name" ]] || fail "Error: could not parse Kusto cluster name from --cluster-uri" 2 + cluster_uri="${uri%/*}" + + resource_id="$(az resource list \ + --subscription "$SUBSCRIPTION_ID" \ + --resource-type Microsoft.Kusto/clusters \ + --query "[?name=='${cluster_name}'].id | [0]" \ + -o tsv 2>/dev/null || true)" + + if [[ -n "$resource_id" && "$resource_id" != "null" ]]; then + printf '%s\n' "$resource_id" + return 0 + fi + + resource_id="$(az graph query \ + -q "Resources | where type =~ 'microsoft.kusto/clusters' | where name =~ '${cluster_name}' or tostring(properties.uri) =~ '${cluster_uri}' | project id | limit 2" \ + -o json 2>/dev/null | jq -r '.data[].id' 2>/dev/null || true)" + graph_result_count="$(printf '%s\n' "$resource_id" | sed '/^$/d' | wc -l | tr -d ' ')" + + if [[ "$graph_result_count" == "1" ]]; then + printf '%s\n' "$resource_id" + return 0 + fi + + if [[ -z "$resource_id" || "$resource_id" == "null" ]]; then + fail "Error: could not resolve Kusto cluster '${cluster_name}'. Pass --cluster-resource-id so deployment can assign AllDatabasesViewer before creating the connector." 2 + fi + + fail "Error: Kusto cluster '${cluster_name}' resolved to multiple resources. Pass --cluster-resource-id explicitly so deployment assigns AllDatabasesViewer to the intended cluster." 2 +} + +warn_kusto_private_query_limitation() { + local cluster_id="$1" + local cluster_json + local public_network_access + local private_endpoint_count + local cluster_uri + local docs_url="https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations" + + [[ -n "$cluster_id" ]] || return 0 + + cluster_json="$(az resource show --ids "$cluster_id" --api-version 2023-08-15 -o json 2>/dev/null || true)" + if [[ -z "$cluster_json" ]]; then + echo -e "${YELLOW}Warning: Could not inspect Kusto cluster network access. Deployment will continue.${NC}" + echo " Cluster: $cluster_id" + echo " Review SRE Agent private endpoint limitations: $docs_url" + echo "" + return 0 + fi + + public_network_access="$(echo "$cluster_json" | jq -r '.properties.publicNetworkAccess // .publicNetworkAccess // ""' 2>/dev/null || true)" + private_endpoint_count="$(echo "$cluster_json" | jq -r '((.properties.privateEndpointConnections // .privateEndpointConnections // []) | length)' 2>/dev/null || echo 0)" + cluster_uri="$(echo "$cluster_json" | jq -r '.properties.uri // .uri // empty' 2>/dev/null || true)" + + if [[ "$public_network_access" == "Disabled" ]]; then + echo -e "${YELLOW}Warning: The Kusto cluster denies public query access.${NC}" + echo " Cluster: ${cluster_uri:-$cluster_id}" + echo " publicNetworkAccess: ${public_network_access}" + echo " private endpoint connections: ${private_endpoint_count}" + echo " SRE Agent will still be deployed and the finops-hub-kusto connector will still be created." + echo " Per the SRE Agent known limitations, private endpoint ADX blocks direct KQL queries." + echo " The customer can enable public query access if they want the Kusto connector to become healthy:" + echo " $docs_url" + echo "" + fi +} + +RECIPE_DIR="" +SUBSCRIPTION_ID="" +RESOURCE_GROUP="" +AGENT_NAME="" +LOCATION="" +CLUSTER_URI="" +CLUSTER_RESOURCE_ID="" +DEPLOY_NAME="" +DRY_RUN="" +ENABLE_SUBSCRIPTION_READER="false" +DEPLOYER_PRINCIPAL_TYPE="User" +TARGET_RGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --recipe) + require_value "--recipe" "${2:-}" + RECIPE_DIR="$2" + shift 2 + ;; + --subscription) + require_value "--subscription" "${2:-}" + SUBSCRIPTION_ID="$2" + shift 2 + ;; + -g|--resource-group) + require_value "--resource-group / -g" "${2:-}" + RESOURCE_GROUP="$2" + shift 2 + ;; + -n|--name) + require_value "--name / -n" "${2:-}" + AGENT_NAME="$2" + shift 2 + ;; + -l|--location) + require_value "--location / -l" "${2:-}" + LOCATION="$2" + shift 2 + ;; + --target-resource-group) + require_value "--target-resource-group" "${2:-}" + TARGET_RGS+=("$2") + shift 2 + ;; + --cluster-uri) + require_value "--cluster-uri" "${2:-}" + CLUSTER_URI="$2" + shift 2 + ;; + --cluster-resource-id) + require_value "--cluster-resource-id" "${2:-}" + CLUSTER_RESOURCE_ID="$2" + shift 2 + ;; + --subscription-reader) + ENABLE_SUBSCRIPTION_READER="true" + shift + ;; + --no-subscription-reader) + ENABLE_SUBSCRIPTION_READER="false" + shift + ;; + --deployer-principal-type) + require_value "--deployer-principal-type" "${2:-}" + DEPLOYER_PRINCIPAL_TYPE="$2" + shift 2 + ;; + --deploy-name) + require_value "--deploy-name" "${2:-}" + DEPLOY_NAME="$2" + shift 2 + ;; + --dry-run) + DRY_RUN="true" + shift + ;; + --force) + shift + ;; + -h|--help) + usage 0 + ;; + -*) + fail "Error: unknown flag '$1'" 2 + ;; + *) + fail "Error: unknown argument '$1'" 2 + ;; + esac +done + +[[ -n "$RECIPE_DIR" ]] || fail "Error: --recipe is required" 2 +[[ -d "$RECIPE_DIR" ]] || fail "Error: recipe directory not found: $RECIPE_DIR" 1 +[[ -f "${RECIPE_DIR}/agent.json" ]] || fail "Error: recipe agent.json not found: ${RECIPE_DIR}/agent.json" 1 +[[ -n "$SUBSCRIPTION_ID" ]] || fail "Error: --subscription is required" 2 +[[ -n "$RESOURCE_GROUP" ]] || fail "Error: --resource-group is required" 2 +[[ -n "$AGENT_NAME" ]] || fail "Error: --name is required" 2 +[[ -n "$LOCATION" ]] || fail "Error: --location is required" 2 +validate_kusto_uri "$CLUSTER_URI" +if [[ -n "$CLUSTER_URI" ]]; then + [[ -n "$(parse_kusto_database_name "$CLUSTER_URI")" ]] || fail "Error: could not parse Kusto database name from --cluster-uri" 2 +fi + +append_target_rg "$RESOURCE_GROUP" + +command -v az >/dev/null || fail "Azure CLI (az) is required" 1 +command -v jq >/dev/null || fail "jq is required" 1 +command -v git >/dev/null || fail "git is required" 1 + +PYTHON_CMD="" +if command -v python3 >/dev/null; then + PYTHON_CMD="python3" +elif command -v python >/dev/null && python --version 2>&1 | grep -q "Python 3"; then + PYTHON_CMD="python" +else + fail "Python 3 is required" 1 +fi + +if [[ -n "$CLUSTER_URI" && -z "$CLUSTER_RESOURCE_ID" ]]; then + if [[ -n "$DRY_RUN" ]]; then + fail "Error: --cluster-resource-id is required with --cluster-uri for --dry-run because dry-run makes no Azure calls. Real deployments can resolve it from the Kusto URI." 2 + fi + + echo "Resolving Kusto cluster resource ID from --cluster-uri..." + az account show --subscription "$SUBSCRIPTION_ID" >/dev/null + az account set --subscription "$SUBSCRIPTION_ID" + CLUSTER_RESOURCE_ID="$(resolve_kusto_cluster_resource_id "$CLUSTER_URI")" + echo " Kusto cluster: $CLUSTER_RESOURCE_ID" +fi + +if [[ -n "$CLUSTER_RESOURCE_ID" && -z "$DRY_RUN" ]]; then + warn_kusto_private_query_limitation "$CLUSTER_RESOURCE_ID" +fi + +if [[ -n "$CLUSTER_RESOURCE_ID" ]]; then + CLUSTER_SUBSCRIPTION_ID="$(resource_id_subscription "$CLUSTER_RESOURCE_ID")" + CLUSTER_RESOURCE_GROUP="$(resource_id_resource_group "$CLUSTER_RESOURCE_ID")" + if [[ "$(to_lower "$CLUSTER_SUBSCRIPTION_ID")" == "$(to_lower "$SUBSCRIPTION_ID")" ]]; then + append_target_rg "$CLUSTER_RESOURCE_GROUP" + else + echo -e "${YELLOW}Warning: FinOps Hub cluster is in subscription ${CLUSTER_SUBSCRIPTION_ID}; add agent managed-resource scope and resource-group RBAC for ${CLUSTER_RESOURCE_GROUP} separately.${NC}" >&2 + fi +fi + +ACCESS_LEVEL="$(recipe_value "${RECIPE_DIR}/agent.json" '.access.accessLevel')" +[[ -n "$ACCESS_LEVEL" ]] || ACCESS_LEVEL="Low" +ACTION_MODE_RAW="$(recipe_value "${RECIPE_DIR}/agent.json" '.access.actionMode')" +[[ -n "$ACTION_MODE_RAW" ]] || ACTION_MODE_RAW="Review" +ACTION_MODE="$(normalize_action_mode "$ACTION_MODE_RAW")" +UPGRADE_CHANNEL="$(recipe_value "${RECIPE_DIR}/agent.json" '.upgradeChannel')" +[[ -n "$UPGRADE_CHANNEL" ]] || UPGRADE_CHANNEL="Preview" +DEFAULT_MODEL_PROVIDER="$(recipe_value "${RECIPE_DIR}/agent.json" '.defaultModelProvider')" +[[ -n "$DEFAULT_MODEL_PROVIDER" ]] || DEFAULT_MODEL_PROVIDER="MicrosoftFoundry" +DEFAULT_MODEL_NAME="$(recipe_value "${RECIPE_DIR}/agent.json" '.defaultModelName')" +[[ -n "$DEFAULT_MODEL_NAME" ]] || DEFAULT_MODEL_NAME="Automatic" +MONTHLY_AGENT_UNIT_LIMIT="$(recipe_value "${RECIPE_DIR}/agent.json" '.monthlyAgentUnitLimit')" +[[ -n "$MONTHLY_AGENT_UNIT_LIMIT" ]] || MONTHLY_AGENT_UNIT_LIMIT="10000" +EXPERIMENTAL_SETTINGS="$(jq -c '.experimentalSettings // {"EnableSandboxGroup": true, "EnableWorkspaceTools": true}' "${RECIPE_DIR}/agent.json")" +TAGS="$(jq -c '.tags // {"finops-toolkit":"sre-agent","source":"microsoft-finops-toolkit"}' "${RECIPE_DIR}/agent.json")" +TARGET_RGS_JSON="$(printf '%s\n' "${TARGET_RGS[@]}" | jq -R . | jq -sc '.')" + +[[ -n "$DEPLOY_NAME" ]] || DEPLOY_NAME="$(deterministic_deploy_name)" +BUILD_ROOT="${SRE_AGENT_DEPLOY_DIR:-${HOME}/.cache/finops-toolkit/sre-agent}" +BUILD_DIR="${BUILD_ROOT}/${AGENT_NAME}-${DEPLOY_NAME}" +mkdir -p "$BUILD_DIR" + +PARAMETERS_FILE="${BUILD_DIR}/deploy.parameters.json" +RESULT_FILE="${BUILD_DIR}/deployment-result.json" + +jq -n \ + --arg resourceGroupName "$RESOURCE_GROUP" \ + --arg agentName "$AGENT_NAME" \ + --arg location "$LOCATION" \ + --arg accessLevel "$ACCESS_LEVEL" \ + --arg actionMode "$ACTION_MODE" \ + --arg upgradeChannel "$UPGRADE_CHANNEL" \ + --arg defaultModelProvider "$DEFAULT_MODEL_PROVIDER" \ + --arg defaultModelName "$DEFAULT_MODEL_NAME" \ + --argjson monthlyAgentUnitLimit "$MONTHLY_AGENT_UNIT_LIMIT" \ + --arg kustoClusterId "$CLUSTER_RESOURCE_ID" \ + --argjson enableSubscriptionReaderRole "$ENABLE_SUBSCRIPTION_READER" \ + --arg deployerPrincipalType "$DEPLOYER_PRINCIPAL_TYPE" \ + --argjson targetResourceGroups "$TARGET_RGS_JSON" \ + --argjson experimentalSettings "$EXPERIMENTAL_SETTINGS" \ + --argjson tags "$TAGS" \ + '{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroupName": { "value": $resourceGroupName }, + "agentName": { "value": $agentName }, + "location": { "value": $location }, + "targetResourceGroups": { "value": $targetResourceGroups }, + "accessLevel": { "value": $accessLevel }, + "actionMode": { "value": $actionMode }, + "upgradeChannel": { "value": $upgradeChannel }, + "defaultModelProvider": { "value": $defaultModelProvider }, + "defaultModelName": { "value": $defaultModelName }, + "monthlyAgentUnitLimit": { "value": $monthlyAgentUnitLimit }, + "experimentalSettings": { "value": $experimentalSettings }, + "tags": { "value": $tags }, + "finopsHubKustoClusterResourceId": { "value": $kustoClusterId }, + "enableSubscriptionReaderRole": { "value": $enableSubscriptionReaderRole }, + "deployerPrincipalType": { "value": $deployerPrincipalType } + } + }' > "$PARAMETERS_FILE" + +echo "" +echo -e "${BLUE}============================================================${NC}" +echo -e "${BLUE} Azure SRE Agent - FinOps Toolkit Setup${NC}" +echo -e "${BLUE}============================================================${NC}" +echo "" +echo -e "${YELLOW}[1/4] Checking prerequisites...${NC}" +echo " az: $(az version --query '\"azure-cli\"' -o tsv 2>/dev/null || echo found)" +echo " jq: $(jq --version)" +echo " python: $($PYTHON_CMD --version 2>&1)" +echo " azd: not used" +echo "" + +if [[ -n "$DRY_RUN" ]]; then + echo -e "${YELLOW}[2/4] Planned deployment...${NC}" + echo " Subscription: $SUBSCRIPTION_ID" + echo " Resource group: $RESOURCE_GROUP" + echo " Agent: $AGENT_NAME" + echo " Region: $LOCATION" + echo " Target resource groups: ${TARGET_RGS[*]}" + echo " Subscription Reader: $ENABLE_SUBSCRIPTION_READER" + echo " Parameters: $PARAMETERS_FILE" + echo "" + echo "Dry run complete. No Azure calls were made." + exit 0 +fi + +echo -e "${YELLOW}[2/4] Checking Azure account...${NC}" +az account show --subscription "$SUBSCRIPTION_ID" --query "{subscription:name, id:id}" -o table >/dev/null +az account set --subscription "$SUBSCRIPTION_ID" +echo " Subscription: $SUBSCRIPTION_ID" +echo " Resource group: $RESOURCE_GROUP" +echo " Agent: $AGENT_NAME" +echo " Region: $LOCATION" +echo " Target resource groups: ${TARGET_RGS[*]}" +echo " Subscription Reader: $ENABLE_SUBSCRIPTION_READER" +echo " Parameters: $PARAMETERS_FILE" +echo "" + +echo -e "${YELLOW}[3/4] Deploying infrastructure with Azure CLI + Bicep...${NC}" +echo " Registering Microsoft.App provider..." +az provider register -n Microsoft.App --wait --output none + +echo " Starting deployment: $DEPLOY_NAME" +az deployment sub create \ + --subscription "$SUBSCRIPTION_ID" \ + --location "$LOCATION" \ + --name "$DEPLOY_NAME" \ + --template-file "${INFRA_DIR}/main.bicep" \ + --parameters "@${PARAMETERS_FILE}" \ + --no-wait \ + --output none + +echo " Waiting for deployment to complete..." +DEPLOYMENT_START="$(date +%s)" +DEPLOYMENT_JSON="{}" +STATE="" +while true; do + DEPLOYMENT_JSON="$(az deployment sub show \ + --subscription "$SUBSCRIPTION_ID" \ + --name "$DEPLOY_NAME" \ + --output json 2>/dev/null || echo "{}")" + STATE="$(echo "$DEPLOYMENT_JSON" | jq -r '.properties.provisioningState // "Accepted"' 2>/dev/null)" + + case "$STATE" in + Succeeded|Failed|Canceled) + break + ;; + *) + NOW="$(date +%s)" + echo " Deployment state: ${STATE} ($((NOW - DEPLOYMENT_START))s elapsed)" + sleep 10 + ;; + esac +done + +printf '%s\n' "$DEPLOYMENT_JSON" > "$RESULT_FILE" + +STATE="$(jq -r '.properties.provisioningState // "Failed"' "$RESULT_FILE")" +if [[ "$STATE" != "Succeeded" ]]; then + echo "" + echo "Deployment failed. Diagnostic command:" + echo " az deployment operation sub list --subscription ${SUBSCRIPTION_ID} -n ${DEPLOY_NAME} -o table" + exit 1 +fi + +AGENT_ENDPOINT="$(deployment_output_value "$RESULT_FILE" "SRE_AGENT_ENDPOINT")" +SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID="$(deployment_output_value "$RESULT_FILE" "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID")" +AGENT_PORTAL_URL="$(deployment_output_value "$RESULT_FILE" "AGENT_PORTAL_URL" "https://sre.azure.com")" + +[[ -n "$AGENT_ENDPOINT" ]] || fail "Deployment succeeded but did not return SRE_AGENT_ENDPOINT" 1 +[[ -n "$SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID" ]] || fail "Deployment succeeded but did not return SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID" 1 + +echo "" +echo -e "${YELLOW}[4/4] Applying SRE Agent extras...${NC}" +APPLY_EXTRAS_ARGS=( + --endpoint "$AGENT_ENDPOINT" + --subscription "$SUBSCRIPTION_ID" + --resource-group "$RESOURCE_GROUP" + --name "$AGENT_NAME" + --recipe "$RECIPE_DIR" + --build-dir "${BUILD_DIR}/extras" +) + +if [[ -n "$CLUSTER_URI" ]]; then + APPLY_EXTRAS_ARGS+=(--kusto-connector-uri "$CLUSTER_URI") +fi + +bash "${SCRIPT_DIR}/apply-extras.sh" \ + "${APPLY_EXTRAS_ARGS[@]}" + +echo "" +echo -e "${BLUE}============================================================${NC}" +echo -e "${GREEN} SRE Agent ready${NC}" +echo -e "${BLUE}============================================================${NC}" +echo " Agent portal: $AGENT_PORTAL_URL" +echo " Endpoint: $AGENT_ENDPOINT" +echo " Build dir: $BUILD_DIR" +echo "" diff --git a/src/templates/sre-agent/bin/post-provision.sh b/src/templates/sre-agent/bin/post-provision.sh new file mode 100755 index 000000000..4bf44af9f --- /dev/null +++ b/src/templates/sre-agent/bin/post-provision.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Compatibility wrapper. The SRE Agent recipe now follows the upstream +# Bicep + apply-extras pattern for post-provisioning. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +echo "bin/post-provision.sh is deprecated; forwarding to bin/apply-extras.sh" >&2 +exec bash "${SCRIPT_DIR}/apply-extras.sh" "$@" diff --git a/src/templates/sre-agent/bin/verify-agent.sh b/src/templates/sre-agent/bin/verify-agent.sh new file mode 100755 index 000000000..b6cab003a --- /dev/null +++ b/src/templates/sre-agent/bin/verify-agent.sh @@ -0,0 +1,548 @@ +#!/usr/bin/env bash +# verify-agent.sh — Verify an SRE Agent deployment is complete. +# +# Usage: +# ./verify-agent.sh [--expected ] +# +# Queries ARM + data-plane APIs and prints a pass/fail table. +# If --expected is given, compares counts against the config directory. + +set -uo pipefail + +usage() { + cat < [--expected ] + +Arguments: + Subscription + Resource group + Agent name + +Options: + --expected Expected config directory + -h, --help Show this help +EOF + exit "${1:-0}" +} + +[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage 0 +[[ $# -ge 3 ]] || usage 2 + +SUB="$1" +RG="$2" +AGENT="$3" +EXPECTED_DIR="" +EXPECTED_CONFIG="" +EXPECTED_CONNECTORS="" +EXPECTED_CONFIG_HAS_CONNECTORS="false" +shift 3 +while [[ $# -gt 0 ]]; do + case "$1" in + --expected) + if [[ -z "${2:-}" || "${2:-}" == -* ]]; then + echo "Error: flag --expected requires a value" >&2 + usage 2 + fi + EXPECTED_DIR="$2" + shift 2 + ;; + -h|--help) usage 0 ;; + *) + echo "Error: unknown argument '$1'" >&2 + usage 2 + ;; + esac +done + +az() { command az "$@" --subscription "$SUB"; } + +# Load expected-config.json if present +if [[ -n "$EXPECTED_DIR" && -f "${EXPECTED_DIR}/expected-config.json" ]]; then + EXPECTED_CONFIG=$(cat "${EXPECTED_DIR}/expected-config.json") + if echo "$EXPECTED_CONFIG" | jq -e 'has("connectors")' >/dev/null 2>&1; then + EXPECTED_CONFIG_HAS_CONNECTORS="true" + fi +fi +if [[ -n "$EXPECTED_DIR" && -f "${EXPECTED_DIR}/connectors.json" ]]; then + EXPECTED_CONNECTORS=$(jq -c '.connectors // []' "${EXPECTED_DIR}/connectors.json" 2>/dev/null || echo "[]") +fi + +# Helper: get expected value from expected-config.json +exp() { + local path="$1" fallback="${2:--}" + if [[ -n "$EXPECTED_CONFIG" ]]; then + local val + val=$(echo "$EXPECTED_CONFIG" | jq -r "$path // empty" 2>/dev/null) + [[ -n "$val" && "$val" != "null" ]] && echo "$val" && return + fi + echo "$fallback" +} +exp_list() { + local path="$1" + if [[ -n "$EXPECTED_CONFIG" ]]; then + echo "$EXPECTED_CONFIG" | jq -r "$path // [] | sort | join(\",\")" 2>/dev/null + fi +} +count_present_names() { + local values="$1" + local expected_csv="$2" + echo "$values" | jq -r --arg expected_csv "$expected_csv" ' + ($expected_csv | split(",") | map(select(. != ""))) as $expected + | [.[].name] as $actual + | [$expected[] | . as $name | select($actual | index($name))] | length + ' 2>/dev/null || echo 0 +} +count_enabled_names() { + local values="$1" + local expected_csv="$2" + echo "$values" | jq -r --arg expected_csv "$expected_csv" ' + ($expected_csv | split(",") | map(select(. != ""))) as $expected + | [.[] | select(.enabled == true) | .name] as $actual + | [$expected[] | . as $name | select($actual | index($name))] | length + ' 2>/dev/null || echo 0 +} +knowledge_source_name() { + basename "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/-+/-/g; s/^-//; s/-$//' +} + +API_VERSION="2026-01-01" +ARM_BASE="https://management.azure.com/subscriptions/${SUB}/resourceGroups/${RG}/providers/Microsoft.App/agents/${AGENT}" + +# Resolve agent endpoint +AGENT_JSON=$(az rest -m GET --url "${ARM_BASE}?api-version=${API_VERSION}" -o json 2>/dev/null || echo "{}") +ENDPOINT=$(echo "$AGENT_JSON" | jq -r '.properties.agentEndpoint // empty') +if [[ -z "$ENDPOINT" || "$ENDPOINT" == "null" ]]; then + echo "FAIL: Could not resolve agent endpoint for ${AGENT} in ${RG}" + exit 1 +fi + +TOKEN=$(az account get-access-token --resource https://azuresre.dev --query accessToken -o tsv 2>/dev/null) +if [[ -z "$TOKEN" ]]; then + echo "FAIL: Could not get data-plane token" + exit 1 +fi + +dp_get() { curl -sS "$ENDPOINT$1" -H "Authorization: Bearer $TOKEN" 2>/dev/null; } + +PASS=0 +FAIL=0 +RESULTS="" + +check() { + local name="$1" actual="$2" expected="$3" + if [[ "$expected" == "-" ]]; then + RESULTS="${RESULTS}\n ${name}|${actual}|—|✅" + PASS=$((PASS + 1)) + elif [[ "$actual" == "$expected" ]]; then + RESULTS="${RESULTS}\n ${name}|${actual}|${expected}|✅ PASS" + PASS=$((PASS + 1)) + else + RESULTS="${RESULTS}\n ${name}|${actual}|${expected}|❌ FAIL" + FAIL=$((FAIL + 1)) + fi +} + +echo "" +echo "═══════════════════════════════════════════════════" +echo " SRE Agent Verification: ${AGENT}" +echo " Endpoint: ${ENDPOINT}" +echo "═══════════════════════════════════════════════════" +echo "" + +# ── Agent properties ── +PROPS=$(echo "$AGENT_JSON" | jq -c '{ + accessLevel: .properties.actionConfiguration.accessLevel, + mode: .properties.actionConfiguration.mode, + upgradeChannel: .properties.upgradeChannel, + modelProvider: .properties.defaultModel.provider, + incidentPlatform: (.properties.incidentManagementConfiguration.type // "None"), + experimentalSettings: (.properties.experimentalSettings | keys | sort | join(",")) +}') +check "Agent exists" "yes" "yes" +check "Access level" "$(echo "$PROPS" | jq -r '.accessLevel')" "$(exp '.agent.accessLevel')" +check "Action mode" "$(echo "$PROPS" | jq -r '.mode')" "$(exp '.agent.actionMode')" +check "Upgrade channel" "$(echo "$PROPS" | jq -r '.upgradeChannel')" "$(exp '.agent.upgradeChannel')" +EXP_EXPERIMENTAL_SETTINGS="$(exp_list '.agent.experimentalSettings')" +if [[ -n "$EXP_EXPERIMENTAL_SETTINGS" ]]; then + EXP_EXPERIMENTAL_CT=$(exp '.agent.experimentalSettings | length' "-") + ACTUAL_EXPERIMENTAL_SETTINGS="$(echo "$PROPS" | jq -r '.experimentalSettings')" + ACTUAL_EXPERIMENTAL_VALUES="$(printf '%s\n' "$ACTUAL_EXPERIMENTAL_SETTINGS" | jq -R 'split(",") | map({name: .})')" + EXPERIMENTAL_PRESENT=$(count_present_names "$ACTUAL_EXPERIMENTAL_VALUES" "$EXP_EXPERIMENTAL_SETTINGS") + check "Experimental settings expected" "$EXPERIMENTAL_PRESENT" "$EXP_EXPERIMENTAL_CT" + RESULTS="${RESULTS}\n Experimental settings|${ACTUAL_EXPERIMENTAL_SETTINGS}|—|" +else + check "Experimental settings" "$(echo "$PROPS" | jq -r '.experimentalSettings')" "-" +fi +check "Model provider" "$(echo "$PROPS" | jq -r '.modelProvider')" "$(exp '.agent.defaultModelProvider')" +check "Incident platform" "$(echo "$PROPS" | jq -r '.incidentPlatform')" "$(exp '.agent.incidentPlatform')" + +# ── Onboarding discovery prerequisites ── +MANAGED_RESOURCE_GROUPS=$(echo "$AGENT_JSON" | jq -r '.properties.knowledgeGraphConfiguration.managedResources // [] | join(",")' 2>/dev/null) +EXPECTED_RG_ID="/subscriptions/${SUB}/resourceGroups/${RG}" +MANAGED_RESOURCE_GROUP_PRESENT=$(echo "$AGENT_JSON" | jq -r --arg expected "$EXPECTED_RG_ID" ' + [.properties.knowledgeGraphConfiguration.managedResources[]? | ascii_downcase] | index($expected | ascii_downcase) != null +' 2>/dev/null || echo false) +[[ "$MANAGED_RESOURCE_GROUP_PRESENT" == "true" ]] && check "Onboarding managed resource group" "present" "present" || check "Onboarding managed resource group" "missing" "present" +RESULTS="${RESULTS}\n Managed resource groups|${MANAGED_RESOURCE_GROUPS}|—|" + +ACTION_IDENTITY=$(echo "$AGENT_JSON" | jq -r '.properties.actionConfiguration.identity // empty' 2>/dev/null) +ACTION_PRINCIPAL_ID="" +if [[ "$(printf '%s' "$ACTION_IDENTITY" | tr '[:upper:]' '[:lower:]')" == "system" ]]; then + ACTION_PRINCIPAL_ID=$(echo "$AGENT_JSON" | jq -r '.identity.principalId // empty' 2>/dev/null) +elif [[ -n "$ACTION_IDENTITY" ]]; then + ACTION_PRINCIPAL_ID=$(echo "$AGENT_JSON" | jq -r --arg identity "$ACTION_IDENTITY" ' + .identity.userAssignedIdentities as $identities + | ($identities[$identity].principalId // ( + $identities + | to_entries[] + | select(.key | ascii_downcase == ($identity | ascii_downcase)) + | .value.principalId + ) // empty) + ' 2>/dev/null) +fi + +if [[ -n "$ACTION_PRINCIPAL_ID" ]]; then + EXPECTED_SUBSCRIPTION_ROLES="$(exp_list '.subscriptionRoleAssignments')" + if [[ -n "$EXPECTED_SUBSCRIPTION_ROLES" ]]; then + SUBSCRIPTION_SCOPE="/subscriptions/${SUB}" + SUBSCRIPTION_ROLE_ASSIGNMENTS=$(az role assignment list --assignee "$ACTION_PRINCIPAL_ID" --scope "$SUBSCRIPTION_SCOPE" -o json 2>/dev/null || echo "[]") + SUBSCRIPTION_ROLE_NAMES=$(echo "$SUBSCRIPTION_ROLE_ASSIGNMENTS" | jq -r '[.[].roleDefinitionName] | sort | join(",")' 2>/dev/null) + SUBSCRIPTION_ROLES_PRESENT=$(echo "$SUBSCRIPTION_ROLE_ASSIGNMENTS" | jq -r --arg expected_csv "$EXPECTED_SUBSCRIPTION_ROLES" ' + ($expected_csv | split(",") | map(select(. != ""))) as $expected + | [.[].roleDefinitionName] as $actual + | all($expected[]; $actual | index(.)) + ' 2>/dev/null || echo false) + check "Subscription identity RBAC" "$SUBSCRIPTION_ROLES_PRESENT" "true" + RESULTS="${RESULTS}\n Subscription identity roles (${SUBSCRIPTION_SCOPE})|${SUBSCRIPTION_ROLE_NAMES}|—|" + fi + + EXPECTED_TARGET_ROLES="Log Analytics Reader,Monitoring Reader,Reader" + if [[ "$(echo "$PROPS" | jq -r '.accessLevel')" == "High" ]]; then + EXPECTED_TARGET_ROLES="Contributor,Log Analytics Reader,Monitoring Reader,Reader" + fi + + MANAGED_SCOPE_CT=$(echo "$AGENT_JSON" | jq '.properties.knowledgeGraphConfiguration.managedResources // [] | length' 2>/dev/null || echo 0) + MANAGED_SCOPES_WITH_RBAC=0 + while IFS= read -r managed_scope; do + [[ -n "$managed_scope" ]] || continue + TARGET_ROLE_ASSIGNMENTS=$(az role assignment list --assignee "$ACTION_PRINCIPAL_ID" --scope "$managed_scope" -o json 2>/dev/null || echo "[]") + TARGET_ROLE_NAMES=$(echo "$TARGET_ROLE_ASSIGNMENTS" | jq -r '[.[].roleDefinitionName] | sort | join(",")' 2>/dev/null) + TARGET_ROLES_PRESENT=$(echo "$TARGET_ROLE_ASSIGNMENTS" | jq -r --arg expected_csv "$EXPECTED_TARGET_ROLES" ' + ($expected_csv | split(",") | map(select(. != ""))) as $expected + | [.[].roleDefinitionName] as $actual + | all($expected[]; $actual | index(.)) + ' 2>/dev/null || echo false) + if [[ "$TARGET_ROLES_PRESENT" == "true" ]]; then + MANAGED_SCOPES_WITH_RBAC=$((MANAGED_SCOPES_WITH_RBAC + 1)) + fi + RESULTS="${RESULTS}\n Onboarding identity roles (${managed_scope})|${TARGET_ROLE_NAMES}|—|" + done < <(echo "$AGENT_JSON" | jq -r '.properties.knowledgeGraphConfiguration.managedResources[]?' 2>/dev/null) + check "Onboarding identity RBAC" "$MANAGED_SCOPES_WITH_RBAC" "$MANAGED_SCOPE_CT" +else + check "Onboarding identity RBAC" "missing action identity" "present" +fi + +# ── Connectors ── +CONNECTORS=$(dp_get "/api/v2/extendedAgent/connectors") +CONNECTOR_VALUES=$(echo "$CONNECTORS" | jq -c '(.value // []) | if type == "array" then [.[] | select(.properties.dataConnectorType != "KnowledgeFile" and .properties.dataConnectorType != "KnowledgeText" and .properties.dataConnectorType != "KnowledgeWebPage")] else [] end' 2>/dev/null || echo "[]") +CONN_CT=$(echo "$CONNECTOR_VALUES" | jq 'length') +CONN_HEALTHY=$(echo "$CONNECTOR_VALUES" | jq '[.[] | select((.properties.provisioningState // "Succeeded") == "Succeeded" or (.properties.provisioningState // "Succeeded") == "Running")] | length') +CONN_ERRORED=$(echo "$CONNECTOR_VALUES" | jq '[.[] | select((.properties.provisioningState // "Succeeded") != "Succeeded" and (.properties.provisioningState // "Succeeded") != "Running")] | length') +EXP_CONN_CT=$(exp '.connectors | length' "-") +if [[ "$EXPECTED_CONFIG_HAS_CONNECTORS" != "true" && -n "$EXPECTED_CONNECTORS" ]]; then + EXP_CONN_CT=$(echo "$EXPECTED_CONNECTORS" | jq 'length') +fi +check "Connectors (total)" "$CONN_CT" "$EXP_CONN_CT" +check "Connectors (healthy)" "$CONN_HEALTHY" "$CONN_CT" +# Show errored connectors explicitly +if [[ "$CONN_ERRORED" -gt 0 ]]; then + ERRORED_LIST=$(echo "$CONNECTOR_VALUES" | jq -r '.[] | select((.properties.provisioningState // "Succeeded") != "Succeeded" and (.properties.provisioningState // "Succeeded") != "Running") | "\(.name) (\(.properties.dataConnectorType)): \(.properties.provisioningState)"') + RESULTS="${RESULTS}\n ⚠ Errored connectors|${ERRORED_LIST}||❌ FAIL" + FAIL=$((FAIL + 1)) +fi +CONN_NAMES=$(echo "$CONNECTOR_VALUES" | jq -r '.[].name' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') +EXP_CONN_NAMES=$(exp_list '.connectors[].name') +if [[ "$EXPECTED_CONFIG_HAS_CONNECTORS" != "true" && -n "$EXPECTED_CONNECTORS" ]]; then + EXP_CONN_NAMES=$(echo "$EXPECTED_CONNECTORS" | jq -r '[.[].name] | sort | join(",")') +fi +[[ -n "$EXP_CONN_NAMES" ]] && check "Connector names" "$CONN_NAMES" "$EXP_CONN_NAMES" || RESULTS="${RESULTS}\n Connector names|${CONN_NAMES}|—|" + +# ── 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) +SKILL_NAMES=$(echo "$SKILLS" | jq -r '(if type == "array" then . elif .value then .value else [] end)[].name' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') +EXP_SKILL_CT=$(exp '.skills | length' "-") +EXP_SKILL_NAMES=$(exp_list '.skills') +check "Skills" "$SKILL_CT" "$EXP_SKILL_CT" +[[ -n "$EXP_SKILL_NAMES" ]] && check "Skill names" "$SKILL_NAMES" "$EXP_SKILL_NAMES" || RESULTS="${RESULTS}\n Skill names|${SKILL_NAMES}|—|" + +# ── Subagents ── +SUBAGENTS=$(dp_get "/api/v2/extendedAgent/agents") +SA_CT=$(echo "$SUBAGENTS" | jq '.value | length' 2>/dev/null || echo 0) +SA_NAMES=$(echo "$SUBAGENTS" | jq -r '.value[].name' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') +EXP_SA_CT=$(exp '.subagents | length' "-") +EXP_SA_NAMES=$(exp_list '.subagents') +check "Subagents" "$SA_CT" "$EXP_SA_CT" +[[ -n "$EXP_SA_NAMES" ]] && check "Subagent names" "$SA_NAMES" "$EXP_SA_NAMES" || RESULTS="${RESULTS}\n Subagent names|${SA_NAMES}|—|" + +EXP_TOOL_OWNER_NAMES=$(exp_list '.subagentRequirements.toolOwners') +if [[ -n "$EXP_TOOL_OWNER_NAMES" ]]; then + KNOWLEDGE_ONLY_TOOLS=$(exp_list '.subagentRequirements.knowledgeOnlyTools') + ACTUAL_TOOL_OWNER_NAMES=$(echo "$SUBAGENTS" | jq -r --arg knowledgeOnlyTools "$KNOWLEDGE_ONLY_TOOLS" ' + [.value[]? + | select(((((.properties.tools // []) + (.properties.systemTools // []) + (.properties.mcpTools // [])) - (($knowledgeOnlyTools | split(",") | map(select(length > 0))) // [])) | length) > 0) + | .name] | sort | join(",") + ' 2>/dev/null) + check "Operational tool-bearing agents" "$ACTUAL_TOOL_OWNER_NAMES" "$EXP_TOOL_OWNER_NAMES" +fi + +EXP_NO_TOOL_NAMES=$(exp_list '.subagentRequirements.noTools') +if [[ -n "$EXP_NO_TOOL_NAMES" ]]; then + KNOWLEDGE_ONLY_TOOLS=$(exp_list '.subagentRequirements.knowledgeOnlyTools') + ACTUAL_NO_TOOL_NAMES=$(echo "$SUBAGENTS" | jq -r --arg knowledgeOnlyTools "$KNOWLEDGE_ONLY_TOOLS" ' + [.value[]? + | select(((((.properties.tools // []) + (.properties.systemTools // []) + (.properties.mcpTools // [])) - (($knowledgeOnlyTools | split(",") | map(select(length > 0))) // [])) | length) == 0) + | .name] | sort | join(",") + ' 2>/dev/null) + check "Agents without operational tools" "$ACTUAL_NO_TOOL_NAMES" "$EXP_NO_TOOL_NAMES" +fi + +if [[ -n "$EXPECTED_CONFIG" ]]; then + EXP_TOOLS_BY_AGENT_NAMES=$(echo "$EXPECTED_CONFIG" | jq -r '.subagentRequirements.toolsByAgent // {} | keys | sort | join(",")' 2>/dev/null) + if [[ -n "$EXP_TOOLS_BY_AGENT_NAMES" ]]; then + IFS=',' read -r -a TOOLS_BY_AGENT_NAMES <<< "$EXP_TOOLS_BY_AGENT_NAMES" + for tools_agent in "${TOOLS_BY_AGENT_NAMES[@]}"; do + [[ -n "$tools_agent" ]] || continue + EXP_AGENT_TOOLS=$(echo "$EXPECTED_CONFIG" | jq -r --arg agent "$tools_agent" '.subagentRequirements.toolsByAgent[$agent] // [] | sort | join(",")' 2>/dev/null) + ACTUAL_AGENT_TOOLS=$(echo "$SUBAGENTS" | jq -r --arg agent "$tools_agent" ' + ([.value[]? | select(.name == $agent)][0].properties // {}) + | (((.tools // []) + (.systemTools // []) + (.mcpTools // [])) | sort | join(",")) + ' 2>/dev/null) + check "Tools: ${tools_agent}" "$ACTUAL_AGENT_TOOLS" "$EXP_AGENT_TOOLS" + done + fi +fi + +if [[ -n "$EXPECTED_CONFIG" ]]; then + EXP_HANDOFF_AGENT_NAMES=$(echo "$EXPECTED_CONFIG" | jq -r '.subagentRequirements.handoffs // {} | keys | sort | join(",")' 2>/dev/null) + if [[ -n "$EXP_HANDOFF_AGENT_NAMES" ]]; then + IFS=',' read -r -a HANDOFF_AGENTS <<< "$EXP_HANDOFF_AGENT_NAMES" + for handoff_agent in "${HANDOFF_AGENTS[@]}"; do + [[ -n "$handoff_agent" ]] || continue + EXP_HANDOFFS=$(echo "$EXPECTED_CONFIG" | jq -r --arg agent "$handoff_agent" '.subagentRequirements.handoffs[$agent] // [] | sort | join(",")' 2>/dev/null) + ACTUAL_HANDOFFS=$(echo "$SUBAGENTS" | jq -r --arg agent "$handoff_agent" '[.value[]? | select(.name == $agent)][0].properties.handoffs // [] | sort | join(",")' 2>/dev/null) + check "Handoffs: ${handoff_agent}" "$ACTUAL_HANDOFFS" "$EXP_HANDOFFS" + done + fi + + EXP_NO_HANDOFF_NAMES=$(exp_list '.subagentRequirements.noHandoffs') + if [[ -n "$EXP_NO_HANDOFF_NAMES" ]]; then + ACTUAL_NO_HANDOFF_NAMES=$(echo "$SUBAGENTS" | jq -r ' + [.value[]? | select(((.properties.handoffs // []) | length) == 0) | .name] | sort | join(",") + ' 2>/dev/null) + check "Agents without handoffs" "$ACTUAL_NO_HANDOFF_NAMES" "$EXP_NO_HANDOFF_NAMES" + fi +fi + +# ── Built-in tool configuration ── +EXP_BUILT_IN_TOOL_NAMES=$(exp_list '.builtInTools.enabled') +if [[ -n "$EXP_BUILT_IN_TOOL_NAMES" ]]; then + AGENT_TOOLS=$(dp_get "/api/v2/agent/tools") + AGENT_TOOL_VALUES=$(echo "$AGENT_TOOLS" | jq -c '.data // []' 2>/dev/null || echo "[]") + EXP_BUILT_IN_TOOL_CT=$(exp '.builtInTools.enabled | length' "-") + BUILT_IN_ENABLED_PRESENT=$(count_enabled_names "$AGENT_TOOL_VALUES" "$EXP_BUILT_IN_TOOL_NAMES") + check "Built-in tools enabled" "$BUILT_IN_ENABLED_PRESENT" "$EXP_BUILT_IN_TOOL_CT" + + EXPECTED_LOG_QUERY_CT=$(exp '.builtInTools.categories["Log Query"]' "-") + if [[ "$EXPECTED_LOG_QUERY_CT" != "-" ]]; then + ACTUAL_LOG_QUERY_CT=$(echo "$AGENT_TOOL_VALUES" | jq '[.[] | select(.category == "Log Query" and .enabled == true)] | length' 2>/dev/null || echo 0) + check "Log Query tools enabled" "$ACTUAL_LOG_QUERY_CT" "$EXPECTED_LOG_QUERY_CT" + fi + + EXPECTED_VISUALIZATION_CT=$(exp '.builtInTools.categories.Visualization' "-") + if [[ "$EXPECTED_VISUALIZATION_CT" != "-" ]]; then + ACTUAL_VISUALIZATION_CT=$(echo "$AGENT_TOOL_VALUES" | jq '[.[] | select(.category == "Visualization" and .enabled == true)] | length' 2>/dev/null || echo 0) + check "Visualization tools enabled" "$ACTUAL_VISUALIZATION_CT" "$EXPECTED_VISUALIZATION_CT" + fi +fi + +# ── Hooks ── +HOOKS=$(dp_get "/api/v2/extendedAgent/hooks") +HOOK_CT=$(echo "$HOOKS" | jq '.value // . | if type == "array" then length else 0 end' 2>/dev/null || echo 0) +HOOK_NAMES=$(echo "$HOOKS" | jq -r '(.value // .)[].name' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') +EXP_HOOK_CT=$(exp '.hooks | length' "-") +EXP_HOOK_NAMES=$(exp_list '.hooks') +check "Hooks" "$HOOK_CT" "$EXP_HOOK_CT" +[[ -n "$EXP_HOOK_NAMES" ]] && check "Hook names" "$HOOK_NAMES" "$EXP_HOOK_NAMES" || RESULTS="${RESULTS}\n Hook names|${HOOK_NAMES}|—|" + +# ── Common Prompts ── +PROMPTS=$(dp_get "/api/v2/extendedAgent/commonprompts") +PROMPT_CT=$(echo "$PROMPTS" | jq '.value // . | if type == "array" then length else 0 end' 2>/dev/null || echo 0) +PROMPT_NAMES=$(echo "$PROMPTS" | jq -r '(.value // .)[].name' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') +EXP_PROMPT_CT=$(exp '.commonPrompts | length' "-") +EXP_PROMPT_NAMES=$(exp_list '.commonPrompts') +check "Common Prompts" "$PROMPT_CT" "$EXP_PROMPT_CT" +[[ -n "$EXP_PROMPT_NAMES" ]] && check "Prompt names" "$PROMPT_NAMES" "$EXP_PROMPT_NAMES" || RESULTS="${RESULTS}\n Prompt names|${PROMPT_NAMES}|—|" + +# ── Scheduled Tasks ── +TASKS=$(dp_get "/api/v1/scheduledtasks") +TASK_CT=$(echo "$TASKS" | jq 'if type == "array" then length else 0 end' 2>/dev/null || echo 0) +TASK_UNIQUE=$(echo "$TASKS" | jq '[.[].name] | unique | length' 2>/dev/null || echo 0) +TASK_NAMES=$(echo "$TASKS" | jq -r '[.[].name] | unique | sort | join(",")' 2>/dev/null) +EXP_TASK_CT=$(exp '.scheduledTasks | length' "-") +EXP_TASK_NAMES=$(exp_list '.scheduledTasks') +check "Scheduled Tasks (unique)" "$TASK_UNIQUE" "$EXP_TASK_CT" +[[ -n "$EXP_TASK_NAMES" ]] && check "Task names" "$TASK_NAMES" "$EXP_TASK_NAMES" || true +if [[ "$TASK_CT" != "$TASK_UNIQUE" ]]; then + RESULTS="${RESULTS}\n Scheduled task duplicates|${TASK_CT} total, ${TASK_UNIQUE} unique|0 duplicates|❌ FAIL" + FAIL=$((FAIL + 1)) +fi +EXP_TASK_OWNER=$(exp '.scheduledTaskRequirements.ownerAgent' "") +if [[ -n "$EXP_TASK_OWNER" ]]; then + TASKS_WITH_EXPECTED_OWNER=$(echo "$TASKS" | jq -r --arg owner "$EXP_TASK_OWNER" ' + [.[]? | select((.agent // .properties.agent // .spec.agent // .agentName // .properties.agentName // .properties.targetAgent // "") == $owner)] | length + ' 2>/dev/null || echo 0) + check "Scheduled task owner" "$TASKS_WITH_EXPECTED_OWNER" "$TASK_CT" + TASK_OWNER_MISMATCHES=$(echo "$TASKS" | jq -r --arg owner "$EXP_TASK_OWNER" ' + [.[]? + | (.agent // .properties.agent // .spec.agent // .agentName // .properties.agentName // .properties.targetAgent // "missing") as $actual + | select($actual != $owner) + | "\(.name // .metadata.name // .id // "unnamed") -> \($actual)" + ] | join("; ") + ' 2>/dev/null) + if [[ -n "$TASK_OWNER_MISMATCHES" ]]; then + RESULTS="${RESULTS}\n Scheduled task owner mismatches|${TASK_OWNER_MISMATCHES}|${EXP_TASK_OWNER}|❌ FAIL" + FAIL=$((FAIL + 1)) + fi +fi +if [[ -n "$EXPECTED_CONFIG" ]]; then + EXP_TASK_PROMPT_REQUIRED=$(echo "$EXPECTED_CONFIG" | jq -c '.scheduledTaskRequirements.promptRequiredText // []' 2>/dev/null || echo "[]") + EXP_TASK_PROMPT_REQUIRED_CT=$(echo "$EXP_TASK_PROMPT_REQUIRED" | jq 'length' 2>/dev/null || echo 0) + if [[ "$EXP_TASK_PROMPT_REQUIRED_CT" -gt 0 ]]; then + TASKS_WITH_REQUIRED_PROMPT=$(echo "$TASKS" | jq -r --argjson required "$EXP_TASK_PROMPT_REQUIRED" ' + [.[]? + | (.agentPrompt // .agent_prompt // .prompt // .properties.agentPrompt // .properties.agent_prompt // .properties.prompt // .spec.agentPrompt // .spec.agent_prompt // .spec.prompt // "") as $prompt + | select(([$required[] as $needle | select(($prompt | contains($needle)) | not)] | length) == 0) + ] | length + ' 2>/dev/null || echo 0) + check "Scheduled task prompt required text" "$TASKS_WITH_REQUIRED_PROMPT" "$TASK_CT" + TASK_REQUIRED_PROMPT_MISMATCHES=$(echo "$TASKS" | jq -r --argjson required "$EXP_TASK_PROMPT_REQUIRED" ' + [.[]? + | (.name // .metadata.name // .id // "unnamed") as $name + | (.agentPrompt // .agent_prompt // .prompt // .properties.agentPrompt // .properties.agent_prompt // .properties.prompt // .spec.agentPrompt // .spec.agent_prompt // .spec.prompt // "") as $prompt + | select(([$required[] as $needle | select(($prompt | contains($needle)) | not)] | length) > 0) + | $name + ] | join(",") + ' 2>/dev/null) + if [[ -n "$TASK_REQUIRED_PROMPT_MISMATCHES" ]]; then + RESULTS="${RESULTS}\n Scheduled task prompt missing routing guard|${TASK_REQUIRED_PROMPT_MISMATCHES}|all tasks|❌ FAIL" + FAIL=$((FAIL + 1)) + fi + fi + + EXP_TASK_PROMPT_FORBIDDEN=$(echo "$EXPECTED_CONFIG" | jq -c '.scheduledTaskRequirements.promptForbiddenText // []' 2>/dev/null || echo "[]") + EXP_TASK_PROMPT_FORBIDDEN_CT=$(echo "$EXP_TASK_PROMPT_FORBIDDEN" | jq 'length' 2>/dev/null || echo 0) + if [[ "$EXP_TASK_PROMPT_FORBIDDEN_CT" -gt 0 ]]; then + TASKS_WITH_FORBIDDEN_PROMPT=$(echo "$TASKS" | jq -r --argjson forbidden "$EXP_TASK_PROMPT_FORBIDDEN" ' + [.[]? + | (.agentPrompt // .agent_prompt // .prompt // .properties.agentPrompt // .properties.agent_prompt // .properties.prompt // .spec.agentPrompt // .spec.agent_prompt // .spec.prompt // "") as $prompt + | select(([$forbidden[] as $needle | select($prompt | contains($needle))] | length) > 0) + ] | length + ' 2>/dev/null || echo 0) + check "Scheduled task prompt forbidden text" "$TASKS_WITH_FORBIDDEN_PROMPT" "0" + TASK_FORBIDDEN_PROMPT_MISMATCHES=$(echo "$TASKS" | jq -r --argjson forbidden "$EXP_TASK_PROMPT_FORBIDDEN" ' + [.[]? + | (.name // .metadata.name // .id // "unnamed") as $name + | (.agentPrompt // .agent_prompt // .prompt // .properties.agentPrompt // .properties.agent_prompt // .properties.prompt // .spec.agentPrompt // .spec.agent_prompt // .spec.prompt // "") as $prompt + | [ $forbidden[] as $needle | select($prompt | contains($needle)) | $needle ] as $matches + | select(($matches | length) > 0) + | "\($name): \($matches | join("; "))" + ] | join(" | ") + ' 2>/dev/null) + if [[ -n "$TASK_FORBIDDEN_PROMPT_MISMATCHES" ]]; then + RESULTS="${RESULTS}\n Scheduled task prompt forbidden matches|${TASK_FORBIDDEN_PROMPT_MISMATCHES}|none|❌ FAIL" + FAIL=$((FAIL + 1)) + fi + fi +fi + +# ── Knowledge sources ── +DATA_CONNECTORS=$(dp_get "/api/v2/extendedAgent/connectors") +KNOWLEDGE_SOURCE_VALUES=$(echo "$DATA_CONNECTORS" | jq -c '[.value[]? | select(.properties.dataConnectorType == "KnowledgeFile" or .properties.dataConnectorType == "KnowledgeText" or .properties.dataConnectorType == "KnowledgeWebPage")]' 2>/dev/null || echo "[]") +KS_CT=$(echo "$KNOWLEDGE_SOURCE_VALUES" | jq 'length') +KS_NAMES=$(echo "$KNOWLEDGE_SOURCE_VALUES" | jq -r '[.[].name] | sort | join(",")' 2>/dev/null) +EXP_KS_CT=$(exp '.knowledgeSources | length' "-") +EXP_KS_NAMES=$(exp_list '.knowledgeSources') +check "Knowledge sources total" "$KS_CT" "-" +if [[ -n "$EXP_KS_NAMES" && "$EXP_KS_CT" != "-" ]]; then + KS_EXPECTED_PRESENT=0 + KS_EXPECTED_INDEXED=0 + KS_EXPECTED_UNINDEXED="" + IFS=',' read -r -a EXPECTED_KNOWLEDGE_DOCS <<< "$EXP_KS_NAMES" + for doc in "${EXPECTED_KNOWLEDGE_DOCS[@]}"; do + [[ -n "$doc" ]] || continue + source_name="$(knowledge_source_name "$doc")" + present=$(echo "$KNOWLEDGE_SOURCE_VALUES" | jq -r --arg name "$source_name" --arg display "$doc" ' + [ .[] | select(.name == $name or .properties.extendedProperties.displayName == $display) ] | length + ' 2>/dev/null || echo 0) + if [[ "$present" -gt 0 ]]; then + KS_EXPECTED_PRESENT=$((KS_EXPECTED_PRESENT + 1)) + detail=$(dp_get "/api/v2/extendedAgent/connectors/${source_name}") + indexed=$(echo "$detail" | jq -r '.properties.extendedProperties.createdAt // empty' 2>/dev/null || true) + if [[ -n "$indexed" ]]; then + KS_EXPECTED_INDEXED=$((KS_EXPECTED_INDEXED + 1)) + else + KS_EXPECTED_UNINDEXED="${KS_EXPECTED_UNINDEXED}${KS_EXPECTED_UNINDEXED:+; }${source_name}" + fi + fi + done + check "Knowledge sources expected" "$KS_EXPECTED_PRESENT" "$EXP_KS_CT" + check "Knowledge sources indexed" "$KS_EXPECTED_INDEXED" "$EXP_KS_CT" + if [[ -n "$KS_EXPECTED_UNINDEXED" ]]; then + RESULTS="${RESULTS}\n ⚠️ Unindexed knowledge sources|${KS_EXPECTED_UNINDEXED}|—|❌ FAIL" + FAIL=$((FAIL + 1)) + fi +fi +RESULTS="${RESULTS}\n Knowledge source names|${KS_NAMES}|—|" + +# ── Response Plans (Incident Filters) ── +FILTERS=$(dp_get "/api/v1/incidentPlayground/filters") +FILTER_CT=$(echo "$FILTERS" | jq 'if type == "array" then length else 0 end' 2>/dev/null || echo 0) +FILTER_NAMES=$(echo "$FILTERS" | jq -r '.[].id' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') +EXP_FILTER_CT=$(exp '.responsePlans | length' "-") +EXP_FILTER_NAMES=$(exp_list '.responsePlans[].name') +check "Response Plans" "$FILTER_CT" "$EXP_FILTER_CT" +[[ -n "$EXP_FILTER_NAMES" ]] && check "Filter names" "$FILTER_NAMES" "$EXP_FILTER_NAMES" || RESULTS="${RESULTS}\n Filter names|${FILTER_NAMES}|—|" + +# ── GitHub ── +GH_STATUS=$(dp_get "/api/v1/Github/auth/status") +GH_CONFIGURED=$(echo "$GH_STATUS" | jq -r '.isConfigured // .hosts[0].isConfigured // false' 2>/dev/null) +check "GitHub OAuth" "$GH_CONFIGURED" "-" + +# ── Repos ── +REPOS=$(dp_get "/api/v2/repos") +REPO_CT=$(echo "$REPOS" | jq '.value // . | if type == "array" then length else 0 end' 2>/dev/null || echo 0) +REPO_NAMES=$(echo "$REPOS" | jq -r '(.value // .)[].name' 2>/dev/null | sort | tr '\n' ', ' | sed 's/,$//') +EXP_REPO_CT=$(exp '.repos | length' "-") +EXP_REPO_NAMES=$(exp_list '.repos') +check "Repos" "$REPO_CT" "$EXP_REPO_CT" +[[ -n "$EXP_REPO_NAMES" ]] && check "Repo names" "$REPO_NAMES" "$EXP_REPO_NAMES" || RESULTS="${RESULTS}\n Repo names|${REPO_NAMES}|—|" + +# ── Print results ── +echo "" +printf " %-25s %-10s %-10s %s\n" "Check" "Actual" "Expected" "Result" +printf " %-25s %-10s %-10s %s\n" "─────────────────────────" "──────────" "──────────" "──────" +echo -e "$RESULTS" | while IFS='|' read -r name actual expected result; do + [[ -z "$name" ]] && continue + printf " %-25s %-10s %-10s %s\n" "$name" "$actual" "$expected" "$result" +done + +echo "" +echo "═══════════════════════════════════════════════════" +echo " Results: ${PASS} passed, ${FAIL} failed" +echo " Portal: https://sre.azure.com/#/agent/${SUB}/${RG}/${AGENT}" +echo "═══════════════════════════════════════════════════" +echo "" + +[[ "$FAIL" -gt 0 ]] && exit 1 +exit 0 diff --git a/src/templates/sre-agent/createUiDefinition.json b/src/templates/sre-agent/createUiDefinition.json new file mode 100644 index 000000000..3b17beb78 --- /dev/null +++ b/src/templates/sre-agent/createUiDefinition.json @@ -0,0 +1,169 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "basics": { + "description": "Deploy Azure SRE Agent with the FinOps toolkit recipe for FinOps hub analysis, capacity monitoring, scheduled tasks, and specialist agents.", + "location": { + "label": "Location", + "resourceTypes": [ + "Microsoft.App/agents", + "Microsoft.Insights/components", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.OperationalInsights/workspaces", + "Microsoft.Resources/deploymentScripts" + ] + } + } + }, + "resourceTypes": [ + "Microsoft.App/agents", + "Microsoft.Insights/components", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.OperationalInsights/workspaces", + "Microsoft.Resources/deploymentScripts" + ], + "basics": [ + { + "name": "resourceGroupName", + "type": "Microsoft.Common.TextBox", + "label": "Agent resource group", + "defaultValue": "finops-hub-sre", + "toolTip": "Resource group that will contain the Azure SRE Agent, monitoring resources, and deployment script identity.", + "constraints": { + "required": true, + "regex": "^[a-zA-Z0-9._()\\-]{1,90}$", + "validationMessage": "Enter a valid Azure resource group name." + }, + "visible": true + }, + { + "name": "agentName", + "type": "Microsoft.Common.TextBox", + "label": "Agent name", + "defaultValue": "finops-hub-sre", + "toolTip": "Name of the Azure SRE Agent resource.", + "constraints": { + "required": true, + "regex": "^[a-zA-Z0-9][a-zA-Z0-9\\-]{1,61}[a-zA-Z0-9]$", + "validationMessage": "Name must be 3-63 characters and can contain letters, numbers, and hyphens. The first and last characters must be alphanumeric." + }, + "visible": true + } + ], + "steps": [ + { + "name": "configuration", + "label": "Configuration", + "elements": [ + { + "name": "finopsHubKustoConnectorUri", + "type": "Microsoft.Common.TextBox", + "label": "FinOps hub Kusto URI", + "defaultValue": "", + "toolTip": "Optional database-qualified Azure Data Explorer URI for the FinOps hub. Example: https://cluster.region.kusto.windows.net/Hub", + "constraints": { + "required": false, + "regex": "^$|^https://.+\\.kusto\\.windows\\.net/.+", + "validationMessage": "Enter a database-qualified Kusto URI such as https://cluster.region.kusto.windows.net/Hub, or leave blank." + }, + "visible": true + }, + { + "name": "finopsHubKustoClusterResourceId", + "type": "Microsoft.Common.TextBox", + "label": "FinOps hub Kusto cluster resource ID", + "defaultValue": "", + "toolTip": "Optional Azure Data Explorer cluster resource ID. Provide this to assign the agent managed identity AllDatabasesViewer on the cluster.", + "constraints": { + "required": false, + "regex": "^$|^/subscriptions/.+/resourceGroups/.+/providers/Microsoft\\.Kusto/clusters/.+", + "validationMessage": "Enter a valid Microsoft.Kusto/clusters resource ID, or leave blank." + }, + "visible": true + }, + { + "name": "targetResourceGroups", + "type": "Microsoft.Common.TextBox", + "label": "Additional target resource groups", + "defaultValue": "", + "toolTip": "Optional comma-separated resource group names the agent can observe or act on. The agent resource group is always included.", + "constraints": { + "required": false, + "regex": "^$|^[A-Za-z0-9_.()\\-]+(\\s*,\\s*[A-Za-z0-9_.()\\-]+)*$", + "validationMessage": "Enter a comma-separated list of resource group names without blank entries or trailing commas." + }, + "visible": true + }, + { + "name": "accessLevel", + "type": "Microsoft.Common.OptionsGroup", + "label": "Access level", + "defaultValue": "Low", + "toolTip": "Low grants read-only monitoring roles (recommended for reporting and analysis). High adds Contributor on target resource groups for autonomous remediation workflows.", + "constraints": { + "allowedValues": [ + { + "label": "High", + "value": "High" + }, + { + "label": "Low", + "value": "Low" + } + ], + "required": true + }, + "visible": true + }, + { + "name": "actionMode", + "type": "Microsoft.Common.OptionsGroup", + "label": "Action mode", + "defaultValue": "review", + "toolTip": "Review requires approval before write actions. Autonomous lets the agent run enabled actions within its assigned access.", + "constraints": { + "allowedValues": [ + { + "label": "Autonomous", + "value": "autonomous" + }, + { + "label": "Review", + "value": "review" + }, + { + "label": "Read only", + "value": "readOnly" + } + ], + "required": true + }, + "visible": true + }, + { + "name": "enableSubscriptionReaderRole", + "type": "Microsoft.Common.CheckBox", + "label": "Grant subscription Reader to the agent", + "toolTip": "Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.", + "defaultValue": false, + "visible": true + } + ] + } + ], + "outputs": { + "resourceGroupName": "[basics('resourceGroupName')]", + "agentName": "[basics('agentName')]", + "location": "[location()]", + "targetResourceGroupNames": "[steps('configuration').targetResourceGroups]", + "finopsHubKustoConnectorUri": "[steps('configuration').finopsHubKustoConnectorUri]", + "finopsHubKustoClusterResourceId": "[steps('configuration').finopsHubKustoClusterResourceId]", + "accessLevel": "[steps('configuration').accessLevel]", + "actionMode": "[steps('configuration').actionMode]", + "enableSubscriptionReaderRole": "[steps('configuration').enableSubscriptionReaderRole]" + } + } +} diff --git a/src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml b/src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml new file mode 100644 index 000000000..a13358445 --- /dev/null +++ b/src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml @@ -0,0 +1,33 @@ +name: Deploy FinOps SRE Agent + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Deploy FinOps SRE Agent + working-directory: src/templates/sre-agent + env: + CLUSTER_URI: ${{ secrets.SRE_AGENT_CLUSTER_URI }} + run: > + bash bin/deploy.sh + --recipe recipes/finops-hub + --resource-group ${{ vars.SRE_AGENT_RESOURCE_GROUP }} + --name ${{ vars.SRE_AGENT_NAME }} + --location ${{ vars.SRE_AGENT_LOCATION }} + --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + --cluster-uri "$CLUSTER_URI" + --deployer-principal-type ServicePrincipal + --force diff --git a/src/templates/sre-agent/infra/main.bicep b/src/templates/sre-agent/infra/main.bicep new file mode 100644 index 000000000..67198548c --- /dev/null +++ b/src/templates/sre-agent/infra/main.bicep @@ -0,0 +1,137 @@ +// Copied from microsoft/sre-agent labs/starter-lab/infra/main.bicep and +// updated for the FinOps Toolkit SRE Agent template: +// - no azd environment dependency +// - no Grubify sample application +// - resource-group scoped target access + +targetScope = 'subscription' + +@description('Resource group that holds the SRE Agent resources.') +param resourceGroupName string + +@description('SRE Agent name.') +param agentName string + +@description('Primary location for all resources.') +@allowed(['australiaeast', 'canadacentral', 'eastus2', 'francecentral', 'koreacentral', 'swedencentral', 'uksouth']) +param location string = 'eastus2' + +@description('Resource groups the agent can observe or act on.') +param targetResourceGroups array = [] + +@description('Agent access level.') +@allowed(['Low', 'High']) +param accessLevel string = 'Low' + +@description('Agent action mode.') +@allowed(['review', 'autonomous', 'readOnly']) +param actionMode string = 'autonomous' + +@description('Agent upgrade channel.') +@allowed(['Stable', 'Preview']) +param upgradeChannel string = 'Preview' + +@description('Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal.') +@allowed(['MicrosoftFoundry', 'Anthropic']) +param defaultModelProvider string = 'MicrosoftFoundry' + +@description('Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider.') +param defaultModelName string = 'Automatic' + +@description('Monthly agent unit limit.') +@minValue(1) +param monthlyAgentUnitLimit int = 10000 + +@description('Agent experimental settings.') +param experimentalSettings object = { + EnableSandboxGroup: true + EnableWorkspaceTools: true +} + +@description('Azure resource tags.') +param tags object = {} + +@description('Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment.') +param finopsHubKustoClusterResourceId string = '' + +@description('Assign Reader on the deployment subscription to the agent managed identity. Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.') +param enableSubscriptionReaderRole bool = false + +@description('Principal type of the deployer.') +@allowed(['User', 'ServicePrincipal']) +param deployerPrincipalType string = 'User' + +var targetRgs = union([resourceGroupName], targetResourceGroups) +var agentResourceGroupId = subscriptionResourceId('Microsoft.Resources/resourceGroups', resourceGroupName) +var targetRgIds = [for rgName in targetRgs: subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)] +var namingSeed = toLower('${subscription().subscriptionId}|${agentResourceGroupId}|${agentName}') +var readerRoleId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7' + +resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = { + name: resourceGroupName + location: location + tags: tags +} + +module resources 'resources.bicep' = { + name: 'resources-deployment' + scope: rg + params: { + agentName: agentName + location: location + namingSeed: namingSeed + targetResourceGroupIds: targetRgIds + accessLevel: accessLevel + actionMode: actionMode + upgradeChannel: upgradeChannel + defaultModelProvider: defaultModelProvider + defaultModelName: defaultModelName + monthlyAgentUnitLimit: monthlyAgentUnitLimit + experimentalSettings: experimentalSettings + deployerPrincipalType: deployerPrincipalType + tags: tags + } +} + +module targetRbac 'modules/resource-group-rbac.bicep' = [for rgName in targetRgs: { + name: 'target-rbac-${uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)), namingSeed)}' + scope: resourceGroup(rgName) + params: { + principalId: resources.outputs.agentPrincipalId + accessLevel: accessLevel + } +}] + +resource subscriptionReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableSubscriptionReaderRole) { + name: guid(subscription().id, namingSeed, readerRoleId) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', readerRoleId) + principalId: resources.outputs.agentPrincipalId + principalType: 'ServicePrincipal' + } +} + +var hasKustoCluster = !empty(finopsHubKustoClusterResourceId) +var kustoClusterSubscriptionId = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[2] : '' +var kustoClusterResourceGroupName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[4] : '' +var kustoClusterName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[8] : '' + +module finopsHubKustoAllDatabasesViewerRbac 'modules/kusto-all-databases-viewer-rbac.bicep' = if (hasKustoCluster) { + name: 'kusto-rbac-${uniqueString(finopsHubKustoClusterResourceId, namingSeed)}' + scope: resourceGroup(kustoClusterSubscriptionId, kustoClusterResourceGroupName) + params: { + clusterName: kustoClusterName + principalApplicationId: resources.outputs.agentPrincipalId + principalTenantId: tenant().tenantId + principalAssignmentName: 'sre-agent-${uniqueString(finopsHubKustoClusterResourceId, namingSeed, 'all-db-viewer')}' + } +} + +output AZURE_RESOURCE_GROUP string = rg.name +output AZURE_LOCATION string = location +output SRE_AGENT_NAME string = resources.outputs.agentName +output SRE_AGENT_ENDPOINT string = resources.outputs.agentEndpoint +output AGENT_PORTAL_URL string = resources.outputs.agentPortalUrl +output SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID string = resources.outputs.agentPrincipalId +output SYSTEM_MANAGED_IDENTITY_TENANT_ID string = resources.outputs.agentTenantId +output LOG_ANALYTICS_WORKSPACE_ID string = resources.outputs.logAnalyticsWorkspaceId diff --git a/src/templates/sre-agent/infra/modules/apply-extras.bicep b/src/templates/sre-agent/infra/modules/apply-extras.bicep new file mode 100644 index 000000000..4cc76f86f --- /dev/null +++ b/src/templates/sre-agent/infra/modules/apply-extras.bicep @@ -0,0 +1,110 @@ +targetScope = 'resourceGroup' + +@description('Location for the deployment script resources.') +param location string + +@description('SRE Agent name.') +param agentName string + +@description('SRE Agent data-plane endpoint.') +param agentEndpoint string + +@description('Subscription that contains the SRE Agent.') +param subscriptionId string + +@description('Public URI for the generated SRE Agent recipe package.') +param recipePackageUri string + +@description('SHA256 hash of the recipe package for integrity verification.') +param recipePackageSha256 string + +@description('Optional database-qualified Kusto connector URI.') +param kustoConnectorUri string = '' + +@description('Forces the deployment script to run when the template is redeployed.') +param forceUpdateTag string + +@description('Azure resource tags.') +param tags object = {} + +var identityName = 'id-sre-apply-${uniqueString(resourceGroup().id, agentName)}' +var scriptName = 'apply-sre-${uniqueString(resourceGroup().id, agentName)}' +var sreAgentAdminRoleId = 'e79298df-d852-4c6d-84f9-5d13249d1e55' + +#disable-next-line BCP081 +resource sreAgent 'Microsoft.App/agents@2026-01-01' existing = { + name: agentName +} + +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location + tags: tags +} + +resource sreAgentAdminRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(sreAgent.id, identity.id, sreAgentAdminRoleId) + scope: sreAgent + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', sreAgentAdminRoleId) + principalId: identity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +resource applyExtras 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + name: scriptName + location: location + tags: tags + kind: 'AzurePowerShell' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } + properties: { + azPowerShellVersion: '11.0' + retentionInterval: 'PT1H' + cleanupPreference: 'OnSuccess' + timeout: 'PT2H' + forceUpdateTag: forceUpdateTag + scriptContent: loadTextContent('../scripts/Apply-SreAgentExtras.ps1') + environmentVariables: [ + { + name: 'subscriptionId' + value: subscriptionId + } + { + name: 'resourceGroupName' + value: resourceGroup().name + } + { + name: 'agentName' + value: agentName + } + { + name: 'agentEndpoint' + value: agentEndpoint + } + { + name: 'recipePackageUri' + value: recipePackageUri + } + { + name: 'recipePackageSha256' + value: recipePackageSha256 + } + { + name: 'kustoConnectorUri' + value: kustoConnectorUri + } + ] + } + dependsOn: [ + sreAgentAdminRoleAssignment + ] +} + +output identityName string = identity.name +output scriptName string = applyExtras.name diff --git a/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep b/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep new file mode 100644 index 000000000..61baa4c44 --- /dev/null +++ b/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep @@ -0,0 +1,26 @@ +@description('Kusto cluster name.') +param clusterName string + +@description('Microsoft Entra application/client ID to assign to the Kusto cluster.') +param principalApplicationId string + +@description('Principal tenant ID.') +param principalTenantId string + +@description('Stable principal assignment name.') +param principalAssignmentName string + +resource cluster 'Microsoft.Kusto/clusters@2024-04-13' existing = { + name: clusterName +} + +resource allDatabasesViewer 'Microsoft.Kusto/clusters/principalAssignments@2024-04-13' = { + parent: cluster + name: principalAssignmentName + properties: { + principalId: principalApplicationId + principalType: 'App' + role: 'AllDatabasesViewer' + tenantId: principalTenantId + } +} diff --git a/src/templates/sre-agent/infra/modules/monitoring.bicep b/src/templates/sre-agent/infra/modules/monitoring.bicep new file mode 100644 index 000000000..6a0102df1 --- /dev/null +++ b/src/templates/sre-agent/infra/modules/monitoring.bicep @@ -0,0 +1,39 @@ +@description('Location for resources') +param location string + +@description('Log Analytics Workspace name') +param logAnalyticsName string + +@description('Application Insights name') +param appInsightsName string + +// Log Analytics Workspace +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +// Application Insights (linked to Log Analytics) +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + Request_Source: 'IbizaAIExtension' + WorkspaceResourceId: logAnalyticsWorkspace.id + } +} + +// Outputs +output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.id +output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.name +output appInsightsId string = applicationInsights.id +output appInsightsAppId string = applicationInsights.properties.AppId +output appInsightsConnectionString string = applicationInsights.properties.ConnectionString diff --git a/src/templates/sre-agent/infra/modules/resource-group-rbac.bicep b/src/templates/sre-agent/infra/modules/resource-group-rbac.bicep new file mode 100644 index 000000000..15ede6c34 --- /dev/null +++ b/src/templates/sre-agent/infra/modules/resource-group-rbac.bicep @@ -0,0 +1,33 @@ +@description('Principal ID of the managed identity to assign roles to.') +param principalId string + +@description('Agent access level.') +@allowed(['Low', 'High']) +param accessLevel string + +var readerRoleId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7' +var monitoringReaderRoleId = '43d0d8ad-25c7-4714-9337-8ba259a9fe05' +var logAnalyticsReaderRoleId = '73c42c96-874c-492b-b04d-ab87d138a893' +var contributorRoleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c' + +var roleIds = accessLevel == 'High' + ? [ + readerRoleId + monitoringReaderRoleId + logAnalyticsReaderRoleId + contributorRoleId + ] + : [ + readerRoleId + monitoringReaderRoleId + logAnalyticsReaderRoleId + ] + +resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for roleId in roleIds: { + name: guid(resourceGroup().id, principalId, roleId) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId) + principalId: principalId + principalType: 'ServicePrincipal' + } +}] diff --git a/src/templates/sre-agent/infra/modules/sre-agent.bicep b/src/templates/sre-agent/infra/modules/sre-agent.bicep new file mode 100644 index 000000000..ded39f596 --- /dev/null +++ b/src/templates/sre-agent/infra/modules/sre-agent.bicep @@ -0,0 +1,107 @@ +// Copied from microsoft/sre-agent labs/starter-lab/infra/modules/sre-agent.bicep +// and updated for the FinOps Toolkit SRE Agent template. + +@description('Location for resources.') +param location string + +@description('SRE Agent name.') +param agentName string + +@description('Application Insights App ID.') +param appInsightsAppId string + +@description('Application Insights connection string.') +@secure() +param appInsightsConnectionString string + +@description('Application Insights resource ID.') +param appInsightsId string + +@description('Resource group IDs to add as managed resources.') +param managedResourceGroupIds array + +@description('Agent access level.') +param accessLevel string + +@description('Agent action mode.') +param actionMode string + +@description('Agent upgrade channel.') +param upgradeChannel string + +@description('Default SRE Agent model provider.') +param defaultModelProvider string + +@description('Default SRE Agent model name.') +param defaultModelName string + +@description('Monthly agent unit limit.') +param monthlyAgentUnitLimit int + +@description('Agent experimental settings.') +param experimentalSettings object + +@description('Azure resource tags.') +param tags object = {} + +@description('Principal type of the deployer.') +@allowed(['User', 'ServicePrincipal']) +param deployerPrincipalType string = 'User' + +var sreAgentAdminRoleId = 'e79298df-d852-4c6d-84f9-5d13249d1e55' + +#disable-next-line BCP081 +resource sreAgent 'Microsoft.App/agents@2026-01-01' = { + name: agentName + location: location + tags: union(tags, { + 'hidden-link: /app-insights-resource-id': appInsightsId + source: 'microsoft-sre-agent-starter-lab' + 'finops-toolkit': 'sre-agent' + }) + identity: { + type: 'SystemAssigned' + } + properties: { + knowledgeGraphConfiguration: { + managedResources: managedResourceGroupIds + identity: 'system' + } + actionConfiguration: { + mode: actionMode + identity: 'system' + accessLevel: accessLevel + } + upgradeChannel: upgradeChannel + defaultModel: { + provider: defaultModelProvider + name: defaultModelName + } + monthlyAgentUnitLimit: monthlyAgentUnitLimit + experimentalSettings: experimentalSettings + mcpServers: [] + logConfiguration: { + applicationInsightsConfiguration: { + appId: appInsightsAppId + connectionString: appInsightsConnectionString + } + } + } +} + +resource sreAgentAdminRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(sreAgent.id, deployer().objectId, sreAgentAdminRoleId) + scope: sreAgent + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', sreAgentAdminRoleId) + principalId: deployer().objectId + principalType: deployerPrincipalType + } +} + +output agentName string = sreAgent.name +output agentId string = sreAgent.id +output agentEndpoint string = sreAgent.properties.agentEndpoint +output agentPortalUrl string = 'https://sre.azure.com/#/agent/${subscription().subscriptionId}/${resourceGroup().name}/${sreAgent.name}' +output agentPrincipalId string = sreAgent.identity.principalId +output agentTenantId string = sreAgent.identity.tenantId diff --git a/src/templates/sre-agent/infra/resources.bicep b/src/templates/sre-agent/infra/resources.bicep new file mode 100644 index 000000000..3107d9dee --- /dev/null +++ b/src/templates/sre-agent/infra/resources.bicep @@ -0,0 +1,83 @@ +// Copied from microsoft/sre-agent labs/starter-lab/infra/resources.bicep and +// updated for the FinOps Toolkit SRE Agent template. + +@description('SRE Agent name.') +param agentName string + +@description('Location for all resources.') +param location string + +@description('Deterministic naming seed built from subscription ID, agent resource group ID, and agent name.') +param namingSeed string + +@description('Resource group IDs shown as managed resources in the SRE Agent.') +param targetResourceGroupIds array + +@description('Agent access level.') +param accessLevel string + +@description('Agent action mode.') +param actionMode string + +@description('Agent upgrade channel.') +param upgradeChannel string + +@description('Default SRE Agent model provider.') +param defaultModelProvider string + +@description('Default SRE Agent model name.') +param defaultModelName string + +@description('Monthly agent unit limit.') +param monthlyAgentUnitLimit int + +@description('Agent experimental settings.') +param experimentalSettings object + +@description('Principal type of the deployer.') +@allowed(['User', 'ServicePrincipal']) +param deployerPrincipalType string = 'User' + +@description('Azure resource tags.') +param tags object = {} + +var uniqueSuffix = uniqueString(namingSeed) +var logAnalyticsName = 'law-${uniqueSuffix}' +var appInsightsName = 'appi-${uniqueSuffix}' + +module monitoring 'modules/monitoring.bicep' = { + name: 'monitoring' + params: { + location: location + logAnalyticsName: logAnalyticsName + appInsightsName: appInsightsName + } +} + +module sreAgent 'modules/sre-agent.bicep' = { + name: 'sre-agent' + params: { + location: location + agentName: agentName + appInsightsAppId: monitoring.outputs.appInsightsAppId + appInsightsConnectionString: monitoring.outputs.appInsightsConnectionString + appInsightsId: monitoring.outputs.appInsightsId + managedResourceGroupIds: targetResourceGroupIds + accessLevel: accessLevel + actionMode: actionMode + upgradeChannel: upgradeChannel + defaultModelProvider: defaultModelProvider + defaultModelName: defaultModelName + monthlyAgentUnitLimit: monthlyAgentUnitLimit + experimentalSettings: experimentalSettings + deployerPrincipalType: deployerPrincipalType + tags: tags + } +} + +output agentName string = sreAgent.outputs.agentName +output agentEndpoint string = sreAgent.outputs.agentEndpoint +output agentPortalUrl string = sreAgent.outputs.agentPortalUrl +output agentPrincipalId string = sreAgent.outputs.agentPrincipalId +output agentTenantId string = sreAgent.outputs.agentTenantId +output logAnalyticsWorkspaceId string = monitoring.outputs.logAnalyticsWorkspaceId diff --git a/src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1 b/src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1 new file mode 100644 index 000000000..85f6e9daf --- /dev/null +++ b/src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1 @@ -0,0 +1,330 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ErrorActionPreference = 'Stop' + +function Get-RequiredEnv($Name) { + $value = [Environment]::GetEnvironmentVariable($Name) + if ([string]::IsNullOrWhiteSpace($value)) { + throw "Missing required environment variable: $Name" + } + return $value +} + +function Get-OptionalEnv($Name) { + return [Environment]::GetEnvironmentVariable($Name) +} + +function Get-TempRoot() { + $tempRoot = [System.IO.Path]::GetTempPath() + if ([string]::IsNullOrWhiteSpace($tempRoot)) { + $tempRoot = Get-OptionalEnv 'AZ_SCRIPTS_PATH_OUTPUT_DIRECTORY' + } + if ([string]::IsNullOrWhiteSpace($tempRoot)) { + $tempRoot = (Get-Location).Path + } + return $tempRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) +} + +function ConvertTo-BodyJson($Value) { + return ($Value | ConvertTo-Json -Depth 100 -Compress) +} + +function Get-PropertyValue($Object, [string[]]$Names, $Default = '') { + foreach ($name in $Names) { + if ($null -ne $Object -and $Object.PSObject.Properties[$name] -and $null -ne $Object.$name) { + return $Object.$name + } + } + return $Default +} + +function Get-HttpStatusCode($Exception) { + if ($Exception.Response -and $Exception.Response.StatusCode) { + return [int]$Exception.Response.StatusCode + } + return $null +} + +function Invoke-WithRetry([scriptblock]$Action, [string]$Label, [int]$MaxAttempts = 5, [int]$DelaySeconds = 15) { + for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { + try { + return & $Action + } + catch { + $statusCode = Get-HttpStatusCode $_.Exception + if ($statusCode -and $statusCode -notin @(401, 403, 408, 429, 500, 502, 503, 504)) { + throw + } + if ($attempt -eq $MaxAttempts) { + throw + } + Write-Output "$Label attempt $attempt/$MaxAttempts failed: $($_.Exception.Message)" + Start-Sleep -Seconds $DelaySeconds + } + } +} + +function Invoke-JsonRest([string]$Method, [string]$Uri, [string]$Token, $Body = $null) { + $headers = @{ + Authorization = "Bearer $Token" + 'Content-Type' = 'application/json' + } + $parameters = @{ + Method = $Method + Uri = $Uri + Headers = $headers + } + if ($null -ne $Body) { + $parameters.Body = ConvertTo-BodyJson $Body + } + return Invoke-RestMethod @parameters +} + +function Get-KnowledgeSourceName([string]$FileName) { + $name = [IO.Path]::GetFileName($FileName).ToLowerInvariant() -replace '[^a-z0-9-]+', '-' + $name = $name -replace '-+', '-' + return $name.Trim('-') +} + +function Get-Collection($Value) { + if ($null -eq $Value) { + return @() + } + return @($Value) +} + +function Convert-TokenToString($Token) { + if ($Token -is [Security.SecureString]) { + $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token) + try { + return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) + } + finally { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + } + } + return [string]$Token +} + +function Get-ArmAccessTokenString() { + return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://management.azure.com/').Token +} + +function Get-SreAccessTokenString() { + return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://azuresre.dev').Token +} + +$subscriptionId = Get-RequiredEnv 'subscriptionId' +$resourceGroupName = Get-RequiredEnv 'resourceGroupName' +$agentName = Get-RequiredEnv 'agentName' +$agentEndpoint = (Get-RequiredEnv 'agentEndpoint').TrimEnd('/') +$recipePackageUri = Get-RequiredEnv 'recipePackageUri' +$recipePackageSha256 = Get-RequiredEnv 'recipePackageSha256' +$kustoConnectorUri = Get-OptionalEnv 'kustoConnectorUri' +$armApiVersion = '2025-05-01-preview' +$armBase = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.App/agents/$agentName" + +Write-Output 'Connecting with deployment script managed identity...' +Connect-AzAccount -Identity | Out-Null +Set-AzContext -Subscription $subscriptionId | Out-Null + +$tempRoot = Get-TempRoot +$workRoot = Join-Path $tempRoot 'sre-agent-recipe' +$zipPath = Join-Path $tempRoot 'sre-agent-recipe.zip' +Remove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue +New-Item -Path $workRoot -ItemType Directory -Force | Out-Null + +Write-Output "Validating SRE Agent recipe package URI: $recipePackageUri" +[uri]$parsedUri = $recipePackageUri +if ($parsedUri.Scheme -ne 'https') { + throw "Recipe package URI must use https scheme, got: $($parsedUri.Scheme)" +} + +$allowedHosts = @( + 'raw.githubusercontent.com', + 'github.com', + 'aka.ms' +) + +if ($allowedHosts -notcontains $parsedUri.Host -and $parsedUri.Host -notlike '*.blob.core.windows.net') { + throw "Recipe package host '$($parsedUri.Host)' is not in the allowlist. Allowed: raw.githubusercontent.com, github.com, aka.ms, *.blob.core.windows.net" +} + +Write-Output "Downloading SRE Agent recipe package: $recipePackageUri" +Invoke-WithRetry -Label 'download recipe package' -Action { + Invoke-WebRequest -Uri $recipePackageUri -OutFile $zipPath +} | Out-Null + +Write-Output "Verifying recipe package integrity..." +$actualHash = (Get-FileHash -Algorithm SHA256 -Path $zipPath).Hash +if ($actualHash -ne $recipePackageSha256) { + throw "Recipe package hash mismatch: expected $recipePackageSha256 got $actualHash" +} + +Expand-Archive -Path $zipPath -DestinationPath $workRoot -Force + +$extrasPath = Join-Path $workRoot 'extras.json' +if (-not (Test-Path $extrasPath)) { + throw "Recipe package did not contain extras.json" +} + +$extras = Get-Content $extrasPath -Raw | ConvertFrom-Json -Depth 100 +Write-Output "Loaded recipe package from $extrasPath" + +if ([string]::IsNullOrWhiteSpace($kustoConnectorUri)) { + $extras.connectors = @( + Get-Collection $extras.connectors | Where-Object { + $type = Get-PropertyValue $_.properties @('dataConnectorType', 'type') + $type -ine 'Kusto' + } + ) +} +else { + foreach ($connector in Get-Collection $extras.connectors) { + $type = Get-PropertyValue $connector.properties @('dataConnectorType', 'type') + if ($type -ieq 'Kusto') { + $connector.properties.dataSource = $kustoConnectorUri + if (-not $connector.properties.PSObject.Properties['identity']) { + $connector.properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system' + } + } + } +} + +function Invoke-ArmPutConnector([string]$Name, $Body) { + $uri = "$armBase/connectors/$Name`?api-version=$armApiVersion" + Invoke-WithRetry -Label "ARM PUT connector $Name" -Action { + Invoke-JsonRest -Method PUT -Uri $uri -Token (Get-ArmAccessTokenString) -Body $Body + } | Out-Null + Write-Output " ARM PUT connectors/${Name}: ok" +} + +function Invoke-SreApi([string]$Method, [string]$Path, $Body = $null) { + $uri = "$agentEndpoint$Path" + return Invoke-WithRetry -Label "$Method $Path" -MaxAttempts 21 -DelaySeconds 30 -Action { + Invoke-JsonRest -Method $Method -Uri $uri -Token (Get-SreAccessTokenString) -Body $Body + } +} + +function Invoke-ExtendedPut([string]$Kind, [string]$Name, [string]$Type, $Properties) { + $encodedName = [Uri]::EscapeDataString($Name) + $body = @{ + name = $Name + type = $Type + tags = @() + properties = $Properties + } + Invoke-SreApi -Method PUT -Path "/api/v2/extendedAgent/$Kind/$encodedName" -Body $body | Out-Null + Write-Output " PUT $Kind/${Name}: ok" +} + +Write-Output 'Step 1/7: Applying connectors...' +foreach ($connector in Get-Collection $extras.connectors) { + $name = [string]$connector.name + $properties = $connector.properties + if (-not $properties.PSObject.Properties['identity']) { + $properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system' + } + Invoke-ArmPutConnector -Name $name -Body @{ properties = $properties } +} + +Write-Output 'Step 2/7: Configuring built-in tools...' +$overrides = Get-Collection $extras.builtInTools.overrides +if ($overrides.Count -gt 0) { + $body = @{ + overrides = @($overrides | ForEach-Object { + @{ + name = $_.name + enabled = [bool]$_.enabled + } + }) + } + Invoke-SreApi -Method POST -Path '/api/v2/agent/tools/configure' -Body $body | Out-Null + Write-Output " built-in tools configured: $($overrides.Count) overrides" +} + +Write-Output 'Step 3/7: Uploading knowledge sources...' +foreach ($item in Get-Collection $extras.knowledgeItems) { + $sourceName = Get-KnowledgeSourceName ([string]$item.name) + $bytes = [Text.Encoding]::UTF8.GetBytes([string]$item.content) + $body = @{ + properties = @{ + dataConnectorType = 'KnowledgeFile' + dataSource = $sourceName + extendedProperties = @{ + displayName = [string]$item.name + fileName = [string]$item.name + fileContent = [Convert]::ToBase64String($bytes) + contentType = [string](Get-PropertyValue $item @('contentType') 'application/octet-stream') + } + } + } + Invoke-ArmPutConnector -Name $sourceName -Body $body + Start-Sleep -Seconds 5 +} + +Write-Output 'Step 4/7: Applying tools...' +foreach ($tool in Get-Collection $extras.tools) { + $name = [string]$tool.metadata.name + $properties = $tool.spec + if ($properties.type -eq 'PythonTool') { + $properties.type = 'PythonFunctionTool' + } + Invoke-ExtendedPut -Kind 'tools' -Name $name -Type 'ExtendedAgentTool' -Properties $properties +} + +Write-Output 'Step 5/7: Applying skills...' +foreach ($skill in Get-Collection $extras.skills) { + $name = [string]$skill.metadata.name + $tools = @() + if ($skill.metadata.spec -and $skill.metadata.spec.tools) { + $tools = Get-Collection $skill.metadata.spec.tools + } + $properties = @{ + name = $name + description = [string](Get-PropertyValue $skill.metadata @('description')) + tools = $tools + skillContent = [string](Get-PropertyValue $skill @('skillContent')) + additionalFiles = @() + } + Invoke-ExtendedPut -Kind 'skills' -Name $name -Type 'Skill' -Properties $properties +} + +Write-Output 'Step 6/7: Applying subagents...' +foreach ($subagent in Get-Collection $extras.subagents) { + $name = [string]$subagent.metadata.name + Invoke-ExtendedPut -Kind 'agents' -Name $name -Type 'ExtendedAgent' -Properties $subagent.spec +} + +Write-Output 'Step 7/7: Applying scheduled tasks...' +$scheduledTasks = Get-Collection $extras.scheduledTasks +if ($scheduledTasks.Count -gt 0) { + $existing = Invoke-SreApi -Method GET -Path '/api/v1/scheduledtasks' + foreach ($task in $scheduledTasks) { + $name = [string]$task.metadata.name + foreach ($match in (Get-Collection $existing | Where-Object { $_.name -eq $name })) { + if ($match.id) { + Invoke-SreApi -Method DELETE -Path "/api/v1/scheduledtasks/$($match.id)" | Out-Null + Write-Output " deleted existing scheduledtasks/$name" + } + } + } + + foreach ($task in $scheduledTasks) { + $name = [string]$task.metadata.name + $spec = $task.spec + $properties = @{ + name = [string](Get-PropertyValue $spec @('name') $name) + description = [string](Get-PropertyValue $spec @('description')) + cronExpression = [string](Get-PropertyValue $spec @('schedule', 'cronExpression', 'cron_expression')) + agentPrompt = [string](Get-PropertyValue $spec @('prompt', 'agentPrompt', 'agent_prompt')) + agentMode = [string](Get-PropertyValue $spec @('mode', 'agentMode', 'agent_mode') 'Review') + isEnabled = [bool](Get-PropertyValue $spec @('enabled') $true) + agent = [string](Get-PropertyValue $spec @('agent')) + } + Invoke-ExtendedPut -Kind 'scheduledtasks' -Name $name -Type 'ScheduledTask' -Properties $properties + } +} + +Write-Output 'SRE Agent extras complete.' diff --git a/src/templates/sre-agent/main.bicep b/src/templates/sre-agent/main.bicep new file mode 100644 index 000000000..3ca789d79 --- /dev/null +++ b/src/templates/sre-agent/main.bicep @@ -0,0 +1,173 @@ +// One-click portal entry point for the FinOps Toolkit SRE Agent. +// The CLI path keeps using infra/main.bicep directly. + +targetScope = 'subscription' + +@description('Resource group that holds the SRE Agent resources.') +param resourceGroupName string + +@description('SRE Agent name.') +param agentName string + +@description('Primary location for all resources.') +@allowed(['australiaeast', 'canadacentral', 'eastus2', 'francecentral', 'koreacentral', 'swedencentral', 'uksouth']) +param location string = 'eastus2' + +@description('Resource groups the agent can observe or act on. The agent resource group is always included.') +param targetResourceGroups array = [] + +@description('Comma-separated resource groups the agent can observe or act on. Used by the Azure portal form.') +param targetResourceGroupNames string = '' + +@description('Optional database-qualified FinOps Hub Kusto connector URI. Example: https://..kusto.windows.net/Hub') +param finopsHubKustoConnectorUri string = '' + +@description('Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment.') +param finopsHubKustoClusterResourceId string = '' + +@description('Agent access level. Low (read-only) is recommended for reporting and analysis without modification risk.') +@allowed(['Low', 'High']) +param accessLevel string = 'Low' + +@description('Agent action mode.') +@allowed(['review', 'autonomous', 'readOnly']) +param actionMode string = 'review' + +@description('Agent upgrade channel.') +@allowed(['Stable', 'Preview']) +param upgradeChannel string = 'Preview' + +@description('Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal.') +@allowed(['MicrosoftFoundry', 'Anthropic']) +param defaultModelProvider string = 'MicrosoftFoundry' + +@description('Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider.') +param defaultModelName string = 'Automatic' + +@description('Monthly agent unit limit.') +@minValue(1) +param monthlyAgentUnitLimit int = 10000 + +@description('Agent experimental settings.') +param experimentalSettings object = { + EnableSandboxGroup: true + EnableWorkspaceTools: true +} + +@description('Assign Reader on the deployment subscription to the agent managed identity. Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.') +param enableSubscriptionReaderRole bool = false + +@description('Public URI for the generated SRE Agent recipe package. Deploy-to-Azure links derive this from the template URI.') +param recipePackageUri string = uri(any(deployment()).properties.templateLink.uri, 'sre-agent-recipe.zip') + +@description('SHA256 hash of the recipe package for integrity verification.') +param recipePackageSha256 string = 'PLACEHOLDER_RECIPE_PACKAGE_SHA256' + +@description('Forces the recipe deployment script to run when the template is redeployed.') +param forceUpdateTag string = utcNow() + +@description('Azure resource tags.') +param tags object = { + 'finops-toolkit': 'sre-agent' + source: 'microsoft-finops-toolkit' +} + +@description('Principal type of the deployer.') +@allowed(['User', 'ServicePrincipal']) +param deployerPrincipalType string = 'User' + +var rawTargetResourceGroups = split(replace(targetResourceGroupNames, ' ', ''), ',') +var parsedTargetResourceGroups = filter(rawTargetResourceGroups, rgName => !empty(rgName)) +var targetRgs = union([resourceGroupName], targetResourceGroups, parsedTargetResourceGroups) +var agentResourceGroupId = subscriptionResourceId('Microsoft.Resources/resourceGroups', resourceGroupName) +var targetRgIds = [for rgName in targetRgs: subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)] +var namingSeed = toLower('${subscription().subscriptionId}|${agentResourceGroupId}|${agentName}') +var readerRoleId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7' +var hasKustoCluster = !empty(finopsHubKustoClusterResourceId) +var kustoClusterSubscriptionId = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[2] : '' +var kustoClusterResourceGroupName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[4] : '' +var kustoClusterName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[8] : '' + +resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = { + name: resourceGroupName + location: location + tags: tags +} + +module resources 'infra/resources.bicep' = { + name: 'resources-deployment' + scope: rg + params: { + agentName: agentName + location: location + namingSeed: namingSeed + targetResourceGroupIds: targetRgIds + accessLevel: accessLevel + actionMode: actionMode + upgradeChannel: upgradeChannel + defaultModelProvider: defaultModelProvider + defaultModelName: defaultModelName + monthlyAgentUnitLimit: monthlyAgentUnitLimit + experimentalSettings: experimentalSettings + deployerPrincipalType: deployerPrincipalType + tags: tags + } +} + +module targetRbac 'infra/modules/resource-group-rbac.bicep' = [for rgName in targetRgs: { + name: 'target-rbac-${uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)), namingSeed)}' + scope: resourceGroup(rgName) + params: { + principalId: resources.outputs.agentPrincipalId + accessLevel: accessLevel + } +}] + +resource subscriptionReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableSubscriptionReaderRole) { + name: guid(subscription().id, namingSeed, readerRoleId) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', readerRoleId) + principalId: resources.outputs.agentPrincipalId + principalType: 'ServicePrincipal' + } +} + +module finopsHubKustoAllDatabasesViewerRbac 'infra/modules/kusto-all-databases-viewer-rbac.bicep' = if (hasKustoCluster) { + name: 'kusto-rbac-${uniqueString(finopsHubKustoClusterResourceId, namingSeed)}' + scope: resourceGroup(kustoClusterSubscriptionId, kustoClusterResourceGroupName) + params: { + clusterName: kustoClusterName + principalApplicationId: resources.outputs.agentPrincipalId + principalTenantId: tenant().tenantId + principalAssignmentName: 'sre-agent-${uniqueString(finopsHubKustoClusterResourceId, namingSeed, 'all-db-viewer')}' + } +} + +module applyExtras 'infra/modules/apply-extras.bicep' = { + name: 'apply-extras' + scope: rg + params: { + location: location + agentName: agentName + agentEndpoint: resources.outputs.agentEndpoint + subscriptionId: subscription().subscriptionId + recipePackageUri: recipePackageUri + recipePackageSha256: recipePackageSha256 + kustoConnectorUri: finopsHubKustoConnectorUri + forceUpdateTag: forceUpdateTag + tags: tags + } + dependsOn: [ + targetRbac + ] +} + +output AZURE_RESOURCE_GROUP string = rg.name +output AZURE_LOCATION string = location +output SRE_AGENT_NAME string = resources.outputs.agentName +output SRE_AGENT_ENDPOINT string = resources.outputs.agentEndpoint +output AGENT_PORTAL_URL string = resources.outputs.agentPortalUrl +output SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID string = resources.outputs.agentPrincipalId +output SYSTEM_MANAGED_IDENTITY_TENANT_ID string = resources.outputs.agentTenantId +output LOG_ANALYTICS_WORKSPACE_ID string = resources.outputs.logAnalyticsWorkspaceId +output APPLY_EXTRAS_SCRIPT string = applyExtras.outputs.scriptName diff --git a/src/templates/sre-agent/package-manifest.json b/src/templates/sre-agent/package-manifest.json new file mode 100644 index 000000000..b3bde6c79 --- /dev/null +++ b/src/templates/sre-agent/package-manifest.json @@ -0,0 +1,21 @@ +{ + "deployment": { + "Files": [ + { + "sourceFolder": ".", + "source": "azuredeploy.json", + "destination": "azuredeploy.json" + }, + { + "sourceFolder": ".", + "source": "createUiDefinition.json", + "destination": "createUiDefinition.json" + }, + { + "sourceFolder": "assets", + "source": "sre-agent-recipe.zip", + "destination": "sre-agent-recipe.zip" + } + ] + } +} diff --git a/src/templates/sre-agent/recipes/finops-hub/README.md b/src/templates/sre-agent/recipes/finops-hub/README.md new file mode 100644 index 000000000..4af60119b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/README.md @@ -0,0 +1,33 @@ +# FinOps Hub SRE Agent recipe + +Deploys the FinOps Toolkit SRE Agent using the canonical Microsoft SRE Agent recipe layout. + +## Contents + +- 5 custom agents in `config/subagents/`: `finops-practitioner` plus four delegated subagents +- 3 tool-bearing delegated subagents: `azure-capacity-manager`, `ftk-database-query`, and `ftk-hubs-agent`; `chief-financial-officer` has no tools +- 3 skills in `config/skills/` +- 50 tools: 37 generated Kusto tools from `src/queries/catalog/` plus 13 Python tools in `config/tools/` +- 9 platform tool overrides in `config/built-in-tools.json` +- 19 scheduled tasks in `automations/scheduled-tasks/`, all owned by `finops-practitioner` +- 1 connector in `connectors.json`: FinOps Hub Kusto +- 6 uploaded KnowledgeFile sources: files in `knowledge/` plus `../../../claude-plugin/output-styles/ftk-output-style.md` + +## Framework mapping + +The authoritative FinOps Framework mapping for this recipe is [../../CATALOG.md](../../CATALOG.md). It maps the toolkit and SRE Agent to FinOps domains, capabilities, personas, principles, phases, agent ownership, tool ownership, scheduled-task ownership, and required handoffs. Keep this recipe aligned to that catalog when changing subagents, tools, skills, knowledge sources, or scheduled tasks. + +## Deploy + +```bash +bash ../../bin/deploy.sh \ + --recipe . \ + --subscription \ + --resource-group \ + --name \ + --location \ + --cluster-uri https://..kusto.windows.net/Hub \ + [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/] +``` + +The deployment uses the explicit subscription passed with `--subscription`. Real deployments resolve the Kusto cluster resource ID from `--cluster-uri` when possible. Pass `--cluster-resource-id` when the cluster cannot be resolved automatically or when running `--dry-run`. diff --git a/src/templates/sre-agent/recipes/finops-hub/agent.json b/src/templates/sre-agent/recipes/finops-hub/agent.json new file mode 100644 index 000000000..b2b21d260 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/agent.json @@ -0,0 +1,21 @@ +{ + "access": { + "accessLevel": "Low", + "actionMode": "autonomous" + }, + "upgradeChannel": "Preview", + "defaultModelProvider": "MicrosoftFoundry", + "defaultModelName": "Automatic", + "monthlyAgentUnitLimit": 10000, + "tags": { + "finops-toolkit": "sre-agent", + "source": "microsoft-finops-toolkit" + }, + "toggles": { + "enableWebhookBridge": false + }, + "experimentalSettings": { + "EnableSandboxGroup": true, + "EnableWorkspaceTools": true + } +} diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/advisor-suppression-review.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/advisor-suppression-review.yaml new file mode 100644 index 000000000..573266364 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/advisor-suppression-review.yaml @@ -0,0 +1,79 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: AdvisorSuppressionReview +spec: + name: AdvisorSuppressionReview + description: Monthly review of active Advisor recommendation suppressions for stale or expired decisions + cron_expression: 0 9 1 * * + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Load your relevant FinOps practitioner skill. Do not call Python, Kusto, Resource Graph, Azure CLI, visualization, Teams, or Outlook tools directly; delegate tool work to the specialist subagent named in the routing guard. + + + ## Specialist tools to request + + + Specialist tools use their configured authentication and the agent system-managed identity where required. + + + - `ftk-hubs-agent`: 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 + + + Ask `ftk-hubs-agent` to 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. Include at minimum a suppression status chart and a stale suppression age 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/ai-workload-cost-analysis.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/ai-workload-cost-analysis.yaml new file mode 100644 index 000000000..2d24fdb3d --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/ai-workload-cost-analysis.yaml @@ -0,0 +1,194 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: AIWorkloadCostAnalysis +spec: + name: AIWorkloadCostAnalysis + description: Monthly AI workload cost analysis — token economics, model efficiency, and cost allocation for Azure OpenAI + cron_expression: 0 10 1 * * + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# AI Workload Cost Analysis + + + ## Phase 0: Preparation + + + Before starting any analysis, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Load your finops-toolkit and azure-cost-management skills. Lead the analysis as the FinOps practitioner. Delegate all FinOps Hub Kusto evidence collection to `ftk-database-query`, delegate GPU and regional capacity checks to `azure-capacity-manager`, and consult `chief-financial-officer` for executive unit-economics and investment 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. + + ## Specialist evidence requests for this task + + Do not run Kusto tools directly. Ask `ftk-database-query` to run AI cost and token tools, and ask `azure-capacity-manager` to run GPU and regional capacity checks. Ask `ftk-hubs-agent` to run `resource-graph-query` only for Azure resource inventory and governance context. + + - `ftk-hubs-agent`: Run `resource-graph-query` with query and subscriptions to execute Azure Resource Graph KQL for AI resource discovery + - `azure-capacity-manager`: Run `vm-quota-usage` with subscription_id and location to get VM family quota usage with at-risk thresholds for GPU capacity + - `ftk-database-query`: Run `ai-token-usage-breakdown`, `ai-daily-trend`, and `ai-model-cost-comparison` as the FinOps KPI-backed token consumption evidence set + + + ## FOCUS Alignment + + + AI cost data uses these FOCUS columns: + + - ConsumedQuantity = token count + + - ConsumedUnit = tokens + + - x_SkuMeterCategory = Azure OpenAI + + - x_SkuMeterSubcategory = model version + direction (e.g., "gpt 4o 0513 Input global Tokens") + + - Unit economics = EffectiveCost / ConsumedQuantity + + + ## Phase 1: Token Economics + + + This phase covers the `Token Consumption Metrics` KPI in `src/queries/KPI.md`. Keep `ai-token-usage-breakdown`, `ai-daily-trend`, and `ai-model-cost-comparison` results together as the AI unit-economics KPI evidence set. + + + ### 1.1 Token Usage Breakdown + + Ask `ftk-database-query` to run `ai-token-usage-breakdown` with startDate=startofmonth(ago(30d)), endDate=startofmonth(now()). 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 + + Ask `ftk-database-query` to run `ai-model-cost-comparison` with the same date range. Compare cost per 1K tokens across model versions. 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 + + Ask `ftk-database-query` to run `ai-daily-trend` with startDate=ago(30d). 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 + + Ask `ftk-hubs-agent` to run `resource-graph-query` to discover Azure OpenAI, AI Foundry, Machine Learning, and GPU compute resources, then ask `ftk-database-query` to run `ai-cost-by-application` with the same date range. 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 + + Ask `ftk-database-query` to run `ai-token-usage-breakdown` again with the PRIOR month (startDate=startofmonth(ago(60d)), endDate=startofmonth(ago(30d))). Compare: + + - 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` in GPU-capable regions to identify GPU quota constraints for AI workloads that rely on 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 + + - FOCUS compliance note (all metrics use standard FOCUS columns) + + + Use 🤖 for AI metrics, 💰 for savings, 📊 for trends, 🎯 for recommendations. + + + Do not stop until the report is complete. + + + ## Phase 5: Deliver + + + + **Visualizations:** Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (financial results only):** Post the final AI workload cost report to our Teams channel. Include the executive summary, token economics, model efficiency table, and top optimization recommendations. Do not post intermediate results — only the completed report. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/alert-coverage-audit.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/alert-coverage-audit.yaml new file mode 100644 index 000000000..f9b6482f5 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/alert-coverage-audit.yaml @@ -0,0 +1,83 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: AlertCoverageAudit +spec: + name: AlertCoverageAudit + description: Monthly audit of cost anomaly alert coverage across active subscriptions + cron_expression: 0 8 16 * * + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Load your relevant FinOps practitioner skill. Do not call Python, Kusto, Resource Graph, Azure CLI, visualization, Teams, or Outlook tools directly; delegate tool work to the specialist subagent named in the routing guard. + + + ## Specialist tools to request + + + Specialist tools use their configured authentication and the agent system-managed identity where required. + + + - `ftk-hubs-agent`: Run `resource-graph-query` with `query`, optional `subscription_ids` to find active subscriptions, cost anomaly alerts, action groups, alert rules, and monitoring metadata. + + - `ftk-database-query`: Run `anomaly-detection-rate` and `anomaly-variance-total` for Kusto-backed Anomaly Management KPI context. + + + ## 1. Establish subscription inventory + + + Ask `ftk-hubs-agent` to run `resource-graph-query` to identify active subscriptions and ownership metadata. Exclude subscriptions explicitly documented as out of scope. + + + ## 2. Find anomaly alert coverage + + + Ask `ftk-hubs-agent` to 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. + + Ask `ftk-database-query` to run `anomaly-detection-rate` and `anomaly-variance-total` for the same monitoring period. Use those outputs to distinguish missing alert coverage from low observed anomaly volume, and cite the KPI tool result when anomaly 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/benefit-recommendation-review.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/benefit-recommendation-review.yaml new file mode 100644 index 000000000..85c40c48d --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/benefit-recommendation-review.yaml @@ -0,0 +1,83 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: BenefitRecommendationReview +spec: + name: BenefitRecommendationReview + description: Weekly executive review of reservation and savings plan recommendations + cron_expression: 0 8 * * 5 + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Load the FinOps toolkit and Azure Cost Management skills. Lead this as the FinOps practitioner. Ask `azure-capacity-manager` to run `benefit-recommendations` for current Cost Management recommendations, delegate any Kusto-backed recommendation, utilization, transaction, or savings evidence to `ftk-database-query`, 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. + + - `ftk-database-query`: Run `reservation-recommendation-breakdown`, `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, `macc-consumption-vs-commitment`, and `savings-summary-report` for Kusto-backed Rate Optimization and commitment KPI 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. + + Ask `ftk-database-query` to run `reservation-recommendation-breakdown`, `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, `macc-consumption-vs-commitment`, and `savings-summary-report`. Use these outputs to reconcile external benefit recommendations against realized commitment utilization, commitment waste, compute coverage, MACC drawdown, and effective savings rate. + + + ## 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. Include at minimum a savings by recommendation type chart and a cost versus savings 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/budget-coverage-audit.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/budget-coverage-audit.yaml new file mode 100644 index 000000000..cd35b3dd9 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/budget-coverage-audit.yaml @@ -0,0 +1,79 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: BudgetCoverageAudit +spec: + name: BudgetCoverageAudit + description: Monthly audit of subscription budget coverage and missing budget controls + cron_expression: 0 8 15 * * + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Load your relevant FinOps practitioner skill. Do not call Python, Kusto, Resource Graph, Azure CLI, visualization, Teams, or Outlook tools directly; delegate tool work to the specialist subagent named in the routing guard. + + + ## Specialist tools to request + + + Specialist tools use their configured authentication and the agent system-managed identity where required. + + + - `ftk-hubs-agent`: 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 + + + Ask `ftk-hubs-agent` to 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 + + + Ask `ftk-hubs-agent` to run `resource-graph-query` against budget resource types and related policy or governance metadata. 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. Include at minimum a budget coverage by portfolio chart and a missing-budget count by environment 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-daily-monitor.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-daily-monitor.yaml new file mode 100644 index 000000000..d4ed64f60 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-daily-monitor.yaml @@ -0,0 +1,134 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: CapacityDailyMonitor +spec: + name: CapacityDailyMonitor + description: Daily FinOps capacity management health check — quota usage, CRG utilization, zone capacity + cron_expression: 30 6 * * * + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Daily FinOps Capacity Management Monitor + + + ## Phase 0: Preparation + + + Before starting any checks, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its azure-capacity-management skill during the handoff. + + + 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 use the following Python tools instead of `az` CLI commands when available. Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `vm-quota-usage`: Run with subscription_id and location to get VM family quota usage with at-risk thresholds + - `capacity-reservation-groups`: Run with subscription_id and resource_group when scoped to list CRGs with instanceView utilization + - `data-freshness-check`: Run with cluster_uri and database to check Hub data staleness + - `non-compute-quotas`: Run with subscription_id and location to get Storage and Network quota usage + + + ## 1. Quota Health + + + Ask `azure-capacity-manager` to use `vm-quota-usage` against each active 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 + + Ask `azure-capacity-manager` to use `non-compute-quotas` for Storage and Network quota usage so non-compute limits are included in the daily capacity status. + + + Reference: `references/docs/operations/quota/README.md` + + + ## 2. Capacity Reservation Utilization + + + Ask `azure-capacity-manager` to use `capacity-reservation-groups` for each active CRG: + + - 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 + + + Reference: `references/docs/operations/capacity-reservations/README.md` + + + ## 3. AKS Node Pool Capacity + + + For AKS clusters with CRG associations: + + - Verify node pool CRG association is still active + + - Check if autoscaler is hitting quota limits + + - Report node pool scale events in last 24 hours + + + Reference: `references/docs/operations/aks-capacity/README.md` + + + ## 4. Alert Status + + + Check for any fired alerts in last 24 hours: + + - Quota threshold alerts + + - Budget alerts + + - Cost anomaly alerts + + Ask `ftk-hubs-agent` to run `data-freshness-check` to verify Hub data health before trusting alert and cost context. + + + Reference: `references/docs/operations/monitoring-alerting/README.md` + + + ## 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 + + + Do not stop until all checks are complete. + + + ## Deliver + + + + **Visualizations:** Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (status results only):** Post the final daily capacity health check to our Teams channel. Include the overall status (🔴🟡🟢), any critical or warning items, and recommended actions. Do not post intermediate results — only the completed summary. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-monthly-planning.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-monthly-planning.yaml new file mode 100644 index 000000000..883424c5a --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-monthly-planning.yaml @@ -0,0 +1,166 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: CapacityMonthlyPlanning +spec: + name: CapacityMonthlyPlanning + description: Monthly capacity planning cycle — demand forecast, capacity request pipeline, governance review + cron_expression: 0 9 1 * * + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Monthly Capacity Planning Cycle + + + ## Phase 0: Preparation + + + Before starting any analysis, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its azure-capacity-management skill during the handoff. + + + 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 use the following Python tools instead of `az` CLI commands when available. Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `vm-quota-usage`: Run with subscription_id and location to get VM family quota usage with at-risk thresholds + - `capacity-reservation-groups`: Run with subscription_id and resource_group when scoped to list CRGs with instanceView utilization + - `azure-capacity-manager`: Run `benefit-recommendations` with billing_scope and lookback to get reservation and savings plan recommendations + - `ftk-hubs-agent`: Run `resource-graph-query` with query and subscriptions to execute Azure Resource Graph KQL + - `ftk-database-query`: Run `compute-spend-commitment-coverage`, `compute-cost-per-core`, `commitment-utilization-score`, `commitment-discount-waste`, and `macc-consumption-vs-commitment` for capacity-related Rate Optimization KPI evidence + + + ## 1. Demand Forecast + + + Ask `ftk-database-query` to run `cost-forecasting-model` Kusto tool with 180-day historical, 90-day forecast. + + Filter to compute services. Project: + + - VM core demand by family for next 3 months + + - Growth rate by region + + - Seasonal patterns (if detectable) + + + Cross-reference with business targets: + + - New customer deployments planned? + + - Stamp scale-out events expected? + + + Reference: `references/docs/operations/capacity-planning/README.md` + + + ## 2. Capacity Request Pipeline + + + Based on the demand forecast, use `vm-quota-usage` 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 + + + Reference: `references/docs/operations/quota/README.md` and capacity request guidance in the uploaded knowledge. + + + ## 3. Allocation Review + + + Ask `azure-capacity-manager` to use `capacity-reservation-groups` to review current CRG portfolio: + + - 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 + + + Reference: `references/docs/operations/capacity-reservations/README.md` + + + ## 4. Cost Impact + + + Ask `azure-capacity-manager` to run `benefit-recommendations`, then ask `ftk-database-query` to run `commitment-discount-utilization`, `savings-summary-report`, `compute-spend-commitment-coverage`, `compute-cost-per-core`, `commitment-utilization-score`, `commitment-discount-waste`, and `macc-consumption-vs-commitment` for Kusto-backed rate-optimization and capacity unit-economics 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. Ask `ftk-hubs-agent` to use `resource-graph-query` for subscription, alert, budget, and governance inventory where ARM resource data is needed: + + - 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? + + + Reference: `references/docs/operations/capacity-governance/README.md` and `references/docs/operations/monitoring-alerting/README.md` + + + ## 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 + + + Use deep investigation for any critical capacity gaps. Use #remember to note key capacity findings. + + + ## Deliver + + + + **Visualizations:** Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (capacity results only):** Post the final monthly capacity planning report to our Teams channel. Include the demand forecast, capacity request pipeline, CRG portfolio review, governance scorecard, and recommended actions. Do not post intermediate results — only the completed report. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-quarterly-strategy.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-quarterly-strategy.yaml new file mode 100644 index 000000000..7a5d71dd8 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-quarterly-strategy.yaml @@ -0,0 +1,163 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: CapacityQuarterlyStrategy +spec: + name: CapacityQuarterlyStrategy + description: Quarterly capacity strategy review — FinOps maturity, commitment alignment, architecture evolution + cron_expression: 0 9 1 1,4,7,10 * + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Quarterly Capacity Strategy Review + + + ## Phase 0: Preparation + + + Before starting any analysis, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its azure-capacity-management skill during the handoff. + + + 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 use the following Python tools instead of `az` CLI commands when available. Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `azure-capacity-manager`: Run `benefit-recommendations` with billing_scope and lookback to get reservation and savings plan recommendations + - `ftk-hubs-agent`: Run `resource-graph-query` with query and subscriptions to execute Azure Resource Graph KQL + - `vm-quota-usage`: Run with subscription_id and location to get VM family quota usage with at-risk thresholds + - `ftk-database-query`: Run `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, `compute-cost-per-core`, and `macc-consumption-vs-commitment` for quarterly Rate Optimization KPI evidence + + + ## 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` for Azure Cost Management recommendations. Ask `ftk-database-query` to run `reservation-recommendation-breakdown`, `commitment-discount-utilization`, `savings-summary-report`, `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, `compute-cost-per-core`, and `macc-consumption-vs-commitment` with quarterly scope for Kusto-backed commitment, savings, coverage, and unit-economics 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 + + + Reference: Skill section on "Capacity reservation vs Azure Reservation vs savings plan" + + + ## 3. Architecture Review + + + Ask `ftk-hubs-agent` to run `resource-graph-query` for resource inventory, and ask `azure-capacity-manager` to use `vm-quota-usage` for multi-subscription quota distribution: + + - Stamp-based isolation health — any stamps at capacity? + + - Multi-subscription quota distribution — balanced? + + - Regional expansion needs for next quarter + + - AKS cluster capacity governance + + + Reference: `references/docs/operations/capacity-planning/README.md` and `references/docs/operations/aks-capacity/README.md` + + + ## 4. Non-Compute Quotas + + + Review non-compute service quotas from available Hub data and resource inventory: + + - Storage account limits + + - Cosmos DB throughput limits + + - Service Bus/Event Hub namespace limits + + - Any service-specific scaling constraints + + + Reference: `references/docs/operations/non-compute-quotas/README.md` + + + ## 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? + + + Reference: `references/docs/operations/capacity-governance/README.md` + + + ## 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, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (strategy results only):** Post the final quarterly capacity strategy brief to our Teams channel. Include the maturity scorecard, commitment alignment matrix, quarterly roadmap, and key risks. Do not post intermediate results — only the completed strategy brief. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-weekly-supply-review.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-weekly-supply-review.yaml new file mode 100644 index 000000000..ba3458bbe --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/capacity-weekly-supply-review.yaml @@ -0,0 +1,149 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: CapacityWeeklySupplyReview +spec: + name: CapacityWeeklySupplyReview + description: Weekly FinOps capacity management review — quota headroom, CRG cost optimization, SKU availability, benefit recommendations + cron_expression: 0 8 * * 1 + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Weekly FinOps Capacity Management Review + + + ## Phase 0: Preparation + + + Before starting any analysis, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `azure-capacity-manager` to load its azure-capacity-management skill during the handoff. + + + 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 use the following Python tools instead of `az` CLI commands when available. Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `vm-quota-usage`: Run with subscription_id and location to get VM family quota usage with at-risk thresholds + - `non-compute-quotas`: Run with subscription_id and location to get Storage and Network quota usage + - `capacity-reservation-groups`: Run with subscription_id and resource_group when scoped to list CRGs with instanceView utilization + - `sku-availability`: Ask `azure-capacity-manager` to run with subscription_id and location to get Compute SKU availability and restrictions by default. Use `resource_provider: kusto` with a planned Data Explorer SKU in `sku_filter` to preflight Azure Data Explorer / Kusto eligibility for FinOps Hub analytics deployments; never infer Kusto availability from Compute SKU results. + - `azure-capacity-manager`: Run `benefit-recommendations` with billing_scope and lookback to get reservation and savings plan recommendations + - `ftk-database-query`: Run `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, and `compute-cost-per-core` when Kusto cost evidence is available + + + ## 1. Quota Headroom Analysis + + + Ask `azure-capacity-manager` to run comprehensive quota analysis across all subscriptions using `vm-quota-usage` and `non-compute-quotas`: + + - 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 + + + Reference: `references/docs/operations/quota/README.md` and `references/docs/operations/quota-groups/README.md` + + + ## 2. CRG Cost-Waste Audit + + + Ask `azure-capacity-manager` to use `capacity-reservation-groups` for all capacity reservations: + + - Calculate weekly cost of unused reserved capacity (reserved minus allocated × pay-as-you-go rate) + + - Compare CRG cost against equivalent Azure Reservation or savings plan cost + + - Identify CRGs that could be rightsized (reduce reservation quantity) + + - Check for CRGs without matching Azure Reservations (paying both capacity guarantee AND full price) + + + Key distinction from skill: Capacity reservations guarantee supply. Azure Reservations reduce cost. Best practice is to pair both. + + + Reference: `references/docs/operations/capacity-reservations/README.md` + + + ## 3. SKU and Zone Availability Validation + + + Ask `azure-capacity-manager` to use `sku-availability` to validate SKU and zone 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 + + - For FinOps Hub Data Explorer backend work, ask `azure-capacity-manager` to run `sku-availability` with `resource_provider: kusto` and the planned ADX SKU as `sku_filter`; treat `is_available: false` as a deploy or upgrade blocker. + + + Reference: Skill section on zone alignment and `references/docs/operations/capacity-planning/README.md` + + + ## 4. Benefit Recommendations + + + Ask `azure-capacity-manager` to run `benefit-recommendations` when current RI/SP recommendations are needed. Where Hub cost context is available, ask `ftk-database-query` to run `reservation-recommendation-breakdown`, `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, and `compute-cost-per-core` for Kusto-backed recommendation, commitment, coverage, and unit-economics 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 use `sku-availability` to check for any new region access or zonal enablement needs: + + - Upcoming deployments that need new regions? + + - SKU restrictions blocking planned scaling? + + + Reference: `references/docs/operations/capacity-planning/README.md` + + + ## 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 + + + Use #remember to note key FinOps capacity findings. Use deep investigation if any quota is critically low. + + + ## Deliver + + + + **Visualizations:** Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (capacity results only):** Post the final weekly capacity briefing to our Teams channel. Include the quota headroom scorecard, CRG waste summary, benefit recommendations, and capacity action items. Do not post intermediate results — only the completed briefing. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/compute-utilization-trend.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/compute-utilization-trend.yaml new file mode 100644 index 000000000..0d4b69a2a --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/compute-utilization-trend.yaml @@ -0,0 +1,83 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: ComputeUtilizationTrend +spec: + name: ComputeUtilizationTrend + description: Weekly VM quota utilization trend review across subscriptions and regions + cron_expression: 0 7 * * 1 + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, 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 use Python tools with DefaultAzureCredential and the agent system-managed identity. + + + - `ftk-hubs-agent`: Run `resource-graph-query` with `query` and optional `subscription_ids` to discover active subscriptions, active VM resources, regions, and prior tagging or ownership context. + + - `vm-quota-usage`: Run with `subscription_id` and optional `location` to retrieve VM family current usage, limits, and utilization percentages. + + + ## 1. Establish scope + + + Ask `ftk-hubs-agent` to use `resource-graph-query` to identify active subscriptions with compute resources and the 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 + + + For each active subscription and region, ask `azure-capacity-manager` to run `vm-quota-usage` with `subscription_id`, `location`. Capture VM family current usage, limit, utilization percentage, warning status, and critical status. + + + ## 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/cost-optimization.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/cost-optimization.yaml new file mode 100644 index 000000000..19fe9233a --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/cost-optimization.yaml @@ -0,0 +1,333 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: CostOptimization +spec: + name: CostOptimization + description: Comprehensive cost optimization report with orphaned resources, rightsizing, and commitment analysis + cron_expression: 0 8 * * 1 + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Cost optimization report + + + ## Phase 0: Preparation + + + Before starting any analysis, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Generate a comprehensive cost optimization report for the current Azure environment. Works with or without FinOps hubs. + + ## 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. + + - `ftk-hubs-agent`: Run `resource-graph-query` with query and subscriptions to execute Azure Resource Graph KQL for orphaned resource detection + - `azure-capacity-manager`: Run `benefit-recommendations` with billing_scope and lookback to get reservation and savings plan recommendations + - `ftk-database-query`: Run Kusto KPI tools when a FinOps hub is connected, especially `cost-optimization-index`, `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, `compute-cost-per-core`, `cost-per-gb-stored`, and `storage-tier-distribution` + + + ## 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 autonomous 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. + + 4. Check for FinOps hubs: Read `.ftk/environments.local.md` to see if a FinOps hub is connected. If available, note it for Phase 2. + + + ## 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. + + + Ask `ftk-hubs-agent` to run `resource-graph-query` with the queries from `references/azure-orphaned-resources.md` 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. Ask `ftk-hubs-agent` to run `resource-graph-query` for any Advisor recommendation Resource Graph queries in the references. + + + Use `references/azure-advisor.md` for query patterns: + + + 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. Ask `azure-capacity-manager` to run `benefit-recommendations` for new reservation and savings plan purchase recommendations. + + + Use `references/azure-savings-plans.md` and `references/azure-reservations.md` for: + + - 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 + + + Use `references/azure-commitment-discount-decision.md` for the decision framework when recommending new purchases. + + + ### 2d: FinOps hubs data (if available) + + + If a FinOps hub is connected from Phase 1, do not run Kusto directly. Ask `ftk-database-query` to collect these evidence packages with the connected cluster URI, database, and analysis window: + + + - `monthly-cost-trend` for the last three complete months. + + - `top-services-by-cost` for the current and prior complete month. + + - `cost-by-region-trend` when placement or capacity decisions affect optimization. + + - `cost-anomaly-detection` for unusual service-level spikes or drops. + + - `cost-optimization-index`, `commitment-utilization-score`, `commitment-discount-waste`, `compute-spend-commitment-coverage`, `compute-cost-per-core`, `cost-per-gb-stored`, and `storage-tier-distribution` for the generated FinOps KPI optimization scorecard. + + + Use the returned evidence to identify cost anomalies and trends that inform optimization priorities. + + + ## 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. + + + Use `references/azure-retail-prices.md` 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. + + + Use `references/azure-vm-rightsizing.md` for: + + - 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 use `non-compute-quotas` 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} + + **FinOps Hubs:** {connected / not connected} + + + ## 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 (FinOps hubs) + + {Include if FinOps hubs connected, otherwise note: "Connect FinOps hubs for trend analysis — run /ftk-hubs-connect"} + + + ## 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, connect FinOps hubs for trend analysis + + + ## Audit trail + + + | Data Source | Query Time | Records | + + |-------------|-----------|---------| + + | Resource Graph (orphaned) | {timestamp} | {count} | + + | Advisor recommendations | {timestamp} | {count} | + + | Benefit Recommendations API | {timestamp} | {count} | + + | FinOps hubs (if connected) | {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") + + - If FinOps hubs are not connected, recommend `/ftk-hubs-connect` for deeper analysis + + - 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, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (financial results only):** Post the final cost optimization report to our Teams channel. Include the executive summary, total identified savings, quick wins, and prioritized next steps. Do not post intermediate results — only the completed report. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/db-quota-audit.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/db-quota-audit.yaml new file mode 100644 index 000000000..354ff8453 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/db-quota-audit.yaml @@ -0,0 +1,60 @@ +api_version: azuresre.ai/v1 +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 + cron_expression: 0 7 * * 3 + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, 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 use Python tools with DefaultAzureCredential and the agent system-managed identity. + + - `db-service-quotas`: Run with `subscription_id`, optional `location`, optional `services`, optional `include_capabilities` 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`, optional `location`. 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 + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + ## Visualizations + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 Teams message and attach or embed them in the Outlook email alongside the text tables. diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/hubs-health-check.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/hubs-health-check.yaml new file mode 100644 index 000000000..6877b4ab4 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/hubs-health-check.yaml @@ -0,0 +1,106 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: HubsHealthCheck +spec: + name: HubsHealthCheck + description: FinOps hub version and data freshness validation + cron_expression: 0 6 * * * + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Health check for FinOps hubs + + + ## Phase 0: Preparation + + + Before starting any checks, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate FinOps Hub platform evidence collection and specialist analysis to `ftk-hubs-agent`; do not try to run Hub platform tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply `ftk-output-style.md`, generate supported visualizations, and deliver the completed output through configured connectors. + + ## Specialist tools to request from `ftk-hubs-agent` + + Ask `ftk-hubs-agent` to use its assigned Hub health tools instead of ad hoc `az` CLI commands when available. Specialist tools use their configured authentication and the agent system-managed identity where required. + + - `data-freshness-check`: Run with cluster_uri and database to check Hub data staleness + + - `ftk-database-query`: Run `cost-visibility-delay` and `data-update-frequency` to collect the FinOps KPI data-ingestion scorecard after `data-freshness-check` establishes authoritative freshness. + + + ## Step 1: Check the latest released FinOps hub version + + + Get the content from this file to determine the latest stable version of FinOps hubs: `https://raw.githubusercontent.com/microsoft/finops-toolkit/refs/heads/main/src/templates/finops-hub/modules/fx/ftkver.txt`. + + + Get the content from this file to determine the latest development version of FinOps hubs: `https://raw.githubusercontent.com/microsoft/finops-toolkit/refs/heads/dev/src/templates/finops-hub/modules/fx/ftkver.txt`. + + + If either URL fails or returns empty content, do not infer the latest version. Report the version lookup as unavailable and continue with the remaining health checks. + + + FinOps hubs use semantic versioning (SemVer) format for version numbers, which is `major.minor`, `major.minor.patch`, or `major.minor-prerelease`. If the version number has `-dev` at the end of it, + that means it''s a development version. + + + Compare the version of the current FinOps hub instance with the latest stable version of FinOps hubs. If it''s the same version as stable, tell the user they are using the latest released version and + skip to the next step. + + + If the FinOps hub version is the same as the development version, tell the user they are using the development version and they should monitor the repository to ensure it''s updated with the latest + changes, then skip to the next step. + + + If the FinOps hub version is older than the development version and matches or is older than the latest stable version, tell the user they are using an older development version and should update to + the latest stable release or development version. Mention their version number and the latest stable and development version numbers. Give them this link to deploy the latest stable version depending + on their Azure cloud environment: + + + - For the Azure public, commercial cloud, use https://aka.ms/finops/hubs/deploy + + - For the Azure Government cloud, use https://aka.ms/finops/hubs/deploy/gov + + - For the Azure China cloud, use https://aka.ms/finops/hubs/deploy/china + + + ## Step 2: Check the latest data refresh/update date + + + Ask `ftk-hubs-agent` to run `data-freshness-check` as the authoritative data staleness tool. This tool checks actual data dates in the Hub database through direct Azure Data Explorer REST queries, not Kusto ingestion timestamps or stale memory. Treat `Costs()` as the primary freshness signal. If the latest cost data is 3 days old or newer, skip stale-data remediation because Azure cost data normally lags 24-48 hours. If stale-memory, raw-KQL, or ingestion timestamp conclusions conflict with `data-freshness-check`, mark those older conclusions as superseded and unsafe. + + + If the latest `Costs()` data is more than 3 days old, or if `Costs()` has no rows, inform the user that the data may be stale and they should check the Microsoft Cost Management exports and Azure Data Factory data ingestion pipelines to ensure they are running without errors. + + + If `Transactions()` returns zero rows, do not report a generic empty table or a healthy success. Report the `TRANSACTIONS_ZERO_ROWS` diagnostic from `data-freshness-check` and state that reservation transaction analysis is unavailable until the Cost Management `reservationtransactions` export, Azure Data Factory Transactions ingestion, and Hub `Transactions()` / `Transactions_v1_2()` stored-function behavior are verified. Empty `Transactions()` is not enough by itself to mark core `Costs()` freshness stale, but it is an action item. + + + Report empty `Prices()` or `Recommendations()` results as follow-up items to confirm whether those exports should be configured. Do not report the hub as stale solely because an optional function is empty. + + Ask `ftk-database-query` to run `cost-visibility-delay` and `data-update-frequency` after the Hub platform freshness check. Use those KPI outputs as supporting Data Ingestion evidence, but do not let them override the `data-freshness-check` direct REST verdict for current `Costs()` staleness. + + + Give them a link to Microsoft Cost Management to check the exports: https://portal.azure.com/#view/Microsoft_Azure_CostManagement/Menu/~/exports + + + Give them a link to the Azure Data Factory portal to check data ingestion pipelines: https://adf.azure.com/monitoring/pipelineruns + + + Tell the user you can help them troubleshoot any issues with [common errors](https://learn.microsoft.com/cloud-computing/finops/toolkit/help/errors) and the [troubleshooting guide](https://learn.microsoft.com/cloud-computing/finops/toolkit/help/troubleshooting). + + + ## Deliver + + + + **Visualizations:** Include a trend chart of data freshness (hours since last update) if degradation is detected. Version status should be reported as a table. Skip charts if all checks pass. 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. + + **Teams and Outlook (status results only):** Post the final health check results to our Teams channel. Include version status, data freshness status, and any action items. Do not post intermediate results — only the completed health check summary. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 — version drift patterns, data freshness trends, pipeline failure modes, troubleshooting steps that worked. Never save financial figures or customer financial data to the knowledge base. Financial detail goes to configured Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/monitoring-scope-validation.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/monitoring-scope-validation.yaml new file mode 100644 index 000000000..a0c6c92c9 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/monitoring-scope-validation.yaml @@ -0,0 +1,87 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: MonitoringScopeValidation +spec: + name: MonitoringScopeValidation + description: Weekly validation that FinOps Hub monitoring covers all active subscriptions + cron_expression: 0 9 * * 4 + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Monitoring Scope Validation + + + ## Phase 0: Preparation + + + Before starting, read all documents in the knowledge base. Pay special attention to FinOps Hub deployment notes, ADX cluster details, monitored scope decisions, and data freshness workarounds. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Orchestration rule: This scheduled task is owned by `finops-practitioner`. Delegate monitoring-scope and FinOps Hub platform evidence collection to `ftk-hubs-agent`; do not try to run Hub platform tools directly from the practitioner. When the specialist returns evidence, assemble the final FinOps report, apply `ftk-output-style.md`, generate supported visualizations, and deliver the completed output through configured connectors. + + Ask `ftk-hubs-agent` to load its FinOps Hubs guidance and use assigned Hub health tools with the agent system-managed identity where required. + + + ## Specialist tools to request from `ftk-hubs-agent` + + + Ask `ftk-hubs-agent` to use assigned Hub health tools with their configured authentication and the agent system-managed identity where required. + + + - `ftk-hubs-agent`: Run `resource-graph-query` with `query`, optional `subscription_ids` to list active subscriptions and subscription metadata. + + - `data-freshness-check`: Run with `cluster_uri`, optional `database` to validate FinOps Hub function freshness and coverage signals. + + - `ftk-database-query`: Run `percentage-unallocated-costs`, `percentage-untagged-costs`, `tagging-policy-compliance`, and `allocation-accuracy-index` when Kusto is available to validate allocation coverage quality inside the monitored scope. + + + ## 1. Establish active subscription inventory + + + Ask `ftk-hubs-agent` to run `resource-graph-query` to list active subscriptions and key ownership or environment metadata. Exclude subscriptions documented as intentionally outside the Hub monitoring scope. + + + ## 2. Validate Hub data freshness + + + Ask `ftk-hubs-agent` to run `data-freshness-check` with `cluster_uri`, optional `database`. Capture latest data dates, staleness days, stale functions, missing functions, and query errors. + + + ## 3. Cross-reference monitoring coverage + + + Cross-reference Resource Graph subscription inventory with subscriptions represented in Hub data coverage notes or Hub query results. Identify active subscriptions with missing, stale, or ambiguous Hub coverage. + + Ask `ftk-database-query` to run `percentage-unallocated-costs`, `percentage-untagged-costs`, `tagging-policy-compliance`, and `allocation-accuracy-index` for the monitored scope. Use those Allocation KPI results to distinguish "subscription missing from monitoring" from "subscription present but allocation/tagging coverage is weak." + + + ## 4. Flag scope gaps + + + Flag any active subscription not covered by Hub monitoring, any stale Hub function, and any access or query failure that prevents confidence in coverage. Recommend export, scope, identity, or ingestion remediation. + + + ## Report format + + + Produce a markdown report with an executive summary, a subscription coverage table, a data freshness table, coverage gaps, errors, and recommended remediation actions. + + + ## Persistence + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. Include at minimum a monitored versus unmonitored subscription chart and a data staleness 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/monthly-report.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/monthly-report.yaml new file mode 100644 index 000000000..c32f303a9 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/monthly-report.yaml @@ -0,0 +1,193 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: Monthly +spec: + name: Monthly + description: Autonomous monthly cost analysis with core cost-analysis Kusto tools + cron_expression: 15 17 5 * * + agent_mode: autonomous + agent: finops-practitioner + agent_prompt: '# Monthly Cost Analysis + + + ## Phase 0: Preparation + + + Before starting any analysis, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Load your finops-toolkit and azure-cost-management skills. + + + Perform a comprehensive autonomous 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 + + Lead the workflow as the FinOps practitioner. Do not run Kusto tools directly. Ask `ftk-database-query` to collect FinOps Hub Kusto evidence and ask `azure-capacity-manager` to collect quota and capacity evidence. + + - `ftk-hubs-agent`: Run `data-freshness-check` with cluster_uri and database to check Hub data staleness + - `azure-capacity-manager`: Run `vm-quota-usage` with subscription_id and location to get VM family quota usage with at-risk thresholds + + + ## Phase 1: Data Collection + + + Request these Kusto evidence packages from `ftk-database-query` in sequence. Use #remember to note operational observations (tool errors, data quality issues, query patterns) as you go — never financial figures. + + + ### 1.1 Verify Connectivity + + Ask `ftk-hubs-agent` to run `data-freshness-check` with the FinOps Hub cluster URI and Hub database. Treat its direct REST `Costs()` latest data date as the authoritative freshness source and mark any conflicting stale-memory, raw-KQL, or ingestion timestamp conclusion as superseded and unsafe. Proceed if latest `Costs()` data is 3 days old or newer. Do not ask `ftk-database-query` to run `costs-enriched-base` for freshness checks or month-level reports; use aggregated Kusto tools for complete-month reporting windows and reserve row-level detail for one-day drill-downs only. + + + ### 1.2 Fiscal Trends + + Ask `ftk-database-query` to run `monthly-cost-trend` with startDate=startofmonth(ago(365d)) to get 12-month billed and effective cost trends. + + + ### 1.3 Service Breakdown + + Ask `ftk-database-query` to run `top-services-by-cost` with N=20, startDate=startofmonth(ago(30d)) to identify top cost-driving services. Flag commitment discount opportunities and right-sizing potential. + + + ### 1.4 Resource Group Analysis + + Ask `ftk-database-query` to run `top-resource-groups-by-cost` with default parameters. Identify consolidation opportunities. + + + ### 1.5 Anomaly Detection + + Ask `ftk-database-query` to run `cost-anomaly-detection` with numberOfMonths=12, interval=1d. Use deep investigation on any detected anomalies to form hypotheses about root causes. + + + ### 1.6 Savings and ESR + + Ask `ftk-database-query` to run `savings-summary-report` with startDate=startofmonth(ago(30d)). Record negotiated discount savings, commitment discount savings, total savings, and Effective Savings Rate (ESR). + + + ### 1.7 Commitment Utilization + + Ask `ftk-database-query` to run `commitment-discount-utilization` with startDate=startofmonth(ago(30d)). Compare utilization against the 60-70% industry benchmark. + + + **Checkpoint:** Confirm fiscal trends, service breakdown, resource groups, anomalies, savings, and commitments are all collected before proceeding. + + + ### 1.8 Forecast + + Ask `ftk-database-query` to run `cost-forecasting-model` with startDate=startofmonth(ago(180d)), forecastPeriods=90, interval=1d. Assess forecast confidence. + + + ### 1.9 Reservation Recommendations + + Ask `ftk-database-query` to run `reservation-recommendation-breakdown` to identify top savings opportunities by break-even date and projected savings. + + + ### 1.10 Regional Distribution + + Ask `ftk-database-query` to run `cost-by-region-trend` with default parameters. Identify regional optimization opportunities. + + + ### 1.11 Resource Types + + Ask `ftk-database-query` to run `top-resource-types-by-cost` with N=20. Identify legacy or inefficient resource types. + + Ask `azure-capacity-manager` to run `vm-quota-usage` for subscriptions and locations with significant compute cost to identify VM family quota risks that could affect optimization or forecast assumptions. + + + ### 1.12 Tag Coverage + + Ask `ftk-database-query` to run `cost-by-financial-hierarchy` with N=20, startDate=startofmonth(ago(30d)), and endDate=startofmonth(now()) to review allocation dimensions and identify missing team, product, application, or environment values. Do not run raw `costs-enriched-base` for tag-coverage rollups. + + + ### 1.13 Price Benchmarking + + Ask `ftk-database-query` to run `service-price-benchmarking` with startDate=startofmonth(ago(30d)). Compare list, contracted, and effective costs. Identify negative savings anomalies. Compare variance to the 3-5% industry average + benchmark. + + + ### 1.14 Cost Volatility + + Ask `ftk-database-query` to run `cost-anomaly-detection` adapted for 90-day daily spikes. Identify peak cost dates and root causes. + + + ### 1.15 Marketplace/Other Purchases + + Ask `ftk-database-query` to run `top-other-transactions` with N=20. If it returns zero rows or `ZERO_ROWS_RETURNED`, ask `ftk-hubs-agent` to run `data-freshness-check` and report whether `Transactions()` has the `TRANSACTIONS_ZERO_ROWS` diagnostic. Do not conclude there were no large one-time non-usage purchases until the export, ingestion, and stored-function diagnostic is clear. + + + ### 1.16 Month-over-Month Change + + Ask `ftk-database-query` to run `monthly-cost-change-percentage` with startDate=startofmonth(ago(390d)). Track cost volatility and trend direction. + + + ### 1.17 FinOps KPI Scorecard + + Ask `ftk-database-query` to run the FinOps KPI tools that map directly to `src/queries/KPI.md` for the completed month. Use the same reporting window and keep every KPI result tied to its source tool: `percentage-unallocated-costs`, `percentage-untagged-costs`, `tagging-policy-compliance`, `allocation-accuracy-index`, `anomaly-detection-rate`, `anomaly-variance-total`, `monthly-cost-trend`, `cost-visibility-delay`, `data-update-frequency`, `macc-consumption-vs-commitment`, `ai-token-usage-breakdown`, `ai-daily-trend`, `ai-model-cost-comparison`, `commitment-utilization-score`, `compute-cost-per-core`, `savings-summary-report`, `compute-spend-commitment-coverage`, `commitment-discount-waste`, `cost-optimization-index`, `storage-tier-distribution`, and `cost-per-gb-stored`. + + Summarize the KPI scorecard by FinOps capability: Allocation, Data Ingestion, Anomaly Management, Rate Optimization, Unit Economics, Usage Optimization, AI unit economics, and KPIs & Benchmarking. If a KPI tool returns no rows or cannot run because the underlying Hub function is empty, classify it as a data sufficiency limitation and cite the tool result instead of omitting the KPI. + + + **Checkpoint:** Confirm ALL data collection is complete 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, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (financial results only):** Post the final report to our Teams channel. Include the executive summary, key findings, grade card, and top recommendations. Do not post intermediate results — only the completed report. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/non-compute-quota-audit.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/non-compute-quota-audit.yaml new file mode 100644 index 000000000..0cb3b8621 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/non-compute-quota-audit.yaml @@ -0,0 +1,81 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: NonComputeQuotaAudit +spec: + name: NonComputeQuotaAudit + description: Weekly audit of storage, network, and non-compute quota usage at risk + cron_expression: 0 7 * * 2 + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, 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 use Python tools with DefaultAzureCredential and the agent system-managed identity. + + + - `non-compute-quotas`: Run with `subscription_id`, optional `location` to retrieve storage, network, and estimated non-compute service quota utilization. + + + ## 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 + + + For each active subscription, ask `azure-capacity-manager` to run `non-compute-quotas` with `subscription_id`, optional `location`. Use the tool''s normalized `quotas` array for tables and charts. If only legacy `services` records are available, normalize them with safe accessors before reporting: accept `service_name` or `service`, `quota_name` or `name`, `current_count` or `currentValue` or `current`, and default missing `location` to `subscription`. Capture service name, quota name, current count, limit, utilization percentage, count source, limit source, limit type, and any errors. + + + ## 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/sku-availability-audit.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/sku-availability-audit.yaml new file mode 100644 index 000000000..7ac7e97e2 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/sku-availability-audit.yaml @@ -0,0 +1,86 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: SkuAvailabilityAudit +spec: + name: SkuAvailabilityAudit + description: Weekly audit of regional SKU availability and restrictions that could block deployments + cron_expression: 30 7 * * 3 + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, 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 use Python tools with DefaultAzureCredential and the agent system-managed identity. + + + - `sku-availability`: Ask `azure-capacity-manager` to run with `subscription_id`, `location`, optional `sku_filter`, optional `resource_provider`. Use the default `resource_provider: compute` to list Compute SKU availability, zones, and restrictions. Use `resource_provider: kusto` for Azure Data Explorer / Kusto SKU eligibility checks before FinOps Hub analytics backend deployments or upgrades. + + + ## 1. Establish region and SKU scope + + + Identify deployment regions, priority VM SKU families, and any planned FinOps Hub Data Explorer SKUs from knowledge base notes and prior reports. If no priority SKU list exists, audit commonly deployed VM families and explicitly state the assumed scope. If no Data Explorer SKU list exists, include the currently planned or default Hub analytics backend SKU where documented; otherwise state that ADX scope is unknown. + + + ## 2. Collect SKU availability + + + Ask `azure-capacity-manager` to run `sku-availability` for each scoped subscription and region using `subscription_id`, `location`, optional `sku_filter`, `resource_provider`. + + + - For Compute, use `resource_provider: compute` or omit it to preserve the default behavior. Capture total SKUs, restricted SKUs, restriction reason codes, zone availability, and restriction values. + + - For Azure Data Explorer / Kusto, use `resource_provider: kusto`. Capture `source_endpoint`, returned SKUs, `requested_sku`, `is_available`, and any guidance. ADX Kusto SKU eligibility must use the Microsoft.Kusto regional SKU API and must not be inferred from Microsoft.Compute SKU results. + + + ## 3. Analyze deployment blockers + + + Identify restricted Compute SKUs that could block future deployments, scaling, zone alignment, or capacity reservation planning. Identify Kusto SKUs that are absent from the Microsoft.Kusto regional SKU response and could block FinOps Hub analytics deployments. Group findings by provider, reason code when available, region, zone, subscription, and family or SKU name. + + + ## 4. Recommend actions + + + Recommend whether to request quota, change SKU family, change region, adjust deployment zone, select a returned ADX SKU, or escalate to Azure capacity planning. 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/storage-paas-growth-forecast.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/storage-paas-growth-forecast.yaml new file mode 100644 index 000000000..3452bcdef --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/storage-paas-growth-forecast.yaml @@ -0,0 +1,87 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: StoragePaasGrowthForecast +spec: + name: StoragePaasGrowthForecast + description: Monthly storage and PaaS quota growth forecast across active subscriptions + cron_expression: 0 8 1 * * + agent_mode: autonomous + agent: finops-practitioner + agent_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: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were 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 `ftk-output-style.md`, 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 use Python tools with DefaultAzureCredential and the agent system-managed identity. + + + - `ftk-hubs-agent`: Run `resource-graph-query` with `query` and optional `subscription_ids` to inventory storage accounts, network resources, App Service plans, SQL servers, Service Bus namespaces, Key Vaults, and other PaaS resources. + + - `non-compute-quotas`: Run with `subscription_id` and optional `location` to retrieve storage, network, and estimated PaaS quota utilization. + + - `ftk-database-query`: Run `storage-tier-distribution` and `cost-per-gb-stored` when FinOps Hub Kusto is connected to add storage unit-economics KPI evidence to the quota forecast. + + + ## 1. Establish scope + + + Ask `ftk-hubs-agent` to use `resource-graph-query` 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 + + + For each active subscription, ask `azure-capacity-manager` to run `non-compute-quotas` with `subscription_id`, optional `location`. Use the tool''s normalized `quotas` array for tables and charts. If only legacy `services` records are available, normalize them with safe accessors before reporting: accept `service_name` or `service`, `quota_name` or `name`, `current_count` or `currentValue` or `current`, and default missing `location` to `subscription`. Capture API-reported limits separately from estimated limits and preserve limit source notes. + + + ## 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. + + Ask `ftk-database-query` to run `storage-tier-distribution` and `cost-per-gb-stored` for the same reporting window. Use storage tier mix and cost-per-GB trends to explain whether growth is also creating avoidable storage unit-cost risk. + + + ## 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 + + + **Teams and Outlook (results only):** Post the final report to our Teams channel. Do not post intermediate results. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only. + + + ## Visualizations + + + Before delivery, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 Teams message and attach or embed them in the Outlook email alongside the text tables.' diff --git a/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/yoy-report.yaml b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/yoy-report.yaml new file mode 100644 index 000000000..69dacb965 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/yoy-report.yaml @@ -0,0 +1,199 @@ +api_version: azuresre.ai/v1 +kind: ScheduledTask +metadata: + name: Semiannual +spec: + name: Semiannual + description: Semiannual year-over-year financial analysis for January and July close checkpoints + cron_expression: 0 9 5 1,7 * + agent_mode: autonomous + agent: finops-practitioner + agent_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 any analysis, read all documents in the knowledge base. Pay special attention to known-issues-and-workarounds.md and any prior run notes. Use what you learn to avoid repeating known errors and to build on previous findings. + + Output style: Apply `ftk-output-style.md` from the knowledge base to every report, Teams message, and Outlook email message. Use its evidence labels, source/derivation rules, exact scope and time-period statements, FinOps capability sections, risk thresholds, confidence/caveat language, and disclaimer requirements. + + Tool routing and delivery guard: This scheduled task is owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools. Use subagent handoffs for all tool work and pass the full task scope, evidence already collected, open questions, and desired output shape with each handoff. Use `ftk-database-query` for FinOps Hub Kusto, cost, pricing, recommendation, and transaction query tools. Use `azure-capacity-manager` for capacity, quota, capacity reservation group, SKU availability, database service quota, and benefit recommendation Python tools. Use `ftk-hubs-agent` for FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment, remediation, alert and budget deployment, and Advisor suppression operations. Use `chief-financial-officer` only for executive finance framing. Use a tool-bearing specialist (`ftk-database-query`, `azure-capacity-manager`, or `ftk-hubs-agent`) for visualization and Teams/Outlook delivery after the final evidence package is assembled. Ask that delivery specialist to check available visualization and delivery tools exactly once per run and remember whether `PostTeamsMessage`, `SendOutlookEmail`, `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, and `ExecutePythonCode` are present. Do not call notification, email, connector, Microsoft Graph, `dynamicInvoke`, raw webhook, or connector APIs just to test availability. Use visualization tools first for charts when available, with `ExecutePythonCode` or code-interpreter chart generation only as a fallback. Complete the report before delivery. When a Teams connector/channel is configured, ask the delivery specialist to make one final `PostTeamsMessage` attempt to the configured Teams channel. When an Outlook connector/recipient is configured, ask the delivery specialist to make one final `SendOutlookEmail` attempt with the same final report and any generated chart artifacts. If a configured delivery path fails, mark the task/run failed instead of degrading to local output or probing alternate paths. If neither Teams nor Outlook delivery is configured or available, render the completed report in the run output and explicitly state which delivery paths were skipped. + + + + Load your finops-toolkit and azure-cost-management skills. Lead this as the FinOps practitioner. Delegate all FinOps Hub Kusto evidence collection to `ftk-database-query`, delegate capacity-risk evidence to `azure-capacity-manager`, and consult `chief-financial-officer` for fiscal planning, executive framing, and commitment-risk recommendations. + + + **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 + + Do not run Kusto tools directly. Ask `ftk-database-query` to run fiscal cost, pricing, savings, recommendation, and transaction tools. Ask `azure-capacity-manager` to run `benefit-recommendations` and ask `ftk-hubs-agent` to run `resource-graph-query` where non-Kusto Azure Cost Management or resource-inventory evidence is needed. + + - `azure-capacity-manager`: Run `benefit-recommendations` with billing_scope and lookback to get reservation and savings plan recommendations + - `ftk-hubs-agent`: Run `data-freshness-check` with cluster_uri and database to check Hub data freshness + - `ftk-hubs-agent`: Run `resource-graph-query` with query and subscriptions to execute Azure Resource Graph KQL + + + ## Phase 1: Data Collection + + + ### 1.1 Verify Connectivity + + Ask `ftk-hubs-agent` to run `data-freshness-check` with the FinOps Hub cluster URI and Hub database. Treat its direct REST `Costs()` latest data date as the authoritative freshness source and mark any conflicting stale-memory, raw-KQL, or ingestion timestamp conclusion as superseded and unsafe. Proceed if latest `Costs()` data is 3 days old or newer. Do not ask for `costs-enriched-base` for freshness checks or fiscal-period reports; use aggregated Kusto tools for complete-month reporting windows and reserve row-level detail for one-day drill-downs only. + + + ### 1.2 Year-over-year trends + + Ask `ftk-database-query` to run `monthly-cost-trend` with startDate set to the start of the prior comparison period. Get month-by-month billed and effective cost for the current comparison period and the same prior-year period. + + + ### 1.3 Service Portfolio Analysis + + Ask `ftk-database-query` to run `top-services-by-cost` with N=20 for the current comparison period and the prior-year comparison period. Identify the top services driving year-over-year spend changes. + + + ### 1.4 Quarterly Resource Group Trends + + Ask `ftk-database-query` to run `quarterly-cost-by-resource-group` with N=10, covering the current and prior-year comparison periods. Identify quarterly growth patterns and material resource-group shifts. + + + ### 1.5 Fiscal Year Anomalies + + Ask `ftk-database-query` to run `cost-anomaly-detection` across the current and prior-year comparison periods. Use deep investigation on any significant anomalies or year-over-year discontinuities. + + + ### 1.6 Savings Realization + + Ask `ftk-database-query` to run `savings-summary-report` for the current and prior-year comparison periods. Calculate ESR, total savings, and year-over-year savings movement. + + + ### 1.7 Commitment Performance + + Ask `ftk-database-query` to run `commitment-discount-utilization` for the current and prior-year comparison periods. Assess how commitment purchases changed year over year. + + + ### 1.8 Financial Hierarchy + + Ask `ftk-database-query` to run `cost-by-financial-hierarchy` with N=10 for the current and prior-year comparison periods. Understand cost allocation across billing profiles, teams, and applications. + + + **Checkpoint:** Confirm year-over-year comparison data collection is complete. + + + ### 1.9 Forecast + + Ask `ftk-database-query` to run `cost-forecasting-model` using the current comparison period as actuals and enough historical data to project the next fiscal checkpoint. For the mid-year checkpoint, forecast through fiscal year end (e.g., June 30 for a July–June fiscal year). For the year-end checkpoint, forecast the next fiscal year planning baseline. + + + ### 1.10 Reservation Recommendations + + Ask `azure-capacity-manager` to run `benefit-recommendations` and ask `ftk-database-query` to run `reservation-recommendation-breakdown`. Identify commitments that could save money before fiscal year end. + + + ### 1.11 Regional Analysis + + Ask `ftk-database-query` to run `cost-by-region-trend` for the current and prior-year comparison periods. Identify regional cost shifts. + + + ### 1.12 Resource Type Efficiency + + Ask `ftk-hubs-agent` to run `resource-graph-query` for current Azure resource inventory, then ask `ftk-database-query` to run `top-resource-types-by-cost` with N=20 for the current and prior-year comparison periods. Track resource type evolution. + + + ### 1.13 Price Benchmarking + + Ask `ftk-database-query` to run `service-price-benchmarking` for the current and prior-year comparison periods. Benchmark pricing effectiveness year over year. + + + ### 1.14 Month-over-Month Volatility + + Ask `ftk-database-query` to run `monthly-cost-change-percentage` covering both comparison periods. Identify the most volatile months and distinguish month-over-month volatility from true year-over-year movement. + + + ### 1.15 Commitment Transactions + + Ask `ftk-database-query` to run `top-commitment-transactions` with N=20 for the current and prior-year comparison periods. Track major RI/SP purchases. If it returns zero rows or `ZERO_ROWS_RETURNED`, ask `ftk-hubs-agent` to run `data-freshness-check` and report whether `Transactions()` has the `TRANSACTIONS_ZERO_ROWS` diagnostic. Do not conclude there were no commitment purchases until the export, ingestion, and stored-function diagnostic is clear. + + + ### 1.16 Other Transactions + + Ask `ftk-database-query` to run `top-other-transactions` with N=20 for the current and prior-year comparison periods. If it returns zero rows or `ZERO_ROWS_RETURNED`, ask `ftk-hubs-agent` to run `data-freshness-check` and report whether `Transactions()` has the `TRANSACTIONS_ZERO_ROWS` diagnostic. Do not conclude there were no non-usage spend patterns until the export, ingestion, and stored-function diagnostic is clear. + + + ### 1.17 Fiscal FinOps KPI Scorecard + + Ask `ftk-database-query` to run the full FinOps KPI tool set from `src/queries/KPI.md` for the current fiscal comparison period and, where the query supports date windows, the prior-year comparison period: `percentage-unallocated-costs`, `percentage-untagged-costs`, `tagging-policy-compliance`, `allocation-accuracy-index`, `anomaly-detection-rate`, `anomaly-variance-total`, `monthly-cost-trend`, `cost-visibility-delay`, `data-update-frequency`, `macc-consumption-vs-commitment`, `ai-token-usage-breakdown`, `ai-daily-trend`, `ai-model-cost-comparison`, `commitment-utilization-score`, `compute-cost-per-core`, `savings-summary-report`, `compute-spend-commitment-coverage`, `commitment-discount-waste`, `cost-optimization-index`, `storage-tier-distribution`, and `cost-per-gb-stored`. + + Build the year-over-year KPI scorecard from those outputs. Separate direct KPI measurements from derived executive interpretation, and classify unavailable KPI rows as data sufficiency limitations with the exact source tool named. + + + **Checkpoint:** ALL data collected. Proceed to 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, ask the selected delivery specialist to generate charts for key findings using `PlotBarChart`, `PlotPieChart`, or `PlotAreaChartWithCorrelation` when those visualization tools fit the data. Use `ExecutePythonCode` or matplotlib/seaborn via the code interpreter only when the visualization tools are unavailable or cannot express the required chart. 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 the chart images inline in the Teams message and attach or embed them in the Outlook email alongside the text tables. Charts make the report immediately actionable for stakeholders who scan rather than read. + + **Teams and Outlook (financial results only):** Post the final report to our Teams channel. Include the year-over-year scorecard, forecast scenarios, and planning recommendations. Do not post intermediate results — only the completed report. Ask the selected delivery specialist to use `PostTeamsMessage` and `SendOutlookEmail` only when the delivery guard found each tool available once per run. If neither Teams nor Outlook delivery is configured or available, return the completed report in the run output with a note naming the skipped delivery paths. If any configured Teams or Outlook 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 Teams and Outlook delivery only.' diff --git a/src/templates/sre-agent/recipes/finops-hub/config/built-in-tools.json b/src/templates/sre-agent/recipes/finops-hub/config/built-in-tools.json new file mode 100644 index 000000000..bfd49cc2f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/built-in-tools.json @@ -0,0 +1,40 @@ +{ + "overrides": [ + { + "name": "QueryLogAnalyticsByWorkspaceId", + "enabled": true + }, + { + "name": "QueryAppInsightsByAppId", + "enabled": true + }, + { + "name": "QueryAppInsightsByResourceId", + "enabled": true + }, + { + "name": "QueryLogAnalyticsByResourceId", + "enabled": true + }, + { + "name": "PlotScatter", + "enabled": true + }, + { + "name": "PlotHeatmap", + "enabled": true + }, + { + "name": "PlotBarChart", + "enabled": true + }, + { + "name": "PlotPieChart", + "enabled": true + }, + { + "name": "PlotAreaChartWithCorrelation", + "enabled": true + } + ] +} diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/SKILL.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/SKILL.md new file mode 100644 index 000000000..0b0af57a3 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/SKILL.md @@ -0,0 +1,248 @@ +--- +name: azure-capacity-management +description: | + This skill should be used when the user asks about Azure capacity management, quota + operations, or capacity planning for SaaS ISVs running workloads in their own Azure + subscriptions under EA or MCA. Relevant queries include: how to increase VM quota across + subscriptions, how quota groups work, how to create or share capacity reservation groups, + what the difference is between capacity reservations and Azure Reservations or savings plans, + how to request region access or zonal enablement, how logical and physical availability zones + map across subscriptions, how to configure quota or budget or anomaly alerts, how AKS node + pools interact with capacity reservations, how to manage non-compute quotas, how deployment + stamps map to FinOps Planning & Estimating, Forecasting, Architecting & Workload Placement, + Usage Optimization, and Governance, Policy & Risk, and how Azure capacity controls support + estate-level governance. Also covers quota transfers, overallocation, SKU restrictions, CRG + sharing, billing hierarchy, and subscription vending. +--- + +# Azure capacity management + +Estate-level capacity and quota management for SaaS ISVs operating workloads in subscriptions they own or control under an [Enterprise Agreement (EA)](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/azure-billing-enterprise-agreement) or [Microsoft Customer Agreement (MCA)](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/azure-billing-microsoft-customer-agreement). This skill aligns with the [ISV landing zone guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) and covers pure SaaS and [stamp-based isolation](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) patterns where customers are isolated through dedicated or shared deployment stamps inside the ISV's Azure estate. + +Read the full Azure implementation reference at `references/docs/operations/capacity-and-quotas/README.md`. + +## FinOps capability mapping + +Treat Azure capacity management as implementation detail under the canonical FinOps Framework. Capacity evidence commonly supports Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization, Budgeting, Governance, Policy & Risk, and Automation, Tools & Services. Use [Well-Architected capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [reliable scaling](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling), and [workload supply chain guidance](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) as Azure implementation guidance, not as a replacement framework. [Source](https://www.finops.org/framework/) [Source](https://www.finops.org/framework/domains/) + +| FinOps capability | Capacity question | Azure surfaces | +|------|-------------|----------------| +| Planning & Estimating | What capacity does the planned workload, scenario, or stamp need? | Workload requirements, scale-unit assumptions, Azure Monitor, estimates, FinOps budgets | +| Forecasting | When will demand exceed quota, region, SKU, zone, or reserved-capacity headroom? | Historical usage, growth trends, forecast breach dates, capacity planning models | +| Architecting & Workload Placement | Which regions, zones, SKUs, quota pools, or deployment patterns should change? | SKU availability, region access, quota groups, capacity reservation groups, CRG sharing, overallocation | +| Usage Optimization | Which allocated capacity, quota, or deployment pattern is underused or inefficient? | Utilization, headroom, rightsizing evidence, unused reserved capacity, demand signals | +| Rate Optimization | Where should capacity guarantees be coordinated with reservations or savings plans? | Benefit recommendations, commitment utilization, CRG utilization, pricing evidence from FinOps Hub | +| Governance, Policy & Risk | Which capacity risks need ownership, exception review, or escalation? | Approved regions and SKUs, owner metadata, risk thresholds, exception status | +| Automation, Tools & Services | Which controls expose capacity risk before deployment or scale events? | Quota alerts, budget alerts, anomaly alerts, CI/CD gates, workflow status | + +Read `references/docs/operations/capacity-planning/README.md` for forecasting details and `references/docs/operations/capacity-governance/README.md` for the governance program design. + +## Quota operations + +Azure assigns [default quota limits](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) per subscription. EA subscriptions typically start with 350 cores; pay-as-you-go subscriptions start with 20 cores. Some VM series have offer restrictions that block deployment until you request access. + +Key workflows: +- **Region access:** Submit a [region access support request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) when a subscription can't deploy to a restricted region. +- **Zonal enablement:** Submit a [zonal enablement request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) for restricted VM series in specific availability zones. +- **Quota increases:** Use [per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) for individual subscriptions or [quota group limit increases](https://learn.microsoft.com/en-us/azure/quotas/quota-group-limit-increase) for grouped subscriptions. + +CLI reference: +```bash +# List quota usage for a subscription +az quota usage list --scope /subscriptions/{sub-id}/providers/Microsoft.Compute/locations/{location} + +# Request a quota increase +az quota create --resource-name "StandardDSv3Family" --scope /subscriptions/{sub-id}/providers/Microsoft.Compute/locations/{location} --limit-object value=500 +``` + +Read `references/docs/operations/quota/README.md` for the complete quota operations reference. + +## Quota groups + +[Quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) are ARM objects that aggregate compute quota across eligible subscriptions at the management group scope. They reduce stranded VM-family headroom and let you request group-level increases. + +**Prerequisites:** Register the `Microsoft.Quota` resource provider on each member subscription. The management group must exist before creating the quota group. + +**Limitations:** +- IaaS compute only — doesn't cover storage, networking, or PaaS services +- A subscription can belong to a single quota group at a time ([source](https://learn.microsoft.com/en-us/azure/quotas/quota-groups)) +- Doesn't grant region or zone access — those require separate support requests +- [Quota transfers](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups) move allocation between member subscriptions but don't change the group total + +**Lifecycle:** Create the quota group under a management group, [add subscriptions](https://learn.microsoft.com/en-us/azure/quotas/add-remove-subscriptions-quota-group), then request group-level limit increases. Monitor allocation snapshots and transfer as demand shifts between subscriptions. + +Read `references/docs/operations/quota-groups/README.md` for the complete reference including ARM lifecycle, transfer mechanics, and monitoring integration. + +## Capacity reservations + +[Capacity reservation groups (CRGs)](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) guarantee compute capacity for specific VM sizes in a region or availability zone. CRGs are capacity guarantees, not pricing commitments — unused reserved capacity is billed at the pay-as-you-go rate for the VM size. + +**Cost implications:** Reserved capacity is billed whether or not VMs run against it. Pair CRGs with Azure Reservations or savings plans to get both capacity guarantee and pricing discount. + +**Sharing (preview):** CRGs can be [shared across subscriptions](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share) within the same tenant. The ODCR owner in the consumer subscription needs `Microsoft.Compute/capacityReservationGroups/share/action`. The VM owner in the consumer subscription needs `Microsoft.Compute/capacityReservationGroups/read`, `Microsoft.Compute/capacityReservationGroups/deploy`, `Microsoft.Compute/capacityReservationGroups/capacityReservations/read`, and `Microsoft.Compute/capacityReservationGroups/capacityReservations/deploy`. Portal support isn't available in preview; use CLI, PowerShell, or REST API. + +**Overallocation:** [Overallocation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) lets you deploy more VMs than the reserved quantity. Excess VMs don't have capacity guarantees but benefit from the reservation when capacity is available. + +**Zone alignment:** CRGs are zone-specific. Before sharing across subscriptions, verify logical-to-physical zone mapping with the `Get-AzAvailabilityZoneMapping.ps1` script — [logical zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) can map to different physical zones across subscriptions. + +Read `references/docs/operations/capacity-reservations/README.md` for the complete reference including automation patterns (REST API, Bicep, Terraform). + +## AKS capacity governance + +AKS node pools consume VM quota and can associate with capacity reservation groups, but with constraints specific to the AKS lifecycle: + +- **Node pool CRG association** happens at creation time — you can't associate an existing node pool with a CRG after the fact +- **Identity requirement:** The AKS cluster must use a [user-assigned managed identity](https://learn.microsoft.com/en-us/azure/aks/use-managed-identity) with `Microsoft.Compute/capacityReservationGroups/read` permission on the CRG +- **Disassociation** removes the CRG association but doesn't delete the node pool +- **Zone alignment** matters for cross-subscription CRG sharing — verify physical zone mapping before configuring AKS node pools against shared CRGs + +Read `references/docs/operations/aks-capacity/README.md` for the complete reference including Bicep and Terraform examples. + +## Non-compute quotas + +Storage accounts, App Service plans, Cosmos DB throughput, Service Bus namespaces, Key Vault transactions, and other services have their own quota limits outside the compute quota system. Quota groups don't cover these — manage them through standard [quota requests](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests) and service-specific scaling controls. + +Read `references/docs/operations/non-compute-quotas/README.md` for service-specific quota references. + +## Monitoring and governance + +Three alert types cover the capacity governance space: + +1. **Quota alerts:** [Azure Monitor alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) triggered when quota usage crosses a configured threshold. Requires Reader or higher on the subscription. +2. **Budget alerts:** [Cost Management alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending) triggered when actual or forecasted spend exceeds budget thresholds. Uses Cost Management RBAC. +3. **Anomaly alerts:** Cost Management anomaly detection that flags unexpected spending patterns. Deploy with `references/scripts/anomaly-alerts/Deploy-AnomalyAlert.ps1` or bulk deploy with `references/scripts/anomaly-alerts/Deploy-BulkALZ.ps1`. + +**Governance cadence:** Monthly quota reviews, quarterly capacity planning cycles, and post-incident reviews when scaling events fail. Read `references/docs/operations/monitoring-alerting/README.md` for alert configuration details and `references/docs/operations/capacity-governance/README.md` for the governance program design. + +## Scripts quick reference + +| Script | Path | Purpose | +|--------|------|---------| +| Get-AzVMQuotaUsage.ps1 | `references/scripts/quota/` | Multi-threaded quota analysis across subscriptions | +| Show-AzVMQuotaReport.ps1 | `references/scripts/quota/` | Single-threaded quota reporting | +| Get-AzAvailabilityZoneMapping.ps1 | `references/scripts/quota/` | Logical-to-physical zone mapping | +| Get-BenefitRecommendations.ps1 | `references/scripts/rate/` | Reservation and savings plan recommendations | +| Deploy-AnomalyAlert.ps1 | `references/scripts/anomaly-alerts/` | Deploy cost anomaly alerts | +| Deploy-BulkALZ.ps1 | `references/scripts/anomaly-alerts/` | Bulk deploy anomaly alerts | +| Deploy-Budget.ps1 | `references/scripts/budgets/` | Deploy individual budgets | +| Deploy-BulkBudgets.ps1 | `references/scripts/budgets/` | Bulk deploy budgets | +| Suppress-AdvisorRecommendations.ps1 | `references/scripts/advisor/` | Suppress Advisor recommendations | +| Serverless SQL workbook | `references/scripts/serverless-sql-storage/` | Azure Monitor workbook for serverless SQL allocated vs. used storage; identifies databases worth shrinking to reclaim billing waste | + +Read the README in each script directory for parameter requirements and prerequisites. + +## Key distinctions + +These are commonly confused — keep them separated: + +- **Capacity reservation vs Azure Reservation vs savings plan:** [Capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) guarantee compute supply. [Azure Reservations](https://www.finops.org/framework/capabilities/rate-optimization/) and [savings plans](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/) provide pricing discounts. Capacity guarantees supply; pricing commitments reduce cost. +- **Quota group vs management group:** [Quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) aggregate compute quota. Management groups organize subscriptions for RBAC and policy. Quota groups are created under management groups but don't inherit their policy or access controls. +- **Logical vs physical zone:** [Logical zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) are subscription-specific labels. Physical zones are datacenter locations. Zone 1 in subscription A may map to a different physical zone than zone 1 in subscription B. +- **Region access vs quota increase:** Quota increases raise limits within an already-enabled region. [Region access requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) unblock a restricted region for the subscription. + +## Operational agent + +The `azure-capacity-manager` agent is a capacity evidence specialist for FinOps workflows. It handles operational tasks like quota analysis and reservation evaluation, maps Azure capacity evidence back to FinOps capabilities, has access to the same references, and can run scripts and `az` commands for live operations. + +## Documentation map + +| Domain | Reference path | +|--------|---------------| +| Azure capacity reference | `references/docs/operations/capacity-and-quotas/README.md` | +| Glossary | `references/docs/operations/glossary.md` | +| Quota operations | `references/docs/operations/quota/README.md` | +| Quota groups | `references/docs/operations/quota-groups/README.md` | +| Capacity reservations | `references/docs/operations/capacity-reservations/README.md` | +| AKS capacity | `references/docs/operations/aks-capacity/README.md` | +| Non-compute quotas | `references/docs/operations/non-compute-quotas/README.md` | +| Monitoring and alerting | `references/docs/operations/monitoring-alerting/README.md` | +| Capacity governance | `references/docs/operations/capacity-governance/README.md` | +| Capacity planning | `references/docs/operations/capacity-planning/README.md` | +| Billing (EA) | `references/docs/billing/legacy/README.md` | +| Billing (MCA) | `references/docs/billing/modern/README.md` | +| Deployment patterns | `references/docs/deployment/README.md` | +| Tools and scripts | `references/docs/operations/tools-scripts/README.md` | +| Quota scripts | `references/scripts/quota/README.md` | +| Anomaly alerts | `references/scripts/anomaly-alerts/README.md` | +| Budgets | `references/scripts/budgets/README.md` | +| Rate optimization | `references/scripts/rate/README.md` | +| Serverless SQL storage | `references/scripts/serverless-sql-storage/README.md` | + +## Notification procedures + + +The capacity manager can send alerts and reports through [SRE Agent's notification connectors](https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications). Use Teams for urgent capacity events and email for periodic reports. + +### Teams channel alerts + +Use Teams notifications for urgent capacity events that need immediate attention, following the [Azure SRE Agent notification guidance](https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications). The SRE Agent sends messages through the Teams connector in HTML format, so keep the payload compact, scannable, and action-oriented, as described in [Send notifications from Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications). + +**When to send Teams alerts:** +- Quota utilization exceeds 80 percent for any VM family in a production region. +- VM deployment fails with a quota or capacity error. +- Capacity reservation group utilization drops below 50 percent, which points to wasted spend. +- Quota group is depleted or nearly depleted. +- Region access or zonal enablement request is needed. + +**Teams alert template:** + +Use this HTML template when you compose a Teams message. Include alert severity, the affected resource, current state, the recommended action, and links to the relevant Azure portal blades, following [Send notifications from Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications). + +```html +

⚠️ Quota utilization alert

+

Severity: warning
+VM family: Standard_D_v5
+Region: eastus
+Subscription: prod-001
+Usage: 85/100 vCPUs (85%)

+

Recommended action: Request a quota increase before the next deployment cycle.

+

+ Open quota blade
+ Open capacity reservation groups +

+``` + +### Email capacity reports + +Use Outlook email for periodic capacity reports and non-urgent recommendations, following the same [Azure SRE Agent notification guidance](https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications). The SRE Agent sends email through the Outlook connector in HTML format, so structure the message for scanning first, and detailed follow-up second. + +**When to send email reports:** +- Weekly capacity digest, including quota utilization trends across the estate. +- Reservation expiring within 30 days, including a renewal recommendation and cost analysis. +- Monthly savings plan optimization review. +- Quarterly capacity planning summary. +- Post-incident capacity analysis. + +**Email report template:** + +Use this HTML template for email reports. Include the report title, date range, a summary table of key metrics, highlighted findings, recommended actions with priority, and links to Azure portal resources or supporting documentation, as described in [Send notifications from Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications). + +```html +

Weekly capacity digest

+

Period: March 3–9, 2026
+Subscriptions analyzed: 47

+

Key findings

+ + + + +
RegionVM familyUtilizationAction
eastusStandard_D_v592%Request increase
westus2Standard_E_v578%Monitor
+

Recommendations

+
    +
  1. Request quota increase for Standard_D_v5 in eastus (critical)
  2. +
  3. Review CRG utilization in westeurope—45% utilized, consider right-sizing
  4. +
  5. Savings plan expires April 15—evaluate renewal versus pay-as-you-go
  6. +
+

+ Open quota blade
+ Review quota monitoring guidance +

+``` + +### Scheduled notification patterns + +Configure recurring notifications with [Azure SRE Agent scheduled tasks](https://learn.microsoft.com/en-us/azure/sre-agent/scheduled-tasks), and pair the schedule with the connector flow from [Send notifications from Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications). + +- Daily: quota utilization check, then send a Teams alert if any family is above 80 percent. +- Weekly: capacity digest, then send email to stakeholders. +- Monthly: savings plan and reservation review, then send email with recommendations. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/README.md new file mode 100644 index 000000000..a7ae1665e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/README.md @@ -0,0 +1,16 @@ +--- +title: Billing models +nav_order: 3 +has_children: true +--- + +# Billing models + +Use this section to navigate between Microsoft Customer Agreement (modern) and Enterprise Agreement (legacy) billing guidance: + +- [`modern/`](modern/README.md)—MCA billing hierarchy, automation, and reservation scope guidance. [Source](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/azure-billing-microsoft-customer-agreement) +- [`legacy/`](legacy/README.md)—Enterprise Agreement subscription creation and quota considerations. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) + +Azure enterprise customers typically operate under either the historic Enterprise Agreement (EA) or the modern Microsoft Customer Agreement (MCA). Both contracts deliver the same enterprise-grade commitment, but the MCA streamlines administration and automation workflows according to [MCA billing guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/azure-billing-microsoft-customer-agreement). + +This repo treats EA and MCA customers identically for capacity planning guidance. EA customers can continue to use existing constructs, while MCA customers gain the same enterprise controls plus easier automation and tenant alignment. For an estate-level view that connects these billing models to subscription vending, quota operations, capacity reservations, and automation, see [capacity and billing operations](../operations/capacity-and-quotas/README.md). diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/legacy/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/legacy/README.md new file mode 100644 index 000000000..5d76a19c2 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/legacy/README.md @@ -0,0 +1,33 @@ +--- +title: Enterprise Agreement +parent: Billing models +nav_order: 2 +--- + +# Enterprise Agreement billing model + +## Contract and hierarchy + +- The [Enterprise Agreement (EA)](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/understand-ea-roles) uses a hierarchical structure—enrollment > departments > accounts > subscriptions—managed in the Azure Cost Management portal. +- [Enterprise Administrators](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/understand-ea-roles) control enrollment-level settings, assign Department Administrators and Account Owners, and can provision new subscriptions under any [active account](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/direct-ea-administration). + +## Billing administration tasks + +- [EA administrators](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/direct-ea-administration) manage their enrollment directly in the Azure portal: they select the billing scope, activate the enrollment, adjust policies (for example, dev/test enablement, AO/DA view charges), and configure authentication requirements for account owners. +- [Departments](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/understand-ea-roles) allow cost segmentation and quota/budget controls, while accounts own the subscriptions and surface usage/cost reports for their scope. + +## Subscription provisioning and tenant placement + +- Enterprise Administrators or Account Owners can [create EA subscriptions](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-enterprise-subscription) either for themselves or on behalf of another user, choosing the subscription directory (tenant) during creation and specifying additional subscription owners, including service principals via App IDs. +- [Cross-tenant provisioning](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-enterprise-subscription) is supported: the owner in the target tenant receives an acceptance request before the subscription is finalized. + +## Automation and service principals + +- EA exposes a dedicated [**SubscriptionCreator** role](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/assign-roles-azure-service-principals) for service principals so automation can create subscriptions at the account scope. +- [Automating EA actions](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/assign-roles-azure-service-principals) requires registering a Microsoft Entra application, capturing the service principal object ID, and assigning the desired EA role (for example, SubscriptionCreator or EnrollmentReader) via the EA REST API or PowerShell before calling subscription APIs. + +## Governance, Policy & Risk + +- [Enrollment policies](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/direct-ea-administration#view-and-manage-enrollment-policies) let administrators control who can create subscriptions (authorization levels: Microsoft Account only, Work/School only, cross-tenant) and whether dev/test offers are available to account owners. +- [EA billing roles](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/understand-ea-roles) must be assigned to individual identities (not groups) to ensure compliance and traceability; each user should have a monitored email for notifications so requests don't go unnoticed. + diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/modern/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/modern/README.md new file mode 100644 index 000000000..3677e3bf2 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/modern/README.md @@ -0,0 +1,109 @@ +--- +title: Microsoft Customer Agreement +parent: Billing models +nav_order: 1 +--- + +# Microsoft Customer Agreement billing model + +## Contract overview + +- The [Microsoft Customer Agreement (MCA)](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/understand-mca-roles) is the modern Azure commerce platform that delivers the same enterprise-grade billing foundation as legacy Enterprise Agreements while simplifying contracting and ongoing administration. +- It's anchored to a single Microsoft Entra tenant, but billing owners can [associate additional tenants and link subscriptions across directories](https://learn.microsoft.com/en-us/azure/cost-management-billing/microsoft-customer-agreement/manage-tenants) without changing their resource tenancy. + +## Why ISVs move from pay-as-you-go to MCA + +- Pay-as-you-go subscriptions invoice separately, each with its own payment method, which forces you to manage multiple credit cards for production workloads at scale. +- MCA consolidates subscriptions under a single invoice payable via [wire transfer or ACH](https://learn.microsoft.com/en-us/azure/cost-management-billing/understand/pay-bill), removing the need to attach credit cards to individual subscriptions. +- MCA for enterprise supports [up to 10,000 subscriptions](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/view-all-accounts) under a single billing account, while pay-as-you-go limits you to 5 subscriptions per account. + +## Billing hierarchy + +- The [MCA billing hierarchy](https://learn.microsoft.com/en-us/azure/cost-management-billing/understand/mca-overview) flows from the billing account to billing profiles, invoice sections, and down to subscriptions, as illustrated in the official diagram below. You'll see the scopes you need to automate clearly labeled in that reference image. + +![Microsoft Customer Agreement billing hierarchy diagram](https://learn.microsoft.com/en-us/azure/cost-management-billing/understand/media/mca-overview/mca-billing-hierarchy.png) + +## Structuring costs with billing profiles and invoice sections + +- Billing profiles correspond to individual invoices and payment methods. Each [invoice section](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/mca-section-invoice) under a billing profile groups the charges that appear on that invoice, giving fine-grained cost segmentation for departments, environments, or projects. +- Billing profile owners or contributors can [create additional invoice sections](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/mca-section-invoice) directly in **Cost Management + Billing** to mirror the organization's cost centers or workload boundaries. +- Billing profiles also define the shared scope boundary for [Azure Reservations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/prepare-buy-reservation#scope-reservations) and [Savings Plans](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/scope-savings-plan) in an MCA, so shared benefits only apply to eligible subscriptions that stay within the same billing profile context. + +## Automation and service principals + +- Any automation account or pipeline that needs to [create subscriptions](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) must hold the **Azure subscription creator** role (or owner/contributor) on the target invoice section, billing profile, or billing account—otherwise it can't submit alias requests successfully. +- Microsoft's [subscription-request workflow](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-subscription-request) allows selecting a service principal as the subscription owner by pasting its App (client) ID, confirming that service principals are first-class identities for billing operations. +- To onboard a service principal for subscription creation: + 1. Ensure the service principal exists in the Microsoft Entra tenant associated with the billing account (or an associated tenant). + 2. Assign the service principal the **Azure subscription creator** role on the desired invoice section so it can create subscriptions under that scope. The [official guidance](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) explicitly notes that the same billing roles can be granted to service principals. + 3. When triggering a [subscription request](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-subscription-request) or calling the [subscription-creation APIs](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest), use the service principal's object ID/App ID; the portal experience accepts the value when you add it as a subscription owner. + +## Multi-tenant considerations + +- Billing owners can create subscriptions in any tenant they have associated with the MCA billing account, and they can [transfer billing ownership of existing subscriptions](https://learn.microsoft.com/en-us/azure/cost-management-billing/microsoft-customer-agreement/manage-tenants) without moving the underlying resources. +- Guest users or associated tenants can be [granted billing roles](https://learn.microsoft.com/en-us/azure/cost-management-billing/microsoft-customer-agreement/manage-tenants) so finance teams in other directories can manage invoice sections or run automation without duplicating subscriptions. + +## Rate optimization through commitments + +### MACC (Microsoft Azure Consumption Commitment) + +- A [Microsoft Azure Consumption Commitment (MACC)](https://learn.microsoft.com/en-us/marketplace/azure-consumption-commitment-benefit) is a contractual commitment to spend a specific amount on Azure over a defined period. +- Eligible Azure services and Marketplace purchases automatically count toward fulfillment, and you don't need to manually track which resources apply; however, [not all Azure Marketplace purchases count toward MACC](https://learn.microsoft.com/en-us/marketplace/azure-consumption-commitment-benefit)—verify eligibility for third-party offerings. +- You can [track your MACC progress](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/track-consumption-commitment) directly in the Azure portal under Cost Management + Billing. + +### ACD (Azure Commitment Discount) + +- Azure Commitment Discount (ACD) is a negotiated percentage discount on pay-as-you-go rates for customers with a MACC agreement. +- The discount percentage isn't fixed—it's negotiated as part of your agreement. +- When you purchase a Savings Plan, Azure [automatically applies whichever discount is better](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/discount-application)—the ACD rate or the Savings Plan rate—so you always get the optimal price. + +### Negotiated pricing + +- Organizations can negotiate discounts on their [customer price sheet](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/ea-pricing-overview), which sets custom rates for specific Azure services. +- [Reservation prices can be negotiated](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/ea-portal-vm-reservations) separately from ACD, and ACD doesn't stack on top of reservations—reservations have their own negotiated rates. + +### Visibility through FinOps Hubs + +- Use [FinOps Hubs](https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) to monitor commitment discount utilization and realized savings across your estate; [FinOps Hubs requires deployment](https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) before queries are available. +- The [rate optimization capability](https://www.finops.org/framework/capabilities/rate-optimization/) in the FinOps Framework provides guidance on when to use reservations versus savings plans based on workload stability. +- FinOps Hubs exposes commitment tracking queries documented in the [FinOps Toolkit query index](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md). + +### Capacity allocation as a rate optimization signal + +Capacity reservations and quota staging signal when to evaluate rate optimization purchases. + +> **Warning: Use Cost Management + Billing for enterprise-wide reservation recommendations.** +> [Azure Advisor rightsizing and shutdown recommendations use retail (pay-as-you-go) prices](https://learn.microsoft.com/en-us/azure/advisor/advisor-cost-recommendations), not your negotiated rates. Advisor's reservation recommendations do use negotiated pricing, but Advisor is limited to single-subscription scope. [Cost Management + Billing reservation recommendations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/reserved-instance-purchase-recommendations) see all VMs across your billing account and use your [negotiated price sheet](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/ea-pricing). Navigate to **Reservations > Add** in Cost Management + Billing to get accurate, estate-wide savings estimates. + +**Review usage before purchasing:** + +- [Reservation recommendations are calculated](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/reserved-instance-purchase-recommendations) by evaluating hourly usage over the past 7, 30, and 60 days. Select **See details** on any recommendation to view the usage breakdown chart. +- Review the 60-day usage pattern before purchasing. Usage spikes, seasonal patterns, and planned changes aren't reflected in the recommendation math. +- Consider purchasing 80% of the recommended quantity rather than 100%. The recommendation maximizes theoretical savings assuming consistent future usage—your actual usage will vary. + +**Evaluate before you allocate:** + +- When you create a capacity reservation or increase quota for a new workload, use that as a trigger to run the [reservation-recommendation-breakdown query](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md) to evaluate whether a reservation or savings plan makes sense. +- The query calculates `x_BreakEvenMonths` based on trailing usage patterns, which [may not account for upcoming business changes](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/reserved-instance-purchase-recommendations) like customer churn, planned migrations, or architecture pivots. +- [Savings plans can't be canceled or exchanged](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/cancel-savings-plan)—purchases are final. [Reservations allow unlimited exchanges and $50,000/year in refunds](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/exchange-and-refund-azure-reservations) for most services, but [some reservations can't be exchanged or refunded](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/exchange-and-refund-azure-reservations#products-that-cant-be-exchanged-or-refunded) (Databricks, Synapse pre-purchase, Red Hat, SUSE, Defender for Cloud, Sentinel). +- 3-year commitments offer deeper discounts than 1-year terms; use the [Azure Pricing Calculator](https://azure.microsoft.com/en-us/pricing/calculator/) to calculate specific rates for your SKUs and regions. Consider deferring long-term commitments for workloads you'll migrate to PaaS or serverless before the term expires. + +**Monitor and rebalance:** + +- Run [commitment-discount-utilization](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md) monthly to track coverage. High on-demand percentage indicates optimization headroom; your target ratio depends on workload stability and customer retention patterns. +- Set [reservation utilization alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/reservation-utilization-alerts) based on your reservation cost and workload volatility. Lower thresholds catch waste earlier but may trigger false alerts for seasonal workloads. +- Use [savings-summary-report](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md) to calculate your effective savings rate (ESR). Track ESR month-over-month as a portfolio health indicator. +- When customer churn exceeds forecast or you decommission stamps, [exchange reservations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/exchange-and-refund-azure-reservations) for different SKUs or regions within the same product family—there's no penalty for exchanges. +- For ISVs with stamp-based isolation, verify [reservation scope](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/prepare-buy-reservation#scope-reservations) (single subscription, resource group, or shared) before rescoping. Shared scope applies reservations across all subscriptions in your billing context, which can cross customer isolation boundaries and distort per-stamp cost attribution. + +**What Azure doesn't know:** + +Azure's recommendations are based on your trailing usage. They don't account for: + +- **Customer lifecycle**: A stamp serving a customer whose contract expires in 6 months shouldn't get a 3-year reservation. +- **Planned decommissioning**: Workloads migrating to serverless or a different region will break utilization assumptions. +- **Growth plans**: Historical usage doesn't reflect the customer win you're onboarding next quarter. +- **Cash flow constraints**: 3-year NPV might be better, but your CFO cares about runway. +- **Architecture pivots**: If you're evaluating a move from IaaS to AKS, consider deferring VM reservations until architecture stabilizes. + +Treat recommendations as math, not strategy. You own the business context that makes the decision. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/toc.yml b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/toc.yml new file mode 100644 index 000000000..5f453c85d --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/billing/toc.yml @@ -0,0 +1,91 @@ +- name: Capacity management + href: ../operations/README.md + items: + - name: Overview + href: ../operations/README.md + - name: Planning and forecasting + href: ../operations/capacity-planning/README.md + items: + - name: Capacity and billing operations + href: ../operations/capacity-and-quotas/README.md + - name: Capacity planning + href: ../operations/capacity-planning/README.md + - name: Customer isolation + href: ../deployment/README.md + items: + - name: Overview + href: ../deployment/README.md + - name: Single-tenant + href: ../deployment/single-tenant/README.md + - name: Multi-tenant + href: ../deployment/multi-tenant/README.md + - name: Workload placement + href: ../operations/quota/README.md + items: + - name: Billing models + href: README.md + items: + - name: Overview + href: README.md + - name: Microsoft Customer Agreement + href: modern/README.md + items: + - name: Overview + href: modern/README.md + - name: Subscription creation (billing scope) + href: ../operations/modern/README.md + - name: Enterprise Agreement + href: legacy/README.md + items: + - name: Overview + href: legacy/README.md + - name: Subscription creation (billing scope) + href: ../operations/legacy/README.md + - name: Quota operations + href: ../operations/quota/README.md + - name: Capacity reservations + href: ../operations/capacity-reservations/README.md + - name: AKS capacity governance + href: ../operations/aks-capacity/README.md + - name: Non-compute quotas + href: ../operations/non-compute-quotas/README.md + - name: Usage and rate optimization + href: ../operations/quota-groups/README.md + items: + - name: Quota groups + href: ../operations/quota-groups/README.md + - name: Governance and automation + href: ../operations/monitoring-alerting/README.md + items: + - name: Monitoring and alerting + href: ../operations/monitoring-alerting/README.md + - name: Capacity governance + href: ../operations/capacity-governance/README.md + - name: Tools and scripts + href: ../operations/tools-scripts/README.md + items: + - name: Overview + href: ../operations/tools-scripts/README.md + - name: Get-AzVMQuotaUsage.ps1 + href: ../operations/tools-scripts/get-azvmquotausage.md + - name: Get-AzAvailabilityZoneMapping.ps1 + href: ../operations/tools-scripts/get-azavailabilityzonemapping.md + - name: Show-AzVMQuotaReport.ps1 + href: ../operations/tools-scripts/show-azvmquotareport.md + - name: Get-BenefitRecommendations.ps1 + href: ../operations/tools-scripts/get-benefitrecommendations.md + - name: Deploy-AnomalyAlert.ps1 + href: ../operations/tools-scripts/deploy-anomalyalert.md + - name: Deploy-BulkALZ.ps1 + href: ../operations/tools-scripts/deploy-bulkalz.md + - name: Support and reference + href: ../operations/support-and-reference/README.md + items: + - name: Overview + href: ../operations/support-and-reference/README.md + - name: Support escalation + href: ../operations/escalation/README.md + - name: Tenant hygiene + href: ../operations/tenant-hygiene/README.md + - name: Glossary + href: ../operations/glossary.md diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/README.md new file mode 100644 index 000000000..392c9e5f0 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/README.md @@ -0,0 +1,23 @@ +--- +title: Customer isolation +nav_order: 2 +has_children: true +--- + +# Customer isolation + +ISVs commonly operate on two primary delivery models highlighted in [Microsoft's landing zone guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone). The [ISV landing zone documentation](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) describes pure SaaS, customer-deployed, and dual-deployment SaaS models and how they relate to Azure landing zones. This folder focuses on pure SaaS scenarios where the ISV controls subscriptions and isolates customers through dedicated or shared deployment stamps. + +![SaaS Deployment Topologies showing single-tenant subscription isolation vs multi-tenant shared infrastructure](../images/deployment-topologies.svg) + +- Dedicated (single-tenant) environments where each customer receives an isolated subscription and landing zone. +- Shared (multi-tenant) SaaS platforms that centralize control planes while segmenting tenant workloads through application logic and deployment stamps. + +The [ISV landing zone guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) identifies three common deployment models—pure SaaS, customer-deployed, and dual-deployment SaaS—each with different landing zone implications. The guides in this directory show how those models map to Azure subscription and landing zone design so you can adopt the pattern that fits your product roadmap. + +Use this directory to choose the isolation model that aligns with your product offering: + +- [Single-tenant deployment guide](single-tenant/README.md)—guidance for customer-isolated deployments, including subscription vending, landing zone setup, and availability zone alignment. +- [Multi-tenant deployment guide](multi-tenant/README.md)—guidance for centrally operated SaaS solutions that rely on shared infrastructure, deployment stamps, and multitenant governance. + +Before diving into either model, review the [Azure landing zone guidance tailored for ISVs](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone). It shows how to structure management groups, subscriptions, and shared services to support both deployment approaches. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/multi-tenant/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/multi-tenant/README.md new file mode 100644 index 000000000..00be0dba9 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/multi-tenant/README.md @@ -0,0 +1,71 @@ +--- +title: Multi-tenant +parent: Customer isolation +nav_order: 2 +--- + +# Multi-tenant deployment guide + +Use this guide for SaaS offerings where multiple customers share a centralized platform. The [ISV landing zone guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) describes these scenarios as "pure SaaS" or "dual-deployment" models, depending on whether customers also receive dedicated environments. You'll use this guide to plan tenant isolation, deployment stamps, and supporting services that maintain reliability at scale. + +![Multi-Tenant SaaS Architecture showing shared infrastructure with application-level isolation](../../images/multi-tenant-topology.svg) + +## Critical subscription service limits + +**WARNING**: [Azure subscription service limits](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits) impose hard architectural constraints that affect multi-tenant designs far more than quota limits. These aren't negotiable—they're platform constraints that dictate your architecture: + +### Storage account limits +- **250 storage accounts per region per subscription** (500 with support request) +- If you isolate customer data in separate storage accounts, you hit a scaling wall at 250 tenants +- Each storage account limited to 40,000 requests/second in primary regions +- Maximum 200 private endpoints per storage account limits network isolation options + +### Resource group constraints +- **980 resource groups per subscription maximum** +- **800 resources per resource group, per resource type** +- If you use resource groups for tenant isolation, you're capped at 980 tenants per subscription +- Tagged resource groups further reduce this limit + +### Compute and app limits +- **100 App Service plans per resource group** +- **100 function apps per subscription** (Consumption tier) +- **512 access restriction rules per app** constrains IP-based tenant isolation + +### Network isolation boundaries +- **200 private endpoints per app service** +- Virtual network limits vary by region and subscription type +- Network security group rules have regional caps + +These [service limits](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits) force critical architectural decisions from day one. Unlike quotas that you can request increases for, these are immutable platform constraints that determine whether you need multiple subscriptions, how you isolate tenants, and when you must implement deployment stamps. + +## Architectural principles + +- **Balance all Well-Architected pillars.** SaaS providers must design for reliability, security, cost, operations, and performance simultaneously. Evaluate trade-offs for each release and document how they affect [tenant experience](https://learn.microsoft.com/en-us/azure/well-architected/saas/design-principles). +- **Adopt Zero Trust and least privilege.** Isolate tenants through identity, networking, and data segmentation. Combine resource-level controls with application-layer enforcement to prevent [cross-tenant leakage](https://learn.microsoft.com/en-us/azure/well-architected/saas/design-principles). +- **Design within service limits.** Unlike quotas, [subscription service limits](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits) are fixed platform constraints. Architecture decisions must accommodate these non-negotiable boundaries. + +## Deployment stamps and control planes + +- **Shared deployment stamps.** Use the [Deployment Stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) to scale out infrastructure units that each host multiple tenants. Stamps simplify safe deployment, progressive rollout, and regional expansion. +- **Control plane design.** Separate centralized services (portal, onboarding pipeline, provisioning API) from tenant workloads. The [control plane](https://learn.microsoft.com/en-us/azure/well-architected/saas/design-principles) coordinates stamp creation, tenant placement, and lifecycle operations. +- **Zone alignment for stamps.** When stamps span multiple subscriptions, run `scripts/quota/Get-AzAvailabilityZoneMapping.ps1` to verify logical-to-physical zone mappings. Azure randomizes zone assignments per subscription, so logical zone 1 in subscription A may map to a different physical datacenter than zone 1 in subscription B. This script reveals actual mappings for proper fault domain alignment. + +## Tenancy model decisions + +- **Resource sharing tiers.** Decide which components remain multitenant (for example, compute cluster, databases) versus per-tenant. Factor in cost modeling, [noisy-neighbor risk](https://learn.microsoft.com/en-us/azure/well-architected/saas/compute#tenancy-model-and-isolation), and customer-specific compliance commitments. +- **Governance and monitoring.** Instrument [per-tenant metrics and quotas](https://learn.microsoft.com/en-us/azure/well-architected/saas/compute#tenancy-model-and-isolation) even when tenants share resources. This enables proactive detection of overconsumption and supports usage-based billing. + +## Data architecture + +- **Data store selection.** Choose [transactional data stores](https://learn.microsoft.com/en-us/azure/well-architected/saas/data) that match workload needs (relational vs. nonrelational) and support tenant growth. Minimize the number of technologies to reduce operational complexity. +- **Isolation strategies.** Implement [database-per-tenant, schema-per-tenant, or shared tables with tenant keys](https://learn.microsoft.com/en-us/azure/well-architected/saas/data) depending on compliance and scale requirements. Remember that [storage account limits](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits) (250 per region) constrain per-tenant storage isolation—plan for multi-subscription architectures before you hit these walls. +- **Scale planning.** Document how you enforce tenant-level backup, restore, and data residency guarantees within [service limits](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits). Storage-account-per-tenant architectures fail at 250 tenants without proactive subscription scaling strategies. + +## Operational excellence + +- **Safe deployments.** Use [progressive exposure](https://learn.microsoft.com/en-us/azure/well-architected/saas/design-principles) (rings, feature flags) when rolling out new releases across stamps. Monitor health signals per tenant and stamp to trigger rollbacks based on clear thresholds and policies. +- **Cost governance.** Track [cost of goods sold (COGS)](https://learn.microsoft.com/en-us/azure/well-architected/saas/design-principles) per tenant and align pricing tiers with resource consumption. Automate reporting so finance teams can adjust pricing or optimize resource allocation. + +## Landing zone integration + +- Build the underpinning [Azure landing zone with ISV guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) to guarantee management groups, policy assignments, and cross-subscription networking support multitenant growth. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/single-tenant/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/single-tenant/README.md new file mode 100644 index 000000000..f4e27591f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/single-tenant/README.md @@ -0,0 +1,95 @@ +--- +title: Single-tenant +parent: Customer isolation +nav_order: 1 +--- + +# Single-tenant deployment guide + +Use this guide when each customer gets a dedicated Azure subscription or deployment stamp. The [ISV landing zone guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) defines this "customer-deployed" model as one of the core ISV patterns—customers run workloads inside subscriptions that you provision or manage on their behalf. This approach maximizes isolation, aligns with per-customer regulatory requirements, and simplifies noisy-neighbor mitigation at the expense of higher infrastructure cost per tenant using the [deployment stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern). + +![Single-Tenant SaaS Architecture showing subscription-isolated customer deployments](../../images/single-tenant-topology.svg) + +## Subscription quota flow without quota groups + +When each subscription manages its own quota independently, the subscription vending workflow introduces several failure points that can block or delay customer onboarding. Azure's Quota API handles most regional VM quota increases programmatically, but specific scenarios require manual support ticket escalation. + +```mermaid +flowchart TB + Start([Start]) --> CreateSub[Create Subscription] + CreateSub --> ForEach[For Each Subscription] + ForEach --> Retrieve[Retrieve Quota and Zonal Status] + + Retrieve --> ZonalCheck{Zonal Enablement
Required?} + + ZonalCheck -->|Yes| ZonalQuotaCheck{Quota
Required?} + ZonalCheck -->|No| QuotaOnlyCheck{Quota
Required?} + + ZonalQuotaCheck -->|Yes| ZonalQuotaAPI[Quota API Call] + ZonalQuotaCheck -->|No| ZonalNoQuota[Support Ticket
Zonal enablement only]:::fail + + QuotaOnlyCheck -->|Yes| QuotaOnlyAPI[Quota API Call] + QuotaOnlyCheck -->|No| Complete1([Complete]):::success + + ZonalQuotaAPI --> ZonalAPIResult{API
Success?} + ZonalAPIResult -->|Success| Complete2([Complete]):::success + ZonalAPIResult -->|Fail| ZonalFail[Support Ticket]:::fail + + QuotaOnlyAPI --> QuotaAPIResult{API
Success?} + QuotaAPIResult -->|Success| Complete3([Complete]):::success + QuotaAPIResult -->|Fail| QuotaFail[Support Ticket]:::fail + + classDef fail fill:#ffcccc,stroke:#cc0000,stroke-width:2px,color:#000 + classDef success fill:#ccffcc,stroke:#00cc00,stroke-width:2px,color:#000 +``` + + +> [!WARNING] +> **Quota API rejection.** The [Quota API](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) rejects increase requests when regional capacity is constrained, rate limits are exceeded, or the SKU is restricted by subscription offer type. API rejections return error codes but don't provide capacity availability timelines—operators can't distinguish between temporary capacity shortages and permanent regional restrictions without opening a support ticket. + +> [!WARNING] +> **Zonal enablement isn't self-service.** GPU VM families and other restricted SKUs require [zonal enablement requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) through Azure Support before the Quota API will accept increases. This means subscriptions that need both zonal enablement and quota increases require at least one manual support ticket per subscription-SKU combination, even if the subscription only needs a single VM. + +> [!WARNING] +> **N×M ticket explosion.** When vending multiple subscriptions (N) that each require multiple restricted SKUs (M), the foreach loop over subscriptions generates N×M support tickets. If 10 customers each need 3 GPU SKUs across 2 regions, that's 60 individual tickets—each requiring manual review and approval before deployment can proceed. + +> [!WARNING] +> **Timing gaps.** Quota API requests complete in minutes to hours when capacity is available. Support tickets for zonal enablement or [region access](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) take 3-10 business days on average, and responses don't guarantee capacity—they only confirm eligibility. This timing variance makes it difficult to commit to onboarding SLAs when you can't predict which subscriptions will hit the manual path. +> +> *These timelines reflect Microsoft CSU operational experience, not documented Azure SLAs.* + +> [!IMPORTANT] +> **Quota doesn't guarantee capacity.** Approved quota increases establish the subscription's entitlement ceiling but don't reserve physical capacity. ARM deployments can still fail with `SkuNotAvailable` errors when regional capacity is exhausted, even if the subscription has sufficient quota headroom. This gap between quota approval and deployment success means testing the full onboarding flow in production is the only way to validate capacity availability. + +## Landing zone preparation + +1. **Design the control plane.** Follow the [ISV landing zone guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) to organize management groups, policies, and shared services before onboarding customer subscriptions. Separate corporate IT assets from the SaaS product landing zone if your organization uses different operating models. +2. **Define subscription vending workflows.** Automate subscription creation (EA or MCA) and attach the new subscription to the correct management group, policy assignments, and billing profile. Capture owner/contributor assignments during provisioning so operations teams can [manage the environment end to end](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone). +3. **Apply baseline policies.** Enforce guardrails for networking, identity, tagging, cost management, and diagnostics so every customer environment starts [compliant with corporate standards](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone). + +## Deployment stamps + +- **Stamp per customer.** Deploy a repeatable infrastructure footprint (virtual network, hub services, shared monitoring) per customer. [Single-tenant stamps](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) provide clear isolation boundaries and avoid the need for multitenant logic inside the workload. +- **Automate stamp rollout.** Use infrastructure as code (Bicep, Terraform, ARM) or SDK automation to instantiate the stamp whenever a new customer onboards. Maintain versioning so [upgrades can be rolled out safely](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) across the stamp fleet. +- **Support dual-deployment scenarios.** Some ISVs offer both dedicated and shared deployment options. Document how the single-tenant stamp integrates with any central control plane so you can [reuse automation and monitoring patterns](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) across models. +- **Plan for regional variation.** Some customers require specific geographies or availability zones. Incorporate availability zone alignment and [regional access requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) into the onboarding checklist to prevent last-minute deployment blocks. + +## Workload configuration + +- **Compute isolation.** Choose SKUs and scaling rules that reflect customer contract expectations. Single-tenant models justify higher per-customer spend, so [align pricing with the required compute footprint and SLAs](https://learn.microsoft.com/en-us/azure/well-architected/saas/compute#tenancy-model-and-isolation). +- **Data isolation.** Provision dedicated data stores (for example, separate Azure SQL databases or Cosmos DB accounts) per customer. This simplifies compliance audits and supports [customer-specific backup/restore operations](https://learn.microsoft.com/en-us/azure/well-architected/saas/data). + +## Cost allocation surfaces + +Azure exposes these surfaces for per-tenant cost tracking in single-tenant deployments: + +- **Tag inheritance:** [Tag inheritance policies](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/enable-tag-inheritance) apply subscription and resource group tags to cost records, allowing filtering by customer or business unit without modifying resources. +- **Cost data ingestion:** [FinOps Hubs](https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) aggregates cost data from tenant subscriptions into Azure Data Explorer for analysis across the stamp fleet. +- **Cost exports:** [Scheduled exports](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-export-acm-data) deliver cost data to storage accounts for integration with internal systems. + +## Lifecycle management + +- **Onboarding:** Automate the sequence—create subscription, apply the landing zone blueprint, deploy the stamp, run validation tests, and hand over to customer success so teams aren't rebuilding steps manually. +- **Expansion:** When customers need more capacity, scale the stamp vertically or horizontally, or [deploy additional stamps](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) for regional redundancy. Coordinate with quota and reservation guides to guarantee capacity. +- **Recycle vs. retire:** When a customer churns, reclaim quotas and shared services but keep the subscription if [region/zone enablement or regulatory approvals](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) might be reused for future tenants. + diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/toc.yml b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/toc.yml new file mode 100644 index 000000000..b62f5bbb7 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/deployment/toc.yml @@ -0,0 +1,91 @@ +- name: Capacity management + href: ../operations/README.md + items: + - name: Overview + href: ../operations/README.md + - name: Planning and forecasting + href: ../operations/capacity-planning/README.md + items: + - name: Capacity and billing operations + href: ../operations/capacity-and-quotas/README.md + - name: Capacity planning + href: ../operations/capacity-planning/README.md + - name: Customer isolation + href: README.md + items: + - name: Overview + href: README.md + - name: Single-tenant + href: single-tenant/README.md + - name: Multi-tenant + href: multi-tenant/README.md + - name: Workload placement + href: ../operations/quota/README.md + items: + - name: Billing models + href: ../billing/README.md + items: + - name: Overview + href: ../billing/README.md + - name: Microsoft Customer Agreement + href: ../billing/modern/README.md + items: + - name: Overview + href: ../billing/modern/README.md + - name: Subscription creation (billing scope) + href: ../operations/modern/README.md + - name: Enterprise Agreement + href: ../billing/legacy/README.md + items: + - name: Overview + href: ../billing/legacy/README.md + - name: Subscription creation (billing scope) + href: ../operations/legacy/README.md + - name: Quota operations + href: ../operations/quota/README.md + - name: Capacity reservations + href: ../operations/capacity-reservations/README.md + - name: AKS capacity governance + href: ../operations/aks-capacity/README.md + - name: Non-compute quotas + href: ../operations/non-compute-quotas/README.md + - name: Usage and rate optimization + href: ../operations/quota-groups/README.md + items: + - name: Quota groups + href: ../operations/quota-groups/README.md + - name: Governance and automation + href: ../operations/monitoring-alerting/README.md + items: + - name: Monitoring and alerting + href: ../operations/monitoring-alerting/README.md + - name: Capacity governance + href: ../operations/capacity-governance/README.md + - name: Tools and scripts + href: ../operations/tools-scripts/README.md + items: + - name: Overview + href: ../operations/tools-scripts/README.md + - name: Get-AzVMQuotaUsage.ps1 + href: ../operations/tools-scripts/get-azvmquotausage.md + - name: Get-AzAvailabilityZoneMapping.ps1 + href: ../operations/tools-scripts/get-azavailabilityzonemapping.md + - name: Show-AzVMQuotaReport.ps1 + href: ../operations/tools-scripts/show-azvmquotareport.md + - name: Get-BenefitRecommendations.ps1 + href: ../operations/tools-scripts/get-benefitrecommendations.md + - name: Deploy-AnomalyAlert.ps1 + href: ../operations/tools-scripts/deploy-anomalyalert.md + - name: Deploy-BulkALZ.ps1 + href: ../operations/tools-scripts/deploy-bulkalz.md + - name: Support and reference + href: ../operations/support-and-reference/README.md + items: + - name: Overview + href: ../operations/support-and-reference/README.md + - name: Support escalation + href: ../operations/escalation/README.md + - name: Tenant hygiene + href: ../operations/tenant-hygiene/README.md + - name: Glossary + href: ../operations/glossary.md diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/docfx.yml b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/docfx.yml new file mode 100644 index 000000000..965b7a258 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/docfx.yml @@ -0,0 +1,45 @@ +### YamlMime:DocFX +metadata: +- name: _appTitle + content: ISV Quota & Capacity Management +- name: _appName + content: Azure ISV Documentation +- name: _enableNewTab + content: true +- name: _enableSearch + content: true + +build: + content: + - files: + - "**/*.md" + - "**/*.yml" + exclude: + - "_site/**" + - "obj/**" + src: . + dest: . + resource: + - files: + - "images/**" + - "**/*.png" + - "**/*.jpg" + - "**/*.jpeg" + - "**/*.gif" + - "**/*.svg" + overwrite: + - files: + - "apidoc/**.md" + exclude: + - "obj/**" + - "_site/**" + globalMetadata: + _appTitle: ISV Quota & Capacity Management + _appFooter: Copyright © 2024 Microsoft Corporation + _gitContribute: + repo: https://github.com/microsoft/azcapman + branch: main + template: + - default + - modern + dest: _site \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/azure-logo.svg b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/azure-logo.svg new file mode 100644 index 000000000..0ad8bbe1f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/azure-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/deployment-topologies.svg b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/deployment-topologies.svg new file mode 100644 index 000000000..c1bfb9f30 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/deployment-topologies.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + SaaS Deployment Topologies for ISVs + + + + Customer Isolation Strategies in Azure + + + + + + + + + + + Single-Tenant SaaS (Subscription Isolated) + + + + + + ISV Control Plane + + + Provisioning, Billing, Management + + + + + Customer Azure Subscriptions: + + + + + + + Customer A Subscription + + + + Resources + + + + Database + + + + + + + + Customer B Subscription + + + + Resources + + + + Database + + + + + + + + Customer C Subscription + + + + Resources + + + + Database + + + + + + + + Customer D Subscription + + + + Resources + + + + Database + + + + + + Key Characteristics: + + + • Full infrastructure isolation per customer + + + • Independent scaling and deployment + + + • Customer-specific compliance boundaries + + + • Higher operational overhead + + + + + + + + + + + + Multi-Tenant SaaS (Shared Infrastructure) + + + + + + ISV Shared Platform (Single/Few Subscriptions) + + + + + + Control Plane (Authentication, Billing, Provisioning) + + + + + + Application Tier + + + + + Customer A + + + + Customer B + + + + Customer C + + + + Customer D + + + + + + Shared Database with Logical Isolation + + + + + + Multi-Tenant Database + + + + TenantID | Data | ... + + + Row-Level Security + + + + + Key Characteristics: + + + • Shared infrastructure across all customers + + + • Application-level tenant isolation + + + • Efficient resource utilization + + + • Lower per-customer operational cost + + + + + + + + + + + + + + + Single-Tenant SaaS + + + Isolation Level: + + + Infrastructure + + + Resource Model: + + + 1:1 (Customer:Subscription) + + + + + Multi-Tenant SaaS + + + Isolation Level: + + + Application + + + Resource Model: + + + N:1 (Customers:Platform) + + + + + vs + + + \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/multi-tenant-topology.svg b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/multi-tenant-topology.svg new file mode 100644 index 000000000..fdfb2acd1 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/multi-tenant-topology.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + Multi-Tenant SaaS Architecture + + + + Shared Infrastructure with Application Isolation + + + + + + + + + + + Multi-Tenant SaaS (Shared Infrastructure) + + + + + + ISV Shared Platform (Single/Few Subscriptions) + + + + + + Control Plane (Authentication, Billing, Provisioning) + + + + + + Application Tier + + + + + Customer A + + + + Customer B + + + + Customer C + + + + Customer D + + + + + + Shared Database with Logical Isolation + + + + + + Multi-Tenant Database + + + + TenantID | Data | ... + + + Row-Level Security + + + + + Key Characteristics: + + + • Shared infrastructure across all customers + + + • Application-level tenant isolation + + + • Efficient resource utilization + + + • Lower per-customer operational cost + + + + + + + + Isolation Level: Application + + + Resource Model: N:1 (Customers:Platform) + + + \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/single-tenant-topology.svg b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/single-tenant-topology.svg new file mode 100644 index 000000000..81bc88182 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/images/single-tenant-topology.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + Single-Tenant SaaS Architecture + + + + Subscription-Isolated Customer Deployments + + + + + + + + + + + Single-Tenant SaaS (Subscription Isolated) + + + + + + ISV Control Plane + + + Provisioning, Billing, Management + + + + + Customer Azure Subscriptions: + + + + + + + Customer A Subscription + + + + Resources + + + + Database + + + + + + + + Customer B Subscription + + + + Resources + + + + Database + + + + + + + + Customer C Subscription + + + + Resources + + + + Database + + + + + + + + Customer D Subscription + + + + Resources + + + + Database + + + + + + Key Characteristics: + + + • Full infrastructure isolation per customer + + + • Independent scaling and deployment + + + • Customer-specific compliance boundaries + + + • Higher operational overhead + + + + + + + + Isolation Level: Infrastructure + + + Resource Model: 1:1 (Customer:Subscription) + + + \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/index.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/index.md new file mode 100644 index 000000000..50f6761f1 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/index.md @@ -0,0 +1,100 @@ +--- +title: Overview +nav_order: 1 +--- + +
+

Azure capacity governance for ISVs

+

Azure quota, access, reservations, monitoring, and billing controls mapped to the canonical FinOps Framework capabilities for SaaS ISV estates.

+ Open the capability map → +
+ + + +
+ +## FinOps capability map + +Learn how Azure capacity evidence supports the FinOps Framework capabilities for SaaS ISV estates. The FinOps Foundation defines Domains and Capabilities as the outcomes and activities of a FinOps practice, not as a serial process. [Source](https://www.finops.org/framework/) [Source](https://www.finops.org/framework/domains/) + +
+
+ +### Plan and forecast demand + +- [Capacity planning](operations/capacity-planning/README.md) +- [Scale units and stamps](deployment/README.md) +- [Capacity and billing operations](operations/capacity-and-quotas/README.md) + +
+
+ +### Place workloads and secure capacity + +- [Region access requests](operations/quota/README.md) +- [Quota increases](operations/quota/README.md#requesting-quota-increases) +- [Quota groups](operations/quota-groups/README.md) +- [Capacity reservations](operations/capacity-reservations/README.md) + +
+
+ +### Optimize, govern, and automate + +- [Monitoring and alerts](operations/monitoring-alerting/README.md) +- [Governance program](operations/capacity-governance/README.md) +- [Escalation paths](operations/escalation/README.md) +- [Support and reference](operations/support-and-reference/README.md) + +
+
+
+ +
+ +## Explore more + +
+
+

Customer isolation

+

Single-tenant and multi-tenant deployment patterns for ISVs.

+
+
+

Billing enrollment

+

EA and MCA billing structures for capacity management.

+
+
+

Glossary

+

Key terms and definitions for capacity governance.

+
+
+
diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-02-forecasting.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-02-forecasting.md new file mode 100644 index 000000000..d6c44d73f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-02-forecasting.md @@ -0,0 +1,654 @@ +# Lab 2: Forecasting + +## Overview + +Forecasting capacity needs is about collecting utilization signals, analyzing patterns, and translating them into infrastructure commitments. This lab walks you through using Azure Cost Management APIs, quota telemetry, and utilization metrics to build a forecast of compute and storage demand across your SaaS platform, aligned with [Well-Architected capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) guidance. + +You'll gather baseline quota usage, analyze utilization patterns in Azure Monitor, pull savings plan recommendations, assess storage costs, and map your forecast to deployment scale units. These outputs feed directly into procurement and allocation decisions. + +> **Pilot scope boundary:** Forecasting improves planning quality and escalation readiness, but it doesn't guarantee regional or zonal service availability on its own. Region and zone deployment access still depends on subscription-level access approvals and current capacity availability in Azure regions ([Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [Region access request process](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process), [Zonal enablement request process](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series)). + +### Required lab outputs + +Produce these outputs before moving to procurement and allocation work: + +1. **FY26 forecasting template output** with assumptions, confidence ranges, and quarter-by-quarter demand projections ([Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)). +2. **Technical action register** with signal, threshold, evidence, recommended action, and validation timestamp for every identified quota, capacity, or cost action ([Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain)). +3. **Escalation-ready evidence pack** with support-ready artifacts for quota, region access, and reservation blockers ([Create an Azure support request](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request), [Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests)). +4. **Regional PaaS readiness matrix** for non-compute services in each target region, backed by reproducible API output ([Azure REST API reference](https://learn.microsoft.com/en-us/rest/api/azure/), [Azure CLI quota reference](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest)). + +### Technical decision evidence model + +Capture a decision artifact only when forecast output changes one or more technical control values: + +- Target deployment date or milestone date +- Target region or availability zone +- Requested quota, capacity reservation quantity, or commitment amount +- Service scaling boundary (for example, storage sharding, partitioning, or throughput target) + +If none of these values change, keep the item as an observation in your evidence workbook, not as a decision artifact ([Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [FinOps planning and estimating](https://www.finops.org/framework/capabilities/planning-estimating/)). + +> **Policy callout:** All training steps in this lab must be reproducible with publicly documented Azure APIs or documented CLI surfaces. Don't rely on private tools, unpublished endpoints, or portal-only behavior without an API equivalent ([Azure REST API reference](https://learn.microsoft.com/en-us/rest/api/azure/), [Azure CLI overview](https://learn.microsoft.com/en-us/cli/azure/what-is-azure-cli)). + +**Prerequisites:** +- Authenticated access to a billing scope and at least one Azure subscription that contains workload resources for analysis ([scripts/rate/README.md](../../scripts/rate/README.md), [Azure subscriptions and management groups](https://learn.microsoft.com/en-us/azure/governance/management-groups/overview)) +- A supported command surface for reproducible queries: Azure CLI and, for PowerShell examples in this lab, the Az PowerShell modules used by the scripts ([Azure CLI overview](https://learn.microsoft.com/en-us/cli/azure/what-is-azure-cli), [Install Azure PowerShell](https://learn.microsoft.com/en-us/powershell/azure/install-azure-powershell), [scripts/rate/README.md](../../scripts/rate/README.md), [scripts/quota/README.md](../../scripts/quota/README.md)) +- Read access on the target billing or subscription scope so you can query cost, quota, and monitoring data ([scripts/rate/README.md](../../scripts/rate/README.md), [Use cost alerts to monitor usage and spending](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending), [How to monitor quotas and generate alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting)) + +--- + +## Exercise 1: Gather baseline quota and usage data + +**Objective:** Collect current quota limits and actual usage across compute SKUs and regions to understand headroom for growth. + +Quota tells you what you're allowed to run, and usage tells you what you're actually running. The gap is your growth buffer for planning and escalation lead time ([Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)). + +### Step 1.1: Export VM SKU quota usage across subscriptions + +Run the multithreaded quota analysis script to scan all subscriptions for compute quota: + +```powershell +.\scripts/quota/Get-AzVMQuotaUsage.ps1 ` + -SKUs "Standard_D2s_v3", "Standard_D4s_v3", "Standard_E2s_v3", "Standard_E4s_v3" ` + -Locations "eastus", "westus2", "northeurope" ` + -Threads 4 ` + -UsePhysicalZones +``` + +This script: +- Queries each subscription in parallel (4 concurrent threads) ([Get-AzVMQuotaUsage script](../../scripts/quota/Get-AzVMQuotaUsage.ps1), [quota scripts README](../../scripts/quota/README.md)) +- Returns quota limits, current usage, and utilization % per SKU per location ([Get-AzVMQuotaUsage script](../../scripts/quota/Get-AzVMQuotaUsage.ps1), [quota scripts README](../../scripts/quota/README.md)) +- Identifies subscriptions approaching quota limits (the constraint on scaling) ([Get-AzVMQuotaUsage script](../../scripts/quota/Get-AzVMQuotaUsage.ps1), [quota scripts README](../../scripts/quota/README.md)) +- Includes physical zone information if -UsePhysicalZones is set ([Get-AzVMQuotaUsage script](../../scripts/quota/Get-AzVMQuotaUsage.ps1), [quota scripts README](../../scripts/quota/README.md)) + +**Output interpretation:** +- Use your own alert policy thresholds for investigation and escalation. If you use >80% and <20% as internal triage bands, treat them as operator-defined heuristics, not Azure platform defaults ([How to monitor quotas and generate alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)) +- Compare across regions to identify uneven capacity distribution + +### Step 1.2: Query quota usage via Azure CLI + +For a specific subscription and region, use the CLI to drill down: + +```bash +az vm list-usage \ + --location eastus \ + --subscription YOUR_SUBSCRIPTION_ID \ + --query "[?contains(name.value, 'DSv3')].{Family: name.localizedValue, Limit: limit, CurrentUsage: currentValue}" \ + --output table +``` + +This returns per-family vCPU limits and current usage. Filter by family name substring (e.g., `DSv3`, `ESv3`) — the `name.value` field contains family identifiers like `standardDSv3Family`, not individual SKU names. + +Reference the [az vm list-usage documentation](https://learn.microsoft.com/en-us/cli/azure/vm?view=azure-cli-latest#az-vm-list-usage) for output schema details. + +**What to look for (operator-defined triage heuristics):** +- Subscriptions or regions where current usage approaches configured alert thresholds for the quota family. +- SKU families with sustained low utilization relative to allocated limits. +- Regional concentration patterns that create uneven headroom across target deployment regions. + +References: [How to monitor quotas and generate alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning). + +### Step 1.3: Document baseline + +Save the output CSVs. You'll use these as a baseline to track growth rate quarter-over-quarter. + +Also capture escalation evidence fields in your baseline workbook so the output can move directly into support or capacity escalation when needed: + +- Subscription ID, tenant ID, and billing scope +- Region and availability zone (logical and physical mapping, where applicable) +- VM family, current limit, current usage, requested limit, and utilization % +- Blocking deployment timestamp, error code, and failed operation ID +- Forecasted demand window (date range), estimated shortfall, and business impact statement +- Evidence links (CLI output, script export, workbook snapshot), and last validated date + +References: [Create an Azure support request](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request), [Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests), [Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain). + +### Step 1.4: Run explicit multi-subscription analysis and hand off allocation decisions + +Run this exercise across all in-scope subscriptions, not a single subscription sample. Forecast quality depends on estate-level variance by subscription and region ([Quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)). + +At the end of Exercise 1, hand off these outputs to [Lab 3: Allocation](./lab-03-allocation.md) for quota-group allocation decisions: + +- Regional and per-family utilization heatmap by subscription +- Candidate subscriptions for quota transfer or rebalance +- Escalation candidates where projected demand exceeds available headroom + +### Step 1.5: Run non-compute quota and access checks with public APIs + +Run service-specific checks in parallel with compute quota checks. Quota groups cover IaaS compute, not PaaS service limits ([Quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups), [Non-compute quota guide](../operations/non-compute-quotas/README.md)). + +**Azure SQL Database usage check (`Microsoft.Sql/locations/usages`):** + +```bash +az rest --method GET \ + --url "https://management.azure.com/subscriptions/YOUR_SUBSCRIPTION_ID/providers/Microsoft.Sql/locations/eastus/usages?api-version=2023-08-01-preview" +``` + +Reference: [SQL usage operations](https://learn.microsoft.com/en-us/rest/api/sql/usages/list-by-location). + +> **API version hygiene:** This sample uses a preview API version (`2023-08-01-preview`). Before running it in production or automation, verify the current supported `Microsoft.Sql` `usages` API version in the [SQL usage operations reference](https://learn.microsoft.com/en-us/rest/api/sql/usages/list-by-location), and update the command if a newer stable version is available. + +**Azure Cosmos DB regional readiness check (regional footprint and account capability):** + +```bash +az cosmosdb show \ + --resource-group YOUR_RESOURCE_GROUP \ + --name YOUR_COSMOS_ACCOUNT \ + --query "{writeRegions:writableLocations[].locationName,readRegions:readableLocations[].locationName,capabilities:capabilities[].name}" \ + --output json +``` + +Reference: [Azure Cosmos DB account management and global distribution](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-manage-database-account), [az cosmosdb reference](https://learn.microsoft.com/en-us/cli/azure/cosmosdb?view=azure-cli-latest). + +**Azure Database for PostgreSQL regional capability check (`az postgres flexible-server list-skus`):** + +```bash +az postgres flexible-server list-skus \ + --location eastus \ + --output table +``` + +Reference: [az postgres flexible-server list-skus](https://learn.microsoft.com/en-us/cli/azure/postgres/flexible-server?view=azure-cli-latest#az-postgres-flexible-server-list-skus). + +If you need a REST equivalent for automation parity, keep that endpoint mapping as pending validation in this lab revision, and only add it after validating the exact API path and version in Microsoft Learn. + +**Azure App Service quota and zonal readiness checks (`Microsoft.Web`):** + +```bash +# 1) Subscription-level App Service quota usage surface +az quota usage list \ + --scope /subscriptions/YOUR_SUBSCRIPTION_ID \ + --resource-provider Microsoft.Web \ + --output json + +# 2) App Service plan zonal readiness surface +az appservice plan show \ + --resource-group YOUR_RESOURCE_GROUP \ + --name YOUR_APP_SERVICE_PLAN \ + --query "{id:id,location:location,sku:sku.name,zoneRedundant:properties.zoneRedundant,maximumNumberOfZones:properties.maximumNumberOfZones,numberOfWorkers:properties.numberOfWorkers,maximumNumberOfWorkers:properties.maximumNumberOfWorkers}" \ + --output json +``` + +References: [az quota usage list](https://learn.microsoft.com/en-us/cli/azure/quota/usage?view=azure-cli-latest#az-quota-usage-list), [az appservice plan show](https://learn.microsoft.com/en-us/cli/azure/appservice/plan?view=azure-cli-latest#az-appservice-plan-show), [App Service Plans - Get (Microsoft.Web/serverfarms)](https://learn.microsoft.com/en-us/rest/api/appservice/app-service-plans/get?view=rest-appservice-2024-11-01). + +**Optional instance-level evidence (physical zone signal from runtime instances):** + +```bash +# 3a) List app instances +az rest --method GET \ + --url "https://management.azure.com/subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RESOURCE_GROUP/providers/Microsoft.Web/sites/YOUR_APP_NAME/instances?api-version=2024-11-01" + +# 3b) Get one instance detail +az rest --method GET \ + --url "https://management.azure.com/subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RESOURCE_GROUP/providers/Microsoft.Web/sites/YOUR_APP_NAME/instances/YOUR_INSTANCE_ID?api-version=2024-11-01" +``` + +References: [Web Apps - List Instance Identifiers](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/list-instance-identifiers?view=rest-appservice-2024-11-01), [Web Apps - Get Instance Info](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/get-instance-info?view=rest-appservice-2024-11-01). + +**Interpretation signals and evidence fields:** + +- **Quota signal (`Microsoft.Web`):** Track current usage versus limit for App Service quota entries returned by `az quota usage list`; flag an escalation candidate when projected demand exceeds available headroom in the forecast window. +- **Plan zonal readiness signal:** Use `zoneRedundant=true` as the configuration signal that the plan is set for zone balancing; treat `zoneRedundant=false` as non-zonal configuration for that plan. Validate worker headroom with `numberOfWorkers` versus `maximumNumberOfWorkers` from the same plan output. +- **Maximum zones evidence field:** Capture `maximumNumberOfZones` from plan output when present, and record `null` or missing explicitly when the provider response doesn't expose it for the selected SKU, region, or API shape. +- **Runtime placement evidence (optional):** Capture per-instance `properties.physicalZone` from the instance APIs as supporting evidence for where active workers are running. +- **Minimum evidence record:** Subscription ID, resource group, app name, plan name, region, timestamp, command used, raw JSON output link, `zoneRedundant`, `maximumNumberOfZones`, `numberOfWorkers`, `maximumNumberOfWorkers`, and any returned `physicalZone` values. + +**Azure SQL Managed Instance placeholder:** + +API details for a reproducible regional readiness check are pending validation in this lab revision. Don't add speculative commands. Use documented APIs only once validated and cited. + +--- + +## Exercise 2: Analyze compute utilization with Azure Monitor + +**Objective:** Measure actual CPU and memory consumption to forecast peak demand and right-size commitments. + +Quota and usage tell you how many VMs you can and are running. Utilization metrics tell you how hard those VMs are working—critical for forecasting when you'll hit scaling limits. + +### Step 2.1: Query CPU metrics across VMs + +Query aggregate CPU utilization across all VMs using [VM insights](https://learn.microsoft.com/en-us/azure/azure-monitor/vm/vminsights-overview) data in Log Analytics: + +```kusto +InsightsMetrics +| where Origin == "vm.azm.ms" +| where Namespace == "Processor" +| where Name == "UtilizationPercentage" +| where TimeGenerated > ago(30d) +| summarize AvgCPU = avg(Val), MaxCPU = max(Val), P95CPU = percentile(Val, 95) by Computer, bin(TimeGenerated, 1h) +| render timechart +``` + +Run this in the [Log Analytics workspace](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-overview) connected to your VMs, or via CLI: + +```bash +az monitor log-analytics query \ + --workspace YOUR_WORKSPACE_ID \ + --analytics-query "InsightsMetrics | where Origin == 'vm.azm.ms' | where Namespace == 'Processor' | where Name == 'UtilizationPercentage' | where TimeGenerated > ago(30d) | summarize AvgCPU = avg(Val), MaxCPU = max(Val), P95CPU = percentile(Val, 95) by Computer, bin(TimeGenerated, 1h)" \ + --output table +``` + +For a single VM's platform metrics (no Log Analytics required): + +```bash +az monitor metrics list \ + --resource "/subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RG/providers/Microsoft.Compute/virtualMachines/YOUR_VM_NAME" \ + --metrics "Percentage CPU" \ + --interval PT1H \ + --aggregation Average \ + --output table +``` + +See the [InsightsMetrics table reference](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/insightsmetrics) for the full list of VM insights namespaces and metric names. + +Reference the [capacity planning section of the Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) for guidance on which metrics to track. + +### Step 2.2: Identify utilization patterns + +Look for: +- **Peak hours:** When does CPU spike? This shows your scaling trigger points. +- **Baseline:** What's the minimum CPU during off-peak? This tells you static vs. variable cost. +- **Trend:** Is utilization growing week-over-week? This informs forecast slope. +- **P95:** 95th percentile tells you the threshold for "normal" workload bursts. Budget capacity for this, not average. + +References: [InsightsMetrics table reference](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/insightsmetrics), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning). + +### Step 2.3: Establish growth trends + +Track week-over-week utilization changes from your monitoring data. The growth trend feeds directly into your quota increase cadence—if utilization is climbing, schedule quota requests before you hit your operator-defined 80% triage threshold from Exercise 1 ([How to monitor quotas and generate alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)). + +--- + +## Exercise 3: Pull savings plan recommendations + +**Objective:** Use Azure Cost Management's Benefit Recommendations API to forecast commitment levels that match your utilization forecast. + +Set commitment targets from your own historical usage and from recommendation surfaces, including [Azure Reservations guidance](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations), [savings plan guidance](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/savings-plan-compute-overview), and [FinOps rate optimization guidance](https://www.finops.org/framework/capabilities/rate-optimization/). This exercise focuses on the savings plan recommendation layer in that decision. + +### Step 3.1: Query savings plan recommendations + +```powershell +.\scripts/rate/Get-BenefitRecommendations.ps1 ` + -BillingScope "/subscriptions/YOUR_SUBSCRIPTION_ID" ` + -LookBackPeriod "Last30Days" ` + -Term "P3Y" +``` + +Parameters: +- **BillingScope:** billing account (for EA), subscription, or resource group. Narrower scopes give more targeted recommendations ([scripts/rate/README.md](../../scripts/rate/README.md), [Get-BenefitRecommendations script](../../scripts/rate/Get-BenefitRecommendations.ps1)). +- **LookBackPeriod:** Last7Days / Last30Days / Last60Days. Use 30+ days for reliable trending ([scripts/rate/README.md](../../scripts/rate/README.md), [Get-BenefitRecommendations script](../../scripts/rate/Get-BenefitRecommendations.ps1)). +- **Term:** P1Y (1-year) or P3Y (3-year). 3-year has higher discount but less flexibility ([scripts/rate/README.md](../../scripts/rate/README.md), [Get-BenefitRecommendations script](../../scripts/rate/Get-BenefitRecommendations.ps1), [Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations)). + +**Output fields:** +- `commitmentAmount`: Recommended commitment amount returned by the API. +- `commitmentGranularity`: Time granularity used for the commitment amount. +- `savingsPercentage`: Estimated savings percentage for the look-back period. +- `coveragePercentage`: Estimated coverage percentage for the look-back period. +- `averageUtilizationPercentage`: Estimated average utilization percentage for the look-back period. +- `overageCost`: Estimated overage cost for usage beyond the commitment. +- `wastageCost`: Estimated unused portion of the benefit cost. + +Field names and descriptions above are based on the Cost Management Benefit Recommendations API schema and the script reference used in this repo ([Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations), [scripts/rate/README.md](../../scripts/rate/README.md)). + +### Step 3.2: Interpret the recommendation + +Example output (illustrative scenario, not a platform default target): +- Hourly commitment: $500 +- Coverage: 85% +- Savings: 40% +- Wastage: $12/day + +This means: +- Buy $500/hour of compute commitment (1-year or 3-year term) +- This covers 85% of your typical hourly usage +- The remaining 15% runs at pay-as-you-go rates unless covered by another pricing benefit +- You're leaving ~$12/day on the table to underutilized reserved capacity + +**Decision point (illustrative):** Do you accept 85% coverage to save 40%, or do you need tighter alignment? If your forecast shows 10% growth over the next quarter, and coverage is already 85%, you might increase commitment targets to limit expected overage risk as utilization climbs. + +### Step 3.3: Validate against your forecast + +Cross-check the API's recommendation against your capacity forecast from Exercise 2: +- Example: if your forecast growth rate suggests 12% month-over-month increase, and the API recommendation maps to 85% coverage, plan a commitment refresh window before projected overage risk crosses your internal threshold. +- Schedule your next forecast refresh accordingly. + +Reference the [FinOps Framework planning and estimating guidance](https://www.finops.org/framework/capabilities/planning-estimating/) and [Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations) for structuring this decision. + +--- + +## Exercise 4: Assess storage and PaaS costs and forecast + +**Objective:** Analyze storage, disk, and PaaS service costs across your estate, then forecast growth based on recent trends. + +Storage and PaaS services scale differently from compute. Understanding which resources drive cost—and their growth rate—prevents surprise bill spikes and ensures you allocate capacity correctly across regions. Use [Cost Management cost analysis](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/quick-acm-cost-analysis) to query cost data across service categories. + +### Step 4.1: Query storage costs with Cost Management + +Use the Azure CLI to pull storage costs grouped by subscription and resource for the last billing period: + +```bash +az rest --method POST \ + --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/YOUR_BILLING_ACCOUNT_ID/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \ + --body '{ + "type": "ActualCost", + "timeframe": "TheLastBillingMonth", + "dataset": { + "granularity": "None", + "filter": { + "dimensions": { "name": "ServiceName", "operator": "In", "values": ["Storage"] } + }, + "grouping": [ + { "type": "Dimension", "name": "SubscriptionName" }, + { "type": "Dimension", "name": "ResourceId" } + ], + "aggregation": { "totalCost": { "name": "Cost", "function": "Sum" } } + } + }' +``` + +Reference the [Cost Management Query API](https://learn.microsoft.com/en-us/rest/api/cost-management/query/usage) for request body schema and supported dimensions. + +Alternatively, in the Azure portal: +1. Go to **Cost Management** > **Cost analysis** +2. Select **Group by** > **Service name**, then filter to **Storage** +3. Select the **DailyCosts** view to see the trend over the billing period + +For recurring analysis, [create a Cost Management export](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-export-acm-data) to automatically deliver cost data to a storage account on a schedule. This gives you a historical dataset for trend analysis without manual queries. + +### Step 4.2: Identify growth drivers + +For each high-cost storage resource: +- Compare this month's cost to the previous month's cost +- Calculate growth rate: (Current - Previous) / Previous * 100 +- If growth > 10% per month, flag for capacity planning + +Example: An analytics storage account grew from 500 GB to 600 GB in 30 days = 20% growth. At that rate, it reaches 1 TB in ~4 months. Plan ingestion limits, archival policies, or regional replica targets now. + +Storage cost varies by tier (hot, cool, archive), replication (LRS, GRS, RA-GRS), and region. Drill down into high-growth accounts and determine if cost is driven by increasing volume, access pattern shifts, or replication overhead. + +### Step 4.3: Forecast Premium SSD v2 disk costs + +[Premium SSD v2](https://learn.microsoft.com/en-us/azure/virtual-machines/disks-types#premium-ssd-v2) disks bill separately for capacity, throughput, and IOPS—each dimension scales independently. This makes forecasting more granular than standard managed disks. + +Query disk costs with: + +```bash +az rest --method POST \ + --url "https://management.azure.com/subscriptions/YOUR_SUBSCRIPTION_ID/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \ + --body '{ + "type": "ActualCost", + "timeframe": "TheLastBillingMonth", + "dataset": { + "granularity": "None", + "filter": { + "dimensions": { "name": "MeterSubCategory", "operator": "In", "values": ["Premium SSD v2"] } + }, + "grouping": [{ "type": "Dimension", "name": "ResourceId" }], + "aggregation": { "totalCost": { "name": "Cost", "function": "Sum" } } + } + }' +``` + +When forecasting Premium SSD v2 growth, track three dimensions separately: +- **Capacity (GiB):** Grows with data volume—forecast from ingestion rate +- **Provisioned throughput (MBps):** Grows with workload IO demands—forecast from performance baselines +- **Provisioned IOPS:** Grows with transaction density—forecast from application telemetry + +Each dimension has its own per-unit price, so a workload that's IOPS-heavy but storage-light has a different cost profile than one that's capacity-heavy. Model each dimension independently for accurate projections. + +### Step 4.4: Forecast Azure Cosmos DB costs + +[Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/cost-management) uses request units (RUs) as its primary capacity metric. Forecasting Cosmos DB costs requires tracking RU consumption patterns alongside storage growth. + +Query Cosmos DB costs with: + +```bash +az rest --method POST \ + --url "https://management.azure.com/subscriptions/YOUR_SUBSCRIPTION_ID/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \ + --body '{ + "type": "ActualCost", + "timeframe": "TheLastBillingMonth", + "dataset": { + "granularity": "None", + "filter": { + "dimensions": { "name": "ServiceName", "operator": "In", "values": ["Azure Cosmos DB"] } + }, + "grouping": [{ "type": "Dimension", "name": "ResourceId" }], + "aggregation": { "totalCost": { "name": "Cost", "function": "Sum" } } + } + }' +``` + +Key forecasting dimensions for Cosmos DB: +- **Provisioned throughput (RU/s):** If you use [provisioned throughput](https://learn.microsoft.com/en-us/azure/cosmos-db/set-throughput), forecast from peak RU consumption trends in Azure Monitor. Autoscale accounts bill for the max RU/s reached per hour. +- **Serverless consumption:** If you use [serverless](https://learn.microsoft.com/en-us/azure/cosmos-db/serverless), forecast from total RU consumption per billing period. Costs scale linearly with request volume. +- **Storage:** Cosmos DB bills per GB stored. Forecast from data growth rate—partition splits don't change cost, but multi-region writes multiply storage charges per replica. + +For stamp-based deployments, treat this as a workload-specific operator assumption: if your data model assigns throughput and storage boundaries per tenant, Cosmos DB cost can track tenant count more closely than user count. Validate that assumption with your own per-tenant RU telemetry before using it in forecasts ([Set throughput on Azure Cosmos DB containers and databases](https://learn.microsoft.com/en-us/azure/cosmos-db/set-throughput), [Azure Cosmos DB serverless](https://learn.microsoft.com/en-us/azure/cosmos-db/serverless), [Azure Cosmos DB cost management](https://learn.microsoft.com/en-us/azure/cosmos-db/cost-management)). + +### Step 4.5: Map forecasts to workload types + +Combine your storage, disk, and PaaS cost data into a unified forecast: +- **Azure Storage:** Forecast by tier, replication, and transaction volume +- **Premium SSD v2:** Forecast by capacity, throughput, and IOPS independently +- **Azure Cosmos DB:** Forecast by RU consumption pattern and storage growth + +Cross-reference these forecasts with your compute stamp projections from Exercise 5—PaaS costs often scale proportionally with stamp count, but the relationship isn't always linear. A new stamp may need pre-provisioned Cosmos DB throughput before tenants migrate to it. + +### Step 4.6: Add storage scaling and partitioning architecture guidance + +Document the storage scaling and partitioning design that matches each high-growth workload: + +- Storage account sharding or split strategy by workload boundary and region +- Partition-key strategy for high-volume PaaS data paths, including expected hot-partition risk +- Replication pattern (for example, LRS, GRS, or RA-GRS) and the cost-to-recovery tradeoff + +Use service documentation to justify the design choices and scaling constraints ([Storage scalability targets](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview#scalability-targets-for-standard-storage-accounts), [Azure Cosmos DB partitioning overview](https://learn.microsoft.com/en-us/azure/cosmos-db/partitioning-overview)). + +### Step 4.7: Define storage escalation triggers and evidence requirements + +Create explicit escalation triggers for storage and PaaS growth: + +- Trigger when forecasted growth crosses a documented service limit or support-reviewed operational threshold. +- Trigger when projected cost variance crosses your FinOps guardrail for the planning window. +- Trigger when regional readiness checks show a required service isn't ready in a target region. + +For each trigger, capture required evidence: 30-day trend, current usage, forecast method, target region, and requested change ([Create an Azure support request](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request), [FinOps planning and estimating](https://www.finops.org/framework/capabilities/planning-estimating/)). + +> **Implementation note:** Don't hardcode unverified numeric limits in lab assets. Pull limits from current Microsoft Learn documentation or live API output each run, then cite the source used ([Azure REST API reference](https://learn.microsoft.com/en-us/rest/api/azure/), [Azure limits and quotas](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits)). + +--- + +## Exercise 5: Map forecasts to scale units + +**Objective:** Translate compute and storage forecasts into capacity per deployment stamp or region, identifying when you'll exceed current allocation. + +ISVs often deploy multiple independent instances of their platform across regions (the [Deployment Stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern)). Forecasting capacity per stamp—not only the aggregate—ensures you don't overload one region. + +### Step 5.1: Define your stamp architecture + +Document your deployment unit. Example: +- **Stamp composition:** + - 3 API frontend VMs (Standard_D2s_v3, autoscale 3–10) + - 1 data platform VM (Standard_D8s_v3, fixed) + - 1 cache VM (Standard_E4s_v3, fixed) + - 1 storage account (500 GB hot, 2 TB cool for backups) +- **Stamp capacity limit:** 10,000 concurrent users +- **Deployment regions:** eastus, westus2, and northeurope + +### Step 5.2: Forecast per-stamp demand + +Use utilization data from Exercise 2 to estimate: +- Concurrent users / peak demand per week +- Storage growth per region + +Example forecast: +- **Week 1:** 15,000 users across 2 stamps deployed (1 eastus, 1 westus2)—each stamp handles up to 10,000 users, so you're at 75% capacity +- **Week 12 (end of quarter):** 22,000 users—2 stamps can't cover the load, so deploy a 3rd stamp in northeurope + +### Step 5.3: Calculate quota and commitment needs + +From Exercise 1, your quota per region: 50 Standard_D2s_v3, 10 Standard_D8s_v3, 10 Standard_E4s_v3. + +With 2 stamps fully deployed: +- In-use: 6 D2s, 2 D8s, 2 E4s +- Headroom: 44 D2s, 8 D8s, 8 E4s + +Forecast shows you need 3 stamps by week 12 (9 D2s, 3 D8s, 3 E4s in-use). You're safe. But if westus2 is your high-demand region, you may hit quota limits in that region before others—request regional quota increase now. + +From Exercise 3, your savings plan commitment is $500/hour. With 2 stamps: +- Compute cost: ~$300/hour at on-demand +- Your commitment covers: $300 / $500 = 60% (acceptable) + +At 3 stamps (week 12): +- Compute cost climbs to ~$450/hour +- Commitment covers: $450 / $500 = 90% (tight but viable) + +Decision: Buy additional commitment buffer now for the 3rd stamp, or risk overages in week 12. + +### Step 5.4: Schedule capacity milestones + +Create a table: + +| Milestone | Timestamp | Stamps Deployed | Quota Headroom (D2s) | Commitment Buffer | Action | +|-----------|-----------|-----------------|----------------------|-------------------|--------| +| Current | Week 1 | 2 | 44 | $200/hr | Baseline | +| Q1 End | Week 12 | 3 | 41 | $50/hr | Request +10 D2s quota; buy +$200/hr commitment | +| Q2 End | Week 26 | 4 | 38 | Risk | Plan regional expansion | + +Reference the [Deployment Stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) and [Well-Architected scaling guidance](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling) for stamp-based capacity planning. + +### Step 5.5: Branch for target-region-unavailable scenarios + +If the target region is unavailable, run this branch before finalizing milestone dates: + +1. **Select escape region options** and map each option to required quota, service readiness, and expected latency impact ([Region access request process](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process), [Deployment Stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern)). +2. **Assess surge handling implications** for failover or temporary concentration in remaining regions, including autoscale and quota pressure in those regions ([Well-Architected scaling guidance](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling), [Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests)). +3. **Recalculate timeline impact** from supply constraints, including region access approval lead time and datacenter capacity constraints such as power and infrastructure pressure in the target geography ([Region access request process](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process)). + +Update your milestone table with a primary plan and an escape-region plan so procurement and allocation can proceed without reopening forecast assumptions. + +--- + +## Exercise 6: Review the Cost Optimization workbook + +**Objective:** Use Azure Advisor's Cost Optimization workbook to surface right-sizing and underutilization recommendations. + +Azure Advisor and the Cost Optimization workbook provide cost recommendation signals you can review alongside your own telemetry and forecast model ([Cost Optimization workbook](https://learn.microsoft.com/en-us/azure/advisor/advisor-workbook-cost-optimization), [Advisor VM and VMSS cost recommendations](https://learn.microsoft.com/en-us/azure/advisor/advisor-cost-recommendations)). + +### Step 6.1: Open the workbook + +1. In the Azure portal, go to **Azure Advisor** +2. Select **Cost Optimization** tab +3. Open **Cost Optimization workbook** (link in the top toolbar) + +Reference the [Cost Optimization workbook documentation](https://learn.microsoft.com/en-us/azure/advisor/advisor-workbook-cost-optimization) for setup details. + +### Step 6.2: Review recommendation categories + +The workbook typically surfaces recommendation categories across compute, storage, and commitment optimization, including Advisor recommendation tiles and usage optimization views ([Cost Optimization workbook](https://learn.microsoft.com/en-us/azure/advisor/advisor-workbook-cost-optimization)). + +Examples you should evaluate: + +**Underutilized VMs:** +- Advisor can recommend VM and VMSS resize or shutdown actions for low-utilization resources based on its documented utilization criteria and lookback configuration ([Advisor VM and VMSS cost recommendations](https://learn.microsoft.com/en-us/azure/advisor/advisor-cost-recommendations)). +- Decision: Right-size (move to smaller SKU), turn off during off-hours, or delete +- Impact on forecast: Treat underutilized recommendations as an efficiency signal, then validate with your own CPU, memory, and network telemetry before updating per-stamp efficiency assumptions ([Advisor VM and VMSS cost recommendations](https://learn.microsoft.com/en-us/azure/advisor/advisor-cost-recommendations)). + +**Unattached disks:** +- Disks not connected to any VM, still accruing cost +- Decision: Delete if no snapshot backups rely on them +- Impact: Cost reduction without compute capacity impact ([Cost Optimization workbook](https://learn.microsoft.com/en-us/azure/advisor/advisor-workbook-cost-optimization)). + +**Oversized database and storage:** +- Databases and storage accounts with excess reserved capacity +- Decision: Right-size tier or replication settings +- Impact: Reduces storage forecasting headroom; ensure measured growth justifies existing allocation ([Cost Optimization workbook](https://learn.microsoft.com/en-us/azure/advisor/advisor-workbook-cost-optimization)). + +**Unused reservations:** +- Commitments (RI / Savings Plans) with low utilization +- Decision: Evaluate if workload pattern changed; may need to trim commitment or accelerate utilization +- Impact: Informs the next commitment purchase decision (Exercise 3) ([Cost Optimization workbook](https://learn.microsoft.com/en-us/azure/advisor/advisor-workbook-cost-optimization), [Azure Reservations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations), [Azure savings plan for compute](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/savings-plan-compute-overview)). + +### Step 6.3: Cross-check against your forecast + +For each recommendation: +1. Does it contradict your utilization forecast from Exercise 2? + - If Advisor flags underutilization but your metrics show growth, treat that as a validation task before making SKU changes ([Advisor VM and VMSS cost recommendations](https://learn.microsoft.com/en-us/azure/advisor/advisor-cost-recommendations), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)). +2. Does it reveal an allocation misalignment? + - If one region shows persistent underutilization while demand grows elsewhere, validate stamp placement and regional distribution assumptions before you rebalance ([Deployment Stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)). +3. Does it confirm your savings plan coverage? + - If recommendation signals show persistent underutilization of commitments, re-check renewal targets against measured coverage and wastage trends ([Azure Reservations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations), [Azure savings plan for compute](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/savings-plan-compute-overview), [FinOps planning and estimating](https://www.finops.org/framework/capabilities/planning-estimating/)). + +### Step 6.4: Feed findings into procurement + +Document findings in your capacity planning tracker: +- Right-sizing actions: Record the recommendation, expected savings estimate, and target SKU change, then validate against your effective rates before actioning because Advisor savings are based on retail rates ([Advisor VM and VMSS cost recommendations](https://learn.microsoft.com/en-us/azure/advisor/advisor-cost-recommendations)). +- Headroom recovery: Consolidation frees X quota units per region and can delay the next quota increase window ([Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests), [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)). +- Commitment efficiency: Compare current commitment coverage against measured utilization, then adjust the next purchase target to reduce overage and wastage risk ([Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations), [Azure savings plan for compute](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/savings-plan-compute-overview), [Azure Reservations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations)). + +### Step 6.5: Add dev and test technical validation checkpoint + +Before closing the lab, add one validation row for each forecast action that changes dev or test capacity settings: + +- Environment and scope (subscription, region, and service) +- Planned change (quota, SKU, throughput, storage tier, or reservation target) +- Baseline measurement (query output timestamp and value) +- Post-change validation query and expected threshold +- Rollback trigger and evidence link + +Use the same evidence depth you apply to production checks so forecast assumptions stay testable across environments ([Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain)). + +--- + +## Wrap-up: From forecast to procurement + +You've now gathered all the signals needed to make infrastructure commitments with confidence: + +1. **Quota and usage (Exercise 1):** Know what you're allowed to run and what you're running +2. **Utilization patterns (Exercise 2):** Know the growth rate and peak load patterns +3. **Savings plan recommendations (Exercise 3):** Know the optimal commitment level and discount +4. **Storage and PaaS trends (Exercise 4):** Know which storage, disk, and PaaS resources drive cost and when they'll require scale action +5. **Stamp-level forecasts (Exercise 5):** Know when to deploy new stamps or request regional quota +6. **Advisor recommendations (Exercise 6):** Know where to recover efficiency and trim waste + +**Escalation evidence path:** + +When forecast outputs identify blockers, package technical evidence first, then open the correct Azure support path: + +- Classify blocker type: quota limit, region access restriction, zonal enablement restriction, or reservation mismatch ([Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests), [Region access request process](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process), [Zonal enablement request process](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series), [Capacity reservation overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview)). +- Attach reproducible evidence: failed deployment timestamp, operation ID, requested versus current limits, region and zone target, and forecasted shortfall window ([Create an Azure support request](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request)). +- Map blocker-to-request type so each item routes to the matching support request category without rework ([Create an Azure support request](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request)). + +Reference: [Support escalation guide](../operations/escalation/README.md). + +**Next steps:** +- Refresh these analyses monthly. Create a recurring task to run Exercises 1, 2, and 6 each month. +- Use 3-month utilization trends to refresh reservation and savings-plan targets, and update commitment levels when measured coverage and wastage drift from your forecast bounds. +- Align stamps with regions: review Exercise 5 data in your billing API to confirm actual per-region deployment matches your forecast assumptions. +- Archive historical CSVs (quota, utilization, storage) to track forecast accuracy and refine your projections for next quarter. + +References: [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain), [Azure Reservations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations), [Azure savings plan for compute](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/savings-plan-compute-overview). + +**FOCUS exports and reservation sizing:** +If you use FOCUS (FinOps Open Cost and Usage Specification) exports from Cost Management, map commitment discount utilization to your reservation sizing model. See the [FOCUS exports overview](https://learn.microsoft.com/en-us/cloud-computing/finops/focus/overview) for guidance on structuring cost data for forecasting and commitment planning. + +**Operationalization cadence:** +Define and run a recurring review cadence for utilization analysis, projection updates, and forecast validation so quota and commitment decisions stay aligned with measured demand and delivery timelines ([Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain)). + +**Release gates:** +Before each stamp deployment, validate three gates: +1. Quota headroom ≥ scale unit requirement (from Exercise 5) +2. CRG reserved quantity ≥ deployment target (if applicable) +3. Budget posture allows the spend increase + +These gates prevent quota exhaustion, commitment overages, and budget surprises when you enforce them before each release ([Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests), [Capacity reservation overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview), [Use cost alerts to monitor usage and spending](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending), [Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain)). + +References: [Per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests), [Capacity reservation overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview), [Use cost alerts to monitor usage and spending](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending). + +The output of forecasting feeds directly into: +- **Allocation decisions:** How much quota and commitment per region? +- **Procurement timing:** When to renew savings plans or request capacity increases? +- **Architecture reviews:** Are stamp capacities sized correctly, or is one region a bottleneck? + +For deeper guidance on the FinOps discipline around forecasting, refer to the [FinOps Framework planning and estimating section](https://www.finops.org/framework/capabilities/planning-estimating/). diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-04-procurement.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-04-procurement.md new file mode 100644 index 000000000..0c5a4470c --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-04-procurement.md @@ -0,0 +1,731 @@ +# Lab 4: Procurement + +## Overview + +This lab covers how to procure compute capacity on Azure for production workloads. You'll work with [capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) to guarantee supply, assess zone alignment across subscriptions, configure sharing for multi-subscription architectures, and layer pricing commitments (Azure Reservations and savings plans) on top of supply guarantees. + +The core concept: capacity reservations are supply instruments, not pricing instruments. They guarantee compute availability at pay-as-you-go rates. Pricing commitments (reservations and savings plans) are separate knobs that stack on top. + +By the end of this lab, you'll understand: + +- How to validate SKU availability before committing to capacity +- How to create and manage capacity reservation groups (CRGs) +- How to associate workloads with reserved capacity +- How to share reservations across subscriptions and handle zone alignment +- How to monitor utilization and overallocation +- How to evaluate savings plans and spot VMs as complementary procurement levers + +**Prerequisites:** Access to an Azure subscription with contributor permissions, Azure CLI or PowerShell Az module installed, and familiarity with resource groups and virtual machines. + +--- + +## Exercise 1: Validate SKU availability and zone alignment + +Before creating capacity reservations, confirm that the VM SKUs you need are available in your target regions and zones. Zone alignment matters when sharing CRGs across subscriptions—logical zone 1 in subscription A may map to a different physical zone than zone 1 in subscription B. + +### Step 1: Check SKU availability in a region + +Use `Get-AzComputeResourceSku` to list available VM SKUs: + +```powershell +Get-AzComputeResourceSku ` + -Location "eastus" | + Where-Object { + $_.ResourceType -eq "virtualMachines" -and + $_.Name -eq "Standard_D4s_v5" + } | + Select-Object Name, Locations, Restrictions +``` + +This returns the SKU name, regions where it's available, and any restrictions (regional, zonal, or license-related). + +### Step 2: Map logical zones to physical zones + +Capacity reservations are created in specific logical zones (1, 2, 3). When you share a CRG across subscriptions, you need to confirm that logical zone 1 in the producer subscription aligns with the same physical infrastructure in the consumer subscription. + +Run the provided script to fetch zone mappings: + +```powershell +.\scripts/quota/Get-AzAvailabilityZoneMapping.ps1 ` + -SubscriptionId "your-subscription-id" ` + -Location "eastus" +``` + +Output shows: + +``` +TenantId : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +SubscriptionId : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +Location : eastus +LogicalZone : 1 +PhysicalZone : 3 +PhysicalZoneName : US-VA-AZ-3 +``` + +Run this for both the producer (CRG owner) and consumer subscriptions. If physical zones don't align, shared workloads may see latency or cross-zone traffic charges. + +### Step 3: Check zone restrictions + +Some SKUs are restricted to specific zones. Query the Compute Resource SKU API: + +```powershell +Get-AzComputeResourceSku ` + -Location "eastus" | + Where-Object { $_.Restrictions -ne $null } | + Select-Object Name, @{ Label="RestrictionType"; Expression={$_.Restrictions.Type} } +``` + +If a SKU shows `ZonesRestricted`, it's available only in certain zones. If `RegionRestricted`, file a [region access request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process). + +--- + +## Exercise 2: Create a capacity reservation group + +A [capacity reservation group (CRG)](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) is a container for one or more capacity reservations in a specific region and logical zone. All VMs associated with a CRG must use the same zone. + +### Step 1: Create a CRG via Azure portal + +1. Go to **Home** > **Create a resource** > search for "capacity reservation group" +2. Select **Capacity Reservation Group** and choose **Create** +3. Fill in: + - **Subscription:** Your target subscription + - **Resource group:** Create or select one + - **Name:** e.g., `crg-eastus-z1-prod` + - **Region:** e.g., East US + - **Availability zone:** e.g., 1 +4. Select **Review + create**, then **Create** + +### Step 2: Create a CRG via Azure CLI + +```bash +az capacity reservation group create \ + --resource-group "rg-prod" \ + --name "crg-eastus-z1-prod" \ + --location "eastus" \ + --zones 1 +``` + +### Step 3: Create a CRG via ARM template + +```json +{ + "type": "Microsoft.Compute/capacityReservationGroups", + "apiVersion": "2023-09-01", + "name": "crg-eastus-z1-prod", + "location": "eastus", + "zones": ["1"], + "properties": {} +} +``` + +Deploy with: + +```bash +az deployment group create \ + --resource-group "rg-prod" \ + --template-file "crg-template.json" +``` + +### Step 4: Verify creation + +```bash +az capacity reservation group show \ + --resource-group "rg-prod" \ + --name "crg-eastus-z1-prod" +``` + +You now have an empty CRG. Next, you'll add capacity reservations to it. + +--- + +## Exercise 2b: Verify two-sided quota constraints + +Before creating a capacity reservation group, confirm you have sufficient quota headroom. Quota works in two dimensions: total regional vCPU and per-family vCPU. Deployments must satisfy both constraints simultaneously, and CRG creation itself consumes quota. + +### Step 1: Understand the quota dimensions + +- **Regional vCPU limit:** Total vCores allocated to a single family (e.g., Standard D family) in a region, regardless of zone +- **Per-VM-family vCPU limit:** Individual per-family caps on regional allocation + +When you create a capacity reservation for 10 Standard_D4s_v5 instances (40 vCores), those vCores are reserved immediately and count against your regional quota. If you have only 35 vCores available in the Standard D quota, the CRG creation will fail. + +### Step 2: Check quota headroom + +Use the quota script to confirm you have room: + +```powershell +.\scripts/quota/Get-AzVMQuotaUsage.ps1 ` + -SubscriptionId "" ` + -Location "eastus" | + Where-Object { $_.SKU -match "Standard_D" } +``` + +For each family, calculate available headroom: + +``` +AvailableQuota = QuotaLimit - UsedQuota +``` + +If you plan to create a CRG for 10 D4s_v5 instances (40 vCores), ensure `AvailableQuota >= 40` in the Standard D family quota. + +### Step 3: Common mistake—forgetting the headroom check + +A common error: teams request a CRG without checking available quota first. The request fails at creation time, delaying workload deployments and forcing emergency quota increases. Always verify both dimensions before submitting the CRG creation request. + +### Step 4: Plan quota growth + +If your procurement roadmap spans multiple quarters, request quota increases early. Azure quota requests typically complete within 1–2 business days, so build review time into your release schedule. See the [VM quotas](https://learn.microsoft.com/en-us/azure/virtual-machines/quotas) reference for details on requesting increases. + +--- + +## Exercise 3: Add member reservations and associate workloads + +A capacity reservation is a reservation for a specific VM SKU and quantity within a CRG. Once created, you can [deploy VMs and associate them with the reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-associate-vm). + +### Step 1: Create a capacity reservation within the CRG + +Via Azure CLI: + +```bash +az capacity reservation create \ + --resource-group "rg-prod" \ + --capacity-reservation-group-name "crg-eastus-z1-prod" \ + --name "capres-standard-d4s-v5" \ + --sku "Standard_D4s_v5" \ + --quantity 10 +``` + +This reserves 10 instances of Standard_D4s_v5 in zone 1 of East US. The reservation runs at pay-as-you-go rates—you're charged for all 10 slots whether or not you deploy VMs into them. + +### Step 2: Deploy a VM and associate it with the reservation + +Via Azure CLI: + +```bash +az vm create \ + --resource-group "rg-prod" \ + --name "vm-prod-01" \ + --image "UbuntuLTS" \ + --size "Standard_D4s_v5" \ + --zone 1 \ + --capacity-reservation-group "/subscriptions//resourceGroups/rg-prod/providers/Microsoft.Compute/capacityReservationGroups/crg-eastus-z1-prod" +``` + +Replace `` with your subscription ID. The `--zone` must match the CRG's zone. + +### Step 3: Deploy via ARM template + +```json +{ + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "vm-prod-01", + "location": "eastus", + "zones": ["1"], + "properties": { + "capacityReservationGroup": { + "id": "/subscriptions//resourceGroups/rg-prod/providers/Microsoft.Compute/capacityReservationGroups/crg-eastus-z1-prod" + }, + "hardwareProfile": { + "vmSize": "Standard_D4s_v5" + }, + "osProfile": { + "computerName": "vm-prod-01", + "adminUsername": "azureuser" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-focal", + "sku": "20_04-lts-gen2", + "version": "latest" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions//resourceGroups/rg-prod/providers/Microsoft.Network/networkInterfaces/nic-prod-01" + } + ] + } + } +} +``` + +### Step 4: Check utilization + +Query the reservation's instanceView to see allocated vs. reserved capacity: + +```bash +az capacity reservation show \ + --resource-group "rg-prod" \ + --capacity-reservation-group-name "crg-eastus-z1-prod" \ + --name "capres-standard-d4s-v5" \ + --expand "instanceview" +``` + +Look for: +- `availableCapacity`: slots not yet allocated to VMs +- `virtualMachinesAllocated`: count of running VMs using this reservation +- `remainingCapacity`: available slots + +If `virtualMachinesAllocated` exceeds the reservation quantity, those excess VMs are in overallocation mode—they run without the supply SLA. + +--- + +## Exercise 4: Share a CRG across subscriptions + +Capacity reservations are created in a producer subscription and can be [shared with up to 100 consumer subscriptions](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share). This pattern is common in multi-tenant ISV platforms where a central team manages procurement and consumer teams deploy workloads. + +### Step 1: Configure sharing on the CRG + +Before enabling sharing, use this pre-share checklist to ensure readiness: + +**Pre-share checklist:** + +- **Zone mapping validated:** Run the zone mapping script in both producer and consumer subscriptions (from Exercise 1). Confirm that logical zones align (e.g., logical zone 1 maps to the same physical zone in both subscriptions) +- **Consumer subscriptions identified:** List all subscriptions that will use the shared CRG and confirm access permissions +- **RBAC roles assigned:** The consumer subscription (or specific service principals) must have Reader access at minimum. Confirm the identities that will associate VMs with the CRG +- **Billing scope aligned:** Decide whether the CRG's cost should roll up to the producer subscription's cost center or be shared. Align CRG ownership to cost boundaries so the team that owns the capacity also sees the cost in their billing scope + +In the producer subscription, grant the consumer subscription access to the CRG: + +```bash +az role assignment create \ + --role "Reader" \ + --assignee-object-id "" \ + --scope "/subscriptions//resourceGroups/rg-prod/providers/Microsoft.Compute/capacityReservationGroups/crg-eastus-z1-prod" +``` + +For UI-based assignment, go to the CRG in the producer subscription: +1. Select **Access control (IAM)** +2. Select **Add** > **Add role assignment** +3. Role: **Reader** +4. Assign to: Service principal or user from the consumer subscription +5. Select **Save** + +See [capacity reservation group sharing](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share) for detailed steps. + +### Step 2: Deploy from a consumer subscription + +In the consumer subscription, create a VM and reference the CRG by its full resource ID: + +```bash +az vm create \ + --resource-group "rg-consumer" \ + --name "vm-consumer-01" \ + --image "UbuntuLTS" \ + --size "Standard_D4s_v5" \ + --zone 1 \ + --capacity-reservation-group "/subscriptions//resourceGroups/rg-prod/providers/Microsoft.Compute/capacityReservationGroups/crg-eastus-z1-prod" +``` + +### Step 3: Verify zone alignment + +Before deploying from the consumer subscription, confirm that logical zone 1 in the producer subscription aligns with logical zone 1 in the consumer subscription. Run the zone mapping script in both subscriptions and compare physical zone names. If they differ, file a [zonal enablement request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) to address the misalignment. + +### Step 4: Monitor shared utilization + +The producer subscription owner can see all allocated VMs across all consumer subscriptions. In the producer subscription: + +```bash +az capacity reservation show \ + --resource-group "rg-prod" \ + --capacity-reservation-group-name "crg-eastus-z1-prod" \ + --name "capres-standard-d4s-v5" \ + --expand "instanceview" +``` + +The `virtualMachinesAllocated` count includes VMs from consumer subscriptions. + +--- + +## Exercise 5: Monitor overallocation and utilization + +[Overallocation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) happens when you deploy more VMs than the reserved quantity. Overallocated VMs run without the supply SLA and can be interrupted if capacity is needed. + +### Step 1: Deploy beyond reserved capacity + +You have a reservation for 10 Standard_D4s_v5 instances. Deploy 12 VMs: + +```bash +for i in {1..12}; do + az vm create \ + --resource-group "rg-prod" \ + --name "vm-prod-$i" \ + --image "UbuntuLTS" \ + --size "Standard_D4s_v5" \ + --zone 1 \ + --capacity-reservation-group "/subscriptions//resourceGroups/rg-prod/providers/Microsoft.Compute/capacityReservationGroups/crg-eastus-z1-prod" \ + --no-wait +done +``` + +### Step 2: Check utilization states + +```bash +az capacity reservation show \ + --resource-group "rg-prod" \ + --capacity-reservation-group-name "crg-eastus-z1-prod" \ + --name "capres-standard-d4s-v5" \ + --expand "instanceview" +``` + +The output shows three capacity states: + +1. **Available**: Reserved slots not yet allocated (reserved - allocated) +2. **Consumed**: VMs running within the reserved quantity +3. **Overallocated**: VMs beyond the reserved quantity (virtualMachinesAllocated - reserved quantity) + +For example, if reserved = 10 and allocated = 12, you have 0 available, 10 consumed, and 2 overallocated. + +### Step 3: Understand overallocation implications + +Overallocated VMs don't have the availability SLA from the capacity reservation. They're useful for non-critical or batch workloads, but shouldn't be used for production services. + +If you want to raise the reservation, run: + +```bash +az capacity reservation update \ + --resource-group "rg-prod" \ + --capacity-reservation-group-name "crg-eastus-z1-prod" \ + --name "capres-standard-d4s-v5" \ + --quantity 15 +``` + +--- + +## Exercise 5b: Apply the create-migrate-delete pattern + +You can't edit a capacity reservation's VM size or location in place. If you need to change a reservation's parameters, you must create a new reservation with the target specs, migrate running workloads, and delete the old reservation. This pattern has a cost implication: you'll pay for both reservations during the migration window. + +### Step 1: Understand the immutability constraint + +Once a capacity reservation is created, you cannot modify: + +- SKU (e.g., changing from Standard_D4s_v5 to Standard_D8s_v5) +- Availability zone (e.g., moving from zone 1 to zone 2) +- Region (e.g., moving from East US to West US) + +If your requirements change, the create-migrate-delete pattern is the only path forward. + +### Step 2: Create a new CRG and capacity reservation + +Create a new capacity reservation group in the target zone (if needed) and a new capacity reservation with the new SKU: + +```bash +az capacity reservation group create \ + --resource-group "rg-prod" \ + --name "crg-eastus-z1-prod-new" \ + --location "eastus" \ + --zones 1 + +az capacity reservation create \ + --resource-group "rg-prod" \ + --capacity-reservation-group-name "crg-eastus-z1-prod-new" \ + --name "capres-standard-d8s-v5" \ + --sku "Standard_D8s_v5" \ + --quantity 10 +``` + +### Step 3: Migrate workloads step by step + +Follow this sequence to minimize downtime: + +1. **Disassociate VMs from the old CRG** (they move to pay-as-you-go mode): + +```bash +az vm update \ + --resource-group "rg-prod" \ + --name "vm-prod-01" \ + --capacity-reservation-group "" +``` + +2. **Associate the VM with the new CRG** (it now uses the new reservation): + +```bash +az vm update \ + --resource-group "rg-prod" \ + --name "vm-prod-01" \ + --capacity-reservation-group "/subscriptions//resourceGroups/rg-prod/providers/Microsoft.Compute/capacityReservationGroups/crg-eastus-z1-prod-new" +``` + +3. **Verify the migration** by checking the instanceView: + +```bash +az vm show \ + --resource-group "rg-prod" \ + --name "vm-prod-01" \ + --expand "instanceview" | + jq '.capacityReservationGroup' +``` + +4. **Repeat for all workloads** until the old CRG is empty. + +### Step 4: Understand the billing impact + +During the migration window, you're paying for both the old and new capacity reservations. For example, if you're migrating 10 instances and each pays 10 vCores, you'll pay for 20 vCores across both reservations during the migration. + +**Minimize double-billing:** + +- Schedule migrations during off-peak hours to reduce the number of simultaneous VMs +- Batch VM migrations to complete within 1–2 hours rather than spreading over days +- Request a cost allocation tag or cost center split to track the migration costs separately + +### Step 5: Delete the old CRG + +Once all workloads have migrated, delete the old capacity reservation and CRG: + +```bash +az capacity reservation delete \ + --resource-group "rg-prod" \ + --capacity-reservation-group-name "crg-eastus-z1-prod" \ + --name "capres-standard-d4s-v5" + +az capacity reservation group delete \ + --resource-group "rg-prod" \ + --name "crg-eastus-z1-prod" +``` + +See [Capacity reservation overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) for more details on managing reservations across their lifecycle. + +--- + +## Exercise 6: Pull savings plan recommendations and compare rate instruments + +Capacity reservations guarantee supply at standard pay-as-you-go rates. To reduce costs further, layer pricing commitments—Azure Reservations (1-year or 3-year) or Savings Plans (1-year or 3-year)—on top of reserved capacity. + +### Step 1: Get savings plan recommendations + +Use the provided script to fetch recommendations from the Cost Management API: + +```powershell +.\scripts/rate/Get-BenefitRecommendations.ps1 ` + -BillingScope "/subscriptions/" ` + -LookBackPeriod 30 ` + -Term "P1Y" +``` + +This queries your actual usage over the past 30 days and recommends purchase quantities for a 1-year savings plan. Output includes: + +``` +SKU : Standard_D4s_v5 +RecommendedQty : 8 +RecommendedSpend : $18,000 +AnnualSavings : $4,500 +``` + +The 80% heuristic: savings plans and reservations should cover ~80% of your expected usage. The remaining 20% runs at pay-as-you-go rates, giving you flexibility without over-committing. + +### Step 2: Create a savings plan in the Azure portal + +1. Go to **Cost Management + Billing** > **Savings plans** +2. Select **Buy savings plan** +3. Choose **Compute** and configure: + - **Term:** 1-year or 3-year + - **Billing frequency:** Monthly or upfront + - **Scope:** Subscription or shared (if multi-subscription) +4. Review the recommendation and confirm purchase + +See [savings plans for compute](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/savings-plan-compute-overview) for full details. + +### Step 3: Compare rate instruments + +You now have three layers: + +1. **Capacity reservation (supply instrument):** Guarantees supply at PAYG rates. Unused slots cost money. +2. **Savings plan (pricing commitment):** Applies a discounted rate across your compute usage. Doesn't guarantee supply. +3. **Azure Reservation (pricing commitment):** Instance-based discount for 1-year or 3-year terms. Doesn't guarantee supply. + +Strategy: Use capacity reservations for SKUs and regions where you need guaranteed supply. Layer savings plans or reservations on top to reduce the effective rate. For example: + +- Reserve 80% of expected D4s_v5 demand in East US Zone 1 via a capacity reservation group +- Purchase a 1-year savings plan for 80% of the same volume to reduce the effective rate to ~25% below PAYG +- Let the remaining 20% run at PAYG rates to absorb spikes without over-committing + +### Step 4: Monitor blended cost + +Query your actual costs with and without commitments: + +```bash +az costmanagement query --type "Usage" \ + --timeframe "MonthToDate" \ + --granularity "Daily" \ + --aggregation '{"totalCost":{"name":"PreTaxCost","function":"Sum"}}' \ + --grouping '[{"type":"Dimension","name":"ChargeType"}]' +``` + +This breaks down costs by charge type, helping you see the impact of capacity reservations and savings plans. See [FinOps rate optimization](https://www.finops.org/framework/capabilities/rate-optimization/) for rate optimization strategy. + +--- + +## Exercise 7: Evaluate Spot VM placement scores + +Spot VMs run on excess Azure capacity at steep discounts (up to 90% off PAYG) but can be evicted with 30 seconds' notice. They're ideal for non-critical workloads (batch jobs, dev/test, fault-tolerant services). + +### Step 1: Check Spot placement scores + +Before deploying Spot VMs, use the Spot Placement Score API to assess deployment success likelihood: + +Via Azure CLI: + +```bash +az compute-recommender spot-placement-score \ + --location "eastus" \ + --desired-locations "eastus" "westus2" \ + --desired-sizes "[{sku:Standard_D4s_v5}]" \ + --desired-count 10 +``` + +Via REST API: + +```bash +az rest \ + --method post \ + --url "/subscriptions//providers/Microsoft.Compute/locations/eastus/placementScores/spot/generate?api-version=2025-06-05" \ + --body '{ + "desiredLocations": ["eastus", "westus2"], + "desiredSizes": [{"sku": "Standard_D4s_v5"}], + "desiredCount": 10, + "availabilityZones": false + }' +``` + +Output includes a placement score—**High**, **Medium**, or **Low**—for each SKU-region combination, along with quota availability. A score of High indicates the deployment is highly likely to succeed; Low indicates low likelihood. Scores are point-in-time recommendations and don't guarantee allocation. If a SKU isn't available for your subscription, the API returns **Restricted SKU** instead of a placement score. + +See the [Spot Placement Score documentation](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/spot-placement-score) for details. + +### Step 2: Deploy a Spot VM + +```bash +az vm create \ + --resource-group "rg-prod" \ + --name "vm-spot-01" \ + --image "UbuntuLTS" \ + --size "Standard_D4s_v5" \ + --priority "Spot" \ + --max-price "0.50" \ + --eviction-policy "Deallocate" +``` + +Set `--max-price` to your bid. If the spot price exceeds your bid, the VM is evicted. Use `--eviction-policy "Delete"` for stateless workloads or `--eviction-policy "Deallocate"` to pause the VM and avoid re-initialization costs. + +### Step 3: Understand Spot capacity pool + +Spot VMs use a separate capacity pool from reserved or standard PAYG VMs. You can have: + +- 10 Standard_D4s_v5 instances in a capacity reservation group (supply SLA) +- 12 Standard_D4s_v5 PAYG instances (no SLA) +- 50 Standard_D4s_v5 Spot instances (no SLA, up to 90% discount) + +All three types use the same SKU but different pricing and SLA models. See [Spot VMs overview](https://learn.microsoft.com/en-us/azure/virtual-machines/spot-vms) for more detail. + +--- + +## Exercise 8: Validate quota before large procurements + +[Quota](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) limits how many vCores of each family you can allocate in a region. Before creating capacity reservations, confirm you have sufficient quota. + +### Step 1: Check current quota usage + +```powershell +.\scripts/quota/Get-AzVMQuotaUsage.ps1 ` + -SubscriptionId "" ` + -Location "eastus" +``` + +Output shows: + +``` +SKU : Standard_D4s_v5 +UsedQuota : 6 +QuotaLimit : 20 +AvailableQuota : 14 +RegionRestricted : False +ZonesRestricted : False +``` + +In this case, you've allocated 6 instances of D4s_v5 out of a 20-instance limit in East US. + +### Step 2: Request a quota increase + +If your procurement plan exceeds available quota, request an increase: + +1. Go to **Help + support** > **New support request** +2. **Issue type:** Service and subscription limits (quotas) +3. **Quota type:** Compute-VM (cores) quotas +4. **Location:** Your target region +5. **SKU:** e.g., Standard D family +6. **New limit:** Enter desired quota +7. Submit the request + +Azure reviews quota requests based on capacity availability. Requests typically complete within 1–2 business days. + +### Step 3: Plan quota for multi-region and multi-zone + +If you're procuring Standard_D4s_v5 across East US zone 1, East US zone 2, and West US zone 1, quota is counted per-region (not per-zone). A 30-instance capacity reservation in East US consumes 120 vCores from the East US Standard D quota, regardless of zone distribution. + +--- + +## Wrap-up + +Procurement is the foundation for capacity planning, cost management, and availability. The outputs of this lab feed into: + +1. **Monitoring & Observability (Deck 5):** Track actual vs. reserved utilization, alert on overallocation, and audit cost allocation across teams +2. **Governance & automation:** Enforce procurement policies (e.g., "all prod VMs must use capacity reservations"), automate quota requests, and scale procurement based on demand signals + +Key takeaways: + +- Validate SKU availability and zone alignment before committing capacity +- Use capacity reservations for supply guarantees; layer savings plans or reservations for cost reduction +- Share CRGs across subscriptions for centralized procurement, but monitor zone alignment +- Spot VMs complement reserved capacity for non-critical, cost-sensitive workloads +- Monitor utilization regularly and adjust reservations quarterly based on actual demand +- Quota is a guardrail; request increases early to avoid allocation delays during scale-outs + +### Billing scope alignment + +Cost allocation varies by your agreement type. Align CRG boundaries to match your cost structure: + +- **Enterprise Agreements (EA):** Capacity reservation purchases are visible at the billing account (enrollment) scope. If you have multiple cost centers or departments, create separate CRGs per cost center to track procurement costs separately +- **Microsoft Customer Agreement (MCA):** Capacity reservation purchases are visible at the billing profile scope. Align CRG ownership to billing profile boundaries so costs roll up correctly for chargeback or FinOps analysis + +See [Understand Cost Management scopes](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/understand-work-scopes) for guidance on mapping subscriptions, resource groups, and billing accounts to your organizational structure. + +--- + +## Beyond VMs: PaaS capacity procurement + +CRGs are VM-specific. For PaaS services, procurement uses reserved capacity pricing commitments rather than supply-side reservations. These commitments lock in discounted rates for 1-year or 3-year terms but don't guarantee physical capacity the way CRGs do. + +- **Premium SSD v2:** Provisioned IOPS and throughput are billed independently from disk size. You configure capacity by adjusting IOPS and throughput values on the managed disk resource—there's no separate reservation mechanism. Purchase through the Azure portal or `az disk create`/`az disk update` with `--disk-iops-read-write` and `--disk-mbps-read-write` parameters. See [Premium SSD v2 overview](https://learn.microsoft.com/en-us/azure/virtual-machines/disks-types#premium-ssd-v2) for provisioning details. + +- **Azure Database for PostgreSQL flexible server:** [Reserved capacity](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-reserved-capacity) provides vCore-based pricing discounts for 1-year or 3-year terms. Purchase through **Cost Management + Billing** > **Reservations** > **Add** > **Azure Database for PostgreSQL** in the portal, or use the [Reservations REST API](https://learn.microsoft.com/en-us/rest/api/reserved-vm-instances/). + +- **Azure Database for MySQL flexible server:** [Reserved capacity](https://learn.microsoft.com/en-us/azure/mysql/flexible-server/concept-reserved-pricing) works the same way as PostgreSQL—vCore-based 1-year or 3-year commitments purchased through the portal reservations blade or REST API. + +- **Azure SQL Managed Instance:** [Reserved capacity](https://learn.microsoft.com/en-us/azure/azure-sql/managed-instance/reserved-capacity?view=azuresql) provides vCore-based pricing discounts by service tier (General Purpose, Business Critical). Purchase through the portal reservations blade, selecting the service tier, region, and term. Use `az reservations reservation-order purchase` for CLI-based procurement. + +For all PaaS reserved capacity, the pricing discount applies automatically to matching resources in the reservation scope (subscription or shared). Unlike CRGs, these don't guarantee that capacity will be available at deployment time—they're cost optimization instruments only. + +--- + +## References + +- [Capacity reservation overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) +- [Capacity reservation group sharing](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share) +- [Overallocation in capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) +- [Associate VM to capacity reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-associate-vm) +- [Associate VMSS to capacity reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-associate-virtual-machine-scale-set-flex) +- [Remove VM from capacity reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-remove-vm) +- [Reserved Instance discounts](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations) +- [Savings plans for compute](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/savings-plan-compute-overview) +- [Spot VMs overview](https://learn.microsoft.com/en-us/azure/virtual-machines/spot-vms) +- [Spot Placement Score API](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/spot-placement-score) +- [Get-AzComputeResourceSku cmdlet](https://learn.microsoft.com/en-us/powershell/module/az.compute/get-azcomputeresourcesku) +- [FinOps rate optimization](https://www.finops.org/framework/capabilities/rate-optimization/) +- [Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations) +- [Zonal enablement requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) +- [Region access requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) +- [VM quotas](https://learn.microsoft.com/en-us/azure/virtual-machines/quotas) +- [Understand Cost Management scopes](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/understand-work-scopes) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-05-monitoring-governance.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-05-monitoring-governance.md new file mode 100644 index 000000000..fc70d0c00 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/labs/lab-05-monitoring-governance.md @@ -0,0 +1,807 @@ +# Lab 5: Monitoring & governance + +## Overview + +This lab walks you through the operational controls that keep your Azure consumption predictable and aligned with business objectives. You'll set up quota alerts to catch capacity constraints early, deploy budgets to track spend against forecasts, implement cost anomaly detection, monitor capacity reservation utilization, suppress noisy advisor recommendations, and build dashboards that feed forecasting decisions. These controls align with the [workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) model and [cost management alerting](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending). + +We focus on the mechanics—the knobs you turn, the thresholds you set, the data you extract—not prescriptive governance models. The tools here assume you're running SaaS workloads under an EA or Microsoft Customer Agreement with multiple subscriptions organized under management groups. + +### Prerequisites + +- Access to at least two subscriptions (one for exercises 1–5, one additional for scale exercises) +- Subscriptions organized under a management group +- Contributor role on subscriptions; Advisor Contributor role for suppression exercises +- Azure CLI installed with the quota extension +- PowerShell 7+ with Azure modules (Az.CostManagement, Az.Advisor) +- Action groups already created for alerts (or create one during exercise 1) +- Read-only access to the lab script repository (scripts/ directory) + +### Lab goals + +By the end of this lab, you'll be able to: + +- configure quota usage alerts at 60%, 80%, and 90% thresholds +- deploy budgets programmatically to individual subscriptions and at scale +- activate cost anomaly detection for anomalous spend patterns +- query and interpret capacity reservation allocation states +- suppress Advisor recommendations with time-to-live (TTL) constraints +- export quota usage data and build trend dashboards +- integrate monitoring signals into your forecasting feedback loop + +--- + +## The 4-signal model + +Monitoring in this context means tracking four signals: + +- **Signal 1: Quota**—usage vs. limit by subscription, region, and VM family +- **Signal 2: Reservation**—capacity reservation group utilization and overallocation state via instanceView +- **Signal 3: Cost**—budget consumption, anomaly detection, and commitment utilization +- **Signal 4: Gate**—CI/CD pipeline pass/fail for capacity checks before stamp deployments + +Each signal maps to a different enforcement point and a different response. The exercises below cover all four signals. + +Reference: [Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) + +--- + +## Exercise 1: Create quota usage alerts + +### Goal + +Set up proactive quota alerts so you catch capacity constraints before they block deployments. You'll use the My quotas portal experience to configure thresholds and attach action groups. + +### Context + +Azure quotas limit the number of resources you can create in a subscription by type—compute cores, network interfaces, public IPs, and so on. When a quota fills up, new resource deployments fail. Quota alerts notify you at 60%, 80%, and 90% utilization so you can request quota increases before you hit the hard limit. + +The alert mechanism integrates with action groups to send emails, webhooks, or other actions when thresholds are crossed. This guidance describes [quota monitoring and alerting](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting). + +### Steps + +1. Sign in to the Azure portal and go to **Quotas** (search "quotas" in the top bar). + +2. Filter to your subscription and region. You'll see a table of resource types with current usage and limits. + +3. Select a quota you want to monitor—for example, **Standard DSv3 Family vCPUs** in East US. Select the checkbox next to it. + +4. Choose **Create alert**. The alert creation panel opens. + +5. Select or create an action group. If creating one: + - Set **Action Group Name** to something like `quota-alerts-compute` + - Set **Short Name** to `quota-cmp` + - Add a receiver (email, webhook, etc.) + +6. Configure thresholds and severity tiers: + - Enter **60** for the first threshold (Severity 3 = informational, no action) + - Enter **80** for the second (Severity 2 = plan quota action) + - Enter **90** for the third (Severity 1 = block new stamps) + - Enter **95** for the fourth (Severity 0 = critical/emergency, escalate immediately and block stamp expansion) + - Leave **100** to alert on hard limit + + Each severity tier should route to a different action group. For example: + - 60% → email to the platform team (informational) + - 80% → email to platform and finance teams (planning phase) + - 90% → email and webhook to PagerDuty on-call rotation (deployment blocker) + - 95% → immediate escalation via PagerDuty, ITSM ticket creation, and leadership notification (emergency) + +7. Enter an alert name like `quota-alert-dsvx-eastus-60pct` and select **Create**. + +8. Repeat for other quota types relevant to your workloads (vCPUs, managed disks, network interfaces). Create separate action groups for each severity level to ensure the right people get the right alerts at the right time. + +### Validation + +After 5–10 minutes, the alerts appear in **Monitor > Alerts**. When utilization crosses a threshold, the action group fires. You can test by selecting **Test action group** from the alert rule's detail page. + +--- + +## Exercise 2: Deploy subscription budgets + +### Goal + +Deploy a budget to a single subscription using the `Deploy-Budget.ps1` script, configure thresholds, and verify it tracks actual spend. + +### Context + +Budgets are subscription-scoped cost controls. They track actual and forecasted spend and trigger alerts at your chosen thresholds (50%, 75%, 90%, and optionally 100% of forecasted spend). Unlike quotas, budgets don't block resources—they notify you so you can course-correct. This guidance describes [budget alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending). + +The `Deploy-Budget.ps1` script automates budget creation. It accepts budget amounts as parameters or reads them from a subscription tag (`BudgetAmount`). This approach lets you manage per-subscription budgets without editing scripts. + +### Steps + +1. Identify or create a subscription tag called `BudgetAmount` with a value—for example, `5000` (USD). This becomes your monthly budget cap. If you omit the tag, the script defaults to `$10`. + +2. Open PowerShell and go to the lab scripts directory. + +3. Run the script with parameters: + +```powershell +$contactEmails = @("platform-team@example.com", "finance@example.com") +$subscriptionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +.\Deploy-Budget.ps1 ` + -SubscriptionId $subscriptionId ` + -ContactEmails $contactEmails ` + -Amount 5000 ` + -TimeGrain Monthly ` + -StartDate (Get-Date) ` + -FirstThreshold 50 ` + -SecondThreshold 75 ` + -ThirdThreshold 90 ` + -ForecastedThreshold 100 +``` + +4. The script creates a budget named `subscription-budget-` with action groups targeting your contact emails. + +5. Verify in the portal: go to **Cost Management > Budgets** and confirm the budget appears with correct thresholds. + +### Parameters explained + +- `-Amount`: monthly budget in currency units (USD by default) +- `-TimeGrain`: Monthly, Quarterly, or Annually +- `-ContactEmails`: array of email addresses for alerts +- `-FirstThreshold`, `-SecondThreshold`, `-ThirdThreshold`, `-ForecastedThreshold`: percentage thresholds (0–100) +- `-WhatIf`: preview changes without applying +- `-Force`: skip confirmation prompts +- `-Quiet`: suppress console output + +### Validation + +In Cost Management, select the budget and confirm it shows: + +- Start and end dates +- Threshold alerts listed +- Associated action group(s) + +Wait 24 hours for the first cost data to populate (Cost Management refreshes daily). Then check **Alerts** to see if any fire. + +--- + +## Exercise 3: Deploy budgets at scale + +### Goal + +Use `Deploy-BulkBudgets.ps1` to deploy budgets across all subscriptions under a management group. Preview changes with `-WhatIf` before applying. + +### Context + +When you have dozens or hundreds of subscriptions, deploying budgets one at a time is unsustainable. The bulk script iterates subscriptions under a management group and applies the same budget template to each. Tag-based amounts let you vary the budget per subscription without script changes. This guidance describes [budget alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending). + +### Steps + +1. Ensure all subscriptions under your target management group have the `BudgetAmount` tag set appropriately. If a subscription lacks the tag, the script uses the default amount you specify. + +2. Run the script with `-WhatIf` to preview: + +```powershell +$contactEmails = @("platform-team@example.com") +$managementGroup = "prod-workloads" # name or ID +$defaultAmount = 10000 + +.\Deploy-BulkBudgets.ps1 ` + -ManagementGroup $managementGroup ` + -ContactEmails $contactEmails ` + -Amount $defaultAmount ` + -TimeGrain Monthly ` + -WhatIf +``` + +3. Review the `-WhatIf` output. It shows each subscription and the budget that would be created. + +4. If the preview looks correct, run without `-WhatIf`: + +```powershell +.\Deploy-BulkBudgets.ps1 ` + -ManagementGroup $managementGroup ` + -ContactEmails $contactEmails ` + -Amount $defaultAmount ` + -TimeGrain Monthly ` + -Quiet +``` + +The `-Quiet` flag suppresses per-subscription output, useful for large deployments. + +5. Verify in Cost Management: filter to **Budgets** and confirm one budget per subscription appears. + +### Scale considerations + +- The script paginates subscriptions internally—no manual pagination needed +- For 100+ subscriptions, the script may take 5–10 minutes +- Each budget gets its own action group to avoid alert storms on shared groups +- If a subscription already has a budget with the same name, the script skips it (use `-Force` to overwrite) + +--- + +## Exercise 4: Deploy cost anomaly alerts + +### Goal + +Deploy cost anomaly detection to catch unusual spending patterns. You'll deploy to a single subscription first, then at enterprise scale using the bulk script. + +### Context + +Cost anomaly alerts use machine learning to detect when spending deviates from historical trends. Unlike budgets (which use fixed thresholds), anomaly detection learns your baseline spend and flags outliers. This is particularly useful for SaaS workloads with variable usage patterns. This guidance describes [analyzing unexpected charges](https://learn.microsoft.com/en-us/azure/cost-management-billing/understand/analyze-unexpected-charges) and the [scheduled actions API](https://learn.microsoft.com/en-us/rest/api/cost-management/scheduled-actions). + +The `Deploy-AnomalyAlert.ps1` script creates a scheduled action—a standing automation rule—that monitors daily cost data and notifies you when anomalies occur. + +### Steps (single subscription) + +1. Identify the target subscription and a notification email. + +2. Run the script: + +```powershell +$emailRecipients = @("platform-team@example.com", "finance-alerts@example.com") +$notificationEmail = "platform-team@example.com" +$subscriptionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +.\Deploy-AnomalyAlert.ps1 ` + -SubscriptionId $subscriptionId ` + -EmailRecipients $emailRecipients ` + -NotificationEmail $notificationEmail ` + -Quiet +``` + +3. The script deploys a Bicep template (`anomaly-alert.bicep`) that creates a `Microsoft.CostManagement/scheduledActions` resource at subscription scope. + +4. Verify in the portal: go to **Cost Management > Cost anomaly** and confirm a detection rule appears. + +### Steps (enterprise scale) + +For multiple subscriptions under a management group: + +```powershell +$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +$managementGroup = "prod-workloads" +$emailRecipients = @("platform-team@example.com") +$notificationEmail = "platform-team@example.com" + +.\Deploy-BulkALZ.ps1 ` + -TenantId $tenantId ` + -ManagementGroup $managementGroup ` + -EmailRecipients $emailRecipients ` + -NotificationEmail $notificationEmail ` + -WhatIf +``` + +After reviewing the `-WhatIf` output, run without it to deploy. + +### How anomaly detection works + +Once deployed, the alert monitors spend each day. The system builds a baseline over the first ~10 days. After that, Cost Management's ML models detect statistically significant deviations from your baseline spending pattern and trigger notifications. Unlike budgets, anomaly thresholds don't require manual tuning for each subscription. + +### Validation + +Check **Cost Management > Alerts > Cost anomaly**. You'll see: + +- Detection rules listed per subscription or management group +- **Alert** tab showing any triggered anomalies (there won't be any until spend deviates from baseline) + +--- + +## Exercise 5: Monitor capacity reservation utilization + +### Goal + +Query capacity reservation utilization via the instanceView property, interpret allocation states, and use that data to adjust reservation sizes. + +### Context + +Capacity reservations guarantee VM instance availability in a specific zone. The instanceView property tracks three quantities: + +- **available**: reserved capacity not yet allocated to instances +- **consumed**: capacity actively in use by running instances +- **overallocated**: capacity reserved but sold beyond the limit (only possible if you explicitly enabled overallocation) + +Monitoring these three states helps you right-size reservations—keeping available capacity low without running short. This guidance describes [capacity reservation overallocation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate). + +### Steps + +1. List all capacity reservations in your subscription: + +```bash +az capacity reservation list --subscription $SUBSCRIPTION_ID -o json +``` + +2. For each reservation, get the instanceView to see allocation states: + +```bash +az capacity reservation show \ + --capacity-reservation-group myGroup \ + --name myReservation \ + --resource-group myRG \ + --subscription $SUBSCRIPTION_ID +``` + +Look for the `instanceView` object, which contains: + +```json +"instanceView": { + "statuses": [ + { + "code": "ProvisioningState/succeeded", + "level": "Info" + } + ], + "utilizationInfo": { + "virtualMachinesAllocated": [ + { + "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1" + } + ] + } +} +``` + +The `sku.capacity` property on the reservation itself defines the reserved quantity. Compare `virtualMachinesAllocated` count against `sku.capacity` to calculate utilization. See the [capacity reservation overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) for the full response schema. + +3. Calculate your utilization ratio: + +``` +Utilization = count(virtualMachinesAllocated) / sku.capacity +``` + +Aim for 80–95% utilization. If utilization creeps above 95%, request a larger reservation. If it drops below 50%, consider reducing the reserved quantity. + +4. If you've enabled [overallocation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) on a reservation, the count of allocated VMs may exceed `sku.capacity`. This is intentional and allows you to pack more VMs than you reserved, accepting burst risk. + +### Interpretation guide + +| State | Meaning | Action | +|-------|---------|--------| +| allocated VMs < sku.capacity | Unused reserved capacity | Consider reducing reservation size to improve ROI | +| allocated VMs ≈ sku.capacity | Optimal utilization | No action needed | +| allocated VMs > sku.capacity | Overallocation enabled | Expected; verify burst tolerance with platform team | +| allocated VMs < 0.5 x sku.capacity | Underutilized | Reduce reservation size or consolidate workloads | + +### Export for dashboards + +You can export instanceView data via the portal or CLI: + +```bash +az capacity reservation list \ + --subscription $SUBSCRIPTION_ID \ + --query "[].{name: name, reserved: sku.capacity, allocatedVMs: length(instanceView.utilizationInfo.virtualMachinesAllocated || [])}" \ + -o table +``` + +Use this output to feed dashboards in Azure Monitor or Power BI. + +--- + +## Exercise 5b: Validate monitoring scope coverage + +### Goal + +Ensure that your cost, quota, and capacity reservation monitoring rules cover all relevant scopes so you don't miss alerts due to scope mismatches. + +### Context + +Different monitoring signal types operate at different scopes: + +- **Cost anomaly detection** operates at subscription or resource group scope +- **Quota alerts** are per subscription + region + VM family +- **Capacity reservation utilization** is per capacity reservation group (there's no built-in anomaly detection—you monitor utilization via instanceView) + +If your monitoring scopes don't align with where changes actually occur, you'll miss notifications. For example, if you configure a cost anomaly at the management group level but your actual spend variance happens at a resource group level, the aggregation might hide the signal. + +### Steps + +1. List your subscriptions and organize them by region and VM family (for quota tracking): + +```bash +az account list -o table --query "[].{name: name, subscriptionId: id}" +``` + +2. For cost anomalies, define your scope explicitly: + - Are you monitoring at subscription or resource group level? + - Document which subscriptions have cost anomalies enabled and at what scope + - Verify that all cost-critical subscriptions are covered + +3. For quota anomalies, map your subscriptions to the regions and VM families you use: + +```bash +az vm list-usage --subscription $SUBSCRIPTION_ID --location eastus -o table +``` + +Record the VM families in use. For each subscription + region + family combination, ensure you have a quota alert configured (see Exercise 1). + +4. For capacity reservations, list all CRGs and verify monitoring is enabled: + +```bash +az capacity reservation group list --subscription $SUBSCRIPTION_ID -o json | jq -r '.[].id' +``` + +Check that you're extracting instanceView data regularly for each CRG (see Exercise 5). + +5. Create a scope coverage matrix: + +| Scope Type | Scope | Anomaly Type | Coverage | Action Group | +|------------|-------|--------------|----------|--------------| +| Subscription | sub-prod-1 | Cost | Yes | cost-alerts-prod | +| Subscription + Region + VM Family | sub-prod-1, eastus, Standard_D2s_v3 | Quota | Yes | quota-alerts-compute | +| Capacity Reservation Group | crg-prod-app-tier | Overallocation | Yes | reservation-alerts | + +Document any gaps. If a scope is missing, either configure monitoring or document why it's intentionally excluded. + +### Validation + +After defining your scope boundaries: + +- Run a test anomaly detection run to verify the scope captures the signal +- Confirm that each monitoring rule fires only for its intended scope +- Review alerts from the past week to ensure scope coverage is complete + +Reference: [Capacity reservation overallocation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) + +--- + +## Exercise 6: Suppress noisy Advisor recommendations + +### Goal + +Use the Advisor suppression API to mute recommendations you've reviewed and decided not to act on. Suppressions include a TTL (time-to-live) so recommendations re-surface after investigation. + +### Context + +Azure Advisor generates recommendations across categories: cost, security, reliability, operational excellence, and performance. Some recommendations are noisy (e.g., "use reserved instances" even though you already use them for most workloads) or not applicable to your architecture. Suppression lets you silence them without losing the capability to review them later. + +Suppressions expire after a maximum of 90 days, forcing you to actively re-evaluate rather than permanently hiding recommendations. This guidance describes [Advisor suppression](https://learn.microsoft.com/en-us/azure/advisor/suppress-recommendations). + +Your account must have the **Advisor Contributor** role. This guidance describes [Advisor permissions](https://learn.microsoft.com/en-us/azure/advisor/permissions). + +### Steps + +1. Identify noisy recommendation types. Go to **Azure Advisor > Cost** and look for recommendations you want to suppress (e.g., "buy reserved instances"). + +2. Note the recommendation type ID. You can find these in the Advisor API docs or by querying the API directly: + +```bash +az rest --method get \ + --url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.Advisor/recommendations?api-version=2020-01-01" \ + --query "value[0].properties.recommendationType" -o tsv +``` + +Common type IDs include: +- Cost optimization recommendations (GUIDs vary by region and recommendation category) + +3. Run the suppression script with a TTL (max 90 days): + +```powershell +$managementGroupId = "prod-workloads" +$recommendationTypeIds = @( + "01234567-89ab-cdef-0123-456789abcdef", # example GUID + "fedcba98-7654-3210-fedc-ba9876543210" # another example +) +$days = 30 + +.\Suppress-AdvisorRecommendations.ps1 ` + -ManagementGroupId $managementGroupId ` + -RecommendationTypeIds $recommendationTypeIds ` + -Days $days ` + -WhatIf +``` + +4. Review the `-WhatIf` output and confirm the recommendation types you want to suppress. + +5. Run without `-WhatIf` to apply: + +```powershell +.\Suppress-AdvisorRecommendations.ps1 ` + -ManagementGroupId $managementGroupId ` + -RecommendationTypeIds $recommendationTypeIds ` + -Days $days +``` + +6. Verify in the portal: go to **Azure Advisor** and confirm suppressed recommendations no longer appear (or appear with a "suppressed" badge). + +### TTL and re-evaluation + +After 30 days (in the example above), the suppression expires and the recommendation re-surfaces. This forces periodic re-review rather than fire-and-forget. If you want permanent suppression, re-run the script periodically or increase the TTL up to 90 days. + +--- + +## Exercise 6b: CI/CD capacity gates + +### Goal + +Integrate four capacity checks into your deployment pipeline to prevent stamp deployments that would violate quota, budget, or reservation constraints. + +### Context + +The workload supply chain model uses gates to promote workloads through dev → staging → production. Capacity gates are enforcement points that verify sufficient headroom exists before a deployment proceeds. This prevents cascading failures and ensures your monitoring and governance layers are connected to operational decisions. + +### Steps + +Add the following four checks to your deployment pipeline (e.g., in your CI/CD orchestration or pre-deployment validation step): + +1. **Quota headroom check**: Verify available quota is greater than the deployment requirement for both the region and VM family + + ```bash + # Pseudocode for quota gate + REQUIRED_CORES=20 + QUOTA_FAMILY="Standard_D2s_v3" + TARGET_REGION="eastus" + + AVAILABLE=$(az vm list-usage \ + --location $TARGET_REGION \ + --subscription $SUBSCRIPTION_ID \ + --query "[?name.value=='$QUOTA_FAMILY'].{available: (limit - currentValue)}" \ + -o tsv) + + if [ "$AVAILABLE" -lt "$REQUIRED_CORES" ]; then + echo "GATE_FAILED: Insufficient quota in $TARGET_REGION for $QUOTA_FAMILY" + exit 1 + fi + ``` + +2. **Alert coverage check**: Verify that quota and cost alerts are configured for the target subscription and region + + ```bash + # Pseudocode for alert coverage gate + SUBSCRIPTION_ID="target-sub" + TARGET_REGION="eastus" + + # Check for quota alerts in this region + QUOTA_ALERTS=$(az monitor metrics alert list \ + --subscription $SUBSCRIPTION_ID \ + --query "[?contains(description, '$TARGET_REGION')].name" \ + -o tsv) + + if [ -z "$QUOTA_ALERTS" ]; then + echo "GATE_FAILED: No quota alerts configured for $TARGET_REGION" + exit 1 + fi + ``` + +3. **Capacity reservation check** (if applicable): Verify reserved quantity ≥ deployment target + + ```bash + # Pseudocode for CRG gate + TARGET_CRG="crg-prod-app-tier" + REQUIRED_INSTANCES=10 + + RESERVED=$(az capacity reservation show \ + --capacity-reservation-group $TARGET_CRG \ + --subscription $SUBSCRIPTION_ID \ + --query "instanceView.allocatedQuantity" \ + -o tsv) + + if [ "$RESERVED" -lt "$REQUIRED_INSTANCES" ]; then + echo "GATE_FAILED: Insufficient reserved capacity in $TARGET_CRG" + exit 1 + fi + ``` + +4. **Budget posture check**: Verify current spend + projected deployment spend doesn't exceed budget + + ```bash + # Pseudocode for budget gate + SUBSCRIPTION_ID="target-sub" + PROJECTED_MONTHLY_SPEND=500 # USD + + CURRENT_SPEND=$(az rest --method post \ + --url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \ + --body '{"type":"ActualCost","timeframe":"MonthToDate","dataset":{"granularity":"None","aggregation":{"totalCost":{"name":"Cost","function":"Sum"}}}}' \ + --query "properties.rows[0][0]" \ + -o tsv) + + BUDGET=$(az consumption budget show \ + --budget-name "subscription-budget-$SUBSCRIPTION_ID" \ + --query "amount" \ + -o tsv) + + if [ $(($CURRENT_SPEND + $PROJECTED_MONTHLY_SPEND)) -gt "$BUDGET" ]; then + echo "GATE_FAILED: Budget insufficient for deployment" + exit 1 + fi + ``` + +5. Integrate these checks as pass/fail gates in your pipeline. Each check should either pass (proceed to next stage) or fail (block deployment and alert the team). + +### Example pipeline integration (GitHub Actions) + +```yaml +name: Deploy with capacity gates + +jobs: + capacity-gates: + runs-on: ubuntu-latest + steps: + - name: Quota headroom gate + run: | + # Run quota check script + ./scripts/check-quota-headroom.sh ${{ secrets.SUBSCRIPTION_ID }} eastus + - name: Alert coverage gate + run: | + # Run alert coverage check + ./scripts/check-alert-coverage.sh ${{ secrets.SUBSCRIPTION_ID }} eastus + - name: CRG capacity gate + run: | + # Run reservation check + ./scripts/check-crg-capacity.sh ${{ secrets.SUBSCRIPTION_ID }} crg-prod-app-tier + - name: Budget posture gate + run: | + # Run budget check + ./scripts/check-budget-posture.sh ${{ secrets.SUBSCRIPTION_ID }} + - name: Deploy + if: success() + run: | + # Proceed to deployment only if all gates pass + az deployment group create --resource-group myRG --template-file main.bicep +``` + +### Validation + +After implementing capacity gates: + +- Attempt a deployment that would exceed quota—verify it blocks +- Attempt a deployment when budget is exhausted—verify it blocks +- Attempt a deployment when alert coverage is missing—verify it blocks +- Attempt a valid deployment—verify it proceeds + +Reference: [Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) + +--- + +## Exercise 7: Build a quota usage dashboard + +### Goal + +Export quota usage data and build a dashboard that shows trends over time, helping you forecast when you'll need quota increases. + +### Context + +Quota usage changes as your workloads scale. A dashboard that tracks utilization trends helps you predict when you'll hit limits and request increases proactively. The `Get-AzVMQuotaUsage.ps1` script provides multi-threaded quota data collection—fast enough for large subscriptions. + +This guidance describes the [workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) and how it connects to forecasting. + +### Steps + +1. Export quota usage for a subscription using the script: + +```powershell +.\Get-AzVMQuotaUsage.ps1 -SubscriptionId $subscriptionId +``` + +This returns a PowerShell object with quota type, current usage, limit, and percentage utilization. + +2. Export to CSV for ingestion into a dashboard tool: + +```powershell +$quotas = .\Get-AzVMQuotaUsage.ps1 -SubscriptionId $subscriptionId +$quotas | Export-Csv -Path "quota-usage-$(Get-Date -Format 'yyyy-MM-dd').csv" -NoTypeInformation +``` + +3. Collect CSV files daily or weekly via Azure Automation or a scheduled task, appending to a data store (Azure Table Storage, SQL Database, etc.). + +4. Build a dashboard in Power BI or Azure Monitor that shows: + + - Quota utilization over time (line chart with percentage on Y axis, date on X axis) + - Trends per quota type (e.g., vCPUs, managed disks) + - Forecasted utilization (using linear regression or moving averages) + - Subscriptions where utilization exceeds 80% (highlight for action) + +5. Schedule the dashboard as a daily email or embed it in your platform operations portal. + +### Example dashboard queries + +If you're storing quota data in a SQL database: + +```sql +SELECT + quota_type, + subscription_id, + current_usage, + quota_limit, + CAST(current_usage AS FLOAT) / quota_limit * 100 AS utilization_pct, + captured_date +FROM quota_usage +WHERE subscription_id = @subscriptionId + AND captured_date >= DATEADD(day, -90, GETDATE()) +ORDER BY captured_date DESC, quota_type; +``` + +Use this to build trend charts and alert thresholds in your BI tool. + +### PaaS service monitoring + +Quota groups cover IaaS compute only. For PaaS services, monitor quotas and costs through service-specific mechanisms: + +- **Azure Storage**: Storage accounts have a [per-subscription quota](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-storage-limits) (default 250 per region). Monitor account counts and configure budget alerts for storage spend. + + ```bash + az storage account list --subscription $SUBSCRIPTION_ID \ + --query "length([?location=='eastus'])" -o tsv + ``` + +- **Azure Cosmos DB**: Monitor [provisioned RU throughput](https://learn.microsoft.com/en-us/azure/cosmos-db/monitor#monitoring-overview) and set budget alerts for Cosmos DB spend. Use Azure Monitor metrics (`TotalRequestUnits`, `NormalizedRUConsumption`) to track utilization against provisioned capacity. + + ```bash + az monitor metrics list --resource $COSMOS_RESOURCE_ID \ + --metric "NormalizedRUConsumption" --interval PT1H -o table + ``` + +- **Azure Service Bus**: Monitor [messaging unit utilization](https://learn.microsoft.com/en-us/azure/service-bus-messaging/monitor-service-bus-reference) with Azure Monitor metrics (`NamespaceMemoryUsage`, `IncomingMessages`). Premium namespaces have messaging unit quotas that constrain throughput. + + ```bash + az monitor metrics list --resource $SERVICEBUS_RESOURCE_ID \ + --metric "IncomingMessages" --interval PT1H -o table + ``` + +- **Azure Key Vault**: Monitor [transaction rate limits](https://learn.microsoft.com/en-us/azure/key-vault/general/service-limits) with Azure Monitor metrics (`ServiceApiHit`, `ServiceApiLatency`). Key Vault enforces throttling at 4,000 transactions per 10 seconds for vaults in the same region. + +- **Azure Event Hubs**: Monitor [throughput units](https://learn.microsoft.com/en-us/azure/event-hubs/monitor-event-hubs-reference) with Azure Monitor metrics (`IncomingBytes`, `OutgoingBytes`). Auto-inflate can scale throughput units automatically, but set budget alerts to catch unexpected cost growth. + +Include these PaaS metrics in your quota usage dashboard alongside compute data for a complete view of your capacity posture. + +### Connecting to forecasting + +Once you have historical quota usage data, you can forecast when you'll hit limits: + +1. Calculate the rate of quota usage increase (e.g., 5% per week) +2. Extrapolate: if current utilization is 60% and you're growing at 5% per week, you'll hit 90% in 6 weeks +3. Submit quota increase requests with 2–4 weeks lead time + +This closes the feedback loop from monitoring to forecasting to capacity planning. + +--- + +## Wrap-up: Monitoring feeds forecasting + +The exercises above create a monitoring and governance backbone organized around the 4-signal model: + +1. **Signal 1: Quota**—quota alerts catch capacity constraints early (Exercises 1, 5b, 6b) +2. **Signal 2: Reservation**—capacity reservation monitoring ensures you're getting ROI on reserved capacity (Exercises 5, 5b, 6b) +3. **Signal 3: Cost**—budgets track spend against forecasts, anomaly detection surfaces unusual patterns, and dashboards drive capacity planning (Exercises 2, 3, 4, 7) +4. **Signal 4: Gate**—CI/CD pipeline gates validate quota headroom, alert coverage, reservation capacity, and budget posture before deployments proceed (Exercise 6b) + +These signals feed into a closed loop: monitoring collects the four signals, dashboards visualize them, gates enforce them at deployment time, and forecasting uses them to drive capacity and budget planning. You circle back to monitoring to validate your forecasts. + +Additionally: + +- **Advisor suppression** reduces alert fatigue while maintaining accountability (Exercise 6) +- **Workload supply chain integration**: Monitoring data feeds the promote-through-gates model. As workloads move from dev → staging → production, each stage's deployment gates enforce the same capacity checks, ensuring consistent governance across your environment. The four signals (quota, reservation, cost, gate) align with the supply chain's enforcement points, creating a unified operational model. + +The [workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) describes this full lifecycle in detail. The monitoring layer you've built is the observability component of that chain. + +Use these signals to inform your FinOps optimization strategy—cost data and quota constraints drive rate-optimization decisions. See the [FinOps rate optimization guidance](https://www.finops.org/framework/capabilities/rate-optimization/) for details. + +--- + +## Troubleshooting + +### Budget not showing spend + +- Budgets require 24–48 hours for initial cost data population +- Verify the subscription has incurred costs +- Check that the budget's scope (subscription/management group) matches your intent + +### Anomaly detection not firing + +- Machine learning baseline takes ~10 days to establish +- Verify the scheduled action is deployed and active in the target subscription +- Check that the notification email is valid and not in spam + +### Quota alert not triggering + +- Alerts fire based on actual quota utilization, not on alert configuration +- Verify you've provisioned enough resources to approach the quota +- Wait 5–10 minutes after threshold is crossed for the alert to fire + +### Suppression script fails with permission error + +- Confirm your account has **Advisor Contributor** role at the management group scope +- Confirm your account has the [required permissions](https://learn.microsoft.com/en-us/azure/advisor/permissions) + +--- + +## References + +- [Quota monitoring and alerting](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting) +- [Create quota usage alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) +- [Budget alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending) +- [Capacity reservation overallocation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) +- [Workload supply chain](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) +- [Advisor suppression](https://learn.microsoft.com/en-us/azure/advisor/suppress-recommendations) +- [Advisor permissions](https://learn.microsoft.com/en-us/azure/advisor/permissions) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/README.md new file mode 100644 index 000000000..097a7e991 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/README.md @@ -0,0 +1,31 @@ +--- +title: Capacity management +nav_order: 4 +has_children: true +--- + +# Capacity management overview + +Use this section to access subscription lifecycle guides, capacity and quota guidance, and supporting operational references. The goal is to connect estate-level Azure controls to Azure landing zone, Well-Architected, and FinOps Framework guidance without prescribing your internal operating model. [Source](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) [Source](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) [Source](https://www.finops.org/framework/) + +- [Planning and forecasting](capacity-planning/README.md)—capacity planning inputs, isolation decisions, and demand signals for Planning & Estimating and Forecasting. +- [Workload placement](quota/README.md)—billing scopes, region and zonal access, quota requests, SKU readiness, and capacity reservation constraints for Architecting & Workload Placement. +- [Usage and rate optimization](quota-groups/README.md)—quota groups, capacity reservations, and pricing-commitment coordination for Usage Optimization and Rate Optimization. +- [Governance and automation](monitoring-alerting/README.md)—quota and reservation monitoring, alerting, governance gates, and support references for Governance, Policy & Risk and Automation, Tools & Services. + +## Subscription vending context + +- [Subscription vending](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/subscription-vending#determine-subscription-placement) standardizes how platform teams capture requests, enforce approval logic, and automate placement of new landing zones so application teams can focus on workload delivery. +- The Cloud Center of Excellence defines intake requirements (budget, owner, network expectations, data classifications) and connects the approval flow to the subscription deployment pipeline to keep governance aligned with [Azure landing zone design areas](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/subscription-vending#determine-subscription-placement). + +## Programmatic subscription creation + +- Azure supports [programmatic subscription creation](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription) for Enterprise Agreements, Microsoft Customer Agreements, and Microsoft Partner Agreements via modern REST APIs. +- Legacy EA processes use enrollment accounts to scope billing, while modern MCA workflows rely on billing profiles and invoice sections. See the dedicated legacy and modern pages in this folder for detailed guides. +- When planning subscription vending product lines, align the automation entry points from the EA and MCA procedures with the [placement guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/subscription-vending#determine-subscription-placement) so workload subscriptions land in the correct management group and billing scope. + +## Support workflows + +- [Regional access requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) unblock subscriptions in restricted regions; submit quota support tickets when deployment plans require new geographies and make sure you follow up on any offer flags set at subscription creation. +- [Zonal access requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) grant restricted VM series access to targeted availability zones, preserving high-availability plans across logical mappings. +- [VM-family](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) and [regional quota increases](https://learn.microsoft.com/en-us/azure/quotas/regional-quota-requests) continue to flow through Azure's quota tooling for any needs outside pooled quota groups. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/aks-capacity/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/aks-capacity/README.md new file mode 100644 index 000000000..fa72befb5 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/aks-capacity/README.md @@ -0,0 +1,135 @@ +--- +title: AKS capacity governance +parent: Operations +nav_order: 10 +--- + +# AKS capacity governance + +> For ISVs running stamp-based SaaS on AKS, capacity governance ensures node pools can scale without hitting quota limits or availability gaps. This page covers quota management and capacity reservation patterns specific to [Azure Kubernetes Service (AKS)](https://learn.microsoft.com/en-us/azure/aks/intro-kubernetes) node pools. + +AKS node pools consume the same regional VM quota and capacity as standalone VMs. You manage quota through [quota groups](../quota-groups/README.md) and guarantee capacity through [capacity reservation groups](../capacity-reservations/README.md), but AKS enforces constraints on how node pools associate with CRGs. This guide explains those constraints and their implications for stamp provisioning workflows. + +## Quota considerations for AKS clusters + +[AKS clusters](https://learn.microsoft.com/en-us/azure/aks/quotas-skus-regions) draw from the same VM family quota as standalone VMs. When you create or scale a node pool, AKS requests quota from the `Microsoft.Compute` provider in the subscription and region where the cluster runs. + +- **Node pool scaling failures:** If regional quota is exhausted, [autoscaler operations fail](https://learn.microsoft.com/en-us/azure/aks/cluster-autoscaler) and pods remain pending until quota increases or nodes are freed elsewhere in the region. +- **Quota group integration:** Add AKS subscriptions to a [quota group](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) so node pools share quota with other workloads across your estate. This prevents isolated exhaustion in subscriptions dedicated to specific stamps. +- **VM family alignment:** Choose [VM sizes](https://learn.microsoft.com/en-us/azure/aks/quotas-skus-regions#vm-size-restrictions) that align with your quota allocation strategy. Mixing multiple VM families across node pools fragments quota and complicates capacity planning. +- **System node pool requirements:** Every AKS cluster requires a [system node pool](https://learn.microsoft.com/en-us/azure/aks/use-system-pools) to run critical control-plane components (CoreDNS, kube-proxy, metrics-server). Don't configure system node pools to scale to zero—they must maintain at least one node. + +## Capacity reservations for node pools + +AKS node pools support [capacity reservation groups (CRGs)](https://learn.microsoft.com/en-us/azure/aks/use-capacity-reservation-groups) to guarantee that VM capacity is available when the cluster scales out. CRGs for AKS work the same way as [VM capacity reservations](../capacity-reservations/README.md)—you reserve specific VM sizes in a region and zone, then associate node pools with those reservations—but AKS enforces additional constraints on association and identity. + +### Node pool association constraints + +You can't update an existing node pool to add a capacity reservation group. You must [create a new node pool](https://learn.microsoft.com/en-us/azure/aks/use-capacity-reservation-groups#create-a-new-node-pool-with-a-capacity-reservation-group) with the `--crg-id` parameter during creation. + +```bash +# This operation isn't supported +az aks nodepool update --crg-id $CRG_ID --cluster-name myCluster --name existingPool --resource-group myRG + +# Create a new node pool instead +az aks nodepool add \ + --resource-group myRG \ + --cluster-name myCluster \ + --name newPool \ + --node-count 3 \ + --node-vm-size Standard_D4s_v3 \ + --crg-id /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Compute/capacityReservationGroups/{crg-name} +``` + +**Implication for stamp automation:** If you create stamps through infrastructure-as-code pipelines, ensure CRG associations are embedded in the initial node pool definition. Don't expect to retrofit existing pools with CRG references after deployment. + +### Identity and permission requirements + +AKS requires a [user-assigned managed identity](https://learn.microsoft.com/en-us/azure/aks/use-capacity-reservation-groups#prerequisites) to associate node pools with CRGs. System-assigned managed identities don't work for this scenario. + +1. **Create or identify a user-assigned identity:** The identity must exist in the same subscription as the AKS cluster. +2. **Assign Contributor role on the CRG's resource group:** The identity needs `Microsoft.Compute/capacityReservationGroups/*` permissions. [Grant the role](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) at the resource group scope that contains the CRG. +3. **Wait for propagation:** Role assignments can take [up to 60 minutes](https://learn.microsoft.com/en-us/azure/aks/use-capacity-reservation-groups#prerequisites) to propagate. Don't create node pools immediately after assigning roles—your pipeline will fail if the identity isn't yet active. +4. **Attach the identity to the cluster:** When you [create the AKS cluster](https://learn.microsoft.com/en-us/azure/aks/use-managed-identity), specify the user-assigned identity with `--assign-identity`. Existing clusters can't switch from system-assigned to user-assigned identity—you'd need to recreate the cluster. + +### Disassociation and lifecycle behavior + +When you delete a node pool or cluster, AKS [automatically disassociates](https://learn.microsoft.com/en-us/azure/aks/use-capacity-reservation-groups#delete-a-node-pool-or-cluster-associated-with-a-capacity-reservation-group) it from any associated CRG before completing the delete operation. This differs from standalone VMs, where you must [manually disassociate VMs](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-remove-vm) before deleting the capacity reservation. + +- **Node pool deletion:** `az aks nodepool delete` removes the pool and its CRG association in a single operation. No manual cleanup is required. +- **Cluster deletion:** `az aks delete` disassociates all node pools from their CRGs before deleting the cluster resource. +- **Warning scenarios:** If the specified CRG doesn't exist or contains no member reservations, AKS [creates the node pool with a warning](https://learn.microsoft.com/en-us/azure/aks/use-capacity-reservation-groups#limitations-for-using-capacity-reservation-groups-with-aks-node-pools) but doesn't establish the CRG association. The node pool runs without capacity guarantees until you recreate it with a valid CRG reference. + +### Zone alignment and cross-subscription sharing + +If you're using [availability zones](https://learn.microsoft.com/en-us/azure/aks/availability-zones) with AKS, ensure the CRG's zonal capacity reservations align with the physical zones where your node pool deploys. Azure assigns different logical-to-physical zone mappings per subscription, so [validate zone mappings](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share#zone-mapping-differences-across-subscriptions) if the CRG is shared from a different subscription. + +Use the [Get-AzAvailabilityZoneMapping.ps1](../tools-scripts/get-azavailabilityzonemapping.md) script to compare zone mappings across subscriptions before configuring CRG sharing. + +## Automation patterns + +### Infrastructure-as-code templates + +Embed CRG references in your Bicep or Terraform templates to ensure node pools are created with capacity guarantees from the start. + +**Bicep example:** + +```bicep +resource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2024-01-01' = { + parent: aksCluster + name: 'prodpool' + properties: { + count: 5 + vmSize: 'Standard_D4s_v3' + capacityReservationGroupID: '/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Compute/capacityReservationGroups/{crg-name}' + mode: 'User' + osType: 'Linux' + } +} +``` + +**Terraform example:** + +```hcl +resource "azurerm_kubernetes_cluster_node_pool" "prod" { + name = "prodpool" + kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id + vm_size = "Standard_D4s_v3" + node_count = 5 + capacity_reservation_group_id = azurerm_capacity_reservation_group.crg.id +} +``` + +### Pipeline sequencing for identity propagation + +If your pipeline creates the user-assigned identity and assigns roles in the same run, add a delay before creating the AKS cluster or node pool. + +```bash +# Create identity and assign role +az identity create --name aks-crg-identity --resource-group myRG +IDENTITY_ID=$(az identity show --name aks-crg-identity --resource-group myRG --query id -o tsv) +az role assignment create --assignee $IDENTITY_ID --role Contributor --scope /subscriptions/{sub-id}/resourceGroups/{crg-rg} + +# Wait for propagation (adjust based on observed latency) +echo "Waiting 5 minutes for role propagation..." +sleep 300 + +# Create cluster with the identity +az aks create \ + --resource-group myRG \ + --name myCluster \ + --assign-identity $IDENTITY_ID \ + --enable-managed-identity +``` + +For production pipelines, poll the role assignment status instead of using fixed delays. + +## Operational checklist + +1. Verify [regional quota](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal) before creating AKS clusters in new regions. AKS doesn't provide distinct quota from `Microsoft.Compute`—both draw from the same pool. +2. Add AKS subscriptions to [quota groups](../quota-groups/README.md) if you manage multiple stamps or environments that share capacity. +3. Create [capacity reservation groups](../capacity-reservations/README.md) for critical node pools that must scale reliably during demand surges. +4. Use [user-assigned managed identities](https://learn.microsoft.com/en-us/azure/aks/use-managed-identity) for all AKS clusters that will associate with CRGs. Don't rely on system-assigned identities. +5. Embed CRG references in node pool definitions during initial creation. Don't expect to retrofit existing pools. +6. Monitor [CRG utilization](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) to ensure reserved capacity aligns with autoscaler policies. Underutilized reservations waste cost; overallocated pools lose capacity guarantees. +7. Validate [zone mappings](../tools-scripts/get-azavailabilityzonemapping.md) if sharing CRGs across subscriptions in zonal deployments. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-and-quotas/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-and-quotas/README.md new file mode 100644 index 000000000..8661803f9 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-and-quotas/README.md @@ -0,0 +1,69 @@ +--- +title: Capacity and billing operations +parent: Operational topics +nav_order: 2 +has_children: true +--- + +# Capacity and billing operations + +This hub connects Azure billing models, subscription vending, quota operations, capacity reservations, monitoring, and automation so Azure capacity evidence fits inside the canonical FinOps Framework. It maps Azure controls to Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization, Governance, Policy & Risk, and Automation, Tools & Services, while using [Well-Architected capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [reliable scaling](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling), and [workload supply chain guidance](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) as Azure implementation references. [Source](https://www.finops.org/framework/) [Source](https://www.finops.org/framework/domains/) + +Use this page as the estate-level reference for how billing scopes, subscription creation, quota groups, capacity reservations, and CI/CD gates work together for SaaS ISVs operating Azure estates under the [ISV landing zone](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/isv-landing-zone) expectations. + +## Billing models and scopes (EA vs MCA) + +Azure enterprise customers typically operate under either the historic Enterprise Agreement (EA) or the modern Microsoft Customer Agreement (MCA). Both contracts provide an enterprise footing for Azure subscriptions, but the MCA simplifies hierarchy, automation, and tenant alignment according to [MCA billing guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/azure-billing-microsoft-customer-agreement). + +- EA continues to use enrollments and departments to structure subscriptions, and many ISVs keep this model in place for existing estates. Subscription creation flows through enrollment accounts and automation patterns documented for Enterprise Agreements. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) +- MCA anchors the billing account to a Microsoft Entra tenant and introduces billing accounts, billing profiles, and invoice sections, which define how subscriptions inherit tenant context and how charges roll up. [Source](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/azure-billing-microsoft-customer-agreement) +- MCA billing roles are designed for automation: subscription creator, Owner, or Contributor roles at the billing account, billing profile, or invoice section scope can create subscriptions through [programmatic subscription creation](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) APIs. + +From a FinOps perspective, these billing scopes define where [Azure Reservations and savings plans](https://www.finops.org/framework/capabilities/rate-optimization/) attach as pricing constructs. Reservations and savings plans deliver rate optimization at the billing account, billing profile, or subscription scope, while [capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) guarantee supply at the region or availability zone scope. Treat these as complementary instruments: pricing commitments live at billing scopes, and capacity reservations live at compute scopes. + +## Subscription vending and support workflows + +Subscription vending connects billing scopes to workload landing zones. The goal is to standardize how you collect subscription requests, apply placement rules, and connect approvals to automation so developers receive subscriptions that match landing zone design and capacity governance. [Source](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/subscription-vending) + +- Subscription vending design areas describe how platform teams capture intake data (owners, budget, network topology, data classifications) and route deployments into the correct management group and billing scope. This keeps subscriptions aligned with [Azure landing zone design areas](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/subscription-vending#determine-subscription-placement) while preserving internal governance. +- Programmatic subscription creation for EA, MCA, and partner agreements uses modern REST APIs so you can integrate vending into CI/CD or workflow engines. Align the entry points from [EA subscription creation](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) and [MCA subscription creation](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) with your subscription vending flows. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription) +- Cross-tenant subscription requests let an MCA billing owner create a subscription for a user or service principal in another tenant using the [subscription request workflow](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-subscription-request). This pattern is important when the ISV operates multiple tenants. + +Support workflows are part of the Azure implementation surface for Architecting & Workload Placement, Planning & Estimating, Forecasting, and Governance, Policy & Risk: + +- Region access requests unblock subscriptions in restricted regions and should be planned ahead of major launches. [Source](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) +- Zonal enablement requests grant access to restricted VM series in specific availability zones and protect high-availability designs that rely on those SKUs. [Source](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) +- VM-family and regional quota increases use standard [per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) and [regional quota requests](https://learn.microsoft.com/en-us/azure/quotas/regional-quota-requests) when capacity needs fall outside quota groups or existing limits. + +## Map Azure controls to FinOps capabilities + +- Planning & Estimating and Forecasting: Size scale units or deployment stamps from telemetry, business targets, and Well-Architected capacity planning guidance before you request quota or reservations. This connects capacity assumptions to FinOps planning, budget, and forecast evidence. [Source](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) [Source](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) [Source](https://www.finops.org/framework/capabilities/) +- Architecting & Workload Placement: Get region and zonal access approved, verify SKU availability, and use quota groups at the management group scope to avoid stranded VM-family headroom. These Azure controls shape where workloads can run. [Source](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) [Source](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) [Source](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) +- Usage Optimization: Track quota, reserved capacity, and deployment-stamp utilization so unused buffers, stranded headroom, and inefficient placement decisions are visible. Capacity reservations expose reserved versus allocated capacity through instance view. [Source](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) [Source](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) [Source](https://www.finops.org/framework/capabilities/usage-optimization/) +- Rate Optimization: Coordinate Azure Reservations and savings plans with capacity reservations when workload demand is stable enough to support a pricing commitment. Capacity reservations guarantee compute supply; Reservations and savings plans reduce rate. [Source](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) [Source](https://www.finops.org/framework/capabilities/rate-optimization/) +- Governance, Policy & Risk and Automation, Tools & Services: Wire quota and reservation utilization alerts so onboarding or seasonal spikes don't stall, and connect those signals to CI/CD gates, owner routing, budgets, anomaly alerts, and exception review. [Source](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) [Source](https://www.finops.org/framework/capabilities/governance-policy-risk/) [Source](https://www.finops.org/framework/capabilities/automation-tools-services/) + +### Links by capability area + +- Planning & Estimating and Forecasting: [Capacity planning](../capacity-planning/README.md) +- Architecting & Workload Placement: [Quota operations](../quota/README.md), [Quota groups](../quota-groups/README.md), and [Deployment patterns](../../deployment/README.md) +- Usage Optimization and Rate Optimization: [Capacity reservations](../capacity-reservations/README.md) and [Billing models](../../billing/README.md) +- Governance, Policy & Risk and Automation, Tools & Services: [Monitoring & alerting](../monitoring-alerting/README.md), [Capacity governance](../capacity-governance/README.md), and [Tools & scripts index](../tools-scripts/README.md) + +## FinOps intersections + +Capacity and billing operations intersect with multiple FinOps Framework domains and capabilities. Domains are outcomes, and capabilities describe the activities that achieve those outcomes. [Source](https://www.finops.org/framework/domains/) [Source](https://www.finops.org/framework/capabilities/) + +- Understand Usage & Cost: Capacity evidence depends on Data Ingestion, Allocation, Reporting & Analytics, and Anomaly Management so teams can see utilization, headroom, and cost signals in one reporting context. [Source](https://www.finops.org/framework/domains/) +- Quantify Business Value: Planning & Estimating, Forecasting, Budgeting, KPIs & Benchmarking, and Unit Economics connect demand projections to business context, budget guardrails, and product economics. [Source](https://www.finops.org/framework/domains/) +- Optimize Usage & Cost: Architecting & Workload Placement, Usage Optimization, and Rate Optimization cover region, zone, SKU, utilization, and pricing-commitment choices. [Source](https://www.finops.org/framework/domains/) +- Manage the FinOps Practice: Governance, Policy & Risk and Automation, Tools & Services cover alerts, controls, owner routing, exception review, and tool-supported evidence collection. [Source](https://www.finops.org/framework/domains/) + +Across these capabilities, the distinction between pricing commitments and capacity guarantees is critical. Pricing constructs such as Azure Reservations and savings plans reduce unit cost but don't reserve capacity, while capacity reservations guarantee supply without changing price on their own. The capability view on this page keeps those roles clear so ISV platform, finance, and product teams can make coordinated decisions. + + +## Tools + +For operational scripts and reporting, see: + +- [Tools & scripts index](../tools-scripts/README.md) for quota, cost, and automation utilities diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-governance/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-governance/README.md new file mode 100644 index 000000000..691faf30f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-governance/README.md @@ -0,0 +1,65 @@ +--- +title: Capacity governance program +parent: Capacity & quotas +nav_order: 6 +--- + +# Capacity governance program + +Azure guidance connects [capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [scale-unit architecture](https://learn.microsoft.com/en-us/azure/well-architected/mission-critical/application-design#scale-unit-architecture), quota management, [Azure Reservations and savings plans](https://www.finops.org/framework/capabilities/rate-optimization/), and [monitoring](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling#choose-appropriate-scale-units) into Azure capacity governance controls. This page aggregates those references so you can align the other guides in this site with the official documentation and the FinOps Framework without guessing where guidance lives. You'll see how each link maps to the guides elsewhere in this repo. [Source](https://www.finops.org/framework/capabilities/) + +> This page aggregates Azure's capacity governance surfaces so you can integrate them into Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization, Governance, Policy & Risk, and Automation, Tools & Services. Each section cites Microsoft Learn and links to supporting guides. + +## Azure capacity governance surfaces + +Azure exposes these controls for capacity governance: + +- **Planning & Estimating and Forecasting:** [Capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) guidance describes demand modeling using telemetry and business context; [scale-unit architecture](https://learn.microsoft.com/en-us/azure/well-architected/mission-critical/application-design#scale-unit-architecture) and [deployment stamps](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern) show how to structure scalable capacity envelopes. +- **Architecting & Workload Placement:** [Region access](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) and [zone enablement](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) unblock restricted areas; [quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) pool vCPU limits across subscriptions for shared inventory management. +- **Usage Optimization and Rate Optimization:** [On-demand reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) guarantee VM availability for specific sizes, regions, and zones; [overallocation tracking](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) exposes utilization states beyond reserved quantities; [rate optimization](https://www.finops.org/framework/capabilities/rate-optimization/) surfaces include reservation and savings plan utilization views. +- **Governance, Policy & Risk and Automation, Tools & Services:** [Quota alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) provide usage signals as consumption approaches limits, and [Operational excellence guidance](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) describes how to integrate capacity controls into CI/CD promotion paths. + +### Quick links + +- Planning and forecasting: [Capacity planning](../capacity-planning/README.md) +- Workload placement: [Quota operations](../quota/README.md), [Quota groups](../quota-groups/README.md) +- Usage and rate optimization: [Capacity reservations](../capacity-reservations/README.md) +- Governance and automation: [Monitoring & alerting](../monitoring-alerting/README.md), [Capacity governance](../capacity-governance/README.md), [Workload supply chain guidance](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain) + +## Forecasts and scale units + +- The Well-Architected [capacity planning article](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) describes capacity planning as an iterative process that uses historical telemetry, business context, and forecasting to keep workloads reliable without overprovisioning. +- The reliable scaling guidance recommends designing around [scale units](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling#choose-appropriate-scale-units)—logical groupings of components that scale together—and notes that you can scale individual resources, full components, or entire solutions as deployment stamps. +- In the mission-critical application design guidance, a [scale unit](https://learn.microsoft.com/en-us/azure/well-architected/mission-critical/application-design#scale-unit-architecture) is defined as a logical unit or function that can be scaled independently, potentially including code components, hosting platforms, deployment stamps, and even subscriptions when multitenant requirements are involved. +- The same guidance illustrates that [scale units](https://learn.microsoft.com/en-us/azure/well-architected/mission-critical/application-design#scale-unit-architecture) can range from microservice pods to cluster nodes and regional deployment stamps, and that using scale units helps standardize how capacity is added and validated before directing user traffic. + +## Quota groups and shared quota + +- The [Azure Quota Groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) article explains that quota groups are ARM objects created at the management group scope that allow you to share procured quota between subscriptions, distribute or reallocate unused quota, and submit group-level quota increase requests. +- [Supported scenarios](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) include deallocating unused quota from subscriptions into the group, allocating quota from the group back to subscriptions, and using group-level limit increases to make quota available for future transfers. +- Documentation notes that [quota groups are independent](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#quota-group-is-an-arm-object) of subscription placement in the management group hierarchy and do not automatically synchronize subscription membership, which keeps quota management orthogonal to policy and role hierarchies. +- The [transfer](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups#transfer-quota) and [quota allocation snapshot](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups#quota-allocation-snapshot) APIs provide a view of per-subscription limits and shareable quota for VM families and regions within a group, using the same quota constructs that apply to standard subscription quota checks. + +## Capacity reservations and compute supply + +This section focuses on compute supply through capacity reservations and capacity reservation groups, which are distinct from Azure Reservations and savings plans used for pricing and discount optimization. + +- The [on-demand capacity reservation overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) describes capacity reservations as a way to reserve compute capacity for a specific VM size in a region or availability zone, managed through capacity reservation groups. +- Reservations are created for a VM size, location, and quantity, and they can be [adjusted by changing the capacity property](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview#work-with-capacity-reservation); changes such as VM size or location require creating a new reservation and migrating workloads if needed. +- The documentation explains that [deployments that reference a capacity reservation group](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview#work-with-capacity-reservation) consume from the reserved quantity and skip quota checks up to that quantity, while deployments beyond the reserved quantity are considered [overallocations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) and are not covered by the capacity reservation SLA. +- [Overallocate capacity reservation guidance](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) clarifies the states for a reservation—capacity available, fully consumed, and overallocated—and shows how instance view data can be used to understand allocated versus reserved quantities. +- The capacity reservation overview also notes that [reserved capacity can be combined automatically with reserved instances](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview#benefits-of-capacity-reservation) to apply term-commitment discounts, while capacity reservations themselves do not require a one- or three-year commitment. + +## Reservations, savings plans, and utilization + +- [FinOps rate optimization guidance](https://www.finops.org/framework/capabilities/rate-optimization/) highlights Azure Advisor recommendations, reservation purchase recommendations, and savings plan purchase recommendations as starting points for deciding when to buy reservations or savings plans based on historical usage and cost. +- After commitments are purchased, the same guidance points to [portal experiences for viewing utilization](https://www.finops.org/framework/capabilities/rate-optimization/) for reservations and savings plans, with options to adjust scope or enable instance size flexibility to increase utilization. +- The documentation also describes [reservation utilization alerts](https://www.finops.org/framework/capabilities/rate-optimization/) that can notify stakeholders when utilization drops below a desired threshold, and showback and chargeback reports for reservations and savings plans. +- These [utilization views and alerts](https://www.finops.org/framework/capabilities/rate-optimization/) complement [capacity reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) and quota monitoring by providing cost-side signals about how effectively reserved capacity and savings plans are being used. + +## Monitoring quotas and capacity signals + +- The [quota monitoring and alerting article](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting) describes how the Quotas experience in the Azure portal tracks resource usage against quota limits and supports configuring alerts when usage approaches those limits. +- The [Create alerts for quotas](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) documentation details how to create alert rules from the **My quotas** blade by selecting a quota name, choosing severity, and setting a usage-percentage threshold for triggering alerts. +- Together with [quota allocation snapshots](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups#quota-allocation-snapshot) for quota groups and [instance view data](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) for capacity reservations, these monitoring capabilities provide the platform-level signals referenced in Azure's guidance on [scaling and capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning). + diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-planning/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-planning/README.md new file mode 100644 index 000000000..f88515f37 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-planning/README.md @@ -0,0 +1,61 @@ +--- +title: Capacity planning +parent: Capacity & quotas +nav_order: 1 +--- + +# Capacity planning framework + +Effective capacity planning combines historical usage analysis, forecasting, and scaling strategies so Azure workloads remain reliable without overspending, as described in [capacity planning in the Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) and [reliable scaling guidance](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling). Use this framework to structure planning cycles and link outputs to quota and reservation decisions, which aligns with [workload supply chain guidance](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain). + +[FinOps capability mapping](../capacity-and-quotas/README.md): Capacity planning supports Planning & Estimating and Forecasting by sizing scale units or deployment stamps, then exposing quota, region access, and reservation needs as capacity evidence. [Source](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) [Source](https://www.finops.org/framework/capabilities/) + +## Gather utilization data + +- [Collect telemetry for CPU, memory, storage, network throughput, and request latency](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#gather-capacity-data) from Azure Monitor logs or platform metrics. +- [Cleanse and normalize the data](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#gather-capacity-data)—remove anomalies, fill gaps, and align timestamps—before generating visuals or forecasts. +- [Document business context](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#gather-capacity-data) (seasonal events, marketing campaigns, product releases) to explain historical peaks and inform future scenarios. + +## Analyze existing workloads + +- [Identify peak utilization windows, transaction rates, and concurrency](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#understand-an-existing-workload) to pinpoint components that approach their limits. +- [Visualize metrics to highlight trends and anomalies](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#understand-an-existing-workload); charts show operators where bottlenecks have occurred or may emerge. +- [Map performance thresholds](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#understand-an-existing-workload) (SLA targets, response time goals) to resource utilization to determine safe operating ranges. + +## Plan for new workloads + +- When historical data is unavailable, [estimate resource demand by modeling expected user journeys, transaction volumes, and dependency behavior](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#understand-a-new-workload). +- [Incorporate buffer capacity for uncertainty](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning#understand-a-new-workload) and explicitly track assumptions so forecasts can be revised once real usage arrives. + +## Forecast demand + +- [Produce short-term (weekly/monthly) and long-term (quarterly/annual) projections](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) using historical trends or scenario planning. +- [Include confidence ranges and plan for both normal and surge conditions](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) (for example, special events, regulatory deadlines). + +> **Boundary:** Forecasting improves readiness and procurement planning, but it doesn't guarantee regional or zonal service availability by itself. If a target region or zone is restricted for a subscription, submit a [region access request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) and, where applicable, a [zonal enablement request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series). + +## Align scaling strategies + +- [Determine where horizontal scaling (additional instances) versus vertical scaling (larger SKUs) is appropriate](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling), ensuring services remain stateless where possible to support scale-out. +- [Mix scheduling, autoscale, and manual interventions](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling) to match predictable and unpredictable load patterns. Configure autoscale rules for sudden spikes while scheduling known seasonal adjustments. +- [Tie capacity plans to quota group management, capacity reservations, or savings plans](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) so infrastructure is ready when scaling triggers occur. + +## Governance cadence + +- **Monthly:** [Treat capacity planning as an iterative process](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning)—compare forecasts to actuals and adjust plans accordingly. +- **Quarterly:** [Revisit assumptions, incorporate new business initiatives, and adjust strategic investments](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) such as new regions or disaster recovery capacity. +- **Post-incident:** [When capacity shortfalls occur, update models with new data](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) and revise monitoring thresholds and escalation paths. + +## Outputs and integration + +- [Maintain a living capacity plan](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) that documents forecasts, scaling tactics, and required quota changes so adjustments can be made as conditions evolve. +- [Feed forecasted demand into budgeting and reservation purchasing cycles](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) to balance cost and performance. +- [Store charts, scripts, and assumptions in version control](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) to support knowledge sharing and future recalibration. + +## Incorporate rate optimization into capacity forecasts + +- Capacity planning must account for regional pricing differences, so include region-specific rate inputs in your forecast model using your billing and cost datasets ([FinOps Framework planning and estimating capability](https://www.finops.org/framework/capabilities/planning-estimating/), [FinOps Hubs overview](https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/hubs/finops-hubs-overview)). +- The [FinOps Framework planning and estimating capability](https://www.finops.org/framework/capabilities/planning-estimating/) provides guidance on integrating cost projections with capacity needs. +- Use the [Cost Optimization workbook](https://learn.microsoft.com/en-us/azure/advisor/advisor-workbook-cost-optimization) in Azure Advisor to identify reservation purchase recommendations aligned with your capacity forecasts. +- When forecasting capacity for new regions, query historical pricing via [FinOps Hubs](https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) to project accurate unit costs. + diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-reservations/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-reservations/README.md new file mode 100644 index 000000000..af45e7b01 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/capacity-reservations/README.md @@ -0,0 +1,75 @@ +--- +title: Capacity reservations +parent: Capacity & quotas +nav_order: 2 +--- + +# Capacity reservation operations + +> FinOps capability mapping: Capacity reservations support Architecting & Workload Placement, Usage Optimization, Rate Optimization coordination, Planning & Estimating, and Automation, Tools & Services. Use them after access and quota are staged, so critical SKUs, regions, and zones are covered before you onboard or surge. [Source](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) [Source](https://www.finops.org/framework/capabilities/) + +On-demand capacity reservations guarantee that compute capacity is available when critical workloads scale out. This guide explains how to create, share, monitor, and automate capacity reservation groups (CRGs) so platform teams can coordinate with quota and deployment workflows, and it reminds you where the platform enforces prerequisites. + +## Cost implications of capacity allocation + +- Capacity reservations are billed at pay-as-you-go rates for the reserved VM size whether or not VMs are deployed against them—unused reservations still incur cost. +- Regional pricing varies: the same VM SKU costs different amounts in different regions. Check [Azure pricing by region](https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/) before committing capacity to a specific location. +- Use [FinOps Hubs](https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) to analyze historical pricing across regions before allocating; see the [FinOps Toolkit query index](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md) for available queries. +- Capacity reservations are eligible for [Reserved Instance discounts](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations)—layer rate commitments on top of capacity commitments to maximize savings. +- Factor both capacity cost and rate optimization into unit economics before allocating; unused capacity reservations without RI coverage pay full PAYG rates. +- Run [reservation-recommendation-breakdown](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md) to evaluate whether reserved instances reduce costs for capacity you've already reserved. +- Use [commitment-discount-utilization](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md) to track whether capacity reservation costs are offset by reservation discounts. +- Use [savings-summary-report](https://github.com/microsoft/finops-toolkit/blob/main/src/queries/INDEX.md) to calculate your effective savings rate across both capacity and rate commitments. + +## Prerequisites and access + +- **Quota:** Creating [reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) consumes the same regional quota used by standard VM deployments. If the requested VM size, region, or zone lacks quota or inventory, the reservation request fails and must be retried after adjusting the request or increasing quota. +- **Permission scope:** The subscription that owns the CRG manages reservation creation, resizing, deletion, and [sharing](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share) (preview). Sharing requires two sets of rights: the ODCR owner in the consumer subscription needs `Microsoft.Compute/capacityReservationGroups/share/action`; the VM owner in the consumer subscription needs `Microsoft.Compute/capacityReservationGroups/read`, `Microsoft.Compute/capacityReservationGroups/deploy`, `Microsoft.Compute/capacityReservationGroups/capacityReservations/read`, and `Microsoft.Compute/capacityReservationGroups/capacityReservations/deploy`. +- **Supported SKUs:** Only specific [VM series](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) are eligible for capacity reservations; confirm support through the `ResourceSkus` API before planning rollouts. + +## Create and manage reservations + +1. **Create a CRG:** In the Azure portal, select **Virtual machines > Capacity reservations > Add**. Provide the subscription, resource group, region, and optional [availability zone](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview). +2. **Add member reservations:** Within the CRG, specify VM size (for example, `Standard_D2s_v3`) and quantity. Azure immediately attempts to [reserve capacity](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview); if it's unavailable the deployment fails and must be retried after adjusting parameters. +3. **Associate workloads:** When deploying a VM or scale set, set the `capacityReservationGroup.id` property so the workload [consumes the reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) and receives the capacity SLA. +4. **Adjust quantities:** Update the reservation to increase or reduce the quantity. [Reducing to zero](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) releases the capacity but retains metadata, which is useful when pausing workloads temporarily. +5. **Delete reservations:** Remove all associated VMs and reduce the quantity to zero before [deleting a member reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) or its CRG to avoid orphaned associations. + +## Sharing across subscriptions (preview) + +> This feature is in public preview. Portal support isn't available; use CLI, PowerShell, or REST API. See [preview terms](https://azure.microsoft.com/support/legal/preview-supplemental-terms/). **Source**: [Share a capacity reservation group](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share) + +Sharing lets a central subscription guarantee capacity for dependent workloads: + +1. **Designate roles:** Assign an [On-demand Capacity Reservation (ODCR) owner](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share?tabs=api-1%2Capi-2%2Capi-3%2Capi-4%2Capi-5%2Capi-6%2Cportal-7) in the consumer subscription with share permissions and VM owners with deploy permissions as required. +2. **Grant access:** From the producer subscription, add consumer subscription IDs to the [CRG share list](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share?tabs=api-1%2Capi-2%2Capi-3%2Capi-4%2Capi-5%2Capi-6%2Cportal-7). You can share individual CRGs or all CRGs in the provider subscription, and up to 100 consumer subscriptions can be granted access per group. If you're sharing zonal capacity reservations, [validate zone alignment between producer and consumer subscriptions](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share) because Azure assigns different logical-to-physical zone mappings per subscription and mismatched zones won't fail at deployment time but will result in workloads landing in the wrong physical zones. +3. **Deploy from consumers:** Consumer subscriptions enumerate shared CRGs and specify the `capacityReservationGroup` field during VM deployment. [Capacity usage is billed](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share?tabs=api-1%2Capi-2%2Capi-3%2Capi-4%2Capi-5%2Capi-6%2Cportal-7) to the provider subscription, while VM runtime usage is billed to the consuming subscription. +4. **Revoke:** Remove the consumer subscription or associated identities from the share list to stop new deployments. [Existing VMs must be disassociated](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share?tabs=api-1%2Capi-2%2Capi-3%2Capi-4%2Capi-5%2Capi-6%2Cportal-7) or deallocated before revocation completes. + +## Overallocating and utilization states + +CRGs support temporary overallocations to absorb burst traffic: + +- **Reserved capacity available:** Allocated VM count is lower than reserved quantity. Consider [reducing quantity](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) if the buffer is no longer required. +- **Reservation consumed:** Allocated VM count equals reserved quantity. [Additional workloads deploy without SLA](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) until capacity increases. +- **Reservation overallocated:** Allocated VM count exceeds reserved quantity. [Excess VMs run without the capacity SLA](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) and won't regain it after deallocation unless capacity is increased. + +![Diagram that shows capacity reservation with the third VM allocated.](https://learn.microsoft.com/en-us/azure/virtual-machines/media/capacity-reservation-overview/capacity-reservation-3.jpg) + +Use the [CRG Instance View](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) (`$expand=instanceview`) to track utilization and determine whether to right-size or overprovision reservations. + +## Monitoring and reporting + +- Export [Instance View data](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) regularly to track allocated versus reserved quantities per member reservation. +- Correlate reservation usage with [quota audits](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) (for example, `az quota usage list --resource-provider Microsoft.Compute`) to ensure reservation growth aligns with available regional quota. + +## Automation patterns + +- **Create/update reservations:** Use the [`Microsoft.Compute/capacityReservationGroups` REST API](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) or `az resource` commands within CI/CD pipelines to create CRGs and member reservations with declarative templates. +- **Associate workloads:** Embed the [`capacityReservationGroup` property](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) in ARM/Bicep templates or Terraform modules so deployments automatically consume the reservation when promoted to production. +- **Sharing automation:** Script [share assignments](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share?tabs=api-1%2Capi-2%2Capi-3%2Capi-4%2Capi-5%2Capi-6%2Cportal-7) by calling the `share` action with the desired consumer subscription list, ensuring idempotent operations during pipeline runs. + +## Operational checklist + +1. Validate required [quota and inventory](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) before reserving capacity for new regions or VM sizes. +2. Review [reservation utilization](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate) regularly; adjust quantities when buffers consistently remain unused or when overallocations persist. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/escalation/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/escalation/README.md new file mode 100644 index 000000000..f67498f6e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/escalation/README.md @@ -0,0 +1,33 @@ +--- +title: Support escalation +parent: Support & reference +nav_order: 1 +--- + +# Support escalation guide + +Self-service quota tooling resolves most requests, but some capacity problems still require Microsoft intervention. Use this guide to recognize when escalation is necessary and how to submit a support ticket with the required context so you don't lose time gathering details after the ticket opens. + +## When to escalate + +- **Restricted regions or zones:** Subscriptions cannot deploy to a region or zone because of [access restrictions](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) that only Microsoft can lift. +- **Non-adjustable quotas:** The **My quotas** blade flags the target quota as [non-adjustable](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal) or the automated request is denied. +- **Service-specific limits:** Services such as Azure Cosmos DB require engineering review to raise [account/container limits or throughput ceilings](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). +- **Capacity SLA claims:** [Capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) fail to meet the SLA despite available quantity, requiring investigation and potential credits. + +## Pre-submission checklist + +- Confirm you have Owner, Contributor, or Support Request Contributor rights on the subscription; without appropriate RBAC the portal blocks [ticket creation](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request). + +## Creating the request + +1. Open the Azure portal, select the **?** icon, and choose [**Create a support request**](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request). +2. On the **Problem description** tab, select [**Service and subscription limits (quotas)**](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request), choose the subscription, and pick the relevant quota type (for example, `Compute-VM (cores-vCPUs)`, [`Azure Cosmos DB`](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase), or `Microsoft Fabric`). +3. Provide detailed [problem statements](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process), including region, VM series, desired quota value, and [deployment blockers](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). +4. Attach [supporting files](https://learn.microsoft.com/en-us/azure/azure-portal/supportability/how-to-create-azure-support-request) (screenshots, export logs) and specify severity and preferred contact method. +5. Submit and capture the support request ID for tracking. + +## Region and zone access workflow + +- When requesting [region or zone enablement](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process), list all regions, VM series, and logical zones required for upcoming deployments within the ticket. +- Reference [prior approvals](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) if recycling subscriptions so Microsoft can reconnect previously granted access. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/glossary.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/glossary.md new file mode 100644 index 000000000..5964b4e53 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/glossary.md @@ -0,0 +1,49 @@ +--- +title: Glossary & FAQ +parent: Support & reference +nav_order: 3 +--- + +# Glossary & FAQ + +Common terminology used across the quota and capacity management guides. Each entry links to the authoritative Microsoft documentation so you're never guessing which article to cite. + +## Glossary + +- **Capacity and billing operations:** Unified concept for estate-level capacity planning, quota governance, capacity reservations, and Azure billing models (EA and MCA), aligned with [Well-Architected capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning), [workload supply chain guidance](https://learn.microsoft.com/en-us/azure/well-architected/operational-excellence/workload-supply-chain), and the [FinOps Framework](https://www.finops.org/framework/). See [capacity and billing operations](capacity-and-quotas/README.md) for the full reference. +- **Capacity reservation group (CRG):** A logical container that holds one or more [on-demand capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) for specific VM sizes, regions, and zones. CRGs guarantee capacity and can be shared across subscriptions. +- **Capacity reservation:** A compute object that reserves capacity for a specific VM size in a region or availability zone, managed through a capacity reservation group, as described in the [on-demand capacity reservations overview](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview). Capacity reservations protect supply but do not change pricing on their own. +- **Azure Reservation:** A pricing construct that applies term-commitment discounts to eligible compute usage over one- or three-year terms, as described in the [FinOps rate optimization guidance](https://www.finops.org/framework/capabilities/rate-optimization/) and [Azure Reservations documentation](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/save-compute-costs-reservations). Azure Reservations reduce cost but don't guarantee capacity. +- **Azure savings plan:** A flexible pricing construct that applies discounts to eligible compute usage across services and regions over a fixed term, as described in [Azure savings plan for compute](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/). Savings plans optimize rates but do not guarantee capacity. +- **FinOps Hubs:** Data pipeline infrastructure from the [FinOps Toolkit](https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) that ingests cost and usage data into Azure Data Explorer via Data Factory. Hubs support KQL queries against normalized cost data using the [FOCUS schema](https://focus.finops.org/). +- **Quota group:** An Azure Resource Manager object created under a management group that [aggregates compute quota](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) across eligible subscriptions, allowing transfers and group-level increase requests. +- **Logical availability zone:** Subscription-specific mapping to [physical datacenter zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support); mappings can differ across subscriptions and must be queried via Azure Resource Manager APIs. +- **Quota alert:** An Azure Monitor alert triggered when [quota usage crosses a configured threshold](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) in the **My quotas** experience. +- **Budget alert:** A Cost Management alert generated when [actual or forecasted spend exceeds defined budget thresholds](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending). +- **Deployment stamp:** A self-contained unit of infrastructure that deploys a full copy of an application along with its supporting services, following the [Deployment Stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview#deployment-stamps-pattern). ISVs use stamps to isolate tenants, control blast radius, and scale independently across regions and zones. Each stamp is a quota and capacity consumer. +- **MACC (Microsoft Azure Consumption Commitment):** A contractual commitment to spend a minimum dollar amount on Azure over a defined period, as described in the [Microsoft Azure Consumption Commitment benefit](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/azure-consumption-commitment-benefit). Qualifying Azure services count toward MACC drawdown; marketplace purchases may or may not count depending on the offer. +- **Offer restriction:** A constraint on a VM series that blocks deployment in a subscription until the restriction is lifted via a support request. Offer restrictions differ from regional quota: quota limits how much you can deploy; offer restrictions prevent any deployment of the restricted SKU. Submit a [zonal enablement request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) or a [region access request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) to clear an offer restriction. +- **Overallocation:** The state in which more VMs are associated with a capacity reservation group than its reserved quantity, as described in [capacity reservation overallocation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overallocate). Overallocated VMs run without a capacity SLA and don't regain it after deallocation unless the reserved quantity is increased. +- **Scale unit:** A pre-defined, deployable increment of capacity that delivers a target service level for a specific customer or tenant count. Platform teams size scale units from load-testing and telemetry, then use scale units as the unit of demand in capacity forecasting. See [capacity planning](https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/capacity-planning) and [Well-Architected reliable scaling](https://learn.microsoft.com/en-us/azure/well-architected/reliability/scaling). +- **Subscription request:** Workflow that allows an MCA billing owner to [create a subscription for a user or service principal in another tenant](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-subscription-request), requiring the recipient to accept ownership. +- **Quota:** A per-subscription limit on the number of resources (typically vCPUs per VM family) that can be deployed in a given region, managed through [per-VM quota requests](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) or [regional quota requests](https://learn.microsoft.com/en-us/azure/quotas/regional-quota-requests). +- **Management group:** An Azure Resource Manager scope above subscriptions that organizes subscriptions for [RBAC, policy, and governance](https://learn.microsoft.com/en-us/azure/governance/management-groups/overview). Quota groups are created under management groups but don't inherit their policy or access controls. +- **Subscription vending:** A design pattern that standardizes how platform teams collect subscription requests, apply placement rules, and connect approvals to automation, as described in the [subscription vending design area](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/subscription-vending). +- **Anomaly alert:** A Cost Management alert that uses machine learning to detect [unexpected spending patterns](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending) and flag deviations from historical baselines. +- **Region access:** A support workflow that unblocks a subscription in a restricted Azure region. Submit a [region access support request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) when a subscription can't deploy to a specific region. Region access differs from quota: quota limits how much you can deploy; region access controls whether you can deploy at all. +- **Zonal enablement:** A support workflow that grants access to restricted VM series in specific availability zones. Submit a [zonal enablement request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) to clear the restriction. +- **FinOps:** A cloud financial management discipline that combines systems, best practices, and culture to increase an organization's ability to understand cloud costs and make informed trade-offs, as described in the [FinOps Framework](https://www.finops.org/framework/). + +## FAQ + +**When should we request a region access ticket instead of increasing quota?** +Quota groups and standard increases manage capacity within already-enabled regions. If the subscription cannot deploy to a specific region because access is restricted, submit a [region access support request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process). + +**How do we recycle a subscription without losing zone enablement?** +Reclaim quota and billing ownership but keep the subscription active. Zone access flags remain in place; deleting the subscription may require repeating the [access request workflow](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) for future projects. + +**What is the difference between capacity reservations, Azure Reservations, and savings plans?** +[Capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) ensure availability of specific VM capacity in a region or availability zone through capacity reservation groups. Azure Reservations and [Azure savings plans](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/) provide pricing discounts over a defined term, as described in [FinOps rate optimization guidance](https://www.finops.org/framework/capabilities/rate-optimization/), but they do not guarantee capacity. + +**Do quota alerts and budget alerts require different permissions?** +[Quota alerts](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) rely on Azure Monitor alert permissions (Reader or higher on the subscription), while [budget alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending) follow Cost Management RBAC (Owner, Contributor, Cost Management roles). Configure both to ensure quota usage and cost trends reach the right stakeholders. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/legacy/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/legacy/README.md new file mode 100644 index 000000000..5a0c583d4 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/legacy/README.md @@ -0,0 +1,30 @@ +--- +title: EA subscription creation (billing scope) +parent: Subscription operations +nav_order: 2 +--- + +# Enterprise Agreement subscription creation (billing scope) + +Use this guide when you're scripting Enterprise Agreement (EA) subscription creation and need reminders about which enrollment scopes and roles drive the subscription alias APIs. In EA, the control surface for creating a subscription is the enrollment account, not the subscription itself. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) + +## Scope boundaries + +- Enrollment scopes: Enrollment accounts determine billing ownership and who can create subscriptions. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) +- Subscription scope: The `Microsoft.Subscription/aliases` request returns a subscription ID, after which subscription-scoped automation applies landing zone policies, networking, and budgets. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) + +## Roles and prerequisites + +- Only Enterprise Administrators or Enrollment Account owners (or their delegated owners) can [programmatically create EA subscriptions](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest); service principals must receive the same enrollment account role assignment before calling the APIs. +- The enrollment account relationship determines where usage is billed, so automation should [authenticate in the account owner's home directory](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) before issuing API calls. + +## Provisioning flow + +1. List accessible [enrollment accounts](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) through `Microsoft.Billing/billingAccounts` to capture the enrollment and account identifiers required for new subscriptions. +2. Create or reuse a [subscription alias](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) with the target account owner and desired display metadata using the `Microsoft.Subscription/aliases` API. +3. [Assign Azure RBAC roles](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) (owner, contributor, reader) to the subscription during creation to ensure landing zone automation can immediately configure policies, networking, and budget controls. + +## Automation considerations + +- Keep [enrollment-account ownership](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) in sync with subscription vending approval workflows so the platform team can audit who can mint new EA subscriptions. +- When migrating from the legacy 2015-07-01 APIs, reissue [enrollment role assignments](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-enterprise-agreement?tabs=rest) with API version `2019-10-01-preview` to support the latest alias operations. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/modern/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/modern/README.md new file mode 100644 index 000000000..8f7e211bd --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/modern/README.md @@ -0,0 +1,35 @@ +--- +title: MCA subscription creation (billing scope) +parent: Subscription operations +nav_order: 1 +--- + +# Microsoft Customer Agreement subscription creation (billing scope) + +Use this guide when you're automating Microsoft Customer Agreement (MCA) subscription creation and need reminders about which billing identifiers and scopes feed the subscription alias APIs. In MCA, the control surface for creating a subscription lives at the billing account, billing profile, and invoice section scopes—not at the subscription scope itself. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) + +## Scope boundaries + +- Billing scopes: Billing account, billing profile, and invoice section determine where charges roll up and which identities can create subscriptions. [Source](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/azure-billing-microsoft-customer-agreement) [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) +- Subscription scope: The `Microsoft.Subscription/aliases` request returns a subscription ID, after which subscription-scoped automation applies landing zone policies, networking, and budgets. [Source](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) + +## Roles and prerequisites + +- [Billing account, billing profile, invoice section, and Azure subscription creator roles](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) can programmatically mint MCA subscriptions; the same roles can be delegated to service principals for automation. +- Automation should capture [billing account, billing profile, and invoice section identifiers](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) before provisioning so the subscription alias points to the correct billing scope. + +## Standard provisioning flow + +1. [Enumerate accessible billing accounts](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) via `Microsoft.Billing/billingAccounts` and confirm the `agreementType` is `MicrosoftCustomerAgreement`. +2. [Retrieve billing profiles and invoice sections](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) within the target account to determine the billing scope path used during alias creation. +3. [Submit a `Microsoft.Subscription/aliases` request](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement?tabs=rest) with the destination tenant, owner object ID, workload classification, and billing scope. Azure returns the subscription ID after the alias is ready. + +## Associated billing tenant scenario + +- When the destination tenant is associated to the billing account, [register an application in that tenant, grant it the required billing role, and then call the alias API directly from the destination tenant's service principal](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-customer-agreement-associated-billing-tenants). +- This [streamlined method transfers creation permissions to the destination tenant](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-customer-agreement-associated-billing-tenants), which is useful for SaaS platforms or regulated environments that still need centralized billing. + +## Two-phase cross-tenant scenario + +- For tighter governance, use the [dual-application pattern: register apps in both source (billing) and destination tenants, assign billing roles in the source, and have the destination app accept the subscription during alias creation](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement-across-tenants). +- The [two-phase flow lets the source tenant retain approval authority](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription-microsoft-customer-agreement-across-tenants) while allowing workloads to exist in isolated Entra tenants tied back to the same MCA billing account. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/monitoring-alerting/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/monitoring-alerting/README.md new file mode 100644 index 000000000..612c5ef1d --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/monitoring-alerting/README.md @@ -0,0 +1,32 @@ +--- +title: Monitoring & alerting +parent: Capacity & quotas +nav_order: 7 +--- + +# Monitoring & alerting guide + +> FinOps capability mapping: Quota and reservation alerts support Automation, Tools & Services, Forecasting, Anomaly Management, Budgeting, and Governance, Policy & Risk. Alert on quota and reservation utilization before saturation so releases and onboarding don't stall. [Source](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) [Source](https://www.finops.org/framework/capabilities/) + +Proactive monitoring prevents quota exhaustion and cost overruns from surprising application teams. This guide outlines how to turn on quota monitoring, author alert rules, and align quota telemetry with cost guardrails so you don't miss leading indicators. + +## Turn on quota monitoring + +- From the Azure portal, open **Quotas** and select **My quotas**. When [monitoring is turned on](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting) for a provider, adjustable quotas become highlighted and clickable, exposing usage details and alert creation shortcuts. +- Ensure administrators hold the [required Azure RBAC permissions](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting)—typically Owner, Contributor, or a custom role with alert authoring rights—before configuring monitoring and alert rules. + +## Create quota usage alerts + +1. **Select quota:** In **My quotas**, choose the [resource provider](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) (for example, `Microsoft.Compute`, `Microsoft.Storage`, or `Microsoft.Web`) and select the quota to monitor. +2. **Launch alert wizard:** Select the quota name to open the [**Create usage alert rule**](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) experience. Quota-specific dimensions (subscription, region, quota name) are pre-populated. +3. **Configure thresholds:** Set the [usage percentage trigger](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) (for example, 70%, 85%, 95%) and choose the evaluation cadence (5–15 minutes) to balance responsiveness and noise. +4. **Notifications:** Define [action groups](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) (email, Teams, ITSM) to route alerts to responders. Confirm the managed identity or user configuring the alert has Reader access on the subscription. + +## Quota dashboards + +- Export quota usage via [`az quota usage list`](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) for each provider and ingest into Log Analytics or Power BI for trend dashboards. Consistent exports allow teams to visualize approaching limits and correlate with deployment events. + +## Cost management guardrails + +- [Configure budget alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending) at the subscription, billing profile, or invoice section level to warn stakeholders when actual or forecasted spend approaches agreed thresholds. Budget alerts deliver notifications in tandem with quota alerts to reinforce accountability. +- Set [cost anomaly alerts](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending) to detect unexpected spikes that may indirectly signal runaway deployments consuming quota faster than planned. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/non-compute-quotas/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/non-compute-quotas/README.md new file mode 100644 index 000000000..dca314c53 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/non-compute-quotas/README.md @@ -0,0 +1,93 @@ +--- +title: Non-compute quotas +parent: Capacity & quotas +nav_order: 3 +--- + +# Non-compute quota guide + +## When to use this guide + +Azure capacity planning extends well beyond vCPU cores. Storage accounts, App Service plans, Azure Cosmos DB accounts, and emerging platform services all impose limits that can block customer onboarding if they are not tracked and increased ahead of demand. This guide captures the baseline limits, monitoring patterns, and escalation paths for the most common non-compute services so operations teams can manage quota holistically without juggling separate notes. + +## Service quick reference + +| Service | Default scope & notable limits | How to check usage | How to request more | +| --- | --- | --- | --- | +| **Azure Storage** | [250 standard storage accounts per subscription and region (increaseable to 500)](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests); [per-account throughput and egress limits vary by SKU](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview#scalability-targets-for-standard-storage-accounts). | [`az storage account show-usage`, `Get-AzStorageUsage`](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests), or [`az quota usage list --resource-provider Microsoft.Storage`](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest). | Use [**My quotas > Storage** to submit a numeric limit](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests); [fallback to support ticket if auto-approval fails](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal). | +| **Azure App Service** | [App Service plans capped per region (10 Free/Shared, 100 per resource group for higher tiers); storage quota enforced per plan and per region/resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-app-service-limits). | [`az quota usage list --resource-provider Microsoft.Web`](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) to export plan counts; portal usage charts per plan. | [Submit App Service quota adjustments through **My quotas > Web**; escalate via support when non-adjustable](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal). | +| **Azure Cosmos DB** | [500 databases/containers per account, request throughput change limits per 5-minute window; higher limits require support review](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). | Monitor provisioned throughput and request units in portal/metrics; track account limits manually. | [Create a support request (Quota type: Azure Cosmos DB) with workload details and desired limits](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). | + +If your workloads depend on other services (for example, Azure OpenAI, Dev Box, Azure Deployment Environments), extend this guide by adding their limits, monitoring commands, and support workflows so everyone's using the same reference. + +## Azure Storage quota operations + +### Key limits and dependencies + +- [Each subscription can hold up to 250 standard storage accounts per region by default; increases up to 500 require approval](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests). +- [Per-account scalability targets (aggregate ingress/egress, request rate, replication constraints) depend on the account kind](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview#scalability-targets-for-standard-storage-accounts). Include these constraints when forecasting storage demand. + +### Usage and tooling + +- Run [`az storage account show-usage --location ` to list the current count versus limit for storage accounts in a region](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests). +- [PowerShell administrators can retrieve the same data with `Get-AzStorageUsage` for automation pipelines](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests). +- Use [`az quota usage list --scope /subscriptions/ --resource-provider Microsoft.Storage` to generate machine-readable quota snapshots](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) that align with other quota reporting scripts. + +### Request workflow + +1. [Open **Azure portal > Quotas > Storage** and select the subscription](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests). +2. [Choose the region and select the pencil icon under **Request adjustment** to enter a new limit (up to 500)](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests). +3. [Submit the request; most approvals complete within minutes](https://learn.microsoft.com/en-us/azure/quotas/storage-account-quota-requests). +4. [If the request is denied or the limit is non-adjustable, use the **Create support request** link presented in **My quotas** to route the request to Microsoft support](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal). + + +## Azure App Service quota operations + +### Key limits and dependencies + +- [Free and Shared plans are limited to 10 instances per region, while Basic, Standard, Premium, and Isolated tiers allow up to 100 plans per resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-app-service-limits). +- [Storage quotas are enforced per App Service plan (10 GB Basic, 50 GB Standard, 250 GB Premium, 1 TB Isolated) and aggregated across plans within the same region/resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-app-service-limits). +- [Scale-out ceilings range from 3 instances (Basic) to 30 instances (Premium v2/v3/v4) and 100 instances (Isolated)](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-app-service-limits). + +### Usage and tooling + +- Use [`az quota usage list --resource-provider Microsoft.Web --scope /subscriptions/` to pull plan counts and limits for automation or dashboards](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest). +- [Review per-plan metrics (connections, storage consumption) in the App Service blade to anticipate when plan-level storage limits approach exhaustion](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-app-service-limits). + +### Request workflow + +1. [Navigate to **Azure portal > Quotas > Web** and locate the target region](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal). +2. [Select the relevant quota row (for example, `AppServicePlanCount`) and choose **New quota request**](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal). +3. [Enter the desired limit and submit. Azure applies the increase automatically when capacity is available](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal). +4. [If the quota is non-adjustable or the request fails, generate a support ticket from the same blade with justification and deployment timelines](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal). + + +## Azure Cosmos DB quota operations + +### Key limits and dependencies + +- [Each account supports up to 500 databases and containers combined, and provisioned throughput changes are limited to 25 updates per five-minute interval](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). +- [Azure Cosmos DB enforces additional request limits (for example, list/get keys operations) that can throttle automation if not accounted for](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). + +### Request workflow + +1. [From **Help + Support**, create a new support request with Issue type **Service and subscription limits (quotas)** and Quota type **Azure Cosmos DB**](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). +2. [Provide workload context, current limits, desired values, and any diagnostic artifacts requested on the Additional details tab](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). +3. [Specify severity and preferred contact, then submit. The Cosmos DB engineering team typically responds within 24 hours to confirm or gather more information](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). + +[Because increases require manual approval, plan requests well ahead of large onboarding waves and track throughput usage via Azure Monitor to justify the ask](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase). + +## Monitoring and alerting + +- [Turn on quota monitoring in the Azure portal; adjustable quotas become clickable, allowing you to open the alert rule wizard directly from **My quotas**](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting). +- [Create usage alert rules with thresholds (for example, 70/85/95 percent) and severity levels aligned to escalation procedures](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting). +- [Integrate alerts with cost monitoring by configuring budget alerts for the same subscriptions, ensuring cost anomalies and quota exhaustion trigger complementary notifications](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/cost-mgt-alerts-monitor-usage-spending). + +## Extend this guide + +Maintain a backlog of additional services the community relies on (Azure OpenAI, Azure SQL, Azure Deployment Environments). For each addition, document: + +- Default limits and any preview restrictions. +- CLI/PowerShell/REST commands to retrieve usage. +- The portal or support path required for increases. +- Monitoring hooks and escalation timelines. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/quota-groups/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/quota-groups/README.md new file mode 100644 index 000000000..ffbb21f20 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/quota-groups/README.md @@ -0,0 +1,63 @@ +--- +title: Quota groups reference +parent: Capacity & quotas +nav_order: 5 +--- + +# Azure quota groups reference + +> FinOps capability mapping: Quota groups support Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, and Automation, Tools & Services by pooling eligible compute quota across subscriptions after region and zonal access are available. [Source](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) [Source](https://www.finops.org/framework/capabilities/) + +[Azure quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) are Azure Resource Manager (ARM) objects that let you share and self-manage compute quota across a set of subscriptions. This page aggregates key prerequisites, limitations, lifecycle behavior, and monitoring options from the official documentation so you can reason about quota groups alongside the other guides in this site, and you'll see where each Microsoft article fits. + +## Feature overview + +- [Quota groups elevate quota](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) from a per-subscription construct to a group-level ARM resource created under a management group, while quota enforcement at deployment time still occurs at the subscription level. +- The feature supports [quota sharing across subscriptions](https://learn.microsoft.com/en-us/azure/quotas/quota-groups), self-service reallocation of unused quota, and group-level quota increase requests that can later be allocated to individual subscriptions. +- [Supported scenarios](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) in the documentation include deallocating unused quota from subscriptions into the group, allocating quota from the group back to target subscriptions, and submitting quota group limit increase requests for specific regions and VM families. + +## Prerequisites + +- The [`Microsoft.Quota` and `Microsoft.Compute` resource providers must be registered](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#prerequisites) on all subscriptions you plan to add to a quota group. +- [A management group is required](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#prerequisites) to create a quota group. The group is created at the [management group scope](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#quota-group-is-an-arm-object) and inherits read and write permissions from that parent. +- [Subscriptions from different management groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#prerequisites) can be added to the same quota group; [subscription membership is independent](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#quota-group-is-an-arm-object) of the management group hierarchy as long as permissions allow it. +- The official guidance calls out the following [roles for operating quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#permissions): + - Assign the GroupQuota Request Operator role on the management group where the quota group is created. + - Assign the Quota Request Operator role on participating subscriptions for users or applications that perform quota operations. + - Assign Reader on participating subscriptions for users or applications that need to view quota group resources in the portal. + +## Limitations and scope + +- [Quota groups are available](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#limitations) only for Enterprise Agreement, Microsoft Customer Agreement, and internal subscriptions. +- They currently [support IaaS compute resources only](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#limitations) and are available in public cloud regions. +- [A subscription can belong to only one quota group](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#limitations) at a time, as noted in both the [limitations](https://learn.microsoft.com/en-us/azure/quotas/add-remove-subscriptions-quota-group#add-subscriptions-to-a-quota-group) and subscription management documentation. +- Quota groups focus on quota management; they [don't grant regional or zonal access](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#limitations). [Region and zonal access still require separate support requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process), and quota transfers or deployments can fail if the target subscription lacks region or zone access. +- [Management group deletion affects access](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#limitations) to the quota group limit. You must clear out group limits and delete the quota group object before deleting the management group, or recreate the management group with the same ID to regain access. + +## ARM object and lifecycle behavior + +- [Quota groups are global ARM resources](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#quota-group-is-an-arm-object) created at the management group scope and designed as an orthogonal grouping mechanism for quota management, separate from subscription placement in the management group hierarchy. +- [Subscription lists aren't automatically synchronized](https://learn.microsoft.com/en-us/azure/quotas/quota-groups#quota-group-is-an-arm-object) from management groups; instead, you [explicitly add and remove subscriptions](https://learn.microsoft.com/en-us/azure/quotas/add-remove-subscriptions-quota-group#add-subscriptions-to-a-quota-group) to control which ones participate in group-level quota operations. + +![Diagram of Management Group hierarchy with sample Quota Groups created under Management Group.](https://learn.microsoft.com/en-us/azure/quotas/media/quota-groups/sample-management-group-quota-group-hierarchy.png) + +- [Creating or deleting a quota group](https://learn.microsoft.com/en-us/azure/quotas/create-quota-groups) requires the GroupQuota Request Operator role on the management group. +- When you [add subscriptions to a quota group](https://learn.microsoft.com/en-us/azure/quotas/add-remove-subscriptions-quota-group#add-subscriptions-to-a-quota-group), they carry their existing quota and usage; adding them doesn't change their subscription limits or usage values. +- When you [remove subscriptions from a quota group](https://learn.microsoft.com/en-us/azure/quotas/add-remove-subscriptions-quota-group#remove-subscriptions-from-a-quota-group), they retain their existing quota and usage. The group limit isn't automatically changed by removal operations. +- At creation time, the [quota group limit is set to zero](https://learn.microsoft.com/en-us/azure/quotas/quota-groups). You must either transfer quota from a subscription in the group or [submit a quota group limit increase request](https://learn.microsoft.com/en-us/azure/quotas/quota-group-limit-increase) and wait for approval before the group can allocate capacity. +- [Before deleting a quota group](https://learn.microsoft.com/en-us/azure/quotas/create-quota-groups), all subscriptions must be removed from it, as described in the create/delete guidance. + +## Quota transfers and allocation snapshots + +- The ["Transfer quota within an Azure Quota Group" article](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups#transfer-quota) describes how to move unused quota from a subscription to the group (deallocation) or from the group to a subscription (allocation) using the quota group ARM object. +- [Quota allocation snapshots](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups#quota-allocation-snapshot) expose, for each subscription in the group, a Limit value (current subscription limit) and a Shareable quota value that reflects how many cores have been deallocated or transferred between the subscription and the group. +- The [example in the snapshot section](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups#quota-allocation-snapshot) shows a shareable quota of `-10` to indicate that 10 cores were given from the subscription to the group, alongside a new subscription limit of 50 cores for the corresponding VM family and region. +- These documented fields give you a [consistent view of how quota is distributed](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) between the group and its member subscriptions without changing how [deployments are evaluated](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups#quota-allocation-snapshot) against per-subscription limits. + +## Monitoring and alerting + +- The [Quotas experience in the Azure portal](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting) includes a **My quotas** view that continuously tracks resource usage against quota limits for providers such as Microsoft.Compute, and supports alerting when usage approaches limits. +- The [quota monitoring and alerting documentation](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting) explains that quota alerts are notifications triggered when resource usage nears the predefined quota limit, and that you can create multiple alert rules across quotas in a subscription. +- The ["Create alerts for quotas" article](https://learn.microsoft.com/en-us/azure/quotas/how-to-guide-monitoring-alerting) documents how to create alert rules from the Quotas page by selecting a quota name in **My quotas**, choosing an alert severity, and configuring a usage-percentage threshold for triggering alerts. +- While [quota group operations](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) are scoped to a management group, the [quota monitoring and alerting features](https://learn.microsoft.com/en-us/azure/quotas/monitoring-alerting) give you a way to observe usage and quota consumption trends for the underlying subscriptions that participate in the group. + diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/quota/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/quota/README.md new file mode 100644 index 000000000..692e36acb --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/quota/README.md @@ -0,0 +1,145 @@ +--- +title: Quota operations +parent: Capacity & quotas +nav_order: 4 +--- + +# Quota operations guide + +Use this guide when you're auditing or increasing Azure quotas so every request pulls from the same reference. + +[FinOps capability mapping](../capacity-and-quotas/README.md): Quota, region access, zonal enablement, and SKU readiness support Architecting & Workload Placement, Planning & Estimating, Forecasting, and Governance, Policy & Risk. Use this guide to verify those Azure constraints before moving quota into groups or reserving capacity. [Source](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) [Source](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) [Source](https://www.finops.org/framework/capabilities/) + +## Understand default quotas and enforcement + +- Azure enforces [quotas per subscription and region](https://learn.microsoft.com/en-us/azure/virtual-machines/quotas), tracking both total regional vCPUs and per VM-family vCPUs; deployments must stay within both limits or the [platform blocks the request](https://learn.microsoft.com/en-us/azure/quotas/regional-quota-requests). +- Default vCPU quotas vary by offer type. [Enterprise Agreement subscriptions default to 350 cores](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/ea-portal-agreements#resource-prepayment-and-requesting-quota-increases) for both the total regional quota and each VM family (D-series, E-series, etc.), while pay-as-you-go subscriptions default to 20 cores per region. Validate your actual limits in the [**Quotas**](https://learn.microsoft.com/en-us/azure/quotas/view-quotas) blade to confirm no offer restrictions are reducing your baseline. +- All quotas can be increased through support requests, regardless of the initial default assigned to your offer type. +- [Quota calculations](https://learn.microsoft.com/en-us/azure/virtual-machines/quotas) include allocated and deallocated virtual machines, so idle cores still count against the quota until resources are deleted or quota is increased. + +## Understand offer restrictions + +Azure applies [SKU restrictions based on your subscription's offer type](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-sku-not-available), blocking deployment of certain VM families in specific regions or zones even when quota is available. These restrictions exist independently of quota limits and you'll need to request access before deploying to affected areas. + +### What offer restrictions are + +- SKU restrictions tie to the subscription's offer type (pay-as-you-go, trial, student, MSDN, EA, or MCA Enterprise) and determine which VM families can deploy in each region and zone. +- Azure imposes two types of restrictions: [location restrictions](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-sku-not-available) block the SKU from an entire region, while zone restrictions block specific availability zones within an otherwise accessible region. +- When [`Get-AzComputeResourceSku`](https://learn.microsoft.com/en-us/powershell/module/az.compute/get-azcomputeresourcesku) shows `NotAvailableForSubscription` in the `Restriction` column, the SKU isn't available for this subscription's offer type, regardless of quota availability. + +### Why restrictions exist + +- Azure uses restrictions to [control capacity distribution](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-sku-not-available) across subscription tiers and manage datacenter resource allocation. +- Trial, student, and MSDN subscriptions face more restrictions because they're designed for development and learning scenarios rather than production workloads. +- Enterprise-tier subscriptions (EA and MCA Enterprise) have fewer restrictions, reflecting their use for production SaaS estates and mission-critical workloads. + +### Error messages you'll see + +When deployments fail because of offer restrictions, you'll encounter these error codes: + +- [`SkuNotAvailable`](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-sku-not-available)—the primary error when the requested SKU isn't available in the target region or zone. When triggered via ARM templates, this surfaces as `InvalidTemplateDeployment` during validation, and [no deployment history is created](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/find-error-code)—check the activity log instead. +- `ZonalAllocationFailed`—Azure can't allocate resources in the specific zone you requested, often because of zone restrictions or transient capacity constraints. +- `AllocationFailed`—a general allocation failure indicating capacity issues, restrictions, or other constraints preventing deployment. +- [`OverconstrainedAllocationRequest`](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/allocation-failure)—too many constraints (VM size, availability zone, accelerated networking, proximity placement group, ephemeral disk, Ultra disk) prevent Azure from finding available capacity. Remove constraints incrementally to identify the blocker. + +### Detect restrictions before deployment + +- Run [`Get-AzVMQuotaUsage.ps1`](https://github.com/microsoft/azcapman/tree/main/scripts/quota) to check restrictions before deploying. The script outputs `RegionRestricted` (True/False indicating whether the SKU is blocked for the entire region) and `ZonesRestricted` (comma-separated list of logical zones where the SKU is unavailable) columns for each subscription and location. +- Use [`Get-AzComputeResourceSku`](https://learn.microsoft.com/en-us/powershell/module/az.compute/get-azcomputeresourcesku) directly to inspect the `Restrictions` property for specific SKUs, which exposes location and zone restrictions along with the reason code. +- Always check restrictions before filing quota increase requests, because increased quota won't help if the offer type blocks access to the SKU in your target region or zone. + +### Remediate restrictions + +- Submit a [zonal enablement request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) when you need access to specific zones for restricted VM families; the support workflow lets you select regions, logical zones, and VM series. +- File a [region access request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) when entire regions are blocked for your subscription, ensuring quotas and offer flags match your deployment plan before attempting scale-outs or quota transfers. +- Plan ahead for these requests—there's no guaranteed SLA for approval times, so factor enablement lead time into your capacity planning and deployment schedules. + +### Special case: Spot VM capacity + +- [Azure Spot VMs](https://learn.microsoft.com/en-us/azure/virtual-machines/spot-vms) use a [separate capacity pool](https://learn.microsoft.com/en-us/azure/virtual-machines/spot-vms#frequently-asked-questions) from on-demand VMs, so a SKU can be available for regular deployments but have zero Spot capacity. +- The `Get-AzComputeResourceSku` cmdlet doesn't show Spot-specific availability. Use the [Spot Placement Score API](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/spot-placement-score) to evaluate deployment likelihood before committing to Spot-based architectures. +- Spot VMs have [no availability guarantees or SLA](https://learn.microsoft.com/en-us/azure/architecture/guide/spot/spot-eviction)—Azure can evict them whenever on-demand capacity is needed, providing [30 seconds total for detection and graceful shutdown](https://learn.microsoft.com/en-us/azure/architecture/guide/spot/spot-eviction), not 30 seconds after notification. +- Don't rely on Spot capacity for workloads where you've committed SLAs to your customers; use [on-demand capacity reservations](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) instead for guaranteed availability. + +## Quota analysis scripts + +> [!IMPORTANT] +> This repository includes PowerShell scripts for quota analysis developed through ISV engagements. These tools address quota management scenarios for organizations not yet using Quota Groups. + +### Available scripts + +| Script | Purpose | Use Case | +|--------|---------|----------| +| **Get-AzVMQuotaUsage.ps1** | Multi-threaded quota analysis with zone restrictions | Large-scale enterprise analysis across 100+ subscriptions | +| **Show-AzVMQuotaReport.ps1** | Single-threaded quota reporting | Smaller deployments or learning scenarios | +| **Get-AzAvailabilityZoneMapping.ps1** | Logical-to-physical zone mapping | Critical for multi-subscription architectures | + +### Quick start + +Download and run the multi-threaded quota analyzer: + +```powershell +# Download the script +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/azcapman/main/scripts/quota/Get-AzVMQuotaUsage.ps1" -OutFile "Get-AzVMQuotaUsage.ps1" + +# Analyze specific SKUs and regions +.\Get-AzVMQuotaUsage.ps1 -SKUs @('Standard_D2s_v5', 'Standard_E2s_v5') -Locations @('eastus', 'westus2') -Threads 4 +``` + +### Script capabilities + +**Get-AzVMQuotaUsage.ps1** (Recommended for production): +- Analyzes quota usage across multiple subscriptions in parallel +- Reports zone restrictions for VM SKUs +- Maps logical zones to physical datacenters +- Outputs comprehensive CSV for further analysis +- Supports 4+ concurrent threads for faster processing + +**Get-AzAvailabilityZoneMapping.ps1** (Essential for multi-subscription deployments): +- Shows how logical zones (1,2,3) map to physical zones per subscription +- Critical because Azure randomizes zone mappings per subscription +- Required for ensuring true zone alignment across subscriptions +- Outputs zone peering data for cross-subscription planning + +[View complete script documentation →](https://github.com/microsoft/azcapman/tree/main/scripts/quota) + +## Audit regional quota and zone access + +- Run `scripts/quota/Get-AzVMQuotaUsage.ps1` for comprehensive multi-threaded analysis or `scripts/quota/Show-AzVMQuotaReport.ps1` for simpler single-threaded reporting to enumerate VM family usage versus limits per subscription and region. +- Include `-UsePhysicalZones` when you need cross-subscription mapping, because [Azure maps physical datacenters to logical availability zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) differently per subscription and the `checkZonePeers` API exposes the authoritative mapping. +- Use `scripts/quota/Get-AzAvailabilityZoneMapping.ps1` to generate a complete zone mapping matrix before planning multi-subscription deployments that require zone alignment. +- Scope the scripts with `-SubscriptionIds` and `-Locations` to focus on business-critical subscriptions or surge regions, then export the CSV output and compare against [`az quota usage list`](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) to validate the data against the Microsoft.Quota service. +- When you need to file a quota increase from automation, submit the request with [`az quota create`](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) and record the request ID so you can track approvals and retry deployments deterministically. +- Flag regions where the report shows restricted or missing zones and initiate the [zonal enablement workflow](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) to request access for the required VM series and zones before attempting redeployments. +- If entire regions are unavailable for a subscription, raise a [region access request](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) so that quota transfers or scale-outs do not fail when you move capacity between subscriptions. + +## Regional and zonal access workflows + +- [Region enablement requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) grant access to restricted geographies and ensure quotas and offer flags match planned deployments; submit through Azure support when the portal limits block deployments in new regions. +- [Zonal enablement requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) authorize deployments into specific availability zones for restricted VM families; use the support workflow to select regions, logical zones, and VM series before scaling out. +- Each subscription receives a [unique logical-to-physical zone mapping](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) at creation time, so use `checkZonePeers` when planning multi-subscription resilience to align physical fault domains. + +## Create and govern quota groups + +- Establish [quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) under the management group that owns your shared capacity so you can pool vCPU limits across EA and MCA subscriptions without filing support requests for every transfer. [Quota groups](https://learn.microsoft.com/en-us/azure/quotas/quota-groups) have Azure-enforced constraints: they support only EA and MCA Enterprise subscriptions, apply exclusively to compute quota (VM cores), and require registration of the `Microsoft.Quota` resource provider. +- [Create the group](https://learn.microsoft.com/en-us/azure/quotas/create-quota-groups?tabs=rest-1%2Crest-2) via the Microsoft.Quota REST API or Azure portal once the GroupQuota Request Operator role is assigned at the management group scope. +- [Add newly provisioned or recycled subscriptions](https://learn.microsoft.com/en-us/azure/quotas/add-remove-subscriptions-quota-group?tabs=rest-1%2Crest-2) to the quota group so their existing limits are tracked centrally while retaining subscription-level enforcement at deployment time. + +## Reallocate and increase capacity + +- Increase compute capacity with [capacity reservation groups](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview) when you need guaranteed VM availability, reserving specific VM sizes in targeted regions or zones with pay-as-you-go billing. +- [Share capacity reservation groups](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-group-share) with up to 100 consumer subscriptions (preview) so platform teams own the reservation and consumer subscriptions consume capacity; unused reservations bill to the owner while VM usage accrues to consumers. +- Use [quota allocations to transfer unused cores](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups?tabs=rest-1) from retiring subscriptions back into the group and push them to surge subscriptions that need immediate capacity. +- Submit [group-level limit increase requests](https://learn.microsoft.com/en-us/azure/quotas/quota-group-limit-increase?tabs=rest-1) when aggregate demand outpaces the pooled limit; approved increases stamp capacity on the quota group so you can redistribute it without additional tickets. +- Continue to request [VM-family quota increases](https://learn.microsoft.com/en-us/azure/quotas/per-vm-quota-requests) for edge cases directly from **Quotas** when you need adjustments outside of the pooled families or regions. + +## Reclaim and recycle subscriptions + +- Recycle subscriptions with zonal or regional access rather than deleting them. There's no documented SLA for [zonal](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) or [regional enablement requests](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process)—the Azure engineering team manually reviews each request. If you offer SLAs to your customers, you can't guarantee infrastructure availability without pre-approved access flags in place. +- Before decommissioning a workload, [return its quota to the group](https://learn.microsoft.com/en-us/azure/quotas/transfer-quota-groups?tabs=rest-1) and keep the subscription for future use so that existing [zonal](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series) and [regional access flags](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) remain in place and you avoid repeating the support-ticket workflow. +- When onboarding a new subscription, check the quota report and [`az quota list`](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) output to confirm the baseline allocations before moving workloads or transferring additional quota. + +## Operational tips + +- Schedule the [quota report](https://learn.microsoft.com/en-us/azure/virtual-machines/quotas) and [CLI usage checks](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) to run after major deployments so the platform team has an auditable history of usage, available capacity, and zone coverage. +- If subscriptions span multiple tenants, pair the quota audits with the [`checkZonePeers` API](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) to ensure logical zone identifiers align before you redistribute workload placements. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/support-and-reference/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/support-and-reference/README.md new file mode 100644 index 000000000..feb46a7d6 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/support-and-reference/README.md @@ -0,0 +1,10 @@ +--- +title: Support & reference +parent: Operational topics +nav_order: 3 +has_children: true +--- + +# Support & reference + +[Azure region access](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process), [zonal enablement](https://learn.microsoft.com/en-us/troubleshoot/azure/general/zonal-enablement-request-for-restricted-vm-series), and [quota support processes](https://learn.microsoft.com/en-us/azure/quotas/quickstart-increase-quota-portal), together with glossary material, underpin the support and reference content in this section. Use this section to find escalation guidance, tenant hygiene practices, and glossary entries so you don't piece together your own reference guide. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/support-and-reference/citation-matrix.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/support-and-reference/citation-matrix.md new file mode 100644 index 000000000..23d188f96 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/support-and-reference/citation-matrix.md @@ -0,0 +1,144 @@ +--- +title: Citation traceability matrix +parent: Support & Reference +nav_order: 4 +--- + +# Citation traceability matrix + +This matrix lists external links used across the documentation, along with the files that reference them. + +**Total unique URLs:** 132 + +| URL | Label | Used in | +| --- | --- | --- | +| | Azure Pricing Calculator | docs/billing/modern/README.md | +| | Azure pricing by region | docs/operations/capacity-reservations/README.md | +| | FOCUS schema | docs/operations/glossary.md | +| | View full script source → | docs/operations/tools-scripts/deploy-anomalyalert.md | +| | View full script source → | docs/operations/tools-scripts/deploy-bulkalz.md | +| | anomaly-alert.bicep | docs/operations/tools-scripts/deploy-anomalyalert.md, docs/operations/tools-scripts/deploy-bulkalz.md | +| | View full script source → | docs/operations/tools-scripts/get-azavailabilityzonemapping.md | +| | View full script source → | docs/operations/tools-scripts/get-azvmquotausage.md | +| | View full script source → | docs/operations/tools-scripts/show-azvmquotareport.md | +| | View full script source → | docs/operations/tools-scripts/get-benefitrecommendations.md | +| | GitHub repository | docs/operations/tools-scripts/README.md | +| | Suppress-AdvisorRecommendations.ps1 | docs/operations/tools-scripts/README.md | +| | `Get-AzVMQuotaUsage.ps1` | docs/operations/quota/README.md | +| | FinOps Toolkit query index | docs/billing/modern/README.md, docs/operations/capacity-reservations/README.md | +| | Azure Advisor rightsizing and shutdown recommendations use retail (pay-as-you-go) prices | docs/billing/modern/README.md | +| | Cost Optimization workbook | docs/operations/capacity-planning/README.md | +| | Source | docs/operations/tools-scripts/README.md | +| | Deployment Stamps pattern | docs/deployment/multi-tenant/README.md, docs/deployment/single-tenant/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md | +| | no availability guarantees or SLA | docs/operations/quota/README.md | +| | ticket creation | docs/operations/escalation/README.md | +| | Azure subscription service limits | docs/deployment/multi-tenant/README.md | +| | App Service plans capped per region (10 Free/Shared, 100 per resource group for higher tiers); storage quota enforced per plan and per region/resource group | docs/operations/non-compute-quotas/README.md | +| | SKU restrictions based on your subscription's offer type | docs/operations/quota/README.md | +| | no deployment history is created | docs/operations/quota/README.md | +| | Source | docs/billing/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/modern/README.md | +| | Source | docs/operations/capacity-and-quotas/README.md | +| | Subscription vending | docs/operations/README.md, docs/operations/capacity-and-quotas/README.md | +| | Microsoft's landing zone guidance | docs/deployment/README.md, docs/deployment/multi-tenant/README.md, docs/deployment/single-tenant/README.md, docs/operations/README.md, docs/operations/capacity-and-quotas/README.md | +| | account/container limits or throughput ceilings | docs/operations/escalation/README.md, docs/operations/non-compute-quotas/README.md | +| | Cost anomaly detection | docs/operations/tools-scripts/deploy-anomalyalert.md, docs/operations/tools-scripts/deploy-bulkalz.md | +| | budget alerts | docs/operations/capacity-and-quotas/README.md, docs/operations/glossary.md, docs/operations/monitoring-alerting/README.md, docs/operations/non-compute-quotas/README.md | +| | Tag inheritance policies | docs/deployment/single-tenant/README.md | +| | reservation utilization alerts | docs/billing/modern/README.md | +| | Cost Management scheduled actions | docs/operations/tools-scripts/deploy-anomalyalert.md | +| | Scheduled exports | docs/deployment/single-tenant/README.md | +| | **SubscriptionCreator** role | docs/billing/legacy/README.md | +| | create EA subscriptions | docs/billing/legacy/README.md | +| | subscription-request workflow | docs/billing/modern/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/glossary.md, docs/operations/tenant-hygiene/README.md | +| | active account | docs/billing/legacy/README.md | +| | Enrollment policies | docs/billing/legacy/README.md | +| | Enterprise Agreement subscriptions default to 350 cores | docs/operations/quota/README.md | +| | Reservation prices can be negotiated | docs/billing/modern/README.md | +| | negotiated price sheet | docs/billing/modern/README.md | +| | customer price sheet | docs/billing/modern/README.md | +| | invoice section | docs/billing/modern/README.md | +| | register an application in that tenant, grant it the required billing role, and then call the alias API directly from the destination tenant's service principal | docs/operations/modern/README.md | +| | programmatic subscription creation | docs/operations/README.md, docs/operations/capacity-and-quotas/README.md | +| | Source | docs/billing/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/legacy/README.md | +| | dual-application pattern: register apps in both source (billing) and destination tenants, assign billing roles in the source, and have the destination app accept the subscription during alias creation | docs/operations/modern/README.md | +| | create subscriptions | docs/billing/modern/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/modern/README.md | +| | track your MACC progress | docs/billing/modern/README.md | +| | Enterprise Agreement (EA) | docs/billing/legacy/README.md | +| | Microsoft Customer Agreement (MCA) | docs/billing/modern/README.md | +| | up to 10,000 subscriptions | docs/billing/modern/README.md | +| | associate additional tenants and link subscriptions across directories | docs/billing/modern/README.md, docs/operations/tenant-hygiene/README.md | +| | Reservations allow unlimited exchanges and $50,000/year in refunds | docs/billing/modern/README.md | +| | some reservations can't be exchanged or refunded | docs/billing/modern/README.md | +| | Azure Reservations | docs/billing/modern/README.md | +| | Cost Management + Billing reservation recommendations | docs/billing/modern/README.md, docs/operations/tools-scripts/get-benefitrecommendations.md | +| | Reserved Instance discounts | docs/operations/capacity-reservations/README.md | +| | Azure savings plan for compute | docs/operations/glossary.md | +| | Savings plans can't be canceled or exchanged | docs/billing/modern/README.md | +| | automatically applies whichever discount is better | docs/billing/modern/README.md | +| | Savings plan purchase recommendations | docs/operations/tools-scripts/get-benefitrecommendations.md | +| | Savings Plans | docs/billing/modern/README.md | +| | MCA billing hierarchy | docs/billing/modern/README.md | +| | Microsoft Customer Agreement billing hierarchy diagram | docs/billing/modern/README.md | +| | wire transfer or ACH | docs/billing/modern/README.md | +| | Management groups | docs/operations/tools-scripts/deploy-bulkalz.md | +| | Azure Resource Graph overview | docs/operations/tools-scripts/deploy-bulkalz.md | +| | limitations | docs/operations/quota-groups/README.md | +| | remove subscriptions from a quota group | docs/operations/quota-groups/README.md | +| | Add newly provisioned or recycled subscriptions | docs/operations/quota/README.md | +| | Creating or deleting a quota group | docs/operations/quota-groups/README.md | +| | Create the group | docs/operations/quota/README.md | +| | Source | docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/glossary.md, docs/operations/monitoring-alerting/README.md, docs/operations/non-compute-quotas/README.md, docs/operations/quota-groups/README.md | +| | Diagram of Management Group hierarchy with sample Quota Groups created under Management Group. | docs/operations/quota-groups/README.md | +| | quota monitoring and alerting article | docs/operations/capacity-governance/README.md, docs/operations/monitoring-alerting/README.md, docs/operations/non-compute-quotas/README.md, docs/operations/quota-groups/README.md | +| | Quota API | docs/deployment/single-tenant/README.md, docs/operations/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/quota/README.md | +| | non-adjustable | docs/operations/escalation/README.md, docs/operations/non-compute-quotas/README.md, docs/operations/support-and-reference/README.md | +| | submit a quota group limit increase request | docs/operations/quota-groups/README.md | +| | group-level limit increase requests | docs/operations/quota/README.md | +| | Source | docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/glossary.md, docs/operations/quota-groups/README.md, docs/operations/quota/README.md | +| | Quota groups are available | docs/operations/quota-groups/README.md | +| | roles for operating quota groups | docs/operations/quota-groups/README.md | +| | `Microsoft.Quota` and `Microsoft.Compute` resource providers must be registered | docs/operations/quota-groups/README.md | +| | quota groups are independent | docs/operations/capacity-governance/README.md, docs/operations/quota-groups/README.md | +| | regional quota increases | docs/operations/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/quota/README.md | +| | 250 standard storage accounts per subscription and region (increaseable to 500) | docs/operations/non-compute-quotas/README.md | +| | quota allocation snapshot | docs/operations/capacity-governance/README.md, docs/operations/quota-groups/README.md | +| | transfer | docs/operations/capacity-governance/README.md, docs/operations/quota-groups/README.md | +| | quota allocations to transfer unused cores | docs/operations/quota/README.md | +| | **Quotas** | docs/operations/quota/README.md | +| | physical datacenter zones | docs/operations/glossary.md, docs/operations/quota/README.md, docs/operations/tenant-hygiene/README.md | +| | per-account throughput and egress limits vary by SKU | docs/operations/non-compute-quotas/README.md | +| | Spot Placement Score API | docs/operations/quota/README.md | +| | validate zone alignment between producer and consumer subscriptions | docs/operations/capacity-reservations/README.md, docs/operations/quota/README.md | +| | sharing | docs/operations/capacity-reservations/README.md | +| | Source | docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/capacity-reservations/README.md | +| | capacity reservations | docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/capacity-reservations/README.md, docs/operations/escalation/README.md, docs/operations/glossary.md, docs/operations/quota/README.md | +| | reserved capacity can be combined automatically with reserved instances | docs/operations/capacity-governance/README.md | +| | adjusted by changing the capacity property | docs/operations/capacity-governance/README.md | +| | Diagram that shows capacity reservation with the third VM allocated. | docs/operations/capacity-reservations/README.md | +| | quotas per subscription and region | docs/operations/quota/README.md | +| | Azure Spot VMs | docs/operations/quota/README.md | +| | separate capacity pool | docs/operations/quota/README.md | +| | scale-unit architecture | docs/operations/capacity-governance/README.md | +| | workload supply chain guidance | docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/glossary.md | +| | Source | docs/operations/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/capacity-planning/README.md, docs/operations/glossary.md | +| | Collect telemetry for CPU, memory, storage, network throughput, and request latency | docs/operations/capacity-planning/README.md | +| | estimate resource demand by modeling expected user journeys, transaction volumes, and dependency behavior | docs/operations/capacity-planning/README.md | +| | Identify peak utilization windows, transaction rates, and concurrency | docs/operations/capacity-planning/README.md | +| | reliable scaling | docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-planning/README.md | +| | monitoring | docs/operations/capacity-governance/README.md | +| | noisy-neighbor risk | docs/deployment/multi-tenant/README.md, docs/deployment/single-tenant/README.md | +| | transactional data stores | docs/deployment/multi-tenant/README.md, docs/deployment/single-tenant/README.md | +| | tenant experience | docs/deployment/multi-tenant/README.md | +| | quota audits | docs/operations/capacity-reservations/README.md, docs/operations/monitoring-alerting/README.md, docs/operations/non-compute-quotas/README.md, docs/operations/quota/README.md | +| | FinOps Framework | docs/operations/capacity-and-quotas/README.md, docs/operations/glossary.md | +| | rate optimization capability | docs/billing/modern/README.md | +| | FinOps rate optimization | docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/glossary.md, docs/operations/monitoring-alerting/README.md | +| | FinOps Framework planning and estimating capability | docs/operations/capacity-planning/README.md | +| | FinOps Hubs | docs/billing/modern/README.md, docs/deployment/single-tenant/README.md, docs/operations/capacity-planning/README.md, docs/operations/capacity-reservations/README.md, docs/operations/glossary.md | +| | Microsoft Azure Consumption Commitment (MACC) | docs/billing/modern/README.md | +| | `Get-AzComputeResourceSku` | docs/operations/quota/README.md | +| | Azure Cost Management API | docs/operations/tools-scripts/get-benefitrecommendations.md | +| | Microsoft style guide | docs/operations/support-and-reference/citation-matrix.md | +| | region access | docs/deployment/single-tenant/README.md, docs/operations/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/escalation/README.md, docs/operations/glossary.md, docs/operations/quota-groups/README.md, docs/operations/quota/README.md, docs/operations/support-and-reference/README.md, docs/operations/tenant-hygiene/README.md | +| | zonal enablement requests | docs/deployment/single-tenant/README.md, docs/operations/README.md, docs/operations/capacity-and-quotas/README.md, docs/operations/capacity-governance/README.md, docs/operations/quota/README.md, docs/operations/support-and-reference/README.md | +| | `OverconstrainedAllocationRequest` | docs/operations/quota/README.md | diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tenant-hygiene/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tenant-hygiene/README.md new file mode 100644 index 000000000..4807b24ed --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tenant-hygiene/README.md @@ -0,0 +1,31 @@ +--- +title: Tenant & subscription hygiene +parent: Support & reference +nav_order: 2 +--- + +# Tenant & subscription hygiene + +ISVs commonly manage subscriptions across multiple tenants while centralizing billing and governance. This guide documents best practices for maintaining clean tenant relationships, recycling subscriptions, and preserving zone mappings so you aren't surprised by access gaps. + +## Align tenant and billing structures + +- Each [Microsoft Customer Agreement (MCA) billing account](https://learn.microsoft.com/en-us/azure/cost-management-billing/microsoft-customer-agreement/manage-tenants) links to a primary Microsoft Entra tenant but can associate additional tenants for billing operations. +- [Billing owners](https://learn.microsoft.com/en-us/azure/cost-management-billing/microsoft-customer-agreement/manage-tenants) can create, transfer, or link subscriptions across associated tenants without changing the resource tenant. Track which tenants are authorized for each billing profile and invoice section to avoid orphaned access. +- Invite guest users or [associate tenant relationships](https://learn.microsoft.com/en-us/azure/cost-management-billing/microsoft-customer-agreement/manage-tenants) before assigning billing roles to external teams, and ensure invitations are accepted to activate access. + +## Subscription lifecycle hygiene + +- **Onboarding:** Use the [subscription request workflow](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-subscription-request) to provision subscriptions for other tenants while maintaining billing control. Capture required roles (Owner, Contributor, Azure subscription creator) and management group placement as part of the intake checklist. +- **Recycling vs. deletion:** When workloads retire, reclaim quota and billing ownership but keep the subscription if zone enablement or [region access](https://learn.microsoft.com/en-us/troubleshoot/azure/general/region-access-request-process) was previously granted. Deleting the subscription can force new access requests, delaying future projects. + +## Preserve zone consistency + +- [Logical-to-physical zone mappings](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) differ per subscription and are assigned at creation. Export mappings through the `List Locations` API or the `checkZonePeers` API to document how subscriptions align across tenants. +- When planning cross-tenant high availability, compare zone mappings early to avoid placing redundant components in the same physical zone. + +## Automation opportunities + +- Script [subscription request creation and acceptance](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/create-subscription-request) for cross-tenant provisioning to reduce manual errors. +- Automate [zone mapping exports](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) and store results in source control for auditability. + diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/toc.yml b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/toc.yml new file mode 100644 index 000000000..f500c0b52 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/toc.yml @@ -0,0 +1,91 @@ +- name: Capacity management + href: README.md + items: + - name: Overview + href: README.md + - name: Planning and forecasting + href: capacity-planning/README.md + items: + - name: Capacity and billing operations + href: capacity-and-quotas/README.md + - name: Capacity planning + href: capacity-planning/README.md + - name: Customer isolation + href: ../deployment/README.md + items: + - name: Overview + href: ../deployment/README.md + - name: Single-tenant + href: ../deployment/single-tenant/README.md + - name: Multi-tenant + href: ../deployment/multi-tenant/README.md + - name: Workload placement + href: quota/README.md + items: + - name: Billing models + href: ../billing/README.md + items: + - name: Overview + href: ../billing/README.md + - name: Microsoft Customer Agreement + href: ../billing/modern/README.md + items: + - name: Overview + href: ../billing/modern/README.md + - name: Subscription creation (billing scope) + href: modern/README.md + - name: Enterprise Agreement + href: ../billing/legacy/README.md + items: + - name: Overview + href: ../billing/legacy/README.md + - name: Subscription creation (billing scope) + href: legacy/README.md + - name: Quota operations + href: quota/README.md + - name: Capacity reservations + href: capacity-reservations/README.md + - name: AKS capacity governance + href: aks-capacity/README.md + - name: Non-compute quotas + href: non-compute-quotas/README.md + - name: Usage and rate optimization + href: quota-groups/README.md + items: + - name: Quota groups + href: quota-groups/README.md + - name: Governance and automation + href: monitoring-alerting/README.md + items: + - name: Monitoring and alerting + href: monitoring-alerting/README.md + - name: Capacity governance + href: capacity-governance/README.md + - name: Tools and scripts + href: tools-scripts/README.md + items: + - name: Overview + href: tools-scripts/README.md + - name: Get-AzVMQuotaUsage.ps1 + href: tools-scripts/get-azvmquotausage.md + - name: Get-AzAvailabilityZoneMapping.ps1 + href: tools-scripts/get-azavailabilityzonemapping.md + - name: Show-AzVMQuotaReport.ps1 + href: tools-scripts/show-azvmquotareport.md + - name: Get-BenefitRecommendations.ps1 + href: tools-scripts/get-benefitrecommendations.md + - name: Deploy-AnomalyAlert.ps1 + href: tools-scripts/deploy-anomalyalert.md + - name: Deploy-BulkALZ.ps1 + href: tools-scripts/deploy-bulkalz.md + - name: Support and reference + href: support-and-reference/README.md + items: + - name: Overview + href: support-and-reference/README.md + - name: Support escalation + href: escalation/README.md + - name: Tenant hygiene + href: tenant-hygiene/README.md + - name: Glossary + href: glossary.md diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/README.md new file mode 100644 index 000000000..3c8de0d27 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/README.md @@ -0,0 +1,52 @@ +--- +title: Tools & scripts +parent: Operational topics +nav_order: 15 +--- + +# Tools & scripts + +Scripts that extend Azure's native capabilities for ISV capacity management. + +## Quota management + +| Script | Description | +|--------|-------------| +| [Get-AzVMQuotaUsage.ps1](get-azvmquotausage.md) | Multi-threaded quota analysis across 100+ subscriptions | +| [Get-AzAvailabilityZoneMapping.ps1](get-azavailabilityzonemapping.md) | Logical-to-physical zone mapping for cross-subscription alignment | +| [Show-AzVMQuotaReport.ps1](show-azvmquotareport.md) | Single-threaded quota reporting for smaller deployments | + +## Cost optimization + +| Script | Description | +|--------|-------------| +| [Get-BenefitRecommendations.ps1](get-benefitrecommendations.md) | Extract savings plan recommendations from Cost Management API | +| [Deploy-AnomalyAlert.ps1](deploy-anomalyalert.md) | Deploy cost anomaly alerts to individual subscriptions | +| [Deploy-BulkALZ.ps1](deploy-bulkalz.md) | Bulk deploy anomaly alerts across management groups | + +## Budget management + +| Script | Description | +|--------|-------------| +| [Deploy-Budget.ps1](deploy-budget.md) | Deploy a cost budget to a single subscription with tag-based amount configuration | +| [Deploy-BulkBudgets.ps1](deploy-bulkbudgets.md) | Bulk deploy budgets to all subscriptions in a management group | + +## Storage analysis + +| Tool | Description | +|------|-------------| +| [Serverless SQL storage audit](https://github.com/microsoft/azcapman/tree/main/scripts/serverless-sql-storage) | Azure Monitor workbook that surfaces allocated vs. used storage across Azure SQL serverless databases; identifies `DBCC SHRINKDATABASE` candidates to reclaim billing waste | + +## Advisor recommendations + +| Script | Description | +|--------|-------------| +| [Suppress-AdvisorRecommendations.ps1](suppress-advisorrecommendations.md) | Suppress specified Azure Advisor recommendation types across a management group for up to 90 days; useful when FinOps teams manage spend centrally | + +## Utilities + +| Tool | Description | +|------|-------------| +| [calculator.py](https://github.com/microsoft/azcapman/tree/main/scripts/calculator) | Python calculator using SymPy for safe evaluation of mathematical expressions from string input; designed for LLM tool use in cost and quota calculations | + +**Source**: [GitHub repository](https://github.com/microsoft/azcapman/tree/main/scripts) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-anomalyalert.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-anomalyalert.md new file mode 100644 index 000000000..7b5a20424 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-anomalyalert.md @@ -0,0 +1,245 @@ +--- +title: Deploy-AnomalyAlert.ps1 +parent: Tools & scripts +nav_order: 6 +--- + +# Deploy-AnomalyAlert.ps1 + +Deploy cost anomaly alert configuration to individual Azure subscriptions with interactive features and validation. + +## Overview + +This PowerShell script deploys the anomaly alert Bicep template to create a subscription-level scheduled action for cost anomaly monitoring. It provides interactive subscription selection, parameter validation, and deployment options suitable for individual subscription management or CI/CD integration. + +### Key capabilities + +- **Interactive subscription selection**: Choose target subscription from available list +- **Email validation**: Validates email format before deployment +- **What-if deployment**: Preview changes without actual deployment +- **Color-coded logging**: Timestamped output with visual status indicators +- **Force mode**: Skip confirmations for automation scenarios +- **Quiet mode**: Suppress verbose output for cleaner logs + +### When to use this script + +- Deploying anomaly alerts to individual subscriptions +- Testing alert configurations before bulk deployment +- CI/CD pipeline integration for specific subscriptions +- Interactive deployment with validation +- Small-scale deployments (1-10 subscriptions) + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Force -AllowClobber + +# Authenticate to Azure +Connect-AzAccount + +# Verify permissions (Contributor or Owner required) +Get-AzRoleAssignment | Where-Object { + $_.RoleDefinitionName -eq "Contributor" -or + $_.RoleDefinitionName -eq "Owner" +} + +# List available subscriptions +Get-AzSubscription | Select-Object Name, Id, State +``` + +## Parameters + +| Parameter | Type | Description | Required | +|-----------|------|-------------|----------| +| **SubscriptionId** | String | Target subscription ID (GUID format) | No (interactive) | +| **EmailRecipients** | String[] | Array of recipient email addresses | Yes | +| **NotificationEmail** | String | Primary notification email | Yes | +| **DeploymentName** | String | Custom deployment name | No (auto-generated) | +| **Location** | String | Deployment metadata location | No (West US) | +| **Force** | Switch | Skip confirmation prompts | No | +| **Quiet** | Switch | Suppress deployment progress | No | + +## Usage examples + +### Interactive subscription selection + +```powershell +# Select subscription interactively +.\Deploy-AnomalyAlert.ps1 ` + -EmailRecipients @("platform@company.com", "finops@company.com") ` + -NotificationEmail "alerts@company.com" +``` + +### Specific subscription deployment + +```powershell +# Deploy to specific subscription +.\Deploy-AnomalyAlert.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" +``` + +### What-if preview + +```powershell +# Preview deployment without making changes +.\Deploy-AnomalyAlert.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" ` + -WhatIf +``` + +### Automated deployment + +```powershell +# Skip all prompts for automation +.\Deploy-AnomalyAlert.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" ` + -Force ` + -Quiet +``` + +### Multiple recipients + +```powershell +# Send alerts to multiple email addresses +.\Deploy-AnomalyAlert.ps1 ` + -EmailRecipients @( + "platform-team@company.com", + "finance@company.com", + "cloudops@company.com" + ) ` + -NotificationEmail "azure-alerts@company.com" +``` + +## Deployment process + +1. **Subscription validation**: Verifies subscription exists and is accessible +2. **Email validation**: Checks email format for all recipients +3. **Parameter collection**: Gathers required parameters +4. **Confirmation prompt**: Shows deployment summary (unless -Force) +5. **Template deployment**: Deploys anomaly-alert.bicep template +6. **Status reporting**: Shows success/failure with deployment ID + +## What gets deployed + +The script deploys: +- **Cost Management scheduled action** named "AnomalyAlert" +- **Anomaly detection** at subscription level +- **Email notifications** to specified recipients + +This uses the Azure public Bicep registry module: +``` +br/public:cost/subscription-scheduled-action:1.0.2 +``` + +## Error handling + +The script includes comprehensive error handling: +- Validates subscription ID format (GUID) +- Validates email address formats +- Checks Azure authentication status +- Verifies subscription access permissions +- Handles deployment failures gracefully + +## Troubleshooting + +### Authentication issues + +```powershell +# Clear and re-authenticate +Clear-AzContext -Force +Connect-AzAccount + +# Verify current context +Get-AzContext | Select-Object Account, Subscription, Tenant +``` + +### Permission errors + +```powershell +# Check role assignments +Get-AzRoleAssignment -Scope "/subscriptions/YOUR-SUB-ID" | + Select-Object DisplayName, RoleDefinitionName + +# Minimum required: Contributor or Owner role +``` + +### Subscription not found + +```powershell +# List all accessible subscriptions +Get-AzSubscription | Format-Table Name, Id, State + +# Ensure subscription is enabled +Get-AzSubscription -SubscriptionId "YOUR-SUB-ID" | + Select-Object State +``` + +### Deployment failures + +```powershell +# Check deployment history +Get-AzSubscriptionDeployment -Name "YOUR-DEPLOYMENT-NAME" | + Select-Object ProvisioningState, Timestamp + +# Get detailed error +$deployment = Get-AzSubscriptionDeploymentOperation ` + -DeploymentName "YOUR-DEPLOYMENT-NAME" +$deployment.Properties.StatusMessage +``` + +## CI/CD integration + +### Azure DevOps pipeline + +```yaml +- task: AzurePowerShell@5 + displayName: 'Deploy Anomaly Alert' + inputs: + azureSubscription: 'ServiceConnection' + ScriptType: 'FilePath' + ScriptPath: '$(System.DefaultWorkingDirectory)/Deploy-AnomalyAlert.ps1' + ScriptArguments: > + -SubscriptionId $(SubscriptionId) + -EmailRecipients @("$(EmailRecipients)") + -NotificationEmail "$(NotificationEmail)" + -Force + -Quiet + azurePowerShellVersion: 'LatestVersion' +``` + +### GitHub Actions + +```yaml +- name: Deploy Anomaly Alert + uses: azure/powershell@v1 + with: + inlineScript: | + .\Deploy-AnomalyAlert.ps1 ` + -SubscriptionId "${{ secrets.AZURE_SUBSCRIPTION_ID }}" ` + -EmailRecipients @("${{ vars.ALERT_RECIPIENTS }}") ` + -NotificationEmail "${{ vars.NOTIFICATION_EMAIL }}" ` + -Force ` + -Quiet + azPSVersion: "latest" +``` + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/anomaly-alerts/Deploy-AnomalyAlert.ps1) + +## Related scripts + +- [Deploy-BulkALZ.ps1](deploy-bulkalz.md) - Enterprise bulk deployment +- [anomaly-alert.bicep](https://github.com/microsoft/azcapman/blob/main/scripts/anomaly-alerts/anomaly-alert.bicep) - Bicep template + +## Related documentation + +- [Cost anomaly detection](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/anomaly-detection) +- [Cost Management scheduled actions](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/save-share-views) \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-budget.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-budget.md new file mode 100644 index 000000000..8ff3ab06e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-budget.md @@ -0,0 +1,178 @@ +--- +title: Deploy-Budget.ps1 +parent: Tools & scripts +nav_order: 8 +--- + +# Deploy-Budget.ps1 + +Deploy a cost budget to a single Azure subscription with tag-based amount configuration and configurable notification thresholds. + +## Overview + +This PowerShell script deploys a `Microsoft.Consumption/budgets` resource to an Azure subscription. Budget amounts can be set via explicit parameter or read from a `BudgetAmount` subscription tag, letting different subscriptions carry different spending limits without modifying the script. + +### Key capabilities + +- **Tag-based amounts**: Reads `BudgetAmount` tag from the subscription to set per-subscription limits +- **Interactive selection**: Prompts for subscription when no ID is provided +- **Threshold configuration**: Configures actual and forecasted notification thresholds +- **What-if mode**: Preview deployment without making changes +- **Force mode**: Skip confirmation for automation scenarios + +### When to use this script + +- Deploying a budget to a single subscription +- Testing budget configuration before bulk deployment +- Onboarding a new subscription to cost governance +- CI/CD pipeline integration for subscription-level budgets + +### Budget amount priority + +The script resolves the budget amount in this order: + +1. **`-Amount` parameter** — explicit value takes precedence +2. **`BudgetAmount` subscription tag** — tag-based per-subscription configuration +3. **Default** — falls back to $10/month + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Repository PSGallery -Force + +# Authenticate to Azure +Connect-AzAccount + +# Verify subscription access +Get-AzSubscription | Select-Object Name, Id, State +``` + +## Parameters + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| **SubscriptionId** | String | Target subscription GUID | Interactive selection | +| **ContactEmails** | String[] | Email addresses for notifications | Required | +| **Amount** | Decimal | Budget amount (overrides tag) | Tag or $10 | +| **BudgetName** | String | Name of the budget resource | `SubscriptionBudget` | +| **TimeGrain** | String | Reset period: Monthly, Quarterly, or Annually | Monthly | +| **StartDate** | String | Budget start date (YYYY-MM-DD) | 1st of next month | +| **EndDate** | String | Budget end date (YYYY-MM-DD) | 1 year from start | +| **ContactRoles** | String[] | Azure roles to notify | Owner, Contributor | +| **FirstThreshold** | Int | First notification threshold % | 50 | +| **SecondThreshold** | Int | Second notification threshold % | 75 | +| **ThirdThreshold** | Int | Third notification threshold % | 90 | +| **ForecastedThreshold** | Int | Forecasted cost threshold % | 100 | +| **Force** | Switch | Skip confirmation prompt | False | +| **Quiet** | Switch | Suppress verbose output | False | +| **WhatIf** | Switch | Preview without deploying | False | + +## Usage examples + +### Interactive subscription selection + +```powershell +.\Deploy-Budget.ps1 -ContactEmails "admin@company.com" +``` + +### Specific subscription + +```powershell +.\Deploy-Budget.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -ContactEmails "admin@company.com" +``` + +### Override budget amount + +```powershell +.\Deploy-Budget.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -ContactEmails "finops@company.com" ` + -Amount 5000 +``` + +### What-if preview + +```powershell +.\Deploy-Budget.ps1 ` + -ContactEmails "admin@company.com" ` + -WhatIf +``` + +### Automated deployment (no prompts) + +```powershell +.\Deploy-Budget.ps1 ` + -SubscriptionId "12345678-1234-1234-1234-123456789012" ` + -ContactEmails "admin@company.com" ` + -Force -Quiet +``` + +## Setting the BudgetAmount tag + +Set the `BudgetAmount` tag on a subscription to control its per-subscription limit without changing script parameters: + +```powershell +# PowerShell +Update-AzTag -ResourceId "/subscriptions/" ` + -Tag @{BudgetAmount = "500"} ` + -Operation Merge +``` + +```bash +# Azure CLI +az tag update \ + --resource-id "/subscriptions/" \ + --operation merge \ + --tags BudgetAmount=500 +``` + +## Notification thresholds + +By default, notifications are sent at these percentages of the configured budget amount: + +| Threshold | Type | Default | +|-----------|------|---------| +| First | Actual | 50% | +| Second | Actual | 75% | +| Third | Actual | 90% | +| Forecasted | Forecasted | 100% | + +All four thresholds are configurable via parameters. Actual thresholds trigger when actual spend crosses the percentage; the forecasted threshold triggers when projected spend is expected to cross it. + +## Troubleshooting + +### Subscription not found + +```powershell +# List accessible subscriptions +Get-AzSubscription | Where-Object State -eq 'Enabled' | Select-Object Name, Id +``` + +### Budget already exists + +The script uses `New-AzConsumptionBudget` with upsert behavior. Re-running against the same subscription updates the existing budget rather than creating a duplicate. + +### Authentication errors + +```powershell +# Re-authenticate +Clear-AzContext -Force +Connect-AzAccount -Tenant 'your-tenant-id' +``` + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/budgets/Deploy-Budget.ps1) + +## Related scripts + +- [Deploy-BulkBudgets.ps1](deploy-bulkbudgets.md) - Bulk budget deployment across a management group +- [Deploy-AnomalyAlert.ps1](deploy-anomalyalert.md) - Deploy anomaly alerts to individual subscriptions + +## Related documentation + +- [Azure budgets overview](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-acm-create-budgets) +- [Microsoft.Consumption/budgets API](https://learn.microsoft.com/en-us/azure/templates/microsoft.consumption/budgets) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-bulkalz.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-bulkalz.md new file mode 100644 index 000000000..8b02377e9 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-bulkalz.md @@ -0,0 +1,265 @@ +--- +title: Deploy-BulkALZ.ps1 +parent: Tools & scripts +nav_order: 7 +--- + +# Deploy-BulkALZ.ps1 + +Enterprise-scale deployment of anomaly alerts to all subscriptions in a management group with intelligent pagination support. + +## Overview + +This PowerShell script deploys cost anomaly alerts across entire Azure Landing Zone management groups, handling hundreds or thousands of subscriptions efficiently. It uses Azure Resource Graph for subscription discovery and implements intelligent pagination to work within Azure's query limits. + +### Key capabilities + +- **Management group targeting**: Deploy to entire hierarchy at once +- **Azure Resource Graph integration**: Efficient subscription discovery +- **Intelligent pagination**: Automatically handles Azure's 1,000 result limit +- **Progressive discovery**: Real-time progress reporting +- **Performance optimization**: Quiet mode for faster bulk operations +- **Safety confirmations**: Prevents accidental enterprise-wide changes + +### When to use this script + +- Enterprise-scale anomaly alert deployment +- Management group-wide rollouts +- Automated governance implementation +- Large estates with 100+ subscriptions +- Azure Landing Zone (ALZ) implementations + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Force -AllowClobber + +# Install Azure Resource Graph module (required) +Install-Module -Name Az.ResourceGraph -Force -AllowClobber + +# Authenticate to Azure +Connect-AzAccount -Tenant "YOUR-TENANT-ID" + +# Verify management group access +Get-AzManagementGroup | Select-Object Name, DisplayName + +# Verify Resource Graph access +Search-AzGraph -Query "resourcecontainers | where type == 'microsoft.resources/subscriptions' | limit 1" +``` + +## Parameters + +| Parameter | Type | Description | Required | +|-----------|------|-------------|----------| +| **TenantId** | String | Azure tenant ID (GUID) | Yes | +| **ManagementGroup** | String | Target management group name | Yes | +| **EmailRecipients** | String[] | Alert recipient email addresses | Yes | +| **NotificationEmail** | String | Primary notification email | Yes | +| **WhatIf** | Switch | Preview mode without deployment | No | +| **Quiet** | Switch | Suppress Azure warnings | No | + +## Usage examples + +### Basic deployment to management group + +```powershell +.\Deploy-BulkALZ.ps1 ` + -TenantId "12345678-1234-1234-1234-123456789012" ` + -ManagementGroup "ALZ" ` + -EmailRecipients @("platform@company.com", "finops@company.com") ` + -NotificationEmail "alerts@company.com" +``` + +### Preview deployment (what-if) + +```powershell +# See what would be deployed without making changes +.\Deploy-BulkALZ.ps1 ` + -TenantId "12345678-1234-1234-1234-123456789012" ` + -ManagementGroup "Production" ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" ` + -WhatIf +``` + +### Quiet mode for automation + +```powershell +# Suppress warnings for cleaner output +.\Deploy-BulkALZ.ps1 ` + -TenantId "12345678-1234-1234-1234-123456789012" ` + -ManagementGroup "ALZ" ` + -EmailRecipients @("admin@company.com") ` + -NotificationEmail "alerts@company.com" ` + -Quiet +``` + +### Deploy to specific environment + +```powershell +# Target specific management group like Dev, Test, or Prod +.\Deploy-BulkALZ.ps1 ` + -TenantId "12345678-1234-1234-1234-123456789012" ` + -ManagementGroup "Development" ` + -EmailRecipients @("dev-team@company.com") ` + -NotificationEmail "dev-alerts@company.com" +``` + +## Pagination system + +The script implements intelligent pagination to handle Azure Resource Graph's 1,000 result limit: + +1. **Automatic page detection**: Determines if pagination is needed +2. **Progressive queries**: Fetches subscriptions in 1,000-item batches +3. **Skip token handling**: Uses Azure's continuation tokens +4. **Real-time progress**: Reports each page as it's processed + +### How pagination works + +```powershell +# Internal process (handled automatically): +# Page 1: Subscriptions 1-1000 +# Page 2: Subscriptions 1001-2000 +# Page 3: Subscriptions 2001-3000 +# Continues until all subscriptions are discovered +``` + +## Deployment process + +1. **Tenant connection**: Authenticates to specified tenant +2. **Subscription discovery**: Queries Resource Graph for all subscriptions in management group +3. **Pagination handling**: Processes results in batches if needed +4. **Confirmation prompt**: Shows total count and asks for confirmation +5. **Sequential deployment**: Deploys to each subscription with progress tracking +6. **Summary report**: Shows success/failure counts + +## Performance considerations + +- **Query optimization**: Uses Resource Graph for fast discovery +- **Batch size**: Fixed at 1,000 subscriptions per query (Azure limit) +- **Sequential deployment**: Deploys one subscription at a time to avoid throttling +- **Warning suppression**: Use `-Quiet` to reduce output overhead + +### Estimated deployment times + +| Subscriptions | Estimated Time | +|---------------|----------------| +| 10 | ~1 minute | +| 100 | ~10 minutes | +| 1,000 | ~100 minutes | +| 5,000 | ~8 hours | + +## Error handling + +The script includes explicit error handling: +- Validates tenant ID format +- Verifies management group exists +- Handles pagination failures +- Reports deployment failures per subscription +- Continues deployment despite individual failures + +## Troubleshooting + +### Management group not found + +```powershell +# List available management groups +Get-AzManagementGroup | Format-Table Name, DisplayName + +# Verify exact name (case-sensitive) +Get-AzManagementGroup -GroupName "YOUR-MG-NAME" +``` + +### Resource Graph errors + +```powershell +# Test Resource Graph access +Search-AzGraph -Query "resourcecontainers | where type == 'microsoft.resources/subscriptions' | limit 1" + +# Check module installation +Get-Module -ListAvailable Az.ResourceGraph +``` + +### Authentication issues + +```powershell +# Clear all contexts +Clear-AzContext -Force + +# Connect to specific tenant +Connect-AzAccount -Tenant "YOUR-TENANT-ID" + +# Verify tenant context +Get-AzContext | Select-Object Tenant +``` + +### No subscriptions found + +```powershell +# Test query directly +$query = "resourcecontainers | where type == 'microsoft.resources/subscriptions'" +$mgScope = "/providers/Microsoft.Management/managementGroups/YOUR-MG" +Search-AzGraph -Query $query -ManagementGroup "YOUR-MG" +``` + +## Security considerations + +- **Tenant isolation**: Explicitly connects to specified tenant +- **Confirmation prompts**: Requires confirmation before bulk changes +- **What-if mode**: Always test with `-WhatIf` first +- **Audit trail**: Each deployment creates audit log entries + +## Integration patterns + +### Scheduled deployment + +```powershell +# PowerShell scheduled task +$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 2am +$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument @" + -File C:\Scripts\Deploy-BulkALZ.ps1 ` + -TenantId 'TENANT-ID' ` + -ManagementGroup 'ALZ' ` + -EmailRecipients @('admin@company.com') ` + -NotificationEmail 'alerts@company.com' ` + -Quiet +"@ +Register-ScheduledTask -TaskName "WeeklyAnomalyAlerts" -Trigger $trigger -Action $action +``` + +### Azure Automation script + +```powershell +# Runbook script +param( + [Parameter(Mandatory=$true)] + [string]$ManagementGroup +) + +# Use managed identity for authentication +Connect-AzAccount -Identity + +# Deploy alerts +.\Deploy-BulkALZ.ps1 ` + -TenantId $env:AZURE_TENANT_ID ` + -ManagementGroup $ManagementGroup ` + -EmailRecipients @($env:ALERT_RECIPIENTS -split ',') ` + -NotificationEmail $env:NOTIFICATION_EMAIL ` + -Quiet +``` + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/anomaly-alerts/Deploy-BulkALZ.ps1) + +## Related scripts + +- [Deploy-AnomalyAlert.ps1](deploy-anomalyalert.md) - Individual subscription deployment +- [anomaly-alert.bicep](https://github.com/microsoft/azcapman/blob/main/scripts/anomaly-alerts/anomaly-alert.bicep) - Bicep template + +## Related documentation + +- [Azure Resource Graph overview](https://learn.microsoft.com/en-us/azure/governance/resource-graph/overview) +- [Management groups](https://learn.microsoft.com/en-us/azure/governance/management-groups/overview) +- [Cost anomaly detection](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/anomaly-detection) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-bulkbudgets.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-bulkbudgets.md new file mode 100644 index 000000000..88a4bb7ef --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/deploy-bulkbudgets.md @@ -0,0 +1,187 @@ +--- +title: Deploy-BulkBudgets.ps1 +parent: Tools & scripts +nav_order: 9 +--- + +# Deploy-BulkBudgets.ps1 + +Deploy cost budgets to all subscriptions in a management group with per-subscription tag-based amount configuration. + +## Overview + +This PowerShell script uses Azure Resource Graph to discover all subscriptions in a management group, then deploys a `Microsoft.Consumption/budgets` resource to each. Each subscription can carry a different budget amount via its `BudgetAmount` tag, so a single bulk run respects per-subscription spending limits without manual intervention. + +### Key capabilities + +- **Management group targeting**: Discover and deploy to all subscriptions in a hierarchy +- **Tag-based amounts**: Reads `BudgetAmount` tag from each subscription for per-subscription limits +- **What-if mode**: Preview deployment scope without making changes +- **Force mode**: Skip confirmation for automation and CI/CD pipelines +- **Quiet mode**: Suppress output for cleaner pipeline logs + +### When to use this script + +- Applying cost governance across a management group at once +- Onboarding a new management group to budget monitoring +- Refreshing budgets after fiscal year changes +- Environments with 10+ subscriptions requiring consistent budget coverage + +## Prerequisites + +```powershell +# Install Azure PowerShell modules +Install-Module -Name Az -Force -AllowClobber +Install-Module -Name Az.ResourceGraph -Force -AllowClobber + +# Authenticate to Azure +Connect-AzAccount + +# Verify management group access +Get-AzManagementGroup | Select-Object Name, DisplayName +``` + +## Parameters + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| **ManagementGroup** | String | Management group to query for subscriptions | Required | +| **ContactEmails** | String[] | Email addresses for notifications | Required | +| **Amount** | Decimal | Budget amount (overrides all subscription tags) | Per-sub tag or $10 | +| **BudgetName** | String | Name of the budget resource | `SubscriptionBudget` | +| **TimeGrain** | String | Reset period: Monthly, Quarterly, or Annually | Monthly | +| **StartDate** | String | Budget start date (YYYY-MM-DD) | 1st of next month | +| **EndDate** | String | Budget end date (YYYY-MM-DD) | 1 year from start | +| **ContactRoles** | String[] | Azure roles to notify | Owner, Contributor | +| **FirstThreshold** | Int | First notification threshold % | 50 | +| **SecondThreshold** | Int | Second notification threshold % | 75 | +| **ThirdThreshold** | Int | Third notification threshold % | 90 | +| **ForecastedThreshold** | Int | Forecasted cost threshold % | 100 | +| **Force** | Switch | Skip confirmation prompt | False | +| **Quiet** | Switch | Suppress verbose output | False | +| **WhatIf** | Switch | Preview without deploying | False | + +## Usage examples + +### Deploy to all subscriptions in a management group + +```powershell +.\Deploy-BulkBudgets.ps1 ` + -ManagementGroup "ALZ" ` + -ContactEmails "admin@company.com" +``` + +### Override amount for all subscriptions + +```powershell +.\Deploy-BulkBudgets.ps1 ` + -ManagementGroup "ALZ" ` + -ContactEmails "admin@company.com" ` + -Amount 5000 +``` + +### What-if preview + +```powershell +.\Deploy-BulkBudgets.ps1 ` + -ManagementGroup "Production" ` + -ContactEmails "finops@company.com" ` + -WhatIf +``` + +### Automated deployment + +```powershell +.\Deploy-BulkBudgets.ps1 ` + -ManagementGroup "ALZ" ` + -ContactEmails "finops@company.com","alerts@company.com" ` + -Force -Quiet +``` + +### Production management group with custom thresholds + +```powershell +.\Deploy-BulkBudgets.ps1 ` + -ManagementGroup "Production" ` + -ContactEmails "finops@company.com","alerts@company.com" ` + -FirstThreshold 70 ` + -SecondThreshold 85 ` + -ThirdThreshold 95 ` + -ForecastedThreshold 100 +``` + +## Tag-based amount configuration + +Set the `BudgetAmount` tag on individual subscriptions before running bulk deployment to control per-subscription limits: + +```powershell +# Tag a dev subscription with a low budget +Update-AzTag ` + -ResourceId "/subscriptions/" ` + -Tag @{BudgetAmount = "50"} ` + -Operation Merge + +# Tag a production subscription with a higher budget +Update-AzTag ` + -ResourceId "/subscriptions/" ` + -Tag @{BudgetAmount = "10000"} ` + -Operation Merge +``` + +When `-Amount` is provided at the command line, it overrides all subscription tags. + +## Deployment process + +1. **Management group query**: Uses Resource Graph to discover all enabled subscriptions +2. **Confirmation prompt**: Shows subscription count and asks for confirmation (unless `-Force`) +3. **Sequential deployment**: Deploys to each subscription with progress output +4. **Summary report**: Shows success and failure counts at completion + +## Troubleshooting + +### Management group not found + +```powershell +# List available management groups +Get-AzManagementGroup | Format-Table Name, DisplayName + +# Verify exact name (case-sensitive) +Get-AzManagementGroup -GroupName "YOUR-MG-NAME" +``` + +### Resource Graph module missing + +```powershell +Install-Module -Name Az.ResourceGraph -Force -AllowClobber +``` + +### No subscriptions found + +```powershell +# Test the Resource Graph query directly +Search-AzGraph ` + -Query "resourcecontainers | where type == 'microsoft.resources/subscriptions'" ` + -ManagementGroup "YOUR-MG" +``` + +### Authentication errors + +```powershell +Clear-AzContext -Force +Connect-AzAccount -Tenant 'your-tenant-id' +``` + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/budgets/Deploy-BulkBudgets.ps1) + +## Related scripts + +- [Deploy-Budget.ps1](deploy-budget.md) - Single subscription budget deployment +- [Deploy-BulkALZ.ps1](deploy-bulkalz.md) - Bulk anomaly alert deployment pattern + +## Related documentation + +- [Azure budgets overview](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-acm-create-budgets) +- [Azure Resource Graph overview](https://learn.microsoft.com/en-us/azure/governance/resource-graph/overview) +- [Microsoft.Consumption/budgets API](https://learn.microsoft.com/en-us/azure/templates/microsoft.consumption/budgets) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-azavailabilityzonemapping.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-azavailabilityzonemapping.md new file mode 100644 index 000000000..fddc0be82 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-azavailabilityzonemapping.md @@ -0,0 +1,209 @@ +--- +title: Get-AzAvailabilityZoneMapping.ps1 +parent: Tools & scripts +nav_order: 2 +--- + +# Get-AzAvailabilityZoneMapping.ps1 + +Maps logical availability zones to physical datacenters across Azure subscriptions to ensure proper fault domain alignment. + +## Overview + +Azure randomizes the mapping between logical zones (1, 2, 3) and physical datacenters for each subscription. This means logical zone 1 in subscription A might map to a completely different physical datacenter than zone 1 in subscription B. This script reveals these mappings, which is critical for multi-subscription architectures requiring true zone alignment. + +### Key capabilities + +- **Physical zone discovery**: Reveals actual physical datacenter identifiers +- **Cross-subscription mapping**: Shows how zones align across multiple subscriptions +- **Compliance validation**: Ensures deployments meet data residency requirements +- **Architecture planning**: Essential data for multi-subscription deployment patterns + +### When to use this script + +- Planning multi-subscription deployments with zone redundancy +- Verifying zone alignment before deployment stamp creation +- Troubleshooting zone-related deployment failures +- Compliance audits requiring physical datacenter documentation +- Capacity reservation planning across subscriptions + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Repository PSGallery -Force + +# Authenticate to Azure +Connect-AzAccount + +# Verify subscription access +Get-AzSubscription | Select-Object Name, Id, State + +# Ensure you have Reader access to target subscriptions +Get-AzRoleAssignment | Where-Object {$_.RoleDefinitionName -like "*Reader*"} +``` + +## Parameters + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| **SubscriptionIds** | String[] | Array of subscription IDs to analyze | All accessible subscriptions | +| **OutputFile** | String | CSV output filename | ZonePeers.csv | + +## Usage examples + +### Basic usage - all subscriptions + +```powershell +.\Get-AzAvailabilityZoneMapping.ps1 +``` + +### Specific subscriptions for deployment planning + +```powershell +.\Get-AzAvailabilityZoneMapping.ps1 ` + -SubscriptionIds @('prod-sub-guid', 'dr-sub-guid') ` + -OutputFile "ProdDRZoneAlignment.csv" +``` + +### Multi-subscription stamp validation + +```powershell +# Get all production subscription IDs +$prodSubs = Get-AzSubscription | Where-Object Name -like "Prod-*" | Select-Object -ExpandProperty Id + +# Analyze zone mappings +.\Get-AzAvailabilityZoneMapping.ps1 ` + -SubscriptionIds $prodSubs ` + -OutputFile "ProductionZoneMappings_$(Get-Date -Format 'yyyyMMdd').csv" +``` + +## Sample output + +Running the script across multiple subscriptions produces output like this: + +```csv +TenantId,SubscriptionId,SubscriptionName,Location,LogicalZone,PhysicalZone,PhysicalZoneName +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Production-Primary,eastus,1,2,eastus-az2 +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Production-Primary,eastus,2,3,eastus-az3 +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Production-Primary,eastus,3,1,eastus-az1 +12345678-1234-1234-1234-123456789012,efgh5678-90ab-cdef-1234-567890abcdef,Production-Secondary,eastus,1,3,eastus-az3 +12345678-1234-1234-1234-123456789012,efgh5678-90ab-cdef-1234-567890abcdef,Production-Secondary,eastus,2,1,eastus-az1 +12345678-1234-1234-1234-123456789012,efgh5678-90ab-cdef-1234-567890abcdef,Production-Secondary,eastus,3,2,eastus-az2 +12345678-1234-1234-1234-123456789012,ijkl9012-3456-7890-abcd-ef1234567890,Development,eastus,1,1,eastus-az1 +12345678-1234-1234-1234-123456789012,ijkl9012-3456-7890-abcd-ef1234567890,Development,eastus,2,2,eastus-az2 +12345678-1234-1234-1234-123456789012,ijkl9012-3456-7890-abcd-ef1234567890,Development,eastus,3,3,eastus-az3 +``` + +### Understanding the output + +- **TenantId**: Microsoft Entra tenant identifier +- **SubscriptionId**: Unique subscription identifier +- **SubscriptionName**: Friendly subscription name +- **Location**: Azure region +- **LogicalZone**: The zone number applications see (1, 2, 3) +- **PhysicalZone**: Actual physical datacenter identifier +- **PhysicalZoneName**: Full physical datacenter name + +### Critical findings from sample + +In this example: +- **Production-Primary**: Logical zone 1 → Physical zone 2 +- **Production-Secondary**: Logical zone 1 → Physical zone 3 +- **Development**: Logical zone 1 → Physical zone 1 + +This means deploying to "zone 1" across these subscriptions would actually spread resources across three different physical datacenters, defeating zone alignment goals. + +## Real-world implications + +### Incorrect assumption +```plaintext +Deploy to Zone 1 in all subscriptions = Same physical datacenter ❌ +``` + +### Reality revealed by script +```plaintext +Subscription A Zone 1 = Physical Datacenter 2 +Subscription B Zone 1 = Physical Datacenter 3 +Subscription C Zone 1 = Physical Datacenter 1 +Result: No actual zone alignment! ⚠️ +``` + +### Correct approach +```plaintext +Use script output to map deployments: +- Subscription A: Deploy to Zone 3 (Physical DC 1) +- Subscription B: Deploy to Zone 2 (Physical DC 1) +- Subscription C: Deploy to Zone 1 (Physical DC 1) +Result: True zone alignment ✅ +``` + +## Troubleshooting + +### No zone data returned + +```powershell +# Verify the region supports availability zones +Get-AzLocation | Where-Object Location -eq 'eastus' | Select-Object -ExpandProperty Zones + +# Check if subscription has zone access +Get-AzComputeResourceSku -Location 'eastus' | + Where-Object {$_.LocationInfo.Zones.Count -gt 0} | + Select-Object -First 5 +``` + +### Access denied errors + +```powershell +# Verify role assignments +Get-AzRoleAssignment -SignInName (Get-AzContext).Account.Id + +# Switch to correct subscription context +Set-AzContext -SubscriptionId 'target-subscription-id' +``` + +### Performance optimization + +For large numbers of subscriptions: +```powershell +# Process in batches +$allSubs = Get-AzSubscription | Select-Object -ExpandProperty Id +$batchSize = 10 + +for ($i = 0; $i -lt $allSubs.Count; $i += $batchSize) { + $batch = $allSubs[$i..([Math]::Min($i + $batchSize - 1, $allSubs.Count - 1))] + .\Get-AzAvailabilityZoneMapping.ps1 ` + -SubscriptionIds $batch ` + -OutputFile "ZoneMapping_Batch_$($i / $batchSize + 1).csv" +} +``` + +## Integration patterns + +### Excel analysis +1. Import CSV into Excel +2. Create pivot table by Physical Zone +3. Filter by Location +4. Identify subscription groups with matching physical zones + +### Automated deployment alignment +```powershell +# Load zone mappings +$zoneMappings = Import-Csv "ZonePeers.csv" + +# Find subscriptions where logical zone 1 maps to physical zone 1 +$alignedSubs = $zoneMappings | + Where-Object {$_.LogicalZone -eq 1 -and $_.PhysicalZone -eq 1} | + Select-Object -Unique SubscriptionId, SubscriptionName + +# Deploy to these subscriptions in zone 1 for alignment +``` + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/quota/Get-AzAvailabilityZoneMapping.ps1) + +## Related scripts + +- [Get-AzVMQuotaUsage.ps1](get-azvmquotausage.md) - Comprehensive quota analysis with zone restrictions +- [Show-AzVMQuotaReport.ps1](show-azvmquotareport.md) - Single-threaded quota reporting \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-azvmquotausage.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-azvmquotausage.md new file mode 100644 index 000000000..7da986e09 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-azvmquotausage.md @@ -0,0 +1,177 @@ +--- +title: Get-AzVMQuotaUsage.ps1 +parent: Tools & scripts +nav_order: 1 +--- + +# Get-AzVMQuotaUsage.ps1 + +Multi-threaded Azure VM quota analysis script that provides comprehensive quota visibility across your subscription estate. + +## Overview + +This PowerShell script analyzes Azure VM quota usage and availability zone restrictions across multiple subscriptions and regions in parallel. It addresses the challenge of understanding quota consumption at scale when managing hundreds of subscriptions without Quota Groups. + +### Key capabilities + +- **Parallel processing**: Multi-threaded execution across subscriptions for faster analysis +- **Zone restriction detection**: Identifies which availability zones are restricted for specific VM SKUs +- **Physical zone mapping**: Maps logical zones to physical datacenters for cross-subscription alignment +- **Comprehensive reporting**: Outputs detailed CSV with quota usage, limits, and zone availability + +### When to use this script + +- Quarterly capacity planning reviews +- Pre-deployment quota validation +- Cross-subscription deployment planning +- Zone alignment verification for multi-subscription architectures +- Identifying restricted SKUs and zones before scaling + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Repository PSGallery -Force + +# PowerShell 7+ recommended for multi-threading +$PSVersionTable.PSVersion # Should be 7.0 or higher + +# Authenticate to Azure +Connect-AzAccount + +# Verify you have Reader access to target subscriptions +Get-AzSubscription | Select-Object Name, Id, State +``` + +## Parameters + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| **SKUs** | String[] | Array of VM SKU names to analyze | Downloads all SKUs | +| **Locations** | String[] | Array of Azure regions to query | All regions | +| **SubscriptionIds** | String[] | Array of subscription IDs to analyze | All accessible subscriptions | +| **Threads** | Int | Number of concurrent threads | 2 (0 for auto-detect) | +| **UsePhysicalZones** | Switch | Normalize output to physical zones | False | +| **OutputFile** | String | CSV output filename | VMQuotaUsage.csv | +| **MeterDataUri** | String | URL for VM SKU metadata | Azure public data | + +## Usage examples + +### Basic usage - analyze specific SKUs + +```powershell +.\Get-AzVMQuotaUsage.ps1 -SKUs @('Standard_D4s_v5', 'Standard_E4s_v5') -Locations @('eastus', 'westus2') +``` + +### Multi-threaded analysis across all subscriptions + +```powershell +.\Get-AzVMQuotaUsage.ps1 -SKUs @('Standard_D4s_v5', 'Standard_E4s_v5') ` + -Locations @('eastus', 'westus2', 'northeurope') ` + -Threads 4 ` + -OutputFile "QuotaAnalysis_$(Get-Date -Format 'yyyyMMdd').csv" +``` + +### Physical zone mapping for cross-subscription deployment + +```powershell +.\Get-AzVMQuotaUsage.ps1 -SKUs @('Standard_D4s_v5') ` + -Locations @('eastus') ` + -SubscriptionIds @('sub1-guid', 'sub2-guid') ` + -UsePhysicalZones ` + -OutputFile "CrossSubZoneAlignment.csv" +``` + +### Full estate analysis with auto-threading + +```powershell +.\Get-AzVMQuotaUsage.ps1 -Threads 0 -OutputFile "FullEstateQuota.csv" +``` + +## Sample output + +Running the script in a production environment produces output like this: + +```csv +TenantId,SubscriptionId,SubscriptionName,Location,Family,Size,RegionRestricted,ZonesPresent,ZonesRestricted,CoresUsed,CoresTotal +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Production-East,eastus,standardDSv5Family,Standard_D4s_v5,False,"1,2,3",,24,350 +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Production-East,eastus,standardESv5Family,Standard_E4s_v5,False,"1,2,3",,16,350 +12345678-1234-1234-1234-123456789012,efgh5678-90ab-cdef-1234-567890abcdef,Production-West,westus2,standardDSv5Family,Standard_D4s_v5,False,"1,2,3",,48,350 +12345678-1234-1234-1234-123456789012,efgh5678-90ab-cdef-1234-567890abcdef,Production-West,westus2,standardESv5Family,Standard_E4s_v5,False,"1,2,3",,0,350 +12345678-1234-1234-1234-123456789012,ijkl9012-3456-7890-abcd-ef1234567890,Development,northeurope,standardDSv5Family,Standard_D4s_v5,True,"1,2,3","2,3",0,100 +``` + +### Understanding the output + +- **TenantId**: Your Microsoft Entra tenant identifier +- **SubscriptionId**: Unique subscription identifier +- **SubscriptionName**: Friendly subscription name +- **Location**: Azure region +- **Family**: VM family for quota calculation +- **Size**: Specific VM SKU +- **RegionRestricted**: Whether the SKU has regional restrictions (True/False) +- **ZonesPresent**: Available availability zones (comma-separated) +- **ZonesRestricted**: Restricted zones requiring enablement request (comma-separated) +- **CoresUsed**: Current vCPU consumption +- **CoresTotal**: Total vCPU quota limit + +## Troubleshooting + +### Common issues + +**PowerShell version error** +```powershell +# Check version +$PSVersionTable.PSVersion + +# Install PowerShell 7 if needed +winget install Microsoft.PowerShell +``` + +**Authentication failures** +```powershell +# Clear cached credentials +Clear-AzContext -Force + +# Re-authenticate +Connect-AzAccount -Tenant 'your-tenant-id' +``` + +**Throttling with high thread count** +```powershell +# Reduce threads if seeing 429 errors +.\Get-AzVMQuotaUsage.ps1 -Threads 2 # Lower thread count +``` + +**No data returned** +```powershell +# Verify subscription access +Get-AzSubscription | Where-Object State -eq 'Enabled' + +# Check region availability +Get-AzLocation | Select-Object Location, DisplayName +``` + +## Performance considerations + +- **Thread optimization**: Start with 2-4 threads, increase gradually +- **Regional filtering**: Specify `-Locations` to reduce API calls +- **SKU filtering**: Target specific SKUs rather than analyzing all +- **Output size**: Large estates may produce CSV files over 10MB + +## Integration with other tools + +This script output integrates with: +- Excel pivot tables for quota analysis +- Power BI for visualization +- Azure Monitor for alerting +- Capacity planning spreadsheets + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/quota/Get-AzVMQuotaUsage.ps1) + +## Related scripts + +- [Get-AzAvailabilityZoneMapping.ps1](get-azavailabilityzonemapping.md) - Map logical to physical zones +- [Show-AzVMQuotaReport.ps1](show-azvmquotareport.md) - Single-threaded alternative \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-benefitrecommendations.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-benefitrecommendations.md new file mode 100644 index 000000000..34b2c6825 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/get-benefitrecommendations.md @@ -0,0 +1,223 @@ +--- +title: Get-BenefitRecommendations.ps1 +parent: Tools & scripts +nav_order: 5 +--- + +# Get-BenefitRecommendations.ps1 + +Queries the Azure Cost Management API to retrieve detailed benefit recommendations that aren't fully exposed in the Azure portal. + +## Overview + +This PowerShell script extracts comprehensive savings plan and reserved instance recommendations directly from the Azure Cost Management API. The API returns additional details and granular data that the portal interface doesn't display, providing deeper insights for cost optimization decisions. + +### Key capabilities + +- **API-level detail extraction**: Accesses recommendation data not visible in portal +- **Flexible lookback periods**: Analyze 7, 30, or 60 days of historical usage +- **Multiple commitment terms**: Compare 1-year vs 3-year savings plans +- **Billing scope flexibility**: Works at subscription or billing account level + +### When to use this script + +- Extracting detailed recommendation data for FinOps reporting +- Comparing savings opportunities across different commitment terms +- Analyzing recommendations based on different usage windows +- Accessing API data fields not shown in portal views +- Building automated cost optimization workflows + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Repository PSGallery -Force + +# Authenticate to Azure +Connect-AzAccount + +# Verify Cost Management Reader permissions +Get-AzRoleAssignment | Where-Object {$_.RoleDefinitionName -like "*Cost Management*"} + +# Find your billing account (for MCA) +Get-AzBillingAccount | Select-Object Name, Id + +# List available subscriptions +Get-AzSubscription | Select-Object Name, Id +``` + +## Parameters + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| **BillingScope** | String | Billing scope path (required) | None | +| **LookBackPeriod** | String | Historical period to analyze | Last7Days | +| **Term** | String | Commitment term: P1Y or P3Y | P3Y | + +### BillingScope format + +For subscriptions: +``` +subscriptions/12345678-1234-1234-1234-123456789012 +``` + +For billing accounts: +``` +providers/Microsoft.Billing/billingAccounts/12345678 +``` + +### LookBackPeriod values + +- `Last7Days` - Analyze last 7 days of usage +- `Last30Days` - Analyze last 30 days of usage +- `Last60Days` - Analyze last 60 days of usage + +### Term values + +- `P1Y` - 1-year commitment term +- `P3Y` - 3-year commitment term + +## Usage examples + +### Basic subscription analysis + +```powershell +# Get 3-year recommendations based on last 7 days +.\Get-BenefitRecommendations.ps1 ` + -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012" +``` + +### Extended analysis period + +```powershell +# Analyze 30 days of usage for 1-year commitments +.\Get-BenefitRecommendations.ps1 ` + -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012" ` + -LookBackPeriod "Last30Days" ` + -Term "P1Y" +``` + +### Billing account level + +```powershell +# Get billing account ID +$billingAccount = Get-AzBillingAccount | Select-Object -First 1 + +# Analyze entire billing account +.\Get-BenefitRecommendations.ps1 ` + -BillingScope "providers/Microsoft.Billing/billingAccounts/$($billingAccount.Name)" ` + -LookBackPeriod "Last60Days" ` + -Term "P3Y" +``` + +### Export results for analysis + +```powershell +# Run script and capture output +$result = .\Get-BenefitRecommendations.ps1 ` + -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012" ` + -LookBackPeriod "Last30Days" + +# The script outputs raw JSON and formatted tables +# Capture and process as needed for your workflows +``` + +## Output format + +The script provides three types of output: + +1. **Raw JSON output** - Complete API response for processing +2. **Recommended savings plan** - Primary recommendation in table format +3. **All savings plan recommendations** - Full list of recommendations + +## API endpoint + +The script calls the Cost Management API: +``` +https://management.azure.com/{BillingScope}/providers/Microsoft.CostManagement/benefitRecommendations +``` + +With parameters: +- `$filter`: Filters by lookback period and term +- `$expand`: Includes usage and recommendation details +- `api-version`: 2024-08-01 + +## Troubleshooting + +### No recommendations returned + +```powershell +# Verify you have usage data in the period +Get-AzConsumptionUsageDetail ` + -StartDate (Get-Date).AddDays(-7) ` + -EndDate (Get-Date) | + Measure-Object + +# Check permissions +Get-AzRoleAssignment -Scope "/subscriptions/YOUR-SUB-ID" | + Where-Object {$_.RoleDefinitionName -like "*Cost Management*"} +``` + +### Authentication errors + +```powershell +# Clear cached credentials +Clear-AzContext -Force + +# Re-authenticate +Connect-AzAccount + +# Set specific subscription context +Set-AzContext -SubscriptionId "YOUR-SUB-ID" +``` + +### API access issues + +```powershell +# Test direct API access +$scope = "subscriptions/YOUR-SUB-ID" +$uri = "https://management.azure.com/$scope/providers/Microsoft.CostManagement/query?api-version=2024-08-01" +Invoke-AzRestMethod -Uri $uri -Method POST +``` + +## Integration patterns + +### Scheduled analysis + +```powershell +# Weekly analysis across all subscriptions +$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled" + +foreach ($sub in $subscriptions) { + Write-Host "Analyzing subscription: $($sub.Name)" + .\Get-BenefitRecommendations.ps1 ` + -BillingScope "subscriptions/$($sub.Id)" ` + -LookBackPeriod "Last30Days" +} +``` + +### CI/CD integration + +```yaml +# Azure DevOps pipeline +- task: AzurePowerShell@5 + inputs: + azureSubscription: 'ServiceConnection' + ScriptType: 'FilePath' + ScriptPath: '$(System.DefaultWorkingDirectory)/Get-BenefitRecommendations.ps1' + ScriptArguments: > + -BillingScope "subscriptions/$(SubscriptionId)" + -LookBackPeriod "Last30Days" + -Term "P3Y" + azurePowerShellVersion: 'LatestVersion' +``` + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/rate/Get-BenefitRecommendations.ps1) + +## Related documentation + +- [Azure Cost Management API](https://learn.microsoft.com/en-us/rest/api/cost-management/) +- [Savings plan purchase recommendations](https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/purchase-recommendations) +- [Understanding reservation recommendations](https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/reserved-instance-purchase-recommendations) \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/show-azvmquotareport.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/show-azvmquotareport.md new file mode 100644 index 000000000..676313d4a --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/show-azvmquotareport.md @@ -0,0 +1,244 @@ +--- +title: Show-AzVMQuotaReport.ps1 +parent: Tools & scripts +nav_order: 3 +--- + +# Show-AzVMQuotaReport.ps1 + +Single-threaded Azure VM quota reporting script that provides streamlined quota analysis for smaller deployments or learning scenarios. + +## Overview + +This PowerShell script offers a simplified approach to Azure VM quota analysis. Unlike its multi-threaded counterpart, it processes subscriptions sequentially and displays results in a formatted table, making it ideal for smaller estates or when you're learning how Azure quota management works. + +### Key capabilities + +- **Sequential processing**: Straightforward single-threaded execution for easier debugging +- **Formatted output**: Displays results in a clean table format at completion +- **Zone restriction detection**: Identifies which availability zones are restricted for specific VM SKUs +- **Physical zone mapping**: Optional mapping of logical to physical zones for cross-subscription planning +- **Educational design**: Simpler code structure for understanding quota analysis patterns + +### When to use this script + +- Learning Azure quota management concepts +- Analyzing smaller subscription counts (< 10 subscriptions) +- Debugging quota issues with verbose output +- Quick ad-hoc quota checks for specific SKUs/regions +- Teaching quota analysis patterns to new team members + +### Comparison with Get-AzVMQuotaUsage.ps1 + +| Feature | Show-AzVMQuotaReport | Get-AzVMQuotaUsage | +|---------|----------------------|--------------------| +| Threading | Single-threaded | Multi-threaded (configurable) | +| Best for | < 10 subscriptions | 100+ subscriptions | +| Performance | Sequential | Parallel processing | +| Code complexity | Simpler | Production-optimized | +| Output format | Table + CSV | CSV only | +| Use case | Learning/debugging | Production analysis | + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Repository PSGallery -Force + +# Authenticate to Azure +Connect-AzAccount + +# Verify subscription access +Get-AzSubscription | Select-Object Name, Id, State + +# Check Reader permissions +Get-AzRoleAssignment | Where-Object {$_.RoleDefinitionName -like "*Reader*"} +``` + +## Parameters + +| Parameter | Type | Description | Default | +|-----------|------|-------------|---------| +| **SKUs** | String[] | Array of VM SKU names to analyze | Catalogs API-first discovery; falls back to deprecated CSV only if API lookup fails and `-SKUs` is omitted | +| **Locations** | String[] | Array of Azure regions to query | All regions | +| **SubscriptionIds** | String[] | Array of subscription IDs to analyze | All accessible subscriptions | +| **CatalogLocation** | String | Azure region used for Catalogs API lookup | eastus | +| **OutputFile** | String | CSV output filename | QuotaQuery.csv | +| **UsePhysicalZones** | Switch | Map logical zones to physical datacenters | False | +| **MeterDataUri** | String | Deprecated fallback source URI for VM SKU metadata, used only when Catalogs API lookup fails and `-SKUs` is not provided | Azure public data | + +## Usage examples + +### Basic quota check for specific SKUs + +```powershell +.\Show-AzVMQuotaReport.ps1 -SKUs @('Standard_D2s_v5', 'Standard_E4s_v5') -Locations @('eastus', 'westus2') +``` + +### Analyze specific subscriptions with physical zone mapping + +```powershell +.\Show-AzVMQuotaReport.ps1 ` + -SubscriptionIds @('sub1-guid', 'sub2-guid') ` + -UsePhysicalZones ` + -OutputFile "DevSubscriptionQuota.csv" +``` + +### Quick quota audit for a single region + +```powershell +.\Show-AzVMQuotaReport.ps1 ` + -SKUs @('Standard_D4s_v5') ` + -Locations @('eastus') ` + -OutputFile "EastUSQuotaCheck_$(Get-Date -Format 'yyyyMMdd').csv" +``` + +### Full analysis with default SKU discovery + +```powershell +.\Show-AzVMQuotaReport.ps1 -OutputFile "CompleteQuotaAnalysis.csv" +``` + +## Sample output + +Running the script produces both console output and a CSV file: + +### Console output +```plaintext +Querying Azure Catalogs API for VM SKU details in eastus +Retrieved 412 unique VM SKUs from Catalogs API +Listing Subscriptions +Processing subscription: Development (12345678-abcd-1234-5678-123456789012) + Analyzing location: eastus + Checking SKU: Standard_D2s_v5 + Checking SKU: Standard_E4s_v5 + Analyzing location: westus2 + Checking SKU: Standard_D2s_v5 + Checking SKU: Standard_E4s_v5 + +Results saved to: QuotaQuery.csv + +Summary Table: +Subscription Location SKU CoresUsed/Total Zones Available +------------ -------- --------------- --------------- --------------- +Development eastus Standard_D2s_v5 8/100 1,2,3 +Development eastus Standard_E4s_v5 0/100 1,2,3 +Development westus2 Standard_D2s_v5 4/100 1,2,3 +Development westus2 Standard_E4s_v5 16/100 1,2,3 +``` + +### CSV output +```csv +TenantId,SubscriptionId,SubscriptionName,Location,Family,Size,RegionRestricted,ZonesPresent,ZonesRestricted,CoresUsed,CoresTotal +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Development,eastus,standardDSv5Family,Standard_D2s_v5,False,"1,2,3",,8,100 +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Development,eastus,standardESv5Family,Standard_E4s_v5,False,"1,2,3",,0,100 +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Development,westus2,standardDSv5Family,Standard_D2s_v5,False,"1,2,3",,4,100 +12345678-1234-1234-1234-123456789012,abcd1234-5678-90ab-cdef-123456789012,Development,westus2,standardESv5Family,Standard_E4s_v5,False,"1,2,3",,16,100 +``` + +### Understanding the output + +- **TenantId**: Microsoft Entra tenant identifier +- **SubscriptionId**: Unique subscription identifier +- **SubscriptionName**: Friendly subscription name +- **Location**: Azure region +- **Family**: VM family for quota calculation +- **Size**: Specific VM SKU +- **RegionRestricted**: Whether the SKU has regional restrictions +- **ZonesPresent**: Available availability zones +- **ZonesRestricted**: Zones requiring enablement request +- **CoresUsed**: Current vCPU consumption +- **CoresTotal**: Total vCPU quota limit + +## Troubleshooting + +### Script runs slowly + +This is expected behavior for the single-threaded version. For faster processing: +```powershell +# Use the multi-threaded version instead +.\Get-AzVMQuotaUsage.ps1 -Threads 4 +``` + +### Catalogs API returns no SKU data + +The script now uses the Catalogs API as the primary SKU source. If SKU discovery fails: + +```powershell +# Verify the selected catalog location +Get-AzLocation | Where-Object Location -eq 'eastus' + +# Verify the Catalogs API permission is available +Get-AzRoleAssignment | + Where-Object { $_.RoleDefinitionName -like '*Reader*' } +``` + +If you already know the SKU names, pass them with `-SKUs` to skip catalog discovery. + +### Authentication or permission errors + +Catalog lookup requires an authenticated Az session and the `Microsoft.Capacity/catalogs/read` +permission in addition to subscription read access. + +```powershell +# Clear and re-authenticate +Clear-AzContext -Force +Connect-AzAccount -Tenant 'your-tenant-id' + +# Verify current context +Get-AzContext | Select-Object Account, Subscription, Tenant +``` + +If the script reports an authorization failure from the Catalogs API, ask your Azure administrator +to grant access that includes `Microsoft.Capacity/catalogs/read`. + +### CSV fallback behavior + +If the Catalogs API lookup fails and you do not pass `-SKUs`, the script falls back to the +deprecated meter data CSV source. That fallback helps older workflows continue, but it may not +include the latest SKU catalog changes. + +```powershell +# Check whether the default local fallback CSV exists after a failed Catalogs API lookup +Test-Path "AutofitComboMeterData.csv" +``` + +### Empty zones data + +```powershell +# Some regions don't support availability zones +Get-AzLocation | Where-Object Location -eq 'westus' | + Select-Object -ExpandProperty Zones + +# If empty, the region doesn't support zones +``` + +## Educational value + +This script serves as a learning tool for understanding: +- How Azure organizes quota by VM family +- The relationship between logical and physical zones +- Sequential API interaction patterns +- CSV export for further analysis + +For production use with large subscription counts, use Get-AzVMQuotaUsage.ps1 which provides: +- Parallel processing for 10x faster execution +- Configurable thread counts +- Better error handling for large-scale analysis + +## Performance comparison + +| Subscriptions | Show-AzVMQuotaReport | Get-AzVMQuotaUsage (4 threads) | +|---------------|----------------------|----------------------------------| +| 5 | ~2 minutes | ~30 seconds | +| 25 | ~10 minutes | ~3 minutes | +| 100 | ~40 minutes | ~10 minutes | + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/blob/main/scripts/quota/Show-AzVMQuotaReport.ps1) + +## Related scripts + +- [Get-AzVMQuotaUsage.ps1](get-azvmquotausage.md) - Multi-threaded version for production use +- [Get-AzAvailabilityZoneMapping.ps1](get-azavailabilityzonemapping.md) - Dedicated zone mapping analysis diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/suppress-advisorrecommendations.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/suppress-advisorrecommendations.md new file mode 100644 index 000000000..29a8e1a6b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/operations/tools-scripts/suppress-advisorrecommendations.md @@ -0,0 +1,144 @@ +--- +title: Suppress-AdvisorRecommendations.ps1 +parent: Tools & scripts +nav_order: 10 +--- + +# Suppress-AdvisorRecommendations.ps1 + +Suppress specific Azure Advisor recommendation types across all subscriptions in a management group for up to 90 days. + +## Overview + +Azure Policy can't disable Advisor recommendations. Advisor exposes a [suppression API](https://learn.microsoft.com/en-us/azure/advisor/suppress-recommendations?tabs=rest) with a TTL of up to 90 days. This script iterates over a management group's subscriptions and calls that API for each specified recommendation type, so FinOps or platform teams can bulk-suppress noise without navigating the portal subscription by subscription. + +### Key capabilities + +- **Management group scope**: Suppress across all subscriptions in a hierarchy +- **Recommendation type targeting**: Suppress by recommendation type GUID +- **TTL control**: Configurable suppression duration up to 90 days +- **What-if mode**: Preview scope without writing suppressions + +### When to use this script + +- Suppressing recommendations that your team manages centrally (e.g., reserved instance purchases handled by a FinOps team, not individual subscription owners) +- Quieting noise for known workload patterns that Advisor doesn't understand +- Temporary suppression during migrations or planned outages +- Weekly automation to maintain suppression across a large subscription estate + +> **Note**: Suppression TTL is capped at 90 days per the [Advisor suppressions API](https://learn.microsoft.com/en-us/rest/api/advisor/suppressions/create?tabs=HTTP). Run the script on a weekly or bi-weekly schedule to maintain coverage. + +## Prerequisites + +```powershell +# Install Azure PowerShell module +Install-Module -Name Az -Repository PSGallery -Force + +# Authenticate to Azure +Connect-AzAccount + +# Verify management group access +Get-AzManagementGroup | Select-Object Name, DisplayName +``` + +**Permissions**: Advisor Contributor or higher on the management group and its subscriptions. See [Advisor permissions](https://learn.microsoft.com/en-us/azure/advisor/permissions). + +## Parameters + +| Parameter | Type | Description | Required | +|-----------|------|-------------|----------| +| **ManagementGroupId** | String | Management group name to target | Yes | +| **RecommendationTypeIds** | String[] | Array of recommendation type GUIDs to suppress | Yes | +| **Days** | Int | Suppression duration in days (1–90) | Yes | +| **WhatIf** | Switch | Preview scope without writing suppressions | No | + +## Usage examples + +### Dry run — preview scope + +```powershell +.\Suppress-AdvisorRecommendations.ps1 ` + -ManagementGroupId "your-mg" ` + -RecommendationTypeIds @( + "89515250-1243-43d1-b4e7-f9437cedffd8", + "84b1a508-fc21-49da-979e-96894f1665df", + "48eda464-1485-4dcf-a674-d0905df5054a" + ) ` + -Days 30 ` + -WhatIf +``` + +### Execute suppression + +```powershell +.\Suppress-AdvisorRecommendations.ps1 ` + -ManagementGroupId "your-mg" ` + -RecommendationTypeIds @( + "89515250-1243-43d1-b4e7-f9437cedffd8", + "84b1a508-fc21-49da-979e-96894f1665df", + "48eda464-1485-4dcf-a674-d0905df5054a" + ) ` + -Days 30 +``` + +## Finding recommendation type GUIDs + +Advisor recommendation type GUIDs aren't displayed in the portal. Retrieve them via the [Advisor REST API](https://learn.microsoft.com/en-us/rest/api/advisor/recommendations/list?tabs=HTTP): + +```bash +# List recommendations with their type GUIDs for a subscription +az rest --method GET \ + --url "https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Advisor/recommendations?api-version=2023-01-01" +``` + +Look for the `recommendationTypeId` field in the response. + +## Scheduling + +Suppression TTL tops out at 90 days. To maintain continuous suppression, schedule this script on a weekly cadence via Azure Automation or a CI/CD pipeline: + +```powershell +# Azure Automation runbook pattern (using managed identity) +Connect-AzAccount -Identity + +.\Suppress-AdvisorRecommendations.ps1 ` + -ManagementGroupId $env:MANAGEMENT_GROUP_ID ` + -RecommendationTypeIds ($env:RECOMMENDATION_TYPE_IDS -split ',') ` + -Days 30 +``` + +**Source**: [Suppressions API reference](https://learn.microsoft.com/en-us/rest/api/advisor/suppressions/create?tabs=HTTP) + +## Troubleshooting + +### Permission errors + +```powershell +# Check current role assignments +Get-AzRoleAssignment -SignInName (Get-AzContext).Account.Id | + Where-Object { $_.Scope -like "*/managementGroups/*" } +``` + +You need Advisor Contributor or higher. Reader alone isn't sufficient—write access to the suppression resource is required. + +### No subscriptions found + +```powershell +# Verify management group access and name +Get-AzManagementGroup -GroupName "your-mg" -Expand -Recurse | + Select-Object -ExpandProperty Children +``` + +### Suppression not persisting + +Verify the TTL hasn't expired. Suppressions older than `Days` are automatically cleared by Advisor. Check the [Advisor portal](https://portal.azure.com/#blade/Microsoft_Azure_Expert/AdvisorMenuBlade/overview) > Settings > Suppressions to confirm active suppression records. + +## Script source + +[View full script source →](https://github.com/microsoft/azcapman/tree/main/scripts/advisor) + +## Related documentation + +- [Suppress Azure Advisor recommendations](https://learn.microsoft.com/en-us/azure/advisor/suppress-recommendations?tabs=rest) +- [Advisor suppressions API](https://learn.microsoft.com/en-us/rest/api/advisor/suppressions/create?tabs=HTTP) +- [Advisor permissions](https://learn.microsoft.com/en-us/azure/advisor/permissions) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/toc.yml b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/toc.yml new file mode 100644 index 000000000..be391facc --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/docs/toc.yml @@ -0,0 +1,104 @@ +- name: Overview + href: index.md +- name: Labs + href: labs/lab-02-forecasting.md + items: + - name: Planning and forecasting + href: labs/lab-02-forecasting.md + - name: Workload placement + href: labs/lab-03-allocation.md + - name: Capacity guarantees + href: labs/lab-04-procurement.md + - name: Governance and automation + href: labs/lab-05-monitoring-governance.md +- name: Capacity management + href: operations/README.md + items: + - name: Overview + href: operations/README.md + - name: Planning and forecasting + href: operations/capacity-planning/README.md + items: + - name: Capacity and billing operations + href: operations/capacity-and-quotas/README.md + - name: Capacity planning + href: operations/capacity-planning/README.md + - name: Customer isolation + href: deployment/README.md + items: + - name: Overview + href: deployment/README.md + - name: Single-tenant + href: deployment/single-tenant/README.md + - name: Multi-tenant + href: deployment/multi-tenant/README.md + - name: Workload placement + href: operations/quota/README.md + items: + - name: Billing models + href: billing/README.md + items: + - name: Overview + href: billing/README.md + - name: Microsoft Customer Agreement + href: billing/modern/README.md + items: + - name: Overview + href: billing/modern/README.md + - name: Subscription creation (billing scope) + href: operations/modern/README.md + - name: Enterprise Agreement + href: billing/legacy/README.md + items: + - name: Overview + href: billing/legacy/README.md + - name: Subscription creation (billing scope) + href: operations/legacy/README.md + - name: Quota operations + href: operations/quota/README.md + - name: Capacity reservations + href: operations/capacity-reservations/README.md + - name: AKS capacity governance + href: operations/aks-capacity/README.md + - name: Non-compute quotas + href: operations/non-compute-quotas/README.md + - name: Usage and rate optimization + href: operations/quota-groups/README.md + items: + - name: Quota groups + href: operations/quota-groups/README.md + - name: Governance and automation + href: operations/monitoring-alerting/README.md + items: + - name: Monitoring and alerting + href: operations/monitoring-alerting/README.md + - name: Capacity governance + href: operations/capacity-governance/README.md + - name: Tools and scripts + href: operations/tools-scripts/README.md + items: + - name: Overview + href: operations/tools-scripts/README.md + - name: Get-AzVMQuotaUsage.ps1 + href: operations/tools-scripts/get-azvmquotausage.md + - name: Get-AzAvailabilityZoneMapping.ps1 + href: operations/tools-scripts/get-azavailabilityzonemapping.md + - name: Show-AzVMQuotaReport.ps1 + href: operations/tools-scripts/show-azvmquotareport.md + - name: Get-BenefitRecommendations.ps1 + href: operations/tools-scripts/get-benefitrecommendations.md + - name: Deploy-AnomalyAlert.ps1 + href: operations/tools-scripts/deploy-anomalyalert.md + - name: Deploy-BulkALZ.ps1 + href: operations/tools-scripts/deploy-bulkalz.md + - name: Support and reference + href: operations/support-and-reference/README.md + items: + - name: Overview + href: operations/support-and-reference/README.md + - name: Support escalation + href: operations/escalation/README.md + - name: Tenant hygiene + href: operations/tenant-hygiene/README.md + - name: Glossary + href: operations/glossary.md diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/advisor/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/advisor/README.md new file mode 100644 index 000000000..3c5a8cd81 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/advisor/README.md @@ -0,0 +1,27 @@ +# Azure Advisor suppression + +Azure Policy cannot disable Advisor recommendations; Advisor provides suppression APIs with TTL up to 90 days. Use this script to suppress specific recommendation types across a management group. [Source](https://learn.microsoft.com/en-us/azure/advisor/suppress-recommendations?tabs=rest) + +## Usage + +```powershell +# Dry run +.\Suppress-AdvisorRecommendations.ps1 -ManagementGroupId "your-mg" ` + -RecommendationTypeIds @( + "89515250-1243-43d1-b4e7-f9437cedffd8", + "84b1a508-fc21-49da-979e-96894f1665df", + "48eda464-1485-4dcf-a674-d0905df5054a" + ) -Days 30 -WhatIf + +# Execute +.\Suppress-AdvisorRecommendations.ps1 -ManagementGroupId "your-mg" ` + -RecommendationTypeIds @(...) -Days 30 +``` + +## Schedule + +Run weekly via Azure Automation or CI/CD pipeline; suppression TTL is capped at 90 days. [Source](https://learn.microsoft.com/en-us/rest/api/advisor/suppressions/create?tabs=HTTP) + +## Permissions + +Advisor Contributor (or higher) on the management group and subscriptions. [Source](https://learn.microsoft.com/en-us/azure/advisor/permissions) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/advisor/Suppress-AdvisorRecommendations.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/advisor/Suppress-AdvisorRecommendations.ps1 new file mode 100644 index 000000000..acc45f178 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/advisor/Suppress-AdvisorRecommendations.ps1 @@ -0,0 +1,79 @@ +<# +.SYNOPSIS + Suppress Azure Advisor recommendations across a management group. + +.EXAMPLE + .\Suppress-AdvisorRecommendations.ps1 -ManagementGroupId "ALZ" -Days 30 -WhatIf +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory)] + [string]$ManagementGroupId, + + [string[]]$RecommendationTypeIds = @( + "89515250-1243-43d1-b4e7-f9437cedffd8", + "84b1a508-fc21-49da-979e-96894f1665df", + "48eda464-1485-4dcf-a674-d0905df5054a" + ), + + [ValidateRange(1,90)] + [int]$Days = 30 +) + +$ErrorActionPreference = 'Stop' + +# Advisor suppression TTL uses ISO 8601 duration and max 90 days. +function Get-TtlString([int]$days) { + return "P{0}D" -f $days +} + +# Get all subscriptions under management group +$mg = Get-AzManagementGroup -GroupId $ManagementGroupId -Expand -Recurse +$subs = @() +function Get-Subs($node) { + if ($node.Children) { + foreach ($c in $node.Children) { + if ($c.Type -eq '/subscriptions') { $script:subs += @{Id=$c.Name; Name=$c.DisplayName} } + else { Get-Subs $c } + } + } +} +Get-Subs $mg + +Write-Host "Processing $($subs.Count) subscription(s)..." -ForegroundColor Cyan + +$total = @{Found=0; Suppressed=0; Failed=0} +$ttl = Get-TtlString -days $Days + +foreach ($sub in $subs) { + Set-AzContext -SubscriptionId $sub.Id -ErrorAction SilentlyContinue | Out-Null + + $recs = Get-AzAdvisorRecommendation -ErrorAction SilentlyContinue | + Where-Object { $_.RecommendationTypeId -in $RecommendationTypeIds } + + if (-not $recs) { continue } + + $total.Found += $recs.Count + Write-Host "[$($sub.Name)] Found $($recs.Count)" -ForegroundColor Yellow + + foreach ($rec in $recs) { + if ($WhatIfPreference) { + Write-Host " WhatIf: $($rec.ShortDescriptionProblem)" -ForegroundColor Gray + continue + } + + $resourceUri = $rec.Id.Substring(0, $rec.Id.IndexOf("/providers/Microsoft.Advisor")) + $recId = ($rec.Id -split '/recommendations/')[-1] + $suppressionId = [Guid]::NewGuid() + $uri = "https://management.azure.com$resourceUri/providers/Microsoft.Advisor/recommendations/$recId/suppressions/$suppressionId?api-version=2023-01-01" + + $body = @{properties=@{ttl=$ttl}} | ConvertTo-Json + $resp = Invoke-AzRestMethod -Method PUT -Uri $uri -Payload $body + + if ($resp.StatusCode -in 200,201) { $total.Suppressed++ } + else { $total.Failed++; Write-Warning "Failed ($($resp.StatusCode)): $($rec.ShortDescriptionProblem)" } + } +} + +Write-Host "`nFound: $($total.Found) | Suppressed: $($total.Suppressed) | Failed: $($total.Failed)" -ForegroundColor Cyan diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/Deploy-AnomalyAlert.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/Deploy-AnomalyAlert.ps1 new file mode 100644 index 000000000..ec116c820 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/Deploy-AnomalyAlert.ps1 @@ -0,0 +1,312 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Deploy anomaly alert configuration to an Azure subscription. + +.DESCRIPTION + This script deploys the anomaly alert Bicep template to create a subscription-level + scheduled action for cost anomaly monitoring. The script handles subscription + validation, parameter collection, and deployment with proper error handling. + +.PARAMETER SubscriptionId + The target Azure subscription ID where the anomaly alert will be deployed. + If not provided, the script will prompt for selection from available subscriptions. + +.PARAMETER EmailRecipients + Array of email addresses that'll receive anomaly alert notifications. + These should be valid email addresses of stakeholders who need to be notified. + +.PARAMETER NotificationEmail + Primary notification email address for the anomaly alert system. + This is typically an administrative or monitoring email address. + +.PARAMETER DeploymentName + Optional custom name for the deployment. If not provided, a timestamped name will be generated. + +.PARAMETER Location + Azure region for the deployment. Defaults to 'West US' if not specified. + Note: This is only for deployment metadata, not resource location. + +.PARAMETER Force + Skip confirmation prompts and proceed with deployment automatically. + +.PARAMETER Quiet + Suppress verbose deployment progress output to avoid the 5-second polling wait. + +.EXAMPLE + ./Deploy-AnomalyAlert.ps1 -EmailRecipients @("admin@company.com", "finance@company.com") -NotificationEmail "alerts@company.com" + + Deploy anomaly alert with interactive subscription selection and specified email addresses. + +.EXAMPLE + ./Deploy-AnomalyAlert.ps1 -SubscriptionId "12345678-1234-1234-1234-123456789012" -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" -WhatIf + + Show what would be deployed in the specified subscription without actually deploying. + +.EXAMPLE + ./Deploy-AnomalyAlert.ps1 -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" -Force + + Deploy without confirmation prompts. + +.NOTES + - Requires Azure PowerShell module (Az) + - User must be authenticated to Azure (Connect-AzAccount) + - User must have appropriate permissions to create subscription-level resources + - The deployment creates a cost management scheduled action for anomaly detection +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $false)] + [ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')] + [string]$SubscriptionId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$EmailRecipients, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$NotificationEmail, + + [Parameter(Mandatory = $false)] + [string]$DeploymentName, + + [Parameter(Mandatory = $false)] + [string]$Location = 'West US', + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [switch]$Quiet +) + +#region Helper Functions + +function Write-Log { + param( + [string]$Message, + [ValidateSet('Info', 'Warning', 'Error', 'Success')] + [string]$Level = 'Info' + ) + + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $colors = @{ + 'Info' = 'White' + 'Warning' = 'Yellow' + 'Error' = 'Red' + 'Success' = 'Green' + } + + Write-Host "[$timestamp] " -NoNewline -ForegroundColor Gray + Write-Host $Message -ForegroundColor $colors[$Level] +} + +function Test-AzureConnection { + try { + $context = Get-AzContext + if (-not $context) { + return $false + } + return $true + } + catch { + return $false + } +} + +function Get-SubscriptionSelection { + param([string]$TargetSubscriptionId) + + try { + $subscriptions = Get-AzSubscription | Sort-Object Name + + if ($subscriptions.Count -eq 0) { + throw "No subscriptions found. Check if you are logged in with the correct Azure account." + } + + if ($TargetSubscriptionId) { + $selectedSub = $subscriptions | Where-Object { $_.Id -eq $TargetSubscriptionId } + if (-not $selectedSub) { + throw "Subscription with ID '$TargetSubscriptionId' not found or not accessible." + } + return $selectedSub + } + + if ($subscriptions.Count -eq 1) { + Write-Log "Using the only available subscription: $($subscriptions[0].Name)" -Level Info + return $subscriptions[0] + } + + Write-Log "Multiple subscriptions found. Please select:" -Level Info + for ($i = 0; $i -lt $subscriptions.Count; $i++) { + Write-Host " [$i] $($subscriptions[$i].Name) ($($subscriptions[$i].Id))" + } + + do { + $selection = Read-Host "Enter subscription number [0-$($subscriptions.Count - 1)]" + $selectionIndex = $null + if ([int]::TryParse($selection, [ref]$selectionIndex) -and + $selectionIndex -ge 0 -and $selectionIndex -lt $subscriptions.Count) { + return $subscriptions[$selectionIndex] + } + Write-Log "Invalid selection. Please enter a number between 0 and $($subscriptions.Count - 1)." -Level Warning + } while ($true) + } + catch { + throw "Failed to get subscription information: $($_.Exception.Message)" + } +} + +function Confirm-Deployment { + param( + [object]$Subscription, + [string[]]$EmailRecipients, + [string]$NotificationEmail, + [string]$DeploymentName + ) + + if ($Force) { + return $true + } + + Write-Host "" + Write-Log "Deployment summary:" -Level Info + Write-Host " Subscription: $($Subscription.Name) ($($Subscription.Id))" + Write-Host " Deployment name: $DeploymentName" + Write-Host " Email recipients: $($EmailRecipients -join ', ')" + Write-Host " Notification email: $NotificationEmail" + Write-Host "" + + $confirmation = Read-Host "Do you want to proceed with this deployment? (y/N)" + return ($confirmation -eq 'y' -or $confirmation -eq 'Y') +} + +#endregion + +#region Main Script + +try { + Write-Log "Starting anomaly alert deployment script..." -Level Info + + # Check if Azure PowerShell is available + if (-not (Get-Module -Name Az.Accounts -ListAvailable)) { + throw "Azure PowerShell module (Az) isn't installed. Please install it using: Install-Module -Name Az" + } + + # Check Azure connection + if (-not (Test-AzureConnection)) { + Write-Log "Not connected to Azure. Please run 'Connect-AzAccount' first." -Level Error + throw "Azure authentication required" + } + + Write-Log "Azure connection verified." -Level Success + + # Get target subscription + $targetSubscription = Get-SubscriptionSelection -TargetSubscriptionId $SubscriptionId + Write-Log "Selected subscription: $($targetSubscription.Name)" -Level Success + + # Set the subscription context + $null = Set-AzContext -SubscriptionId $targetSubscription.Id + + # Generate deployment name if not provided + if (-not $DeploymentName) { + $timestamp = Get-Date -Format "yyyyMMddHHmmss" + $DeploymentName = "anomaly-alert-$timestamp" + } + + # Get the script directory and template path + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + $templateFile = Join-Path $scriptDir "anomaly-alert.bicep" + + # Verify template file exists + if (-not (Test-Path $templateFile)) { + throw "Bicep template file not found: $templateFile" + } + + Write-Log "Template file found: $templateFile" -Level Success + + # Validate email addresses + $emailRegex = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + foreach ($email in $EmailRecipients) { + if ($email -notmatch $emailRegex) { + throw "Invalid email address format: $email" + } + } + + if ($NotificationEmail -notmatch $emailRegex) { + throw "Invalid notification email address format: $NotificationEmail" + } + + Write-Log "Email addresses validated." -Level Success + + # Prepare deployment parameters + $deploymentParams = @{ + emailRecipients = $EmailRecipients + notificationEmail = $NotificationEmail + } + + # Show deployment summary and get confirmation + if (-not (Confirm-Deployment -Subscription $targetSubscription -EmailRecipients $EmailRecipients -NotificationEmail $NotificationEmail -DeploymentName $DeploymentName)) { + Write-Log "Deployment cancelled by user." -Level Warning + return + } + + # Prepare deployment splat + $deploymentSplat = @{ + Name = $DeploymentName + Location = $Location + TemplateFile = $templateFile + TemplateParameterObject = $deploymentParams + } + + if ($WhatIfPreference) { + $deploymentSplat.Add('WhatIf', $true) + Write-Log "Running deployment validation (What-If)..." -Level Info + } else { + Write-Log "Starting deployment..." -Level Info + } + + # Execute deployment + if ($PSCmdlet.ShouldProcess($targetSubscription.Name, "Deploy anomaly alert")) { + if ($Quiet) { + $deployment = New-AzSubscriptionDeployment @deploymentSplat + } else { + $deployment = New-AzSubscriptionDeployment @deploymentSplat -Verbose + } + + if (-not $WhatIfPreference) { + if ($deployment.ProvisioningState -eq 'Succeeded') { + Write-Log "Deployment completed successfully!" -Level Success + Write-Host "" + Write-Log "Deployment details:" -Level Info + Write-Host " Deployment name: $($deployment.DeploymentName)" + Write-Host " Resource ID: $($deployment.Id)" + Write-Host " Status: $($deployment.ProvisioningState)" + Write-Host " Timestamp: $($deployment.Timestamp)" + Write-Host "" + Write-Log "Anomaly alert has been configured and will monitor cost anomalies for this subscription." -Level Info + Write-Log "Notifications will be sent to: $($EmailRecipients -join ', ')" -Level Info + } else { + Write-Log "Deployment failed with status: $($deployment.ProvisioningState)" -Level Error + if ($deployment.Error) { + Write-Log "Error details: $($deployment.Error.Message)" -Level Error + } + throw "Deployment failed" + } + } else { + Write-Log "What-If operation completed. Review the output above." -Level Success + } + } +} +catch { + Write-Log "Script execution failed: $($_.Exception.Message)" -Level Error + Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level Error + exit 1 +} +finally { + Write-Log "Script execution completed." -Level Info +} + +#endregion diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/Deploy-BulkALZ.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/Deploy-BulkALZ.ps1 new file mode 100755 index 000000000..890b7297b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/Deploy-BulkALZ.ps1 @@ -0,0 +1,168 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Deploy anomaly alerts to all subscriptions in a management group. + +.DESCRIPTION + Deploy anomaly alerts to all subscriptions in the specified management group with + intelligent pagination support for large enterprise environments. The script uses + Azure Resource Graph to discover subscriptions and deploys alerts in batches. + +.PARAMETER TenantId + Azure tenant ID to connect to. + +.PARAMETER ManagementGroup + Management group name to query for subscriptions. + +.PARAMETER EmailRecipients + Array of email addresses that'll receive anomaly notifications. + +.PARAMETER NotificationEmail + Primary notification email address for alerts. + +.PARAMETER WhatIf + Preview changes without deploying. + +.PARAMETER Quiet + Suppress Azure PowerShell warning messages for cleaner output. + +.EXAMPLE + ./Deploy-BulkALZ.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -ManagementGroup "ALZ" -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" + + Deploy anomaly alerts to all subscriptions in the ALZ management group. + +.EXAMPLE + ./Deploy-BulkALZ.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -ManagementGroup "ALZ" -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" -WhatIf + + Preview what would be deployed without making actual changes. + +.EXAMPLE + ./Deploy-BulkALZ.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -ManagementGroup "ALZ" -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" -Quiet + + Deploy with minimal output for automated scenarios. + +.NOTES + - Requires Azure PowerShell and Azure Resource Graph modules + - Supports intelligent pagination for thousands of subscriptions + - Automatically handles Azure Resource Graph's 1,000 result limit + - User must have appropriate permissions across target subscriptions +#> + +param( + [Parameter(Mandatory=$true)] + [string[]]$EmailRecipients, + + [Parameter(Mandatory=$true)] + [string]$NotificationEmail, + + [Parameter(Mandatory=$true)] + [string]$TenantId, + + [Parameter(Mandatory=$true)] + [string]$ManagementGroup, + + [switch]$WhatIf, + + [switch]$Quiet +) + +Write-Host "=== Bulk anomaly alert deployment ===" -ForegroundColor Cyan +Write-Host "Target: $ManagementGroup management group" -ForegroundColor Yellow +Write-Host "Tenant: $TenantId" -ForegroundColor Yellow +Write-Host "Mode: $(if ($WhatIf) { 'What-if (preview)' } else { 'Deploy' })" -ForegroundColor Yellow + +# Suppress Azure PowerShell warnings if Quiet mode is enabled +if ($Quiet) { + $WarningPreference = 'SilentlyContinue' +} + +# Connect to target tenant +Write-Host "Connecting to tenant..." -ForegroundColor Yellow +Connect-AzAccount -TenantId $tenantId + +# Get subscriptions +Write-Host "Finding subscriptions in $ManagementGroup management group..." -ForegroundColor Yellow + +# Handle pagination for large result sets +$allSubscriptions = @() +$skip = 0 +$pageSize = 1000 + +do { + Write-Host "Querying subscriptions (page $([math]::Floor($skip / $pageSize) + 1))..." -ForegroundColor Gray + + $query = "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | where properties.state == 'Enabled' | project subscriptionId, name" + + if ($skip -eq 0) { + $pageResults = Search-AzGraph -Query $query -ManagementGroup $managementGroup -First $pageSize + } else { + $pageResults = Search-AzGraph -Query $query -ManagementGroup $managementGroup -First $pageSize -Skip $skip + } + + if ($pageResults) { + $allSubscriptions += $pageResults + $skip += $pageResults.Count + Write-Host "Found $($pageResults.Count) subscriptions in this page (total: $($allSubscriptions.Count))" -ForegroundColor Gray + } + + # Continue if we got a full page (indicating there might be more) +} while ($pageResults -and $pageResults.Count -eq $pageSize) + +$subscriptions = $allSubscriptions +Write-Host "Total subscriptions found: $($subscriptions.Count)" -ForegroundColor Green + +if (!$subscriptions -or $subscriptions.Count -eq 0) { + Write-Host "No subscriptions found!" -ForegroundColor Red + exit 1 +} + +Write-Host "Subscriptions to deploy to:" -ForegroundColor Green +foreach ($sub in $subscriptions | Select-Object -First 10) { + Write-Host " - $($sub.name)" -ForegroundColor White +} + +if ($subscriptions.Count -gt 10) { + Write-Host " ... and $($subscriptions.Count - 10) more" -ForegroundColor Gray +} + +# Confirm deployment +if (!$WhatIf) { + Write-Host "" + $confirm = Read-Host "Deploy to all subscriptions? (y/N)" + if ($confirm -ne 'y' -and $confirm -ne 'Y') { + Write-Host "Cancelled." -ForegroundColor Yellow + exit 0 + } +} + +# Deploy to each subscription +Write-Host "" +Write-Host "Starting deployment..." -ForegroundColor Green + +foreach ($sub in $subscriptions) { + Write-Host "Deploying to: $($sub.name)" -ForegroundColor Cyan + + try { + if ($WhatIf) { + if ($Quiet) { + ./Deploy-AnomalyAlert.ps1 -SubscriptionId $sub.subscriptionId -EmailRecipients $EmailRecipients -NotificationEmail $NotificationEmail -WhatIf -Force -Quiet -WarningAction SilentlyContinue + } else { + ./Deploy-AnomalyAlert.ps1 -SubscriptionId $sub.subscriptionId -EmailRecipients $EmailRecipients -NotificationEmail $NotificationEmail -WhatIf -Force -Quiet + } + } else { + if ($Quiet) { + ./Deploy-AnomalyAlert.ps1 -SubscriptionId $sub.subscriptionId -EmailRecipients $EmailRecipients -NotificationEmail $NotificationEmail -Force -Quiet -WarningAction SilentlyContinue + } else { + ./Deploy-AnomalyAlert.ps1 -SubscriptionId $sub.subscriptionId -EmailRecipients $EmailRecipients -NotificationEmail $NotificationEmail -Force -Quiet + } + } + Write-Host "✅ Success: $($sub.name)" -ForegroundColor Green + } + catch { + Write-Host "❌ Failed: $($sub.name) - $($_.Exception.Message)" -ForegroundColor Red + } +} + +Write-Host "" +Write-Host "Deployment complete!" -ForegroundColor Green diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/README.md new file mode 100644 index 000000000..023a3d55e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/README.md @@ -0,0 +1,436 @@ +# Anomaly alert deployment + +Deploy cost anomaly alerts across your Azure subscriptions at enterprise scale. + +## Table of contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Deployment scripts](#deployment-scripts) + - [Individual subscription deployment](#deploy-anomalyalertps1-individual-subscriptions) + - [Enterprise bulk deployment](#deploy-bulkalzps1-enterprise-bulk-deployment) +- [Quick start guide](#quick-start-guide) + - [Single subscription](#single-subscription-deployment) + - [Enterprise bulk deployment](#enterprise-bulk-deployment) +- [Parameters reference](#parameters-reference) + - [Deploy-AnomalyAlert.ps1 parameters](#deploy-anomalyalertps1-parameters) + - [Deploy-BulkALZ.ps1 parameters](#deploy-bulkalzps1-parameters) +- [Enterprise-scale features](#enterprise-scale-features) + - [Pagination system](#pagination-system) + - [Performance optimization](#performance-optimization) +- [Advanced usage](#advanced-usage) + - [Azure CLI deployment](#azure-cli-deployment) + - [Validation and what-if](#validation-and-what-if) + - [Custom bulk deployment examples](#custom-bulk-deployment-examples) +- [Troubleshooting](#troubleshooting) +- [Security considerations](#security-considerations) +- [Technical notes](#technical-notes) + +## Overview + +### What's included + +- `anomaly-alert.bicep` - Bicep template that creates subscription-level scheduled actions for anomaly detection +- `Deploy-AnomalyAlert.ps1` - PowerShell script for individual subscription deployments +- `Deploy-BulkALZ.ps1` - Enterprise-scale bulk deployment script with intelligent pagination +- `anomaly-alert.parameters.json` - Sample parameters file for deployment +- `README.md` - This documentation + +### What gets deployed + +This deployment creates: +- **Cost management scheduled action** - Named "AnomalyAlert" +- **Anomaly detection** - Monitors cost anomalies at the subscription level +- **Email notifications** - Sends alerts to your specified email addresses when anomalies are detected + +## Prerequisites + +1. **Azure PowerShell module** - Install the Az PowerShell module: + ```powershell + Install-Module -Name Az -Force -AllowClobber + ``` + +2. **Azure Resource Graph module** (for bulk operations) - Required for querying multiple subscriptions: + ```powershell + Install-Module -Name Az.ResourceGraph -Force -AllowClobber + ``` + +3. **Azure authentication** - Sign in to Azure: + ```powershell + Connect-AzAccount + ``` + +4. **Permissions** - You'll need Contributor or Owner role on target subscriptions. + +## Deployment scripts + +Choose the right script for your deployment scenario: + +### Deploy-AnomalyAlert.ps1 (individual subscriptions) + +**Best for**: Single subscription deployments, CI/CD pipelines, testing environments + +**Key features**: +- Interactive subscription selection +- Parameter validation and error handling +- Email address format validation +- What-if deployment support +- Color-coded logging with timestamps +- Force deployment option for automation +- Quiet mode for clean output +- Detailed help documentation + +### Deploy-BulkALZ.ps1 (enterprise bulk deployment) + +**Best for**: Large-scale enterprise deployments, management group-wide rollouts + +**Key features**: +- Automated deployment across thousands of subscriptions +- Azure Resource Graph integration for subscription discovery +- Management group targeting with intelligent filtering +- **Enterprise-grade pagination** for handling massive subscription sets +- **Automatic page size optimization** (1000 subscriptions per query) +- **Progressive discovery** with real-time progress reporting +- Performance optimization with quiet deployment mode +- Built-in error handling and rollback safety +- Confirmation prompts for enterprise safety + +## Quick start guide + +### Single subscription deployment + +#### Option 1: interactive subscription selection +```powershell +./Deploy-AnomalyAlert.ps1 -EmailRecipients @("admin@company.com", "finance@company.com") -NotificationEmail "alerts@company.com" +``` + +#### Option 2: specify target subscription +```powershell +./Deploy-AnomalyAlert.ps1 -SubscriptionId "12345678-1234-1234-1234-123456789012" -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" +``` + +#### Option 3: what-if preview +```powershell +./Deploy-AnomalyAlert.ps1 -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" -WhatIf +``` + +#### Option 4: automated/silent deployment +```powershell +./Deploy-AnomalyAlert.ps1 -SubscriptionId "12345678-1234-1234-1234-123456789012" -EmailRecipients @("admin@company.com") -NotificationEmail "alerts@company.com" -Force -Quiet +``` + +### Enterprise bulk deployment + +#### Option 1: deploy to management group +```powershell +./Deploy-BulkALZ.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -ManagementGroup "ALZ" -EmailRecipients @("finops@company.com", "alerts@company.com") -NotificationEmail "alerts@company.com" +``` + +#### Option 2: bulk what-if preview +```powershell +./Deploy-BulkALZ.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -ManagementGroup "ALZ" -EmailRecipients @("finops@company.com") -NotificationEmail "alerts@company.com" -WhatIf +``` + +#### Option 3: quiet enterprise deployment +```powershell +./Deploy-BulkALZ.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -ManagementGroup "ALZ" -EmailRecipients @("finops@company.com") -NotificationEmail "alerts@company.com" -Quiet +``` + +## Parameters reference + +### Deploy-AnomalyAlert.ps1 parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `EmailRecipients` | Yes | Array of email addresses that'll receive anomaly notifications | +| `NotificationEmail` | Yes | Primary email address for the anomaly alert system | +| `SubscriptionId` | No | Target subscription ID (if not provided, script prompts for selection) | +| `DeploymentName` | No | Custom deployment name (auto-generated if not provided) | +| `Location` | No | Azure region for deployment metadata (defaults to 'West US') | +| `Force` | No | Skip confirmation prompts | +| `Quiet` | No | Suppress verbose output and enable quiet deployment mode | +| `WhatIf` | No | Preview deployment without making changes | + +### Deploy-BulkALZ.ps1 parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `TenantId` | Yes | Azure tenant ID for authentication | +| `ManagementGroup` | Yes | Management group name to query for subscriptions | +| `EmailRecipients` | Yes | Array of email addresses that'll receive anomaly notifications | +| `NotificationEmail` | Yes | Primary email address for the anomaly alert system | +| `WhatIf` | No | Preview deployment without making changes | +| `Quiet` | No | Suppress Azure PowerShell warnings and enable quiet deployment mode | + +## Enterprise-scale features + +### Pagination system + +The `Deploy-BulkALZ.ps1` script includes intelligent pagination designed for large enterprise environments with thousands of subscriptions. + +#### How pagination works + +The script uses Azure Resource Graph's pagination features to automatically discover and process subscriptions in optimized batches: + +1. **Automatic page size optimization** - Queries subscriptions in batches of 1,000 per page (optimal for Azure Resource Graph performance) +2. **Progressive discovery** - Automatically detects when more subscriptions exist and continues querying +3. **Real-time progress reporting** - Shows current page number and running totals as subscriptions are discovered +4. **Memory efficient** - Processes large subscription sets without overwhelming system memory + +#### Implementation details + +```powershell +# The script automatically handles pagination like this: +$allSubscriptions = @() +$skip = 0 +$pageSize = 1000 + +do { + Write-Host "Querying subscriptions (page $([math]::Floor($skip / $pageSize) + 1))..." -ForegroundColor Gray + + $query = "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | where properties.state == 'Enabled' | project subscriptionId, name" + + if ($skip -eq 0) { + $pageResults = Search-AzGraph -Query $query -ManagementGroup $managementGroup -First $pageSize + } else { + $pageResults = Search-AzGraph -Query $query -ManagementGroup $managementGroup -First $pageSize -Skip $skip + } + + if ($pageResults) { + $allSubscriptions += $pageResults + $skip += $pageResults.Count + Write-Host "Found $($pageResults.Count) subscriptions in this page (total: $($allSubscriptions.Count))" -ForegroundColor Gray + } + +# Continue if we got a full page (indicating there might be more) +} while ($pageResults -and $pageResults.Count -eq $pageSize) +``` + +### Performance optimization + +#### Sample output for large environments + +``` +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 +``` + +#### Enterprise environment benefits + +- **Scalability** - Handles environments with 5,000+ subscriptions without performance degradation +- **Reliability** - Prevents timeout issues that can occur when querying large datasets +- **Visibility** - Provides clear progress reporting during the discovery phase +- **Efficiency** - Optimizes network requests by using maximum practical page sizes + +## Advanced usage + +### Azure CLI deployment + +Deploy using Azure CLI with the parameters file: + +```bash +az deployment sub create \ + --name "anomaly-alert-deployment" \ + --location "West US" \ + --template-file "anomaly-alert.bicep" \ + --parameters "@anomaly-alert.parameters.json" +``` + +Or using Azure PowerShell with parameters file: + +```powershell +New-AzSubscriptionDeployment -Name "anomaly-alert-deployment" -Location "West US" -TemplateFile "anomaly-alert.bicep" -TemplateParameterFile "anomaly-alert.parameters.json" +``` + +### Validation and what-if + +Before deploying, validate your template using these commands: + +#### Azure CLI validation +```bash +az deployment sub validate \ + --location "West US" \ + --template-file "anomaly-alert.bicep" \ + --parameters "@anomaly-alert.parameters.json" +``` + +#### Azure PowerShell validation +```powershell +Test-AzSubscriptionDeployment -Location "West US" -TemplateFile "anomaly-alert.bicep" -TemplateParameterFile "anomaly-alert.parameters.json" +``` + +#### What-if analysis (Azure CLI) +```bash +az deployment sub what-if \ + --location "West US" \ + --template-file "anomaly-alert.bicep" \ + --parameters "@anomaly-alert.parameters.json" +``` + +#### What-if analysis (Azure PowerShell) +```powershell +New-AzSubscriptionDeployment -Name "anomaly-alert-whatif" -Location "West US" -TemplateFile "anomaly-alert.bicep" -TemplateParameterFile "anomaly-alert.parameters.json" -WhatIf +``` + +### Custom bulk deployment examples + +#### Example 1: deploy to all subscriptions in a management group + +```powershell +# Set your management group name +$managementGroupName = "Production" +$emailRecipients = @("finops@company.com", "alerts@company.com") +$notificationEmail = "alerts@company.com" + +# Query subscriptions in the management group +$subscriptions = Search-AzGraph -Query "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, name" -ManagementGroup $managementGroupName + +Write-Host "Found $($subscriptions.Count) subscriptions in management group '$managementGroupName'" + +# Deploy to each subscription +foreach ($sub in $subscriptions) { + try { + Write-Host "Deploying anomaly alert to: $($sub.name) ($($sub.subscriptionId))" -ForegroundColor Cyan + ./Deploy-AnomalyAlert.ps1 -SubscriptionId $sub.subscriptionId -EmailRecipients $emailRecipients -NotificationEmail $notificationEmail -Force + Write-Host "✅ Successfully deployed to $($sub.name)" -ForegroundColor Green + } + catch { + Write-Host "❌ Failed to deploy to $($sub.name): $($_.Exception.Message)" -ForegroundColor Red + } +} +``` + +#### Example 2: deploy to filtered subscriptions + +```powershell +# Deploy only to production subscriptions (filtering by name pattern) +$emailRecipients = @("finops@company.com") +$notificationEmail = "alerts@company.com" + +# Query subscriptions with name filtering +$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 production subscription: $($sub.name)" -ForegroundColor Yellow + ./Deploy-AnomalyAlert.ps1 -SubscriptionId $sub.subscriptionId -EmailRecipients $emailRecipients -NotificationEmail $notificationEmail -Force +} +``` + +#### Example 3: deploy with what-if validation first + +```powershell +# Validate deployments across multiple subscriptions before actual deployment +$managementGroupName = "Development" +$subscriptions = Search-AzGraph -Query "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, name" -ManagementGroup $managementGroupName + +# First pass: What-if validation +Write-Host "=== VALIDATION PHASE ===" -ForegroundColor Magenta +foreach ($sub in $subscriptions) { + Write-Host "Validating deployment for: $($sub.name)" -ForegroundColor Cyan + ./Deploy-AnomalyAlert.ps1 -SubscriptionId $sub.subscriptionId -EmailRecipients @("test@company.com") -NotificationEmail "test@company.com" -WhatIf +} + +# Prompt for confirmation +$confirm = Read-Host "Do you want to proceed with actual deployment? (y/N)" +if ($confirm -eq 'y' -or $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 query reference + +Here are useful Azure Resource Graph queries for subscription discovery: + +| Query purpose | KQL query | +|---------------|-----------| +| All accessible subscriptions | `ResourceContainers \| where type =~ 'microsoft.resources/subscriptions' \| project subscriptionId, name` | +| Subscriptions in a management group | Use `-ManagementGroup` parameter with the above query | +| Subscriptions with name filter | `ResourceContainers \| where type =~ 'microsoft.resources/subscriptions' \| where name contains 'keyword' \| project subscriptionId, name` | +| Enabled subscriptions only | `ResourceContainers \| where type =~ 'microsoft.resources/subscriptions' \| where properties.state == 'Enabled' \| project subscriptionId, name` | + +**Pagination note**: Azure Resource Graph returns a maximum of 1,000 results per query. The `Deploy-BulkALZ.ps1` script automatically handles this by implementing pagination with the `-First` and `-Skip` parameters. Manual queries may need pagination for large result sets: + +```powershell +# Example of manual pagination +$allResults = @() +$skip = 0 +$pageSize = 1000 + +do { + $pageResults = Search-AzGraph -Query $query -First $pageSize -Skip $skip + $allResults += $pageResults + $skip += $pageResults.Count +} while ($pageResults.Count -eq $pageSize) +``` + +## Troubleshooting + +### Common issues and solutions + +1. **Permission errors** - Ensure you have Contributor or Owner role on the target subscription +2. **Authentication issues** - Run `Connect-AzAccount` and verify you're signed into the correct tenant +3. **Module not found** - Install the Azure PowerShell module: `Install-Module -Name Az` +4. **Location conflicts** - If you get an "Invalid deployment location" error, the subscription may already have an anomaly alert in a different region. The scripts default to "West US" which works for most cases. +5. **Template validation errors** - Ensure the Bicep template includes `targetScope = 'subscription'` for subscription-level deployments +6. **Resource Graph module missing** - For bulk operations, install: `Install-Module -Name Az.ResourceGraph` +7. **Management group access** - Ensure you have Reader role or higher on management groups when using `-ManagementGroup` parameter +8. **Query timeout** - Large management groups may cause query timeouts; the pagination system automatically handles this by processing subscriptions in batches of 1,000 +9. **Memory usage** - The pagination system efficiently manages memory when processing thousands of subscriptions +10. **Azure Resource Graph limits** - The script respects Azure Resource Graph's rate limits and pagination requirements automatically +11. **Rate limiting** - Azure Resource Manager may throttle requests during bulk deployments; add delays between deployments if needed +12. **Existing anomaly alerts** - Subscriptions may already have anomaly alerts configured; the script will update existing ones +13. **Verbose output issues** - Use the `-Quiet` parameter to suppress verbose Azure PowerShell warnings and deployment monitoring delays + +### Best practices for enterprise deployment + +1. **Test first** - Always use what-if validation or deploy to a test subscription first +2. **Error handling** - Wrap deployments in try-catch blocks to handle failures gracefully +3. **Logging** - Log successful and failed deployments for audit purposes +4. **Use enterprise script for scale** - For deployments involving 100+ subscriptions, use `Deploy-BulkALZ.ps1` which includes automatic pagination +5. **Monitor progress** - The pagination system provides real-time progress updates during subscription discovery +6. **Permissions** - Verify you have appropriate permissions before starting bulk operations +7. **Resource Graph optimization** - The built-in pagination handles Azure Resource Graph's 1,000 result limit automatically +8. **Performance** - Use `-Quiet` parameter for faster deployments without verbose progress output + +### Getting help + +The PowerShell script includes detailed help documentation: + +```powershell +Get-Help ./Deploy-AnomalyAlert.ps1 -Full +``` + +For the bulk script, basic usage is shown with: + +```powershell +Get-Help ./Deploy-BulkALZ.ps1 +``` + +## Security considerations + +- Email addresses are validated for proper format before deployment +- The script uses Azure PowerShell's built-in authentication mechanisms +- No credentials are stored or logged in the script +- All Azure interactions use the authenticated user's permissions + +## Technical notes + +- The Bicep template includes `targetScope = 'subscription'` for proper subscription-level deployment +- The anomaly alert uses the public Bicep registry module `br/public:cost/subscription-scheduled-action:1.0.2` +- Alerts are configured for "InsightAlert" type, which specifically monitors for cost anomalies +- The display name is set to "My anomaly check" but can be customized by modifying the Bicep template +- The scheduled action operates at the subscription level +- Default deployment location is "West US" to avoid conflicts with existing deployments +- Both scripts support email address validation and proper error handling diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/anomaly-alert.bicep b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/anomaly-alert.bicep new file mode 100644 index 000000000..2fe00e2cf --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/anomaly-alert.bicep @@ -0,0 +1,17 @@ +targetScope = 'subscription' + +param emailRecipients array +param notificationEmail string + + +module anomalyAlert 'br/public:cost/subscription-scheduled-action:1.0.2' = { + name: 'anomalyAlert' + scope: subscription() + params: { + name: 'AnomalyAlert' + kind: 'InsightAlert' + displayName: 'Cost anomaly alert' + emailRecipients: emailRecipients + notificationEmail: notificationEmail + } +} diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/anomaly-alert.parameters.json b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/anomaly-alert.parameters.json new file mode 100644 index 000000000..9a83cccd1 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/anomaly-alerts/anomaly-alert.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "emailRecipients": { + "value": [ + "admin@company.com", + "finance@company.com" + ] + }, + "notificationEmail": { + "value": "admin@company.com" + } + } +} diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/Deploy-Budget.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/Deploy-Budget.ps1 new file mode 100644 index 000000000..392118481 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/Deploy-Budget.ps1 @@ -0,0 +1,449 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Deploy budget configuration to an Azure subscription. + +.DESCRIPTION + This script deploys the budget Bicep template to create a subscription-level + cost budget with notification thresholds. The script handles subscription + validation, parameter collection, and deployment with proper error handling. + +.PARAMETER SubscriptionId + The target Azure subscription ID where the budget will be deployed. + If not provided, the script will prompt for selection from available subscriptions. + +.PARAMETER BudgetName + Name of the budget. Defaults to 'SubscriptionBudget'. + +.PARAMETER Amount + The total budget amount in the billing currency. Defaults to 1000. + +.PARAMETER TimeGrain + Budget reset period: Monthly, Quarterly, or Annually. Defaults to Monthly. + +.PARAMETER StartDate + Budget start date in YYYY-MM-DD format. Must be first of month. + Defaults to first of next month. + +.PARAMETER EndDate + Budget end date in YYYY-MM-DD format. Defaults to one year from start date. + +.PARAMETER ContactEmails + Array of email addresses that'll receive budget notifications. + +.PARAMETER ContactRoles + Array of Azure roles to notify. Defaults to Owner and Contributor. + +.PARAMETER FirstThreshold + First notification threshold percentage. Defaults to 50. + +.PARAMETER SecondThreshold + Second notification threshold percentage. Defaults to 75. + +.PARAMETER ThirdThreshold + Third notification threshold percentage. Defaults to 90. + +.PARAMETER ForecastedThreshold + Forecasted cost notification threshold percentage. Defaults to 100. + +.PARAMETER DeploymentName + Optional custom name for the deployment. If not provided, a timestamped name will be generated. + +.PARAMETER Location + Azure region for the deployment. Defaults to 'West US'. + +.PARAMETER Force + Skip confirmation prompts and proceed with deployment automatically. + +.PARAMETER Quiet + Suppress verbose deployment progress output. + +.EXAMPLE + ./Deploy-Budget.ps1 -ContactEmails @("admin@company.com") -Amount 5000 + + Deploy budget with interactive subscription selection and $5000 monthly budget. + +.EXAMPLE + ./Deploy-Budget.ps1 -SubscriptionId "12345678-1234-1234-1234-123456789012" -ContactEmails @("admin@company.com") -Amount 10000 -TimeGrain Quarterly -WhatIf + + Preview quarterly budget deployment without making changes. + +.EXAMPLE + ./Deploy-Budget.ps1 -ContactEmails @("admin@company.com") -Amount 2000 -Force + + Deploy without confirmation prompts. + +.NOTES + - Requires Azure PowerShell module (Az) + - User must be authenticated to Azure (Connect-AzAccount) + - User must have appropriate permissions to create subscription-level resources + - Start date must be the first of the month +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $false)] + [ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')] + [string]$SubscriptionId, + + [Parameter(Mandatory = $false)] + [string]$BudgetName = 'SubscriptionBudget', + + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$Amount, + + [Parameter(Mandatory = $false)] + [ValidateSet('Monthly', 'Quarterly', 'Annually')] + [string]$TimeGrain = 'Monthly', + + [Parameter(Mandatory = $false)] + [string]$StartDate, + + [Parameter(Mandatory = $false)] + [string]$EndDate, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$ContactEmails, + + [Parameter(Mandatory = $false)] + [string[]]$ContactRoles = @('Owner', 'Contributor'), + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 1000)] + [int]$FirstThreshold = 50, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 1000)] + [int]$SecondThreshold = 75, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 1000)] + [int]$ThirdThreshold = 90, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 1000)] + [int]$ForecastedThreshold = 100, + + [Parameter(Mandatory = $false)] + [string]$DeploymentName, + + [Parameter(Mandatory = $false)] + [string]$Location = 'West US', + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [switch]$Quiet +) + +#region Helper Functions + +function Write-Log { + param( + [string]$Message, + [ValidateSet('Info', 'Warning', 'Error', 'Success')] + [string]$Level = 'Info' + ) + + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $colors = @{ + 'Info' = 'White' + 'Warning' = 'Yellow' + 'Error' = 'Red' + 'Success' = 'Green' + } + + Write-Host "[$timestamp] " -NoNewline -ForegroundColor Gray + Write-Host $Message -ForegroundColor $colors[$Level] +} + +function Test-AzureConnection { + try { + $context = Get-AzContext + if (-not $context) { + return $false + } + return $true + } + catch { + return $false + } +} + +function Get-SubscriptionSelection { + param([string]$TargetSubscriptionId) + + try { + $subscriptions = Get-AzSubscription | Sort-Object Name + + if ($subscriptions.Count -eq 0) { + throw "No subscriptions found. Check if you are logged in with the correct Azure account." + } + + if ($TargetSubscriptionId) { + $selectedSub = $subscriptions | Where-Object { $_.Id -eq $TargetSubscriptionId } + if (-not $selectedSub) { + throw "Subscription with ID '$TargetSubscriptionId' not found or not accessible." + } + return $selectedSub + } + + if ($subscriptions.Count -eq 1) { + Write-Log "Using the only available subscription: $($subscriptions[0].Name)" -Level Info + return $subscriptions[0] + } + + Write-Log "Multiple subscriptions found. Please select:" -Level Info + for ($i = 0; $i -lt $subscriptions.Count; $i++) { + Write-Host " [$i] $($subscriptions[$i].Name) ($($subscriptions[$i].Id))" + } + + do { + $selection = Read-Host "Enter subscription number [0-$($subscriptions.Count - 1)]" + $selectionIndex = $null + if ([int]::TryParse($selection, [ref]$selectionIndex) -and + $selectionIndex -ge 0 -and $selectionIndex -lt $subscriptions.Count) { + return $subscriptions[$selectionIndex] + } + Write-Log "Invalid selection. Please enter a number between 0 and $($subscriptions.Count - 1)." -Level Warning + } while ($true) + } + catch { + throw "Failed to get subscription information: $($_.Exception.Message)" + } +} + +function Get-DefaultStartDate { + # Returns first of next month in YYYY-MM-DD format + $today = Get-Date + $nextMonth = $today.AddMonths(1) + $firstOfMonth = Get-Date -Year $nextMonth.Year -Month $nextMonth.Month -Day 1 + return $firstOfMonth.ToString('yyyy-MM-dd') +} + +function Get-DefaultEndDate { + param([string]$StartDate) + # Returns one year from start date + $start = [DateTime]::Parse($StartDate) + $end = $start.AddYears(1) + return $end.ToString('yyyy-MM-dd') +} + +function Confirm-Deployment { + param( + [object]$Subscription, + [string]$BudgetName, + [int]$Amount, + [string]$TimeGrain, + [string]$StartDate, + [string]$EndDate, + [string[]]$ContactEmails, + [string]$DeploymentName + ) + + if ($Force) { + return $true + } + + Write-Host "" + Write-Log "Deployment summary:" -Level Info + Write-Host " Subscription: $($Subscription.Name) ($($Subscription.Id))" + Write-Host " Budget name: $BudgetName" + Write-Host " Amount: $Amount ($TimeGrain)" + Write-Host " Period: $StartDate to $EndDate" + Write-Host " Email recipients: $($ContactEmails -join ', ')" + Write-Host " Deployment name: $DeploymentName" + Write-Host "" + + $confirmation = Read-Host "Do you want to proceed with this deployment? (y/N)" + return ($confirmation -eq 'y' -or $confirmation -eq 'Y') +} + +#endregion + +#region Main Script + +try { + Write-Log "Starting budget deployment script..." -Level Info + + # Check if Azure PowerShell is available + if (-not (Get-Module -Name Az.Accounts -ListAvailable)) { + throw "Azure PowerShell module (Az) isn't installed. Please install it using: Install-Module -Name Az" + } + + # Check Azure connection + if (-not (Test-AzureConnection)) { + Write-Log "Not connected to Azure. Please run 'Connect-AzAccount' first." -Level Error + throw "Azure authentication required" + } + + Write-Log "Azure connection verified." -Level Success + + # Get target subscription + $targetSubscription = Get-SubscriptionSelection -TargetSubscriptionId $SubscriptionId + Write-Log "Selected subscription: $($targetSubscription.Name)" -Level Success + + # Set the subscription context + $null = Set-AzContext -SubscriptionId $targetSubscription.Id + + # Determine budget amount: explicit parameter > BudgetAmount tag > default ($10) + $defaultAmount = 10 + if ($PSBoundParameters.ContainsKey('Amount')) { + Write-Log "Using explicitly provided amount: $Amount" -Level Info + } else { + # Check for BudgetAmount tag on subscription (must use Get-AzTag, not Get-AzSubscription) + $tagValue = $null + try { + $subscriptionResourceId = "/subscriptions/$($targetSubscription.Id)" + $tags = Get-AzTag -ResourceId $subscriptionResourceId -ErrorAction SilentlyContinue + if ($tags -and $tags.Properties -and $tags.Properties.TagsProperty) { + $tagValue = $tags.Properties.TagsProperty['BudgetAmount'] + } + } catch { + Write-Log "Could not retrieve subscription tags: $($_.Exception.Message)" -Level Warning + } + + if ($tagValue) { + $parsedAmount = 0 + if ([int]::TryParse($tagValue, [ref]$parsedAmount) -and $parsedAmount -gt 0) { + $Amount = $parsedAmount + Write-Log "Using BudgetAmount tag value: $Amount" -Level Info + } else { + Write-Log "Invalid BudgetAmount tag value '$tagValue', using default: $defaultAmount" -Level Warning + $Amount = $defaultAmount + } + } else { + $Amount = $defaultAmount + Write-Log "No BudgetAmount tag found, using default: $Amount" -Level Info + } + } + + # Set default dates if not provided + if (-not $StartDate) { + $StartDate = Get-DefaultStartDate + Write-Log "Using default start date: $StartDate" -Level Info + } + + if (-not $EndDate) { + $EndDate = Get-DefaultEndDate -StartDate $StartDate + Write-Log "Using default end date: $EndDate" -Level Info + } + + # Validate start date is first of month + $startDateParsed = [DateTime]::Parse($StartDate) + if ($startDateParsed.Day -ne 1) { + throw "Start date must be the first of the month. Got: $StartDate" + } + + # Generate deployment name if not provided + if (-not $DeploymentName) { + $timestamp = Get-Date -Format "yyyyMMddHHmmss" + $DeploymentName = "budget-$timestamp" + } + + # Get the script directory and template path + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + $templateFile = Join-Path $scriptDir "budget.bicep" + + # Verify template file exists + if (-not (Test-Path $templateFile)) { + throw "Bicep template file not found: $templateFile" + } + + Write-Log "Template file found: $templateFile" -Level Success + + # Validate email addresses + $emailRegex = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + foreach ($email in $ContactEmails) { + if ($email -notmatch $emailRegex) { + throw "Invalid email address format: $email" + } + } + + Write-Log "Email addresses validated." -Level Success + + # Prepare deployment parameters + $deploymentParams = @{ + budgetName = $BudgetName + amount = $Amount + timeGrain = $TimeGrain + startDate = $StartDate + endDate = $EndDate + contactEmails = $ContactEmails + contactRoles = $ContactRoles + firstThreshold = $FirstThreshold + secondThreshold = $SecondThreshold + thirdThreshold = $ThirdThreshold + forecastedThreshold = $ForecastedThreshold + } + + # Show deployment summary and get confirmation + if (-not (Confirm-Deployment -Subscription $targetSubscription -BudgetName $BudgetName -Amount $Amount -TimeGrain $TimeGrain -StartDate $StartDate -EndDate $EndDate -ContactEmails $ContactEmails -DeploymentName $DeploymentName)) { + Write-Log "Deployment cancelled by user." -Level Warning + return + } + + # Prepare deployment splat + $deploymentSplat = @{ + Name = $DeploymentName + Location = $Location + TemplateFile = $templateFile + TemplateParameterObject = $deploymentParams + } + + if ($WhatIfPreference) { + Write-Log "Running deployment validation (What-If)..." -Level Info + + # Run What-If deployment preview + if ($Quiet) { + $whatIfResult = New-AzSubscriptionDeployment @deploymentSplat -WhatIf -WhatIfResultFormat FullResourcePayloads 2>&1 + } else { + $whatIfResult = New-AzSubscriptionDeployment @deploymentSplat -WhatIf -WhatIfResultFormat FullResourcePayloads -Verbose 2>&1 + } + + Write-Host $whatIfResult + Write-Log "What-If operation completed." -Level Success + } else { + Write-Log "Starting deployment..." -Level Info + + # Execute actual deployment + if ($Quiet) { + $deployment = New-AzSubscriptionDeployment @deploymentSplat + } else { + $deployment = New-AzSubscriptionDeployment @deploymentSplat -Verbose + } + + if ($deployment.ProvisioningState -eq 'Succeeded') { + Write-Log "Deployment completed successfully!" -Level Success + Write-Host "" + Write-Log "Deployment details:" -Level Info + Write-Host " Deployment name: $($deployment.DeploymentName)" + Write-Host " Budget name: $BudgetName" + Write-Host " Amount: $Amount ($TimeGrain)" + Write-Host " Status: $($deployment.ProvisioningState)" + Write-Host " Timestamp: $($deployment.Timestamp)" + Write-Host "" + Write-Log "Budget has been configured and will monitor costs for this subscription." -Level Info + Write-Log "Notifications will be sent to: $($ContactEmails -join ', ')" -Level Info + } else { + Write-Log "Deployment failed with status: $($deployment.ProvisioningState)" -Level Error + if ($deployment.Error) { + Write-Log "Error details: $($deployment.Error.Message)" -Level Error + } + throw "Deployment failed" + } + } +} +catch { + Write-Log "Script execution failed: $($_.Exception.Message)" -Level Error + Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level Error + exit 1 +} +finally { + Write-Log "Script execution completed." -Level Info +} + +#endregion diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/Deploy-BulkBudgets.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/Deploy-BulkBudgets.ps1 new file mode 100644 index 000000000..cdf9238bf --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/Deploy-BulkBudgets.ps1 @@ -0,0 +1,257 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Deploy budgets to all subscriptions in a management group. + +.DESCRIPTION + Deploy budgets to all subscriptions in the specified management group with + intelligent pagination support for large enterprise environments. The script uses + Azure Resource Graph to discover subscriptions and deploys budgets in batches. + Assumes user is already authenticated via Connect-AzAccount. + +.PARAMETER ManagementGroup + Management group name to query for subscriptions. + +.PARAMETER ContactEmails + Array of email addresses that'll receive budget notifications. + +.PARAMETER BudgetName + Name of the budget. Defaults to 'SubscriptionBudget'. + +.PARAMETER Amount + The total budget amount in the billing currency. Defaults to 1000. + +.PARAMETER TimeGrain + Budget reset period: Monthly, Quarterly, or Annually. Defaults to Monthly. + +.PARAMETER StartDate + Budget start date in YYYY-MM-DD format. Must be first of month. + Defaults to first of next month. + +.PARAMETER EndDate + Budget end date in YYYY-MM-DD format. Defaults to one year from start date. + +.PARAMETER WhatIf + Preview changes without deploying. + +.PARAMETER Quiet + Suppress Azure PowerShell warning messages for cleaner output. + +.EXAMPLE + ./Deploy-BulkBudgets.ps1 -ManagementGroup "ALZ" -ContactEmails @("admin@company.com") -Amount 5000 + + Deploy $5000 monthly budgets to all subscriptions in the ALZ management group. + +.EXAMPLE + ./Deploy-BulkBudgets.ps1 -ManagementGroup "ALZ" -ContactEmails @("admin@company.com") -Amount 10000 -TimeGrain Quarterly -WhatIf + + Preview quarterly budget deployment without making actual changes. + +.EXAMPLE + ./Deploy-BulkBudgets.ps1 -ManagementGroup "ALZ" -ContactEmails @("admin@company.com") -Quiet + + Deploy with minimal output for automated scenarios (uses BudgetAmount tag or $10 default). + +.NOTES + - Requires Azure PowerShell and Azure Resource Graph modules + - Supports intelligent pagination for thousands of subscriptions + - Automatically handles Azure Resource Graph's 1,000 result limit + - User must have appropriate permissions across target subscriptions +#> + +param( + [Parameter(Mandatory=$true)] + [string]$ManagementGroup, + + [Parameter(Mandatory=$true)] + [string[]]$ContactEmails, + + [Parameter(Mandatory=$false)] + [string]$BudgetName = 'SubscriptionBudget', + + [Parameter(Mandatory=$false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$Amount, + + [Parameter(Mandatory=$false)] + [ValidateSet('Monthly', 'Quarterly', 'Annually')] + [string]$TimeGrain = 'Monthly', + + [Parameter(Mandatory=$false)] + [string]$StartDate, + + [Parameter(Mandatory=$false)] + [string]$EndDate, + + [Parameter(Mandatory=$false)] + [string[]]$ContactRoles = @('Owner', 'Contributor'), + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 1000)] + [int]$FirstThreshold = 50, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 1000)] + [int]$SecondThreshold = 75, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 1000)] + [int]$ThirdThreshold = 90, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 1000)] + [int]$ForecastedThreshold = 100, + + [switch]$WhatIf, + + [switch]$Quiet, + + [switch]$Force +) + +# Set default dates if not provided +if (-not $StartDate) { + $today = Get-Date + $nextMonth = $today.AddMonths(1) + $firstOfMonth = Get-Date -Year $nextMonth.Year -Month $nextMonth.Month -Day 1 + $StartDate = $firstOfMonth.ToString('yyyy-MM-dd') +} + +if (-not $EndDate) { + $start = [DateTime]::Parse($StartDate) + $EndDate = $start.AddYears(1).ToString('yyyy-MM-dd') +} + +Write-Host "=== Bulk budget deployment ===" -ForegroundColor Cyan +Write-Host "Target: $ManagementGroup management group" -ForegroundColor Yellow +if ($PSBoundParameters.ContainsKey('Amount')) { + Write-Host "Budget: $Amount ($TimeGrain) - explicit override" -ForegroundColor Yellow +} else { + Write-Host "Budget: Per-subscription BudgetAmount tag, default `$10 ($TimeGrain)" -ForegroundColor Yellow +} +Write-Host "Period: $StartDate to $EndDate" -ForegroundColor Yellow +Write-Host "Mode: $(if ($WhatIf) { 'What-if (preview)' } else { 'Deploy' })" -ForegroundColor Yellow + +# Suppress Azure PowerShell warnings if Quiet mode is enabled +if ($Quiet) { + $WarningPreference = 'SilentlyContinue' +} + +# Verify Azure connection (assume already logged in) +$context = Get-AzContext +if (-not $context) { + Write-Host "Not connected to Azure. Please run 'Connect-AzAccount' first." -ForegroundColor Red + exit 1 +} + +# Get subscriptions +Write-Host "Finding subscriptions in $ManagementGroup management group..." -ForegroundColor Yellow + +# Handle pagination for large result sets +$allSubscriptions = @() +$skip = 0 +$pageSize = 1000 + +do { + Write-Host "Querying subscriptions (page $([math]::Floor($skip / $pageSize) + 1))..." -ForegroundColor Gray + + $query = "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | where properties.state == 'Enabled' | project subscriptionId, name" + + if ($skip -eq 0) { + $pageResults = Search-AzGraph -Query $query -ManagementGroup $ManagementGroup -First $pageSize + } else { + $pageResults = Search-AzGraph -Query $query -ManagementGroup $ManagementGroup -First $pageSize -Skip $skip + } + + if ($pageResults) { + $allSubscriptions += $pageResults + $skip += $pageResults.Count + Write-Host "Found $($pageResults.Count) subscriptions in this page (total: $($allSubscriptions.Count))" -ForegroundColor Gray + } + + # Continue if we got a full page (indicating there might be more) +} while ($pageResults -and $pageResults.Count -eq $pageSize) + +$subscriptions = $allSubscriptions +Write-Host "Total subscriptions found: $($subscriptions.Count)" -ForegroundColor Green + +if (!$subscriptions -or $subscriptions.Count -eq 0) { + Write-Host "No subscriptions found!" -ForegroundColor Red + exit 1 +} + +Write-Host "Subscriptions to deploy to:" -ForegroundColor Green +foreach ($sub in $subscriptions | Select-Object -First 10) { + Write-Host " - $($sub.name)" -ForegroundColor White +} + +if ($subscriptions.Count -gt 10) { + Write-Host " ... and $($subscriptions.Count - 10) more" -ForegroundColor Gray +} + +# Confirm deployment +if (!$WhatIf -and !$Force) { + Write-Host "" + $confirm = Read-Host "Deploy to all subscriptions? (y/N)" + if ($confirm -ne 'y' -and $confirm -ne 'Y') { + Write-Host "Cancelled." -ForegroundColor Yellow + exit 0 + } +} + +# Deploy to each subscription +Write-Host "" +Write-Host "Starting deployment..." -ForegroundColor Green + +$successCount = 0 +$failCount = 0 + +foreach ($sub in $subscriptions) { + Write-Host "Deploying to: $($sub.name)" -ForegroundColor Cyan + + try { + $deployParams = @{ + SubscriptionId = $sub.subscriptionId + BudgetName = $BudgetName + TimeGrain = $TimeGrain + StartDate = $StartDate + EndDate = $EndDate + ContactEmails = $ContactEmails + ContactRoles = $ContactRoles + FirstThreshold = $FirstThreshold + SecondThreshold = $SecondThreshold + ThirdThreshold = $ThirdThreshold + ForecastedThreshold = $ForecastedThreshold + Force = $true + Quiet = $true + } + + # Only pass Amount if explicitly provided; otherwise let Deploy-Budget.ps1 use tag/default + if ($PSBoundParameters.ContainsKey('Amount')) { + $deployParams.Add('Amount', $Amount) + } + + if ($WhatIf) { + $deployParams.Add('WhatIf', $true) + } + + if ($Quiet) { + & "$PSScriptRoot/Deploy-Budget.ps1" @deployParams -WarningAction SilentlyContinue + } else { + & "$PSScriptRoot/Deploy-Budget.ps1" @deployParams + } + + Write-Host "✅ Success: $($sub.name)" -ForegroundColor Green + $successCount++ + } + catch { + Write-Host "❌ Failed: $($sub.name) - $($_.Exception.Message)" -ForegroundColor Red + $failCount++ + } +} + +Write-Host "" +Write-Host "Deployment complete!" -ForegroundColor Green +Write-Host " Succeeded: $successCount" -ForegroundColor Green +Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { 'Red' } else { 'Green' }) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/README.md new file mode 100644 index 000000000..a71468d49 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/README.md @@ -0,0 +1,152 @@ +# Azure subscription budget deployment + +Deploy cost budgets to Azure subscriptions with automatic tag-based amount configuration. + +## Overview + +These scripts deploy `Microsoft.Consumption/budgets` resources to Azure subscriptions with configurable notification thresholds. Budget amounts can be set per-subscription using the `BudgetAmount` tag, allowing different spending limits across your environment. + +## Budget amount priority + +1. **Explicit parameter** - If `-Amount` is provided, that value is used +2. **Subscription tag** - If the subscription has a `BudgetAmount` tag, that value is used +3. **Default** - Falls back to $10/month + +## Prerequisites + +- Azure PowerShell module (`Az`) +- Azure Resource Graph module (`Az.ResourceGraph`) - for bulk deployments +- Authenticated to Azure (`Connect-AzAccount`) +- Appropriate permissions to create subscription-level resources + +## Scripts + +### Deploy-Budget.ps1 + +Deploy a budget to a single subscription. + +```powershell +# Interactive - prompts for subscription selection +./Deploy-Budget.ps1 -ContactEmails "admin@company.com" + +# Specific subscription +./Deploy-Budget.ps1 -SubscriptionId "12345678-1234-1234-1234-123456789012" -ContactEmails "admin@company.com" + +# Override the tag/default amount +./Deploy-Budget.ps1 -ContactEmails "admin@company.com" -Amount 5000 + +# Preview without deploying +./Deploy-Budget.ps1 -ContactEmails "admin@company.com" -WhatIf + +# Skip confirmation prompt +./Deploy-Budget.ps1 -ContactEmails "admin@company.com" -Force +``` + +#### Parameters + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `-SubscriptionId` | No | Interactive | Target subscription GUID | +| `-ContactEmails` | Yes | - | Email addresses for notifications | +| `-Amount` | No | Tag or $10 | Budget amount (overrides tag) | +| `-BudgetName` | No | `SubscriptionBudget` | Name of the budget resource | +| `-TimeGrain` | No | `Monthly` | Reset period: Monthly, Quarterly, Annually | +| `-StartDate` | No | 1st of next month | Budget start date (YYYY-MM-DD) | +| `-EndDate` | No | 1 year from start | Budget end date (YYYY-MM-DD) | +| `-ContactRoles` | No | Owner, Contributor | Azure roles to notify | +| `-FirstThreshold` | No | 50 | First notification threshold % | +| `-SecondThreshold` | No | 75 | Second notification threshold % | +| `-ThirdThreshold` | No | 90 | Third notification threshold % | +| `-ForecastedThreshold` | No | 100 | Forecasted cost threshold % | +| `-Force` | No | - | Skip confirmation prompt | +| `-Quiet` | No | - | Suppress verbose output | +| `-WhatIf` | No | - | Preview without deploying | + +### Deploy-BulkBudgets.ps1 + +Deploy budgets to all subscriptions in a management group. + +```powershell +# Deploy to all subscriptions in management group (uses tag/default per subscription) +./Deploy-BulkBudgets.ps1 -ManagementGroup "ALZ" -ContactEmails "admin@company.com" + +# Override amount for all subscriptions +./Deploy-BulkBudgets.ps1 -ManagementGroup "ALZ" -ContactEmails "admin@company.com" -Amount 5000 + +# Preview without deploying +./Deploy-BulkBudgets.ps1 -ManagementGroup "ALZ" -ContactEmails "admin@company.com" -WhatIf + +# Automated deployment (no prompts, minimal output) +./Deploy-BulkBudgets.ps1 -ManagementGroup "ALZ" -ContactEmails "admin@company.com" -Force -Quiet +``` + +#### Parameters + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `-ManagementGroup` | Yes | - | Management group to query for subscriptions | +| `-ContactEmails` | Yes | - | Email addresses for notifications | +| `-Amount` | No | Per-sub tag or $10 | Budget amount (overrides all tags) | +| `-BudgetName` | No | `SubscriptionBudget` | Name of the budget resource | +| `-TimeGrain` | No | `Monthly` | Reset period: Monthly, Quarterly, Annually | +| `-StartDate` | No | 1st of next month | Budget start date (YYYY-MM-DD) | +| `-EndDate` | No | 1 year from start | Budget end date (YYYY-MM-DD) | +| `-ContactRoles` | No | Owner, Contributor | Azure roles to notify | +| `-FirstThreshold` | No | 50 | First notification threshold % | +| `-SecondThreshold` | No | 75 | Second notification threshold % | +| `-ThirdThreshold` | No | 90 | Third notification threshold % | +| `-ForecastedThreshold` | No | 100 | Forecasted cost threshold % | +| `-Force` | No | - | Skip confirmation prompt | +| `-Quiet` | No | - | Suppress verbose output | +| `-WhatIf` | No | - | Preview without deploying | + +## Setting the BudgetAmount Tag + +Set the `BudgetAmount` tag on subscriptions to control per-subscription budget amounts: + +```powershell +# Set tag on a subscription +Update-AzTag -ResourceId "/subscriptions/" -Tag @{BudgetAmount = "500"} -Operation Merge +``` + +Or via Azure CLI: + +```bash +az tag update --resource-id "/subscriptions/" --operation merge --tags BudgetAmount=500 +``` + +## Notification Thresholds + +By default, notifications are sent when: + +| Threshold | Type | Default | +|-----------|------|---------| +| First | Actual | 50% | +| Second | Actual | 75% | +| Third | Actual | 90% | +| Forecasted | Forecasted | 100% | + +## Examples + +### Enterprise deployment + +```powershell +# Connect to Azure +Connect-AzAccount + +# Preview deployment to production management group +./Deploy-BulkBudgets.ps1 -ManagementGroup "Production" -ContactEmails "finops@company.com","alerts@company.com" -WhatIf + +# Deploy after review +./Deploy-BulkBudgets.ps1 -ManagementGroup "Production" -ContactEmails "finops@company.com","alerts@company.com" -Force +``` + +### Development environment with low budget + +```powershell +# Tag dev subscriptions with low budget +Update-AzTag -ResourceId "/subscriptions/" -Tag @{BudgetAmount = "50"} -Operation Merge + +# Deploy - will use $50 from tag +./Deploy-Budget.ps1 -SubscriptionId "" -ContactEmails "dev-team@company.com" +``` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/budget.bicep b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/budget.bicep new file mode 100644 index 000000000..f37b6ab6b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/budget.bicep @@ -0,0 +1,89 @@ +targetScope = 'subscription' + +@description('Name of the Budget. It should be unique within the subscription.') +param budgetName string = 'SubscriptionBudget' + +@description('The total amount of cost to track with the budget (in the billing currency). If subscription has BudgetAmount tag, that value is used instead.') +param amount int = 10 + +@description('The time covered by a budget. Tracking of the amount will be reset based on the time grain.') +@allowed([ + 'Monthly' + 'Quarterly' + 'Annually' +]) +param timeGrain string = 'Monthly' + +@description('The start date must be first of the month in YYYY-MM-DD format.') +param startDate string + +@description('The end date for the budget in YYYY-MM-DD format.') +param endDate string + +@description('First threshold percentage. Notification sent when cost exceeds this threshold.') +param firstThreshold int = 50 + +@description('Second threshold percentage. Notification sent when cost exceeds this threshold.') +param secondThreshold int = 75 + +@description('Third threshold percentage. Notification sent when cost exceeds this threshold.') +param thirdThreshold int = 90 + +@description('Forecasted threshold percentage. Notification sent when FORECASTED cost exceeds this threshold.') +param forecastedThreshold int = 100 + +@description('The list of email addresses to send the budget notification to when thresholds are exceeded.') +param contactEmails array + +@description('The list of contact roles to send the budget notification to when the threshold is exceeded.') +param contactRoles array = [ + 'Owner' + 'Contributor' +] + +resource budget 'Microsoft.Consumption/budgets@2023-11-01' = { + name: budgetName + properties: { + timePeriod: { + startDate: startDate + endDate: endDate + } + timeGrain: timeGrain + amount: amount + category: 'Cost' + notifications: { + Notification_FirstThreshold: { + enabled: true + operator: 'GreaterThan' + threshold: firstThreshold + contactEmails: contactEmails + contactRoles: contactRoles + } + Notification_SecondThreshold: { + enabled: true + operator: 'GreaterThan' + threshold: secondThreshold + contactEmails: contactEmails + contactRoles: contactRoles + } + Notification_ThirdThreshold: { + enabled: true + operator: 'GreaterThan' + threshold: thirdThreshold + contactEmails: contactEmails + contactRoles: contactRoles + } + Notification_Forecasted: { + enabled: true + operator: 'GreaterThan' + threshold: forecastedThreshold + thresholdType: 'Forecasted' + contactEmails: contactEmails + contactRoles: contactRoles + } + } + } +} + +output name string = budget.name +output resourceId string = budget.id diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/budget.parameters.json b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/budget.parameters.json new file mode 100644 index 000000000..fbc2d154b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/budgets/budget.parameters.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "budgetName": { + "value": "SubscriptionBudget" + }, + "amount": { + "value": 10 + }, + "timeGrain": { + "value": "Monthly" + }, + "startDate": { + "value": "2026-02-01" + }, + "endDate": { + "value": "2027-02-01" + }, + "firstThreshold": { + "value": 50 + }, + "secondThreshold": { + "value": 75 + }, + "thirdThreshold": { + "value": 90 + }, + "forecastedThreshold": { + "value": 100 + }, + "contactEmails": { + "value": [ + "admin@company.com", + "finance@company.com" + ] + }, + "contactRoles": { + "value": [ + "Owner", + "Contributor" + ] + } + } +} diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/README.md new file mode 100644 index 000000000..75c0808c5 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/README.md @@ -0,0 +1,219 @@ +# LLM-optimized calculator + +A Python calculator script using **SymPy** for safe evaluation of mathematical expressions from string input—perfect for LLM usage. + +## Why SymPy (not NumPy)? + +- **String parsing**: Accepts natural math expressions as strings +- **Safe evaluation**: Safer than Python's `eval()`, designed for untrusted input +- **Symbolic + numeric**: Handles exact symbolic math and floating-point evaluation +- **Rich functions**: Built-in support for sqrt, sin, cos, log, factorial, etc. +- **Multiple formats**: Can parse LaTeX, natural math syntax, implicit multiplication + +NumPy is for array/matrix operations, not parsing calculator-style string expressions. + +## Installation + +```bash +pip install sympy +# or use the requirements file +pip install -r calculator-requirements.txt +``` + +## Usage + +### Basic arithmetic +```bash +python3 calculator.py "2 + 2" # 4.00000000000000 +python3 calculator.py "10 * 5 + 3" # 53.0000000000000 +python3 calculator.py "100 / 4" # 25.0000000000000 +``` + +### Exponentiation (use ^ or **) +```bash +python3 calculator.py "2^8" # 256.000000000000 +python3 calculator.py "3**4" # 81.0000000000000 +python3 calculator.py "sqrt(16) + 3^2" # 13.0000000000000 +``` + +### Mathematical functions +```bash +python3 calculator.py "sin(pi/2)" # 1.00000000000000 +python3 calculator.py "cos(pi)" # -1.00000000000000 +python3 calculator.py "tan(pi/4)" # 1.00000000000000 +python3 calculator.py "log(100, 10)" # 2.00000000000000 (log base 10) +python3 calculator.py "ln(e^2)" # 2.00000000000000 (natural log) +python3 calculator.py "factorial(5)" # 120.000000000000 +python3 calculator.py "sqrt(144)" # 12.0000000000000 +python3 calculator.py "abs(-42)" # 42.0000000000000 +``` + +### Symbolic expressions +```bash +python3 calculator.py "x^2 + 2*x + 1" --symbolic # x**2 + 2*x + 1 +python3 calculator.py "sin(x) + cos(x)" -s # sin(x) + cos(x) +``` + +### Precision control +```bash +python3 calculator.py "pi" --precision 50 +# 3.1415926535897932384626433832795028841971693993751 +``` + +## LLM integration examples + +### Example 1: calculator tool for an LLM agent +```python +from calculator import calculate + +# LLM generates expression string +llm_output = "sqrt(144) + 5^2" +result = calculate(llm_output) +print(result) # 37.0000000000000 +``` + +### Example 2: safe math evaluation +```python +# User provides input (potentially unsafe) +user_input = "sin(pi/6) * 2" +result = calculate(user_input) +print(result) # 1.00000000000000 +``` + +### Example 3: symbolic manipulation +```python +# Preserve symbolic form +expr = "(x + 1)^2" +result = calculate(expr, symbolic=True) +print(result) # (x + 1)**2 +``` + +## Supported operations + +### Arithmetic +- Addition: `+` +- Subtraction: `-` +- Multiplication: `*` +- Division: `/` +- Exponentiation: `^` or `**` +- Modulo: `%` + +### Functions +- `sqrt(x)` - Square root +- `sin(x)`, `cos(x)`, `tan(x)` - Trigonometric +- `asin(x)`, `acos(x)`, `atan(x)` - Inverse trig +- `log(x, base)` - Logarithm (default base e) +- `ln(x)` - Natural logarithm +- `exp(x)` - Exponential (e^x) +- `factorial(n)` - Factorial +- `abs(x)` - Absolute value + +### Constants +- `pi` - π (3.14159...) +- `e` - Euler's number (2.71828...) +- `oo` - Infinity + +### Implicit multiplication (works automatically) +```bash +python3 calculator.py "2x + 3y" # Requires --symbolic since x,y are variables +python3 calculator.py "2(3 + 4)" # 14.0000000000000 +``` + +## Features + +1. **Safe evaluation**: No arbitrary code execution +2. **Flexible parsing**: Handles `^` for exponents, implicit multiplication +3. **Symbolic and numeric**: Choose between exact symbolic or numeric evaluation +4. **Error handling**: Clear error messages for invalid expressions +5. **High precision**: Configurable decimal precision + +## Command-line options + +- `--symbolic` or `-s`: Return symbolic result (don't evaluate numerically) +- `--precision N` or `-p N`: Set decimal precision (default: 15) + +## Error handling + +```bash +python3 calculator.py "1/0" +# Error: ValueError: Division by zero + +python3 calculator.py "invalid expression" +# Error: SyntaxError: invalid syntax +``` + +## Integration with LLM tools + +### As a function calling tool +```json +{ + "name": "calculate", + "description": "Evaluate mathematical expressions safely", + "parameters": { + "type": "object", + "properties": { + "expression": { + "type": "string", + "description": "Mathematical expression to evaluate (e.g., 'sqrt(16) + 3^2')" + }, + "symbolic": { + "type": "boolean", + "description": "Return symbolic result instead of numeric evaluation" + } + }, + "required": ["expression"] + } +} +``` + +### Python function interface +```python +def calculate(expression: str, symbolic: bool = False, precision: int = 15) -> str: + """ + Evaluate a mathematical expression safely using SymPy. + + Args: + expression: String mathematical expression + symbolic: Return symbolic result if True + precision: Decimal places for numeric evaluation + + Returns: + String representation of the result + """ +``` + +## Advanced examples + +### Complex expressions +```bash +python3 calculator.py "(sin(pi/4))^2 + (cos(pi/4))^2" # 1.00000000000000 +python3 calculator.py "log(1000, 10) + sqrt(144)" # 15.0000000000000 +python3 calculator.py "factorial(6) / factorial(4)" # 30.0000000000000 +``` + +### Scientific notation +```bash +python3 calculator.py "1.5e10 + 2.3e9" # 17300000000.0 +``` + +## Why this design? + +**Optimized for LLM usage:** +- LLMs output strings, not Python objects +- Simple command-line interface +- Clear error messages for debugging +- No eval() security risks +- Handles common mathematical notation naturally +- Symbolic mode for algebraic manipulation + +**SymPy advantages over alternatives:** +- **vs eval()**: Much safer, no arbitrary code execution +- **vs NumPy**: NumPy requires array syntax, doesn't parse strings naturally +- **vs ast.literal_eval()**: Doesn't support mathematical functions +- **vs custom parser**: SymPy has years of battle-testing and edge case handling + +## License + +This calculator is part of the azcapman project. + +**Source**: [SymPy documentation](https://docs.sympy.org/latest/index.html) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/calculator.py b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/calculator.py new file mode 100755 index 000000000..758abedce --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/calculator.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +LLM-Optimized Calculator using SymPy + +A calculator script designed for LLM usage with safe string evaluation, +symbolic mathematics, and comprehensive mathematical operations. + +Usage: + python calculator.py "2 + 2" + python calculator.py "sqrt(16) + 3^2" + python calculator.py "sin(pi/2)" + python calculator.py "x^2 + 2*x + 1" --symbolic +""" + +import sys +from sympy import parse_expr, sympify, symbols, N +from sympy.parsing.sympy_parser import ( + parse_expr, + standard_transformations, + implicit_multiplication_application, + convert_xor, +) + + +def calculate(expression: str, symbolic: bool = False, precision: int = 15) -> str: + """ + Evaluate a mathematical expression safely using SymPy. + + Args: + expression: String mathematical expression (e.g., "2 + 2", "sqrt(16)") + symbolic: If True, return symbolic result; if False, evaluate numerically + precision: Number of decimal places for numeric evaluation + + Returns: + String representation of the result + + Examples: + >>> calculate("2 + 2") + '4' + >>> calculate("sqrt(16) + 3**2") + '13' + >>> calculate("sin(pi/2)") + '1.00000000000000' + >>> calculate("x^2 + 2*x + 1", symbolic=True) + 'x**2 + 2*x + 1' + """ + try: + # Define transformations to make parsing more flexible + transformations = ( + standard_transformations + + (implicit_multiplication_application,) + + (convert_xor,) # Convert ^ to ** for exponentiation + ) + + # Parse the expression with transformations + expr = parse_expr(expression, transformations=transformations) + + if symbolic or expr.free_symbols: + # Return symbolic form if requested or if expression contains variables + return str(expr) + else: + # Evaluate numerically + result = N(expr, precision) + return str(result) + + except Exception as e: + return f"Error: {type(e).__name__}: {str(e)}" + + +def main(): + """Command-line interface for the calculator.""" + if len(sys.argv) < 2: + print(__doc__) + print("\nExamples:") + print(' python calculator.py "2 + 2"') + print(' python calculator.py "sqrt(16) + 3^2"') + print(' python calculator.py "sin(pi/2)"') + print(' python calculator.py "log(100, 10)"') + print(' python calculator.py "factorial(5)"') + print(' python calculator.py "integrate(x^2, x)" --symbolic') + sys.exit(1) + + expression = sys.argv[1] + symbolic = "--symbolic" in sys.argv or "-s" in sys.argv + + # Check for precision flag + precision = 15 + for i, arg in enumerate(sys.argv): + if arg in ["--precision", "-p"] and i + 1 < len(sys.argv): + try: + precision = int(sys.argv[i + 1]) + except ValueError: + print(f"Warning: Invalid precision value '{sys.argv[i + 1]}', using default {precision}") + + result = calculate(expression, symbolic=symbolic, precision=precision) + print(result) + + +if __name__ == "__main__": + main() diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/example_usage.py b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/example_usage.py new file mode 100644 index 000000000..09e5096fa --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/example_usage.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +Example usage of the LLM-optimized calculator module. +Demonstrates how to integrate the calculator into LLM agents or other Python applications. +""" + +from calculator import calculate + + +def main(): + print("LLM-Optimized Calculator Examples\n" + "=" * 50 + "\n") + + # Example 1: Basic arithmetic + print("Example 1: Basic Arithmetic") + expressions = [ + "2 + 2", + "10 * 5 + 3", + "100 / 4", + "2^8", + ] + for expr in expressions: + result = calculate(expr) + print(f" {expr:20s} = {result}") + print() + + # Example 2: Mathematical functions + print("Example 2: Mathematical Functions") + expressions = [ + "sqrt(144)", + "sin(pi/2)", + "cos(pi)", + "log(100, 10)", + "factorial(5)", + ] + for expr in expressions: + result = calculate(expr) + print(f" {expr:20s} = {result}") + print() + + # Example 3: Symbolic expressions + print("Example 3: Symbolic Expressions") + symbolic_exprs = [ + "x^2 + 2*x + 1", + "sin(x) + cos(x)", + "(a + b)^2", + ] + for expr in symbolic_exprs: + result = calculate(expr, symbolic=True) + print(f" {expr:20s} = {result}") + print() + + # Example 4: Complex calculations + print("Example 4: Complex Calculations") + expressions = [ + "(sin(pi/4))^2 + (cos(pi/4))^2", + "log(1000, 10) + sqrt(144)", + "factorial(6) / factorial(4)", + ] + for expr in expressions: + result = calculate(expr) + print(f" {expr:35s} = {result}") + print() + + # Example 5: High precision + print("Example 5: High Precision (50 decimal places)") + result = calculate("pi", precision=50) + print(f" pi = {result}") + print() + + # Example 6: Error handling + print("Example 6: Error Handling") + bad_expressions = [ + "1 / 0", + "invalid expression", + "sqrt(-1)", # This will return I (imaginary unit) in SymPy + ] + for expr in bad_expressions: + result = calculate(expr) + print(f" {expr:20s} = {result}") + print() + + # Example 7: LLM tool integration pattern + print("Example 7: LLM Tool Integration Pattern") + print(" Simulating LLM generating math expressions...") + + # Simulate LLM outputs + llm_outputs = [ + "What is the square root of 256?", # Extract: sqrt(256) + "Calculate 15 factorial", # Extract: factorial(15) + "What's sin(30 degrees)?", # Convert: sin(30*pi/180) + ] + + # In a real LLM integration, you'd have the LLM extract/convert to expressions + extracted_expressions = [ + "sqrt(256)", + "factorial(15)", + "sin(30*pi/180)", + ] + + for question, expr in zip(llm_outputs, extracted_expressions): + result = calculate(expr) + print(f" Q: {question}") + print(f" A: {result}\n") + + +if __name__ == "__main__": + main() diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/requirements.txt b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/requirements.txt new file mode 100644 index 000000000..31a0ac42e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/calculator/requirements.txt @@ -0,0 +1 @@ +sympy>=1.12 diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/generate_citation_matrix.py b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/generate_citation_matrix.py new file mode 100644 index 000000000..37847c0e6 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/generate_citation_matrix.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import re +from pathlib import Path +from collections import defaultdict + +ROOT = Path(__file__).resolve().parents[1] +DOCS_DIR = ROOT / "docs" + +# Match inline Markdown links: [label](url) +LINK_RE = re.compile(r"\[([^\]]+)\]\((https?://[^)]+)\)") + + +def extract_links(md_path: Path): + """Extract all inline Markdown links from a file.""" + links = [] # list of (label, url) + content = md_path.read_text(encoding="utf-8", errors="ignore") + for match in LINK_RE.finditer(content): + label, url = match.groups() + links.append((label, url)) + return links + + +def main(): + md_files = sorted( + [ + p + for p in DOCS_DIR.rglob("*.md") + if "_site" not in p.parts and "AGENTS.md" not in p.name + ] + ) + + url_map = defaultdict(list) # url -> list of (file_rel, label) + + for md in md_files: + rel = md.relative_to(ROOT).as_posix() + for label, url in extract_links(md): + url_map[url].append((rel, label)) + + matrix_path = DOCS_DIR / "operations" / "support-and-reference" / "citation-matrix.md" + matrix_path.parent.mkdir(parents=True, exist_ok=True) + + lines = [] + lines.append("---") + lines.append("title: Citation traceability matrix") + lines.append("parent: Support & Reference") + lines.append("nav_order: 4") + lines.append("---") + lines.append("") + lines.append("# Citation traceability matrix") + lines.append("") + lines.append( + "This matrix lists external links used across the documentation, along with the files that reference them." + ) + lines.append("") + lines.append(f"**Total unique URLs:** {len(url_map)}") + lines.append("") + lines.append("| URL | Label | Used in |") + lines.append("| --- | --- | --- |") + + for url in sorted(url_map.keys()): + uses = url_map[url] + # Pick first label as the display label + label = uses[0][1] + # Deduplicate files + files = sorted(set(rel for rel, _ in uses)) + files_desc = ", ".join(files) + # Escape pipes in label + label_escaped = label.replace("|", "\\|") + lines.append(f"| <{url}> | {label_escaped} | {files_desc} |") + + lines.append("") + + matrix_path.write_text("\n".join(lines), encoding="utf-8") + print(f"Generated {matrix_path} with {len(url_map)} unique URLs") + + +if __name__ == "__main__": + main() diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzAvailabilityZoneMapping.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzAvailabilityZoneMapping.ps1 new file mode 100644 index 000000000..a3b61d6af --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzAvailabilityZoneMapping.ps1 @@ -0,0 +1,232 @@ + +<# +.SYNOPSIS + Maps logical availability zones to physical zones across Azure subscriptions. + +.DESCRIPTION + This script analyzes the relationship between logical and physical availability zones + across multiple Azure subscriptions. Logical zones (1, 2, 3) may map to different + physical zones in different subscriptions, which is critical for understanding when + planning multi-subscription deployments that require specific zone alignment. + + The script outputs a detailed CSV mapping that shows: + - How logical zones map to physical zones per subscription + - The actual physical zone identifiers and names + - Location information for zone planning + + This data is essential for: + - Cross-subscription deployment planning + - Understanding zone distribution across subscriptions + - Ensuring proper zone alignment for multi-subscription architectures + - Compliance with data residency requirements + +.PARAMETER SubscriptionIds + Array of Azure subscription IDs to analyze for zone mappings. + If not specified, analyzes all accessible subscriptions in the current tenant. + Example: @('00000000-0000-0000-0000-000000000000', '11111111-1111-1111-1111-111111111111') + +.PARAMETER OutputFile + Name of the output CSV file containing the zone mapping analysis. + Default: "ZonePeers.csv" + +.EXAMPLE + .\Get-AzAvailabilityZoneMapping.ps1 + + Analyzes zone mappings for all accessible subscriptions and outputs to ZonePeers.csv. + +.EXAMPLE + .\Get-AzAvailabilityZoneMapping.ps1 -SubscriptionIds @('sub1-id','sub2-id') -OutputFile "CrossSubZones.csv" + + Analyzes specific subscriptions and outputs to a custom filename. + +.NOTES + Requires Azure PowerShell module and authenticated session with Reader access to target subscriptions. + Output CSV contains columns: TenantId, SubscriptionId, SubscriptionName, Location, + LogicalZone, PhysicalZone, PhysicalZoneName + + Zone Mapping Concepts: + - Logical Zone: The zone number (1, 2, 3) that applications see + - Physical Zone: The actual physical datacenter identifier (often a single character) + - Physical Zone Name: The full physical zone name (e.g., "eastus-az1", "eastus-az2") + + Different subscriptions may have different logical-to-physical zone mappings for the same region. + This is by design to ensure even distribution of resources across physical infrastructure. +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('00000000-0000-0000-0000-000000000000')")] + [string[]]$SubscriptionIds = @(), + + [Parameter(Mandatory = $false, HelpMessage = "Output CSV filename")] + [string]$OutputFile = "ZonePeers.csv" +) +# ================================================================================ +# HELPER FUNCTIONS +# ================================================================================ + +<# +.SYNOPSIS + Retrieves all accessible subscription IDs within the current tenant. + +.DESCRIPTION + Gets all Azure subscriptions that the current authenticated user has access to + within the current tenant context. Used when no specific subscription IDs are provided. + +.OUTPUTS + Array of subscription ID strings +#> +function Get-SubscriptionIds { + Write-Host "Listing Subscriptions" + return (Get-AzSubscription -TenantId ((Get-AzContext).Tenant.TenantId) | Select-Object -ExpandProperty SubscriptionId) +} + +<# +.SYNOPSIS + Extracts the last character from a string. + +.DESCRIPTION + Utility function used to extract the physical zone identifier from the full physical zone name. + Physical zones are typically identified by the last character (e.g., 'A', 'B', 'C') of the + full physical zone string (e.g., "eastus-az1" -> "1", "westus2-azA" -> "A"). + +.PARAMETER inputString + The input string to extract the last character from. + +.OUTPUTS + The last character of the input string, or empty string if input is null/empty. + +.EXAMPLE + Get-LastChar("eastus-az1") + Returns: "1" + +.EXAMPLE + Get-LastChar("westus2-azA") + Returns: "A" +#> +function Get-LastChar { + param ( + [string]$inputString + ) + + # Handle null or empty input + if ([string]::IsNullOrEmpty($inputString)) { + return "" + } + + # Additional length check for safety (though redundant with IsNullOrEmpty) + if ($inputString.Length -lt 1) { + return "" + } + + # Return the last character + return $inputString[-1] +} + +<# +.SYNOPSIS + Retrieves availability zone peering information for a specific subscription. + +.DESCRIPTION + Queries the Azure REST API to get detailed availability zone mapping information + for all regions within a subscription. This information shows how logical zones + (1, 2, 3) map to physical zones for each region. + +.PARAMETER SubscriptionId + The Azure subscription ID to query for zone peering information. + +.OUTPUTS + Array of location objects containing availability zone mapping details for all regions. + +.NOTES + Uses the Azure REST API directly as this detailed zone mapping information is not + available through standard PowerShell cmdlets. The API returns comprehensive + location data including availabilityZoneMappings for regions that support zones. + + The returned data structure includes: + - Location information (name, type, etc.) + - availabilityZoneMappings array with logicalZone -> physicalZone mappings +#> +function Get-ZonePeers { + param ( + [string]$SubscriptionId + ) + + # Construct REST API URI for location zone mappings using subscription-specific endpoint + $uri = "{0}subscriptions/{1}/locations?api-version=2022-12-01" -f $resourceManagerUrl, $SubscriptionId + + # Call Azure REST API to get comprehensive location and zone mapping data + $response = Invoke-AzRest -Method GET -Uri $uri + return ($response.Content | ConvertFrom-Json).value +} + +# ================================================================================ +# MAIN SCRIPT EXECUTION +# ================================================================================ + +# Set error handling and output preferences +$ErrorActionPreference = 'Stop' # Stop execution on any error +$VerbosePreference = 'SilentlyContinue' # Suppress verbose output for cleaner execution + +# Define CSV header structure for the zone mapping output +$csvHeaderString = "TenantId,SubscriptionId,SubscriptionName,Location,LogicalZone,PhysicalZone,PhysicalZoneName" + +# Initialize output file with CSV headers +$csvHeaderString | Out-File -Force -FilePath $OutputFile + +# Get the Azure Resource Manager URL for the current Azure environment +# This is needed for constructing REST API calls and may vary by cloud (Public, Government, etc.) +$resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl + +# Populate subscription list if not provided by user +if ($SubscriptionIds.Count -eq 0) { + $SubscriptionIds = Get-SubscriptionIds +} + +# Initialize array to collect all zone mapping data +$zoneMaps = @() + +# Process each subscription to extract zone mapping information +ForEach ($subscriptionId in $SubscriptionIds){ + Write-Output "Get zone peer details for $subscriptionId" + + # Get comprehensive zone peering data for this subscription + $zonePeers = Get-ZonePeers -SubscriptionId $subscriptionId + + # Extract availability zone mappings from regions only (filter out other location types) + # Only regions have availability zone mappings - other location types don't support zones + $zoneMappings = ($zonePeers | Where-Object { $_.type -eq "Region"}).availabilityZoneMappings + + # Get subscription details for output labeling + $subscription = Get-AzSubscription -SubscriptionId $subscriptionId + + # Process each zone mapping within this subscription + foreach($mapping in $zoneMappings) { + # Skip mappings with missing zone information + # Some regions may have incomplete or null zone mapping data + if([string]::IsNullOrEmpty($mapping.logicalZone) -or [string]::IsNullOrEmpty($mapping.physicalZone)) { + continue + } + + # Create zone mapping record with comprehensive details + $zoneMap = [PSCustomObject]@{ + TenantId = $Subscription.TenantId # Azure tenant identifier + SubscriptionId = $Subscription.Id # Subscription GUID + SubscriptionName = $Subscription.Name # Human-readable subscription name + Location = $mapping.physicalZone.Split("-")[0] # Region name (e.g., "eastus" from "eastus-az1") + LogicalZone = $mapping.logicalZone # Logical zone number (1, 2, 3) + PhysicalZone = Get-LastChar($mapping.physicalZone) # Physical zone identifier (A, B, C or 1, 2, 3) + PhysicalZoneName = $mapping.physicalZone # Full physical zone name (e.g., "eastus-az1") + } + + # Add this mapping to the collection + $zoneMaps += $zoneMap + } +} + +# Output all collected zone mapping data to CSV file +# Using -NoHeader since we already wrote headers at the beginning +$zoneMaps | ConvertTo-Csv -NoHeader | Out-File -Force -Append -FilePath $OutputFile + +Write-Host "Zone mapping analysis complete. Results written to: $OutputFile" +Write-Host "Total zone mappings found: $($zoneMaps.Count)" \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzVMQuotaUsage.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzVMQuotaUsage.ps1 new file mode 100644 index 000000000..d6f2b2de8 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzVMQuotaUsage.ps1 @@ -0,0 +1,555 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +<# +.SYNOPSIS + Multi-threaded Azure VM quota analysis script that queries compute resource SKU availability, + quota usage, and availability zone restrictions across subscriptions. + +.DESCRIPTION + This script analyzes Azure VM quota usage and availability zone restrictions across multiple + subscriptions and regions. It provides detailed information about: + - Current quota usage vs. limits for VM families + - Available and restricted availability zones per SKU + - Region-level restrictions for specific VM SKUs + - Physical vs. logical availability zone mappings + + The Catalogs API is the primary SKU source. The deprecated meter data CSV is used only as a + fallback when Catalogs API lookup fails and the SKUs parameter is not provided. + + The script supports multi-threading for faster execution across large numbers of subscriptions + and can normalize zone information to physical zones for cross-subscription deployment planning. + +.PARAMETER SKUs + Array of VM SKU names to analyze. If not specified, resolves VM SKUs from the Azure Catalogs + API first and falls back to the deprecated meter data CSV only if that lookup fails. + Example: @('Standard_D2s_v3', 'Standard_D4s_v3', 'Standard_E4s_v5') + +.PARAMETER Locations + Array of Azure regions to analyze. If not specified, queries all physical Azure regions. + Example: @('eastus', 'westus2', 'centralus') + +.PARAMETER SubscriptionIds + Array of subscription IDs to analyze. If not specified, queries all accessible subscriptions. + Example: @('00000000-0000-0000-0000-000000000000') + +.PARAMETER MeterDataUri + Deprecated fallback source URI for normalized VM SKU data from Cost Management connector data. + Used only when the Catalogs API lookup fails and the SKUs parameter is not provided. + +.PARAMETER CatalogLocation + Azure region used for Catalogs API requests when resolving VM SKU names from the primary SKU + source. + +.PARAMETER UsePhysicalZones + Switch to normalize availability zone output to physical zones instead of logical zones. + Useful for cross-subscription deployment planning where logical zones may differ. + +.PARAMETER Threads + Number of concurrent threads for processing subscriptions. Set to 0 for auto-detection. + Higher values speed up execution but may hit API rate limits. + +.PARAMETER OutputFile + Name of the output CSV file containing the analysis results. + +.EXAMPLE + .\Get-AzVMQuotaUsage.ps1 -SKUs @('Standard_D2s_v5','Standard_E4s_v5') -Locations @('eastus','westus2') + + Analyzes specific VM SKUs in specific regions across all accessible subscriptions. + +.EXAMPLE + .\Get-AzVMQuotaUsage.ps1 -UsePhysicalZones -Threads 4 -OutputFile "MyQuotaAnalysis.csv" + + Runs full analysis with physical zone mapping using 4 threads. + +.NOTES + Requires Azure PowerShell module and authenticated session with Reader access to target subscriptions. + Catalogs API access also requires the Microsoft.Capacity/catalogs/read permission. + Output CSV contains columns: TenantId, SubscriptionId, SubscriptionName, Location, Family, Size, + RegionRestricted, ZonesPresent, ZonesRestricted, CoresUsed, CoresTotal, CoresRequested, ZonesRequested +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('Standard_D2s_v3', 'Standard_D4s_v3')")] + [string[]]$SKUs = @(), + + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('eastus', 'westus')")] + [string[]]$Locations = @(), + + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('00000000-0000-0000-0000-000000000000')")] + [string[]]$SubscriptionIds = @(), + + [Parameter(Mandatory = $false, HelpMessage = "Deprecated fallback URL to download normalized VM SKU list")] + [string]$MeterDataUri = "https://ccmstorageprod.blob.core.windows.net/instancesizeflexibility-data/isfratioblob.csv", + + [Parameter(Mandatory = $false, HelpMessage = "Azure region to query for VM catalog data")] + [string]$CatalogLocation = "eastus", + + [Parameter(Mandatory = $false, HelpMessage = "Normalize output to physical availability zones")] + [switch]$UsePhysicalZones = $false, + + [Parameter(Mandatory = $false, HelpMessage = "Concurrent threads to use. Set to '0' for auto-detect")] + [ValidateRange(0, 40)] + [int]$Threads = 2, + + [Parameter(Mandatory = $false, HelpMessage = "Output file name")] + [string]$OutputFile = "QuotaQuery.csv" +) + +# ================================================================================ +# HELPER FUNCTIONS +# ================================================================================ + +<# +.SYNOPSIS + Retrieves normalized VM SKU names from the Azure Catalogs API, with CSV as fallback. + +.DESCRIPTION + Queries the Azure Catalogs API for normalized VM SKU names and uses that as the + primary source for quota analysis. If the Catalogs API call fails, falls back to + AutofitComboMeterData.csv. Filters out SQL-related SKUs and returns unique SKU names. + +.OUTPUTS + Array of normalized VM SKU names (e.g., 'Standard_D2s_v3', 'Standard_E4s_v5') +#> +function Get-SKUDetails { + try { + Write-Host "Querying Azure Catalogs API for VM SKU details in $CatalogLocation" + $catalogData = Get-VMCatalogData -SubscriptionId $SubscriptionIds[0] -Location $CatalogLocation + [string[]]$skuNames = @( + ($catalogData | Select-Object -Property ArmSkuName -Unique | Where-Object { $_.ArmSkuName -notlike "*sql*" }).ArmSkuName | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + + if ($skuNames.Count -eq 0) { + throw "Catalogs API returned zero VM SKUs" + } + + Write-Host "Retrieved $($skuNames.Count) unique VM SKUs from Catalogs API" + return $skuNames + } + catch { + Write-Warning "Catalogs API failed: $($_.Exception.Message)" + Write-Warning "Falling back to CSV download from $MeterDataUri" + + # Extract filename from URI for local storage + [string]$meterDataFile = $MeterDataUri.Split('/')[-1] + + # Download the Cost Management connector data file + Invoke-WebRequest -Uri $MeterDataUri -OutFile $meterDataFile + + # Parse CSV and extract unique, non-SQL VM SKUs + $meterData = Get-Content $meterDataFile | ConvertFrom-Csv + [string[]]$skuNames = @( + ($meterData | Select-Object -Property ArmSkuName -Unique | Where-Object { $_.ArmSkuName -notlike "*sql*" }).ArmSkuName | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + return $skuNames + } +} + +<# +.SYNOPSIS + Retrieves VM catalog data from the Azure Microsoft.Capacity catalogs API. + +.DESCRIPTION + Calls the Azure catalogs endpoint for VirtualMachines in the specified subscription + and location using Invoke-AzRestMethod. Follows any nextLink values until all pages + are retrieved and returns only entries that contain the required instance size + flexibility properties. + +.PARAMETER SubscriptionId + The Azure subscription ID used to scope the catalogs API request. + +.PARAMETER Location + The Azure region name used in the catalogs API filter. + +.OUTPUTS + Array of PSCustomObject values with InstanceSizeFlexibilityGroup, ArmSkuName, and Ratio properties. +#> +function Get-VMCatalogData { + param ( + [string]$SubscriptionId, + [string]$Location + ) + + $catalogItems = [System.Collections.Generic.List[object]]::new() + $path = "/subscriptions/{0}/providers/Microsoft.Capacity/catalogs?api-version=2022-11-01&reservedResourceType=VirtualMachines&location={1}" -f $SubscriptionId, $Location + $nextLink = $null + + do { + $response = if ([string]::IsNullOrWhiteSpace($nextLink)) { + Invoke-AzRestMethod -Method GET -Path $path + } + else { + Invoke-AzRestMethod -Method GET -Uri $nextLink + } + + $payload = $response.Content | ConvertFrom-Json -Depth 10 + + foreach ($item in @($payload.value)) { + $groupProperty = @($item.skuProperties | Where-Object { $_.name -eq 'ReservationsAutofitGroup' } | Select-Object -First 1) + $ratioProperty = @($item.skuProperties | Where-Object { $_.name -eq 'ReservationsAutofitRatio' } | Select-Object -First 1) + + if ([string]::IsNullOrWhiteSpace([string]$item.name) -or + $groupProperty.Count -eq 0 -or + $ratioProperty.Count -eq 0 -or + [string]::IsNullOrWhiteSpace([string]$groupProperty[0].value) -or + [string]::IsNullOrWhiteSpace([string]$ratioProperty[0].value)) { + continue + } + + $catalogItems.Add([PSCustomObject]@{ + InstanceSizeFlexibilityGroup = $groupProperty[0].value + ArmSkuName = $item.name + Ratio = $ratioProperty[0].value + }) + } + + $nextLink = $payload.nextLink + } while (-not [string]::IsNullOrWhiteSpace($nextLink)) + + return $catalogItems.ToArray() +} + +<# +.SYNOPSIS + Retrieves all accessible subscription IDs within the current tenant. + +.DESCRIPTION + Gets all Azure subscriptions that the current authenticated user has access to + within the current tenant context. + +.OUTPUTS + Array of subscription ID strings +#> +function Get-SubscriptionIds { + Write-Host "Listing Subscriptions" + return (Get-AzSubscription -TenantId ((Get-AzContext).Tenant.TenantId) | Select-Object -ExpandProperty SubscriptionId) +} + +<# +.SYNOPSIS + Extracts the last character from a string. + +.DESCRIPTION + Utility function used to extract physical zone identifiers from availability zone mappings. + Physical zones are typically identified by the last character of the physical zone string. + +.PARAMETER inputString + The input string to extract the last character from. + +.OUTPUTS + The last character of the input string, or empty string if input is null/empty. +#> +function Get-LastChar { + param ( + [string]$inputString + ) + + if ([string]::IsNullOrEmpty($inputString)) { + return "" + } + return $inputString[-1] +} + +<# +.SYNOPSIS + Retrieves all physical Azure regions. + +.DESCRIPTION + Gets all Azure regions that are physical locations (not logical regions) + and have a defined physical location. Filters out logical regions and + regions without physical presence. + +.OUTPUTS + Array of Azure region names (e.g., 'eastus', 'westus2', 'centralus') +#> +function Get-Locations { + Write-Host "Listing Locations" + return (Get-AzLocation | Where-Object { $_.RegionType -eq 'Physical' -and $_.PhysicalLocation -ne "" -and $_.Location } | Select-Object -Property Location -Unique).Location +} + +<# +.SYNOPSIS + Retrieves availability zone peering information for a subscription. + +.DESCRIPTION + Gets the logical-to-physical availability zone mappings for all regions + within a specific subscription. This information is crucial for understanding + how logical zones (1, 2, 3) map to physical zones across different subscriptions. + +.PARAMETER SubscriptionId + The Azure subscription ID to query for zone peering information. + +.OUTPUTS + Array of location objects containing availability zone mapping details. + +.NOTES + Uses the Azure REST API directly as this information is not available through + standard PowerShell cmdlets. +#> +function Get-ZonePeers { + param ( + [string]$SubscriptionId + ) + Write-Host "Get Zone Peering Information for subscription $SubscriptionId" + + # Construct REST API URI for location zone mappings + $uri = "{0}subscriptions/{1}/locations?api-version=2022-12-01" -f (Get-AzContext).Environment.ResourceManagerUrl, $SubscriptionId + + # Call Azure REST API to get zone peering data + $response = Invoke-AzRest -Method GET -Uri $uri + return ($response.Content | ConvertFrom-Json).value +} + +<# +.SYNOPSIS + Analyzes VM quota usage and restrictions for a specific subscription. + +.DESCRIPTION + Core function that performs detailed quota analysis for a single subscription. + Queries compute resource SKUs, VM usage statistics, and availability zone + restrictions across specified locations and SKUs. + +.PARAMETER SubscriptionId + The Azure subscription ID to analyze. + +.PARAMETER Locations + Array of Azure regions to analyze within this subscription. + +.PARAMETER SKUs + Array of VM SKU names to analyze. + +.PARAMETER OutputFile + Name of the CSV file to append results to. + +.OUTPUTS + Appends CSV rows to the output file with quota and restriction details. + +.NOTES + This function is designed to run in parallel across multiple subscriptions + for optimal performance. Each execution handles one subscription completely. +#> +function Get-QuotaDetails { + param ( + [string]$SubscriptionId, + [string[]]$Locations, + [string[]]$SKUs, + [string]$OutputFile + ) + + $start = Get-Date + try { + # Get subscription details and validate access + $Subscription = Get-AzSubscription -SubscriptionId $SubscriptionId -WarningAction SilentlyContinue + if ($null -eq $Subscription) { + throw "Subscription not found" + } + + # Set context to target subscription for all subsequent queries + Set-AzContext -SubscriptionId $Subscription.Id -WarningAction SilentlyContinue | Out-Null + + # Get zone peering information if physical zone mapping is requested + if($UsePhysicalZones) + { + $zonePeers = Get-ZonePeers -SubscriptionId $SubscriptionId + if ($zonePeers.Count -eq 0) { + Write-Host "No Zone Peering Information found for subscription $SubscriptionId" -ForegroundColor Yellow + } + } + + # Process each location within the subscription + foreach ($Location in $Locations) { + "Querying: $SubscriptionId - $($Subscription.Name) - $Location" + + # Get all VM SKUs available in this location + $computeSKUs = Get-AzComputeResourceSku -Location $Location -ErrorAction SilentlyContinue | Where-Object { $_.ResourceType -eq 'virtualMachines' } + + # Get current VM quota usage for this location + $vmUsage = Get-AZVMUsage -Location $Location -ErrorAction SilentlyContinue + + # Get availability zone mappings for this location (if using physical zones) + $availabilityZoneMappings = ($zonePeers | Where-Object { $_.name -like $Location -and $_.type -eq "Region"}).availabilityZoneMappings + + # Process each requested SKU + foreach ($SKU in $SKUs) { + # Find the specific SKU data for this location + $filteredSku = $computeSKUs | Where-Object { $_.Name.ToLowerInvariant() -eq $SKU.ToLowerInvariant() -and $_.LocationInfo.Location -like $Location } + + # Find the quota usage data for this SKU's family + $skuUsage = $vmUsage | Select-Object -ExpandProperty Name -Property CurrentValue, Limit | Where-Object { $_.Value -eq $filteredSku.Family } + + # Skip if SKU not found in this location + if ($null -eq $filteredSku ) { + continue + } + + # Skip if quota usage data not found + if ($null -eq $skuUsage) { + continue + } + + # Process availability zones (logical or physical) + $zones = @($filteredSku.LocationInfo.Zones) + if($UsePhysicalZones) + { + # Convert logical zones to physical zones + for ($i = 0; $i -lt $zones.Length; $i++) { + $zones[$i] = Get-LastChar(($availabilityZoneMappings | Where-Object {$_.LogicalZone -like $zones[$i]}).physicalZone) + } + } + + # Create the quota analysis object + $auditedSku = [PSCustomObject]@{ + TenantId = $Subscription.TenantId + SubscriptionId = $Subscription.Id + SubscriptionName = $Subscription.Name + Location = $Location + Family = $skuUsage.LocalizedValue # VM family name (e.g., "Dv3 Family vCPUs") + Size = $filteredSku.Name # Specific SKU (e.g., "Standard_D2s_v3") + RegionRestricted = 'False' # Whether SKU is restricted in this entire region + ZonesPresent = ($zones | Sort-Object) -join "," # Available zones for this SKU + ZonesRestricted = '' # Zones where SKU is restricted + CoresUsed = $skuUsage.CurrentValue # Current quota usage + CoresTotal = $skuUsage.Limit # Total quota limit + CoresRequested = '' # Placeholder for future use + ZonesRequested = '' # Placeholder for future use + } + + # Process any restrictions on this SKU + foreach ($restriction in $filteredSku.Restrictions) { + if ($restriction.Type -like "Zone") { + # Handle zone-specific restrictions + $zoneRestrictions = @($restriction.RestrictionInfo.Zones) + if($UsePhysicalZones) + { + # Convert restricted logical zones to physical zones + for ($i = 0; $i -lt $zoneRestrictions.Length; $i++) { + $zoneRestrictions[$i] = Get-LastChar(($availabilityZoneMappings | Where-Object {$_.LogicalZone -like $zoneRestrictions[$i]}).physicalZone) + } + } + $auditedSku.ZonesRestricted = ($zoneRestrictions | Sort-Object) -join "," + } + elseif ($restriction.Type -like "Location") { + # Handle region-wide restrictions + $auditedSku.RegionRestricted = 'True' + } + } + + # Append the result to the CSV file + $auditedSku | ConvertTo-Csv -NoHeader | Out-File -Force -Append -FilePath .\$OutputFile + } + } + } + catch { + Write-Host "Failed Querying Subscription ID: $SubscriptionId" -ForegroundColor Yellow + $_.Exception.Message + } + finally { + # Report processing time for this subscription + $end = Get-Date + "Processed: $SubscriptionId - $($Subscription.Name) in $([math]::Round((New-TimeSpan -Start $start -End $end).TotalSeconds, 2)) seconds" + } +} + +# ================================================================================ +# MAIN SCRIPT EXECUTION +# ================================================================================ + +# Set error handling and output preferences +$ErrorActionPreference = 'Stop' +$VerbosePreference = 'SilentlyContinue' +$begin = Get-Date + +# Define CSV header structure for output file +$csvHeaderString = "TenantId,SubscriptionId,SubscriptionName,Location,Family,Size,RegionRestricted,ZonesPresent,ZonesRestricted,CoresUsed,CoresTotal,CoresRequested,ZonesRequested" + +# Auto-detect optimal thread count if not specified +if($Threads -eq 0) +{ + try { + # Use WMI to detect CPU configuration + $spec = Get-WmiObject -Class Win32_Processor | Select-Object -Property NumberOfCores, NumberOfLogicalProcessors + if($spec.NumberOfLogicalProcessors -gt $spec.NumberOfCores) { + # Hyper-threading enabled: Use physical cores to avoid e-core performance issues + $Threads = $spec.NumberOfLogicalProcessors - $spec.NumberOfCores + } + else { + # Hyper-threading disabled: Use all available cores + $Threads = $spec.NumberOfCores + } + } + catch { + # Fallback to single-threaded if CPU detection fails + $Threads = 1 + } +} + +# Populate subscription list if not provided +if ($SubscriptionIds.Count -eq 0) { + $SubscriptionIds = Get-SubscriptionIds +} + +# Populate locations list if not provided, otherwise sort provided list +if ($Locations.Count -eq 0) { + $Locations = Get-Locations | Sort-Object +} else { + $Locations = $Locations | Sort-Object +} + +# Populate SKUs list if not provided +if ($SKUs.Count -eq 0) { + $SKUs = Get-SKUDetails +} + +# Display configuration information +if($UsePhysicalZones) +{ + Write-Host "Output will be normalized to physical availability zones" +} +else { + Write-Host "Output will not be normalized to physical availability zones" +} + +Write-Host "Querying $($SubscriptionIds.Count) subscriptions with $($SKUs.Count) SKUs in $($Locations.Count) locations using $Threads threads" + +# Execute quota analysis using parallel processing +$funcDef = ${function:Get-QuotaDetails}.ToString() +$SubscriptionIds | Foreach-Object -ThrottleLimit $Threads -Parallel { + # Import function definition into parallel runspace + ${function:Get-QuotaDetails} = $using:funcDef + + # Use separate output files for multi-threaded execution to avoid conflicts + if($USING:Threads -gt 1) { + $outFile = "QuotaQuery_{0}.csv" -f $PSItem + } else { + $outFile = $USING:OutputFile + } + + # Write CSV header to output file + $USING:csvHeaderString | Out-File -Force $outFile + + # Execute quota analysis for this subscription + Get-QuotaDetails -SubscriptionId $_ -Locations $USING:Locations -SKUs $USING:SKUs -outputFile $outFile +} + +# Merge multiple CSV files if multi-threaded execution was used +if($Threads -gt 1) { + Write-Host "Merging CSV files" + + # Create consolidated output file with header + $csvHeaderString | Out-File -Force -FilePath $OutputFile + + # Append content from all thread-specific files (skipping headers) + Get-ChildItem -Path .\QuotaQuery_*.csv | ForEach-Object { + Get-Content $_.FullName | Select-Object -Skip 1 | Add-Content $OutputFile + Remove-Item $_.FullName # Clean up temporary files + } +} + +# Display completion summary +Write-Host "Output written to $OutputFile" +$end = Get-Date +Write-Host "Processed $($SubscriptionIds.Count) subscriptions in $([math]::Round((New-TimeSpan -Start $begin -End $end).TotalSeconds, 2)) seconds" diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzVMQuotaUsageCli.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzVMQuotaUsageCli.ps1 new file mode 100644 index 000000000..d5b21ab3b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Get-AzVMQuotaUsageCli.ps1 @@ -0,0 +1,597 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +<# +.SYNOPSIS + Multi-threaded Azure VM quota analysis script that queries compute resource SKU availability, + quota usage, and availability zone restrictions across subscriptions using Azure CLI. + +.DESCRIPTION + This script analyzes Azure VM quota usage and availability zone restrictions across multiple + subscriptions and regions. It provides detailed information about: + - Current quota usage vs. limits for VM families + - Available and restricted availability zones per SKU + - Region-level restrictions for specific VM SKUs + - Physical vs. logical availability zone mappings + + The Catalogs API is the primary SKU source. The deprecated meter data CSV is used only as a + fallback when Catalogs API lookup fails and the SKUs parameter is not provided. + + The script supports multi-threading for faster execution across large numbers of subscriptions + and can normalize zone information to physical zones for cross-subscription deployment planning. + + This is the Azure CLI variant of Get-AzVMQuotaUsage.ps1. It uses `az` commands exclusively + and does not require the Azure PowerShell (Az) module. + +.PARAMETER SKUs + Array of VM SKU names to analyze. If not specified, resolves VM SKUs from the Azure Catalogs + API first and falls back to the deprecated meter data CSV only if that lookup fails. + Example: @('Standard_D2s_v3', 'Standard_D4s_v3', 'Standard_E4s_v5') + +.PARAMETER Locations + Array of Azure regions to analyze. If not specified, queries all physical Azure regions. + Example: @('eastus', 'westus2', 'centralus') + +.PARAMETER SubscriptionIds + Array of subscription IDs to analyze. If not specified, queries all accessible subscriptions. + Example: @('00000000-0000-0000-0000-000000000000') + +.PARAMETER MeterDataUri + Deprecated fallback source URI for normalized VM SKU data from Cost Management connector data. + Used only when the Catalogs API lookup fails and the SKUs parameter is not provided. + +.PARAMETER CatalogLocation + Azure region used for Catalogs API requests when resolving VM SKU names from the primary SKU + source. + +.PARAMETER UsePhysicalZones + Switch to normalize availability zone output to physical zones instead of logical zones. + Useful for cross-subscription deployment planning where logical zones may differ. + +.PARAMETER Threads + Number of concurrent threads for processing subscriptions. Set to 0 for auto-detection. + Higher values speed up execution but may hit API rate limits. + +.PARAMETER OutputFile + Name of the output CSV file containing the analysis results. + +.EXAMPLE + .\Get-AzVMQuotaUsageCli.ps1 -SKUs @('Standard_D2s_v5','Standard_E4s_v5') -Locations @('eastus','westus2') + + Analyzes specific VM SKUs in specific regions across all accessible subscriptions. + +.EXAMPLE + .\Get-AzVMQuotaUsageCli.ps1 -UsePhysicalZones -Threads 4 -OutputFile "MyQuotaAnalysis.csv" + + Runs full analysis with physical zone mapping using 4 threads. + +.NOTES + Requires Azure CLI (`az`) installed and an authenticated session (`az login`) with Reader + access to target subscriptions. The Az PowerShell module is not required. + Catalogs API access also requires the Microsoft.Capacity/catalogs/read permission. + Output CSV contains columns: TenantId, SubscriptionId, SubscriptionName, Location, Family, Size, + RegionRestricted, ZonesPresent, ZonesRestricted, CoresUsed, CoresTotal, CoresRequested, ZonesRequested +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('Standard_D2s_v3', 'Standard_D4s_v3')")] + [string[]]$SKUs = @(), + + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('eastus', 'westus')")] + [string[]]$Locations = @(), + + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('00000000-0000-0000-0000-000000000000')")] + [string[]]$SubscriptionIds = @(), + + [Parameter(Mandatory = $false, HelpMessage = "Deprecated fallback URL to download normalized VM SKU list")] + [string]$MeterDataUri = "https://ccmstorageprod.blob.core.windows.net/instancesizeflexibility-data/isfratioblob.csv", + + [Parameter(Mandatory = $false, HelpMessage = "Azure region to query for VM catalog data")] + [string]$CatalogLocation = "eastus", + + [Parameter(Mandatory = $false, HelpMessage = "Normalize output to physical availability zones")] + [switch]$UsePhysicalZones = $false, + + [Parameter(Mandatory = $false, HelpMessage = "Concurrent threads to use. Set to '0' for auto-detect")] + [ValidateRange(0, 40)] + [int]$Threads = 2, + + [Parameter(Mandatory = $false, HelpMessage = "Output file name")] + [string]$OutputFile = "QuotaQuery.csv" +) + +# ================================================================================ +# HELPER FUNCTIONS +# ================================================================================ + +<# +.SYNOPSIS + Retrieves normalized VM SKU names from the Azure Catalogs API, with CSV as fallback. + +.DESCRIPTION + Queries the Azure Catalogs API for normalized VM SKU names and uses that as the + primary source for quota analysis. If the Catalogs API call fails, falls back to + AutofitComboMeterData.csv. Filters out SQL-related SKUs and returns unique SKU names. + +.OUTPUTS + Array of normalized VM SKU names (e.g., 'Standard_D2s_v3', 'Standard_E4s_v5') +#> +function Get-SKUDetails { + try { + Write-Host "Querying Azure Catalogs API for VM SKU details in $CatalogLocation" + $catalogData = Get-VMCatalogData -SubscriptionId $SubscriptionIds[0] -Location $CatalogLocation + [string[]]$skuNames = @( + ($catalogData | Select-Object -Property ArmSkuName -Unique | Where-Object { $_.ArmSkuName -notlike "*sql*" }).ArmSkuName | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + + if ($skuNames.Count -eq 0) { + throw "Catalogs API returned zero VM SKUs" + } + + Write-Host "Retrieved $($skuNames.Count) unique VM SKUs from Catalogs API" + return $skuNames + } + catch { + Write-Warning "Catalogs API failed: $($_.Exception.Message)" + Write-Warning "Falling back to CSV download from $MeterDataUri" + + # Extract filename from URI for local storage + [string]$meterDataFile = $MeterDataUri.Split('/')[-1] + + # Download the Cost Management connector data file + Invoke-WebRequest -Uri $MeterDataUri -OutFile $meterDataFile + + # Parse CSV and extract unique, non-SQL VM SKUs + $meterData = Get-Content $meterDataFile | ConvertFrom-Csv + [string[]]$skuNames = @( + ($meterData | Select-Object -Property ArmSkuName -Unique | Where-Object { $_.ArmSkuName -notlike "*sql*" }).ArmSkuName | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + return $skuNames + } +} + +<# +.SYNOPSIS + Retrieves VM catalog data from the Azure Microsoft.Capacity catalogs API using Azure CLI. + +.DESCRIPTION + Calls the Azure catalogs endpoint for VirtualMachines in the specified subscription + and location using `az rest`. Follows any nextLink values until all pages are + retrieved and returns only entries that contain the required instance size + flexibility properties. + +.PARAMETER SubscriptionId + The Azure subscription ID used to scope the catalogs API request. + +.PARAMETER Location + The Azure region name used in the catalogs API filter. + +.OUTPUTS + Array of PSCustomObject values with InstanceSizeFlexibilityGroup, ArmSkuName, and Ratio properties. +#> +function Get-VMCatalogData { + param ( + [string]$SubscriptionId, + [string]$Location + ) + + $catalogItems = [System.Collections.Generic.List[object]]::new() + + # Get the resource manager endpoint for the current cloud (supports sovereign clouds) + $resourceManagerUrl = (az cloud show --query endpoints.resourceManager -o tsv).TrimEnd('/') + + $uri = "{0}/subscriptions/{1}/providers/Microsoft.Capacity/catalogs?api-version=2022-11-01&reservedResourceType=VirtualMachines&location={2}" -f $resourceManagerUrl, $SubscriptionId, $Location + + do { + $response = az rest --method GET --url $uri -o json | ConvertFrom-Json + + foreach ($item in @($response.value)) { + $groupProperty = @($item.skuProperties | Where-Object { $_.name -eq 'ReservationsAutofitGroup' } | Select-Object -First 1) + $ratioProperty = @($item.skuProperties | Where-Object { $_.name -eq 'ReservationsAutofitRatio' } | Select-Object -First 1) + + if ([string]::IsNullOrWhiteSpace([string]$item.name) -or + $groupProperty.Count -eq 0 -or + $ratioProperty.Count -eq 0 -or + [string]::IsNullOrWhiteSpace([string]$groupProperty[0].value) -or + [string]::IsNullOrWhiteSpace([string]$ratioProperty[0].value)) { + continue + } + + $catalogItems.Add([PSCustomObject]@{ + InstanceSizeFlexibilityGroup = $groupProperty[0].value + ArmSkuName = $item.name + Ratio = $ratioProperty[0].value + }) + } + + $uri = $response.nextLink + } while (-not [string]::IsNullOrWhiteSpace($uri)) + + return $catalogItems.ToArray() +} + +<# +.SYNOPSIS + Retrieves all accessible subscription IDs within the current tenant using Azure CLI. + +.DESCRIPTION + Gets all Azure subscriptions that the current authenticated user has access to + within the current tenant context. Uses `az account list` filtered by the current + tenant ID obtained from `az account show`. + +.OUTPUTS + Array of subscription ID strings +#> +function Get-SubscriptionIds { + Write-Host "Listing Subscriptions" + + # Get the current tenant ID from the active CLI session + $tenantId = az account show --query tenantId -o tsv + + # List all subscriptions in this tenant and return their IDs + $subscriptions = az account list --query "[?tenantId=='$tenantId'].id" -o json | ConvertFrom-Json + return $subscriptions +} + +<# +.SYNOPSIS + Extracts the last character from a string. + +.DESCRIPTION + Utility function used to extract physical zone identifiers from availability zone mappings. + Physical zones are typically identified by the last character of the physical zone string. + +.PARAMETER inputString + The input string to extract the last character from. + +.OUTPUTS + The last character of the input string, or empty string if input is null/empty. +#> +function Get-LastChar { + param ( + [string]$inputString + ) + + if ([string]::IsNullOrEmpty($inputString)) { + return "" + } + return $inputString[-1] +} + +<# +.SYNOPSIS + Retrieves all physical Azure regions using Azure CLI. + +.DESCRIPTION + Gets all Azure regions that are physical locations (not logical regions) + and have a defined physical location. Uses `az account list-locations` with + a JMESPath query to filter out logical regions and regions without physical presence. + +.OUTPUTS + Array of Azure region names (e.g., 'eastus', 'westus2', 'centralus') +#> +function Get-Locations { + Write-Host "Listing Locations" + + # Query physical regions only — exclude logical regions and those without a physical location. + # The Az module equivalent filters on RegionType == 'Physical' and PhysicalLocation != ''. + # az account list-locations exposes this via metadata.regionType and metadata.physicalLocation. + $locations = az account list-locations --query "[?metadata.regionType=='Physical' && metadata.physicalLocation!=''].name" -o json | ConvertFrom-Json + return $locations +} + +<# +.SYNOPSIS + Retrieves availability zone peering information for a subscription using Azure CLI. + +.DESCRIPTION + Gets the logical-to-physical availability zone mappings for all regions + within a specific subscription. This information is crucial for understanding + how logical zones (1, 2, 3) map to physical zones across different subscriptions. + +.PARAMETER SubscriptionId + The Azure subscription ID to query for zone peering information. + +.OUTPUTS + Array of location objects containing availability zone mapping details. + +.NOTES + Uses the Azure REST API via `az rest` as this information is not available through + standard Azure CLI commands. The resource manager endpoint is obtained from `az cloud show`. +#> +function Get-ZonePeers { + param ( + [string]$SubscriptionId + ) + Write-Host "Get Zone Peering Information for subscription $SubscriptionId" + + # Get the resource manager endpoint for the current cloud (supports sovereign clouds) + # TrimEnd('/') ensures sovereign clouds (AzureUSGovernment, AzureChinaCloud) that omit + # a trailing slash don't produce a malformed URI like "https://...netsubscriptions/..." + $resourceManagerUrl = (az cloud show --query endpoints.resourceManager -o tsv).TrimEnd('/') + + # Construct REST API URI for location zone mappings + $uri = "${resourceManagerUrl}/subscriptions/${SubscriptionId}/locations?api-version=2022-12-01" + + # Call Azure REST API to get zone peering data; pass --subscription to avoid context issues + $response = az rest --method GET --url $uri --subscription $SubscriptionId -o json | ConvertFrom-Json + return $response.value +} + +<# +.SYNOPSIS + Analyzes VM quota usage and restrictions for a specific subscription using Azure CLI. + +.DESCRIPTION + Core function that performs detailed quota analysis for a single subscription. + Queries compute resource SKUs, VM usage statistics, and availability zone + restrictions across specified locations and SKUs using `az` CLI commands. + + Unlike the Az module variant, this function passes --subscription to each + az command rather than calling az account set, which avoids race conditions + when running in parallel across multiple subscriptions. + + UsePhysicalZones is accepted as a parameter (not inherited from script scope) + so that parallel runspaces can correctly receive the switch value via $using:. + +.PARAMETER SubscriptionId + The Azure subscription ID to analyze. + +.PARAMETER Locations + Array of Azure regions to analyze within this subscription. + +.PARAMETER SKUs + Array of VM SKU names to analyze. + +.PARAMETER OutputFile + Name of the CSV file to append results to. + +.OUTPUTS + Appends CSV rows to the output file with quota and restriction details. + +.NOTES + This function is designed to run in parallel across multiple subscriptions + for optimal performance. Each execution handles one subscription completely. + Pass --subscription to every az command; do not use az account set inside + parallel runspaces as it is process-global and causes race conditions. +#> +function Get-QuotaDetails { + param ( + [string]$SubscriptionId, + [string[]]$Locations, + [string[]]$SKUs, + [string]$OutputFile, + [bool]$UsePhysicalZones = $false + ) + + $start = Get-Date + try { + # Get subscription details and validate access using Azure CLI. + # Pass --subscription so we never change the global az context. + $subscriptionJson = az account show --subscription $SubscriptionId -o json 2>$null | ConvertFrom-Json + if ($null -eq $subscriptionJson) { + throw "Subscription not found" + } + + # Build a subscription object with the same field names used throughout the function + $Subscription = [PSCustomObject]@{ + Id = $subscriptionJson.id + Name = $subscriptionJson.name + TenantId = $subscriptionJson.tenantId + } + + # Get zone peering information if physical zone mapping is requested + if ($UsePhysicalZones) { + $zonePeers = Get-ZonePeers -SubscriptionId $SubscriptionId + if ($zonePeers.Count -eq 0) { + Write-Host "No Zone Peering Information found for subscription $SubscriptionId" -ForegroundColor Yellow + } + } + + # Process each location within the subscription + foreach ($Location in $Locations) { + "Querying: $SubscriptionId - $($Subscription.Name) - $Location" + + # Get all VM SKUs available in this location using Azure CLI. + # az vm list-skus returns objects where resourceType is lowercase. + $computeSKUs = az vm list-skus --location $Location --resource-type virtualMachines --subscription $SubscriptionId -o json 2>$null | ConvertFrom-Json + + # Get current VM quota usage for this location using Azure CLI. + # Returns objects with currentValue, limit, and name.value / name.localizedValue. + $vmUsage = az vm list-usage --location $Location --subscription $SubscriptionId -o json 2>$null | ConvertFrom-Json + + # Get availability zone mappings for this location (if using physical zones) + $availabilityZoneMappings = ($zonePeers | Where-Object { $_.name -like $Location -and $_.type -eq "Region" }).availabilityZoneMappings + + # Process each requested SKU + foreach ($SKU in $SKUs) { + # Find the specific SKU data for this location. + # az vm list-skus locationInfo is an array; check locationInfo[0].location. + $filteredSku = $computeSKUs | Where-Object { + $_.name.ToLowerInvariant() -eq $SKU.ToLowerInvariant() -and + $_.locationInfo.Count -gt 0 -and + $_.locationInfo[0].location -like $Location + } + + # Skip if SKU not found in this location + if ($null -eq $filteredSku) { + continue + } + + # Find the quota usage data for this SKU's family. + # In az vm list-usage output: name.value holds the family identifier (e.g. standardDSv5Family) + # and name.localizedValue holds the human-readable label. + $skuUsage = $vmUsage | Where-Object { $_.name.value -eq $filteredSku.family } + + # Skip if quota usage data not found + if ($null -eq $skuUsage) { + continue + } + + # Process availability zones (logical or physical). + # az vm list-skus stores zones in locationInfo[0].zones. + # Guard against empty locationInfo to avoid index-out-of-bounds on SKUs + # that have no location info entries (returns empty array instead of throwing). + $zones = if ($filteredSku.locationInfo.Count -gt 0) { @($filteredSku.locationInfo[0].zones) } else { @() } + if ($UsePhysicalZones) { + # Convert logical zones to physical zones + for ($i = 0; $i -lt $zones.Length; $i++) { + $zones[$i] = Get-LastChar(($availabilityZoneMappings | Where-Object { $_.LogicalZone -like $zones[$i] }).physicalZone) + } + } + + # Create the quota analysis object + $auditedSku = [PSCustomObject]@{ + TenantId = $Subscription.TenantId + SubscriptionId = $Subscription.Id + SubscriptionName = $Subscription.Name + Location = $Location + Family = $skuUsage.name.localizedValue # VM family name (e.g., "Dv3 Family vCPUs") + Size = $filteredSku.name # Specific SKU (e.g., "Standard_D2s_v3") + RegionRestricted = 'False' # Whether SKU is restricted in this entire region + ZonesPresent = ($zones | Sort-Object) -join "," # Available zones for this SKU + ZonesRestricted = '' # Zones where SKU is restricted + CoresUsed = $skuUsage.currentValue # Current quota usage + CoresTotal = $skuUsage.limit # Total quota limit + CoresRequested = '' # Placeholder for future use + ZonesRequested = '' # Placeholder for future use + } + + # Process any restrictions on this SKU. + # az vm list-skus restrictions[].type is lowercase (e.g. "Zone", "Location"). + foreach ($restriction in $filteredSku.restrictions) { + if ($restriction.type -like "Zone") { + # Handle zone-specific restrictions. + # az vm list-skus stores restricted zones in restrictionInfo.zones. + $zoneRestrictions = @($restriction.restrictionInfo.zones) + if ($UsePhysicalZones) { + # Convert restricted logical zones to physical zones + for ($i = 0; $i -lt $zoneRestrictions.Length; $i++) { + $zoneRestrictions[$i] = Get-LastChar(($availabilityZoneMappings | Where-Object { $_.LogicalZone -like $zoneRestrictions[$i] }).physicalZone) + } + } + $auditedSku.ZonesRestricted = ($zoneRestrictions | Sort-Object) -join "," + } + elseif ($restriction.type -like "Location") { + # Handle region-wide restrictions + $auditedSku.RegionRestricted = 'True' + } + } + + # Append the result to the CSV file + $auditedSku | ConvertTo-Csv -NoHeader | Out-File -Force -Append -FilePath .\$OutputFile + } + } + } + catch { + Write-Host "Failed Querying Subscription ID: $SubscriptionId" -ForegroundColor Yellow + $_.Exception.Message + } + finally { + # Report processing time for this subscription + $end = Get-Date + "Processed: $SubscriptionId - $($Subscription.Name) in $([math]::Round((New-TimeSpan -Start $start -End $end).TotalSeconds, 2)) seconds" + } +} + +# ================================================================================ +# MAIN SCRIPT EXECUTION +# ================================================================================ + +# Set error handling and output preferences +$ErrorActionPreference = 'Stop' +$VerbosePreference = 'SilentlyContinue' +$begin = Get-Date + +# Define CSV header structure for output file +$csvHeaderString = "TenantId,SubscriptionId,SubscriptionName,Location,Family,Size,RegionRestricted,ZonesPresent,ZonesRestricted,CoresUsed,CoresTotal,CoresRequested,ZonesRequested" + +# Auto-detect optimal thread count if not specified. +# Uses [Environment]::ProcessorCount (cross-platform) instead of WMI (Windows-only). +if ($Threads -eq 0) { + try { + $Threads = [Math]::Max(1, [Environment]::ProcessorCount / 2) + } + catch { + # Fallback to single-threaded if CPU detection fails + $Threads = 1 + } +} + +# Populate subscription list if not provided +if ($SubscriptionIds.Count -eq 0) { + $SubscriptionIds = Get-SubscriptionIds +} + +# Populate locations list if not provided, otherwise sort provided list +if ($Locations.Count -eq 0) { + $Locations = Get-Locations | Sort-Object +} else { + $Locations = $Locations | Sort-Object +} + +# Populate SKUs list if not provided +if ($SKUs.Count -eq 0) { + $SKUs = Get-SKUDetails +} + +# Display configuration information +if ($UsePhysicalZones) { + Write-Host "Output will be normalized to physical availability zones" +} +else { + Write-Host "Output will not be normalized to physical availability zones" +} + +Write-Host "Querying $($SubscriptionIds.Count) subscriptions with $($SKUs.Count) SKUs in $($Locations.Count) locations using $Threads threads" + +# Execute quota analysis using parallel processing. +# Function definition is serialized as a string and re-imported in each runspace, +# because parallel runspaces do not inherit the calling scope's function definitions. +# IMPORTANT: az account set is NOT used inside the parallel block. Every az command +# receives --subscription explicitly to avoid process-global context race conditions. +$funcDef = ${function:Get-QuotaDetails}.ToString() +$lastCharFuncDef = ${function:Get-LastChar}.ToString() +$getZonePeersFuncDef = ${function:Get-ZonePeers}.ToString() + +$SubscriptionIds | Foreach-Object -ThrottleLimit $Threads -Parallel { + # Import all required function definitions into the parallel runspace + ${function:Get-QuotaDetails} = $using:funcDef + ${function:Get-LastChar} = $using:lastCharFuncDef + ${function:Get-ZonePeers} = $using:getZonePeersFuncDef + + # Use separate output files for multi-threaded execution to avoid write conflicts + if ($USING:Threads -gt 1) { + $outFile = "QuotaQuery_{0}.csv" -f $PSItem + } else { + $outFile = $USING:OutputFile + } + + # Write CSV header to output file + $USING:csvHeaderString | Out-File -Force $outFile + + # Execute quota analysis for this subscription + Get-QuotaDetails -SubscriptionId $_ -Locations $USING:Locations -SKUs $USING:SKUs -OutputFile $outFile -UsePhysicalZones ([bool]$USING:UsePhysicalZones) +} + +# Merge multiple CSV files if multi-threaded execution was used +if ($Threads -gt 1) { + Write-Host "Merging CSV files" + + # Create consolidated output file with header + $csvHeaderString | Out-File -Force -FilePath $OutputFile + + # Append content from all thread-specific files (skipping headers) + Get-ChildItem -Path .\QuotaQuery_*.csv | ForEach-Object { + Get-Content $_.FullName | Select-Object -Skip 1 | Add-Content $OutputFile + Remove-Item $_.FullName # Clean up temporary files + } +} + +# Display completion summary +Write-Host "Output written to $OutputFile" +$end = Get-Date +Write-Host "Processed $($SubscriptionIds.Count) subscriptions in $([math]::Round((New-TimeSpan -Start $begin -End $end).TotalSeconds, 2)) seconds" diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/README.md new file mode 100644 index 000000000..c4a7fe4df --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/README.md @@ -0,0 +1,219 @@ +--- +title: Quota Management +--- + +# Quota management scripts + +These scripts are intended to analyze quota usage across subscriptions and regions in scenarios where Quota Groups **have not** been implemented. + +> [!TIP] +> Use these scripts alongside Azure quota and availability zone guidance so your analysis matches the way Azure enforces vCPU and zone limits.[^vm-quotas][^az-zones][^az-quota] + +--- + +## What you'll need + +Here's what you'll need to get started: + +1. **Azure PowerShell Module**: Install the latest version + ```powershell + Install-Module -Name Az -Repository PSGallery -Force + ``` + +2. **Authentication**: Sign in to your Azure account + ```powershell + Connect-AzAccount + ``` + +3. **Permissions**: Reader access to target subscriptions + +4. **PowerShell Version**: + - PowerShell 5.1+ for single-threaded scripts + - PowerShell 7+ recommended for best performance + +--- + +## Available scripts + +### Get-AzVMQuotaUsage.ps1 (Recommended) + +**Multi-threaded quota analysis** - Fast parallel processing across multiple subscriptions. + +**Requirements**: Azure PowerShell module, PowerShell 7+ for multi-threading +**Best for**: Large-scale analysis + +```powershell +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/azcapman/main/scripts/quota/Get-AzVMQuotaUsage.ps1" -OutFile "Get-AzVMQuotaUsage.ps1" +.\Get-AzVMQuotaUsage.ps1 -SKUs @('Standard_D2s_v5', 'Standard_E2s_v5') -Locations @('eastus', 'westus2') -Threads 4 +``` + +What you'll get: +- Current quota usage vs. limits for VM families +- Available and restricted availability zones per SKU +- Region-level restrictions for specific VM SKUs +- Physical vs. logical availability zone mappings +- Multi-threading for faster processing + +**Script Output:** + +|TenantId|SubscriptionId|SubscriptionName|Location|Family|Size|RegionRestricted|ZonesPresent|ZonesRestricted|CoresUsed|CoresTotal| +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +|...|...|...|eastus|standardDSv5Family|Standard_D2s_v5|False|1,2,3||0|100| +|...|...|...|westus2|standardESv5Family|Standard_E2s_v5|False|1,2,3||0|100| +|...|...|...|westus3|standardFSv2Family|Standard_F2s_v2|True|1,2,3|1,2,3|0|100| + +**What each column means:** +- **TenantId**: Your Microsoft Entra tenant ID +- **SubscriptionId**: Your subscription ID +- **SubscriptionName**: Friendly name you've given your subscription +- **Location**: Azure region name +- **Family**: VM family (used for quota calculations) +- **Size**: Specific VM SKU name +- **RegionRestricted**: Whether the SKU has regional restrictions +- **ZonesPresent**: Available availability zones for the SKU +- **ZonesRestricted**: Restricted availability zones for the SKU +- **CoresUsed**: vCPU cores you're currently using +- **CoresTotal**: Total vCPU cores available to you + +--- + +### Show-AzVMQuotaReport.ps1 (Single-threaded) +**Simple quota analysis** - Single-threaded version for smaller projects. + +**Requirements**: Azure PowerShell module, PowerShell 5.1+ +**Best for**: Smaller analysis or learning + +```powershell +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/azcapman/main/scripts/quota/Show-AzVMQuotaReport.ps1" -OutFile "Show-AzVMQuotaReport.ps1" +.\Show-AzVMQuotaReport.ps1 -SKUs @('Standard_D2s_v5', 'Standard_E2s_v5') -Locations @('eastus', 'westus2') +``` + +This script provides: +- Sequential processing across subscriptions +- Formatted table display of results +- Same core functionality as the multi-threaded version +- Streamlined for educational purposes + +**Script Output:** + +|TenantId|SubscriptionId|SubscriptionName|Location|Family|Size|RegionRestricted|ZonesPresent|ZonesRestricted|CoresUsed|CoresTotal| +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +|...|...|...|eastus|standardDSv5Family|Standard_D2s_v5|False|1,2,3||0|100| +|...|...|...|westus2|standardESv5Family|Standard_E2s_v5|False|1,2,3||0|100| +|...|...|...|westus3|standardFSv2Family|Standard_F2s_v2|True|1,2,3|1,2,3|0|100| + +--- + +### Get-AzAvailabilityZoneMapping.ps1 (Zone Analysis) +**Zone mapping analysis** - Shows how logical zones map to physical zones across subscriptions. + +**Requirements**: Azure PowerShell module, Reader access to target subscriptions +**Best for**: Cross-subscription deployment planning + +```powershell +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/azcapman/main/scripts/quota/Get-AzAvailabilityZoneMapping.ps1" -OutFile "Get-AzAvailabilityZoneMapping.ps1" +.\Get-AzAvailabilityZoneMapping.ps1 -SubscriptionIds @('sub1-id','sub2-id') -OutputFile "ZoneMappings.csv" +``` + +This script provides: +- Logical to physical zone mappings per subscription +- Physical zone identifiers and names +- Essential data for multi-subscription architectures +- Compliance with data residency requirements + +**Script Output:** + +|TenantId|SubscriptionId|SubscriptionName|Location|LogicalZone|PhysicalZone|PhysicalZoneName| +|--------|--------|--------|--------|--------|--------|--------| +|...|...|...|eastus|1|A|eastus-az1| +|...|...|...|eastus|2|B|eastus-az2| +|...|...|...|eastus|3|C|eastus-az3| + +--- + +## Manual quota analysis examples + +### Check your current quota + +```powershell +$Location = 'West US 2' +$VMSize = 'Standard_D4d_v4' +$SKU = Get-AzComputeResourceSku -Location $Location | Where-Object ResourceType -eq "virtualMachines" | Select-Object Name,Family +$VMFamily = ($SKU | Where-Object Name -eq $VMSize | Select-Object -Property Family).Family +$Usage = Get-AzVMUsage -Location $Location | Where-Object { $_.Name.Value -eq $VMFamily } +``` + +--- + +## Script parameters + +### Common parameters (all scripts) + +- **SKUs**: Array of VM SKU names to analyze (e.g., `@('Standard_D2s_v5', 'Standard_E4s_v5')`) +- **Locations**: Array of Azure regions (e.g., `@('eastus', 'westus2')`) +- **SubscriptionIds**: Array of subscription IDs to analyze +- **OutputFile**: Name of the output CSV file + +--- + +### Get-AzVMQuotaUsage.ps1 Specific Parameters + +- **Threads**: Number of concurrent threads (default: 2, set to 0 for auto-detect) +- **UsePhysicalZones**: Switch to normalize output to physical zones +- **MeterDataUri**: URL for downloading VM SKU list + +--- + +### Get-AzAvailabilityZoneMapping.ps1 Specific Parameters + +- **OutputFile**: Default filename is "ZonePeers.csv" + +You'll need Azure PowerShell installed and signed in to use these scripts. + +--- + +## Troubleshooting + +### Common issues + +**Authentication Errors**: +```powershell +# Ensure you're signed in +Connect-AzAccount +# Verify current context +Get-AzContext +``` + +**Permission Denied**: +- Verify you have Reader access to target subscriptions +- Check if the subscription is in a different tenant + +**Empty Results**: +- Confirm the specified regions support the requested VM SKUs +- Verify subscription IDs are correct +- Check if regions have restricted access + +**Multi-threading Issues** (Get-AzVMQuotaUsage.ps1): +- Reduce the number of threads if hitting API limits +- Use PowerShell 7+ for optimal multi-threading support + +**Rate Limiting**: +- Reduce concurrent operations +- Add delays between API calls if necessary + +### Getting help + +For issues with these scripts: +1. Check the [Azure PowerShell documentation](https://learn.microsoft.com/en-us/powershell/azure/) +2. Review Azure subscription and quota limits +3. Contact Azure support for quota increase requests + +## Script versions + +These scripts are maintained in the [azcapman repository](https://github.com/microsoft/azcapman). Check for updates regularly to get the latest features and bug fixes. + +--- + +[^vm-quotas]: [Check vCPU quotas - Azure Virtual Machines](https://learn.microsoft.com/en-us/azure/virtual-machines/quotas) +[^az-quota]: [az quota CLI reference](https://learn.microsoft.com/en-us/cli/azure/quota?view=azure-cli-latest) +[^az-zones]: [Availability zones – physical and logical mapping](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview#configuring-resources-for-availability-zone-support) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Show-AzVMQuotaReport.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Show-AzVMQuotaReport.ps1 new file mode 100644 index 000000000..2939144c1 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/quota/Show-AzVMQuotaReport.ps1 @@ -0,0 +1,517 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +<# +.SYNOPSIS + Single-threaded Azure VM quota analysis script that queries compute resource SKU availability, + quota usage, and availability zone restrictions across subscriptions. + +.DESCRIPTION + This script provides simplified quota analysis functionality for Azure VM resources. + It's designed as a streamlined version for smaller-scale analysis or educational purposes. + The script analyzes: + - Current quota usage vs. limits for VM families + - Available and restricted availability zones per SKU + - Region-level restrictions for specific VM SKUs + - Physical vs. logical availability zone mappings (optional) + + The Catalogs API is the primary SKU source. The deprecated meter data CSV is used only as a + fallback when Catalogs API lookup fails and the SKUs parameter is not provided. + + Unlike the multi-threaded Get-AzVMQuotaUsage.ps1, this version processes subscriptions + sequentially and displays results in a formatted table at the end. + +.PARAMETER SKUs + Array of VM SKU names to analyze. If not specified, resolves VM SKUs from the Azure Catalogs + API first and falls back to the deprecated meter data CSV only if that lookup fails. + Example: @('Standard_D2s_v3', 'Standard_D4s_v3', 'Standard_E4s_v5') + +.PARAMETER Locations + Array of Azure regions to analyze. If not specified, queries all physical Azure regions. + Example: @('eastus', 'westus2', 'centralus') + +.PARAMETER SubscriptionIds + Array of subscription IDs to analyze. If not specified, queries all accessible subscriptions. + Example: @('00000000-0000-0000-0000-000000000000') + +.PARAMETER MeterDataUri + Deprecated fallback source URI for normalized VM SKU data from Cost Management connector data. + Used only when the Catalogs API lookup fails and the SKUs parameter is not provided. + +.PARAMETER CatalogLocation + Azure region used for Catalogs API requests when resolving VM SKU names from the primary SKU + source. + +.PARAMETER OutputFile + Name of the output CSV file containing the analysis results. + Default: "QuotaQuery.csv" + +.PARAMETER UsePhysicalZones + Switch to normalize availability zone output to physical zones instead of logical zones. + Useful for cross-subscription deployment planning where logical zones may differ. + +.EXAMPLE + .\Show-AzVMQuotaReport.ps1 -SKUs @('Standard_D2s_v5','Standard_E4s_v5') -Locations @('eastus','westus2') + + Analyzes specific VM SKUs in specific regions and displays results in table format. + +.EXAMPLE + .\Show-AzVMQuotaReport.ps1 -UsePhysicalZones -OutputFile "MyQuotaAnalysis.csv" + + Runs full analysis with physical zone mapping and custom output filename. + +.NOTES + Requires Azure PowerShell module and authenticated session with Reader access to target subscriptions. + Catalogs API access also requires the Microsoft.Capacity/catalogs/read permission. + This is a simplified, educational version of the quota analysis functionality. + For production use with large numbers of subscriptions, consider using Get-AzVMQuotaUsage.ps1 instead. + + Output CSV contains columns: TenantId, SubscriptionId, SubscriptionName, Location, Family, Size, + RegionRestricted, ZonesPresent, ZonesRestricted, CoresUsed, CoresTotal +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('Standard_D2s_v3', 'Standard_D4s_v3')")] + [string[]]$SKUs = @(), + + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('eastus', 'westus')")] + [string[]]$Locations = @(), + + [Parameter(Mandatory = $false, HelpMessage = "e.g. @('00000000-0000-0000-0000-000000000000')")] + [string[]]$SubscriptionIds = @(), + + [Parameter(Mandatory = $false, HelpMessage = "Deprecated fallback URL to download normalized VM SKU list")] + [string]$MeterDataUri = "https://ccmstorageprod.blob.core.windows.net/instancesizeflexibility-data/isfratioblob.csv", + + [Parameter(Mandatory = $false, HelpMessage = "Azure region to query for VM catalog data")] + [string]$CatalogLocation = "eastus", + + [Parameter(Mandatory = $false, HelpMessage = "Output CSV filename")] + [string]$OutputFile = "QuotaQuery.csv", + + [Parameter(Mandatory = $false, HelpMessage = "Normalize output to physical availability zones")] + [switch]$UsePhysicalZones = $false +) + +# ================================================================================ +# HELPER FUNCTIONS +# ================================================================================ + +<# +.SYNOPSIS + Retrieves normalized VM SKU names from the Azure Catalogs API, with CSV as fallback. + +.DESCRIPTION + Queries the Azure Catalogs API for normalized VM SKU names and uses that as the + primary source for quota analysis. If the Catalogs API call fails, falls back to + AutofitComboMeterData.csv. Filters out SQL-related SKUs and returns unique SKU names. + +.OUTPUTS + Array of normalized VM SKU names (e.g., 'Standard_D2s_v3', 'Standard_E4s_v5') +#> +function Get-SKUDetails { + try { + Write-Host "Querying Azure Catalogs API for VM SKU details in $CatalogLocation" + $catalogData = Get-VMCatalogData -SubscriptionId $SubscriptionIds[0] -Location $CatalogLocation + [string[]]$skuNames = @( + ($catalogData | Select-Object -Property ArmSkuName -Unique | Where-Object { $_.ArmSkuName -notlike "*sql*" }).ArmSkuName | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + + if ($skuNames.Count -eq 0) { + throw "Catalogs API returned zero VM SKUs" + } + + Write-Host "Retrieved $($skuNames.Count) unique VM SKUs from Catalogs API" + return $skuNames + } + catch { + Write-Warning "Catalogs API failed: $($_.Exception.Message)" + Write-Warning "Falling back to CSV download from $MeterDataUri" + + # Extract filename from URI for local storage (more robust approach) + [string]$meterDataFile = $MeterDataUri.Split('/')[$MeterDataUri.Split('/').Length - 1] + + # Download the Cost Management connector data file + Invoke-WebRequest -Uri $MeterDataUri -OutFile $meterDataFile + + # Parse CSV and extract unique, non-SQL VM SKUs + $meterData = Get-Content $meterDataFile | ConvertFrom-Csv + [string[]]$skuNames = @( + ($meterData | Select-Object -Property ArmSkuName -Unique | Where-Object { $_.ArmSkuName -notlike "*sql*" }).ArmSkuName | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + return $skuNames + } +} + +<# +.SYNOPSIS + Retrieves VM catalog data from the Azure Microsoft.Capacity catalogs API. + +.DESCRIPTION + Calls the Azure catalogs endpoint for VirtualMachines in the specified subscription + and location using Invoke-AzRestMethod. Follows any nextLink values until all pages + are retrieved and returns only entries that contain the required instance size + flexibility properties. + +.PARAMETER SubscriptionId + The Azure subscription ID used to scope the catalogs API request. + +.PARAMETER Location + The Azure region name used in the catalogs API filter. + +.OUTPUTS + Array of PSCustomObject values with InstanceSizeFlexibilityGroup, ArmSkuName, and Ratio properties. +#> +function Get-VMCatalogData { + param ( + [string]$SubscriptionId, + [string]$Location + ) + + $catalogItems = [System.Collections.Generic.List[object]]::new() + $path = "/subscriptions/{0}/providers/Microsoft.Capacity/catalogs?api-version=2022-11-01&reservedResourceType=VirtualMachines&location={1}" -f $SubscriptionId, $Location + $nextLink = $null + + do { + $response = if ([string]::IsNullOrWhiteSpace($nextLink)) { + Invoke-AzRestMethod -Method GET -Path $path + } + else { + Invoke-AzRestMethod -Method GET -Uri $nextLink + } + + $payload = $response.Content | ConvertFrom-Json -Depth 10 + + foreach ($item in @($payload.value)) { + $groupProperty = @($item.skuProperties | Where-Object { $_.name -eq 'ReservationsAutofitGroup' } | Select-Object -First 1) + $ratioProperty = @($item.skuProperties | Where-Object { $_.name -eq 'ReservationsAutofitRatio' } | Select-Object -First 1) + + if ([string]::IsNullOrWhiteSpace([string]$item.name) -or + $groupProperty.Count -eq 0 -or + $ratioProperty.Count -eq 0 -or + [string]::IsNullOrWhiteSpace([string]$groupProperty[0].value) -or + [string]::IsNullOrWhiteSpace([string]$ratioProperty[0].value)) { + continue + } + + $catalogItems.Add([PSCustomObject]@{ + InstanceSizeFlexibilityGroup = $groupProperty[0].value + ArmSkuName = $item.name + Ratio = $ratioProperty[0].value + }) + } + + $nextLink = $payload.nextLink + } while (-not [string]::IsNullOrWhiteSpace($nextLink)) + + return $catalogItems.ToArray() +} + +<# +.SYNOPSIS + Retrieves all accessible subscription IDs within the current tenant. + +.DESCRIPTION + Gets all Azure subscriptions that the current authenticated user has access to + within the current tenant context. Used when no specific subscription IDs are provided. + +.OUTPUTS + Array of subscription ID strings +#> +function Get-SubscriptionIds { + Write-Host "Listing Subscriptions" + return (Get-AzSubscription -TenantId ((Get-AzContext).Tenant.TenantId) | Select-Object -ExpandProperty SubscriptionId) +} + +<# +.SYNOPSIS + Extracts the last character from a string. + +.DESCRIPTION + Utility function used to extract the physical zone identifier from the full physical zone name. + Physical zones are typically identified by the last character (e.g., 'A', 'B', 'C') of the + full physical zone string (e.g., "eastus-az1" -> "1", "westus2-azA" -> "A"). + +.PARAMETER inputString + The input string to extract the last character from. + +.OUTPUTS + The last character of the input string, or empty string if input is null/empty. +#> +function Get-LastChar { + param ( + [string]$inputString + ) + + # Handle null or empty input + if ([string]::IsNullOrEmpty($inputString)) { + return "" + } + + # Additional length check for safety (though redundant with IsNullOrEmpty) + if ($inputString.Length -lt 1) { + return "" + } + + # Return the last character + return $inputString[-1] +} + +<# +.SYNOPSIS + Retrieves all physical Azure regions. + +.DESCRIPTION + Gets all Azure regions that are physical locations (not logical regions) + and have a defined physical location. Filters out logical regions and + regions without physical presence. + +.OUTPUTS + Array of Azure region names (e.g., 'eastus', 'westus2', 'centralus') +#> +function Get-Locations { + Write-Host "Listing Locations" + return (Get-AzLocation | Where-Object { $_.RegionType -eq 'Physical' -and $_.PhysicalLocation -ne "" -and $_.Location } | Select-Object -Property Location -Unique).Location +} + +<# +.SYNOPSIS + Retrieves availability zone peering information for a specific subscription. + +.DESCRIPTION + Queries the Azure REST API to get detailed availability zone mapping information + for all regions within a subscription. This information shows how logical zones + (1, 2, 3) map to physical zones for each region. + +.PARAMETER SubscriptionId + The Azure subscription ID to query for zone peering information. + +.OUTPUTS + Array of location objects containing availability zone mapping details for all regions. + +.NOTES + Uses the Azure REST API directly as this detailed zone mapping information is not + available through standard PowerShell cmdlets. +#> +function Get-ZonePeers { + param ( + [string]$SubscriptionId + ) + Write-Host "Get Zone Peering Information for subscription $SubscriptionId" + + # Construct REST API URI for location zone mappings using subscription-specific endpoint + $uri = "{0}subscriptions/{1}/locations?api-version=2022-12-01" -f $resourceManagerUrl, $SubscriptionId + + # Call Azure REST API to get comprehensive location and zone mapping data + $response = Invoke-AzRest -Method GET -Uri $uri + return ($response.Content | ConvertFrom-Json).value +} + +<# +.SYNOPSIS + Analyzes VM quota usage and restrictions for a specific subscription. + +.DESCRIPTION + Core function that performs detailed quota analysis for a single subscription. + This simplified version processes subscriptions sequentially and provides + detailed console output showing progress through regions and SKUs. + +.PARAMETER SubscriptionId + The Azure subscription ID to analyze. + +.PARAMETER Locations + Array of Azure regions to analyze within this subscription. + +.PARAMETER SKUs + Array of VM SKU names to analyze. + +.PARAMETER OutputFile + Name of the CSV file to append results to. + +.OUTPUTS + Appends CSV rows to the output file with quota and restriction details. + Provides console output showing analysis progress. + +.NOTES + This function includes Microsoft.Capacity resource provider registration + to ensure quota APIs are available. Also provides visual progress feedback + with dots for each SKU processed. +#> +function Get-QuotaDetails { + param ( + [string]$SubscriptionId, + [string[]]$Locations, + [string[]]$SKUs, + [string]$OutputFile + ) + + try { + # Get subscription details and validate access + $Subscription = Get-AzSubscription -SubscriptionId $SubscriptionId -WarningAction SilentlyContinue + if ($null -eq $Subscription) { + throw "Subscription not found" + } + + # Set context to target subscription for all subsequent queries + Set-AzContext -SubscriptionId $Subscription.Id -WarningAction SilentlyContinue | Out-Null + + # Ensure Microsoft.Capacity resource provider is registered for quota APIs + if ((Get-AzResourceProvider -ListAvailable | Where-Object { $_.ProviderNamespace -like 'Microsoft.Capacity' }).RegistrationState -notlike 'Registered') { + try { + Register-AzResourceProvider -ProviderNamespace Microsoft.Capacity + } + catch { + Write-Host "Failed Registering Resource Provider: Microsoft.Capacity" -ForegroundColor Yellow + } + } + + # Get zone peering information if physical zone mapping is requested + if($UsePhysicalZones) + { + $zonePeers = Get-ZonePeers -SubscriptionId $SubscriptionId + if ($zonePeers.Count -eq 0) { + Write-Host "No Zone Peering Information found for subscription $SubscriptionId" -ForegroundColor Yellow + } + } + + # Process each location within the subscription + Write-Host "Querying Subscription: $($Subscription.Name)" + foreach ($Location in $Locations) { + Write-Host " Querying Region: $Location" + + # Get all VM SKUs available in this location + $computeSKUs = Get-AzComputeResourceSku -Location $Location -ErrorAction SilentlyContinue | Where-Object { $_.ResourceType -eq 'virtualMachines' } + + # Get current VM quota usage for this location + $vmUsage = Get-AZVMUsage -Location $Location -ErrorAction SilentlyContinue + + # Get availability zone mappings for this location (if using physical zones) + $availabilityZoneMappings = ($zonePeers | Where-Object { $_.name -like $Location -and $_.type -eq "Region"}).availabilityZoneMappings + + # Process each requested SKU with visual progress indicator + foreach ($SKU in $SKUs) { + Write-Host -NoNewline "." # Progress indicator for each SKU processed + + # Find the specific SKU data for this location + $filteredSku = $computeSKUs | Where-Object { $_.Name.ToLowerInvariant() -eq $SKU.ToLowerInvariant() -and $_.LocationInfo.Location -like $Location } + + # Find the quota usage data for this SKU's family + $skuUsage = $vmUsage | Select-Object -ExpandProperty Name -Property CurrentValue, Limit | Where-Object { $_.Value -eq $filteredSku.Family } + + # Skip if SKU not found in this location + if ($null -eq $filteredSku) { + continue + } + + # Process availability zones (logical or physical) + $zones = @($filteredSku.LocationInfo.Zones) + if($UsePhysicalZones) + { + # Convert logical zones to physical zones + for ($i = 0; $i -lt $zones.Length; $i++) { + $zones[$i] = Get-LastChar(($availabilityZoneMappings | Where-Object {$_.LogicalZone -like $zones[$i]}).physicalZone) + } + } + + # Create the quota analysis object + $auditedSku = [PSCustomObject]@{ + TenantId = $Subscription.TenantId + SubscriptionId = $Subscription.Id + SubscriptionName = $Subscription.Name + Location = $Location + Family = $skuUsage.LocalizedValue # VM family name (e.g., "Dv3 Family vCPUs") + Size = $filteredSku.Name # Specific SKU (e.g., "Standard_D2s_v3") + RegionRestricted = 'False' # Whether SKU is restricted in this entire region + ZonesPresent = ($zones | Sort-Object) -join "," # Available zones for this SKU + ZonesRestricted = '' # Zones where SKU is restricted + CoresUsed = $skuUsage.CurrentValue # Current quota usage + CoresTotal = $skuUsage.Limit # Total quota limit + } + + # Process any restrictions on this SKU + foreach ($restriction in $filteredSku.Restrictions) { + if ($restriction.Type -like "Zone") { + # Handle zone-specific restrictions + $zoneRestrictions = @($restriction.RestrictionInfo.Zones) + if($UsePhysicalZones) + { + # Convert restricted logical zones to physical zones + for ($i = 0; $i -lt $zoneRestrictions.Length; $i++) { + $zoneRestrictions[$i] = Get-LastChar(($availabilityZoneMappings | Where-Object {$_.LogicalZone -like $zoneRestrictions[$i]}).physicalZone) + } + } + $auditedSku.ZonesRestricted = ($zoneRestrictions | Sort-Object) -join "," + } + elseif ($restriction.Type -like "Location") { + # Handle region-wide restrictions + $auditedSku.RegionRestricted = 'True' + } + } + + # Append the result to the CSV file + $auditedSku | ConvertTo-Csv -NoHeader | Out-File -Force -Append -FilePath .\$OutputFile + } + Write-Host "" # New line after completing all SKUs in this region + } + } + catch { + Write-Host "Failed Querying Subscription ID: $SubscriptionId" -ForegroundColor Yellow + $_.Exception.Message + } +} + +# ================================================================================ +# MAIN SCRIPT EXECUTION +# ================================================================================ + +# Set error handling and output preferences +$ErrorActionPreference = 'Stop' # Stop execution on any error +$VerbosePreference = 'SilentlyContinue' # Suppress verbose output for cleaner execution + +# Define CSV header structure for output file +$csvHeaderString = "TenantId,SubscriptionId,SubscriptionName,Location,Family,Size,RegionRestricted,ZonesPresent,ZonesRestricted,CoresUsed,CoresTotal" + +# Initialize output file with CSV headers +$csvHeaderString | Out-File -Force -FilePath $OutputFile + +# Get the Azure Resource Manager URL for the current Azure environment +# This is needed for constructing REST API calls and may vary by cloud (Public, Government, etc.) +$resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl + +# Populate subscription list if not provided by user +if ($SubscriptionIds.Count -eq 0) { + $SubscriptionIds = Get-SubscriptionIds +} + +# Populate locations list if not provided by user +if ($Locations.Count -eq 0) { + $Locations = Get-Locations | Sort-Object +} + +# Populate SKUs list if not provided by user +if ($SKUs.Count -eq 0) { + $SKUs = Get-SKUDetails +} + +# Display configuration information +if($UsePhysicalZones) +{ + Write-Host "Output will be normalized to physical availability zones" +} +else { + Write-Host "Output will not be normalized to physical availability zones" +} + +# Execute quota analysis sequentially for each subscription +# This simplified version processes one subscription at a time for educational clarity +foreach ($SubscriptionId in $SubscriptionIds) { + Get-QuotaDetails -SubscriptionId $SubscriptionId -Locations $Locations -SKUs $SKUs -outputFile $OutputFile +} + +# Display results in formatted table for immediate review +Write-Host "" +Write-Host "Analysis complete. Displaying results:" +Get-Content $OutputFile | ConvertFrom-Csv | Format-Table -AutoSize diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/rate/Get-BenefitRecommendations.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/rate/Get-BenefitRecommendations.ps1 new file mode 100644 index 000000000..b7d6ce71c --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/rate/Get-BenefitRecommendations.ps1 @@ -0,0 +1,74 @@ + +<# +.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 \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/rate/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/rate/README.md new file mode 100644 index 000000000..3e099bf4d --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/rate/README.md @@ -0,0 +1,107 @@ +--- +title: Cost Analysis +--- + +# Cost analysis scripts + +Azure Cost Management tools for analyzing savings plan purchase recommendations and storage costs to optimize your Azure spend. + +> [!TIP] +> Use these scripts when you want to turn Azure Cost Management savings plan purchase recommendations into repeatable analyses for finance and operations teams.[^benefit-api] + +--- + +## Available scripts + +### Get-BenefitRecommendations.ps1 + +Queries the Azure Cost Management API to retrieve savings plan purchase recommendations based on your historical compute usage patterns. + +**What it does:** +- Analyzes historical compute usage patterns (7, 30, or 60 days lookback) +- Provides up to 10 savings plan commitment recommendations for specified term (1-year or 3-year) +- Calculates potential cost savings compared to pay-as-you-go pricing +- Shows commitment amounts and usage coverage percentages +- Returns both the top recommended savings plan and all available options + +**Output includes:** +- **Raw JSON response** from Cost Management API (for debugging/verification) +- **Recommended savings plan** with detailed financial analysis: + - Hourly commitment amount + - Total savings amount and percentage + - Usage coverage and utilization percentages + - Overage costs and wastage estimates +- **All savings plan recommendations** showing various commitment levels and their projected performance + +--- + +### Output field descriptions + +The formatted tables include these key financial metrics: + +| Field | Description | +|-------|-------------| +| **averageUtilizationPercentage** | Estimated average utilization percentage for the look-back period with this commitment | +| **commitmentAmount** | The hourly commitment amount at the specified granularity | +| **coveragePercentage** | Estimated benefit coverage percentage for the look-back period with this commitment | +| **savingsAmount** | The total amount saved for the look-back period by purchasing this savings plan | +| **savingsPercentage** | The savings percentage for the look-back period by purchasing this savings plan | +| **totalCost** | Total projected cost (sum of benefit cost and overage cost) | +| **benefitCost** | The estimated cost with benefit for the look-back period (commitmentAmount × totalHours) | +| **overageCost** | The difference between total cost and benefit cost for the look-back period | +| **wastageCost** | Estimated unused portion of the benefit cost | + +--- + +## Prerequisites and Parameters + +**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 + +**Parameters:** +- `BillingScope` (required): Billing account, subscription, or resource group scope + - Billing Account: `providers/Microsoft.Billing/billingAccounts/{billingAccountId}` + - Subscription: `subscriptions/{subscriptionId}` + - Resource Group: `subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}` +- `LookBackPeriod`: Historical analysis period (Last7Days, Last30Days, Last60Days) - default: Last7Days +- `Term`: Savings plan term (P1Y for 1-year, P3Y for 3-year) - default: P3Y + +**Note:** The API returns recommendations with shared scope by default, analyzing usage across the entire billing scope for optimal savings. + +--- + +## Usage examples + +```powershell +# Find your billing scope first +Get-AzBillingAccount # For billing account scope +Get-AzSubscription # For subscription scope + +# Basic usage with subscription scope +.\Get-BenefitRecommendations.ps1 -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012" + +# Advanced usage with billing account, 30-day lookback, and 1-year term +.\Get-BenefitRecommendations.ps1 -BillingScope "providers/Microsoft.Billing/billingAccounts/12345678" -LookBackPeriod "Last30Days" -Term "P1Y" + +# Resource group scope analysis +.\Get-BenefitRecommendations.ps1 -BillingScope "subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myResourceGroup" +``` + +--- + +## About the API + +This script uses the Azure Cost Management [Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations) to provide **savings plan purchase recommendations**. + +The API analyzes your actual compute usage patterns and calculates optimal hourly commitment amounts that minimize total costs compared to pay-as-you-go pricing. + +Recommendations include detailed financial metrics: commitment amounts, projected savings percentages, coverage percentages, utilization rates, and potential overage/wastage costs. The raw JSON output provides full API response details, while formatted tables present actionable purchase recommendations. + +**Source**: [Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations) + +--- + +[^benefit-api]: [Benefit Recommendations API](https://learn.microsoft.com/en-us/rest/api/cost-management/benefit-recommendations) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/serverless-sql-storage/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/serverless-sql-storage/README.md new file mode 100644 index 000000000..22df1c0e6 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/serverless-sql-storage/README.md @@ -0,0 +1,92 @@ +--- +title: Serverless SQL storage audit +--- + +# Serverless SQL storage audit + +Azure SQL Database [serverless compute tier](https://learn.microsoft.com/en-us/azure/azure-sql/database/serverless-tier-overview) [bills on allocated storage, not max size](https://learn.microsoft.com/en-us/azure/azure-sql/database/serverless-tier-overview#billing). Allocated space grows with writes and doesn't auto-shrink—even after deletes. This workbook surfaces the gap between allocated and used storage across all serverless SQL databases in the tenant so you can target `DBCC SHRINKDATABASE` operations where they'll save money. + +## What it measures + +| Metric | Source | Description | +|--------|--------|-------------| +| Max size (GB) | [Azure Resource Graph](https://learn.microsoft.com/en-us/azure/governance/resource-graph/overview) | Configured ceiling (`properties.maxSizeBytes`). Doesn't affect billing directly. | +| Allocated | [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-sql/database/monitoring-sql-database-azure-monitor) `allocated_data_storage` | What you [pay for](https://learn.microsoft.com/en-us/azure/azure-sql/database/serverless-tier-overview#billing). Grows with writes, never auto-shrinks. | +| Used | [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-sql/database/monitoring-sql-database-azure-monitor) `storage` | Actual data on disk. | + +**Waste** = Allocated minus Used. That's the reclamation target. Compare the two columns in the workbook to identify databases worth shrinking. + +## How it works + +The workbook uses the [merge data source](https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/workbooks-data-sources#merge) pattern to join two queries: + +1. **Resource Graph** finds all `GP_S_Gen*` databases across every subscription selected in the subscription picker +2. **Azure Monitor metrics** (hidden step) pulls `allocated_data_storage` and `storage` for each discovered database +3. **Merge** left outer joins on resource ID and renders a single sortable table + +The subscription picker scopes the Resource Graph query via `crossComponentResources`. The merge's left outer join ensures databases appear even when metrics haven't populated yet. + +## Deploy + +### ARM template + +```bash +az deployment group create \ + -g \ + --template-file workbook.json +``` + +The workbook deploys as a `Microsoft.Insights/workbooks` resource. It doesn't require a Log Analytics workspace—Resource Graph and Azure Monitor metrics are the only data sources. + +### Find the workbook after deployment + +Go to **Azure Monitor** > **Workbooks** > select the resource group you deployed to. The workbook appears as **Serverless SQL Storage - FinOps Audit**. + +### Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `workbookDisplayName` | Serverless SQL Storage - FinOps Audit | Display name in the Azure portal | +| `workbookSourceId` | azure monitor | Source binding for the workbook | + +### Workbook controls + +| Control | Options | Description | +|---------|---------|-------------| +| Subscriptions | Multi-select picker | Scopes the Resource Graph query to selected subscriptions | +| Time range | 1h, 4h, 12h, 1d, 3d, 7d, 30d, 90d | Adjusts the metrics query window. Use 30d or 90d for trend analysis. | + +## Prerequisites + +- The deploying identity needs **Reader** on every subscription the workbook should scan (Resource Graph respects RBAC) +- Azure Monitor metrics are available without additional configuration for SQL databases +- No agents, extensions, or diagnostic settings required + +## Reclaiming allocated storage + +After identifying databases with high waste, connect to each database and run: + +```sql +DBCC SHRINKDATABASE (N'', 10); +``` + +The `10` specifies the target free space percentage after shrink. [DBCC SHRINKDATABASE reference](https://learn.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-shrinkdatabase-transact-sql). + +For file-level control: + +```sql +DBCC SHRINKFILE (N'', ); +``` + +[DBCC SHRINKFILE reference](https://learn.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-shrinkfile-transact-sql). + +**Caveat:** Shrinking causes [index fragmentation](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/reorganize-and-rebuild-indexes). Rebuild indexes after shrinking production databases to avoid query performance degradation. + +## Limitations + +- **500 database cap**: The metrics step processes up to 500 databases. If you have more, use the subscription picker to filter to a smaller set. +- **No computed waste column**: Workbook merge steps don't support cross-column arithmetic. The workbook shows Allocated and Used side by side—compare visually, or use the companion CLI script for computed waste and utilization percentages. +- Resource Graph indexing can lag up to 5 minutes after a database is created or deleted. +- Metrics aren't available for databases created within the last few minutes. +- The merge step joins on resource ID; if a database is deleted between the Resource Graph query and the metrics query, that row shows null for Allocated and Used. +- Max size reduction doesn't reclaim allocated storage—only `DBCC SHRINKDATABASE` or `DBCC SHRINKFILE` does. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/serverless-sql-storage/workbook.json b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/serverless-sql-storage/workbook.json new file mode 100644 index 000000000..7f735d9f6 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-capacity-management/references/scripts/serverless-sql-storage/workbook.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "workbookDisplayName": { + "type": "string", + "defaultValue": "Serverless SQL Storage - FinOps Audit" + }, + "workbookSourceId": { + "type": "string", + "defaultValue": "azure monitor" + } + }, + "variables": { + "workbookId": "[guid(resourceGroup().id, parameters('workbookDisplayName'))]" + }, + "resources": [ + { + "type": "Microsoft.Insights/workbooks", + "apiVersion": "2023-06-01", + "name": "[variables('workbookId')]", + "location": "[resourceGroup().location]", + "kind": "shared", + "properties": { + "displayName": "[parameters('workbookDisplayName')]", + "category": "workbook", + "sourceId": "[parameters('workbookSourceId')]", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":1,\"content\":{\"json\":\"# Serverless SQL storage audit\\nIdentifies allocated vs used storage across all serverless SQL databases (`GP_S_Gen*`) in the selected subscriptions. Allocated storage is what you're [billed for](https://learn.microsoft.com/en-us/azure/azure-sql/database/serverless-tier-overview#billing)\\u2014it grows with writes and doesn't auto-shrink. Compare the **Allocated** and **Used** columns to find reclamation targets.\"},\"name\":\"header\"},{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"7b3c9e1a-4f2d-4a8b-9c6e-1d3f5a7b9c2e\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"Subscriptions\",\"type\":6,\"isRequired\":true,\"multiSelect\":true,\"includeAll\":true,\"defaultValue\":\"value::all\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"includeAll\":true}},{\"id\":\"8d4e0f2b-5a3c-4b9d-ae7f-2e4a6b8c0d3f\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"TimeRange\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":2592000000},{\"durationMs\":7776000000}]},\"timeContext\":{\"durationMs\":86400000},\"value\":{\"durationMs\":86400000}}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.resourcegraph/resources\"},\"name\":\"parameters\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"resources\\n| where type =~ 'microsoft.sql/servers/databases'\\n| where sku.name startswith 'GP_S_Gen'\\n| project id, dbName=name, serverName=tostring(split(id, '/')[8]),\\n resourceGroup, subscriptionId, location,\\n skuName=sku.name, maxSizeGB=properties.maxSizeBytes / 1073741824\",\"size\":0,\"queryType\":1,\"resourceType\":\"microsoft.resourcegraph/resources\",\"crossComponentResources\":[\"{Subscriptions}\"],\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"id\",\"formatter\":5},{\"columnMatch\":\"subscriptionId\",\"formatter\":5},{\"columnMatch\":\"maxSizeGB\",\"formatter\":0,\"numberFormat\":{\"unit\":5,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":0}}}],\"labelSettings\":[{\"columnId\":\"dbName\",\"label\":\"Database\"},{\"columnId\":\"serverName\",\"label\":\"Server\"},{\"columnId\":\"resourceGroup\",\"label\":\"Resource group\"},{\"columnId\":\"location\",\"label\":\"Region\"},{\"columnId\":\"skuName\",\"label\":\"SKU\"},{\"columnId\":\"maxSizeGB\",\"label\":\"Max size (GB)\"}]}},\"name\":\"serverlessDatabases\"},{\"type\":10,\"content\":{\"chartId\":\"c5e8a2f1-6b3d-4c9a-b7e2-3f5a1d8c4e6b\",\"version\":\"MetricsItem/2.0\",\"size\":0,\"chartType\":0,\"resourceType\":\"microsoft.sql/servers/databases\",\"metricScope\":0,\"resourceIds\":[\"resources\\n| where type =~ 'microsoft.sql/servers/databases'\\n| where sku.name startswith 'GP_S_Gen'\\n| project id\\n| order by id asc\"],\"timeContextFromParameter\":\"TimeRange\",\"timeContext\":{\"durationMs\":86400000},\"metrics\":[{\"namespace\":\"microsoft.sql/servers/databases\",\"metric\":\"microsoft.sql/servers/databases--allocated_data_storage\",\"aggregation\":4,\"columnName\":\"Allocated\"},{\"namespace\":\"microsoft.sql/servers/databases\",\"metric\":\"microsoft.sql/servers/databases--storage\",\"aggregation\":4,\"columnName\":\"Used\"}],\"gridFormatType\":1,\"resourceLimit\":500,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Metric\",\"formatter\":5},{\"columnMatch\":\"Allocated\",\"formatter\":0,\"numberFormat\":{\"unit\":2,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":2}}},{\"columnMatch\":\"Used\",\"formatter\":0,\"numberFormat\":{\"unit\":2,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":2}}},{\"columnMatch\":\"Timeline\",\"formatter\":5},{\"columnMatch\":\"microsoft.sql/servers/databases--allocated_data_storage\",\"formatter\":5},{\"columnMatch\":\"microsoft.sql/servers/databases--storage\",\"formatter\":5}]}},\"conditionalVisibility\":{\"parameterName\":\"neverShow\",\"comparison\":\"isEqualTo\",\"value\":\"true\"},\"name\":\"storageMetrics\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"{\\\"version\\\":\\\"Merge/1.0\\\",\\\"merges\\\":[{\\\"id\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\",\\\"mergeType\\\":\\\"leftouter\\\",\\\"leftTable\\\":\\\"serverlessDatabases\\\",\\\"rightTable\\\":\\\"storageMetrics\\\",\\\"leftColumn\\\":\\\"id\\\",\\\"rightColumn\\\":\\\"Name\\\"}],\\\"projectRename\\\":[{\\\"originalName\\\":\\\"[serverlessDatabases].dbName\\\",\\\"mergedName\\\":\\\"Database\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[serverlessDatabases].serverName\\\",\\\"mergedName\\\":\\\"Server\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[serverlessDatabases].location\\\",\\\"mergedName\\\":\\\"Region\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[serverlessDatabases].skuName\\\",\\\"mergedName\\\":\\\"SKU\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[serverlessDatabases].maxSizeGB\\\",\\\"mergedName\\\":\\\"Max size (GB)\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[storageMetrics].Allocated\\\",\\\"mergedName\\\":\\\"Allocated\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[storageMetrics].Used\\\",\\\"mergedName\\\":\\\"Used\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[serverlessDatabases].subscriptionId\\\",\\\"mergedName\\\":\\\"Subscription\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[serverlessDatabases].resourceGroup\\\",\\\"mergedName\\\":\\\"Resource group\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"},{\\\"originalName\\\":\\\"[serverlessDatabases].id\\\",\\\"mergedName\\\":\\\"id\\\",\\\"fromId\\\":\\\"d9f2a1b3-7c4e-4d8a-b5f1-6e3a9c2d7b4f\\\"}]}\",\"size\":0,\"queryType\":7,\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"id\",\"formatter\":5},{\"columnMatch\":\"Subscription\",\"formatter\":5},{\"columnMatch\":\"Max size \\\\(GB\\\\)\",\"formatter\":0,\"numberFormat\":{\"unit\":5,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":0}}},{\"columnMatch\":\"Allocated\",\"formatter\":0,\"numberFormat\":{\"unit\":2,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":2}}},{\"columnMatch\":\"Used\",\"formatter\":0,\"numberFormat\":{\"unit\":2,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":2}}}],\"sortBy\":[{\"itemKey\":\"Allocated\",\"sortOrder\":2}],\"labelSettings\":[{\"columnId\":\"Database\",\"label\":\"Database\"},{\"columnId\":\"Server\",\"label\":\"Server\"},{\"columnId\":\"Region\",\"label\":\"Region\"},{\"columnId\":\"SKU\",\"label\":\"SKU\"},{\"columnId\":\"Max size (GB)\",\"label\":\"Max size (GB)\"},{\"columnId\":\"Allocated\",\"label\":\"Allocated\"},{\"columnId\":\"Used\",\"label\":\"Used\"},{\"columnId\":\"Resource group\",\"label\":\"Resource group\"}]}},\"name\":\"mergedStorageView\"},{\"type\":1,\"content\":{\"json\":\"---\\n### How to read this table\\n- **Allocated** = storage you're [billed for](https://learn.microsoft.com/en-us/azure/azure-sql/database/serverless-tier-overview#billing). Grows with writes, never auto-shrinks.\\n- **Used** = actual data on disk.\\n- **Waste** = Allocated minus Used. Compare the two columns to find reclamation targets.\\n- **Max size (GB)** = configured ceiling. Reducing it doesn't reclaim allocated space.\\n- Run [`DBCC SHRINKDATABASE`](https://learn.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-shrinkdatabase-transact-sql) to reclaim. Shrinking causes [index fragmentation](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/reorganize-and-rebuild-indexes)\\u2014rebuild indexes after shrinking production databases.\\n- [Serverless compute tier overview](https://learn.microsoft.com/en-us/azure/azure-sql/database/serverless-tier-overview)\\n\\n> This workbook displays up to 500 databases. If you have more, filter by subscription using the picker above.\"},\"name\":\"footer\"}],\"fallbackResourceIds\":[\"azure monitor\"]}" + } + } + ] +} diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/README.md new file mode 100644 index 000000000..2a8772d00 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/README.md @@ -0,0 +1,48 @@ +# Azure Cost Management skill + +Cost optimization and financial governance for Azure. Provides domain knowledge for Azure Advisor recommendations, commitment discounts (savings plans and reservations), budgets, cost exports, anomaly alerts, credits, and MACC tracking. + +## When this skill activates + +Triggered when you ask about: Azure Advisor, cost recommendations, savings plans, reservations, Azure budgets, cost exports, anomaly alerts, MACC, Azure credits, Azure Prepayment, commitment discounts, or cost optimization. + +## Prerequisites + +- Azure CLI authenticated (`az login`) +- Appropriate RBAC permissions for Cost Management APIs + +## Domains + +| Domain | Purpose | Key operations | +|--------|---------|----------------| +| **Azure Advisor** | Cost recommendations | Query, suppress, and manage recommendations (up to 90-day TTL suppression, bulk via management groups) | +| **Savings plans and reservations** | Commitment discount analysis | Benefit recommendations, coverage analysis, ROI calculations | +| **Budgets** | Budget management | Create budgets with up to 5 notifications, actual/forecasted thresholds, action groups | +| **Cost exports** | Scheduled data exports | FOCUS format exports to storage accounts with backfill support | +| **Anomaly alerts** | Cost spike detection | Enterprise-scale anomaly alert deployment with pagination | +| **Credits** | Azure Prepayment tracking | EA/MCA credit balances, expiration dates, consumption history | +| **MACC** | Consumption commitment tracking | Balance, decrements, milestone progress, eligible spend | + +## Reference documentation + +Each domain has a detailed reference file loaded on demand: + +| File | Contents | +|------|----------| +| `references/azure-advisor.md` | Recommendation queries, suppression workflows, management group bulk operations | +| `references/azure-savings-plans.md` | Benefit Recommendations API, savings plan vs reservation comparison, coverage analysis | +| `references/azure-budgets.md` | Budget creation, notification thresholds, action group integration | +| `references/azure-cost-exports.md` | FOCUS export configuration, backfill procedures, troubleshooting | +| `references/azure-anomaly-alerts.md` | Bulk anomaly alert deployment across subscriptions | +| `references/azure-credits.md` | EA/MCA credit balance tracking, expiration risk assessment | +| `references/azure-macc.md` | MACC balance monitoring, decrement tracking, milestone progress | + +## Quick examples + +```bash +# List cost recommendations +az advisor recommendation list --category Cost --output table + +# List budgets +az consumption budget list --output table +``` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/SKILL.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/SKILL.md new file mode 100644 index 000000000..41dcfd46e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/SKILL.md @@ -0,0 +1,195 @@ +--- +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. + +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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/Get-BenefitRecommendations.ps1 b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/Get-BenefitRecommendations.ps1 new file mode 100644 index 000000000..6a0f890bd --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/Get-BenefitRecommendations.ps1 @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.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 \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-advisor.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-advisor.md new file mode 100644 index 000000000..ba1d5ae48 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-advisor.md @@ -0,0 +1,258 @@ +--- +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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-anomaly-alerts.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-anomaly-alerts.md new file mode 100644 index 000000000..0ef4bcaae --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-anomaly-alerts.md @@ -0,0 +1,291 @@ +--- +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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-budgets.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-budgets.md new file mode 100644 index 000000000..2e4ae85ed --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-commitment-discount-decision.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-commitment-discount-decision.md new file mode 100644 index 000000000..6f7204182 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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://www.finops.org/framework/capabilities/rate-optimization/) + +--- + +## 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://www.finops.org/framework/capabilities/rate-optimization/) +- [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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-cost-exports.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-cost-exports.md new file mode 100644 index 000000000..005bef6ef --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-cost-exports.md @@ -0,0 +1,178 @@ +--- +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 and are required for FinOps Hubs. +--- + +## 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/ftk-costs-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/ftk-costs-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 + +### FinOps Toolkit PowerShell (Recommended) + +```powershell +# Backfill 12 months at billing scope +New-FinOpsCostExport -Name "ftk-costs" ` + -Scope "/providers/Microsoft.Billing/billingAccounts/{enrollmentId}" ` + -StorageAccountId "/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}" ` + -Backfill 12 ` + -Execute +``` + +### 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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-credits.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-credits.md new file mode 100644 index 000000000..618068df4 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-macc.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-macc.md new file mode 100644 index 000000000..9030a0017 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-macc.md @@ -0,0 +1,122 @@ +--- +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. + +**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. **Purchase Reservations/Savings Plans** - Purchases count toward MACC +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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-orphaned-resources.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-orphaned-resources.md new file mode 100644 index 000000000..062caabd8 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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) +- [Usage Optimization (FinOps Framework)](https://www.finops.org/framework/capabilities/usage-optimization/) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-reservations.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-reservations.md new file mode 100644 index 000000000..90f35aff0 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-retail-prices.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-retail-prices.md new file mode 100644 index 000000000..469084eeb --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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 or FinOps hubs cost data. + +--- + +## 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://www.finops.org/framework/capabilities/rate-optimization/) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-savings-plans.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-savings-plans.md new file mode 100644 index 000000000..af480f3f7 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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://www.finops.org/framework/capabilities/rate-optimization/) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-vm-rightsizing.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/azure-cost-management/references/azure-vm-rightsizing.md new file mode 100644 index 000000000..25f03dad9 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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) +- [Usage Optimization (FinOps Framework)](https://www.finops.org/framework/capabilities/usage-optimization/) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/README.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/README.md new file mode 100644 index 000000000..7ace98a82 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/README.md @@ -0,0 +1,103 @@ +# FinOps Toolkit skill + +KQL-based cost analysis and infrastructure deployment for [FinOps hubs](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/finops-hubs-overview). Provides a query catalog of 37 pre-built KQL queries, database schema documentation, hub deployment workflows, and a structured think-execute framework for financial analysis. + +## When this skill activates + +Triggered when you ask about: FinOps hubs, FinOps toolkit, KQL queries, Kusto, cost data analysis, Hub database, Costs function, Prices function, Recommendations function, FinOps hubs deployment, Azure Data Explorer, or ADX cluster. + +## Prerequisites + +- Azure CLI authenticated (`az login`) +- Azure MCP Server (provided by the plugin) +- AllDatabasesViewer access to a FinOps hubs ADX cluster +- Environment configured in `.ftk/environments.local.md` (use `/ftk-hubs-connect`) + +## Core rules + +1. Read the reference docs before writing any query +2. Verify schema before any query (check database guide) +3. Never guess column names or data +4. Show query before execution +5. Stop if confidence < 70% + +## Database functions + +The FinOps hubs database exposes four analytic functions: + +| Function | Purpose | Key columns | +|----------|---------|-------------| +| `Costs()` | Cost and usage analytics (FOCUS-aligned) | `BilledCost`, `EffectiveCost`, `ContractedCost`, `ListCost`, `ServiceName`, `ResourceName`, `Tags` | +| `Prices()` | Price sheets with list, contracted, and effective pricing | `ListUnitPrice`, `ContractedUnitPrice`, `x_EffectiveUnitPrice`, `PricingUnit` | +| `Recommendations()` | Reservation and savings plan recommendations | `x_EffectiveCostBefore`, `x_EffectiveCostAfter`, `x_EffectiveCostSavings` | +| `Transactions()` | Commitment purchases, refunds, and exchanges | `BilledCost`, `ChargeCategory`, `x_SkuTerm`, `x_TransactionType` | + +Columns prefixed with `x_` are toolkit enrichments added during ingestion (e.g., `x_ResourceGroupName`, `x_CommitmentDiscountSavings`, `x_TotalSavings`). + +## Query catalog + +37 pre-built KQL queries in `references/queries/catalog/`. Always check the catalog before writing custom KQL. + +| Query | Purpose | Parameters | +|-------|---------|------------| +| `costs-enriched-base.kql` | Full enrichment base for scoped custom drill-downs | `startDate`, `endDate` | +| `monthly-cost-trend.kql` | Billed and effective cost by month | `startDate`, `endDate` | +| `monthly-cost-change-percentage.kql` | Month-over-month cost change % | `startDate`, `endDate` | +| `top-services-by-cost.kql` | Top N services by cost | `N`, `startDate`, `endDate` | +| `top-resource-types-by-cost.kql` | Top N resource types by cost | `N`, `startDate`, `endDate` | +| `top-resource-groups-by-cost.kql` | Top N resource groups by cost | `N`, `startDate`, `endDate` | +| `quarterly-cost-by-resource-group.kql` | Resource group costs by quarter | `N`, `startDate`, `endDate` | +| `cost-by-region-trend.kql` | Effective cost by Azure region | `startDate`, `endDate` | +| `cost-by-financial-hierarchy.kql` | Cost by billing profile, team, product, app | `N`, `startDate`, `endDate` | +| `ai-cost-by-application.kql` | Azure OpenAI cost by application/team/environment | `startDate`, `endDate` | +| `ai-daily-trend.kql` | Daily Azure OpenAI cost and token trend | `startDate`, `endDate` | +| `ai-model-cost-comparison.kql` | Azure OpenAI model cost efficiency | `startDate`, `endDate` | +| `ai-token-usage-breakdown.kql` | Azure OpenAI tokens by model and direction | `startDate`, `endDate` | +| `cost-anomaly-detection.kql` | Statistical anomaly detection | `numberOfMonths`, `interval` | +| `cost-forecasting-model.kql` | Future cost projections | `forecastPeriods`, `interval` | +| `service-price-benchmarking.kql` | Price comparison across tiers | `startDate`, `endDate` | +| `commitment-discount-utilization.kql` | RI/SP utilization analysis | `startDate`, `endDate` | +| `savings-summary-report.kql` | Total savings and ESR KPI | `startDate`, `endDate` | +| `top-commitment-transactions.kql` | Top N RI/SP purchases | `N`, `startDate`, `endDate` | +| `top-other-transactions.kql` | Top N non-usage transactions | `N`, `startDate`, `endDate` | +| `reservation-recommendation-breakdown.kql` | Reservation recommendations with break-even | Filter by service/region | + +In this recipe, `references/queries/INDEX.md` is local source guidance; deployed SRE Agent instances receive executable query tools and Knowledge Sources, not skill additional files. + +## Hub deployment + +The skill covers FinOps hubs infrastructure deployment via Azure portal, PowerShell (`Deploy-FinOpsHub`), or Bicep modules. Architecture includes: + +- Storage Account (Data Lake Gen2) for data staging +- Azure Data Factory for ingestion pipelines +- Azure Data Explorer or Microsoft Fabric RTI for analytics +- Key Vault for managed identity credentials + +Estimated cost: ~$120/mo + $10/mo per $1M in monitored spend. + +See `references/finops-hubs-deployment.md` for deployment methods, scope configuration, backfill, Fabric setup, and dashboard/Power BI report setup. + +## Reference documentation + +| File | Contents | +|------|----------| +| `references/finops-hubs.md` | Analysis guide: KQL execution, query catalog protocol, tool matrix, performance rules, quality checklist | +| `references/finops-hubs-deployment.md` | Deployment: prerequisites, methods (portal/PowerShell/Bicep), exports, backfill, Fabric, dashboards | +| `references/settings-format.md` | `.ftk/environments.local.md` format: named environments with cluster-uri, tenant, subscription | +| `src/queries/INDEX.md` | Local canonical query-to-scenario matrix with parameters and usage guidance for all 37 pre-built KQL queries | +| `src/queries/finops-hub-database-guide.md` | Local canonical database schema: all four functions, column definitions, enrichment columns, query best practices | +| `references/workflows/ftk-hubs-connect.md` | Hub discovery via Resource Graph, connection validation, environment persistence | +| `references/workflows/ftk-hubs-healthCheck.md` | Version comparison against stable/dev releases, data freshness check | + +## Query execution + +```json +{ + "cluster-uri": "", + "database": "Hub", + "tenant": "", + "query": "" +} +``` + +Always use the "Hub" database (never "Ingestion"). Always include `tenant` for cross-tenant scenarios. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/SKILL.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/SKILL.md new file mode 100644 index 000000000..65fe3c6cf --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/SKILL.md @@ -0,0 +1,373 @@ +--- +name: finops-toolkit +description: This skill should be used when the user asks about "FinOps hubs", "FinOps toolkit", "KQL queries", "Kusto", "cost data analysis", "Hub database", "Costs function", "Prices function", "Recommendations function", "FinOps hubs deployment", "Azure Data Explorer", "ADX cluster", or any FinOps hubs operations. +license: MIT +compatibility: Requires Azure MCP Server and Azure CLI authentication. For queries, requires AllDatabasesViewer access to the FinOps hub ADX cluster. +metadata: + author: microsoft + version: "1.0" +--- + +# FinOps Toolkit + +FinOps hubs are a scalable platform for cloud cost analytics, insights, and optimization built on Azure Data Explorer (Kusto) or Microsoft Fabric RTI. This skill covers KQL-based cost analysis and hub infrastructure deployment. + +## Task routing + +Load only the references relevant to the task at hand. + +> **Maintainer note:** The references in this table intentionally use **shorthand skill paths** for readability. They are **not** a claim that the repo contains literal `toolkit/...`, `focus/...`, or bare `.kql` files next to `SKILL.md`. +> +> The packaging flow copies `src/templates/agent-skills/**` into `release/agent-skills` and zips that content **without rewriting paths** (see `src/scripts/Build-Toolkit.ps1` and `src/scripts/Package-Toolkit.ps1`). Resolve the shorthand in this table as follows: +> +> - `toolkit/...` → `references/docs-mslearn/toolkit/...` +> - `focus/...` → `references/docs-mslearn/focus/...` +> - bare `.kql` names → `references/queries/catalog/...` +> - `references/...` paths are already literal +> +> **Do not flag this table as a packaging bug again unless the packaging scripts change.** + +| Task | Load these references first | +|------|-----------------------------| +| Write or run a KQL query | `references/finops-hubs.md`, `references/queries/finops-hub-database-guide.md` | +| Use a pre-built query | Use the attached Kusto tool with the matching query name; use uploaded Knowledge Sources for schema and output-style guidance. | +| Deploy a new hub | `references/finops-hubs-deployment.md`, `toolkit/hubs/deploy.md`, `toolkit/hubs/template.md` | +| Upgrade a hub | `references/finops-hubs-deployment.md`, `toolkit/hubs/upgrade.md`, `toolkit/hubs/compatibility.md` | +| Savings / ESR analysis | `toolkit/hubs/savings-calculations.md`, `savings-summary-report.kql`, `commitment-discount-utilization.kql` | +| Reservation recommendations | `reservation-recommendation-breakdown.kql`, `toolkit/hubs/savings-calculations.md` | +| FOCUS columns / mapping | `focus/what-is-focus.md`, `focus/mapping.md` | +| Power BI setup | `toolkit/power-bi/setup.md`, `toolkit/power-bi/connector.md` | +| Troubleshooting / errors | `toolkit/help/troubleshooting.md`, `toolkit/help/errors.md` | +| PowerShell commands | `toolkit/powershell/powershell-commands.md` + the specific command file | +| Connect to hub for first time | `references/workflows/ftk-hubs-connect.md`, `references/settings-format.md` | +| Check hub health / version | `references/workflows/ftk-hubs-healthCheck.md`, `toolkit/hubs/compatibility.md` | + +## Database functions + +The Hub database exposes four analytic functions. Always use the `Hub` database — never `Ingestion`. + +| Function | Purpose | Key columns | +|----------|---------|-------------| +| `Costs()` | Cost and usage analytics (FOCUS-aligned) | `BilledCost`, `EffectiveCost`, `ContractedCost`, `ListCost`, `ServiceName`, `ResourceName`, `Tags` | +| `Prices()` | Price sheets with list, contracted, and effective pricing | `ListUnitPrice`, `ContractedUnitPrice`, `x_EffectiveUnitPrice`, `PricingUnit` | +| `Recommendations()` | Reservation and savings plan recommendations | `x_EffectiveCostBefore`, `x_EffectiveCostAfter`, `x_EffectiveCostSavings` | +| `Transactions()` | Commitment purchases, refunds, and exchanges | `BilledCost`, `ChargeCategory`, `x_SkuTerm`, `x_TransactionType` | + +Columns prefixed with `x_` are toolkit enrichments added during ingestion (e.g., `x_ResourceGroupName`, `x_CommitmentDiscountSavings`, `x_TotalSavings`). Full column definitions: `references/queries/finops-hub-database-guide.md`. + +## Query execution + +- Uses **KQL (Kusto)**, not SQL +- Default analysis window: 30 days +- Always include `tenant` — cross-tenant (B2B) scenarios fail without it + +Environment settings are read from `.ftk/environments.local.md` at the project root. Use the `default` environment unless the user specifies one. See `references/settings-format.md` for the file format. + +```json +{ + "cluster-uri": "", + "database": "Hub", + "tenant": "", + "query": "" +} +``` + +## Query catalog + +Use the attached Kusto tools before writing custom KQL. The tool names mirror the canonical `src/queries/catalog` query IDs. In deployed SRE Agent instances, skill reference files are build-time source material and are not sent as skill `additionalFiles`; rely on the attached tools and uploaded Knowledge Sources at runtime. + +| Query | Description | +|-------|-------------| +| [costs-enriched-base.kql](references/queries/catalog/costs-enriched-base.kql) | Base query with full enrichment and savings logic for all cost columns. Use for scoped row-level custom analytics or repeated drill-downs. | +| [monthly-cost-trend.kql](references/queries/catalog/monthly-cost-trend.kql) | Total billed and effective cost by month for trend analysis and executive reporting. | +| [monthly-cost-change-percentage.kql](references/queries/catalog/monthly-cost-change-percentage.kql) | Month-over-month cost change percentage for both billed and effective costs. | +| [top-services-by-cost.kql](references/queries/catalog/top-services-by-cost.kql) | Top N Azure services by cost. Key for cost visibility. | +| [top-resource-types-by-cost.kql](references/queries/catalog/top-resource-types-by-cost.kql) | Top N resource types by cost and usage. | +| [top-resource-groups-by-cost.kql](references/queries/catalog/top-resource-groups-by-cost.kql) | Top N resource groups by effective cost. | +| [quarterly-cost-by-resource-group.kql](references/queries/catalog/quarterly-cost-by-resource-group.kql) | Effective cost by resource group for quarterly or multi-month reporting. | +| [cost-by-region-trend.kql](references/queries/catalog/cost-by-region-trend.kql) | Effective cost by Azure region for regional cost driver analysis. | +| [cost-by-financial-hierarchy.kql](references/queries/catalog/cost-by-financial-hierarchy.kql) | Cost allocation by billing profile, invoice section, team, product, and app for showback/chargeback. | +| [ai-cost-by-application.kql](references/queries/catalog/ai-cost-by-application.kql) | Azure OpenAI costs by application, cost center, team, and environment tags. | +| [ai-daily-trend.kql](references/queries/catalog/ai-daily-trend.kql) | Daily Azure OpenAI token and effective cost trend. | +| [ai-model-cost-comparison.kql](references/queries/catalog/ai-model-cost-comparison.kql) | Azure OpenAI model cost efficiency and discount comparison. | +| [ai-token-usage-breakdown.kql](references/queries/catalog/ai-token-usage-breakdown.kql) | Azure OpenAI token consumption and cost by model and direction. | +| [cost-anomaly-detection.kql](references/queries/catalog/cost-anomaly-detection.kql) | Detect unusual cost spikes or drops using statistical anomaly detection. | +| [anomaly-detection-rate.kql](references/queries/catalog/anomaly-detection-rate.kql) | Measure the share of effective spend on anomaly-flagged daily buckets. | +| [anomaly-variance-total.kql](references/queries/catalog/anomaly-variance-total.kql) | Quantify unpredicted spend variance for anomaly events. | +| [cost-forecasting-model.kql](references/queries/catalog/cost-forecasting-model.kql) | Project future costs for budgeting and planning with configurable forecast horizon. | +| [service-price-benchmarking.kql](references/queries/catalog/service-price-benchmarking.kql) | Compare list, contracted, effective, negotiated, and commitment prices by service. | +| [commitment-discount-utilization.kql](references/queries/catalog/commitment-discount-utilization.kql) | Reservation and savings plan utilization analysis for rate optimization. | +| [commitment-utilization-score.kql](references/queries/catalog/commitment-utilization-score.kql) | Commitment utilization amount, potential, and score by commitment and currency. | +| [commitment-discount-waste.kql](references/queries/catalog/commitment-discount-waste.kql) | Unused commitment value as a share of total commitment cost. | +| [compute-spend-commitment-coverage.kql](references/queries/catalog/compute-spend-commitment-coverage.kql) | Share of compute spend covered by commitment discounts. | +| [cost-optimization-index.kql](references/queries/catalog/cost-optimization-index.kql) | Hub-wide Cost Optimization Index from current recommendations and windowed cost. | +| [macc-consumption-vs-commitment.kql](references/queries/catalog/macc-consumption-vs-commitment.kql) | MACC consumption versus commitment drawdown by billing profile and month. | +| [savings-summary-report.kql](references/queries/catalog/savings-summary-report.kql) | Total realized savings and Effective Savings Rate KPI. | +| [top-commitment-transactions.kql](references/queries/catalog/top-commitment-transactions.kql) | Top N reservation or savings plan purchases by cost impact. | +| [top-other-transactions.kql](references/queries/catalog/top-other-transactions.kql) | Top N non-commitment, non-usage transactions. | +| [reservation-recommendation-breakdown.kql](references/queries/catalog/reservation-recommendation-breakdown.kql) | Microsoft reservation recommendations with projected savings and break-even analysis. | +| [allocation-accuracy-index.kql](references/queries/catalog/allocation-accuracy-index.kql) | Directly attributed cost as a share of total effective cost. | +| [percentage-unallocated-costs.kql](references/queries/catalog/percentage-unallocated-costs.kql) | Share of effective cost without allocation evidence. | +| [percentage-untagged-costs.kql](references/queries/catalog/percentage-untagged-costs.kql) | Share of effective cost associated with resources that have no tags. | +| [tagging-policy-compliance.kql](references/queries/catalog/tagging-policy-compliance.kql) | Cost-weighted compliance with required tag keys. | +| [compute-cost-per-core.kql](references/queries/catalog/compute-cost-per-core.kql) | Hourly and effective average compute cost per consumed vCPU core hour. | +| [cost-per-gb-stored.kql](references/queries/catalog/cost-per-gb-stored.kql) | Storage cost per normalized GB-month. | +| [storage-tier-distribution.kql](references/queries/catalog/storage-tier-distribution.kql) | Storage cost and GB-month distribution by access-tier bucket. | +| [cost-visibility-delay.kql](references/queries/catalog/cost-visibility-delay.kql) | Cost data visibility delay from charge period end to Hub ingestion. | +| [data-update-frequency.kql](references/queries/catalog/data-update-frequency.kql) | Hub ingestion update cadence from distinct ingestion timestamps. | + +## Infrastructure deployment + +Deployment targets: Azure Data Explorer clusters, Microsoft Fabric workspaces, Cost Management exports, Power BI dashboards. + +Key commands: `az deployment`, `az kusto`, `az storage`. PowerShell: `Deploy-FinOpsHub`. + +Estimated cost: ~$120/mo + $10/mo per $1M in monitored spend. + +For detailed documentation: `references/finops-hubs-deployment.md` + +## Reference files + +| File | Description | +|------|-------------| +| [references/finops-hubs.md](references/finops-hubs.md) | Analysis guide: KQL execution, query catalog protocol, tool matrix, performance rules. **Read before any cost query.** | +| [references/finops-hubs-deployment.md](references/finops-hubs-deployment.md) | Deployment and configuration: ADX clusters, Fabric, Data Factory, exports, Key Vault, Power BI dashboards. | +| [references/settings-format.md](references/settings-format.md) | Format specification for `.ftk/environments.local.md` — named environments with cluster-uri, tenant, subscription, and resource-group. | +| `src/queries/INDEX.md` | Build-time canonical query catalog with scenario-to-query matrix, parameter docs, and usage guidance for all 37 pre-built KQL queries. The deployed SRE Agent receives executable tools rather than these files as skill attachments. | +| `src/queries/finops-hub-database-guide.md` | Build-time canonical Hub database schema reference. Runtime schema guidance should come from uploaded Knowledge Sources and attached query tools. | +| [references/workflows/ftk-hubs-connect.md](references/workflows/ftk-hubs-connect.md) | Workflow to discover FinOps hub instances via Resource Graph, connect, and save environment config. | +| [references/workflows/ftk-hubs-healthCheck.md](references/workflows/ftk-hubs-healthCheck.md) | Health check workflow: version comparison against stable/dev releases, upgrade guidance, and diagnostic steps. | + +## Microsoft Learn documentation + +Official Microsoft documentation for FinOps and the FinOps toolkit. Source: [learn.microsoft.com](https://learn.microsoft.com/cloud-computing/finops/). + +### FinOps overview + +| File | Description | +|------|-------------| +| [overview.md](references/docs-mslearn/overview.md) | Microsoft Cloud FinOps overview | +| [implementing-finops-guide.md](references/docs-mslearn/implementing-finops-guide.md) | Guide to implementing FinOps in Azure | +| [conduct-iteration.md](references/docs-mslearn/conduct-iteration.md) | Conducting a FinOps iteration | + +### FinOps Framework + +| File | Description | +|------|-------------| +| [finops-framework.md](references/docs-mslearn/framework/finops-framework.md) | FinOps Framework overview | +| [capabilities.md](references/docs-mslearn/framework/capabilities.md) | FinOps capabilities reference | + +#### Understand Usage & Cost + +| File | Description | +|------|-------------| +| [understand-cloud-usage-cost.md](references/docs-mslearn/framework/understand/understand-cloud-usage-cost.md) | Understand pillar overview | +| [ingestion.md](references/docs-mslearn/framework/understand/ingestion.md) | Data ingestion | +| [allocation.md](references/docs-mslearn/framework/understand/allocation.md) | Cost allocation | +| [reporting.md](references/docs-mslearn/framework/understand/reporting.md) | Reporting and analytics | +| [anomalies.md](references/docs-mslearn/framework/understand/anomalies.md) | Anomaly management | + +#### Quantify business value + +| File | Description | +|------|-------------| +| [quantify-business-value.md](references/docs-mslearn/framework/quantify/quantify-business-value.md) | Quantify pillar overview | +| [planning.md](references/docs-mslearn/framework/quantify/planning.md) | Planning and estimating | +| [budgeting.md](references/docs-mslearn/framework/quantify/budgeting.md) | Budgeting | +| [forecasting.md](references/docs-mslearn/framework/quantify/forecasting.md) | Forecasting | +| [benchmarking.md](references/docs-mslearn/framework/quantify/benchmarking.md) | Benchmarking | +| [unit-economics.md](references/docs-mslearn/framework/quantify/unit-economics.md) | Unit economics | + +#### Optimize cloud usage and cost + +| File | Description | +|------|-------------| +| [optimize-cloud-usage-cost.md](references/docs-mslearn/framework/optimize/optimize-cloud-usage-cost.md) | Optimize pillar overview | +| [architecting.md](references/docs-mslearn/framework/optimize/architecting.md) | Architecting for cloud | +| [workloads.md](references/docs-mslearn/framework/optimize/workloads.md) | Usage optimization | +| [rates.md](references/docs-mslearn/framework/optimize/rates.md) | Rate optimization | +| [licensing.md](references/docs-mslearn/framework/optimize/licensing.md) | Licensing and SaaS | +| [sustainability.md](references/docs-mslearn/framework/optimize/sustainability.md) | Cloud sustainability | + +#### Manage the FinOps practice + +| File | Description | +|------|-------------| +| [manage-finops.md](references/docs-mslearn/framework/manage/manage-finops.md) | Manage pillar overview | +| [onboarding.md](references/docs-mslearn/framework/manage/onboarding.md) | FinOps onboarding | +| [education.md](references/docs-mslearn/framework/manage/education.md) | FinOps education and enablement | +| [operations.md](references/docs-mslearn/framework/manage/operations.md) | FinOps operations | +| [governance.md](references/docs-mslearn/framework/manage/governance.md) | FinOps and governance | +| [assessment.md](references/docs-mslearn/framework/manage/assessment.md) | FinOps assessment | +| [invoicing-chargeback.md](references/docs-mslearn/framework/manage/invoicing-chargeback.md) | Invoicing and chargeback | +| [tools-services.md](references/docs-mslearn/framework/manage/tools-services.md) | Tools and services | +| [intersecting-disciplines.md](references/docs-mslearn/framework/manage/intersecting-disciplines.md) | Intersecting disciplines | + +### FOCUS + +| File | Description | +|------|-------------| +| [what-is-focus.md](references/docs-mslearn/focus/what-is-focus.md) | What is FOCUS (FinOps Open Cost and Usage Specification) | +| [mapping.md](references/docs-mslearn/focus/mapping.md) | FOCUS column mapping | +| [metadata.md](references/docs-mslearn/focus/metadata.md) | FOCUS metadata | +| [convert.md](references/docs-mslearn/focus/convert.md) | Converting data to FOCUS | +| [validate.md](references/docs-mslearn/focus/validate.md) | Validating FOCUS data | +| [conformance-summary.md](references/docs-mslearn/focus/conformance-summary.md) | FOCUS conformance summary | +| [conformance-full-report.md](references/docs-mslearn/focus/conformance-full-report.md) | FOCUS conformance full report | + +### Cost optimization best practices + +| File | Description | +|------|-------------| +| [general.md](references/docs-mslearn/best-practices/general.md) | General cost optimization best practices | +| [compute.md](references/docs-mslearn/best-practices/compute.md) | Compute cost optimization | +| [databases.md](references/docs-mslearn/best-practices/databases.md) | Database cost optimization | +| [networking.md](references/docs-mslearn/best-practices/networking.md) | Networking cost optimization | +| [storage.md](references/docs-mslearn/best-practices/storage.md) | Storage cost optimization | +| [web.md](references/docs-mslearn/best-practices/web.md) | Web and app service cost optimization | +| [library.md](references/docs-mslearn/best-practices/library.md) | Best practices library | + +### FinOps toolkit + +| File | Description | +|------|-------------| +| [finops-toolkit-overview.md](references/docs-mslearn/toolkit/finops-toolkit-overview.md) | FinOps toolkit overview | +| [changelog.md](references/docs-mslearn/toolkit/changelog.md) | Toolkit changelog | +| [roadmap.md](references/docs-mslearn/toolkit/roadmap.md) | Toolkit roadmap | +| [open-data.md](references/docs-mslearn/toolkit/open-data.md) | Open data (pricing units, regions, services, resource types) | +| [data-lake-storage-connectivity.md](references/docs-mslearn/toolkit/data-lake-storage-connectivity.md) | Data lake storage connectivity | + +### FinOps hubs + +| File | Description | +|------|-------------| +| [finops-hubs-overview.md](references/docs-mslearn/toolkit/hubs/finops-hubs-overview.md) | FinOps hubs overview | +| [deploy.md](references/docs-mslearn/toolkit/hubs/deploy.md) | Deploy FinOps hubs | +| [upgrade.md](references/docs-mslearn/toolkit/hubs/upgrade.md) | Upgrade FinOps hubs | +| [template.md](references/docs-mslearn/toolkit/hubs/template.md) | Hub Bicep template reference | +| [data-model.md](references/docs-mslearn/toolkit/hubs/data-model.md) | Hub database data model. **Read for authoritative schema reference.** | +| [data-processing.md](references/docs-mslearn/toolkit/hubs/data-processing.md) | Data processing pipeline | +| [savings-calculations.md](references/docs-mslearn/toolkit/hubs/savings-calculations.md) | Savings calculations methodology | +| [compatibility.md](references/docs-mslearn/toolkit/hubs/compatibility.md) | Version compatibility matrix | +| [configure-scopes.md](references/docs-mslearn/toolkit/hubs/configure-scopes.md) | Configure cost export scopes | +| [configure-dashboards.md](references/docs-mslearn/toolkit/hubs/configure-dashboards.md) | Configure dashboards | +| [configure-remote-hubs.md](references/docs-mslearn/toolkit/hubs/configure-remote-hubs.md) | Configure remote hubs | +| [configure-ai.md](references/docs-mslearn/toolkit/hubs/configure-ai.md) | Configure AI copilot for FinOps hubs | +| [private-networking.md](references/docs-mslearn/toolkit/hubs/private-networking.md) | Private networking configuration | + +### Alerts + +| File | Description | +|------|-------------| +| [finops-alerts-overview.md](references/docs-mslearn/toolkit/alerts/finops-alerts-overview.md) | FinOps alerts overview | +| [configure-finops-alerts.md](references/docs-mslearn/toolkit/alerts/configure-finops-alerts.md) | Configure FinOps alerts | + +### Bicep registry + +| File | Description | +|------|-------------| +| [modules.md](references/docs-mslearn/toolkit/bicep-registry/modules.md) | Bicep registry modules | +| [scheduled-actions.md](references/docs-mslearn/toolkit/bicep-registry/scheduled-actions.md) | Scheduled actions module | + +### Optimization engine + +| File | Description | +|------|-------------| +| [overview.md](references/docs-mslearn/toolkit/optimization-engine/overview.md) | Optimization engine overview | +| [setup-options.md](references/docs-mslearn/toolkit/optimization-engine/setup-options.md) | Setup options | +| [configure-workspaces.md](references/docs-mslearn/toolkit/optimization-engine/configure-workspaces.md) | Configure workspaces | +| [customize.md](references/docs-mslearn/toolkit/optimization-engine/customize.md) | Customize the optimization engine | +| [reports.md](references/docs-mslearn/toolkit/optimization-engine/reports.md) | Optimization reports | +| [suppress-recommendations.md](references/docs-mslearn/toolkit/optimization-engine/suppress-recommendations.md) | Suppress recommendations | +| [troubleshooting.md](references/docs-mslearn/toolkit/optimization-engine/troubleshooting.md) | Troubleshooting | +| [faq.md](references/docs-mslearn/toolkit/optimization-engine/faq.md) | Frequently asked questions | + +### Power BI + +| File | Description | +|------|-------------| +| [reports.md](references/docs-mslearn/toolkit/power-bi/reports.md) | Power BI reports overview | +| [setup.md](references/docs-mslearn/toolkit/power-bi/setup.md) | Power BI setup | +| [connector.md](references/docs-mslearn/toolkit/power-bi/connector.md) | FinOps toolkit Power BI connector | +| [template-app.md](references/docs-mslearn/toolkit/power-bi/template-app.md) | Power BI template app | +| [help-me-choose.md](references/docs-mslearn/toolkit/power-bi/help-me-choose.md) | Help me choose a Power BI report | +| [cost-summary.md](references/docs-mslearn/toolkit/power-bi/cost-summary.md) | Cost summary report | +| [rate-optimization.md](references/docs-mslearn/toolkit/power-bi/rate-optimization.md) | Rate optimization report | +| [workload-optimization.md](references/docs-mslearn/toolkit/power-bi/workload-optimization.md) | Usage optimization report | +| [governance.md](references/docs-mslearn/toolkit/power-bi/governance.md) | Governance report | +| [data-ingestion.md](references/docs-mslearn/toolkit/power-bi/data-ingestion.md) | Data ingestion report | +| [invoicing.md](references/docs-mslearn/toolkit/power-bi/invoicing.md) | Invoicing report | + +### Workbooks + +| File | Description | +|------|-------------| +| [finops-workbooks-overview.md](references/docs-mslearn/toolkit/workbooks/finops-workbooks-overview.md) | FinOps workbooks overview | +| [customize-workbooks.md](references/docs-mslearn/toolkit/workbooks/customize-workbooks.md) | Customize workbooks | +| [optimization.md](references/docs-mslearn/toolkit/workbooks/optimization.md) | Optimization workbook | +| [governance.md](references/docs-mslearn/toolkit/workbooks/governance.md) | Governance workbook | + +### Fabric + +| File | Description | +|------|-------------| +| [create-fabric-workspace-finops.md](references/docs-mslearn/fabric/create-fabric-workspace-finops.md) | Create a Fabric workspace for FinOps | + +### PowerShell commands + +| File | Description | +|------|-------------| +| [powershell-commands.md](references/docs-mslearn/toolkit/powershell/powershell-commands.md) | PowerShell commands overview | + +#### Cost Management commands + +| File | Description | +|------|-------------| +| [cost-management-commands.md](references/docs-mslearn/toolkit/powershell/cost/cost-management-commands.md) | Cost Management commands overview | +| [get-finopscostexport.md](references/docs-mslearn/toolkit/powershell/cost/get-finopscostexport.md) | Get-FinOpsCostExport | +| [new-finopscostexport.md](references/docs-mslearn/toolkit/powershell/cost/new-finopscostexport.md) | New-FinOpsCostExport | +| [start-finopscostexport.md](references/docs-mslearn/toolkit/powershell/cost/start-finopscostexport.md) | Start-FinOpsCostExport | +| [remove-finopscostexport.md](references/docs-mslearn/toolkit/powershell/cost/remove-finopscostexport.md) | Remove-FinOpsCostExport | +| [add-finopsserviceprincipal.md](references/docs-mslearn/toolkit/powershell/cost/add-finopsserviceprincipal.md) | Add-FinOpsServicePrincipal | + +#### Open data commands + +| File | Description | +|------|-------------| +| [open-data-commands.md](references/docs-mslearn/toolkit/powershell/data/open-data-commands.md) | Open data commands overview | +| [get-finopspricingunit.md](references/docs-mslearn/toolkit/powershell/data/get-finopspricingunit.md) | Get-FinOpsPricingUnit | +| [get-finopsregion.md](references/docs-mslearn/toolkit/powershell/data/get-finopsregion.md) | Get-FinOpsRegion | +| [get-finopsresourcetype.md](references/docs-mslearn/toolkit/powershell/data/get-finopsresourcetype.md) | Get-FinOpsResourceType | +| [get-finopsservice.md](references/docs-mslearn/toolkit/powershell/data/get-finopsservice.md) | Get-FinOpsService | + +#### FinOps hubs commands + +| File | Description | +|------|-------------| +| [finops-hubs-commands.md](references/docs-mslearn/toolkit/powershell/hubs/finops-hubs-commands.md) | FinOps hubs commands overview | +| [deploy-finopshub.md](references/docs-mslearn/toolkit/powershell/hubs/deploy-finopshub.md) | Deploy-FinOpsHub | +| [get-finopshub.md](references/docs-mslearn/toolkit/powershell/hubs/get-finopshub.md) | Get-FinOpsHub | +| [initialize-finopshubdeployment.md](references/docs-mslearn/toolkit/powershell/hubs/initialize-finopshubdeployment.md) | Initialize-FinOpsHubDeployment | +| [register-finopshubproviders.md](references/docs-mslearn/toolkit/powershell/hubs/register-finopshubproviders.md) | Register-FinOpsHubProviders | +| [remove-finopshub.md](references/docs-mslearn/toolkit/powershell/hubs/remove-finopshub.md) | Remove-FinOpsHub | +| [remove-finopshubscope.md](references/docs-mslearn/toolkit/powershell/hubs/remove-finopshubscope.md) | Remove-FinOpsHubScope | + +#### Toolkit commands + +| File | Description | +|------|-------------| +| [finops-toolkit-commands.md](references/docs-mslearn/toolkit/powershell/toolkit/finops-toolkit-commands.md) | Toolkit commands overview | +| [get-finopstoolkitversion.md](references/docs-mslearn/toolkit/powershell/toolkit/get-finopstoolkitversion.md) | Get-FinOpsToolkitVersion | + +### Help and support + +| File | Description | +|------|-------------| +| [help-options.md](references/docs-mslearn/toolkit/help/help-options.md) | Help options | +| [support.md](references/docs-mslearn/toolkit/help/support.md) | Support | +| [troubleshooting.md](references/docs-mslearn/toolkit/help/troubleshooting.md) | Troubleshooting | +| [errors.md](references/docs-mslearn/toolkit/help/errors.md) | Error reference | +| [deploy.md](references/docs-mslearn/toolkit/help/deploy.md) | Deployment help | +| [data-dictionary.md](references/docs-mslearn/toolkit/help/data-dictionary.md) | Data dictionary | +| [terms.md](references/docs-mslearn/toolkit/help/terms.md) | Terms and definitions | +| [contributors.md](references/docs-mslearn/toolkit/help/contributors.md) | Contributors | diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-anomaly-detection.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-anomaly-detection.md new file mode 100644 index 000000000..7d54e7d7a --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-anomaly-detection.md @@ -0,0 +1,171 @@ +--- +name: cost-anomaly-detection +description: Detects unusual cost changes in FinOps hubs by establishing baseline spend, surfacing anomalies, and identifying the services, subscriptions, regions, resource groups, resources, or usage types driving the deviation +author: FinOps Toolkit Team +version: 1.0.0 +license: Apache-2.0 +--- + +# Cost anomaly detection + +## Purpose +Use this reference to detect unusual cost behavior in a FinOps hub, determine whether a cost change is a short-lived spike or a sustained baseline shift, and identify the drivers that require investigation. + +## When to use +- “Are there any cost anomalies?” +- “What changed unexpectedly in the last few days or weeks?” +- “Did we have an unusual spike or drop?” +- “Which service, subscription, or resource group caused the anomaly?” +- Weekly operational review, budget alert triage, or incident follow-up + +## Grounding +Use these canonical assets first: +- `cost-anomaly-detection.kql` +- `monthly-cost-change-percentage.kql` +- `Costs()` + +Use the FinOps hub database guide and query catalog as the authoritative source for valid columns and query patterns. + +## How this skill works + +### Step 1: Define the anomaly window +- Default to the last 30 to 90 days for recent anomaly checks. +- Use a longer lookback if you need to distinguish a one-time event from seasonality or a new normal. +- Keep the investigation window explicit so baseline and anomaly periods are comparable. + +### Step 2: Start with the catalog query +Use `cost-anomaly-detection.kql` first to surface unusual movement in total daily cost. + +Questions to answer: +- Which dates are flagged as anomalous? +- Are anomalies positive spikes, negative drops, or repeated oscillations? +- Does the pattern look isolated or sustained? + +### Step 3: Quantify recent change +Use `monthly-cost-change-percentage.kql` when you need to explain whether the anomaly also appears in month-over-month movement. + +This is especially helpful when: +- the anomaly appears near a month boundary +- stakeholders want a simple percentage summary +- you need to distinguish a daily outlier from a broader monthly shift + +### Step 4: Decompose the anomaly with `Costs()` +Use `Costs()` to identify which dimensions explain the anomaly. + +Common fields to test: +- `ServiceName` +- `SubAccountName` +- `RegionName` +- `x_ResourceGroupName` +- `ResourceName` +- `ResourceType` +- `x_UsageType` + +#### Example: daily service anomaly breakdown +```kusto +let startDate = ago(30d); +let endDate = now(); +Costs() +| where ChargePeriodStart between (startDate .. endDate) +| summarize EffectiveCost = sum(EffectiveCost) by Day = startofday(ChargePeriodStart), ServiceName +| order by Day asc, EffectiveCost desc +``` + +#### Example: subscription anomaly breakdown +```kusto +let startDate = ago(30d); +let endDate = now(); +Costs() +| where ChargePeriodStart between (startDate .. endDate) +| summarize EffectiveCost = sum(EffectiveCost) by Day = startofday(ChargePeriodStart), SubAccountName +| order by Day asc, EffectiveCost desc +``` + +#### Example: resource group and resource investigation +```kusto +let startDate = ago(14d); +let endDate = now(); +Costs() +| where ChargePeriodStart between (startDate .. endDate) +| summarize EffectiveCost = sum(EffectiveCost) by Day = startofday(ChargePeriodStart), x_ResourceGroupName, ResourceName, ResourceType +| order by Day asc, EffectiveCost desc +``` + +Use the same pattern to isolate whether the anomaly is concentrated in `RegionName` or `x_UsageType`. + +### Step 5: Classify the anomaly pattern +After reviewing `cost-anomaly-detection.kql` and targeted `Costs()` breakdowns, classify the result: +- **Spike:** abrupt increase followed by reversion +- **Drop:** abrupt decrease followed by reversion +- **Step change:** abrupt increase or decrease that persists +- **Drift:** gradual movement away from prior baseline +- **Concentrated anomaly:** mostly explained by one `ServiceName`, `SubAccountName`, or `x_ResourceGroupName` +- **Broad anomaly:** spread across multiple dimensions + +### Step 6: Name the primary driver +State the primary driver explicitly: +- top `ServiceName` contributor +- top `SubAccountName` contributor +- top `RegionName` contributor +- top `x_ResourceGroupName` contributor +- top `ResourceName` or `ResourceType` contributor when the anomaly is workload-specific + +### Step 7: Determine likely meaning +Interpret the anomaly before recommending action: +- **Expected business event:** planned launch, migration, scale-out, or month-end processing +- **Operational issue:** runaway workload, misconfiguration, or failed cleanup +- **Data-quality concern:** unexpected drop or gap that may indicate ingest delay or incomplete data +- **Optimization opportunity:** sustained increase tied to inefficient resource or usage patterns + +## Output format + +### 1. Executive summary +- Anomaly detected: yes or no +- Most significant anomaly date or period +- Estimated impact in cost and percentage terms +- Primary driver by `ServiceName`, `SubAccountName`, `RegionName`, `x_ResourceGroupName`, `ResourceName`, or `ResourceType` +- One-sentence conclusion + +### 2. Anomaly details +- Detection window analyzed +- Baseline behavior described in plain language +- Observed anomaly pattern: spike, drop, step change, or drift +- Magnitude of deviation from normal +- Whether the anomaly appears isolated or ongoing + +### 3. Driver breakdown + +| Dimension | Top value | Why it matters | +| --- | --- | --- | +| ServiceName | [value] | Largest service contributor | +| SubAccountName | [value] | Largest subscription contributor | +| RegionName | [value] | Regional concentration | +| x_ResourceGroupName | [value] | Workload grouping most affected | +| ResourceType | [value] | Resource class driving change | + +### 4. Interpretation +- What changed first? +- What dimension explains the largest share of the anomaly? +- Does the anomaly indicate a one-time event or a baseline reset? +- Does the pattern require immediate action, monitoring, or routine follow-up? + +### 5. Recommended next step +- Investigate the largest driver when change is concentrated +- Review workload or deployment events around the anomaly date +- Compare against monthly movement if leadership wants trend context +- Continue monitoring if the anomaly is understood and expected + +## Best practices +1. Start with `cost-anomaly-detection.kql` before writing custom anomaly logic. +2. Use `Costs()` to explain the anomaly, not just to confirm it exists. +3. Break down anomalies by `ServiceName`, `SubAccountName`, `RegionName`, and `x_ResourceGroupName` before jumping to conclusions. +4. Use `ResourceName`, `ResourceType`, and `x_UsageType` when the anomaly appears concentrated in a narrow workload slice. +5. Use `monthly-cost-change-percentage.kql` when stakeholders need month-over-month context. +6. Distinguish a short spike from a sustained step change. +7. Treat large negative anomalies as possible data or ingestion issues until validated. + +## See also +- `queries/INDEX.md` +- `queries/finops-hub-database-guide.md` +- `cost-anomaly-detection.kql` +- `monthly-cost-change-percentage.kql` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-comparison.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-comparison.md new file mode 100644 index 000000000..dce63261b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-comparison.md @@ -0,0 +1,293 @@ +--- +name: cost-comparison +description: Compares FinOps hubs cost across time periods, subscriptions, regions, resource groups, billing hierarchy, and optional tags to explain spending differences and benchmark efficiency +author: FinOps Toolkit Team +version: 1.0.0 +license: Apache-2.0 +--- + +# Cost comparison + +## Purpose +Use FinOps hubs data to compare cost across two or more periods or groups, quantify the size of the difference, and explain which services, subscriptions, regions, resource groups, or billing-hierarchy nodes account for the change. + +## When to use +- "Compare costs between period A and period B" +- "Show me month-over-month changes" +- "Which subscription, region, or service is more expensive?" +- "Compare production and non-production costs" +- "Benchmark billing profiles, invoice sections, or resource groups" +- Keywords: compare, comparison, versus, vs, difference, benchmark, relative, month-over-month + +## Prerequisites +- Confirm the hub connection, reporting window, and any required filters. +- Use `references/queries/finops-hub-database-guide.md` to verify available FinOps hubs fields and enrichment columns. +- Use `references/queries/INDEX.md` to select the closest starting query. +- Prefer scenario-specific aggregate queries first. Use `costs-enriched-base.kql` only for scoped row-level drill-downs after aggregate queries do not answer the question. + +## Recommended comparison dimensions +Choose the first grouping that best matches the question: + +- `ServiceName` for side-by-side service comparison across periods or groups +- `SubAccountName` for subscription or sub-account comparison +- `RegionName` for regional comparison +- `x_ResourceGroupName` for resource-group comparison +- `x_BillingProfileName` and `x_InvoiceSectionName` for billing-hierarchy comparison + +Tag-based grouping is optional. If your organization uses tags and coverage is good enough, compare `Tags['team']`, `Tags['product']`, `Tags['application']`, or `Tags['environment']`. + +If tags are missing, blank, or tag coverage is incomplete, fall back to `ServiceName`, `SubAccountName`, `RegionName`, `x_ResourceGroupName`, `x_BillingProfileName`, or `x_InvoiceSectionName` instead. + +Treat blank tag values as incomplete metadata, not as a reliable business grouping. + +## Recommended query assets +- `monthly-cost-change-percentage.kql` for month-over-month change analysis +- `cost-by-region-trend.kql` for region-led comparisons and trend context +- `cost-by-financial-hierarchy.kql` for billing profile and invoice section comparisons +- `costs-enriched-base.kql` only for scoped row-level drill-downs when the aggregate catalog queries do not provide enough detail + +## Analysis workflow + +### Step 1: Identify the comparison type +Confirm whether the request is: + +- period versus period +- month-over-month +- before versus after an optimization or migration +- subscription versus subscription +- region versus region +- resource group versus resource group +- billing profile or invoice section versus peers + +Also confirm whether the user wants a technical comparison, a finance or allocation comparison, or both. + +### Step 2: Choose the strongest base dimension +Start with one high-signal non-tag dimension before adding more detail: + +- `ServiceName` when comparing what changed across equal time windows +- `SubAccountName` when comparing subscriptions or business-owned scopes +- `RegionName` when testing whether geography explains the difference +- `x_ResourceGroupName` when operational ownership matters most +- `x_BillingProfileName` and `x_InvoiceSectionName` when the comparison is for showback or chargeback + +### Step 3: Build comparable datasets + +**Side-by-side period comparison by service** + +This pattern works well for comparing equal windows by `ServiceName`. + +```kusto +let currentStart = datetime(2024-02-01); +let currentEnd = datetime(2024-03-01); +let previousStart = datetime(2024-01-01); +let previousEnd = datetime(2024-02-01); +union +( + Costs() + | where ChargePeriodStart >= currentStart and ChargePeriodStart < currentEnd + | summarize EffectiveCost = sum(EffectiveCost) by ServiceName + | extend ComparisonGroup = 'Current period' +), +( + Costs() + | where ChargePeriodStart >= previousStart and ChargePeriodStart < previousEnd + | summarize EffectiveCost = sum(EffectiveCost) by ServiceName + | extend ComparisonGroup = 'Previous period' +) +| order by ServiceName asc, ComparisonGroup asc +``` + +**Month-over-month comparison** + +Start with `monthly-cost-change-percentage.kql` to quantify the overall shift, then drill into the main driver dimension. + +```kusto +let startDate = startofmonth(ago(90d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by Month = startofmonth(ChargePeriodStart), ServiceName +| order by Month asc, EffectiveCost desc +``` + +**Region comparison** + +Use `cost-by-region-trend.kql` when the question is whether `RegionName` explains cost variance. + +```kusto +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by RegionName, ServiceName +| order by RegionName asc, EffectiveCost desc +``` + +**Billing-hierarchy comparison** + +Use `cost-by-financial-hierarchy.kql` when the baseline needs to follow finance ownership. + +```kusto +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) + by x_BillingProfileName, x_InvoiceSectionName, SubAccountName +| order by EffectiveCost desc +``` + +### Step 4: Calculate comparison metrics +For each comparable row, calculate: + +- **Absolute difference:** `Difference = CostA - CostB` +- **Percentage difference:** `% Difference = ((CostA - CostB) / CostB) * 100` +- **Ratio:** `Ratio = CostA / CostB` +- **Share of total:** each row's cost divided by total cost for its group + +When period lengths differ, normalize to a daily average before interpreting the result. + +### Step 5: Identify the largest differences +Look for: + +- services with the largest dollar delta in `ServiceName` +- subscriptions with the largest swing in `SubAccountName` +- region shifts in `RegionName` +- ownership concentration in `x_ResourceGroupName` +- allocation changes in `x_BillingProfileName` and `x_InvoiceSectionName` +- rows present in one group but absent in the other + +### Step 6: Drill into the root cause +After the first comparison, add one more dimension to explain the difference: + +- `ServiceName` by `SubAccountName` +- `ServiceName` by `RegionName` +- `x_ResourceGroupName` within the most expensive service or subscription +- `x_BillingProfileName` → `x_InvoiceSectionName` → `SubAccountName` + +Use `costs-enriched-base.kql` when you need to pivot the same filtered dataset more than once. + +### Step 7: Add optional tag overlays only after the base comparison is clear +If tag quality is usable, add `Tags['team']`, `Tags['product']`, `Tags['application']`, or `Tags['environment']` to explain ownership or workload context. + +If tags are missing or incomplete, use `ServiceName`, `SubAccountName`, `RegionName`, `x_ResourceGroupName`, `x_BillingProfileName`, or `x_InvoiceSectionName` instead so the comparison remains reliable. + +### Step 8: Explain what changed and what to do next +Summarize: + +- what changed +- where the difference is concentrated +- whether the difference is expected or avoidable +- which follow-up analysis or optimization should happen next + +## Output format + +### 1. Executive summary +- what was compared +- overall cost difference in dollars and percent +- one-sentence explanation of the primary driver +- recommended next action + +### 2. High-level comparison + +| Group | Total cost | Difference from baseline | % difference | +|------|------------|--------------------------|--------------| +| Group A | $X,XXX | +$X,XXX | +XX% | +| Group B | $X,XXX | baseline | 0% | + +### 3. Primary driver breakdown + +| Dimension | Group A | Group B | Difference | % difference | Notes | +|-----------|---------|---------|------------|--------------|-------| +| `ServiceName` or other primary field | $X,XXX | $X,XXX | +$XXX | +XX% | Main explanation | + +### 4. Root-cause drill-down +Use one of these structures: + +- `ServiceName` by `SubAccountName` +- `ServiceName` by `RegionName` +- `x_ResourceGroupName` for the most expensive subscription +- `x_BillingProfileName` and `x_InvoiceSectionName` for allocation conversations + +### 5. Unique or shifted costs +- rows only present in one group +- major cost movements between subscriptions, regions, or resource groups +- one-time charges or newly adopted services + +### 6. Optional tag view +If tag quality is usable, summarize what `Tags['team']`, `Tags['product']`, `Tags['application']`, or `Tags['environment']` adds to the analysis. + +If tags are blank or incomplete, explicitly state that the comparison relied on `ServiceName`, `SubAccountName`, `RegionName`, `x_ResourceGroupName`, `x_BillingProfileName`, or `x_InvoiceSectionName` instead. + +### 7. Recommendations +Recommend: +1. which cost deltas need immediate validation +2. which owners or teams should review the biggest differences +3. which resource groups, subscriptions, or regions need deeper investigation +4. whether metadata cleanup is needed before using tags in future comparisons + +## Common comparison scenarios + +### Scenario 1: This month versus last month +Goal: understand month-over-month change. + +Approach: +1. start with `monthly-cost-change-percentage.kql` +2. compare equal monthly windows +3. break the delta down by `ServiceName` +4. isolate new, removed, or sharply changed services +5. normalize for daily average if month lengths differ + +### Scenario 2: Production versus non-production +Goal: verify non-production is scaled appropriately. + +Approach: +1. use `ServiceName`, `SubAccountName`, or `x_ResourceGroupName` as the baseline comparison +2. add `Tags['environment']` only if the tag is present and trustworthy +3. compare service mix and total cost +4. identify oversized non-production resources +5. recommend rightsizing or shutdown opportunities + +### Scenario 3: Subscription versus subscription +Goal: benchmark business or technical ownership scopes. + +Approach: +1. compare totals by `SubAccountName` +2. split major differences by `ServiceName` +3. check whether `RegionName` or `x_ResourceGroupName` explains the variance +4. identify repeatable practices from the lower-cost peer + +### Scenario 4: Region versus region +Goal: understand regional cost differences. + +Approach: +1. start with `cost-by-region-trend.kql` +2. compare the same services across `RegionName` +3. check whether service mix or data movement explains the variance +4. recommend consolidation or placement review if the premium is avoidable + +### Scenario 5: Before versus after an optimization +Goal: measure realized impact. + +Approach: +1. compare equal windows before and after the change +2. quantify savings in dollars and percent +3. validate which `ServiceName`, `SubAccountName`, or `x_ResourceGroupName` changed +4. document repeatable lessons for future optimization work + +## Best practices +1. Compare equal periods whenever possible. +2. Start with a strong non-tag baseline dimension. +3. Use both dollars and percentages so materiality is obvious. +4. Use `monthly-cost-change-percentage.kql` for month-over-month context, then explain the delta with `ServiceName` or another strong grouping. +5. Use `costs-enriched-base.kql` when you need multiple follow-up pivots from the same filtered scope. +6. Add optional tags only when coverage is good enough to trust. +7. If tags are incomplete, say so and fall back to non-tag fields. + +## See also +- `references/queries/INDEX.md` +- `references/queries/finops-hub-database-guide.md` +- `costs-enriched-base.kql` +- `monthly-cost-change-percentage.kql` +- `cost-by-region-trend.kql` +- `cost-by-financial-hierarchy.kql` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-spike-investigation.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-spike-investigation.md new file mode 100644 index 000000000..733ce043c --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-spike-investigation.md @@ -0,0 +1,192 @@ +--- +name: cost-spike-investigation +description: Investigates sudden cost increases in FinOps hubs by comparing spike periods to baseline periods and isolating the services, subscriptions, regions, resource groups, resources, resource types, or usage types responsible for the change +author: FinOps Toolkit Team +version: 1.0.0 +license: Apache-2.0 +--- + +# Cost spike investigation + +## Purpose +Use this reference to explain why cost increased suddenly in a FinOps hub, determine whether the change is a short-lived spike or the start of a new baseline, and identify the drivers that need follow-up. + +## When to use +- “Why did cost jump?” +- “What caused the spike this week or this month?” +- “Which service or subscription explains the increase?” +- “Is this a one-time event or a sustained change?” +- Budget alert, anomaly triage, or executive escalation + +## Grounding +Use these canonical assets first: +- `cost-anomaly-detection.kql` +- `monthly-cost-change-percentage.kql` +- `top-services-by-cost.kql` +- `Costs()` + +Use the FinOps hub database guide and query catalog as the authoritative source for valid columns and query patterns. + +## How this skill works + +### Step 1: Define the spike and baseline periods +- Clarify the exact period where the spike appeared. +- Choose a comparable baseline period of equal length. +- Default to the last 7 days versus the previous 7 days when the user does not specify a window. +- Keep the comparison explicit so absolute and percentage change are easy to explain. + +### Step 2: Confirm the spike exists +Start with `cost-anomaly-detection.kql` to identify unusual daily movement and confirm when the spike started. + +Questions to answer: +- Which dates show the most unusual movement? +- Does the spike revert quickly or persist for multiple days? +- Is the spike isolated or part of a broader pattern? + +### Step 3: Quantify the magnitude +Use `monthly-cost-change-percentage.kql` when you need a simple percentage summary for stakeholders. + +Focus on: +- absolute increase +- percentage increase +- whether the latest monthly movement supports the same story as the daily spike + +### Step 4: Identify the first major driver +Use `top-services-by-cost.kql` to quickly identify whether one or two services explain most of the increase. + +Focus on: +- `ServiceName` values with the largest current spend +- services with the largest increase from baseline +- whether the spike is concentrated or broad-based + +### Step 5: Decompose the spike with `Costs()` +Use `Costs()` to isolate the dimensions responsible for the spike. + +Common fields to test: +- `ServiceName` +- `SubAccountName` +- `RegionName` +- `x_ResourceGroupName` +- `ResourceName` +- `ResourceType` +- `x_UsageType` + +#### Example: compare service cost across spike and baseline periods +```kusto +let spikeStart = ago(7d); +let spikeEnd = now(); +let baselineStart = ago(14d); +let baselineEnd = ago(7d); +let spike = + Costs() + | where ChargePeriodStart between (spikeStart .. spikeEnd) + | summarize SpikeCost = sum(EffectiveCost) by ServiceName; +let baseline = + Costs() + | where ChargePeriodStart between (baselineStart .. baselineEnd) + | summarize BaselineCost = sum(EffectiveCost) by ServiceName; +spike +| join kind=fullouter baseline on ServiceName +| extend SpikeCost = coalesce(SpikeCost, 0.0), BaselineCost = coalesce(BaselineCost, 0.0) +| extend CostIncrease = SpikeCost - BaselineCost +| order by CostIncrease desc +``` + +#### Example: isolate subscription and region drivers +```kusto +let spikeStart = ago(7d); +let spikeEnd = now(); +Costs() +| where ChargePeriodStart between (spikeStart .. spikeEnd) +| summarize EffectiveCost = sum(EffectiveCost) by SubAccountName, RegionName +| order by EffectiveCost desc +``` + +#### Example: isolate workload-specific resources +```kusto +let spikeStart = ago(7d); +let spikeEnd = now(); +Costs() +| where ChargePeriodStart between (spikeStart .. spikeEnd) +| summarize EffectiveCost = sum(EffectiveCost) by x_ResourceGroupName, ResourceName, ResourceType, x_UsageType +| order by EffectiveCost desc +``` + +### Step 6: Determine the spike shape +After reviewing the catalog queries and targeted `Costs()` breakdowns, classify the spike: +- **Transient spike:** abrupt increase followed by reversion +- **Sustained spike:** abrupt increase that remains elevated +- **Concentrated spike:** mostly explained by one `ServiceName`, `SubAccountName`, or `x_ResourceGroupName` +- **Broad spike:** spread across several dimensions +- **Usage-driven spike:** concentrated in one `x_UsageType` or `ResourceType` + +### Step 7: Name the primary cause +State the most important driver explicitly: +- top `ServiceName` contributor +- top `SubAccountName` contributor +- top `RegionName` contributor +- top `x_ResourceGroupName` contributor +- top `ResourceName` or `ResourceType` contributor when the issue is workload-specific + +Then state whether the driver explains most of the increase or only part of it. + +### Step 8: Recommend the next action +Interpret the spike before recommending action: +- expected event such as scale-out, launch, migration, or planned testing +- operational issue such as runaway workload or failed cleanup +- optimization opportunity tied to inefficient `ResourceType` or `x_UsageType` +- data-quality concern if the pattern conflicts with other trend evidence + +## Output format + +### 1. Executive summary +- Spike detected: yes or no +- Spike period analyzed +- Total increase in cost and percentage terms +- Primary driver by `ServiceName`, `SubAccountName`, `RegionName`, `x_ResourceGroupName`, `ResourceName`, or `ResourceType` +- One-sentence conclusion + +### 2. Spike metrics +- Baseline period cost +- Spike period cost +- Absolute increase +- Percentage increase +- Whether the spike is still ongoing + +### 3. Driver breakdown + +| Dimension | Top value | Why it matters | +| --- | --- | --- | +| ServiceName | [value] | Largest service contribution | +| SubAccountName | [value] | Largest subscription contribution | +| RegionName | [value] | Regional concentration | +| x_ResourceGroupName | [value] | Workload grouping with the largest increase | +| ResourceType | [value] | Resource class driving the spike | + +### 4. Interpretation +- When did the spike begin? +- Which dimension explains the largest share of the increase? +- Is the spike transient or sustained? +- Is the increase expected, wasteful, or still unverified? + +### 5. Recommended next step +- Investigate the primary driver if change is concentrated +- Review recent deployment or scaling events around the spike window +- Monitor closely if the spike is understood and expected +- Pursue optimization if the increase is sustained without business justification + +## Best practices +1. Start with `cost-anomaly-detection.kql` before writing custom spike logic. +2. Use `monthly-cost-change-percentage.kql` when stakeholders need a simple summary of magnitude. +3. Use `top-services-by-cost.kql` to identify the fastest path to likely drivers. +4. Use `Costs()` to explain the spike with `ServiceName`, `SubAccountName`, `RegionName`, and `x_ResourceGroupName`. +5. Use `ResourceName`, `ResourceType`, and `x_UsageType` when the spike appears workload-specific. +6. Compare equal-length periods so the increase is defensible. +7. Distinguish a short spike from a sustained baseline reset. + +## See also +- `queries/INDEX.md` +- `queries/finops-hub-database-guide.md` +- `cost-anomaly-detection.kql` +- `monthly-cost-change-percentage.kql` +- `top-services-by-cost.kql` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-trend-analysis.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-trend-analysis.md new file mode 100644 index 000000000..c5715f7e4 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/cost-trend-analysis.md @@ -0,0 +1,186 @@ +--- +name: cost-trend-analysis +description: Analyzes FinOps hubs cost trends over time to identify direction, month-over-month change, service and subscription drivers, regional patterns, and forecast-ready spending signals +author: FinOps Toolkit Team +version: 1.0.0 +license: Apache-2.0 +--- + +# Cost trend analysis + +## Purpose +Use this reference to analyze how cost changes over time in a FinOps hub, explain whether spend is rising or falling, and identify which services, subscriptions, regions, or resource groups are driving the change. + +## When to use +- “How are my costs trending?” +- “Are we increasing or decreasing month over month?” +- “Which services are driving the trend?” +- “Which subscription or resource group changed the most?” +- “Do regions explain the increase?” +- Budget review, forecast preparation, or executive reporting + +## Grounding +Use these canonical assets first: +- `monthly-cost-trend.kql` +- `monthly-cost-change-percentage.kql` +- `cost-by-region-trend.kql` +- `Costs()` + +Use the FinOps hub database guide and query catalog as the authoritative source for valid columns and query patterns. + +## How this skill works + +### Step 1: Define the analysis window +- Default to the last 12 full months for leadership trend analysis. +- Use at least 3 months for short trend checks. +- Use 12 months when you need to describe seasonality, sustained growth, or baseline shifts. + +### Step 2: Establish the overall monthly trend +Start with `monthly-cost-trend.kql` to understand the total billed and effective cost trajectory. + +Questions to answer: +- Is the overall trend increasing, decreasing, or flat? +- Is the latest month above or below the recent baseline? +- Are billed and effective cost moving together? + +### Step 3: Quantify the month-over-month change +Use `monthly-cost-change-percentage.kql` to measure direction and magnitude. + +Focus on: +- Latest month-over-month change +- Consecutive positive or negative months +- Whether change is accelerating, decelerating, or reverting toward baseline + +### Step 4: Check whether region explains the trend +Use `cost-by-region-trend.kql` when geography may explain the change. + +Focus on: +- `RegionName` values with the largest current spend +- Regions with the largest absolute change +- Whether a single region explains most of the overall movement + +### Step 5: Decompose the trend with `Costs()` +Use `Costs()` when you need a tailored breakdown by driver. + +Common dimensions: +- `ServiceName` +- `SubAccountName` +- `RegionName` +- `x_ResourceGroupName` +- `ResourceName` +- `ResourceType` +- `x_UsageType` + +#### Example: service trend +```kusto +let startDate = startofmonth(ago(365d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by Month = startofmonth(ChargePeriodStart), ServiceName +| order by Month asc +``` + +#### Example: subscription trend +```kusto +let startDate = startofmonth(ago(365d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by Month = startofmonth(ChargePeriodStart), SubAccountName +| order by Month asc +``` + +#### Example: resource group trend +```kusto +let startDate = startofmonth(ago(365d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by Month = startofmonth(ChargePeriodStart), x_ResourceGroupName +| order by Month asc +``` + +Use the same pattern to compare `ResourceName`, `ResourceType`, or `x_UsageType` when the trend appears to be concentrated in a narrow workload slice. + +### Step 6: Classify the pattern +After reviewing the catalog queries and custom `Costs()` breakdowns, classify the trend: +- **Growth:** sustained upward movement over multiple months +- **Decline:** sustained downward movement over multiple months +- **Stable:** narrow range with low month-over-month movement +- **Volatile:** frequent swings with no clear baseline +- **Step change:** a permanent shift beginning in a specific month + +### Step 7: Identify the main driver +Name the primary driver explicitly: +- top `ServiceName` contributor +- top `SubAccountName` contributor +- top `RegionName` contributor +- top `x_ResourceGroupName` contributor + +State whether the driver explains most of the change or only part of it. + +### Step 8: Prepare a forecast-ready conclusion +Trend analysis should support planning, not pretend certainty. + +Provide: +- current direction +- latest month-over-month percentage change +- largest driver +- confidence level based on consistency of recent months +- a conservative, expected, and high scenario if forecasting is requested + +## Output format + +### 1. Executive summary +- Overall trend: increasing, decreasing, stable, or volatile +- Latest monthly effective cost +- Latest month-over-month change from `monthly-cost-change-percentage.kql` +- Primary driver by `ServiceName`, `SubAccountName`, `RegionName`, or `x_ResourceGroupName` +- One-sentence takeaway + +### 2. Core metrics +- Trend period analyzed +- Latest billed cost +- Latest effective cost +- Month-over-month billed change +- Month-over-month effective change +- Number of consecutive months in the same direction + +### 3. Driver breakdown +Summarize the top contributors behind the trend: + +| Dimension | Top value | Why it matters | +| --- | --- | --- | +| ServiceName | [value] | Largest service contribution | +| SubAccountName | [value] | Largest subscription contribution | +| RegionName | [value] | Largest regional contribution | +| x_ResourceGroupName | [value] | Largest workload grouping contribution | + +### 4. Pattern interpretation +- What changed first? +- What kept changing afterward? +- Is the pattern broad-based or concentrated? +- Does the trend look durable or temporary? + +### 5. Recommended next step +- Continue monitoring if stable and expected +- Investigate the main driver if change is concentrated +- Prepare budget or forecast adjustments if growth is sustained +- Review optimization opportunities if growth is not tied to expected business activity + +## Best practices +1. Start with `monthly-cost-trend.kql` before writing a custom query. +2. Use `monthly-cost-change-percentage.kql` to quantify the latest movement instead of describing trend direction qualitatively. +3. Use `cost-by-region-trend.kql` when regional deployment, migration, or failover may explain the shift. +4. Use `Costs()` only after the catalog query establishes baseline context. +5. Decompose the trend with `ServiceName`, `SubAccountName`, `RegionName`, and `x_ResourceGroupName` before jumping to conclusions. +6. Explain whether the trend is broad or concentrated. +7. Keep forecast language bounded by recent trend consistency. + +## See also +- `queries/INDEX.md` +- `queries/finops-hub-database-guide.md` +- `monthly-cost-trend.kql` +- `monthly-cost-change-percentage.kql` +- `cost-by-region-trend.kql` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/custom-dimension-analysis.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/custom-dimension-analysis.md new file mode 100644 index 000000000..b64373eb1 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/custom-dimension-analysis.md @@ -0,0 +1,153 @@ +--- +name: custom-dimension-analysis +description: Analyze costs with business allocation fields and tags in FinOps hubs for showback, chargeback, and ownership reporting when those fields are populated. +author: Microsoft +version: 1.0.0 +license: Apache-2.0 +--- + +# Custom dimension analysis + +## Purpose +Use this reference to analyze costs through business-aligned dimensions such as team, product, application, environment, cost center, or project. In FinOps hubs, `Costs()` is the primary surface for this analysis. + +Treat business allocation metadata as observed and optional. Do not assume every hub populates every tag or enrichment column. + +## Validated grounding +Use these repo assets as the approved starting points: +- `Costs()` for primary analysis +- `costs-enriched-base.kql` to inspect enriched cost records and available metadata +- `cost-by-financial-hierarchy.kql` for a ready-made allocation pattern +- `finops-hub-database-guide.md` for schema details +- `INDEX.md` to find the right catalog query for the scenario + +## Business allocation fields to inspect +Inspect which populated fields, tags, and columns are actually present in your hub before choosing a business dimension. + +Common fields worth checking: +- `Tags['team']` +- `Tags['product']` +- `Tags['application']` +- `Tags['environment']` +- `x_CostCenter` +- `x_Project` +- `x_BillingProfileName` +- `x_InvoiceSectionName` + +Some environments have strong tag coverage, some rely more on financial hierarchy columns, and some have both. Use only the fields that are observed and meaningfully populated in the reporting period. + +## Recommended workflow + +### 1. Inspect populated business fields first +Start with `cost-by-financial-hierarchy.kql` when billing hierarchy answers the allocation question. Use `costs-enriched-base.kql` or a small direct query against `Costs()` only to inspect a narrow sample of populated tags and enrichment fields: + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| project x_BillingProfileName, x_InvoiceSectionName, x_CostCenter, x_Project, Tags +| take 50 +``` + +Review which available fields and tags are actually populated. If available, compare how consistently `Tags['team']`, `Tags['product']`, `Tags['application']`, `Tags['environment']`, `x_CostCenter`, and `x_Project` are filled. + +### 2. Choose the business dimension with the best coverage +If `Tags['team']` is populated, start there: + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| extend Team = tostring(Tags['team']) +| summarize EffectiveCost = sum(EffectiveCost) by Team +| order by EffectiveCost desc +``` + +Repeat the same pattern for `Tags['product']`, `Tags['application']`, `Tags['environment']`, `x_CostCenter`, or `x_Project` when populated. + +### 3. Add financial hierarchy context +Use `cost-by-financial-hierarchy.kql` when you need business reporting that rolls up through billing ownership and allocation layers. + +Example pattern: + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| extend Team = tostring(Tags['team']) +| extend Product = tostring(Tags['product']) +| extend Application = tostring(Tags['application']) +| summarize EffectiveCost = sum(EffectiveCost) + by x_BillingProfileName, x_InvoiceSectionName, Team, Product, Application, x_CostCenter, x_Project +| order by EffectiveCost desc +``` + +This is useful for showback and chargeback because it connects business ownership to the financial hierarchy already present in the hub. + +### 4. Measure allocation coverage honestly +Business reporting is only as strong as field coverage. + +```kusto +let base = + Costs() + | where ChargePeriodStart >= startofmonth(ago(30d)) + | extend Team = tostring(Tags['team']); +base +| summarize + TotalCost = sum(EffectiveCost), + AllocatedCost = sumif(EffectiveCost, isnotempty(Team)) +| extend AllocationCoverage = AllocatedCost / TotalCost +``` + +If the selected field is only partially populated, say so clearly. Unallocated cost is an important finding, not a formatting problem. + +### 5. Compare multiple business dimensions only when useful +If populated, compare team, product, application, cost center, or project to answer questions such as: +- Which team owns the most cost? +- Which product has the fastest growth? +- Which application is expensive across multiple invoice sections? +- Which `x_CostCenter` or `x_Project` values have weak allocation coverage? + +## Output guidance +Present results in a way finance and engineering teams can use: + +### Executive summary +- Dimension analyzed +- Reporting window +- Total effective cost +- Allocation coverage percentage +- Largest observed business owner or group +- Biggest data quality gap, if populated fields are inconsistent + +### Business dimension breakdown +| Business dimension | Effective cost | Percent of total | Coverage note | +|---|---:|---:|---| +| Team A | $X | Y% | Well populated | +| Team B | $X | Y% | Partial tags | +| Unallocated | $X | Y% | Missing tag or field | + +### Financial hierarchy context +Where useful, add: +- `x_BillingProfileName` +- `x_InvoiceSectionName` +- `x_CostCenter` +- `x_Project` + +This helps separate business ownership from billing structure. + +## Interpretation guidance +- Prefer the field with the most stable and populated coverage. +- If multiple fields disagree, call that out as a data-quality issue. +- If `Tags['environment']` is populated, use it to separate production from non-production before comparing teams or products. +- If `x_CostCenter` or `x_Project` is populated only for part of the estate, describe that limitation directly. +- Use `finops-hub-database-guide.md` to verify column meaning before making claims about ownership. +- Use `INDEX.md` to decide whether a catalog query is better than a custom query for the scenario. + +## Good analyst behavior +- Tell readers which fields were observed. +- Tell readers which fields were optional or sparsely populated. +- Tell readers to inspect populated tags before standardizing on a showback dimension. +- Keep the analysis centered on `Costs()` and validated query assets. + +## See also +- `queries/finops-hub-database-guide.md` +- `queries/INDEX.md` +- `queries/catalog/costs-enriched-base.kql` +- `queries/catalog/cost-by-financial-hierarchy.kql` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/finops-hubs-deployment.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/finops-hubs-deployment.md new file mode 100644 index 000000000..6ec35855b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/finops-hubs-deployment.md @@ -0,0 +1,323 @@ +--- +name: FinOps Hubs Deployment +description: How to deploy and configure Microsoft FinOps Hubs for cloud cost management and analytics. Covers prerequisites, deployment methods, scope configuration, data backfill, dashboard setup, and troubleshooting. +disable-model-invocation: true +--- + +# FinOps Hubs Deployment + +## Overview + +FinOps hubs provide a scalable platform for cost analytics, insights, and optimization. This skill covers deployment, configuration, and troubleshooting of the FinOps hub infrastructure. + +**Architecture:** +- Storage Account (Data Lake Gen2) - staging area for data ingestion +- Azure Data Factory - data ingestion and cleanup pipelines +- Azure Data Explorer (optional) - scalable datastore for analytics +- Microsoft Fabric RTI (optional) - alternative to Data Explorer +- Key Vault - stores managed identity credentials + +**Estimated Cost:** ~$120/mo + $10/mo per $1M in monitored spend + +--- + +## Prerequisites + +### Required Permissions + +| Task | Required Role | +|------|---------------| +| Deploy template | Contributor + Role Based Access Control Administrator (or Owner) | +| Configure subscription/RG exports | Cost Management Contributor | +| Configure EA billing exports | Enterprise Reader, Department Reader, or Account Owner | +| Configure MCA billing exports | Contributor on billing account/profile/invoice section | +| Configure MPA billing exports | Contributor on billing account/profile/customer | + +### Resource Providers + +Enable these resource providers before deployment: + +**Azure Portal:** +1. Open subscription → Settings → Resource providers +2. Register `Microsoft.EventGrid` +3. Register `Microsoft.CostManagementExports` + +**PowerShell:** +```powershell +Initialize-FinOpsHubDeployment +``` + +--- + +## Deployment Methods + +### Azure Portal + +Deploy using one of these links: +- **Azure Commercial:** https://aka.ms/finops/hubs/deploy +- **Azure Government:** https://aka.ms/finops/hubs/deploy/gov +- **Azure China (MCA only):** https://aka.ms/finops/hubs/deploy/china + +**Key Parameters:** + +| Parameter | Description | Recommendation | +|-----------|-------------|----------------| +| Hub Name | Used for resource naming and Cost Management grouping | Short, descriptive name | +| Data Explorer Name | Cluster name or Fabric eventhouse Query URI | Required for >$100K spend | +| Storage SKU | `Premium_LRS` or `Premium_ZRS` | Default (LRS) for initial deploy | +| Data Explorer SKU | Cluster size | `Dev(No SLA)_Standard_E2a_v4` to start | + +### PowerShell + +```powershell +# Install the module +Install-Module -Name FinOpsToolkit + +# Deploy to Azure Data Explorer +Deploy-FinOpsHub ` + -Name MyHub ` + -ResourceGroupName MyNewResourceGroup ` + -Location westus ` + -DataExplorerName MyFinOpsHubCluster + +# Deploy to Microsoft Fabric +Deploy-FinOpsHub ` + -Name MyHub ` + -ResourceGroupName MyNewResourceGroup ` + -Location westus ` + -DataExplorerName https://abcxyz123789.x0.kusto.fabric.microsoft.com +``` + +### Bicep Module + +```bicep +module finopsHub 'br/public:avm/ptn/finops-toolkit/finops-hub:' = { + params: { + hubName: 'finops-hub' + location: 'westus' + dataExplorerName: 'myhubcluster' + } +} +``` + +--- + +## Configure Scopes (Cost Management Exports) + +### Manual Export Creation + +Create exports for each scope you want to monitor: + +**Required Settings:** +- **Type of data:** `Cost and usage details (FOCUS)` +- **Dataset version:** `1.0` or `1.0r2` +- **Frequency:** `Daily export of month-to-date costs` +- **Container:** `msexports` +- **Format:** `Parquet` with `Snappy` compression +- **File partitioning:** On +- **Overwrite data:** Off (recommended) + +**Directory Path by Scope:** + +| Scope | Directory Path | +|-------|----------------| +| EA Billing Account | `billingAccounts/{enrollment-number}` | +| MCA Billing Profile | `billingProfiles/{billing-profile-id}` | +| Subscription | `subscriptions/{subscription-id}` | +| Resource Group | `subscriptions/{subscription-id}/resourceGroups/{rg-name}` | + +**Supported Datasets:** +- Cost and usage details (FOCUS) - `1.0`, `1.0r2` +- Price sheet - `2023-05-01` (required for missing prices) +- Reservation details - `2023-03-01` +- Reservation recommendations - `2023-05-01` (required for Rate optimization report) +- Reservation transactions - `2023-05-01` + +### Managed Exports + +Allow FinOps hubs to create and maintain exports automatically: + +1. Get Data Factory identity from deployment outputs (`managedIdentityId`) +2. Grant access to each scope: + - EA: Assign enrollment/department reader role + - Subscriptions: Assign Cost Management Contributor +3. Update `settings.json` in storage account `config` container: + +```json +{ + "scopes": [ + { "scope": "/providers/Microsoft.Billing/billingAccounts/1234567" }, + { "scope": "/subscriptions/aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e" } + ] +} +``` + +### PowerShell Export Commands + +```powershell +# Create FOCUS cost export with 12-month backfill +New-FinOpsCostExport -Name 'ftk-FinOpsHub-costs' ` + -Scope "{scope-id}" ` + -Dataset FocusCost ` + -StorageAccountId "{storage-resource-id}" ` + -StorageContainer msexports ` + -StoragePath 'billingAccounts/###' ` + -Backfill 12 ` + -Execute + +# Create price sheet export +New-FinOpsCostExport -Name 'finops-hub-prices' ` + -Scope '/providers/Microsoft.Billing/billingAccounts/###/billingProfiles/###' ` + -Dataset PriceSheet ` + -StorageAccountId $StorageAccountId ` + -StorageContainer msexports ` + -StoragePath 'billingProfiles/###' ` + -Backfill 13 + +# Create reservation recommendations export +New-FinOpsCostExport -Name 'finops-hub-resrecs' ` + -Scope '/providers/Microsoft.Billing/billingAccounts/###/billingProfiles/###' ` + -Dataset ReservationRecommendations ` + -CommitmentDiscountResourceType VirtualMachines ` + -CommitmentDiscountScope Shared ` + -CommitmentDiscountLookback 30 ` + -StorageAccountId $StorageAccountId ` + -StorageContainer msexports ` + -StoragePath 'billingProfiles/###' +``` + +--- + +## Backfill Historical Data + +### Azure Portal +1. Open Cost Management → Exports +2. Select the export +3. Select **Export selected dates** +4. Specify month (one at a time, up to 12 months) + +### PowerShell +```powershell +# Backfill 13 months +Start-FinOpsCostExport ` + -Scope '/providers/Microsoft.Billing/billingAccounts/###/billingProfiles/###' ` + -Name '{export-name}' ` + -Backfill 13 + +# Or specific date range +Start-FinOpsCostExport ` + -Scope '/providers/Microsoft.Billing/billingAccounts/###' ` + -Name '{export-name}' ` + -StartDate '2024-01-01' -EndDate '2024-12-31' +``` + +### Data Factory Pipeline +Run `config_RunBackfillJob` pipeline after exports complete: + +```powershell +Get-AzDataFactoryV2 -ResourceGroupName "{hub-resource-group}" ` +| ForEach-Object { + Invoke-AzDataFactoryV2Pipeline ` + -ResourceGroupName $_.ResourceGroupName ` + -DataFactoryName $_.DataFactoryName ` + -PipelineName 'config_RunBackfillJob' +} +``` + +--- + +## Microsoft Fabric Setup + +For Fabric as primary data store (instead of Data Explorer): + +### Before Deployment + +1. Create workspace and eventhouse in Fabric +2. Create **Ingestion** database +3. Run setup script from [finops-hub-fabric-setup-Ingestion.kql](https://github.com/microsoft/finops-toolkit/releases/latest/download/finops-hub-fabric-setup-Ingestion.kql) +4. Repeat for **Hub** database using [finops-hub-fabric-setup-Hub.kql](https://github.com/microsoft/finops-toolkit/releases/latest/download/finops-hub-fabric-setup-Hub.kql) +5. Copy the **Query URI** for deployment + +### After Deployment + +Grant Data Factory access to databases: + +```kusto +.add database Ingestion admins ('aadapp=') +.add database Hub admins ('aadapp=') +``` + +--- + +## Dashboard and Report Setup + +### Data Explorer Dashboard + +1. Download [finops-hub-dashboard.json](https://github.com/microsoft/finops-toolkit/releases/latest/download/finops-hub-dashboard.json) +2. Grant users **Viewer** access to Hub and Ingestion databases +3. Go to [dataexplorer.azure.com/dashboards](https://dataexplorer.azure.com/dashboards) +4. Import dashboard from file +5. Edit data source to point to your cluster + +### Power BI Reports + +Download reports based on your backend: +- **KQL (Data Explorer/Fabric):** [PowerBI-kql.zip](https://github.com/microsoft/finops-toolkit/releases/latest/download/PowerBI-kql.zip) +- **Storage:** [PowerBI-storage.zip](https://github.com/microsoft/finops-toolkit/releases/latest/download/PowerBI-storage.zip) +- **Demo (sample data):** [PowerBI-demo.zip](https://github.com/microsoft/finops-toolkit/releases/latest/download/PowerBI-demo.zip) + +**Required Parameters:** +- **Cluster URI:** Data Explorer cluster or Fabric eventhouse query URI +- **Storage URL:** DFS endpoint for the storage account + +--- + +## Template Outputs + +After deployment, retrieve these values from **Deployments → hub → Outputs:** + +| Output | Description | +|--------|-------------| +| `storageAccountId` | Resource ID of storage account | +| `storageAccountName` | Storage account name for Power BI | +| `storageUrlForPowerBI` | URL for custom Power BI reports | +| `clusterUri` | Data Explorer cluster URI | +| `managedIdentityId` | Data Factory managed identity (for managed exports) | +| `managedIdentityTenantId` | Tenant ID for managed exports | + +--- + +## Troubleshooting + +### Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| No data after export | Export takes up to 24 hours | Use "Run now" command | +| Missing prices | Price sheet export not configured | Create price sheet export | +| Power BI timeout | >$1M spend without Data Explorer | Redeploy with Data Explorer or Fabric | +| Export not visible | New subscription | Wait 48 hours for Cost Management activation | + +### Data Processing + +- Files land in `msexports` container +- Data Factory processes into `ingestion` container +- Final data available in Data Explorer **Hub** database + +### Verify Connectivity + +```kusto +Costs() | summarize count(), max(ChargePeriodStart) +``` + +--- + +## References + +- [FinOps Hubs Overview](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) +- [Create and Update FinOps Hubs](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/deploy) +- [Configure Scopes](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/configure-scopes) +- [FinOps Hub Template](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/template) +- [Configure Dashboards](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/configure-dashboards) +- [Troubleshooting Guide](https://learn.microsoft.com/cloud-computing/finops/toolkit/help/troubleshooting) +- [FinOps Toolkit PowerShell](https://learn.microsoft.com/cloud-computing/finops/toolkit/powershell/powershell-commands) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/finops-hubs.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/finops-hubs.md new file mode 100644 index 000000000..5e878afd0 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/finops-hubs.md @@ -0,0 +1,130 @@ +--- +name: FinOps Hubs Analysis +description: Domain knowledge for analyzing cloud financial data in [Microsoft FinOps Hubs](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/finops-hubs-overview). It enables cost analysis, anomaly detection, savings optimization, and FinOps Framework-aligned reporting. **Mission**: Transform cloud cost data into actionable business insights through FinOps best practices. **Capabilities**: KQL analysis | Anomaly detection | Multi-cloud support | FinOps Framework guidance **Temporal**: Data refreshes daily | Default 30-day analysis | UTC timezone +--- + +## Query Access + +All KQL queries are located in `references/queries/`: + +| Resource | Path | Purpose | +|----------|------|---------| +| **Index** | `references/queries/INDEX.md` | Query catalog with descriptions and parameters | +| **Catalog** | `references/queries/catalog/*.kql` | Actual query files to execute | +| **Schema** | `references/queries/finops-hub-database-guide.md` | Database schema and column definitions | + +**To execute a query:** +1. Read the .kql file from `references/queries/catalog/[name].kql` +2. Substitute parameters (startDate, endDate, N, etc.) +3. Get environment config from `.ftk/environments.local.md` (cluster-uri, tenant, subscription, resource-group) +4. Execute via `mcp__azure-mcp-server__kusto` command `kusto_query` + +**Required MCP parameters:** +```json +{ + "cluster-uri": "", + "database": "Hub", + "tenant": "", + "query": "" +} +``` + +> **CRITICAL**: Always include `tenant` parameter. Cross-tenant (B2B) scenarios fail with "Unauthorized" if tenant is omitted. + +--- + +## Critical Constraints + +> **KUSTO (KQL), NOT SQL** +> +> FinOps Hubs uses **Azure Data Explorer (Kusto)**, not SQL Server. Use **KQL syntax only**. +> +> | KQL | SQL | +> |-----|-----| +> | `| where Column == "value"` | `WHERE Column = 'value'` | +> | `| summarize count() by Column` | `SELECT COUNT(*) GROUP BY Column` | +> | `| project Column1, Column2` | `SELECT Column1, Column2` | +> | `| take 10` | `TOP 10` or `LIMIT 10` | +> +> **NEVER use SQL syntax.** Queries will fail. + +**Database Rules:** +- Always use "Hub" database, NEVER "Ingestion" +- Function-based access: `Costs()`, `Prices()`, `Recommendations()`, `Transactions()` + +--- + +## Query Catalog Summary + +> **Tip:** Read `references/queries/INDEX.md` for the full catalog. Prefer a scenario-specific aggregate query first; use `costs-enriched-base.kql` when scoped row-level enrichment is required. + +| FinOps Task | Query File | Key Parameters | +|-------------|------------|----------------| +| Foundation for scoped custom drill-downs | `costs-enriched-base.kql` | `startDate`, `endDate` | +| Monthly cost trends | `monthly-cost-trend.kql` | `startDate`, `endDate` | +| Top resource groups | `top-resource-groups-by-cost.kql` | `N`, `startDate`, `endDate` | +| Top services | `top-services-by-cost.kql` | `N`, `startDate`, `endDate` | +| Anomaly detection | `cost-anomaly-detection.kql` | `numberOfMonths`, `interval` | +| Commitment utilization | `commitment-discount-utilization.kql` | `startDate`, `endDate` | +| Savings summary (ESR) | `savings-summary-report.kql` | `startDate`, `endDate` | +| Cost forecasting | `cost-forecasting-model.kql` | `forecastPeriods`, `interval` | +| AI workload unit economics | `ai-token-usage-breakdown.kql`, `ai-model-cost-comparison.kql`, `ai-daily-trend.kql`, `ai-cost-by-application.kql` | `startDate`, `endDate` | +| Reservation recommendations | `reservation-recommendation-breakdown.kql` | Filter by service/region | + +**Catalog Protocol:** +1. ALWAYS check the catalog FIRST before writing custom queries +2. Read the actual .kql file to get the exact query +3. Adapt only the parameters, never recreate enrichment logic +4. The `x_*` columns are pre-calculated - use them directly + +--- + +## Tool Matrix + +| Scenario | Primary Tool | Fallback | +|----------|--------------|----------| +| Cost queries | Query Catalog → `mcp__azure-mcp-server__kusto` | Manual query | +| Azure docs | `microsoft-docs:microsoft-docs` | Web search | +| Code reference | `microsoft-docs:microsoft-code-reference` | Web search | +| Resources | Azure Resource Graph via `mcp__azure-mcp-server__kusto` | Azure CLI | + +--- + +## Performance Rules + +1. **Default**: 30 days (`ago(30d)`) +2. **Max**: 90 days without approval +3. **Freshness check**: `Costs() | where ChargePeriodStart >= ago(7d) | summarize max(ChargePeriodStart)` + +**Bad**: `Costs() | summarize sum(BilledCost)` +**Good**: `Costs() | where ChargePeriodStart >= ago(30d) | summarize sum(BilledCost)` + +--- + +## FinOps Framework Alignment + +**Understand & Cost**: Allocation (tags) | Anomalies | Ingestion | Analytics +**Optimize**: Reservations | Right-sizing | Scheduling +**Quantify Value**: Unit economics | Budgets | Forecasts +**Manage Practice**: Governance | Onboarding | Education + +--- + +## Quality Checklist + +- [ ] Query Catalog checked FIRST +- [ ] Time filters on ALL queries +- [ ] Query shown to user before execution +- [ ] Results validated for completeness +- [ ] Confidence level stated +- [ ] Recommendations are actionable +- [ ] Impact quantified in dollars + +--- + +## References + +- [FinOps Framework (FinOps Foundation)](https://www.finops.org/framework/) +- [FinOps Hubs Overview](https://learn.microsoft.com/cloud-computing/finops/toolkit/hubs/finops-hubs-overview) +- [KQL Documentation](https://learn.microsoft.com/azure/data-explorer/kusto/query/) +- [FinOps Foundation](https://www.finops.org/framework/) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/queries b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/queries new file mode 120000 index 000000000..34c826fa3 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/queries @@ -0,0 +1 @@ +../../../../../../../../queries \ No newline at end of file diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/service-cost-deep-dive.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/service-cost-deep-dive.md new file mode 100644 index 000000000..b4edc2c24 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/service-cost-deep-dive.md @@ -0,0 +1,374 @@ +--- +name: service-cost-deep-dive +description: Performs a detailed deep dive into a specific service using FinOps hubs cost, price, and recommendation data. +author: FinOps Toolkit Team +version: 1.1.0 +license: Apache-2.0 +--- + +# Service cost deep dive + +## Purpose + +Use this reference to investigate one service in depth, explain what is driving cost, and identify practical optimization actions in a FinOps Toolkit / FinOps hubs environment. + +This deep dive keeps the original intent of service-level analysis, but uses validated FinOps hubs patterns: + +- `Costs()` as the primary analytical surface +- `Prices()` when unit-price validation is relevant +- `Recommendations()` when optimization guidance is relevant +- FinOps hubs fields such as `ServiceName`, `ResourceName`, `ResourceType`, `RegionName`, `SubAccountName`, `x_ResourceGroupName`, `x_UsageType`, and `Tags[...]` + +## Grounding and prerequisites + +Before starting: + +1. Review the schema guidance in [finops-hub-database-guide.md](./queries/finops-hub-database-guide.md). +2. Review the query catalog in [INDEX.md](./queries/INDEX.md). +3. Start from [costs-enriched-base.kql](./queries/catalog/costs-enriched-base.kql) for any custom drilldown that needs enriched fields. +4. Use [top-services-by-cost.kql](./queries/catalog/top-services-by-cost.kql) to confirm the exact `ServiceName` values in scope. +5. Use [service-price-benchmarking.kql](./queries/catalog/service-price-benchmarking.kql) when you need price and realized-savings context. + +Inspect which populated fields, tags, and columns are available in your hub before assuming business context exists. +Treat `x_CostCenter`, `x_Project`, `Tags["environment"]`, and `Tags['team']` as observed or optional fields, not guaranteed schema requirements. +If available, use those fields for attribution. If populated, use them to explain ownership. When populated, use them carefully and call out blanks honestly. + +## When to use + +- “Analyze my Azure SQL costs” +- “Deep dive into Virtual Machines spending” +- “Why is Storage getting more expensive?” +- “Break down this service by subscription, region, and resource group” +- “Show the specific resources behind a service spike” +- “Benchmark the service’s price and savings profile” +- “Find optimization opportunities for one service” + +## Workflow + +### Step 1: Identify the exact service + +Use the catalog query first if the service label is uncertain. + +```kusto +// Start with the catalog pattern from top-services-by-cost.kql +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by ServiceName +| top 20 by EffectiveCost desc +``` + +Pick the exact `ServiceName` that matches the user’s request before drilling deeper. + +### Step 2: Establish the service baseline + +Use `Costs()` as the primary surface for total cost and trend. + +```kusto +let targetService = 'Virtual Machines'; +let startDate = startofmonth(ago(90d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| where ServiceName == targetService +| summarize TotalEffectiveCost = sum(EffectiveCost), + TotalBilledCost = sum(BilledCost), + TotalListCost = sum(ListCost), + TotalSavings = sum(x_TotalSavings) +``` + +```kusto +let targetService = 'Virtual Machines'; +let startDate = startofmonth(ago(90d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| where ServiceName == targetService +| summarize EffectiveCost = sum(EffectiveCost) by Day = startofday(ChargePeriodStart) +| order by Day asc +``` + +Calculate: + +- Total service cost +- Daily or monthly trend +- Share of total spend +- Realized savings already present in `x_TotalSavings` + +### Step 3: Break down the service by operational dimensions + +Start with the enriched base pattern from `costs-enriched-base.kql` when you need more context. + +**By subscription/account** + +```kusto +let targetService = 'Virtual Machines'; +Costs() +| where ServiceName == targetService +| summarize EffectiveCost = sum(EffectiveCost) by SubAccountName +| order by EffectiveCost desc +``` + +**By region** + +```kusto +let targetService = 'Virtual Machines'; +Costs() +| where ServiceName == targetService +| summarize EffectiveCost = sum(EffectiveCost) by RegionName +| order by EffectiveCost desc +``` + +**By resource group** + +```kusto +let targetService = 'Virtual Machines'; +Costs() +| where ServiceName == targetService +| summarize EffectiveCost = sum(EffectiveCost) by x_ResourceGroupName +| order by EffectiveCost desc +``` + +**By resource and resource type** + +```kusto +let targetService = 'Virtual Machines'; +Costs() +| where ServiceName == targetService +| summarize EffectiveCost = sum(EffectiveCost) by ResourceName, ResourceType +| top 50 by EffectiveCost desc +``` + +**By usage pattern** + +```kusto +let targetService = 'Virtual Machines'; +Costs() +| where ServiceName == targetService +| summarize EffectiveCost = sum(EffectiveCost) by x_UsageType +| order by EffectiveCost desc +``` + +Use these breakdowns to answer: + +- Which `SubAccountName` owns the cost? +- Which `RegionName` is driving spend? +- Which `x_ResourceGroupName` clusters the spend? +- Which `ResourceName` and `ResourceType` are the biggest contributors? +- Which `x_UsageType` explains the charge pattern? + +### Step 4: Attribute cost with business context + +Business fields are often optional or only partially populated. + +Inspect which populated fields or tags are present before choosing your allocation lens. + +```kusto +let targetService = 'Virtual Machines'; +Costs() +| where ServiceName == targetService +| project x_CostCenter, x_Project, Tags +| take 20 +``` + +If populated, use business context like this: + +```kusto +let targetService = 'Virtual Machines'; +Costs() +| where ServiceName == targetService +| extend Environment = tostring(Tags["environment"]), + Team = tostring(Tags['team']) +| summarize EffectiveCost = sum(EffectiveCost) + by Environment, Team, x_CostCenter, x_Project +| order by EffectiveCost desc +``` + +Call out blank or missing values explicitly. Untagged or unattributed cost is usually a governance finding, not just a reporting inconvenience. + +### Step 5: Investigate the service spike or trend change + +When the service changed recently, compare periods and isolate the dimensions that moved. + +```kusto +let targetService = 'Virtual Machines'; +let recentStart = startofmonth(ago(30d)); +let priorStart = startofmonth(ago(60d)); +let recent = + Costs() + | where ChargePeriodStart >= recentStart and ChargePeriodStart < now() + | where ServiceName == targetService + | summarize RecentCost = sum(EffectiveCost) by RegionName, x_ResourceGroupName, ResourceType; +let prior = + Costs() + | where ChargePeriodStart >= priorStart and ChargePeriodStart < recentStart + | where ServiceName == targetService + | summarize PriorCost = sum(EffectiveCost) by RegionName, x_ResourceGroupName, ResourceType; +recent +| join kind=fullouter prior on RegionName, x_ResourceGroupName, ResourceType +| extend RecentCost = coalesce(RecentCost, 0.0), + PriorCost = coalesce(PriorCost, 0.0), + Delta = RecentCost - PriorCost +| order by Delta desc +``` + +This highlights whether the change is driven by region expansion, new resource groups, or a different `ResourceType` mix. + +### Step 6: Validate unit-price and savings posture + +Use the service-price-benchmarking pattern first. If you need lower-level unit-price validation, use `Prices()` against the SKU identifiers surfaced from `Costs()`. + +```kusto +let targetService = 'Virtual Machines'; +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +let targetPrices = + Costs() + | where ChargePeriodStart >= startDate and ChargePeriodStart < endDate + | where ServiceName == targetService + | summarize by SkuPriceId; +Prices() +| where SkuPriceId in (targetPrices) +| project SkuPriceId, + ListUnitPrice, + ContractedUnitPrice, + x_EffectiveUnitPrice, + PricingUnit, + x_SkuMeterName, + x_SkuRegion, + x_SkuTerm +``` + +Use this to explain: + +- Whether unit prices look reasonable +- Whether negotiated or commitment discounts are already helping +- Whether price changes or SKU mix shifts are contributing to the increase + +### Step 7: Pull optimization recommendations when relevant + +Use `Recommendations()` when the service deep dive should end with a concrete action list. + +```kusto +let targetService = 'Virtual Machines'; +Recommendations() +| where x_SourceProvider == 'Microsoft' +| extend ServiceName = tostring(x_RecommendationDetails.ServiceName), + RegionName = tostring(x_RecommendationDetails.RegionName), + ResourceType = tostring(x_RecommendationDetails.ResourceType), + ResourceName = tostring(x_RecommendationDetails.ResourceName) +| where ServiceName =~ targetService or ResourceType has targetService +| project x_RecommendationDate, + ServiceName, + RegionName, + ResourceType, + ResourceName, + x_EffectiveCostBefore, + x_EffectiveCostAfter, + x_EffectiveCostSavings +| order by x_EffectiveCostSavings desc +``` + +Recommendation payload details can be optional. If available, use them to connect the service-level story to a short list of remediations. + +## Service-specific interpretation guidance + +### Compute services + +Focus on: + +- `ResourceType` mix +- `x_UsageType` patterns +- rightsizing candidates +- commitment discount coverage +- stop/deallocate opportunities + +Useful signals: + +- high spend concentrated in a few `ResourceName` values +- one `RegionName` carrying disproportionate cost +- expensive usage concentrated in one `x_ResourceGroupName` + +### Storage services + +Focus on: + +- growth by `ResourceName` +- region duplication +- hot vs. cooler usage patterns when visible in `x_UsageType` +- price posture using `Prices()` for the affected SKUs + +### Database services + +Focus on: + +- steady-state growth +- non-production cost hiding in the wrong `Tags["environment"]` +- expensive editions or resource families by `ResourceType` +- savings opportunities surfaced in `Recommendations()` + +### Network and transfer-heavy services + +Focus on: + +- `RegionName` concentration +- abrupt period-over-period movement +- resource-group concentration via `x_ResourceGroupName` +- whether the cost is spread broadly or driven by a few `ResourceName` entries + +## Output format + +Use this structure in the final response. + +### 1. Executive summary + +- Service analyzed: exact `ServiceName` +- Total cost for period +- Trend direction and magnitude +- Biggest cost driver +- Biggest optimization opportunity + +### 2. Cost breakdown + +- Top `SubAccountName` values +- Top `RegionName` values +- Top `x_ResourceGroupName` values +- Top `ResourceName` / `ResourceType` pairs +- Top `x_UsageType` values if available + +### 3. Business attribution + +- `x_CostCenter` and `x_Project` if populated +- `Tags["environment"]` and `Tags['team']` if populated +- clear note for unattributed cost + +### 4. Pricing and savings + +- observed list vs. contracted vs. effective posture +- realized savings from the service-price-benchmarking pattern +- whether unit-price review via `Prices()` changed the interpretation + +### 5. Recommendations + +- recommendation-backed actions from `Recommendations()` where available +- service-specific quick wins +- governance follow-ups for blank tags or weak attribution + +## Best practices + +1. Use `Costs()` first; only branch into `Prices()` or `Recommendations()` when the question requires it. +2. Confirm the exact `ServiceName` instead of guessing from a friendly service label. +3. Start broad, then narrow to `RegionName`, `SubAccountName`, `x_ResourceGroupName`, `ResourceType`, and `ResourceName`. +4. Inspect which populated fields and tags are available before promising allocation detail. +5. Treat business context as observed data, not guaranteed schema. +6. Use `costs-enriched-base.kql` when you need a reusable enriched baseline. +7. Use `top-services-by-cost.kql` for service discovery and `service-price-benchmarking.kql` for savings context. + +## See also + +- [finops-hub-database-guide.md](./queries/finops-hub-database-guide.md) +- [INDEX.md](./queries/INDEX.md) +- [costs-enriched-base.kql](./queries/catalog/costs-enriched-base.kql) +- [top-services-by-cost.kql](./queries/catalog/top-services-by-cost.kql) +- [service-price-benchmarking.kql](./queries/catalog/service-price-benchmarking.kql) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/settings-format.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/settings-format.md new file mode 100644 index 000000000..670d80e4b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/settings-format.md @@ -0,0 +1,71 @@ +# FinOps hubs environment settings + +FinOps hubs environment settings are stored in `.ftk/environments.local.md` at the project root. This file is agent-agnostic and supports multiple named environments. + +## File format + +The settings file uses YAML frontmatter with an optional markdown body for notes: + +```markdown +--- +default: dev-hub +environments: + dev-hub: + cluster-uri: https://myhubdev.eastus.kusto.windows.net + tenant: 00000000-0000-0000-0000-000000000000 + subscription: my-dev-subscription + resource-group: rg-finops-dev + prod-hub: + cluster-uri: https://myhubprod.westus2.kusto.windows.net + tenant: 00000000-0000-0000-0000-000000000000 + subscription: my-prod-subscription + resource-group: rg-finops-prod +--- + +# Environment notes + +Optional notes about your FinOps hub environments. +``` + +## Required fields + +| Field | Required | Description | +|-------|----------|-------------| +| `default` | Yes | Name of the default environment to use | +| `environments` | Yes | Map of named environments | +| `cluster-uri` | Yes | Full Azure Data Explorer cluster URI | +| `tenant` | Yes | Azure AD tenant ID (required for B2B/cross-tenant) | +| `subscription` | No | Azure subscription name or ID | +| `resource-group` | No | Resource group containing the hub | + +## Reading settings + +To read settings from `.ftk/environments.local.md`: + +1. Read the file at `.ftk/environments.local.md` relative to the project root +2. Parse the YAML frontmatter between the `---` delimiters +3. Use the `default` field to select the active environment unless the user specifies one +4. Extract `cluster-uri`, `tenant`, and other fields from the selected environment + +## Writing settings + +The `/ftk-hubs-connect` command discovers FinOps hub instances and writes their configuration to this file. When writing: + +1. Read the existing file if it exists to preserve other environments +2. Add or update the environment entry with the discovered values +3. Set `default` to the newly connected environment if no default exists + +## Using settings with MCP Kusto server + +After reading the active environment, pass the values to the MCP Kusto server: + +```json +{ + "cluster-uri": "", + "database": "Hub", + "tenant": "", + "query": "" +} +``` + +Always include the `tenant` parameter. Cross-tenant (B2B) scenarios fail with "Unauthorized" if tenant is omitted. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/tag-coverage-analysis.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/tag-coverage-analysis.md new file mode 100644 index 000000000..1852f098b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/tag-coverage-analysis.md @@ -0,0 +1,219 @@ +--- +name: tag-coverage-analysis +description: Analyze tag coverage and unattributed cost in FinOps hubs using validated cost fields, observed tags, and business allocation metadata. +author: Microsoft +version: 1.1.0 +license: Apache-2.0 +--- + +# Tag coverage analysis + +## Purpose + +Use this reference to evaluate how well cost records can be attributed through tags and business fields in FinOps hubs. + +Keep the original tag coverage intent, but ground the analysis in validated FinOps hubs patterns: + +- `Costs()` as the primary analytical surface +- `Tags[...]` for tag inspection and tag-based attribution +- business fields such as `x_CostCenter`, `x_Project`, `ServiceName`, `SubAccountName`, and `x_ResourceGroupName` +- query and schema assets including `costs-enriched-base.kql`, `cost-by-financial-hierarchy.kql`, `finops-hub-database-guide.md`, and `INDEX.md` + +Treat tags and business fields as observed and optional. Do not assume every hub populates every tag, enrichment column, or ownership field. + +## Grounding and prerequisites + +Before starting: + +1. Review [finops-hub-database-guide.md](./queries/finops-hub-database-guide.md) for schema details. +2. Review [INDEX.md](./queries/INDEX.md) to confirm whether a catalog query already fits the scenario. +3. Start from [costs-enriched-base.kql](./queries/catalog/costs-enriched-base.kql) when you need to inspect raw enriched records. +4. Use [cost-by-financial-hierarchy.kql](./queries/catalog/cost-by-financial-hierarchy.kql) when tag coverage needs to be compared with financial hierarchy fields. + +Inspect which populated tags, fields, and columns are actually present in the reporting window before calculating coverage. + +## When to use + +- “What is our tag coverage?” +- “How much cost is unattributed because tags are blank?” +- “Which subscriptions or resource groups have weak tag hygiene?” +- “Which services are missing cost allocation tags?” +- “Can we support showback or chargeback with our current tagging?” +- “Which business fields are populated enough to trust?” + +## Recommended workflow + +### Step 1: Inspect observed tags and business fields + +Start by reviewing a small sample from `Costs()`. + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| project ServiceName, SubAccountName, x_ResourceGroupName, x_CostCenter, x_Project, Tags +| take 50 +``` + +Inspect which populated tags and fields are present. Review which available keys inside `Tags` are actually populated, and note whether `x_CostCenter` or `x_Project` is present often enough to support allocation. + +Useful candidate tags often include: + +- `Tags['environment']` +- `Tags['team']` +- `Tags['application']` +- `Tags['owner']` +- `Tags['costCenter']` +- `Tags['project']` + +If available, compare those tags with `x_CostCenter` and `x_Project`. When populated, those business fields may be more stable than raw tags for financial reporting. + +### Step 2: Establish the total cost baseline + +Measure total effective cost for the reporting period before calculating any coverage percentage. + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| summarize TotalCost = sum(EffectiveCost) +``` + +This gives the denominator for all coverage calculations. + +### Step 3: Measure single-tag coverage honestly + +Choose one observed tag and calculate how much cost has a non-empty value. + +```kusto +let base = + Costs() + | where ChargePeriodStart >= startofmonth(ago(30d)) + | extend Environment = tostring(Tags['environment']); +base +| summarize + TotalCost = sum(EffectiveCost), + TaggedCost = sumif(EffectiveCost, isnotempty(Environment)) +| extend UntaggedCost = TotalCost - TaggedCost, + TagCoverage = TaggedCost / TotalCost +``` + +Repeat the same pattern for any observed tag or field that matters to the business, such as `Tags['team']`, `Tags['application']`, `x_CostCenter`, or `x_Project`. + +If populated coverage is weak, say so directly. Blank values are an important finding. + +### Step 4: Break down weak coverage by business context + +Once you find a weak tag, determine where the unattributed cost lives. + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| extend Environment = tostring(Tags['environment']) +| summarize TotalCost = sum(EffectiveCost), + UntaggedCost = sumif(EffectiveCost, isempty(Environment)) + by SubAccountName, ServiceName, x_ResourceGroupName +| order by UntaggedCost desc +``` + +This highlights which `SubAccountName`, `ServiceName`, or `x_ResourceGroupName` values have the largest unattributed cost. + +### Step 5: Compare tag coverage with financial hierarchy fields + +If available, compare tag-based attribution with business hierarchy fields. + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| extend Team = tostring(Tags['team']), + Application = tostring(Tags['application']) +| summarize EffectiveCost = sum(EffectiveCost) + by x_CostCenter, x_Project, Team, Application, SubAccountName +| order by EffectiveCost desc +``` + +Use `cost-by-financial-hierarchy.kql` as the validated pattern when you need a fuller billing hierarchy view. This is especially useful when tags are only partially populated but financial ownership fields are more reliable. + +### Step 6: Check service-level tag hygiene + +Some services may have weaker tagging than others. + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| extend ProjectTag = tostring(Tags['project']) +| summarize TotalCost = sum(EffectiveCost), + TaggedCost = sumif(EffectiveCost, isnotempty(ProjectTag)) + by ServiceName +| extend Coverage = TaggedCost / TotalCost +| order by Coverage asc, TotalCost desc +``` + +Use this to identify high-cost services with poor tag coverage first. + +### Step 7: Check account and resource-group concentration + +Weak tagging is often concentrated in a few places. + +```kusto +Costs() +| where ChargePeriodStart >= startofmonth(ago(30d)) +| extend CostCenter = coalesce(x_CostCenter, tostring(Tags['costCenter'])) +| summarize TotalCost = sum(EffectiveCost), + UnallocatedCost = sumif(EffectiveCost, isempty(CostCenter)) + by SubAccountName, x_ResourceGroupName +| order by UnallocatedCost desc +``` + +This helps prioritize cleanup by subscription and resource group instead of treating tag coverage as one flat percentage. + +## Output guidance + +### 1. Executive summary + +- Reporting window +- Total effective cost analyzed +- Best observed attribution field or tag +- Weakest important tag or field +- Total unattributed cost +- Highest-priority cleanup target + +### 2. Coverage scorecard + +| Field or tag | Coverage % | Tagged cost | Untagged cost | Notes | +|---|---:|---:|---:|---| +| `Tags['environment']` | XX% | $X | $Y | Observed, partially populated | +| `Tags['team']` | XX% | $X | $Y | Optional in some subscriptions | +| `x_CostCenter` | XX% | $X | $Y | Better for finance if populated | +| `x_Project` | XX% | $X | $Y | Sparse in shared platforms | + +### 3. Untagged or unattributed cost breakdown + +Report where blank values concentrate: + +- by `SubAccountName` +- by `ServiceName` +- by `x_ResourceGroupName` +- by `x_CostCenter` or `x_Project` when populated + +### 4. Interpretation notes + +- Call out which tags were observed. +- Call out which fields were optional. +- State whether business conclusions rely on tags, enrichment fields, or both. +- Tell readers to inspect populated tags and fields before standardizing on one allocation key. + +## Analyst guidance + +1. Use `Costs()` first; this is the primary surface for coverage analysis. +2. Inspect populated tags and available fields before choosing the dimensions to report. +3. Treat blank tags as governance evidence, not just missing formatting. +4. Prefer the most consistently populated field, even if it is `x_CostCenter` or `x_Project` instead of a tag. +5. Use `costs-enriched-base.kql` when you need to validate what the hub actually contains. +6. Use `cost-by-financial-hierarchy.kql` when you need to compare tags with billing ownership structure. +7. Use `finops-hub-database-guide.md` and `INDEX.md` before inventing custom assumptions about schema. + +## See also + +- [finops-hub-database-guide.md](./queries/finops-hub-database-guide.md) +- [INDEX.md](./queries/INDEX.md) +- [costs-enriched-base.kql](./queries/catalog/costs-enriched-base.kql) +- [cost-by-financial-hierarchy.kql](./queries/catalog/cost-by-financial-hierarchy.kql) diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/top-cost-drivers.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/top-cost-drivers.md new file mode 100644 index 000000000..eef93808d --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/top-cost-drivers.md @@ -0,0 +1,183 @@ +--- +name: top-cost-drivers +description: Identifies and ranks the biggest contributors to FinOps hubs cost across services, subscriptions, regions, resource groups, billing hierarchy, and optional tags to help prioritize optimization work +author: FinOps Toolkit Team +version: 1.0.0 +license: Apache-2.0 +--- + +# Top cost drivers + +## Purpose +Use FinOps hubs data to identify the largest contributors to cost, measure how concentrated spend is, and prioritize the next optimization or allocation investigation. + +## When to use +- "What are my biggest costs?" +- "Where is most of my spend going?" +- "What should I optimize first?" +- "Show me top spending by service, subscription, region, or resource group" +- Monthly business reviews, budget planning, and allocation reviews +- Keywords: top, biggest, largest, highest, cost drivers, most expensive, concentration, ranking + +## Prerequisites +- Confirm the hub connection and reporting window before starting. +- Use `references/queries/finops-hub-database-guide.md` to verify available FinOps hubs fields and enrichment columns. +- Use `references/queries/INDEX.md` to select the closest aggregate ranking query. Use `costs-enriched-base.kql` only for a scoped row-level drill-down after the aggregate ranking is known. + +## Recommended ranking dimensions +Choose the first grouping that best matches the question: + +- `ServiceName` for top Azure service cost drivers +- `SubAccountName` for top subscriptions or sub-accounts +- `RegionName` for regional rollups +- `x_ResourceGroupName` for resource-group ranking +- `x_BillingProfileName` and `x_InvoiceSectionName` for billing hierarchy and allocation views + +## Analysis workflow + +### Step 1: Scope the request +Confirm: +- analysis period +- filters already in scope +- whether the user wants technical drivers, billing hierarchy drivers, or both +- whether optional business tags are trustworthy enough to use + +### Step 2: Start with the highest-signal primary dimension + +**Top services** +- Use `top-services-by-cost.kql` when the question is service-led. +- This is the fastest way to rank cost by `ServiceName`. + +```kusto +let N = 10; +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by ServiceName +| top N by EffectiveCost desc +``` + +**Top regions** +- Use `cost-by-region-trend.kql` for regional rollup and trend context. +- This groups cost by `RegionName` and helps surface regional concentration. + +```kusto +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by RegionName +| order by EffectiveCost desc +``` + +**Top resource groups** +- Use `top-resource-groups-by-cost.kql` when ranking by `x_ResourceGroupName`. +- If the catalog query is close but not exact, adapt the aggregate pattern before falling back to row-level samples. + +```kusto +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) by x_ResourceGroupName +| top 20 by EffectiveCost desc +``` + +**Top billing hierarchy nodes** +- Use `cost-by-financial-hierarchy.kql` when you need allocation or showback context. +- Focus on `x_BillingProfileName`, `x_InvoiceSectionName`, and `SubAccountName` before adding any lower-level dimensions. + +```kusto +let startDate = startofmonth(ago(30d)); +let endDate = startofmonth(now()); +Costs() +| where ChargePeriodStart >= startDate and ChargePeriodStart < endDate +| summarize EffectiveCost = sum(EffectiveCost) + by x_BillingProfileName, x_InvoiceSectionName, SubAccountName +| top 20 by EffectiveCost desc +``` + +### Step 3: Add a multidimensional breakdown +After the primary ranking, add one more dimension to explain the driver: + +- `ServiceName` by `SubAccountName` +- `ServiceName` by `RegionName` +- `x_ResourceGroupName` within a top `ServiceName` +- `x_BillingProfileName` → `x_InvoiceSectionName` → `SubAccountName` + +Use `costs-enriched-base.kql` when you need to pivot the same filtered dataset multiple ways. + +### Step 4: Add optional tag-based grouping only when it helps +Tag-based grouping is optional. If your organization uses tags and coverage is good enough, add `Tags['team']`, `Tags['product']`, `Tags['application']`, or `Tags['environment']` as an overlay after the non-tag ranking is understood. + +If tags are missing, blank, or tag coverage is incomplete, fall back to `ServiceName`, `SubAccountName`, `RegionName`, `x_ResourceGroupName`, `x_BillingProfileName`, or `x_InvoiceSectionName` instead. + +Treat blank tag values as incomplete metadata, not as a reliable business grouping. + +### Step 5: Quantify concentration +For every ranked output: +1. calculate total spend for the filtered scope +2. calculate each row's percentage of total +3. calculate cumulative percentage +4. identify the smallest set of rows that explains roughly 80 percent of spend + +### Step 6: Add trend context when needed +- Re-run the top driver view over daily or monthly grain when the user asks what changed. +- `cost-by-region-trend.kql` is useful when regional growth or migration is part of the story. +- For custom trend work, start from `costs-enriched-base.kql` and keep the same driver dimensions so comparisons stay consistent. + +## Output format + +### 1. Executive summary +- total spend for the period +- scope analyzed +- top 3 drivers in one sentence +- immediate optimization or allocation takeaway + +### 2. Ranked cost drivers + +| Rank | Dimension | Cost | % of total | Cumulative % | +|------|-----------|------|------------|--------------| +| 1 | Value 1 | $X,XXX | XX% | XX% | +| 2 | Value 2 | $X,XXX | XX% | XX% | +| 3 | Value 3 | $X,XXX | XX% | XX% | + +### 3. Drill-down explanation +For each top driver, explain the most useful secondary split, such as: +- `ServiceName` by `SubAccountName` +- `ServiceName` by `RegionName` +- `x_ResourceGroupName` for the most expensive subscription +- `x_BillingProfileName` and `x_InvoiceSectionName` for chargeback conversations + +### 4. Concentration summary +- Top N rows represent X percent of spend +- Remaining long tail represents Y percent +- Recommendation on where to focus first + +### 5. Optional tag view +If tag quality is usable, summarize what `Tags['team']`, `Tags['product']`, `Tags['application']`, or `Tags['environment']` adds to the analysis. + +If tags are missing or incomplete, explicitly say the fallback ranking used `ServiceName`, `SubAccountName`, `RegionName`, `x_ResourceGroupName`, `x_BillingProfileName`, or `x_InvoiceSectionName` instead. + +### 6. Optimization priorities +Recommend: +1. highest-cost services or regions for immediate review +2. subscriptions or resource groups that need deeper investigation +3. billing hierarchy areas that need ownership clarification +4. metadata cleanup when optional tags are incomplete + +## Best practices +1. Start with one strong non-tag dimension before adding more detail. +2. Prefer `ServiceName`, `RegionName`, `SubAccountName`, and `x_ResourceGroupName` when tags are unreliable. +3. Use `x_BillingProfileName` and `x_InvoiceSectionName` for finance-facing allocation views. +4. Keep the ranking dimension stable when adding trend context. +5. Always report both dollars and percentages so concentration is obvious. + +## See also +- `references/queries/INDEX.md` +- `references/queries/finops-hub-database-guide.md` +- `costs-enriched-base.kql` +- `top-services-by-cost.kql` +- `cost-by-region-trend.kql` +- `cost-by-financial-hierarchy.kql` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/understand-finops-hub-context.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/understand-finops-hub-context.md new file mode 100644 index 000000000..ad08b9e3f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/understand-finops-hub-context.md @@ -0,0 +1,138 @@ +--- +name: understand-finops-hub-context +description: Mandatory foundational reference for establishing FinOps hub context before analysis +author: Microsoft +version: 1.0.0 +license: Apache-2.0 +--- + +# Understand FinOps hub context + +## Purpose + +This is the mandatory foundational step before any cost analysis. Start by confirming the active FinOps hub, validating the cluster URI, and establishing the available hub context before moving into deeper analysis. + +Use this reference once at the beginning of an analysis session, then reuse that context for the rest of the conversation instead of repeating the same setup work. + +## Required grounding references + +- `references/workflows/ftk-hubs-connect.md` +- `references/workflows/ftk-hubs-healthCheck.md` +- `references/queries/finops-hub-database-guide.md` +- `references/queries/INDEX.md` + +## When to use + +- **Mandatory** at the start of any FinOps analysis conversation or workflow +- Before using deeper query-catalog analysis patterns +- When switching to a different hub or cluster URI +- When the current session does not yet have confirmed hub context + +## What this step establishes + +This step confirms the Hub is reachable and identifies what context is actually observable from the FinOps hubs dataset. Ground your work in Azure Data Explorer, KQL, and the query catalog rather than assumptions. + +### Core data surfaces + +Use the documented functions from `references/queries/finops-hub-database-guide.md`: + +- `Costs()` +- `Prices()` +- `Recommendations()` +- `Transactions()` + +### Minimum validation query + +Use a simple KQL summary to understand data coverage before analysis: + +```kusto +Costs() +| summarize Rows=count(), MinCharge=min(ChargePeriodStart), MaxCharge=max(ChargePeriodStart), Services=dcount(ServiceName), ResourceGroups=dcount(x_ResourceGroupName) +``` + +This gives a quick view of row volume, date range, service diversity, and resource-group coverage. + +### Tag-key discovery + +Discover which tags are present instead of assuming a business taxonomy: + +```kusto +Costs() +| mv-expand TagKey = bag_keys(Tags) +| summarize by TagKey +| order by TagKey asc +``` + +Observed keys may include values such as `org`, `env`, `Project`, or `CostCenter`, but business tags are not guaranteed to exist consistently. + +## How to use this reference + +### Step 1: Connect to the correct Hub + +Follow `references/workflows/ftk-hubs-connect.md` to identify or confirm the active FinOps hub and cluster URI. + +If a cluster URI is already established for the session, reuse it. If not, connect first before running analysis queries. + +### Step 2: Validate the Hub and data freshness + +Use `references/workflows/ftk-hubs-healthCheck.md` after connection if you need to confirm version guidance or investigate stale data. + +### Step 3: Establish baseline analytical context + +Use the schema guide in `references/queries/finops-hub-database-guide.md` and the query catalog in `references/queries/INDEX.md` to choose the right starting point. + +For most custom exploration, begin with a small validation query, then expand into a catalog query or a focused KQL investigation. + +### Step 4: Record only observed business context + +Summarize what the Hub actually shows. Keep the summary factual and bounded to observable cost, pricing, recommendation, transaction, enrichment, and tag evidence. + +## Honest interpretation rules + +- Optional team tags may be missing, so treat them as observed only when present. +- Optional product tags may be missing or blank in many datasets. +- Optional application tags may be missing; if present, validate the actual tag key and coverage before using them. +- Optional environment tags are not guaranteed and may appear as `env` instead of a full `environment` key. + +Validation shorthand: `\bteam\b` is optional, `\bproduct\b` is optional, `\bapplication\b` is optional, and `\benvironment\b` is not guaranteed. + +Do not treat business tags as universal requirements across all records. + +Do not infer budget ownership, stakeholder mappings, recharge policy details, or future business milestones from Hub data alone. Those require separate business or governance inputs outside this foundational step. + +## Suggested output format + +After this step, provide a concise summary like: + +### Hub context summary + +- **FinOps hub**: [hub name or cluster short URI] +- **Cluster URI**: [active cluster URI] +- **Data coverage**: [min/max charge period, row count, major service count] +- **Available functions used for follow-up**: `Costs()`, `Prices()`, `Recommendations()`, `Transactions()` +- **Observed tag keys**: [examples discovered with `bag_keys(Tags)`] +- **Known gaps or cautions**: [missing tags, sparse coverage, stale data, or incomplete enrichment] + +### Recommended next move + +- Use `references/queries/INDEX.md` to choose a scenario-specific query. +- Use `references/queries/finops-hub-database-guide.md` to verify columns and enrichment fields. + +## Integration guidance for other references + +Other analysis references should assume this foundational step happened first. They should reuse the established hub context, cluster URI, freshness status, and observed tag evidence instead of re-establishing them from scratch. + +## Best practices + +1. Connect once, then reuse the confirmed hub context. +2. Prefer observed tag keys over assumed business dimensions. +3. Start with lightweight KQL validation before large analysis queries. +4. Use the query catalog for scenario-specific analysis instead of inventing unsupported patterns. +5. Keep conclusions limited to what the Hub data actually shows. + +## See also + +- `references/workflows/ftk-hubs-connect.md` +- `references/workflows/ftk-hubs-healthCheck.md` +- `references/queries/finops-hub-database-guide.md` +- `references/queries/INDEX.md` diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/workflows/ftk-hubs-connect.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/workflows/ftk-hubs-connect.md new file mode 100644 index 000000000..6b49e1a21 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/workflows/ftk-hubs-connect.md @@ -0,0 +1,98 @@ +# Connect to a FinOps hub cluster + +## Step 1: Use the cluster identifier, if specified + +If the user specified a cluster, check `.ftk/environments.local.md` for a matching environment by hub name, cluster name, cluster short URI (name and location), or cluster URI. + +If the cluster has already been added to `.ftk/environments.local.md`, announce that you'll use that FinOps hub instance for the session and skip to step 4. + +If the cluster was not found in `.ftk/environments.local.md`, go to step 2 to find FinOps hub instances that you can connect to. + +## Step 2: Find FinOps hub instances, if not specified + +If a cluster was identified and found in the previous step, skip this step. + +If a cluster was not identified or was not found in the previous step, use this Azure Resource Graph query to find FinOps hub instances that you can connect to: + +```kusto +resources +| where type =~ "microsoft.kusto/clusters" +| where tags['ftk-tool'] == 'FinOps hubs' +| extend hubResourceId = tolower(tags["cm-resource-parent"]) +| extend hubName = split(hubResourceId, '/microsoft.cloud/hubs/')[1] +| extend hubVersion = tostring(tags["ftk-version"]) +| project hubResourceId, hubName, hubVersion, location, clusterResourceId = id, clusterName = name, clusterShortUri = strcat(name, '.', location), clusterUri = properties.uri, resourceGroup, subscriptionId +``` + +Filter this list based on the user's input, if provided. + +Notes about the columns: + +- Use the `clusterShortUri` to refer to the FinOps hub instance. +- Also accept the `hubName`, `clusterName`, or `resourceGroup` to refer to the FinOps hub instance as long as they are unique. If there are multiple FinOps hub instances with the same identifier, list them and ask which the user should use. +- Use the `clusterUri` to connect to the cluster using `#azmcp-kusto-query`. +- The `hubVersion` is the version of the FinOps hub instance. Format this value is a string using Semantic Versioning (SemVer) format (e.g., `major.minor` or `major.minor.patch` or `major.minor-prerelease`). + +Tell the user how many FinOps hub instances you found that matched their inputs, if provided. If there is only one FinOps hub instance, announce that you will use that FinOps hub instance for this session and skip to step 4. If there are multiple FinOps hub instances, list them with the following details: + +- `hubName` +- `hubVersion` +- `clusterShortUri` +- Subscription name + +If you don't find any FinOps hub instances, inform the user that you couldn't find any FinOps hubs and ask them to provide a subscription or cluster URI to connect. If they provide a subscription, repeat step 2 with that subscription name or ID. If they provide a cluster URI, use that for the session and skip to step 4. + +## Step 3: Ask which FinOps hub instance to use + +If a FinOps hub instance was identified in the previous steps, skip this step. + +If multiple FinOps hub instances were found and shared with the user, ask the user to select one of them by providing the `hubName`, `clusterShortUri`, or another cluster URI of the FinOps hub instance they want to use. + +## Step 4: Validate the FinOps hub instance + +If a FinOps hub instance was identified in a previous step, run the following query against the `Hub` database with the #azmcp-kusto-query command to validate the FinOps hub instance. Use the `tenant` from the selected environment. The query reads the version from `Ingestion.HubSettings`, but the primary analytical surface is still `Costs()` in the `Hub` database: + +```kusto +let version = toscalar(database('Ingestion').HubSettings | project version); +Costs() +| summarize + Cost = format_number(sum(EffectiveCost), 'N2'), + Months = dcount(startofmonth(ChargePeriodStart)), + DataLastUpdated = format_datetime(max(ChargePeriodStart), 'yyyy-MM-dd') + by + HubVersion = version, + BillingCurrency +``` + +Announce the name and version of the FinOps hub instance you are connecting to, when data was last updated, and how much cost is covered over how many months. Format the cost using the billing currency. If there are multiple billing currencies, list each in a bulleted list of formatted cost and number of months. + +If the query fails, inform the user that you couldn't connect to the FinOps hub instance and ask them to provide a different cluster URI or subscription name. If they provide a cluster URI, repeat step 4 with that URI. If they provide a subscription name, repeat step 2 with that subscription name. + +## Step 5: Save the environment + +After validating the FinOps hub instance, save the connection details to `.ftk/environments.local.md`: + +1. Read the existing file if it exists to preserve other environments +2. Add or update the environment entry using the `clusterShortUri` as the environment name +3. Include `cluster-uri`, `tenant`, `subscription`, and `resource-group` values +4. Set `default` to this environment if no default exists or if this is the only environment + +Example format: + +```markdown +--- +default: myhub.eastus +environments: + myhub.eastus: + cluster-uri: https://myhub.eastus.kusto.windows.net + tenant: 00000000-0000-0000-0000-000000000000 + subscription: my-subscription + resource-group: rg-finops +--- +``` + +See `references/settings-format.md` for the complete file format documentation. + +## Step 6: Run a health check + +After connecting to the FinOps hub instance, inform the user they can use the `/ftk-hubs-healthCheck` prompt to run a health check. diff --git a/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/workflows/ftk-hubs-healthCheck.md b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/workflows/ftk-hubs-healthCheck.md new file mode 100644 index 000000000..fe5470d2a --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/skills/finops-toolkit/references/workflows/ftk-hubs-healthCheck.md @@ -0,0 +1,31 @@ +# Health check for FinOps hubs + +## Step 1: Check the latest released FinOps hub version + +Get the content from this file to determine the latest stable version of FinOps hubs: `https://raw.githubusercontent.com/microsoft/finops-toolkit/refs/heads/main/src/templates/finops-hub/modules/ftkver.txt`. + +Get the content from this file to determine the latest development version of FinOps hubs: `https://raw.githubusercontent.com/microsoft/finops-toolkit/refs/heads/dev/src/templates/finops-hub/modules/ftkver.txt`. + +FinOps hubs use semantic versioning (SemVer) format for version numbers, which is `major.minor`, `major.minor.patch`, or `major.minor-prerelease`. If the version number has `-dev` at the end of it, that means it's a development version. + +Compare the version of the current FinOps hub instance with the latest stable version of FinOps hubs. If it's the same version as stable, tell the user they are using the latest released version and skip to the next step. + +If the FinOps hub version is the same as the development version, tell the user they are using the development version and they should monitor the repository to ensure it's updated with the latest changes, then skip to the next step. + +If the FinOps hub version is older than the development version and matches or is older than the latest stable version, tell the user they are using an older development version and should update to the latest stable release or development version. Mention their version number and the latest stable and development version numbers. Give them this link to deploy the latest stable version depending on their Azure cloud environment: + +- For the Azure public, commercial cloud, use https://aka.ms/finops/hubs/deploy +- For the Azure Government cloud, use https://aka.ms/finops/hubs/deploy/gov +- For the Azure China cloud, use https://aka.ms/finops/hubs/deploy/china + +## Step 2: Check the latest data refresh/update date + +If the last data refresh/update date is less than 24 hours ago, skip this step. + +If the last data refresh/update date is more than 24 hours ago, inform the user that the data may be stale and they should check the Microsoft Cost Management exports and Azure Data Factory data ingestion pipelines to ensure they are running without errors. + +Give them a link to Microsoft Cost Management to check the exports: https://portal.azure.com/#view/Microsoft_Azure_CostManagement/Menu/~/exports + +Give them a link to the Azure Data Factory portal to check data ingestion pipelines: https://adf.azure.com/monitoring/pipelineruns + +Tell the user you can help them troubleshoot any issues with [common errors](https://learn.microsoft.com/cloud-computing/finops/toolkit/help/errors) and the [troubleshooting guide](https://learn.microsoft.com/cloud-computing/finops/toolkit/help/troubleshooting). diff --git a/src/templates/sre-agent/recipes/finops-hub/config/subagents/azure-capacity-manager.yaml b/src/templates/sre-agent/recipes/finops-hub/config/subagents/azure-capacity-manager.yaml new file mode 100644 index 000000000..aa4ad764f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/subagents/azure-capacity-manager.yaml @@ -0,0 +1,122 @@ +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. + + + Load the azure-capacity-management skill when it''s available before handling capacity work. + + + 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. + + - Use assigned Python capacity tools, knowledge files, and uploaded references to inspect current state, compare scenarios, and recommend next actions. + + - Do not call Kusto, Azure Resource Graph, Azure CLI, deployment, data freshness, or handoff tools. Use Teams, Outlook, and visualization tools only when the practitioner asks you to render or deliver the final evidence package. If Kusto-backed cost evidence is needed, return a precise evidence request for the practitioner to send to `ftk-database-query`. If Resource Graph, Hub health, or infrastructure remediation evidence is needed, return a precise evidence request for the practitioner to send to `ftk-hubs-agent`. + + + 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. + + - 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. + + - Azure Data Explorer / Kusto SKU eligibility is service-specific and can differ from Microsoft.Compute SKU availability. Use `sku-availability` with `resource_provider: kusto` before advising on FinOps Hub analytics backend SKUs. + + + 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. + + + 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. + + + 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 + - PostTeamsMessage + - SendOutlookEmail + - ReplyToTeamsMessage + - GetTeamsMessages + 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/src/templates/sre-agent/recipes/finops-hub/config/subagents/chief-financial-officer.yaml b/src/templates/sre-agent/recipes/finops-hub/config/subagents/chief-financial-officer.yaml new file mode 100644 index 000000000..5dec69675 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/subagents/chief-financial-officer.yaml @@ -0,0 +1,45 @@ +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. + + - For month-level, fiscal-period, or scheduled financial reports, consume evidence packages from the FinOps practitioner, FinOps Hub database specialist, 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/src/templates/sre-agent/recipes/finops-hub/config/subagents/finops-practitioner.yaml b/src/templates/sre-agent/recipes/finops-hub/config/subagents/finops-practitioner.yaml new file mode 100644 index 000000000..29642280e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/subagents/finops-practitioner.yaml @@ -0,0 +1,86 @@ +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 FinOps Toolkit adoption. + + + 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 and FinOps Toolkit capabilities where they materially help. + + + 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. + + - 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. + - When `ftk-database-query` reports that top-other-transactions returned the literal string ZERO_ROWS_RETURNED, treat the tool call itself as completed, then ask `ftk-hubs-agent` to run data-freshness-check before stating that no transactions matched. If data-freshness-check reports TRANSACTIONS_ZERO_ROWS, surface the export / ingestion / stored-function diagnostic as a report limitation instead of a successful empty result. + + + Collaboration guidance: + + - Use FinOps Toolkit and Azure Cost Management concepts when they improve implementation speed, data quality, or governance rigor. + + - Own every scheduled task run and the FinOps operating rhythm. You have only the Knowledge Base search tool (`SearchMemory`) so you can read uploaded recipe knowledge, prior memory, and `ftk-output-style.md`. You have no operational tools. Orchestrate the four delegated subagents for specialist evidence, visualization, delivery, and decision framing. Do not gather data or deliver reports yourself. + + - Hand off every FinOps Hub database, KQL, FOCUS, Costs(), Prices(), Recommendations(), and Transactions() request to `ftk-database-query`. Never call Kusto tools directly. + + - Hand off quota, capacity reservation group, regional SKU, zonal availability, AI/GPU capacity, database service quota, and benefit recommendation questions to `azure-capacity-manager`. + + - Hand off FinOps Hub platform health, data freshness, Azure Resource Graph inventory, deployment validation, alert or budget deployment, Advisor suppression management, and infrastructure remediation questions to `ftk-hubs-agent`. + + - Hand off executive finance narratives, portfolio prioritization, and board-level recommendations to `chief-financial-officer`. The CFO consumes evidence and pontificates on results; it does not gather data or own schedules. + + - Subagent handoffs preserve main-agent context. Pass the exact task scope, evidence already collected, open questions, and requested output shape with each handoff.' + 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: + - chief-financial-officer + - ftk-database-query + - azure-capacity-manager + - ftk-hubs-agent + tools: + - SearchMemory + systemTools: [] + mcpTools: [] + maxReflectionCount: 0 + customReflectionNote: '' + commonPrompts: [] + enableVanillaMode: true diff --git a/src/templates/sre-agent/recipes/finops-hub/config/subagents/ftk-database-query.yaml b/src/templates/sre-agent/recipes/finops-hub/config/subagents/ftk-database-query.yaml new file mode 100644 index 000000000..b6a46bc93 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/subagents/ftk-database-query.yaml @@ -0,0 +1,146 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgent +metadata: + name: ftk-database-query +spec: + instructions: 'You are the FinOps Hub database and KQL specialist for the FinOps Toolkit. + + + Scope: + + - Query, explain, and analyze FinOps Hub cost, pricing, recommendation, and transaction data. + + - Use KQL for all database work. Never use SQL syntax. + + - Always use the Hub database for analytics. Never use Ingestion for end-user analysis. + + - Use attached FinOps Hub Kusto query tooling for live execution when it is available; otherwise provide the exact KQL and required execution parameters. + + - Never use RunAzCliReadCommands, Azure CLI, `az rest`, Python, data-freshness-check, Resource Graph, deployment, or handoff tools. Use Teams, Outlook, and visualization tools only when the practitioner asks you to render or deliver the final evidence package. Use the attached Kusto tools for catalog analytics. If Hub data freshness, Resource Graph inventory, platform health, or infrastructure remediation evidence is needed, return a precise evidence request for the practitioner to send to `ftk-hubs-agent`. + + + Required workflow: + + 1. Check the query catalog first before writing custom KQL. + + 2. Reuse or adapt the closest catalog query when it fits the user''s scenario. + + 3. If no catalog query fits, start custom analysis from costs-enriched-base for scoped row-level drill-down only and extend it instead of rebuilding enrichment logic from scratch. Do not run costs-enriched-base for full-month, fiscal-period, scheduled report, dashboard export, or broad tag-coverage requests. Use aggregation tools first, then drill into a one-day or narrower slice only when raw details are required. + + 4. Filter early with the correct date column for the function and keep the query scoped to the user''s question. Use ChargePeriodStart for Costs() and Transactions(); use x_EffectivePeriodStart for Prices(); do not assume every function has ChargePeriodStart. + + 5. Prefer EffectiveCost unless the user explicitly asks for billed, contracted, or list cost. + + + Database functions to use: + + - Costs() for FOCUS-aligned cost and usage analysis. + + - Prices() for list, contracted, and effective pricing analysis. + + - Recommendations() for reservation and savings plan recommendation analysis. + + - Transactions() for commitment purchases, refunds, exchanges, and other non-usage financial transactions. + + + Query and response rules: + + - Use exact schema column names, including x_ enrichment columns when needed. + + - Treat Tags as dynamic and extract values with tostring(Tags[''key'']). + + - Always include tenant when preparing live query parameters for the customer''s FinOps Hub cluster and Hub database. + + - Explain which function, filters, and metrics you used, then summarize the result clearly. + + - If a requested query is too broad, require tighter time, subscription, region, or resource scope before execution. + - When top-other-transactions returns the literal string ZERO_ROWS_RETURNED, treat the tool call itself as completed, then tell the practitioner to ask `ftk-hubs-agent` to run data-freshness-check before stating that no transactions matched. If data-freshness-check reports TRANSACTIONS_ZERO_ROWS, surface the export / ingestion / stored-function diagnostic as a report limitation instead of a successful empty result. + + + Grounding rules: + + - Check uploaded FinOps Toolkit references and schema guidance before making claims about functions or columns. + + - Do not duplicate long reference docs in your answers; summarize only the parts needed for the current analysis. + + - If data is missing, stale, or inconsistent, say so and suggest the next diagnostic step. + + - When `Transactions()` or transaction tools return zero rows, do not state that no transactions occurred. Tell the practitioner that `ftk-hubs-agent` must run `data-freshness-check` to confirm row counts and diagnostics. If `TRANSACTIONS_ZERO_ROWS` appears, explain that reservation transaction analysis is unavailable until the `reservationtransactions` export, Data Factory Transactions ingestion, and `Transactions()` / `Transactions_v1_2()` stored functions are verified.' + handoffDescription: 'Use this agent for FinOps Hub database and KQL work: query design, schema-aware cost analysis, pricing lookups, recommendation analysis, transaction analysis, and interpretation of + FinOps Hub data in the Hub database.' + handoffs: [] + tools: + - SearchMemory + - PlotBarChart + - PlotPieChart + - PlotAreaChartWithCorrelation + - ai-cost-by-application + - ai-daily-trend + - ai-model-cost-comparison + - ai-token-usage-breakdown + - allocation-accuracy-index + - anomaly-detection-rate + - anomaly-variance-total + - commitment-discount-waste + - commitment-discount-utilization + - commitment-utilization-score + - cost-anomaly-detection + - cost-by-financial-hierarchy + - cost-by-region-trend + - cost-forecasting-model + - cost-optimization-index + - cost-per-gb-stored + - cost-visibility-delay + - costs-enriched-base + - compute-cost-per-core + - compute-spend-commitment-coverage + - data-update-frequency + - macc-consumption-vs-commitment + - monthly-cost-change-percentage + - monthly-cost-trend + - percentage-unallocated-costs + - percentage-untagged-costs + - quarterly-cost-by-resource-group + - reservation-recommendation-breakdown + - savings-summary-report + - service-price-benchmarking + - storage-tier-distribution + - tagging-policy-compliance + - top-commitment-transactions + - top-other-transactions + - top-resource-groups-by-cost + - top-resource-types-by-cost + - top-services-by-cost + - PostTeamsMessage + - SendOutlookEmail + - ReplyToTeamsMessage + - GetTeamsMessages + 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/src/templates/sre-agent/recipes/finops-hub/config/subagents/ftk-hubs-agent.yaml b/src/templates/sre-agent/recipes/finops-hub/config/subagents/ftk-hubs-agent.yaml new file mode 100644 index 000000000..5beea5721 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/subagents/ftk-hubs-agent.yaml @@ -0,0 +1,110 @@ +# T-5: RTM FR-1.1 +api_version: azuresre.ai/v2 +kind: ExtendedAgent +metadata: + name: ftk-hubs-agent +spec: + instructions: |- + You are the FinOps Hubs deployment and operations specialist for the FinOps Toolkit. + + Scope: + - Deploy new FinOps Hubs environments. + - Upgrade existing hub deployments. + - Maintain hub configuration, exports, dashboards, and connectivity. + - Troubleshoot failed deployments and unhealthy hub environments. + - Manage FinOps Hub infrastructure health, Resource Graph inventory, data freshness, deployment validation, alert and budget deployment, and Advisor suppression operations. + + Operating rules: + - Start by checking uploaded knowledge for FinOps Hubs deployment, hub architecture, compatibility, and troubleshooting guidance before taking action. + - Prefer Azure CLI workflows and use Azure CLI help when command syntax or parameters need verification. + - Use read commands for discovery and validation first; use write commands only after the change has been validated. + - For FinOps Hub database freshness, run `data-freshness-check`. Do not use Azure CLI or `az rest` against Azure Data Explorer query or management endpoints. + - Do not run FinOps Hub analytical Kusto tools such as cost, savings, pricing, recommendation, or transaction queries. Those belong to `ftk-database-query`; use `data-freshness-check` only for Hub freshness and function diagnostics. + - Do not duplicate long-form documentation in your responses; summarize only the guidance needed for the current task. + + Deployment and upgrade workflow: + 1. Verify prerequisites before proposing or running changes: + - Confirm subscription, resource group, region, hub name, backend choice, and required parameters. + - Confirm Azure authentication and the required RBAC for deployment and Cost Management export configuration. + - Confirm Bicep is available for template validation. + - Validate required resource providers, especially Microsoft.EventGrid and Microsoft.CostManagementExports. + - Check for naming conflicts, unsupported regions, quota limits, and existing resources that might block the deployment. + - For Data Explorer / Kusto analytics backend deployments or upgrades, run `sku-availability` with `resource_provider: kusto`, `subscription_id`, `location`, and the planned Data Explorer SKU as `sku_filter`. Treat `is_available: false` as a deployment blocker; do not infer Kusto SKU eligibility from Microsoft.Compute SKU availability. + 2. Validate templates before deployment: + - Inspect the template and parameter set. + - Run template validation such as `bicep build --stdout` before deployment. + - Review version compatibility and breaking changes before upgrades. + 3. Required safety gate for every deploy or upgrade: + - Run `az deployment group what-if` or `az deployment sub what-if` before applying changes. + - Show or summarize the expected changes before proceeding. + - If what-if or template validation fails, stop and explain the blocking issue before any write action. + 4. Execute the deployment or upgrade only after prerequisites, template validation, and what-if complete successfully. + 5. Perform post-deployment validation: + - Confirm the deployment succeeded and the expected resources exist. + - Verify storage, Data Factory, Key Vault, and analytics backend connectivity as applicable. + - Validate Hub database readiness, Cost Management export configuration, and any required follow-up setup. + - Use `data-freshness-check` as the source of truth for Hub data freshness. Treat `Costs()` from that direct REST tool as authoritative over stale memory, raw KQL rollups, or ingestion timestamp conclusions. + - Capture important outputs such as cluster URI, storage details, and managed identity IDs for downstream configuration. + + Maintenance and troubleshooting workflow: + - Gather the exact operation, error text, resource IDs, deployment logs, and recent changes. + - Check common root causes first: RBAC, missing providers, region support, quota limits, naming conflicts, missing exports, version incompatibility, and broken connectivity. + - For errors such as "The sku is not supported in " in nested Microsoft.FinOpsHubs.Analytics deployments, preflight the target region with `sku-availability` using `resource_provider: kusto` and recommend a returned SKU or a region where the requested SKU is returned. + - Use deployment operations and current resource state to isolate the fault before recommending fixes. + - Provide concrete remediation steps, validate the result, and summarize next steps. + - Warn before destructive changes and recommend backup or rollback planning for upgrades and major configuration changes. + handoffDescription: Deploys, upgrades, maintains, and troubleshoots FinOps Hubs with validation-first Azure workflows. + handoffs: [] + tools: + - SearchMemory + - RunAzCliReadCommands + - RunAzCliWriteCommands + - GetAzCliHelp + - PlotBarChart + - PlotPieChart + - PlotAreaChartWithCorrelation + - data-freshness-check + - resource-graph-query + - sku-availability + - deploy-anomaly-alert + - deploy-budget + - deploy-bulk-anomaly-alerts + - deploy-bulk-budgets + - suppress-advisor-recommendations + - PostTeamsMessage + - SendOutlookEmail + - ReplyToTeamsMessage + - GetTeamsMessages + 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"})) + maxReflectionCount: 0 + customReflectionNote: '' + commonPrompts: [] + enableVanillaMode: false diff --git a/src/templates/sre-agent/recipes/finops-hub/config/tools/benefit-recommendations.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/benefit-recommendations.yaml new file mode 100644 index 000000000..ea5d59a24 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/benefit-recommendations.yaml @@ -0,0 +1,186 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: benefit-recommendations +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/capacity-reservation-groups.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/capacity-reservation-groups.yaml new file mode 100644 index 000000000..76d58c0a0 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/capacity-reservation-groups.yaml @@ -0,0 +1,199 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: capacity-reservation-groups +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/data-freshness-check.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/data-freshness-check.yaml new file mode 100644 index 000000000..9f14e24bb --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/data-freshness-check.yaml @@ -0,0 +1,454 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: data-freshness-check +spec: + type: PythonTool + connector: '' + toolMode: Auto + description: |- + Checks FinOps Hub function data freshness in an Azure Data Explorer hub database + through direct Azure Data Explorer REST queries. This tool is authoritative for + hub data freshness. Access/queryability failures are reported separately from + stale or empty data. Stale memory notes, raw KQL rollups, and ingestion timestamp + checks are superseded when they conflict with this result. + The tool reports row counts, latest data dates, staleness in days, stored-function + schema status, and precise diagnostics for empty or stale datasets. Transactions() + zero rows are reported as a reservationtransactions export / ingestion / stored-function + issue, not as a generic empty result. + functionCode: |- + def main(**kwargs): + from datetime import datetime, timezone + from urllib.parse import urlparse + + import requests + + KUSTO_SCOPE = "https://api.kusto.windows.net/.default" + KUSTO_QUERY_PATH = "/v2/rest/query" + DEFAULT_FRESHNESS_THRESHOLD_DAYS = 3 + + cluster_uri = kwargs.get("cluster_uri") + database = kwargs.get("database") + freshness_threshold_days = kwargs.get("freshness_threshold_days", DEFAULT_FRESHNESS_THRESHOLD_DAYS) + try: + freshness_threshold_days = int(freshness_threshold_days) + except (TypeError, ValueError): + freshness_threshold_days = DEFAULT_FRESHNESS_THRESHOLD_DAYS + + result = { + "cluster_uri": cluster_uri, + "database": database or "hub", + "functions": [], + "total_functions": 0, + "stale_count": 0, + "empty_count": 0, + "error_count": 0, + "queryable_count": 0, + "hub_data_stale": False, + "attention_required": False, + "status": "unknown", + "diagnostics": [], + "freshest_function": None, + "stalest_function": None, + "source_of_truth": { + "tool": "data-freshness-check", + "method": "direct_adx_rest_query", + "endpoint": KUSTO_QUERY_PATH, + "authoritative_function": "Costs()", + "freshness_threshold_days": freshness_threshold_days, + "supersedes": [ + "stale memory conclusions", + "raw KQL freshness rollups", + "Kusto ingestion timestamp checks", + ], + }, + } + + if not cluster_uri: + result["error"] = "cluster_uri is required" + return result + + raw_cluster_uri = str(cluster_uri).strip() + if not raw_cluster_uri.startswith(("https://", "http://")): + raw_cluster_uri = f"https://{raw_cluster_uri}" + + parsed_cluster_uri = urlparse(raw_cluster_uri) + if parsed_cluster_uri.scheme != "https" or not parsed_cluster_uri.netloc: + result["error"] = "cluster_uri must be an HTTPS Azure Data Explorer cluster URI" + return result + + path_database = parsed_cluster_uri.path.strip("/").split("/")[0] if parsed_cluster_uri.path.strip("/") else None + database = database or path_database or "hub" + cluster_uri = f"https://{parsed_cluster_uri.netloc}".rstrip("/") + result["cluster_uri"] = cluster_uri + result["database"] = database + + def _to_utc(value): + if value is None or value == "": + return None + if isinstance(value, datetime): + parsed = value + else: + parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00")) + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return parsed.astimezone(timezone.utc) + + def _iso(value): + if value is None: + return None + return value.isoformat().replace("+00:00", "Z") + + def _table_rows(payload): + if isinstance(payload, list): + for frame in payload: + if not isinstance(frame, dict): + continue + frame_type = frame.get("FrameType") or frame.get("frameType") + table_kind = frame.get("TableKind") or frame.get("tableKind") + if frame_type == "DataTable" and table_kind in (None, "PrimaryResult"): + rows = frame.get("Rows") or frame.get("rows") or [] + columns = frame.get("Columns") or frame.get("columns") or [] + column_names = [ + column.get("ColumnName") or column.get("columnName") or column.get("name") + for column in columns + if isinstance(column, dict) + ] + return rows, column_names + return [], [] + + if not isinstance(payload, dict): + return [], [] + + tables = payload.get("Tables") or payload.get("tables") or [] + if not tables: + return [], [] + table = tables[0] + rows = table.get("Rows") or table.get("rows") or [] + columns = table.get("Columns") or table.get("columns") or [] + column_names = [ + column.get("ColumnName") or column.get("columnName") or column.get("name") + for column in columns + if isinstance(column, dict) + ] + return rows, column_names + + def _row_value(row, column_names, name, index): + if isinstance(row, dict): + return row.get(name) + if isinstance(row, (list, tuple)): + if name in column_names: + column_index = column_names.index(name) + if column_index < len(row): + return row[column_index] + if index < len(row): + return row[index] + return None + + def _as_int(value): + if value is None or value == "": + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + def _append_diagnostic(function_name, severity, code, message, recommended_actions=None, verification=None): + diagnostic = { + "function_name": function_name, + "severity": severity, + "code": code, + "message": message, + } + if recommended_actions: + diagnostic["recommended_actions"] = recommended_actions + if verification: + diagnostic["verification"] = verification + result["diagnostics"].append(diagnostic) + return diagnostic + + def _execute_kusto(csl): + response = requests.post( + f"{cluster_uri}{KUSTO_QUERY_PATH}", + headers=headers, + json={"db": database, "csl": csl}, + timeout=120, + ) + response.raise_for_status() + return response.json() + + try: + from azure.identity import DefaultAzureCredential + credential = DefaultAzureCredential() + token = credential.get_token(KUSTO_SCOPE).token + headers = { + "Accept": "application/json", + "Authorization": f"Bearer {token}", + "Content-Type": "application/json; charset=utf-8", + "x-ms-app": "FinOpsSreAgentDataFreshnessCheck", + } + except Exception as exc: + result["error"] = f"Failed to initialize Azure Data Explorer REST credential: {exc}" + return result + + now = datetime.now(timezone.utc) + errors = [] + function_checks = [ + { + "function_name": "Costs()", + "source_dataset": "Costs", + "expected_export_dataset": "focuscost", + "query": "Costs() | summarize RowCount=count(), LatestData=max(ChargePeriodStart) | project RowCount, LatestData", + "schema_query": "Costs() | getschema | summarize SchemaColumnCount=count()", + "freshness_column": "LatestData", + "required_for_hub_health": True, + "empty_code": "COSTS_ZERO_ROWS", + "stale_code": "COSTS_STALE", + "empty_message": "Costs() returned zero rows. This is a Hub data blocker, not a successful empty result.", + "empty_actions": [ + "Verify Cost Management cost exports are configured and have recent successful runs.", + "Verify Azure Data Factory ingestion pipelines processed the Costs dataset successfully.", + "Verify the Costs() stored function resolves against the current Hub schema.", + ], + }, + { + "function_name": "Prices()", + "source_dataset": "Prices", + "expected_export_dataset": "pricesheet", + "query": "Prices() | summarize RowCount=count(), LatestData=max(x_EffectivePeriodStart) | project RowCount, LatestData", + "schema_query": "Prices() | getschema | summarize SchemaColumnCount=count()", + "freshness_column": "LatestData", + "required_for_hub_health": False, + "empty_code": "PRICES_ZERO_ROWS", + "stale_code": "PRICES_STALE", + "empty_message": "Prices() returned zero rows. Pricing analysis is unavailable until the pricesheet export and ingestion path are verified.", + "empty_actions": [ + "Verify Cost Management pricesheet exports are configured for supported billing scopes.", + "Verify Azure Data Factory ingestion pipelines processed the Prices dataset successfully.", + "Verify the Prices() stored function resolves against the current Hub schema.", + ], + }, + { + "function_name": "Recommendations()", + "source_dataset": "Recommendations", + "expected_export_dataset": "reservationrecommendations", + "query": "Recommendations() | summarize RowCount=count() | project RowCount", + "schema_query": "Recommendations() | getschema | summarize SchemaColumnCount=count()", + "freshness_column": None, + "required_for_hub_health": False, + "empty_code": "RECOMMENDATIONS_ZERO_ROWS", + "stale_code": "RECOMMENDATIONS_STALE", + "empty_message": "Recommendations() returned zero rows. Recommendation analysis is unavailable until the reservation recommendations export and ingestion path are verified.", + "empty_actions": [ + "Verify Cost Management reservationrecommendations exports are configured for supported billing scopes.", + "Verify Azure Data Factory ingestion pipelines processed the Recommendations dataset successfully.", + "Verify the Recommendations() stored function resolves against the current Hub schema.", + ], + }, + { + "function_name": "Transactions()", + "source_dataset": "Transactions", + "expected_export_dataset": "reservationtransactions", + "query": "Transactions() | extend _IngestionTime = todatetime(column_ifexists('x_IngestionTime', datetime(null))) | summarize RowCount=count(), LatestData=max(ChargePeriodStart), LatestIngestionTime=max(_IngestionTime) | project RowCount, LatestData, LatestIngestionTime", + "schema_query": "Transactions() | getschema | summarize SchemaColumnCount=count()", + "freshness_column": "LatestData", + "required_for_hub_health": False, + "empty_code": "TRANSACTIONS_ZERO_ROWS", + "stale_code": "TRANSACTIONS_STALE", + "empty_message": "Transactions() returned zero rows. The reservation transactions dataset is empty; do not report this as 'no transactions' until export, ingestion, and stored-function behavior are verified.", + "empty_actions": [ + "Verify Cost Management reservationtransactions exports are configured for supported EA or MCA billing scopes.", + "Verify Azure Data Factory ingestion pipelines processed the Transactions dataset successfully.", + "Verify the Transactions() stored function resolves to the current Transactions_v1_2() implementation.", + ], + }, + ] + + for check in function_checks: + function_name = check["function_name"] + latest_data = None + latest_ingestion_time = None + staleness_days = None + is_stale = True + has_data = False + row_count = None + schema_status = "not_checked" + schema_column_count = None + diagnostic_code = "OK" + diagnostic_message = None + schema_error = None + data_error = None + + try: + schema_payload = _execute_kusto(check["schema_query"]) + schema_rows, schema_columns = _table_rows(schema_payload) + if schema_rows: + schema_column_count = _as_int(_row_value(schema_rows[0], schema_columns, "SchemaColumnCount", 0)) + schema_status = "ok" if schema_column_count and schema_column_count > 0 else "empty_schema" + else: + schema_status = "no_schema_rows" + except Exception as exc: + schema_error = str(exc) + schema_status = "error" + errors.append({"function_name": function_name, "phase": "schema", "error": str(exc)}) + diagnostic = _append_diagnostic( + function_name, + "error", + f"{function_name.strip('()').upper()}_SCHEMA_QUERY_FAILED", + f"{function_name} schema verification failed: {exc}", + [ + "If this is HTTP 403, verify the agent identity has ADX AllDatabasesViewer on the cluster.", + "If this is a semantic error, check the Hub version and stored function deployment for this dataset.", + ], + ["Stored-function schema query failed."], + ) + diagnostic_code = diagnostic["code"] + diagnostic_message = diagnostic["message"] + + try: + freshness_payload = _execute_kusto(check["query"]) + freshness_rows, freshness_columns = _table_rows(freshness_payload) + if freshness_rows: + row_count = _as_int(_row_value(freshness_rows[0], freshness_columns, "RowCount", 0)) + has_data = row_count is not None and row_count > 0 + + if check["freshness_column"]: + if freshness_rows: + raw_latest = _row_value(freshness_rows[0], freshness_columns, check["freshness_column"], 1) + latest_data = _to_utc(raw_latest) + latest_ingestion_time = _to_utc(_row_value(freshness_rows[0], freshness_columns, "LatestIngestionTime", 2)) + if latest_data is not None: + staleness_days = round((now - latest_data).total_seconds() / 86400, 2) + is_stale = not has_data or staleness_days is None or staleness_days > freshness_threshold_days + else: + is_stale = not has_data + + if not has_data: + diagnostic = _append_diagnostic( + function_name, + "warning" if not check["required_for_hub_health"] else "error", + check["empty_code"], + check["empty_message"], + check["empty_actions"], + [ + f"{function_name} row count is zero.", + f"{function_name} stored-function schema status: {schema_status}.", + ], + ) + diagnostic_code = diagnostic["code"] + diagnostic_message = diagnostic["message"] + elif staleness_days is not None and staleness_days > freshness_threshold_days: + diagnostic = _append_diagnostic( + function_name, + "warning" if not check["required_for_hub_health"] else "error", + check["stale_code"], + f"{function_name} latest data is {staleness_days} days old, which exceeds the {freshness_threshold_days}-day freshness threshold.", + [ + f"Verify the {check['expected_export_dataset']} export is still running.", + f"Verify Data Factory ingestion processed the {check['source_dataset']} dataset successfully.", + ], + ) + diagnostic_code = diagnostic["code"] + diagnostic_message = diagnostic["message"] + except Exception as exc: + data_error = str(exc) + errors.append({"function_name": function_name, "phase": "data", "error": str(exc)}) + diagnostic = _append_diagnostic( + function_name, + "error", + f"{function_name.strip('()').upper()}_QUERY_FAILED", + f"{function_name} data query failed: {exc}", + [ + "If this is HTTP 403, verify the agent identity has ADX AllDatabasesViewer on the cluster.", + "If this is a semantic error, check the Hub version and stored function compatibility for this dataset.", + ], + ["Data query failed before row count could be verified."], + ) + diagnostic_code = diagnostic["code"] + diagnostic_message = diagnostic["message"] + + result["functions"].append( + { + "function_name": function_name, + "source_dataset": check["source_dataset"], + "expected_export_dataset": check["expected_export_dataset"], + "row_count": row_count, + "latest_data_date": _iso(latest_data), + "latest_ingestion_time": _iso(latest_ingestion_time), + "staleness_days": staleness_days, + "is_stale": is_stale, + "is_queryable": schema_error is None and data_error is None, + "has_data": has_data, + "schema_status": schema_status, + "schema_column_count": schema_column_count, + "schema_error": schema_error, + "data_error": data_error, + "required_for_hub_health": check["required_for_hub_health"], + "diagnostic_code": diagnostic_code, + "diagnostic_message": diagnostic_message, + } + ) + + if check["required_for_hub_health"]: + result["hub_data_stale"] = result["hub_data_stale"] or is_stale + + result["total_functions"] = len(result["functions"]) + result["stale_count"] = sum(1 for function in result["functions"] if function["is_stale"]) + result["empty_count"] = sum(1 for function in result["functions"] if not function["has_data"]) + result["error_count"] = len(errors) + result["queryable_count"] = sum(1 for function in result["functions"] if function["is_queryable"]) + + functions_with_data_dates = [ + function for function in result["functions"] if function["latest_data_date"] is not None + ] + if functions_with_data_dates: + result["freshest_function"] = min(functions_with_data_dates, key=lambda function: function["staleness_days"]) + result["stalest_function"] = max(functions_with_data_dates, key=lambda function: function["staleness_days"]) + + costs_function = next( + (function for function in result["functions"] if function["function_name"] == "Costs()"), + None, + ) + if costs_function: + result["authoritative_freshness_signal"] = { + "function_name": "Costs()", + "latest_data_date": costs_function["latest_data_date"], + "staleness_days": costs_function["staleness_days"], + "is_stale": costs_function["is_stale"], + "has_data": costs_function["has_data"], + "row_count": costs_function["row_count"], + } + + if errors: + result["errors"] = errors + + result["attention_required"] = bool(result["diagnostics"]) + if result["error_count"]: + result["status"] = "error" + elif result["hub_data_stale"]: + result["status"] = "hub_data_stale" + elif result["attention_required"]: + result["status"] = "attention_required" + else: + result["status"] = "healthy" + + return result + timeoutSeconds: 300 + dependencies: + - azure-identity + - requests + authScopes: + - ARM + parameters: + - name: cluster_uri + type: string + description: Azure Data Explorer cluster URI to query for data freshness. + required: true + - name: database + type: string + description: Optional Azure Data Explorer database name. Defaults to hub. + required: false + - name: freshness_threshold_days + type: integer + description: Optional age threshold for production freshness checks. Defaults to 3 days; use a higher value for eval datasets with intentionally old data. + required: false diff --git a/src/templates/sre-agent/recipes/finops-hub/config/tools/db-service-quotas.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/db-service-quotas.yaml new file mode 100644 index 000000000..2da2ead72 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/db-service-quotas.yaml @@ -0,0 +1,562 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: db-service-quotas +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-anomaly-alert.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-anomaly-alert.yaml new file mode 100644 index 000000000..730a35195 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-anomaly-alert.yaml @@ -0,0 +1,114 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: deploy-anomaly-alert +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-budget.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-budget.yaml new file mode 100644 index 000000000..6252d84c8 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-budget.yaml @@ -0,0 +1,176 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: deploy-budget +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-anomaly-alerts.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-anomaly-alerts.yaml new file mode 100644 index 000000000..830e1bec6 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/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: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-budgets.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-budgets.yaml new file mode 100644 index 000000000..af1179247 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-budgets.yaml @@ -0,0 +1,202 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: deploy-bulk-budgets +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/non-compute-quotas.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/non-compute-quotas.yaml new file mode 100644 index 000000000..724553530 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/non-compute-quotas.yaml @@ -0,0 +1,505 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: non-compute-quotas +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/resource-graph-query.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/resource-graph-query.yaml new file mode 100644 index 000000000..6eab06060 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/resource-graph-query.yaml @@ -0,0 +1,129 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: resource-graph-query +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/sku-availability.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/sku-availability.yaml new file mode 100644 index 000000000..80f1ec00e --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/sku-availability.yaml @@ -0,0 +1,247 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: sku-availability +spec: + type: PythonTool + connector: '' + toolMode: Auto + description: |- + Lists regional SKU availability for Azure Compute or Azure Data Explorer + (Kusto). Compute is the default and includes available zones and SKU + availability restrictions with reason codes. Kusto mode queries the + Microsoft.Kusto regional SKU API so FinOps Hub analytics deployments can + preflight whether a Data Explorer SKU is eligible in the target region and + subscription before deployment. + 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" + KUSTO_API_VERSION = "2024-04-13" + + subscription_id = kwargs.get("subscription_id") + location = kwargs.get("location") + sku_filter = kwargs.get("sku_filter") + resource_provider = str(kwargs.get("resource_provider") or "compute").strip().lower() + + if resource_provider in ("microsoft.compute", "compute"): + resource_provider = "compute" + elif resource_provider in ("microsoft.kusto", "kusto", "adx", "azuredataexplorer", "azure data explorer"): + resource_provider = "kusto" + + 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 + + if resource_provider not in ("compute", "kusto"): + result["error"] = "resource_provider must be 'compute' or 'kusto'" + 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: + if resource_provider == "kusto": + # Microsoft Learn documents Get-AzKustoSku and Kusto Skus_List as + # the authoritative regional eligibility surface for Azure Data + # Explorer SKUs. Do not infer Kusto availability from Compute SKUs. + url = f"{ARM_ENDPOINT}/subscriptions/{subscription_id}/providers/Microsoft.Kusto/locations/{location}/skus" + params = {"api-version": KUSTO_API_VERSION} + result["service"] = "Azure Data Explorer" + result["source_endpoint"] = f"{url}?api-version={KUSTO_API_VERSION}" + result["requested_sku"] = str(sku_filter).strip() if sku_filter else None + + response = requests.get( + url, + headers=headers, + params=params, + timeout=120, + ) + response.raise_for_status() + payload = response.json() + resource_skus = payload.get("value", []) + + def kusto_value(sku, *keys): + for key in keys: + if key in sku and sku[key] is not None: + return sku[key] + properties = sku.get("properties") + if isinstance(properties, dict): + for key in keys: + if key in properties and properties[key] is not None: + return properties[key] + return None + + exact_match_found = False + for sku in resource_skus: + name = kusto_value(sku, "name") + if normalized_filter and str(name or "").lower() == normalized_filter: + exact_match_found = True + + sku_item = { + "name": name, + "tier": kusto_value(sku, "tier"), + "resource_type": kusto_value(sku, "resourceType", "resource_type"), + } + + for output_key, source_keys in ( + ("size", ("size",)), + ("capacity", ("capacity",)), + ("locations", ("locations",)), + ("restrictions", ("restrictions",)), + ): + value = kusto_value(sku, *source_keys) + if value is not None: + sku_item[output_key] = value + + result["skus"].append(sku_item) + + result["is_available"] = bool(exact_match_found) if normalized_filter else True + if normalized_filter and not exact_match_found: + result["guidance"] = ( + "The requested Azure Data Explorer SKU was not returned by the Microsoft.Kusto " + "regional SKU API for this subscription and region. Choose one of the returned " + "SKUs in this region/subscription, or deploy to a region where the requested SKU " + "is returned. Do not infer ADX/Kusto SKU eligibility from Microsoft.Compute SKU results." + ) + + result["restriction_summary"]["total_skus"] = len(result["skus"]) + result["restriction_summary"]["restricted_skus"] = sum( + 1 for sku in result["skus"] if sku.get("restrictions") + ) + result["restriction_summary"]["restriction_count"] = sum( + len(sku.get("restrictions") or []) for sku in result["skus"] + ) + + else: + 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: resource_provider + type: string + description: |- + Optional service selector. Use `compute` (default) for Microsoft.Compute SKU availability, zones, and restrictions. Use `kusto` for Microsoft.Kusto/Azure Data Explorer regional SKU eligibility before FinOps Hub analytics backend deployments or upgrades. Kusto availability must be checked through Microsoft.Kusto and must not be inferred from Compute SKU results. + required: false + - name: sku_filter + type: string + description: |- + Optional SKU name filter. In Compute mode this remains a case-insensitive substring filter to preserve existing behavior. In Kusto mode this is an exact case-insensitive requested SKU check; the result includes requested_sku, is_available, source_endpoint, and guidance when the requested SKU is absent. + required: false diff --git a/src/templates/sre-agent/recipes/finops-hub/config/tools/suppress-advisor-recommendations.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/suppress-advisor-recommendations.yaml new file mode 100644 index 000000000..49de81624 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/suppress-advisor-recommendations.yaml @@ -0,0 +1,161 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: suppress-advisor-recommendations +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/config/tools/vm-quota-usage.yaml b/src/templates/sre-agent/recipes/finops-hub/config/tools/vm-quota-usage.yaml new file mode 100644 index 000000000..2fb4de708 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/config/tools/vm-quota-usage.yaml @@ -0,0 +1,196 @@ +api_version: azuresre.ai/v2 +kind: ExtendedAgentTool +metadata: + name: vm-quota-usage +spec: + type: PythonTool + 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/src/templates/sre-agent/recipes/finops-hub/connectors.json b/src/templates/sre-agent/recipes/finops-hub/connectors.json new file mode 100644 index 000000000..e332bab6b --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/connectors.json @@ -0,0 +1,13 @@ +{ + "toggles": {}, + "connectors": [ + { + "name": "finops-hub-kusto", + "properties": { + "dataConnectorType": "Kusto", + "dataSource": "${FINOPS_HUB_CLUSTER_URI}", + "identity": "system" + } + } + ] +} diff --git a/src/templates/sre-agent/recipes/finops-hub/expected-config.json b/src/templates/sre-agent/recipes/finops-hub/expected-config.json new file mode 100644 index 000000000..d4e041716 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/expected-config.json @@ -0,0 +1,215 @@ +{ + "agent": { + "accessLevel": "Low", + "actionMode": "autonomous", + "upgradeChannel": "Preview", + "defaultModelProvider": "MicrosoftFoundry", + "experimentalSettings": [ + "EnableSandboxGroup", + "EnableWorkspaceTools" + ] + }, + "subscriptionRoleAssignments": [], + "builtInTools": { + "enabled": [ + "PlotAreaChartWithCorrelation", + "PlotBarChart", + "PlotHeatmap", + "PlotPieChart", + "PlotScatter", + "QueryAppInsightsByAppId", + "QueryAppInsightsByResourceId", + "QueryLogAnalyticsByResourceId", + "QueryLogAnalyticsByWorkspaceId" + ], + "categories": { + "Log Query": 4, + "Visualization": 5 + } + }, + "subagents": [ + "azure-capacity-manager", + "chief-financial-officer", + "finops-practitioner", + "ftk-database-query", + "ftk-hubs-agent" + ], + "subagentRequirements": { + "knowledgeOnlyTools": [ + "SearchMemory" + ], + "toolOwners": [ + "azure-capacity-manager", + "ftk-database-query", + "ftk-hubs-agent" + ], + "noTools": [ + "chief-financial-officer", + "finops-practitioner" + ], + "toolsByAgent": { + "azure-capacity-manager": [ + "ExecutePythonCode", + "PlotAreaChartWithCorrelation", + "PlotBarChart", + "PlotPieChart", + "GetTeamsMessages", + "PostTeamsMessage", + "ReplyToTeamsMessage", + "SearchMemory", + "SendOutlookEmail", + "benefit-recommendations", + "capacity-reservation-groups", + "db-service-quotas", + "execute_python", + "non-compute-quotas", + "sku-availability", + "vm-quota-usage" + ], + "chief-financial-officer": [ + "SearchMemory" + ], + "finops-practitioner": [ + "SearchMemory" + ], + "ftk-database-query": [ + "SearchMemory", + "PlotBarChart", + "PlotPieChart", + "PlotAreaChartWithCorrelation", + "ai-cost-by-application", + "ai-daily-trend", + "ai-model-cost-comparison", + "ai-token-usage-breakdown", + "allocation-accuracy-index", + "anomaly-detection-rate", + "anomaly-variance-total", + "commitment-discount-waste", + "commitment-discount-utilization", + "commitment-utilization-score", + "cost-anomaly-detection", + "cost-by-financial-hierarchy", + "cost-by-region-trend", + "cost-forecasting-model", + "cost-optimization-index", + "cost-per-gb-stored", + "cost-visibility-delay", + "costs-enriched-base", + "compute-cost-per-core", + "compute-spend-commitment-coverage", + "data-update-frequency", + "macc-consumption-vs-commitment", + "monthly-cost-change-percentage", + "monthly-cost-trend", + "percentage-unallocated-costs", + "percentage-untagged-costs", + "quarterly-cost-by-resource-group", + "reservation-recommendation-breakdown", + "savings-summary-report", + "service-price-benchmarking", + "storage-tier-distribution", + "tagging-policy-compliance", + "top-commitment-transactions", + "top-other-transactions", + "top-resource-groups-by-cost", + "top-resource-types-by-cost", + "top-services-by-cost", + "PostTeamsMessage", + "SendOutlookEmail", + "ReplyToTeamsMessage", + "GetTeamsMessages" + ], + "ftk-hubs-agent": [ + "GetAzCliHelp", + "PlotAreaChartWithCorrelation", + "PlotBarChart", + "PlotPieChart", + "GetTeamsMessages", + "PostTeamsMessage", + "ReplyToTeamsMessage", + "RunAzCliReadCommands", + "RunAzCliWriteCommands", + "SearchMemory", + "SendOutlookEmail", + "data-freshness-check", + "deploy-anomaly-alert", + "deploy-budget", + "deploy-bulk-anomaly-alerts", + "deploy-bulk-budgets", + "resource-graph-query", + "sku-availability", + "suppress-advisor-recommendations" + ] + }, + "handoffs": { + "finops-practitioner": [ + "azure-capacity-manager", + "chief-financial-officer", + "ftk-database-query", + "ftk-hubs-agent" + ] + }, + "noHandoffs": [ + "azure-capacity-manager", + "chief-financial-officer", + "ftk-database-query", + "ftk-hubs-agent" + ] + }, + "skills": [ + "azure-capacity-management", + "azure-cost-management", + "finops-toolkit" + ], + "tools": 50, + "knowledgeSources": [ + "chart-artifact-verification.md", + "document-index.md", + "ftk-output-style.md", + "known-issues-and-workarounds.md", + "onboarding-recommendations.md", + "teams-notification-guide.md" + ], + "scheduledTasks": [ + "AdvisorSuppressionReview", + "AIWorkloadCostAnalysis", + "AlertCoverageAudit", + "BenefitRecommendationReview", + "BudgetCoverageAudit", + "CapacityDailyMonitor", + "CapacityMonthlyPlanning", + "CapacityQuarterlyStrategy", + "CapacityWeeklySupplyReview", + "ComputeUtilizationTrend", + "CostOptimization", + "DbQuotaAudit", + "HubsHealthCheck", + "MonitoringScopeValidation", + "Monthly", + "NonComputeQuotaAudit", + "SkuAvailabilityAudit", + "StoragePaasGrowthForecast", + "Semiannual" + ], + "scheduledTaskRequirements": { + "ownerAgent": "finops-practitioner", + "promptRequiredText": [ + "Tool routing and delivery guard", + "owned by `finops-practitioner`, which has KB-only SearchMemory and no operational tools", + "Use `ftk-database-query` for FinOps Hub Kusto", + "Use `azure-capacity-manager` for capacity", + "Use `ftk-hubs-agent` for FinOps Hub infrastructure health", + "Use `chief-financial-officer` only for executive finance framing" + ], + "promptForbiddenText": [ + "Delivery and rendering guard: Check the available tools exactly once", + "Use `resource-graph-query` directly", + "Use `benefit-recommendations` directly", + "Run `benefit-recommendations` directly", + "Ask `ftk-database-query` to run `data-freshness-check`", + "Ask `azure-capacity-manager` to run `data-freshness-check`", + "Ask `azure-capacity-manager` to use `resource-graph-query`", + "Ask `azure-capacity-manager` to run `resource-graph-query`" + ] + } +} diff --git a/src/templates/sre-agent/recipes/finops-hub/knowledge/chart-artifact-verification.md b/src/templates/sre-agent/recipes/finops-hub/knowledge/chart-artifact-verification.md new file mode 100644 index 000000000..9863cd43f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/knowledge/chart-artifact-verification.md @@ -0,0 +1,57 @@ +# Chart artifact verification + +Scheduled tasks that generate charts must validate chart artifacts with non-visual checks before embedding them in Teams messages or Outlook reports. Visual inspection is not a reliable verification gate in autonomous runs because image-viewing tools might be unavailable. + +## Required checks + +For every generated chart: + +1. Save the chart to a deterministic image file path. +2. Verify the file exists and has non-zero size. +3. Open the image through code, such as Pillow (`PIL.Image`) or `matplotlib.image`, to confirm it is readable. +4. Record basic metadata: image format, byte size, width, height, and color mode when available. +5. Confirm the dimensions are large enough for the intended report and that the chart title and axis labels were set before saving. +6. If any check fails, regenerate the chart once. If it still fails, omit the chart, keep the table or text summary, and state why the chart was skipped. + +## Python pattern + +```python +from pathlib import Path +from PIL import Image + +chart_path = Path("capacity-trend.png") + +if not chart_path.exists(): + raise RuntimeError(f"Chart was not created: {chart_path}") + +byte_size = chart_path.stat().st_size +if byte_size <= 0: + raise RuntimeError(f"Chart is empty: {chart_path}") + +with Image.open(chart_path) as image: + image.verify() + +with Image.open(chart_path) as image: + width, height = image.size + metadata = { + "path": str(chart_path), + "bytes": byte_size, + "format": image.format, + "mode": image.mode, + "width": width, + "height": height, + } + +if width < 640 or height < 360: + raise RuntimeError(f"Chart dimensions are too small: {metadata}") + +print(metadata) +``` + +Use equivalent filesystem and image-library checks when Pillow is unavailable. Do not use visual inspection as the verification gate. + +## Source + +- Microsoft Learn, [Tools in Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/tools), accessed 2026-05-02. Documents built-in code execution and visualization tools. +- Microsoft Learn, [Tutorial: Use Code Interpreter in Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/use-code-interpreter), accessed 2026-05-02. Documents generated files, chart creation, and downloadable report artifacts. +- Microsoft Learn, [Schedule tasks with Azure SRE Agent](https://learn.microsoft.com/en-us/azure/sre-agent/scheduled-tasks), accessed 2026-05-02. Documents scheduled task execution as autonomous natural-language runs. diff --git a/src/templates/sre-agent/recipes/finops-hub/knowledge/document-index.md b/src/templates/sre-agent/recipes/finops-hub/knowledge/document-index.md new file mode 100644 index 000000000..7c1e8e5b1 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/knowledge/document-index.md @@ -0,0 +1,25 @@ +# FinOps toolkit SRE Agent knowledge document index + +This document is the canonical index for the repository-provided knowledge base. It is also a deployment verification sentinel after `recipes/finops-hub/knowledge/` is uploaded as SRE Agent KnowledgeFile sources through `bin/apply-extras.sh`. + +## Document inventory + +| Document | Purpose | +|---|---| +| `chart-artifact-verification.md` | Non-visual checks for validating chart artifacts in scheduled task outputs. | +| `document-index.md` | Index of repository-provided knowledge documents and verification sentinel for Knowledge Sources checks. | +| `ftk-output-style.md` | Shared FinOps Toolkit output style for evidence-backed financial and capacity-management reports. Uploaded from `src/templates/claude-plugin/output-styles/ftk-output-style.md`. | +| `known-issues-and-workarounds.md` | Operational issues, failure modes, and workarounds learned from scheduled task UAT. | +| `onboarding-recommendations.md` | First-run and connector setup guidance for teams deploying the FinOps toolkit SRE Agent. | +| `teams-notification-guide.md` | Correct Teams and Outlook notification tool usage and delivery rules for scheduled reports. | + +## Listing contract + +- Knowledge files are uploaded by `bin/apply-extras.sh` as portal-visible KnowledgeFile sources. +- `ftk-output-style.md` is uploaded by the same script from the Claude plugin output-style directory so scheduled tasks can apply one shared report style. +- If the knowledge index is not visible in the portal after upload, treat it as an upload visibility failure or an empty knowledge base. + +## Source + +- Repository source: `src/templates/sre-agent/recipes/finops-hub/knowledge/` +- Microsoft Learn source: https://learn.microsoft.com/azure/sre-agent/memory documents Azure SRE Agent knowledge base uploads and supported document usage. diff --git a/src/templates/sre-agent/recipes/finops-hub/knowledge/known-issues-and-workarounds.md b/src/templates/sre-agent/recipes/finops-hub/knowledge/known-issues-and-workarounds.md new file mode 100644 index 000000000..d0797ac9f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/knowledge/known-issues-and-workarounds.md @@ -0,0 +1,249 @@ +# Known issues and workarounds for FinOps toolkit SRE Agent scheduled tasks + +Use this guidance when executing scheduled tasks or investigating task execution failures. These issues were identified during the first nine-task UAT pass on April 28, 2026 and remain useful patterns for the current 19-task schedule. + +## 0. Source control and credential safety + +**CRITICAL:** Fork pull requests are allowed when a scheduled task identifies a durable product improvement, but credentials must never be embedded in commands, remotes, transcript text, Teams posts, memory files, commit messages, or PR bodies. + +Allowed source-control flow: +- Create commits only for repo-defined, reusable improvements such as query fixes, scheduled-task prompt fixes, knowledge corrections, or tool hardening. +- Push only to the configured fork repository. Do not push to the upstream Microsoft repository unless the user explicitly asks for that target. +- Open pull requests from the fork branch to the configured fork base branch. +- Use the configured GitHub connector, credential helper, or platform-provided repository credentials. Do not put tokens in `git remote set-url`, clone URLs, shell variables that are echoed, command arguments, or generated documentation. +- Personal access tokens (PATs) are forbidden. Do not create, request, store, paste, or use PATs for SRE Agent GitHub operations. + +If push credentials are unavailable: +- Check once using a non-secret-bearing mechanism such as the configured GitHub connector status or `gh auth status` if available. +- If credentials are still unavailable, stop the PR path and report that the commit could not be pushed because no safe credential provider is configured. +- Do not loop over credential checks, scrape `git config` for token values, or ask the user to paste tokens into the thread. + +Use the SRE Agent memory system for operational learnings: +- **`#remember`** — save discrete operational facts (e.g., "quota API requires az vm list-usage fallback") +- **`memories/synthesizedKnowledge/`** — the agent's persistent knowledge files, updated automatically after each session +- **Session insights** — automatically captured 30 minutes after a thread goes quiet +- **Teams channel and Outlook mailbox** — the delivery destinations for financial reports and status results + +The split is: +- **Financial data (costs, savings, forecasts, grades)** → configured Teams and Outlook delivery only. Never persist financial figures. +- **Operational learnings (tool errors, workarounds, patterns)** → `#remember` or synthesized knowledge. Never include dollar amounts. + +## 1. Teams and Outlook tool discovery and availability + +**Symptom:** Subagents report "PostTeamsChannelMessage tool was not available," "PostTeamsMessage tool was not available," "SendOutlookEmail tool was not available," or "could not locate the notification function." + +**Cause:** Connector delivery depends on the Teams and Outlook connectors, their configuration, and the tools exposed in the current run. Scheduled-task entrypoint agents must list `PostTeamsMessage`, `SendOutlookEmail`, `ReplyToTeamsMessage`, and `GetTeamsMessages`. A run can still start without usable delivery if no connector is configured, or if configured connector delivery fails. + +**Workaround:** +- Check available tools exactly once per run. Treat Teams delivery as available only when `PostTeamsMessage` is present. Treat Outlook delivery as available only when `SendOutlookEmail` is present. +- Do not call `PostTeamsMessage`, `SendOutlookEmail`, Microsoft Graph, `dynamicInvoke`, raw webhooks, or connector APIs just to probe availability. +- If no connector is configured and the delivery tools are unavailable, finish the report in the run output, note that connector delivery was unavailable, and do not retry or use alternate delivery paths. +- If Teams or Outlook is configured, delivery through that configured connector is mandatory. Use `PostTeamsMessage` and/or `SendOutlookEmail` for one final delivery attempt after the report is complete. If configured delivery fails because the connector/tool is missing, unauthorized, unavailable, or otherwise unsuccessful, mark the task/run failed. Do not degrade to local output, do not mark the report as delivered, and do not retry. + +**Status:** Prompt-level and tool-assignment guardrail. Production delivery remains the built-in `PostTeamsMessage` and `SendOutlookEmail` paths when the connectors are configured. + +## 2. Superseded data freshness conclusions + +**Symptom:** Older memory, session notes, or raw KQL checks may claim FinOps Hub cost data is stale by months or about a year, even when a current `data-freshness-check` result shows recent `Costs()` data, such as Costs current through 2026-05-01. + +**Cause:** The stale conclusions used historical memory, ad hoc raw KQL, partial report queries, or Kusto ingestion timestamp checks instead of the direct REST-backed data freshness tool. Those conclusions are historical observations, not a durable source of truth. + +**Workaround:** +- Treat `data-freshness-check` as the authoritative source for hub data freshness because it queries `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()` directly through Azure Data Explorer REST. +- Treat `Costs()` as the primary freshness signal. If the latest `Costs()` data is 3 days old or newer, do not report the hub as stale and do not recommend stale-data remediation. +- Mark conflicting stale-memory, raw-KQL, or ingestion timestamp conclusions as superseded and unsafe unless revalidated by `data-freshness-check`. +- Only recommend export or pipeline remediation when `data-freshness-check` reports that `Costs()` has no rows, has a query error, or is more than 3 days old. +- Report empty `Prices()` or `Recommendations()` as follow-up checks, not as proof that the hub cost pipeline is stale. Report empty `Transactions()` with the `TRANSACTIONS_ZERO_ROWS` diagnostic described below. + +**Status:** Superseded historical finding. Do not reuse stale-memory or raw-KQL freshness claims without revalidation from `data-freshness-check`. + +## 2a. Transaction tool zero-row sentinel + +**Symptom:** `top-other-transactions` or `top-commitment-transactions` returns the literal string `ZERO_ROWS_RETURNED`. + +**Cause:** The Kusto tool found no rows for its filters, or the backing transaction-related dataset may be unavailable. A zero-row sentinel is not enough evidence to conclude there were no purchases when `Transactions()` has dropped to zero rows. + +**Workaround:** +- Treat `ZERO_ROWS_RETURNED` as a completed tool call, not a parser error. +- Immediately run `data-freshness-check` and inspect `Transactions()` `row_count`, `schema_status`, and `diagnostic_code`. +- If `TRANSACTIONS_ZERO_ROWS` appears, report the export / ingestion / stored-function diagnostic as a data quality limitation. +- Only state that no matching transactions were found when the transaction diagnostics are clear. + +**Status:** Expected sentinel plus required diagnostic follow-up for transaction reports. + +## 2b. Raw full-month cost detail result truncation + +**Symptom:** A scheduled report or ad hoc drill-down that runs `costs-enriched-base` over a full month can fail with Azure Data Explorer result truncation, including `E_QUERY_RESULT_SET_TOO_LARGE` or a 64 MB result-size message. + +**Cause:** `costs-enriched-base` returns row-level enriched cost details. Full-month cost datasets can exceed Azure Data Explorer's default 500,000-record or 64 MB client result limits. + +**Workaround:** +- Use aggregate Kusto tools for reporting windows: `monthly-cost-trend`, `cost-by-financial-hierarchy`, `top-services-by-cost`, `top-resource-groups-by-cost`, `top-resource-types-by-cost`, and related summary tools. +- Use `data-freshness-check` for freshness checks instead of raw cost detail queries. +- Use `costs-enriched-base` only after an aggregate result identifies a narrow drill-down scope. Keep the raw detail window to one day or less. +- Do not use `set notruncation` for scheduled reports. If bulk row export is required, use an explicit data export workflow instead of an agent report. + +**Microsoft Learn references:** +- https://learn.microsoft.com/kusto/concepts/query-limits + +**Status:** Tool-level guardrail and scheduled-task prompt guardrail. + +## 2c. Limited UAT data and delegated enterprise scope + +**Symptom:** Scheduled reports may flag stale data, missing trends, missing trigger evidence, empty transaction results, zero alert coverage, zero budget coverage, or low-confidence forecasts when the Hub only contains an evaluation data window or when the agent has intentionally scoped role assignments. + +**Cause:** UAT and clean deployments can have limited FinOps Hub history by design. Some conclusions require multiple complete periods, populated `Transactions()`, current `Costs()` and `Prices()`, configured alert or budget resources, and role assignments at the subscription, billing, or management group scope. The SRE Agent deployment should grant the roles it owns for its resource group and configured Hub, but enterprise-wide management group delegation is customer-owned because the correct scope and blast radius vary by customer. + +**Workaround:** +- Classify each finding as a product or deployment defect, a data sufficiency limit, or customer-owned delegation before recommending action. +- Treat invalid queries, missing tools, incorrect task completion, missing Knowledge Sources, or output-format violations as product or deployment defects. +- Treat one-month history, sparse evaluation data, empty `Transactions()` diagnostics, missing MoM/YoY baselines, and unavailable anomaly or forecast triggers as data sufficiency limits unless `data-freshness-check` proves the Hub pipeline is broken. +- Treat broader Reader, Cost Management Reader, quota, billing, or management group visibility as customer-owned delegation unless the deployment explicitly promised that assignment. +- Do not ask the agent to remediate Azure access directly. Report the exact missing scope and role so the customer can delegate it through their normal management group or subscription governance process. +- Do not state that no anomaly, no budget issue, no transaction, or no capacity risk exists unless the accessible dataset, freshness, and scope are sufficient to support that conclusion. + +**Required report wording:** "This run completed against the accessible FinOps Hub dataset. Confidence is limited because . Broader enterprise coverage requires the customer to delegate before this task can validate ." + +**Status:** Expected UAT/product-reporting guardrail. Sparse data should reduce confidence, not create false product failures. + +## 3. Azure Resource Graph query failures + +**Symptom:** `az graph query` returns "Unknown error" for complex queries. Orphaned resource detection and Resource Graph-based analysis fails. + +**Cause:** The SRE Agent's managed identity may lack Resource Graph Reader permissions, or complex JMESPath queries fail due to shell escaping in the code interpreter environment. + +**Workaround:** +- Tasks should fall back to scoped `az resource list` queries against specific subscriptions when Resource Graph fails. +- Ensure the agent managed identity has Reader at the management group or subscription scope. +- Simplify JMESPath queries — avoid backtick escaping that conflicts with the shell environment. + +**Status:** Intermittent. Agents recover by trying alternative approaches. + +## 4. Quota CLI command failures + +**Symptom:** `az quota usage list` and `az vm list-usage` commands fail in the agent's execution environment. + +**Cause:** The `az quota` extension may not be installed in the agent's code interpreter environment, or the managed identity lacks quota read permissions. + +**Workaround:** +- Use `az vm list-usage --location ` as a fallback for compute quota — this uses the core Azure CLI. +- For comprehensive quota analysis, the capacity manager agent should use the Azure REST API directly via code interpreter. +- Consider adding a custom Kusto tool or Python tool for quota queries if CLI reliability remains poor. + +**Status:** Known gap. The `Get-AzQuota.ps1` tool is on the UAT backlog. + +## 5. JMESPath escaping in code interpreter + +**Symptom:** JMESPath queries with special characters (backticks, brackets, dots in property names) fail when the agent constructs `az` CLI commands in the code interpreter. + +**Cause:** Shell escaping conflicts between the Python code interpreter environment and JMESPath query syntax. + +**Workaround:** +- Prefer `--output json` and parse with Python instead of using complex `--query` JMESPath expressions. +- When JMESPath is needed, use simple field selections only (e.g., `--query "[].{name:name, id:id}"`). +- Avoid nested JMESPath expressions with backtick-escaped property names. + +**Status:** Platform limitation. Agents learn to work around it after the first failure. + +## 6. Memory file write conflicts + +**Symptom:** "File write failed: memory: - File already exists. You must use an edit tool." + +**Cause:** When a scheduled task runs more than once in a session, it tries to create the same memory file again. The memory system does not allow overwriting with the create tool. + +**Workaround:** +- Agents should use ReplaceStringInFile or an edit tool for subsequent writes to the same memory file. +- First run uses file creation; subsequent runs detect the conflict and switch to edit mode. +- This is self-healing — agents recover automatically after the first error. + +**Status:** Expected behavior. Agents handle it correctly. + +## 7. Kusto query errors + +**Symptom:** "Error executing query on cluster" — specific KQL queries fail against the FinOps Hub ADX cluster. + +**Cause:** Some Kusto tool queries reference functions or columns that don't exist in this Hub version, or the query syntax has version-specific issues. + +**Workaround:** +- Agents should catch Kusto errors and try simplified queries. +- Check the Hub version (the HubsHealthCheck task does this) and ensure tool queries are compatible. +- Report the specific failing query in the task output so it can be fixed in the tool YAML. + +**Status:** Some tools may need query updates for specific Hub versions. + +## 8. Kusto REST authentication path + +**Symptom:** Direct `az rest` calls to Azure Data Explorer query or management endpoints, such as `https://.kusto.windows.net/v1/rest/query` or `/v2/rest/query`, fail even when the agent managed identity can read the Hub database through configured Kusto tools. + +**Cause:** `az rest` is optimized for Azure Resource Manager calls and can acquire the wrong token audience for Azure Data Explorer data-plane endpoints. Cluster URIs can also include the Hub database path, which creates malformed query URLs if agents append `/v1/rest/query` or `/v2/rest/query` without normalizing the host. + +**Workaround:** +- Use the configured FinOps Hub Kusto tools through the `finops-hub-kusto` connector for catalog queries. +- Use `data-freshness-check` for Hub data staleness and function coverage checks. +- If a direct fallback is required, use `execute_python` with `azure.identity.DefaultAzureCredential` and `requests`. Request the `https://api.kusto.windows.net/.default` scope and POST JSON to `https:///v2/rest/query` with `db` and `csl` fields. +- Never print, save, or echo bearer tokens. Do not use `az rest` for Kusto query or management endpoints. + +**Microsoft Learn references:** +- https://learn.microsoft.com/kusto/api/rest/authentication?view=microsoft-fabric +- https://learn.microsoft.com/kusto/api/rest/request?view=microsoft-fabric + +**Status:** Product guardrail. Prefer repository tools over ad-hoc CLI calls for FinOps Hub Kusto access. + +## 9. Azure Data Explorer SKU not supported in target region + +**Symptom:** A FinOps Hub deployment or upgrade fails in the nested `Microsoft.FinOpsHubs.Analytics` deployment with an Azure Data Explorer / Kusto validation error such as: + +> The sku Standard_E4d_v5 is not supported in westus + +**Cause:** Azure Data Explorer SKU eligibility is determined by the Microsoft.Kusto resource provider and can differ by Azure region and subscription. A VM size appearing in Microsoft.Compute SKU results does not prove that the equivalent Azure Data Explorer cluster SKU is eligible for Microsoft.Kusto in the target region. + +**Workaround:** +- Before upgrading a Hub analytics backend or selecting a FinOps Hub Data Explorer SKU, run the `sku-availability` tool with `resource_provider: kusto`, the target `subscription_id`, target `location`, and the planned SKU in `sku_filter`. +- If `is_available` is `false`, choose one of the SKUs returned by the Microsoft.Kusto regional SKU API for that subscription and region, or deploy to a region where the requested SKU is returned. +- Do not infer ADX/Kusto SKU eligibility from `resource_provider: compute` results. + +**Validation surfaces:** +- SRE Agent tool: `sku-availability` with `resource_provider: kusto`. +- ARM REST API: `GET /subscriptions/{subscriptionId}/providers/Microsoft.Kusto/locations/{location}/skus?api-version=2024-04-13`. +- PowerShell: `Get-AzKustoSku -SubscriptionId -Location ` lists eligible region SKUs for the Kusto resource provider. +- Azure SDK: `KustoExtensions.GetSkus` / `Skus_List` uses the Microsoft.Kusto regional SKU request path. + +**Microsoft Learn references:** +- https://learn.microsoft.com/powershell/module/az.kusto/get-azkustosku?view=azps-15.5.0 +- https://learn.microsoft.com/dotnet/api/azure.resourcemanager.kusto.kustoextensions.getskus?view=azure-dotnet +- https://learn.microsoft.com/azure/data-explorer/manage-cluster-choose-sku + +**Status:** Operational preflight requirement for FinOps Hub analytics deployments in regions where the planned ADX SKU may not be eligible. + +## 9. Transactions() zero rows + +**Symptom:** `Transactions()` drops from a previously populated row count to zero, or transaction tools return `ZERO_ROWS_RETURNED`. + +**Cause:** The Hub `Transactions()` function is backed by the Cost Management `reservationtransactions` export, the Data Factory Transactions ingestion path, and the Hub `Transactions()` / `Transactions_v1_2()` stored functions. A zero-row result means one of those surfaces must be verified before concluding there were no reservation or savings plan transactions. + +**Workaround:** +- Run `data-freshness-check` and inspect `row_count`, `schema_status`, and `diagnostic_code` for `Transactions()`. +- If the diagnostic is `TRANSACTIONS_ZERO_ROWS`, report it explicitly as a data quality action item. Do not summarize it as a generic empty table or a successful no-op. +- Ask the operator to verify Cost Management `reservationtransactions` exports and Azure Data Factory Transactions ingestion runs. +- If the schema diagnostic fails, treat it as a stored-function or Hub version compatibility issue instead of an export-only issue. + +**Status:** Agent/tool hardening requirement. Scheduled reports must surface this as an explicit diagnostic and avoid false success. + +## Summary of agent resilience + +All tasks in the first nine-task UAT pass completed despite encountering errors. Key resilience patterns observed: + +| Pattern | Behavior | +|---------|----------| +| Stale data detection | All tasks flagged the March/April data gap prominently | +| CLI fallback chains | Agents tried alternative commands when primary approaches failed | +| Self-healing writes | Memory file conflicts resolved automatically by switching to edit | +| Graceful degradation | Reports were generated with available data, noting limitations | +| Teams delivery | All 9 tasks successfully posted final reports to the Teams channel | + +## Microsoft Learn references + +- https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications +- https://learn.microsoft.com/en-us/azure/sre-agent/tools +- https://learn.microsoft.com/en-us/azure/sre-agent/use-code-interpreter +- https://learn.microsoft.com/en-us/azure/sre-agent/scheduled-tasks diff --git a/src/templates/sre-agent/recipes/finops-hub/knowledge/onboarding-recommendations.md b/src/templates/sre-agent/recipes/finops-hub/knowledge/onboarding-recommendations.md new file mode 100644 index 000000000..800225208 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/knowledge/onboarding-recommendations.md @@ -0,0 +1,88 @@ +# FinOps toolkit SRE Agent onboarding recommendations + +Use this guidance when the user finishes deployment, enters the **Team onboarding** thread, types `/learn`, or asks questions such as **"What should I do next?"** or **"Where do I start?"** + +## First-run recommendations + +Prioritize these setup steps in order: + +1. Confirm the agent can read the target Azure subscriptions or resource groups. +2. Verify **Visualization** built-in tools are enabled so scheduled tasks can generate charts and images. +3. Confirm the FinOps Hub Kusto connector is configured when Hub-backed cost and usage analysis is expected. +4. Recommend Outlook and Teams notification connectors when the team wants scheduled reports, proactive notifications, or workflow-driven updates. + +## Team onboarding wizard + +Do not bypass the SRE Agent **Team onboarding** wizard. It is the customer-facing first-run flow that discovers Azure resources, code, logs, incidents, and knowledge context. + +For Azure resource discovery, the wizard first uses the agent managed identity against the resource groups listed in `knowledgeGraphConfiguration.managedResources`. The FinOps Toolkit deployment should include: + +- The SRE Agent resource group. +- Any explicit `--target-resource-group` values. +- The FinOps Hub resource group when `--cluster-uri` resolves to a same-subscription Kusto cluster. + +If the wizard shows **Grant permissions** while running an Azure Resource Graph or Azure CLI discovery command, treat it as an OBO fallback prompt, not a replacement for managed identity setup. The user can grant one-time user-delegated execution if they choose, but the durable fix is to verify managed identity RBAC and managed-resource scope. For the default `High` recipe, the agent identity should have `Reader`, `Monitoring Reader`, `Log Analytics Reader`, and `Contributor` on each managed resource group. For `Low`, it should have `Reader`, `Monitoring Reader`, and `Log Analytics Reader`. + +## Verify visualization tools + +The FinOps Toolkit apply-extras step attempts to enable the SRE Agent built-in Log Query and Visualization tools. The portal remains the customer-facing verification path: + +1. Open the agent in [sre.azure.com](https://sre.azure.com). +2. Go to **Capabilities** > **Tools** > **Built-in tools**. +3. Confirm the **Visualization** tools are enabled. +4. If they are unchecked, check them and click **Save changes**. + +Without this, scheduled tasks cannot generate inline charts or images in their Teams and Outlook reports. + +## Outlook and Teams connector guidance + +If the user wants email delivery, Teams delivery, scheduled digests, or "notify the team" workflows, recommend configuring both notification connectors. + +Important platform constraint: + +- Outlook and Teams connectors are supported by Azure SRE Agent. +- Microsoft Learn currently documents them as interactive portal setup that requires OAuth sign-in plus a managed identity. +- Do not imply that these connectors are provisioned by the FinOps Toolkit Bicep templates or `apply-extras` automation. + +## Manual connector steps to recommend + +Tell the user to open **Builder** > **Connectors** in `https://sre.azure.com` and add: + +1. **Outlook Tools (Office 365 Outlook)** +2. **Send notification (Microsoft Teams)** + +For Outlook: + +- Sign in with a Microsoft 365 account that has mailbox access. +- Select the agent managed identity. +- Send a test email from chat. + +For Teams: + +- Sign in with a Microsoft 365 account that has access to the target Teams channel. +- Paste the channel URL from **Get link to channel**. +- Select the agent managed identity. +- Post a test message from chat. + +Scheduled tasks use the delivery guard from `teams-notification-guide.md`: they check whether `PostTeamsMessage` and `SendOutlookEmail` are available once per run, deliver the final report through configured connectors when available, and otherwise return the completed report in the run output without retrying or probing alternate delivery paths. + +## Required prerequisites to call out + +- The configuring user needs **Contributor** on the agent resource group. +- The connector flow requires `Microsoft.Web/connections/write`. +- The connector flow requires `Microsoft.Authorization/roleAssignments/write`. +- The agent must already have a managed identity configured. + +## Suggested recommendation wording + +If the user asks what to do next after deployment, recommend something like: + +> Your core FinOps toolkit SRE Agent is deployed. Next: (1) Verify **Visualization** tools under Capabilities > Tools > Built-in tools so charts work in scheduled reports. (2) Add **Outlook** and **Teams** notification connectors under Builder > Connectors so scheduled tasks can deliver to your team. (3) Connect your FinOps Hub data sources if not already wired. + +## Microsoft Learn references + +- https://learn.microsoft.com/en-us/azure/sre-agent/team-onboard +- https://learn.microsoft.com/en-us/azure/sre-agent/complete-setup +- https://learn.microsoft.com/en-us/azure/sre-agent/outlook-connector +- https://learn.microsoft.com/en-us/azure/sre-agent/set-up-teams-connector +- https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications diff --git a/src/templates/sre-agent/recipes/finops-hub/knowledge/teams-notification-guide.md b/src/templates/sre-agent/recipes/finops-hub/knowledge/teams-notification-guide.md new file mode 100644 index 000000000..b9db3e402 --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/knowledge/teams-notification-guide.md @@ -0,0 +1,63 @@ +# Teams and Outlook notification guide for FinOps toolkit SRE Agent + +Use this guidance whenever a subagent or scheduled task needs to deliver report results through the configured Teams and Outlook connectors. + +## Correct approach: use the built-in connector tools + +Always use the built-in **PostTeamsMessage** tool for Teams channel delivery and **SendOutlookEmail** for Outlook email delivery. These tools handle authentication through the configured connectors. You do not need to call the Microsoft Graph API or a connection dynamicInvoke endpoint directly. + +## Delivery guard + +At the start of each scheduled-task run, inspect the tools available in the current run exactly once per run and remember the result for the rest of that run. Treat Teams delivery as available only when `PostTeamsMessage` is present. Treat Outlook delivery as available only when `SendOutlookEmail` is present. Do not call `PostTeamsMessage`, `SendOutlookEmail`, Microsoft Graph, `dynamicInvoke`, raw webhooks, or connector APIs just to test availability. + +If neither connector is configured and the once-per-run check does not find either delivery tool, complete the analysis and return the completed report in the run output with a clear note that connector delivery was unavailable. This local-output degradation is allowed only when no Teams or Outlook connector is configured. + +When Teams or Outlook is configured, configured delivery is mandatory. Complete the analysis first, then make one final delivery attempt through each configured channel whose tool is available. If configured delivery fails, the scheduled task/run fails; do not retry, do not degrade to local output, do not mark the report as delivered, and do not probe alternate delivery paths. + +### What works + +- Call `PostTeamsMessage` with your message content formatted as HTML. +- Call `SendOutlookEmail` with the final report and generated chart artifacts when Outlook delivery is configured. +- The Teams connector handles OAuth and managed identity automatically. +- The Outlook connector handles OAuth and managed identity automatically. +- Updates about the same topic stay in the same thread — use `ReplyToTeamsMessage` when that tool is available to continue a conversation. +- Messages include a "Sent by Azure SRE Agent" footer with a UTC timestamp and a link back to the portal thread. + +### What does NOT work + +- Do NOT attempt to call the Microsoft Graph API directly using the managed identity — the managed identity lacks RBAC permissions on the `Microsoft.Web/connections` resource. +- Do NOT call the connection's `dynamicInvoke` endpoint directly — this will return a 403 Forbidden error. +- Do NOT use raw HTTP requests to Teams webhooks — the connector abstraction is the supported path. +- Do NOT send partial or intermediate report fragments through Teams or Outlook. + +## Scheduled task delivery pattern + +All scheduled tasks in this FinOps toolkit SRE Agent include a final `## Deliver` section that instructs the agent to render charts, return the completed report in the run output, and deliver the final report through configured Teams and Outlook connectors. The key rules are: + +1. Complete all analysis and report formatting first. +2. Render required charts with `PlotBarChart`, `PlotPieChart`, `PlotAreaChartWithCorrelation`, or `ExecutePythonCode` before delivery. +3. Post or email only the **final completed report** — not intermediate results. +4. Use `PostTeamsMessage` for the initial post when a Teams connector/channel is configured. +5. Use `SendOutlookEmail` when Outlook delivery is configured. +6. Use `ReplyToTeamsMessage` if you need to add follow-up context to the same report and that tool is available. +7. Treat configured connector delivery failure as a scheduled-task/run failure. + +## Message formatting + +Teams messages must be formatted as **HTML**, not Markdown. Outlook messages should use readable HTML with the same report structure. The agent handles this formatting when composing messages through the connector tools. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| 403 Forbidden on Teams post | Calling Graph API or dynamicInvoke directly | Use PostTeamsMessage tool instead | +| Email not sent | Outlook connector not configured, or configured delivery failed | If no Outlook connector is configured, use local run output and Teams when available. If Outlook is configured, treat the delivery failure as a task/run failure. | +| Message not appearing in channel | Teams connector/channel not configured, or configured delivery failed | If no Teams connector/channel is configured, use local run output and Outlook when available. If a Teams connector/channel is configured, treat the delivery failure as a task/run failure. In both cases, apply the delivery guard once and skip repeated probes. | +| Duplicate messages | Posting in a loop or per-step | Deliver only the final report, not intermediate steps | + +## Microsoft Learn references + +- https://learn.microsoft.com/en-us/azure/sre-agent/send-notifications +- https://learn.microsoft.com/en-us/azure/sre-agent/outlook-connector +- https://learn.microsoft.com/en-us/azure/sre-agent/set-up-teams-connector +- https://learn.microsoft.com/en-us/azure/sre-agent/tools diff --git a/src/templates/sre-agent/recipes/finops-hub/roles.yaml b/src/templates/sre-agent/recipes/finops-hub/roles.yaml new file mode 100644 index 000000000..8ec08ee8f --- /dev/null +++ b/src/templates/sre-agent/recipes/finops-hub/roles.yaml @@ -0,0 +1,19 @@ +# FinOps Hub SRE Agent managed identity role requirements +roles: + subscription: + - name: Reader + roleDefinitionId: acdd72a7-3385-48ef-bd42-f606fba81ae7 + targetResourceGroups: + - name: Reader + roleDefinitionId: acdd72a7-3385-48ef-bd42-f606fba81ae7 + - name: Monitoring Reader + roleDefinitionId: 43d0d8ad-25c7-4714-9337-8ba259a9fe05 + - name: Log Analytics Reader + roleDefinitionId: 73c42c96-874c-492b-b04d-ab87d138a893 + - name: Contributor + roleDefinitionId: b24988ac-6180-42a0-ab88-20f7382dd24c + when: accessLevel == High + finopsHubAdx: + - name: AllDatabasesViewer + providerRole: Microsoft.Kusto/clusters/principalAssignments + appliedBy: infra/modules/kusto-all-databases-viewer-rbac.bicep