finops toolkit
40 TopicsRight‑sizing Azure Savings Plans, one hour at a time
Most of us have, at some point, sized a commitment the same way we order food for a team offsite: estimate, round up, and hope nobody complains. It works for pizza. It is a less robust strategy for a three‑year Savings Plan. Azure Cost Management already produces commitment recommendations for you, and they are a great starting point. But the headline dollar figure on the portal hides the most useful signal a FinOps team can get its hands on: the hourly Pay‑As‑You‑Go (PAYG) usage that the recommendation engine actually looked at. Once you have that signal in a spreadsheet, "right‑sized" stops being a vibe and starts being a number you can defend in a steering meeting. This post walks through how to pull that data out of the Cost Management REST API, and shares a small companion PowerShell script that does the heavy lifting and produces analyst‑friendly CSV and Markdown outputs. Why a single recommended number isn't enough A Savings Plan recommendation is, at its core, an optimisation against a distribution of hourly compute spend over a lookback window. The portal typically surfaces the single commitment level that maximises savings under a "reasonable utilisation" assumption. That's a sensible default, but it answers only one question. The questions FinOps practitioners also want to answer include: What does my PAYG demand actually look like hour‑by‑hour? Smooth and predictable, or spiky and seasonal? How does coverage and savings change if I commit a bit less, or a bit more? Will the recommended commitment leave me with wastage on weekends or overnight? Can I correlate the hourly profile with maintenance windows, batch jobs, or business hours? You cannot answer those questions with a single dollar figure. You can answer all of them with the hourly series and the alternative commitment levels — and the API exposes both. What the Benefit Recommendations API gives you The endpoint is the Benefit Recommendations - List operation (api-version=2025-03-01). Pointed at any of the supported scopes — Billing Account (EA), Billing Profile (MCA), Subscription, or Resource Group — it returns one or more recommendations for the chosen lookBackPeriod (Last7Days, Last30Days, Last60Days) and term (P1Y, P3Y). The two $expand options are where the magic lives: $expand=properties/allRecommendationDetails returns an array of alternative commitment levels (AllSavingsBenefitDetails). Each entry includes commitmentAmount, coveragePercentage, benefitCost, overageCost, savingsAmount, savingsPercentage, wastageCost, and averageUtilizationPercentage. $expand=properties/usage returns the hourly PAYG charge series (usage.charges) that the engine used. Combined with properties.firstConsumptionDate and properties.lastConsumptionDate, you get a complete time‑aligned view of demand. Both expansions are cheap and worth requesting every time. They turn a single recommendation into a small, complete data product. Meet the script: Export-BenefitRecommendations.ps1 To make this useful in five minutes rather than an afternoon, there is a PowerShell script on GitHub that wraps the API call and produces ready‑to‑use artifacts. It is interactive, so there is nothing to memorise: === Azure Benefit Recommendations Export === Select billing scope kind: * [1] Billing Account (EA) [2] Billing Profile (MCA) [3] Subscription [4] Resource Group Enter choice 1-4 (default 1): 1 Billing Account ID: <your-EA-billing-account-id> Select lookback period: [1] Last7Days * [2] Last30Days [3] Last60Days Enter choice 1-3 (default 2): 3 Select term: * [1] P1Y [2] P3Y Enter choice 1-2 (default 1): 2 ... Retrieved 1 recommendation(s). It authenticates via the Az.Accounts module, always requests both $expand options, follows nextLink pagination, verifies API access up front (translating 401/403/404 into actionable hints), and writes four files using a predictable name pattern: yyyy-MM-dd-<scope>-<lookback>-<term>-AllRecommendationDetails.csv — one row per commitment alternative, ready for pivot tables. yyyy-MM-dd-<scope>-<lookback>-<term>-TopRecommendation.md — a Markdown brief on the top recommendation, including the recommended commitment row and the full alternatives table. yyyy-MM-dd-<scope>-<lookback>-<term>-HourlyUsage.csv — hour‑by‑hour PAYG usage for the top recommendation, aligned to the API's actual data window (no padding, no trailing blanks). yyyy-MM-dd-<scope>-<lookback>-<term>-Raw.json — optional full response dump, for archival or downstream tooling. The naming convention is intentional: paste a folder of these into Power BI and the dates, scopes, and terms are already in the filename. A worked example Here is a real shape from a sample Last60Days / P3Y export against an EA billing account. The AllRecommendationDetails CSV gives us four commitment alternatives: averageUtilizationPercentage,coveragePercentage,commitmentAmount,overageCost,benefitCost,savingsAmount,savingsPercentage,totalCost,wastageCost 100.000,28.868,4.124,30914.280,5889.072,6656.889,15.317,36803.352,0.000 100.000,39.168,5.848,26437.791,8350.944,8671.506,19.953,34788.735,0.000 9.984,97.883,16.359,919.948,23360.652,19179.641,44.131,24280.600,3.767 98.684,98.900,16.806,477.956,23998.968,18983.317,43.680,24476.924,315.907 Read those rows like an interactive lever: Commit $4.124/hr → 28.9% coverage, 15.3% savings, zero wastage. Safe, but most of your spend is still PAYG. Commit $5.848/hr → 39.2% coverage, 19.9% savings. Still safe, still conservative. Commit $16.359/hr → 97.9% coverage, 44.1% savings, ~$3.77 of wastage. This is the sweet spot the engine flags as recommended. Commit $16.806/hr → 98.9% coverage, 43.7% savings, but wastage jumps to $315.9 over the window. You bought more coverage and the engine charged you for it. The hourly CSV is what makes the recommendation defensible. It contains 1,428 rows — one per hour from 2026-04-11T00:00 through 2026-06-09T11:00 (the API's reported data range, not "now minus 60 days"): DateTime,HourlyPayGoUsage 2026-04-11T00:00,29.5350191832771 2026-04-11T01:00,29.566694701744 ... 2026-06-09T11:00,32.7312920192344 Drop that into Excel or Power BI, lay a horizontal line at the recommended commitmentAmount ($16.359/hr), and you immediately see how often demand sits above the line (covered by PAYG overage) versus below it (where the commitment is technically idle but, given the savings rate, still net‑positive). You can also slice by hour‑of‑day to spot weekend troughs or batch‑job spikes that change how you interpret "average utilisation." Try it yourself The script and full documentation live at github.com/DirkBrinkmann/azure-savingsplan-insights. Quickstart: git clone https://github.com/DirkBrinkmann/azure-savingsplan-insights.git cd azure-savingsplan-insights Install-Module Az.Accounts -Scope CurrentUser # if you don't already have it #Connect to Azure Connect-AzAccount #Run the script .\Export-BenefitRecommendations.ps1 You will need read permission on the scope you target — Cost Management Reader on a subscription, or Billing Reader at the billing account / billing profile level, is enough. Give it a spin against your own scope and tell us how it goes. The repo at github.com/DirkBrinkmann/azure-savingsplan-insights has Issues and Discussions enabled — bug reports, edge cases ("my response shape looks different"), feature ideas, and "I plotted this in Power BI and learned X" stories are all welcome. The script will only get better with real‑world data behind it, and yours is the kind we want to hear about. Closing nudge In FinOps terms, this is squarely an Inform phase activity: turning a black‑box recommendation into a transparent dataset your team can challenge, chart, and align on. The API has had this data all along; the script just makes it boring to extract — which is exactly what you want a FinOps tool to be. Pull a Last60Days / P3Y export for your top‑spending scope, plot the hourly line, and decide your next Savings Plan with a number you can point to. Your future self (and your finance partner) will thank you.774Views2likes2CommentsWhat's new in FinOps toolkit 14 – April 2026
FinOps toolkit 14 is here: Copilot Studio agent template for your FinOps hub instance, Azure Advisor and customer cost recommendations, a simpler deployment UI, and a preview of commitment discount eligibility data for reservation and savings plan planning, just to name a few!1.4KViews1like0CommentsWhat’s new in FinOps toolkit 13 – January 2026
FinOps toolkit 13 focuses on stability and usability improvements across FinOps hubs, Power BI reports, and Azure Optimization Engine with a few key additions, like configuring Key Vault purge protection when deploying FinOps hubs and support for format and compression when creating Cost Management exports via PowerShell. Read on for details.1.3KViews2likes0CommentsWhat’s new in FinOps toolkit 12 – July 2025
This month, you’ll find support for FOCUS 1.2, autostart in FinOps hubs which can reduce your hub costs, a new page in the Cost summary Power BI report, and various small fixes, improvements, and documentation updates across the board. Read on for details.1.3KViews3likes5CommentsNews and updates from FinOps X 2025: Transforming FinOps in the era of AI
AI is central to nearly every corner of business and technology. It's no surprise that FinOps X 2025 was loaded with news about how AI is changing everything. Learn about how FinOps tools and solutions are evolving in the era of AI!1.6KViews0likes0CommentsA practitioner's guide to accelerating FinOps with GitHub Copilot and FinOps hubs
ℹ️ Quick implementation overview Setup time: ~30 minutes for basic configuration Target audience: FinOps practitioners, finance teams, engineering managers Prerequisites: Azure subscription with FinOps hubs deployed, VS Code, GitHub Copilot Key enabler: FinOps Hub Copilot v0.11 release Key benefits 🎯 Democratized analytics Non-technical team members can perform advanced cost analysis without KQL expertise. ⚡ Faster insights Natural language eliminates query writing overhead and accelerates time-to-insights. 📋 FinOps Framework alignment All queries map directly to validated FinOps Framework capabilities. 🔒 Enterprise ready Built on proven FinOps hub data foundation with security and governance controls. FinOps practitioners face a common challenge: bridging the gap between complex cost data and actionable business insights. While FinOps hubs provide a comprehensive, analytics-ready foundation aligned with the FinOps Framework, accessing and analyzing this data traditionally requires deep technical expertise in KQL and schema knowledge. This guide demonstrates how to perform sophisticated cost analysis using natural language queries using GitHub Copilot in VS Code connected to FinOps hubs 0.11 via the Azure MCP server. This approach democratizes advanced analytics across FinOps teams, supporting faster decision-making and broader organizational adoption of FinOps practices. ℹ️ Understanding the technology stack The Model Context Protocol (MCP) is an open standard that enables AI agents to securely connect to external data sources and tools. The Azure MCP server is Microsoft's implementation that provides this connectivity specifically for Azure resources, while GitHub Copilot acts as the AI agent that translates your natural language questions into the appropriate technical queries. Understanding the foundation: FinOps hubs and natural language integration FinOps hubs serve as the centralized data platform for cloud cost management, providing unified cost and usage data across clouds, accounts, and tenants. The integration with GitHub Copilot through the Azure MCP server introduces a natural language interface that maps practitioner questions directly to validated KQL queries, eliminating the technical barrier that often limits FinOps analysis to specialized team members. Note: The FinOps toolkit also includes Power BI reports, workbooks, alerts, and an optimization engine for advanced analytics and automation. See the FinOps toolkit overview for the full set of capabilities. Key capabilities and technical foundation ℹ️ About the FinOps toolkit ecosystem The FinOps toolkit also includes Power BI reports, workbooks, and an optimization engine for advanced analytics and automation. See the FinOps toolkit overview for the full set of capabilities. FinOps hubs provide several critical capabilities that enable practitioner success: 📊 Data foundation Centralized cost and usage data across multiple cloud providers, billing accounts, and organizational units Native alignment with the FinOps Framework domains and FOCUS specification Analytics-ready data model optimized for performance at scale without complexity overhead 🔗 Integration capabilities Multiple access patterns: Power BI integration, Microsoft Fabric compatibility, and direct KQL access for advanced scenarios Natural language query interface through Azure MCP server integration with Copilot ⚙️ Technical architecture The Azure MCP server acts as the translation layer, implementing the open Model Context Protocol to enable secure communication between AI agents (like GitHub Copilot) and Azure resources. For FinOps scenarios, it specifically provides natural language access to Azure Data Explorer databases containing FinOps hubs data, converting practitioner questions into validated KQL queries while maintaining enterprise authentication and security standards. Mapping FinOps Framework capabilities to natural language queries The integration supports the complete spectrum of FinOps Framework capabilities through natural language interfaces. Each query type maps to specific Framework domains and validated analytical patterns: 💡 Quick reference Each prompt category leverages pre-validated queries from the FinOps hubs query catalog, ensuring consistent, accurate results across different practitioners and use cases. 🔍 Understand phase capabilities Capability Natural language example Business value Cost allocation and accountability "Show me cost allocation by team for Q1" Instant breakdown supporting chargeback discussions Anomaly detection and management "Find any cost anomalies in the last 30 days" Proactive identification of budget risks Reporting and analytics "What are our top resource types by spend?" Data-driven optimization focus areas ⚡ Optimize phase capabilities Capability Natural language example Business value Rate optimization "How much did we save with reservations last month?" Quantification of commitment discount value Workload optimization "Show me underutilized resources" Resource efficiency identification Governance enforcement "Show me resources without proper tags" Policy compliance gaps 📈 Operate phase capabilities Capability Natural language example Business value Forecasting and planning "Forecast next quarter's cloud costs" Proactive budget planning support Performance tracking "Show month-over-month cost trends" Operational efficiency measurement Business value quantification "Calculate our effective savings rate" ROI demonstration for stakeholders Practical implementation: Real-world scenarios and results The following examples demonstrate how natural language queries translate to actionable FinOps insights. Each scenario includes the business context, Framework alignment, query approach, and interpretable results to illustrate the practical value of this integration. ℹ️ Sample data notation All cost figures, dates, and resource names in the following examples are illustrative and provided for demonstration purposes. Actual results will vary based on your organization's Azure usage, billing structure, and FinOps hub configuration. Effective cost allocation and accountability FinOps Framework alignment Domain: Understand usage and cost Capabilities: Allocation, Reporting and analytics Business context Finance teams require accurate cost allocation data to support budget planning and accountability discussions across organizational units. Natural language query What are the top resource groups by cost last month? Query results and business impact The natural language prompt maps to a validated allocation query that aggregates effective cost by resource group, providing the foundational data for chargeback and showback processes. Resource group Effective cost haven $36,972.85 leap $15,613.96 ahbtest $6,824.54 vnet-hub-001 $1,560.13 ... ... 🎯 Key takeaway Natural language queries eliminate the need for complex KQL knowledge while maintaining data accuracy. Finance teams can now perform sophisticated cost allocation analysis without technical barriers. Learn more: Introduction to cost allocation Proactive cost anomaly detection and management FinOps Framework alignment Domain: Understand usage and cost Capabilities: Anomaly management, Reporting and analytics Business context Proactive anomaly detection enables rapid response to unexpected cost changes, supporting budget adherence and operational efficiency. Natural language query Are there any unusual cost spikes or anomalies in the last 12 months? Query results and business impact The system applies time series analysis to identify significant cost deviations, automatically calculating percentage changes and flagging potential anomalies for investigation. Date Daily cost % change vs previous day 2025-06-03 $971.36 -59.54% 2025-06-01 $2,370.16 -4.38% 2025-04-30 $2,302.10 -5.56% 2025-04-02 $2,458.45 +5.79% ... ... ... ⚠️ Warning: Analysis insight The 59% cost reduction on June 3rd indicates a significant operational change, such as workload migration or resource decommissioning, requiring validation to ensure expected behavior. 🎯 Key takeaway Automated anomaly detection enables proactive cost management by identifying unusual spending patterns before they impact budgets, supporting rapid response to operational changes. Learn more: Anomaly management Accurate financial forecasting and budget planning FinOps Framework alignment Domain: Quantify business value Capabilities: Forecasting, Planning and estimating Business context Accurate financial forecasting supports budget planning processes and enables proactive capacity and cost management decisions. Natural language query Forecast total cloud cost for the next 90 days based on the last 12 months. Query results and business impact The forecasting algorithm analyzes historical spending patterns and applies trend analysis to project future costs, providing both daily estimates and aggregate totals for planning purposes. Date Forecasted cost 2025-06-04 $2,401.61 2025-07-01 $2,401.61 2025-08-01 $2,401.61 2025-09-01 $2,401.61 ... ... Total forecasted 90-day spend: $216,145.24 🎯 Key takeaway Natural language forecasting queries provide accurate financial projections based on validated historical analysis, enabling confident budget planning without requiring data science expertise. Learn more: Forecasting Reporting and analytics capabilities FinOps Framework alignment Domain: Understand usage and cost Capabilities: Reporting and analytics Business context Executive reporting requires consistent, reliable cost trend analysis to support strategic decision-making and budget performance tracking. Natural language query Show monthly billed and effective cost trends for the last 12 months. Query results and business impact Month Billed cost Effective cost 2024-06 $46,066.39 $46,773.85 2024-07 $72,951.41 $74,004.08 2024-08 $73,300.31 $74,401.81 2024-09 $71,886.30 $72,951.26 ... ... ... Learn more: Reporting and analytics Resource optimization analysis FinOps Framework alignment Domain: Optimize usage and cost Capabilities: Workload optimization, Reporting and analytics Business context Prioritizing optimization efforts requires understanding which resource types drive the most cost, enabling focused improvement initiatives with maximum business impact. Natural language query What are the top resource types by cost last month? Query results and business impact Resource type Effective cost Fabric Capacity $34,283.52 Virtual machine scale set $15,155.59 SQL database $2,582.99 Virtual machine $2,484.34 ... ... Learn more: Workload optimization Implementation methodology This section provides a systematic approach to implementing natural language FinOps analysis using the technical foundation established above. Prerequisites and environment validation Before proceeding with implementation, ensure you have: ✅ Azure subscription with appropriate FinOps hub deployment permissions ✅ Node.js runtime environment (required by Azure MCP Server) ✅ Visual Studio Code with GitHub Copilot extension ✅ Azure CLI, Azure PowerShell, or Azure Developer CLI authentication configured Access validation methodology Step 1: Verify FinOps hub deployment Confirm hub deployment status and data ingestion through the FinOps hubs setup guide Step 2: Validate database access Test connectivity to the hub database using Azure Data Explorer web application or Azure portal Step 3: Confirm schema availability Verify core functions (Costs, Prices) and databases (Hub, Ingestion) are accessible with current data Expected Database Structure Hub database: Public-facing functions including Costs, Prices, and version-specific functions (e.g., Costs_v1_0) Ingestion database: Raw data tables, configuration settings (HubSettings, HubScopes), and open data tables (PricingUnits) FOCUS-aligned data: All datasets conform to FinOps Open Cost and Usage Specification standards Learn more: FinOps hubs template details Azure MCP server configuration ℹ️ What is Azure MCP Server? The Azure Model Context Protocol (MCP) server is a Microsoft-provided implementation that enables AI agents and clients to interact with Azure resources through natural language commands. It implements the open Model Context Protocol standard to provide secure, structured access to Azure services including Azure Data Explorer (FinOps hub databases). Key capabilities and service support The Azure MCP server provides comprehensive Azure service integration, particularly relevant for FinOps analysis: 🔍 FinOps-relevant services Azure Data Explorer: Execute KQL queries against FinOps hub databases Azure Monitor: Query logs and metrics for cost analysis Resource groups: List and analyze organizational cost structures Subscription management: Access subscription-level cost data 🔧 Additional Azure services Azure Storage, Cosmos DB, Key Vault, Service Bus, and 10+ other services Full list available in the Azure MCP Server tools documentation Installation methodology The Azure MCP Server is available as an NPM package and VS Code extension. For FinOps scenarios, we recommend the VS Code extension approach for seamless integration with GitHub Copilot. Option 1: VS Code extension (recommended) Install the Azure MCP server extension from VS Code Marketplace The extension automatically configures the server in your VS Code settings Open GitHub Copilot and activate Agent Mode to access Azure tools Option 2: Manual configuration Add the following to your MCP client configuration: { "servers": { "Azure MCP Server": { "command": "npx", "args": ["-y", "@azure/mcp@latest", "server", "start"] } } } Authentication requirements Azure MCP Server uses Entra ID through the Azure Identity library, following Azure authentication best practices. It supports: Azure CLI: az login (recommended for development) Azure PowerShell: Connect-AzAccount Azure Developer CLI: azd auth login Managed identity: For production deployments The server uses DefaultAzureCredential and automatically discovers the best available authentication method for your environment. Technical validation steps Step 1: Authentication verification Confirm successful login to supported Azure tools Step 2: Resource discovery Validate MCP Server can access your Azure subscription and FinOps hub resources Step 3: Database connectivity Test query execution against FinOps hub databases Integration with development environment VS Code configuration requirements: GitHub Copilot extension with Agent Mode capability Azure MCP Server installation and configuration FinOps hubs copilot instructions and configuration files The FinOps Hub Copilot v0.11 release provides pre-configured GitHub Copilot instructions specifically tuned for FinOps analysis. This release includes: AI agent instructions optimized for FinOps Framework capabilities GitHub Copilot configuration files for VS Code Agent Mode Validated query patterns mapped to common FinOps scenarios Azure MCP Server integration guides for connecting to FinOps hub data Verification methodology: Open Copilot Chat interface (Ctrl+Shift+I / Cmd+Shift+I) Activate Agent Mode and select tools icon to verify Azure MCP Server availability Execute connectivity test: "What Azure resources do I have access to?" Expected response validation: Successful authentication confirmation Azure subscription and resource enumeration FinOps hub database connectivity status Progressive query validation Foundational test queries: Complexity level Validation query Expected behavior Basic "Show me total cost for last month" Single aggregate value with currency formatting Intermediate "What are my top 10 resource groups by cost?" Tabular results with proper ranking Advanced "Find any costs over $1000 in the last week" Filtered results with anomaly identification Query execution validation: KQL translation accuracy against FinOps hub schema Result set formatting and data type handling Error handling and user feedback mechanisms Operational best practices for enterprise implementation Query optimization and performance considerations Data volume management: Implement temporal filtering to prevent timeout scenarios (Azure Data Explorer 64MB result limit) Use summarization functions for large datasets rather than detailed row-level analysis Apply resource-level filters when analyzing specific environments or subscriptions Schema consistency validation: Reference the FinOps hub database guide for authoritative column definitions Verify data freshness through ingestion timestamp validation Validate currency normalization across multi-subscription environments Query pattern optimization: Leverage the FinOps hub query catalog for validated analytical patterns Customize costs-enriched-base query foundation for organization-specific requirements Implement proper time zone handling for global operational environments Security and access management Authentication patterns: Utilize Azure CLI integrated authentication for development environments Implement service principal authentication for production automation scenarios Maintain principle of least privilege for database access permissions Data governance considerations: Ensure compliance with organizational data classification policies Implement appropriate logging for cost analysis queries and results Validate that natural language prompts don't inadvertently expose sensitive financial data Comprehensive query patterns by analytical domain The following reference provides validated natural language prompts mapped to specific FinOps Framework capabilities and proven KQL implementations. Technical note: Each pattern references validated queries from the FinOps hub query catalog. Verify schema compatibility using the FinOps hub database guide before implementation. Cost visibility and allocation patterns Analytical requirement FinOps Framework alignment Validated natural language query Executive cost trend reporting Reporting and analytics "Show monthly billed and effective cost trends for the last 12 months." Resource group cost ranking Allocation "What are the top resource groups by cost last month?" Quarterly financial reporting Allocation / Reporting and analytics "Show quarterly cost by resource group for the last 3 quarters." Service-level cost analysis Reporting and analytics "Which Azure services drove the most cost last month?" Organizational cost allocation Allocation / Reporting and analytics "Show cost allocation by team and product for last quarter." Optimization and efficiency patterns Analytical requirement FinOps Framework alignment Validated natural language query Resource optimization prioritization Workload optimization "What are the top resource types by cost last month?" Commitment discount analysis Rate optimization "Show reservation recommendations and break-even analysis for our environment." Underutilized resource identification Workload optimization "Find resources with low utilization that could be optimized or decommissioned." Savings plan effectiveness Rate optimization "How much did we save with savings plans compared to pay-as-you-go pricing?" Tag compliance monitoring Data ingestion "Show me resources without required cost center tags." Anomaly detection and monitoring patterns Analytical requirement FinOps Framework alignment Validated natural language query Cost spike identification Anomaly management "Find any unusual cost spikes or anomalies in the last 30 days." Budget variance analysis Budgeting "Show actual vs. budgeted costs by resource group this quarter." Trending analysis Reporting and analytics "Identify resources with consistently increasing costs over the last 6 months." Threshold monitoring Anomaly management "Alert me to any single resources costing more than $5,000 monthly." Governance and compliance patterns Analytical Requirement FinOps Framework Alignment Validated Natural Language Query Policy compliance validation Policy and governance "Show resources that don't comply with our tagging policies." Approved service usage Policy and governance "List any non-approved services being used across our subscriptions." Regional compliance monitoring Policy and governance "Verify all resources are deployed in approved regions only." Cost center accountability Invoicing and chargeback "Generate chargeback reports by cost center for last quarter." Key takeaway: These validated query patterns provide a comprehensive foundation for FinOps analysis across all Framework capabilities. Use them as templates and customize for your organization's specific requirements. Troubleshooting and optimization guidance Common query performance issues ⚠️ Warning: Performance considerations Azure Data Explorer has a 64MB result limit by default. Proper query optimization avoids timeouts and ensures reliable performance. If using Power BI, use DirectQuery to connect to your data. Large dataset timeouts Symptom: Queries failing with timeout errors on large datasets Solution: Add temporal filters ✅ Recommended: "Show costs for last 30 days" ❌ Avoid: "Show all costs" Framework alignment: Data ingestion Memory limit exceptions Symptom: Exceeding Azure Data Explorer 64MB result limit Solution: Use aggregation functions ✅ Recommended: "Summarize costs by month" ❌ Avoid: Daily granular data for large time periods Best practice: Implement progressive drill-down from summary to detail Schema validation errors Symptom: Queries returning empty results or unexpected columns Solution: Verify hub schema version compatibility using the database guide Validation: Test with known queries from the query catalog Query optimization best practices Temporal filtering ✅ Recommended: "Show monthly costs for Q1 2025" ❌ Avoid: "Show all historical costs by day" Aggregation-first approach ✅ Recommended: "Top 10 resource groups by cost" ❌ Avoid: "All resources with individual costs" Multi-subscription handling ✅ Recommended: "Costs by subscription for production environment" ❌ Avoid: "All costs across all subscriptions without filtering" Conclusion The integration of FinOps hubs with natural language querying through GitHub Copilot and Azure MCP Server represents a transformative advancement in cloud financial management accessibility. By eliminating technical barriers traditionally associated with cost analysis, this approach enables broader organizational adoption of FinOps practices while maintaining analytical rigor and data accuracy. Key takeaways for implementation success Foundation building Start with the basics: Ensure robust FinOps hub deployment with clean, consistent data ingestion Validate authentication and connectivity before advancing to complex scenarios Begin with basic queries and progressively increase complexity as team familiarity grows Business value focus Align with organizational needs: Align query patterns with organizational FinOps maturity and immediate business needs Prioritize use cases that demonstrate clear ROI and operational efficiency gains Establish feedback loops with finance and business stakeholders to refine analytical approaches Scale and governance planning Design for enterprise success: Implement appropriate access controls and data governance from the beginning Design query patterns that perform well at organizational scale Establish monitoring and alerting for cost anomalies and policy compliance Future considerations As natural language interfaces continue to evolve, organizations should prepare for enhanced capabilities including: 🔮 Advanced analytics Multi-modal analysis: Integration of cost data with performance metrics, compliance reports, and business KPIs Predictive analytics: Advanced forecasting and scenario modeling through conversational interfaces 🤖 Automated intelligence Automated optimization: Natural language-driven resource rightsizing and commitment recommendations Cross-platform intelligence: Unified analysis across cloud providers, SaaS platforms, and on-premises infrastructure The democratization of FinOps analytics through natural language interfaces positions organizations to make faster, more informed decisions about cloud investments while fostering a culture of cost consciousness across all teams. Success with this integration requires both technical implementation excellence and organizational change management to maximize adoption and business impact. Learn more about the FinOps toolkit and stay updated on new capabilities at the FinOps toolkit website.2.2KViews5likes2CommentsGetting started with FinOps hubs: Multicloud cost reporting with Azure and Google Cloud
Microsoft’s FinOps hubs offer a powerful and trusted foundation for managing, analyzing, and optimizing cloud costs. Built on Azure Data Explorer (ADX) and Azure Data Lake Storage (ADLS), FinOps hubs provide a scalable platform to unify billing data across providers leveraging FOCUS datasets. In this post, we’ll take a hands-on technical walkthrough of how to connect Microsoft FinOps hubs to Google Cloud, enabling you to export and analyze Google Cloud billing data with Azure billing data directly within your FinOps hub instance. This walk through will focus on using only storage in the hub for accessing data and is designed to get you started and understand multicloud connection and reporting from FinOps hubs. For large datasets Azure Data Explorer or Microsoft Fabric is recommended. With the introduction of the FinOps Open Cost and Usage Specification (FOCUS), normalizing billing data and reporting it through a single-pane-of-glass experience has never been easier. As a long-time contributor to the FOCUS working group, I’ve spent the past few years helping define standards to make multicloud reporting simpler and more actionable. FOCUS enables side-by-side views of cost and usage data—such as compute hours across cloud providers—helping organizations make better decisions around workload placement and right-sizing. Before FOCUS, this kind of unified analysis was incredibly challenging due to the differing data models used by each provider. To complete this technical walk through, you’ll need access to both Azure and Google Cloud—and approximately 2 to 3 hours of your time. Getting started: The basics you’ll need Before diving in, ensure you have the following prerequisites in place: ✅ Access to your Google Cloud billing account. ✅ A Google Cloud project with BigQuery and other APIs enabled and linked to your billing account. ✅ All required IAM roles and permissions for working with BigQuery, Cloud Functions, Storage, and billing data (detailed below). ✅ Detailed billing export and pricing export configured in Google Cloud. ✅ An existing deployment of FinOps hubs. What you’ll be building In this walk-through, you’ll set up Google billing exports and an Azure Data Factory pipeline to fetch the exported data, convert to parquet and ingest into your FinOps hub storage, following the FOCUS 1.0 standard for normalization. Through the process you should end up creating: Configure a BigQuery view to convert detailed billing exports into the FOCUS 1.0 schema. Create a metadata table in BigQuery to track export timestamps to enable incremental data exports reducing file export sizes and avoiding duplicate data. Set up a GCS bucket to export FOCUS-formatted data in CSV format. Deploy a Google Cloud Function that performs incremental exports from BigQuery to GCS. Create a Google Cloud Schedule to automate the export of your billing data to Google Cloud Storage. Build a pipeline in Azure Data Factory to: Fetch the CSV billing exports from Google Cloud Storage. Convert them to Parquet. Ingest the transformed data into your FinOps hub ingestion container. Let's get started. We're going to start in Google Cloud before we jump back into Azure to setup the Data Factory pipeline. Enabling FOCUS exports in Google Prerequisite: Enable detailed billing & pricing exports ⚠️ You cannot complete this guide without billing data enabled in BigQuery if you have not enabled detailed billing exports and pricing exports, do this now and come back to the walk-through in 24 hours. Steps to enabled detailed billing exports and pricing exports: Navigate to Billing > Billing Export. Enable Detailed Cost Export to BigQuery. Select the billing project and a dataset - if you have not created a project for billing do so now. Enable Pricing Export to the same dataset. 🔊 “This enables daily cost and usage data to be streamed to BigQuery for granular analysis.” Detailed guidance and information on billing exports in Google can be found here: Google Billing Exports. What you'll create: Service category table Metadata table FOCUS View - you must have detailed billing export and pricing exports enabled to create this view Cloud Function Cloud Schedule What you'll need An active GCP Billing Account (this is your payment method). A GCP Project (new or existing) linked to your GCP billing account. All required APIs enabled. All required IAM roles enabled. Enable Required APIs: BigQuery API Cloud Billing API Cloud Build API Cloud Scheduler Cloud Functions Cloud Storage Cloud Run Admin Pub/Sub (optional, for event triggers) Required IAM Roles Assign the following roles to your user account: roles/billing.viewer or billing.admin roles/bigquery.dataEditor roles/storage.admin roles/cloudfunctions.admin roles/cloudscheduler.admin roles/iam.serviceAccountTokenCreator roles/cloudfunctions.invoker roles/run.admin (if using Cloud Run) roles/project.editor Create a service account (e.g., svc-bq-focus) Assign the following roles to your service account: roles/bigquery.dataOwner roles/storage.objectAdmin roles/cloudfunctions.invoker roles/cloudscheduler.admin roles/serviceAccount.tokenCreator roles/run.admin Your default compute service account will also require access to run cloud build services. Ensure you apply the cloud build role to your default compute service account in your project, it may look like this: projectID-compute@developer.gserviceaccount.com → roles/cloudbuild.builds.builder Create FOCUS data structure and View This section will create two new tables in Big Query, one for service category mappings and one for metadata related to export times. These are important to get in before we create the FOCUS view to extract billing data in FOCUS format. Create a service category mapping table I removed service category from the original google FOCUS view to reduce the size of the SQL Query, therefore, to ensure we mapped Service category properly, I created a new service category table and joined it to the FOCUS view. In this step we will create a new table using open data to map GCP services to service category. Doing this helps reduce the size of the SQL query and simplifies management of Service Category mapping. Leveraging open source data we can easily update service category mappings if they ever change or new categories are added without impacting the FOCUS view query. Process: Download the latest service_category_mapping.csv from the FOCUS Converter repo Go to BigQuery > Your Dataset > Create Table Upload the CSV Table name: service_category Schema: Auto-detect Create a metadata table This table will be used to track the last time detailed billing data was added to your detailed billing export, we use this to enable incremental exports of billing data through the FOCUS view to ensure we only export the latest set of data and not everything all the time. Process: Go to BigQuery > Create Table Table name: metadata_focus_export Schema: Field Name : Format last_export_time: TIMESTAMP export_message: STRING Enter your field name and then choose field format, do not add : 🔊 “Ensures each export only includes new data since the last timestamp.” Create the FOCUS-aligned view Creating a view in BigQuery allows us to un-nest the detailed billing export tables into the format of FOCUS 1.0*. To use Power BI we must un-nest the tables so this step is required. It also ensures we map the right columns in Google Cloud detailed billing export to the right columns in FOCUS. ⚠️ The FOCUS SQL code provided in this walk-through has been altered from the original Google provided code. I believe this new code is better formatted for FOCUS 1.0 than the original however it does contain some nuances that suit my personal views. Please evaluate this carefully before using this in a production system and adjust the code accordingly to your needs. Steps: Navigate to BigQuery > New Query Paste and update the FOCUS view SQL query which is provided below Replace: yourexporttable with detailed export dataset ID and table name that will look like " yourpricingexporttable with pricing export and table name your_billing_dataset with your detailed export dataset ID and table name FOCUS SQL Query: WITH usage_export AS ( SELECT *, ( SELECT AS STRUCT type, id, full_name, amount, name FROM UNNEST(credits) LIMIT 1 ) AS cud, FROM "Your-Detailed-Billing-Export-ID" -- replace with your detailed usage export table path ), prices AS ( SELECT export_time, sku.id AS sku_id, sku.description AS sku_description, service.id AS service_id, service.description AS service_description, tier.* FROM "your_pricing_export_id", UNNEST(list_price.tiered_rates) AS tier ) SELECT "111111-222222-333333" AS BillingAccountId, "Your Company Name" AS BillingAccountName, COALESCE((SELECT SUM(x.amount) FROM UNNEST(usage_export.credits) x),0) + cost as BilledCost, usage_export.currency AS BillingCurrency, DATETIME(PARSE_DATE("%Y%m", invoice.month)) AS BillingPeriodStart, DATETIME(DATE_SUB(DATE_ADD(PARSE_DATE("%Y%m", invoice.month), INTERVAL 1 MONTH), INTERVAL 1 DAY)) AS BillingPeriodEnd, CASE WHEN usage_export.adjustment_info.type IS NOT NULL and usage_export.adjustment_info.type !='' THEN 'Adjustment' WHEN usage_export.cud.type = 'PROMOTION' AND usage_export.cost_type = 'regular' AND usage_export.cud.id IS NOT NULL THEN 'Credit' WHEN usage_export.sku.description LIKE '%Commitment - 3 years - dollar based VCF%' or usage_export.sku.description LIKE '%Prepay Commitment%' THEN 'Purchase' WHEN usage_export.cud.id IS NOT NULL AND usage_export.cud.id != '' THEN 'Credit' WHEN usage_export.cost_type = 'regular' THEN 'Usage' WHEN usage_export.cost_type = 'tax' THEN 'Tax' WHEN usage_export.cost_type = 'adjustment' THEN 'Adjustment' WHEN usage_export.cost_type = 'rounding_error' THEN 'Adjustment' ELSE usage_export.cost_type END AS ChargeCategory, IF(COALESCE( usage_export.adjustment_info.id, usage_export.adjustment_info.description, usage_export.adjustment_info.type, usage_export.adjustment_info.mode) IS NOT NULL, "correction", NULL) AS ChargeClass, CASE WHEN usage_export.adjustment_info.type IS NOT NULL AND usage_export.adjustment_info.type != '' THEN usage_export.adjustment_info.type ELSE usage_export.sku.description END AS ChargeDescription, CAST(usage_export.usage_start_time AS DATETIME) AS ChargePeriodStart, CAST(usage_export.usage_end_time AS DATETIME) AS ChargePeriodEnd, CASE usage_export.cud.type WHEN "COMMITTED_USAGE_DISCOUNT_DOLLAR_BASE" THEN "Spend" WHEN "COMMITTED_USAGE_DISCOUNT" THEN "Usage" END AS CommitmentDiscountCategory, usage_export.cud.id AS CommitmentDiscountId, COALESCE (usage_export.cud.full_name, usage_export.cud.name) AS CommitmentDiscountName, usage_export.cud.type AS CommitmentDiscountType, CAST(usage_export.usage.amount_in_pricing_units AS numeric) AS ConsumedQuantity, usage_export.usage.pricing_unit AS ConsumedUnit, -- review CAST( CASE WHEN usage_export.cost_type = "regular" THEN usage_export.price.effective_price * usage_export.price.pricing_unit_quantity ELSE 0 END AS NUMERIC ) AS ContractedCost, -- CAST(usage_export.price.effective_price AS numeric) AS ContractedUnitPrice, usage_export.seller_name AS InvoiceIssuerName, COALESCE((SELECT SUM(x.amount) FROM UNNEST(usage_export.credits) x),0) + cost as EffectiveCost, CAST(usage_export.cost_at_list AS numeric) AS ListCost, prices.account_currency_amount AS ListUnitPrice, IF( usage_export.cost_type = "regular", IF( LOWER(usage_export.sku.description) LIKE "commitment%" OR usage_export.cud IS NOT NULL, "committed", "standard"), null) AS PricingCategory, IF(usage_export.cost_type = 'regular', usage_export.price.pricing_unit_quantity, NULL) AS PricingQuantity, IF(usage_export.cost_type = 'regular', usage_export.price.unit, NULL) AS PricingUnit, 'Google'AS ProviderName, usage_export.transaction_type AS PublisherName, usage_export.location.region AS RegionId, usage_export.location.region AS RegionName, usage_export.service.id AS ResourceId, REGEXP_EXTRACT (usage_export.resource.global_name, r'[^/]+$') AS ResourceName, usage_export.sku.description AS ResourceType, COALESCE(servicemapping.string_field_1, 'Other') AS ServiceCategory, usage_export.service.description AS ServiceName, usage_export.sku.id AS SkuId, CONCAT("SKU ID:", usage_export.sku.id, ", Price Tier Start Amount: ", price.tier_start_amount) AS SkuPriceId, usage_export.project.id AS SubAccountId, usage_export.project.name AS SubAccountName, (SELECT CONCAT('{', STRING_AGG(FORMAT('%s:%s', kv.key, kv.value), ', '), '}') FROM ( SELECT key, value FROM UNNEST(usage_export.project.labels) UNION ALL SELECT key, value FROM UNNEST(usage_export.tags) UNION ALL SELECT key, value FROM UNNEST(usage_export.labels) ) AS kv) AS Tags, FORMAT_DATE('%B', PARSE_DATE('%Y%m', invoice.month)) AS Month, usage_export.project.name AS x_ResourceGroupName, CAST(usage_export.export_time AS TIMESTAMP) AS export_time, FROM usage_export LEFT JOIN "Your-Service-Category-Id".ServiceCategory AS servicemapping ON usage_export.service.description = servicemapping.string_field_0 LEFT JOIN prices ON usage_export.sku.id = prices.sku_id AND usage_export.price.tier_start_amount = prices.start_usage_amount AND DATE(usage_export.export_time) = DATE(prices.export_time); 🔊 "This creates a FOCUS-aligned view of your billing data using FOCUS 1.0 specification, this view does not 100% conform to FOCUS 1.0. Create GCS Bucket for CSV Exports Your GCS bucket is where you will place your incremental exports to be exported to Azure. Once your data is exported to Azure you may elect to delete the files in the bucket, the metaata table keeps a record of the last export time. Steps: Go to Cloud Storage > Create Bucket Name: focus-cost-export (or any name you would like) Region: Match your dataset region Storage Class: Nearline (cheaper to use Earline but standard will also work just fine) Enable Interoperability settings > create access/secret key The access key and secret are tied to your user account, if you want to be able to use this with multiple people, create an access key for your service account - recommended for Prod, for this guide purpose, an access key linked to your account is fine. Save the access key and secret to a secure location to use later as part of the ADF pipeline setup. 🔊 “This bucket will store daily CSVs. Consider enabling lifecycle cleanup policies.” Create a Cloud Function for incremental export A cloud function is used here to enable incremental exports of your billing data in FOCUS format on a regular schedule. At present there is no known supported on demand export service from Google, so we came up with this little workaround. The function is designed to evaluate your billing data for last export time that is equals to or greater than the last time the function ran. To do this we look at the export_time column and the metadata table for the last time we ran this. This ensure we only export the most recent billing data which aids in reducing data export costs to Azure. This process is done through the GCP GUI using an inline editor to create the cloud function in the cloud run service. Steps: Go to Cloud Run > Write Function Select > Use Inline editor to create a function Service Name: daily_focus_export Region, the same region as your dataset - in our demo case us-central1 Use settings: Runtime: Python 3.11 (you cannot use anything later than 3.11) Trigger: Optional Authentication: Require Authentication Billing: Request based Service Scaling: Auto-scaling set to 0 Ingress: All Containers: leave all settings as set Volumes: Leave all settings as set Networking: Leave all settings as set Security: Choose the service account you created earlier Save Create main.py file and requirements.txt files through the inline editor For requirements.txt copy and paste the below: functions-framework==3.* google-cloud-bigquery Google-cloud-storage For main.py your function entry point is: export_focus_data Paste the code below into your main.py inline editor window import logging import time import json from google.cloud import bigquery, storage logging.basicConfig(level=logging.INFO) def export_focus_data(request): bq_client = bigquery.Client() storage_client = storage.Client() project_id = "YOUR-ProjectID" dataset = "YOUR-Detailed-Export-Dataset" view = "YOUR-FOCUS-View-Name" metadata_table = "Your-Metadata-Table" job_name = "The name you want to call the job for the export" bucket_base = "gs://<your bucketname>/<your foldername>" bucket_name = "Your Bucket Name" metadata_file_path = "Your-Bucket-name/export_metadata.json" try: logging.info("🔁 Starting incremental export based on export_time...") # Step 1: Get last export_time from metadata metadata_query = f""" SELECT last_export_time FROM `{project_id}.{dataset}.{metadata_table}` WHERE job_name = '{job_name}' LIMIT 1 """ metadata_result = list(bq_client.query(metadata_query).result()) if not metadata_result: return "No metadata row found. Please seed export_metadata.", 400 last_export_time = metadata_result[0].last_export_time logging.info(f"📌 Last export_time from metadata: {last_export_time}") # Step 2: Check for new data check_data_query = f""" SELECT COUNT(*) AS row_count FROM `{project_id}.{dataset}.{view}` WHERE export_time >= TIMESTAMP('{last_export_time}') """ row_count = list(bq_client.query(check_data_query).result())[0].row_count if row_count == 0: logging.info("✅ No new data to export.") return "No new data to export.", 204 # Step 3: Get distinct export months folder_query = f""" SELECT DISTINCT FORMAT_DATETIME('%Y%m', BillingPeriodStart) AS export_month FROM `{project_id}.{dataset}.{view}` WHERE export_time >= TIMESTAMP('{last_export_time}') AND BillingPeriodStart IS NOT NULL """ export_months = [row.export_month for row in bq_client.query(folder_query).result()] logging.info(f"📁 Exporting rows from months: {export_months}") # Step 4: Export data for each month for export_month in export_months: export_path = f"{bucket_base}/{export_month}/export_{int(time.time())}_*.csv" export_query = f""" EXPORT DATA OPTIONS( uri='{export_path}', format='CSV', overwrite=true, header=true, field_delimiter=';' ) AS SELECT * FROM `{project_id}.{dataset}.{view}` WHERE export_time >= TIMESTAMP('{last_export_time}') AND FORMAT_DATETIME('%Y%m', BillingPeriodStart) = '{export_month}' """ bq_client.query(export_query).result() # Step 5: Get latest export_time from exported rows max_export_time_query = f""" SELECT MAX(export_time) AS new_export_time FROM `{project_id}.{dataset}.{view}` WHERE export_time >= TIMESTAMP('{last_export_time}') """ new_export_time = list(bq_client.query(max_export_time_query).result())[0].new_export_time # Step 6: Update BigQuery metadata table update_query = f""" MERGE `{project_id}.{dataset}.{metadata_table}` T USING ( SELECT '{job_name}' AS job_name, TIMESTAMP('{new_export_time}') AS new_export_time ) S ON T.job_name = S.job_name WHEN MATCHED THEN UPDATE SET last_export_time = S.new_export_time WHEN NOT MATCHED THEN INSERT (job_name, last_export_time) VALUES (S.job_name, S.new_export_time) """ bq_client.query(update_query).result() # Step 7: Write metadata JSON to GCS blob = storage_client.bucket(bucket_name).blob(metadata_file_path) blob.upload_from_string( json.dumps({"last_export_time": new_export_time.isoformat()}), content_type="application/json" ) logging.info(f"📄 Metadata file written to GCS: gs://{bucket_name}/{metadata_file_path}") return f"✅ Export complete. Metadata updated to {new_export_time}", 200 except Exception as e: logging.exception("❌ Incremental export failed:") return f"Function failed: {str(e)}", 500 Before saving, update variables like project_id, bucket_name, etc. 🔊 “This function exports new billing data based on the last timestamp in your metadata table.” Test the Cloud Function Deploy and click Test Function Ensure a seed export_metadata.json file is uploaded to your GCS bucket (if you have not uploaded a seed export file the function will not run. Example file is below { "last_export_time": "2024-12-31T23:59:59.000000+00:00" } Confirm new CSVs appear in your target folder Automate with Cloud Scheduler This step will set up an automated schedule to run your cloud function on a daily or hourly pattern, you may adjust the frequency to your desired schedule, this demo uses the same time each day at 11pm. Steps: Go to Cloud Scheduler > Create Job Region: Same region as your Function - This Demo us-central1 Name: daily-focus-export Frequency: 0 */6 * * * (every 6 hours) or Frequency: 0 23 * * * (daily at 11 PM) Time zone: your desired time zone Target: HTTP Auth: OIDC Token → Use service account Click CREATE 🔊 "This step automates the entire pipeline to run daily and keep downstream platforms updated with the latest billing data." Wrapping up Google setup Wow—there was a lot to get through! But now that you’ve successfully enabled FOCUS-formatted exports in Google Cloud, you're ready for the next step: connecting Azure to your Google Cloud Storage bucket and ingesting the data into your FinOps hub instance. This is where everything comes together—enabling unified, multi-cloud reporting in your Hub across both Azure and Google Cloud billing data. Let’s dive into building the Data Factory pipeline and the associated datasets needed to fetch, transform, and load your Google billing exports. Connecting Azure to Google billing data With your Google Cloud billing data now exporting in FOCUS format, the next step is to bring it into your Azure environment for centralized FinOps reporting. Using Data Factory, we'll build a pipeline to fetch the CSVs from your Google Cloud Storage bucket, convert them to Parquet, and land them in your FinOps Hub storage account. Azure access and prerequisites Access to the Azure Portal Access to the resource group your hub has been deployed to Contributor access to your resource group At a minimum storage account contributor and storage blob data owner roles An existing deployment of the Microsoft FinOps Hub Toolkit Admin rights on Azure Data Factory (or at least contributor role on ADF) Services and tools required Make sure the following Azure resources are in place: Azure Data Factory instance A linked Azure Storage Account (this is where FinOps Hub is expecting data) (Optional) Azure Key Vault for secret management Pipeline Overview We’ll be creating the following components in Azure Data Factory Component Purpose GCS Linked Service Connects ADF to your Google Cloud Storage bucket Azure Blob Linked Service Connects ADF to your Hub’s ingestion container Source Dataset Reads CSV files from GCS Sink Dataset Writes Parquet files to Azure Data Lake (or Blob) in Hub's expected format Pipeline Logic Orchestrates the copy activity, format conversion, and metadata updates Secrets and authentication To connect securely from ADF to GCS, you will need: The access key and secret from GCS Interoperability settings (created earlier) Store them securely in Azure Key Vault or in ADF pipeline parameters (if short-lived) 💡 Best Practice Tip: Use Azure Key Vault to securely store GCS access credentials and reference them from ADF linked services. This improves security and manageability over hardcoding values in JSON. Create Google Cloud storage billing data dataset Now that we're in Azure, it's time to connect to your Google Cloud Storage bucket and begin building your Azure Data Factory pipeline. Steps Launch Azure Data Factory Log in to the Azure Portal Navigate to your deployed Azure Data Factory instance Click “Author” from the left-hand menu to open the pipeline editor In the ADF Author pane, click the "+" (plus) icon next to Datasets Select “New dataset” Choose Google Cloud Storage as the data source Select CSV as the file format Set the Dataset Name, for example: gcs_focus_export_dataset Click “+ New” next to the Linked Service dropdown Enter a name like: GCS-Billing-LinkedService Under Authentication Type, choose: Access Key (for Interoperability access key) Or Azure Key Vault (recommended for secure credential storage) Fill in the following fields: Access Key: Your GCS interoperability key Secret: Your GCS interoperability secret Bucket Name: focus-cost-export (Optional) Point to a folder path like: focus-billing-data/ Click Test Connection to validate access Click Create Adjust Dataset Properties Now that the dataset has been created, make the following modifications to ensure proper parsing of the CSVs: SettingValue Column delimiter; (semicolon) Escape character" (double quote) You can find these under the “Connection” and “Schema” tabs of the dataset editor. If your connection fails you may need to enable public access on your GCS bucket or check your firewall restrictions from azure to the internet! Create a Google Cloud Storage metadata dataset To support incremental loading, we need a dedicated dataset to read the export_metadata.json file that your Cloud Function writes to Google Cloud Storage. This dataset will be used by the Lookup activity in your pipeline to get the latest export timestamp from Google. Steps: In the Author pane of ADF, click "+" > Dataset > New dataset Select Google Cloud Storage as the source Choose JSON as the format Click Continue Configure the Dataset Setting Value Name gcs_export_metadata_dataset Linked Service Use your existing GCS linked service File path e.g., focus-cost-export/metadata/export_metadata.json Import schema Set to From connection/store or manually define if needed File pattern Set to single file (not folder) Create the sink dataset – "ingestion_gcp" Now that we’ve connected to Google Cloud Storage and defined the source dataset, it’s time to create the sink dataset. This will land your transformed billing data in Parquet format into your Azure FinOps Hub’s ingestion container. Steps: In Azure Data Factory, go to the Author section Under Datasets, click the "+" (plus) icon and select “New dataset” Choose Azure Data Lake Storage Gen2 (or Blob Storage, depending on your Hub setup) Select Parquet as the file format Click Continue Configure dataset properties Name: ingestion_gcp Linked Service: Select your existing linked service that connects to your FinOps Hub’s storage account. (this will have been created when you deployed the hub) File path: Point to the container and folder path where you want to store the ingested files (e.g., ingestion-gcp/cost/billing-provider/gcp/focus/) Optional Settings: Option Recommended Value Compression type snappy Once configured, click Publish All again to save your new sink dataset. Create the sink dataset – "adls_last_import_metadata" The adls_last_import_metadata dataset (sinks dataset) is the location you use to copy the export time json file from google to azure, this location is sued for the pipeline to check the last time the import of data was run by reading the json file you coped from google and comparing it to the new json file from google Steps: In Azure Data Factory, go to the Author section Under Datasets, click the "+" (plus) icon and select “New dataset” Choose Azure Data Lake Storage Gen2 (or Blob Storage, depending on your Hub setup) Select JSON as the file format Click Continue Configure dataset properties Name: adsl_last_import_metadata Linked Service: Select your existing linked service that connects to your FinOps Hub’s storage account. (this will have been created when you deployed the hub) File path: Point to the container and folder path where you want to store the ingested files (e.g., ingestion/cost/metadata) Build the ADF pipeline for incremental import With your datasets created, the final step is building the pipeline logic that orchestrates the data flow. The goal is to only import newly exported billing data from Google, avoiding duplicates by comparing timestamps from both clouds. In this pipeline, we’ll: Fetch Google’s export timestamp from the JSON metadata file Fetch the last successful import time from the Hub’s metadata file in Azure Compare timestamps to determine whether there is new data to ingest If new data exists: Run the Copy Activity to fetch GCS CSVs, convert to Parquet, and write to the ingestion_gcp container Write an updated metadata file in the hub, with the latest import timestamp Pipeline components: Activity Purpose Lookup - GCS Metadata Reads the last export time from Google's metadata JSON in GCS Lookup - Hub Metadata Reads the last import time from Azure’s metadata JSON in ADLS If Condition Compares timestamps to decide whether to continue with copy Copy Data Transfers files from GCS (CSV) to ADLS (Parquet) Set Variable Captures the latest import timestamp Web/Copy Activity Writes updated import timestamp JSON file to ingestion_gcp container What your pipeline will look like: Step-by-Step Create two lookup activities: GCS Metadata Hub Metadata Lookup1 – GCS Metadata Add an activity called Lookup to your new pipeline Select the Source Dataset: gcs_export_metadata_dataset or whatever you named it earlier This lookup reads the the export_metadata.json file created by your Cloud Function in GCS for the last export time available. Configuration view of lookup for GCS metadata file Lookup2 – Hub Metadata Add an activity called lookup and name it Hub Metadata Select the source dataset: adls_last_import_metadata This lookup reads the last import time data from the hub metadata file to compare it to the last export time from GCS Configuration view of Metadata lookup activity Add conditional logic In this step we will add the condition logic Add activity called If Condition Add the expression below to the condition Go to the Expression tab and paste the following into Dynamic Content: @greater(activity('Lookup1').output.last_export_time, activity('Lookup2').output.last_import_time) Configuration view of If Condition Activity Next Add Copy Activity (If Condition = True) next to True condition select edit and add activity Copy Data Configure the activity with the following details Setting Value Source Dataset gcs_focus_export_dataset (CSV from GCS) Sink Dataset ingestion_gcp (Parquet to ADLS) Merge Files Enabled (reduce file count) Filter Expression @activity('Lookup1').output.firstRow.last_export_time Ensure you add filter expression for filter by last modified. This is important, if you do not add the filter by last modified expression your pipeline will not function properly. Finally we create an activity to update the metadata file in the hub Add another copy activity to your if condition and ensure it is linked to the previous copy activity, this ensure it runs after the import activity is completed. Copy metadata activity settings: Source gcs_export_metadata_dataset Sink adls_last_import_dataset Destination Path ingestion_gcp/metadata/last_import.json This step ensures the next run of your pipeline uses the updated import time to decide whether new data exists. Your pipeline now ingests only new billing data from Google and records each successful import to prevent duplicate processing. Wrapping up – A unified FinOps reporting solution Congratulations — you’ve just built a fully functional multi-cloud FinOps data pipeline using Microsoft’s FinOps Hub Toolkit and Google Cloud billing data, normalized with the FOCUS 1.0 standard. By following this guide, you’ve: ✅ Enabled FOCUS billing exports in Google Cloud using BigQuery, GCS, and Cloud Functions ✅ Created normalized, FOCUS-aligned views to unify your GCP billing data ✅ Automated metadata tracking to support incremental exports ✅ Built an Azure Data Factory pipeline to fetch, transform, and ingest GCP data into your Hub ✅ Established a reliable foundation for centralized, multi-cloud cost reporting This solution brings side-by-side visibility into Azure and Google Cloud costs, enabling informed decision-making, workload optimization, and true multi-cloud FinOps maturity. Next steps 🔁 Schedule the ADF pipeline to run daily or hourly using triggers 📈 Build Power BI dashboards or use templates from the Microsoft FinOps Toolkit visualise unified cloud spend 🧠 Extend to AWS by applying the same principles using the AWS FOCUS export and AWS S3 storage Feedback? Have questions or want to see more deep dives like this? Let me know — or connect with me if you’re working on FinOps, FOCUS, or multi-cloud reporting. This blog is just the beginning of what's possible.3.3KViews4likes1Comment