Citadel AI Governance Hub - Hands-On Lab Guide
Duration: 3 hours
Level: Intermediate
Last updated: June 2026
This hands-on lab deploys a governed AI hub-and-spoke architecture into your Azure subscription:
- Citadel Hub — the shared governance plane centered on API Management, observability, policy enforcement, and core AI platform resources.
- Citadel Spoke — a sample Azure AI Foundry workload that connects to the hub as a governed use case.
- Lab environment — your local machine or the included Devcontainer, where deployment commands, notebooks, and validation traffic originate.
You deploy the hub with azd up, then deploy the sample spoke with workshop/scripts/deploy-spoke-foundry.*.
Lab at a glance
Complete the Core rows first (about 1.5 hours) — setup, deployment, a couple of notebooks, and a first look at the metrics get you a working, governed AI gateway end to end. From there, expand into the Standard rows and the Optional notebooks as time allows. The full lab, including all optional notebooks, takes about 4.5 hours. Use the buttons below to auto-expand the optional notebooks that match the time you have.
| Section | Task | Difficulty | Time |
|---|---|---|---|
| Core | Prerequisites — tools, quota, resource providers | ★☆☆ | 15 min |
| Core | Deploy Citadel — azd up + spoke script |
★★☆ | 45 min |
| Core | Run notebooks 1–2 — backend onboarding + Universal LLM API | ★★☆ | 15 min |
| Core | Observability — APIM Analytics + Metrics | ★☆☆ | 15 min |
| Standard | Review deployed services — APIM, backends, policies | ★★☆ | 30 min |
| Standard | Run notebooks 3–6 — access contracts, agents, PII, unified API | ★★☆ | 45 min |
| Standard | Observability — App Insights + Cosmos DB usage records | ★★☆ | 15 min |
| Optional | Notebook 7 — hosted agent with AGT | ★★★ | 30 min |
| Optional | Notebook 8 — publish and use an A2A endpoint | ★★★ | 25 min |
| Optional | Notebook 9 — publish and use the HR MCP via APIM | ★★★ | 35 min |
Choosing your duration (the buttons above need JavaScript, which isn’t run in GitHub’s file preview) — do the work that fits the time you have:
- Core (1.5 hrs): do the Core rows only — setup, deploy, notebooks 1–2, and a quick look at APIM metrics.
- Core + Standard (3 hrs): also review the deployed services, run notebooks 3–6, and explore App Insights + Cosmos DB.
- Everything (4.5 hrs): also expand and run the optional notebooks 7–9.
⏳ Deployment dominates the clock.
azd uptakes 30–45 minutes on every path. Read ahead through the review and notebook sections while it runs.
Lab overview
In this hands-on lab you will:
- Deploy the Citadel Hub to your own Azure subscription using Azure Developer CLI (
azd) - Deploy a sample Citadel Spoke by running the
workshop/scripts/deploy-spoke-foundry.*script afterazd upcompletes - Review the deployed Azure services and their configuration — focusing on API Management as the unified AI gateway between clients and AI backends
- Run validation notebooks from your lab environment to exercise Citadel capabilities (backend onboarding, access contracts, PII processing, etc.)
- Observe telemetry generated by your notebook runs — explore APIM metrics, Application Insights, Log Analytics, and Cosmos DB usage records
Prerequisites
Complete these steps before the lab day. Before you begin, ensure you have:
- An Azure subscription with Owner (or Contributor plus User Access Administrator) permissions —
azd upcreates managed identities and assigns RBAC roles. - Sufficient Azure OpenAI / AI Foundry model quota (GPT-4.1, DeepSeek-R1, etc.) in your target region.
- The lab tools installed — Azure CLI, Azure Developer CLI (
azd), Python 3.13+, Git, and VS Code — or the included Devcontainer. - Basic familiarity with the Azure services Citadel uses (API Management, AI Foundry, Cosmos DB, Key Vault, Event Hub) and with running Jupyter notebooks.
Note: Detailed install links, the required Azure resource-provider registration, the permission rationale, and the Devcontainer option are in the Lab setup guide. Complete those steps before you begin.
Deploy Citadel to your Azure subscription
Goal: Deploy the Citadel Hub first, then deploy a sample Citadel Spoke that connects to it.
Estimated time: 45 minutes
Clone the Repository
git clone https://github.com/mohamedsaif/ai-hub-gateway-solution-accelerator.git
cd ai-hub-gateway-solution-accelerator
git checkout workshop
Alternative — initialize with azd init
mkdir citadel-workshop && cd citadel-workshop
azd init --template https://github.com/mohamedsaif/ai-hub-gateway-solution-accelerator.git -e citadel-workshop --branch workshop
Authenticate to Azure
# Login to Azure CLI
az login
# Login to Azure Developer CLI (uses the same browser auth)
azd auth login
# Confirm you are on the correct subscription
az account show --output table
If you need to target a specific tenant:
az login --tenant-id <your-tenant-id>
azd auth login --tenant-id <your-tenant-id>
Create a New Environment
azd env new citadel-workshop
Configure Environment (Optional)
This section is optional and can be skipped for the lab. It is provided in case you want to experiment with specific settings.
Optional — configure environment settings
The defaults work well for the lab. However, you may want to set the Azure region:
azd env set AZURE_LOCATION swedencentral
💡 Tip: Choose a region that has Azure OpenAI quota available for your subscription. Common choices:
swedencentral,eastus,eastus2.
To reduce costs during the lab, you can optionally disable components you won’t test:
# Optional — disable components to speed up deployment and reduce costs
azd env set ENABLE_API_CENTER false
azd env set ENABLE_MANAGED_REDIS false
azd env set ENABLE_AZURE_AI_SEARCH false
Deploy
Recommended region: For this lab, Sweden Central (
swedencentral) is the recommended region.
azd up
The CLI will prompt you to:
- Select your Azure subscription
- Confirm the Azure region
Then the deployment begins. This takes approximately 30-45 minutes.
⏳ While waiting: Work through the companion Microsoft Learn module Govern AI at scale to build the conceptual background behind Citadel. (Preview link — sign-in required; it will be replaced by a public link later.)
💡 While you wait — just-in-time learning. Expand the sections below to understand what azd up is doing behind the scenes while it runs.
What does azd up actually deploy?
azd up runs the Bicep templates in bicep/infra/main.bicep at subscription scope, provisioning the entire Citadel Hub in one pass:
- Azure API Management — the unified AI gateway that fronts every model call and enforces governance policies.
- Two Azure AI Foundry accounts with model deployments (GPT-4.1, DeepSeek-R1, embeddings) across two regions for load balancing and failover.
- Cosmos DB for usage analytics, Event Hub for real-time usage streaming, and a Logic App that ingests usage records from Application Insights into Cosmos DB.
- Azure AI Language (PII detection) and Content Safety for policy enforcement.
- Key Vault, Log Analytics + Application Insights, a virtual network with private endpoints, and the user-assigned managed identities that let services talk to each other with zero stored credentials.
It also assigns the RBAC roles those identities need (Cognitive Services User, Event Hubs Data Sender/Owner, Cosmos DB Data Contributor, Key Vault Secrets User, etc.).
Why does it take 30–45 minutes?
API Management is the long pole. A StandardV2 instance takes ~15–25 minutes to provision (classic Developer/Premium tiers take 30–45). In parallel, the model deployments, private endpoints, and RBAC role propagation each add time.
azd up is idempotent — if a resource fails with a transient error, re-running the command resumes from where it left off rather than starting over.
What is the hub-and-spoke pattern, and why use it?
The hub is the shared governance plane — API Management plus observability, policy enforcement, and the supporting AI platform services. Spokes are individual workloads (AI Foundry projects) that consume AI only through the hub’s gateway.
Because every call flows through the hub, governance is centralized: one place to enforce access contracts, mask PII, apply content safety, rate-limit, and meter token usage — without each workload team having to build those controls themselves.
🔁 Transient errors: Deployment of some resources, such as model deployments, may occasionally fail with a transient error. If
azd upfails, re-run the command — it will pick up where it left off.
If your deployment uses the Developer APIM SKU (APIM_SKU=”Developer”), run the APIM cleanup script immediately after azd up completes. The script checks the live APIM SKU and only removes the default APIM products and subscriptions when the SKU is Developer.
🪟 PowerShell
.\workshop\scripts\cleanup-apim-defaults.ps1
💻 Bash
./workshop/scripts/cleanup-apim-defaults.sh
Verify Deployment
Once azd up completes:
- The Citadel Hub is deployed.
- The sample Citadel Spoke is not deployed yet.
# View deployment outputs (resource names, endpoints, etc.)
azd env get-values
Deploy the Sample Spoke
After azd up finishes, run the spoke deployment script from the repository root. Pass a unique spoke suffix so the script creates a distinct spoke resource group and unique resource names for the Foundry account, Key Vault, Log Analytics workspace, Application Insights, and Azure Container Registry.
🪟 PowerShell
.\workshop\scripts\deploy-spoke-foundry.ps1 -SpokeSuffix 1
💻 Bash
./workshop/scripts/deploy-spoke-foundry.sh --spoke-suffix 1
💡 While the spoke deploys — just-in-time learning.
What does the spoke deployment script do?
The script deploys a standalone Azure AI Foundry account and project into a new spoke resource group (<hub-rg>-spoke-<suffix>), representing an onboarded workload — a “use case” a team would bring to the platform. Alongside the Foundry resources it creates the spoke’s own Key Vault, Log Analytics workspace, Application Insights, and Azure Container Registry.
The -SpokeSuffix keeps every generated name unique, so you can stand up multiple independent spokes from the same script without editing it.
The key idea: the spoke does not call AI models directly. It connects back to the hub’s API Management gateway, so its traffic is governed, logged, and metered centrally — exactly as a real onboarded workload would be.
This step deploys a sample Citadel Spoke: a standalone Azure AI Foundry account and project that represent an onboarded workload (use case). For example, if the hub resource group is rg-citadel-demo-1, -SpokeSuffix 1 creates a spoke resource group named rg-citadel-demo-1-spoke-1. To create another spoke without editing the scripts, rerun the same command with a different suffix:
🪟 PowerShell
.\workshop\scripts\deploy-spoke-foundry.ps1 -SpokeSuffix 2
.\workshop\scripts\deploy-spoke-foundry.ps1 -SpokeSuffix 10
💻 Bash
./workshop/scripts/deploy-spoke-foundry.sh --spoke-suffix 2
./workshop/scripts/deploy-spoke-foundry.sh --spoke-suffix 10
Advanced — override generated spoke names
The suffix only affects generated defaults. Advanced users can still override individual names with environment variables such as SPOKE_RESOURCE_GROUP_NAME, FOUNDRY_ACCOUNT_NAME, KEY_VAULT_NAME, SPOKE_LOG_ANALYTICS_NAME, SPOKE_APP_INSIGHTS_NAME, and SPOKE_ACR_NAME.
Verify Deployment
Once the script completes, there is no need to note down any values — the notebooks read directly from azd env variables at runtime.
You can verify in the Azure Portal:
- Go to portal.azure.com
- Navigate to the hub resource group and confirm the shared Citadel resources are deployed (APIM, Event Hub, Cosmos DB, Key Vault, Logic App, AI Foundry, etc.)
- Navigate to the spoke resource group created by the script and confirm the sample Azure AI Foundry resources are deployed there
Review deployed services and configuration
Goal: Understand the services Citadel deploys and how they are configured.
Estimated time: 30 minutes
Resource Group Overview
Navigate to your resource group in the Azure Portal. You should see resources including:
| Resource Type | Name Pattern | Purpose |
|---|---|---|
| API Management | apim-* |
Unified AI gateway — the heart of Citadel Hub |
| AI Foundry | aif-* |
Hosts AI model deployments (GPT-4.1, DeepSeek-R1, etc.) |
| Application Insights | appi-apim-* |
APIM performance monitoring and telemetry |
| Cosmos DB | cosmos-* |
Usage record storage for analytics |
| Event Hub | evhns-* |
Real-time usage data streaming |
| Key Vault | kv-* |
Secrets and certificates management |
| Log Analytics | log-* |
Centralized log collection |
| Logic App | logic-* |
Usage ingestion workflow processing |
| Virtual Network | vnet-* |
Network isolation and private endpoints |
| Managed Identity | id-* |
Service-to-service authentication (zero credentials) |
Explore AI Foundry
ℹ️ Note: After
azd up, the AI Foundry resources in Citadel Hub have public network access disabled by design. Related Foundry projects are not accessible through the Foundry portal, and users will see “Private network access required”. This is expected.
Optional — explore deployed models in the Foundry portal
If you would like to explore the deployed models at the end of the lab:
- Go to the Foundry resource.
- Open Resource Management → Networking.
- Change access to All networks.
- Open the related project in the Foundry portal.
- Go to Build → Models to view deployed models.
We recommend doing this at the end of the lab to minimize context switching.
Explore API Management
API Management is the core of Citadel. Explore these areas:
APIs
- Navigate to your API Management resource → APIs
- You should see:
- Azure OpenAI API — Standard Azure OpenAI compatible endpoint
- Universal LLM API — Citadel’s universal inference endpoint
- Unified AI API — Multi-provider wildcard API (if enabled)
- Go to Universal LLM API → Settings. Scroll down to Diagnostics Logs and select Azure Monitor. Observe that “Log LLM messages” is enabled. This is also the case with other APIs — Azure OpenAI API and Unified AI API.
🔎 Optional: Select an API, such as Universal LLM API, open the Test tab, and search for the “chat” operation (“Gets chat completions for the provided chat messages.”). In the request body, change the
modelfield fromgpt-4otogpt-4.1. Click Send and observe the reply.
Products
- Go to Products — each product represents an “access contract” for a team/application
ℹ️ Note: Additional products will be created when you run the validation notebooks.
Policies
- Select an API (e.g. Universal LLM API) → Select All operations → Inbound processing → Policy code editor
- Review the XML policies — pay attention to
<include-fragment>policy blocks. Note down the policy block withfragment-id="set-llm-usage". In the next step, go to APIs → Policy Fragments, select the set-llm-usage policy fragment, then go to Settings → Policy editor to review its policy code.
Backends
- Go to Backends — see registered AI Foundry / OpenAI backends
- Note the circuit breaker configuration on LLM backends (not all backends have circuit breakers — only the LLM-related ones)
- Go to Load balancer — see how backends are grouped into pools for load distribution
Explore Supporting Services
Cosmos DB:
- Go to Settings → Networking and enable Selected networks. Under the Firewall option, add your current IP address and/or accept connections from within public Azure datacenters.
- Note that this is only for the purposes of this lab. In a real implementation, networking should be configured to fit your specific networking setup.
Key Vault:
- Review the Key Vault resource in the spoke resource group — note that there are no secrets stored immediately after
azd up - As part of the notebooks execution, secrets (such as access contract subscription keys) will be created here
Run validation notebooks
Goal: Exercise Citadel’s capabilities by running validation notebooks.
Estimated time: 60 minutes
Set Up Python Environment
If you are using the Devcontainer, the post-create setup installs the lab dependencies automatically. Confirm the selected Python interpreter points to workshop/.venv, then continue to Section 5.2.
Navigate to the workshop folder in your terminal:
cd workshop
Option A — Using uv (recommended)
# Install uv if you don't have it
# Windows:
# powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# macOS/Linux:
# curl -LsSf https://astral.sh/uv/install.sh | sh
# Create environment and install dependencies
uv sync
Option B — Using pip
⚠️ Important: This lab requires Python 3.13 or later. Install it from python.org if you don’t have it.
# Create a virtual environment with Python 3.13
# Windows:
py -3.13 -m venv .venv
# macOS/Linux:
# python3.13 -m venv .venv
# Activate it
# Windows:
.venv\Scripts\activate
# macOS/Linux:
# source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
Open Notebooks in VS Code
- Open the
workshopfolder in VS Code - Open a notebook file (
.ipynb) - see 5.4 Recommended Notebook Execution Order - Select the Python kernel from your virtual environment (
.venvoruvmanaged). In the Devcontainer, use the preconfiguredworkshop/.venvinterpreter.
Configure Notebook Variables
All required variables (resource group name, Azure region, APIM endpoint, etc.) are automatically configured in the notebooks. The initialization cell (Step 0) in each notebook retrieves these values from your azd environment and Azure resource metadata at runtime — you do not need to manually set anything.
Take a moment to review how this works in the code: the notebooks use azd env get-values and Azure CLI commands to discover resource names and endpoints, then inject them as Python variables used throughout the notebook.
Recommended Notebook Execution Order
This lab includes a subset of validation notebooks. Execute them in this order:
| Order | Notebook | File | Details |
|---|---|---|---|
| 1 | LLM Backend Onboarding Runner | 1. llm-backend-onboarding-runner.ipynb |
Validation guide |
| 2 | Universal LLM API — All-Models Tests | 2. citadel-universal-llm-api-all-models-tests.ipynb |
Validation guide |
| 3 | Citadel Access Contracts Tests | 3. citadel-access-contracts-tests.ipynb |
Validation guide |
| 4 | Citadel Agent Frameworks Tests | 4. citadel-agent-frameworks-tests.ipynb |
Validation guide |
| 5 | Citadel PII Processing Tests | 5. citadel-pii-processing-tests.ipynb |
Validation guide |
| 6 | Citadel Unified AI API Tests | 6. citadel-unified-ai-api-tests.ipynb |
Validation guide |
Lab context: For the purposes of this lab, the same backends that were already provisioned by
azd upwill be re-provisioned through the notebook. In a real-world scenario, you would first provision new backends (e.g. a new AI Foundry account) and then use this notebook to register them with APIM. Note that all backends must be included in a single provisioning run — if only one new backend is added via the notebook, previously registered backends will be removed. For example, ifbackend1andbackend2are already registered and you want to addbackend3, all three must be specified in the notebook.
Optional Notebooks (7–9)
The following three notebooks are optional and can be run after notebooks 1–6. They are net-new labs and do not have equivalent notebooks in the validation/ folder. Unless noted, they share the same pre-requisites as notebooks 1–6 (deployed hub + spoke, Azure CLI login, lab Python environment).
7. Citadel Hosted Agent with AGT — build, deploy, and govern a Foundry hosted agent
| Notebook | 7. citadel-hosted-agent-with-agt.ipynb |
| Extra pre-reqs | None beyond the standard notebooks 1–6 pre-requisites |
Builds a Foundry Hosted Agent for a Contoso HR scenario, governed end-to-end with the Microsoft Agent Governance Toolkit (AGT). The notebook generates the agent source, builds and pushes the container image with az acr build (no local Docker), deploys it as a hosted agent version to the spoke Foundry project, assigns the required RBAC, and tests it end-to-end — including governance enforcement (PII/SSN blocking, third-party salary lookups, destructive-tool denial), multi-turn conversations, and streaming responses. AGT policy and audit decisions are exported to Application Insights.
8. Publish and Use an A2A Endpoint — expose a Foundry agent as a governed Agent-to-Agent endpoint
| Notebook | 8. publish-and-use-a2a-endpoint.ipynb |
| Extra pre-reqs | None beyond the standard notebooks 1–6 pre-requisites |
Walks through the full lifecycle of running an HR prompt agent on Microsoft Foundry, enabling its incoming Agent-to-Agent (A2A) endpoint, verifying the agent card, and publishing it through the Citadel Governance Hub (APIM) as a governed access contract (product + subscription). It then calls the agent through APIM with only a subscription key — proving APIM reaches the agent over its private endpoint even with public network access disabled. Everything is resolved from your current azd environment, so no resource names are hardcoded.
9. Publish and Use the HR MCP via APIM — publish and consume an HR MCP server through APIM (requires setup scripts)
| Notebook | 9. publish-and-use-hr-mcp-via-apim.ipynb |
| Extra pre-reqs | The HR MCP server must be deployed and published first (see steps below) |
Demonstrates the HR Model Context Protocol (MCP) server through Azure API Management only. It validates the Citadel-style APIM access contract, exercises MCP protocol/tool calls, demonstrates the APIM 5 tools/call per 30 seconds rate-limit policy, inspects APIM logs, and deploys a Foundry hosted agent whose tools come from the APIM-published MCP endpoint. Everything required to run this notebook lives in the mcp-hr/ folder.
Pre-requisite setup — before running the notebook, run the scripts in mcp-hr/scripts/ in this order (use the .sh scripts on macOS/Linux, .ps1 on Windows). Run them from the workshop/ folder with Azure CLI logged in:
- Deploy the MCP server — provisions the Container Apps host, ACR, telemetry, and Entra auth settings:
# macOS/Linux bash ./mcp-hr/scripts/deploy-hr-mcp.sh # Windows pwsh -File ./mcp-hr/scripts/deploy-hr-mcp.ps1 - Publish to APIM — creates the APIM MCP API, backend, Citadel product, subscription, and policies:
# macOS/Linux bash ./mcp-hr/scripts/publish-hr-mcp-apim.sh # Windows pwsh -File ./mcp-hr/scripts/publish-hr-mcp-apim.ps1 - Validate — run the two test scripts to confirm direct and APIM-mediated access work:
uv run python ./mcp-hr/scripts/test-hr-mcp-direct.py uv run python ./mcp-hr/scripts/test-hr-mcp-apim.py
Once these scripts complete successfully, run the notebook.
Running Notebooks — Tips
- ✅ Run All is fine — you can use Run All to execute the entire notebook, but review the output of each cell afterwards to understand what happened
- 🔐 Azure CLI auth — ensure you are logged in with
az loginbefore running notebooks; this is required for notebooks to discover resources and authenticate - 🧹 Cleanup — do not clean up resources created by notebooks individually; at the end of the lab, run
azd down --purge --force - 🛠️ Errors? — Check the Troubleshooting section
Observability: Metrics, logs, and telemetry
Goal: After running notebooks, explore the telemetry and usage data generated by your API calls.
Estimated time: 30 minutes
API Management — Built-in Analytics
-
Navigate to your API Management resource
- Analytics (under Monitoring):
- Explore the Timeline tab to see request volume over time
- Switch to the Subscriptions tab to view usage broken down by APIM subscription
- Open the Language Models tab to observe per-model metrics
- Metrics:
- Go to Metrics
- Add metric: Requests → Split by ApiId — observe how traffic distributed across your notebook runs. Use Bar Chart for visualization.
- Create new chart, then add a new metric: Backend Duration → Split by ApiId, to see backend response times
- Logs:
- Go to Logs (under Monitoring)
- Run sample queries to explore APIM gateway activity:
Request summary by response code:
ApiManagementGatewayLogs | where TimeGenerated > ago(6h) | summarize count() by ResponseCode, ApiId | order by count_ descRequest volume by API:
ApiManagementGatewayLogs | where TimeGenerated > ago(6h) | summarize RequestCount = count() by ApiId, bin(TimeGenerated, 5m) | render columnchartLatency percentiles:
ApiManagementGatewayLogs | where TimeGenerated > ago(2h) | summarize P50 = percentile(TotalTime, 50), P95 = percentile(TotalTime, 95), P99 = percentile(TotalTime, 99) by ApiIdThrottled requests (429s):
ApiManagementGatewayLogs | where TimeGenerated > ago(6h) | where ResponseCode == 429 | summarize ThrottledCount = count() by ProductId, bin(TimeGenerated, 5m) | render columnchart
Application Insights — APIM Telemetry
From your APIM resource, click the Application Insights option in the sidebar to navigate directly to the associated Application Insights resource.
- Application Map — Visualize how components are connected:
- Go to Application Map
- See APIM, AI Foundry backends, and dependency calls
- Transaction Search — Drill into individual requests:
- Go to Transaction search
- Filter by time range covering your notebook runs
- Click on a request to see full end-to-end trace (request → APIM policy execution → backend call → response)
Cosmos DB — Usage Records
Populate data first: Before querying Cosmos DB, you need to run the Logic App workflow that ingests usage data from Application Insights into Cosmos DB:
- In the Azure Portal, open the Logic Apps service (the resource name starts with
logic-) - Navigate to Workflows → Workflows
- Select the
llm-usage-ingestionworkflow - Click Run to trigger the workflow manually
- Check Run History — wait until you see a successful run
- Explore the run results to observe how data flows from App Insights to Cosmos DB
Once you have a successful run, proceed with importing model pricing data.
Import model pricing data: The model-pricing container stores per-model cost information used for usage analytics. This data is imported from a local JSON file (model-pricing.json) included in the workshop/scripts folder. The import script authenticates via Entra ID and requires the Cosmos DB Built-in Data Contributor role on your Cosmos DB account.
-
First, retrieve your Cosmos DB account name and assign the required role to your current user:
💻 Bash
# Get the Cosmos DB account name from your deployment COSMOS_ACCOUNT=$(az cosmosdb list --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) --query "[0].name" -o tsv) COSMOS_RESOURCE_GROUP=$(azd env get-value AZURE_RESOURCE_GROUP) CURRENT_USER_ID=$(az ad signed-in-user show --query id -o tsv) # Assign "Cosmos DB Built-in Data Contributor" role (role definition id: 00000000-0000-0000-0000-000000000002) az cosmosdb sql role assignment create \ --account-name $COSMOS_ACCOUNT \ --resource-group $COSMOS_RESOURCE_GROUP \ --role-definition-id 00000000-0000-0000-0000-000000000002 \ --principal-id $CURRENT_USER_ID \ --scope "/"🪟 PowerShell
$COSMOS_ACCOUNT = az cosmosdb list --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) --query "[0].name" -o tsv $COSMOS_RESOURCE_GROUP = azd env get-value AZURE_RESOURCE_GROUP $CURRENT_USER_ID = az ad signed-in-user show --query id -o tsv az cosmosdb sql role assignment create ` --account-name $COSMOS_ACCOUNT ` --resource-group $COSMOS_RESOURCE_GROUP ` --role-definition-id 00000000-0000-0000-0000-000000000002 ` --principal-id $CURRENT_USER_ID ` --scope "/" -
Run the import script to load model pricing data into Cosmos DB:
💻 Bash
# Get the Cosmos DB endpoint COSMOS_ENDPOINT=$(az cosmosdb show --name $COSMOS_ACCOUNT --resource-group $COSMOS_RESOURCE_GROUP --query "documentEndpoint" -o tsv) # Import model pricing data uv run python scripts/import-model-pricing.py --endpoint $COSMOS_ENDPOINT🪟 PowerShell
$COSMOS_ENDPOINT = az cosmosdb show --name $COSMOS_ACCOUNT --resource-group $COSMOS_RESOURCE_GROUP --query "documentEndpoint" -o tsv uv run python scripts\import-model-pricing.py --endpoint $COSMOS_ENDPOINTYou should see output confirming each model pricing record was upserted.
Data Explorer: Now navigate to Cosmos DB to explore both usage records and model pricing:
- Navigate to your Cosmos DB resource
- Open Data Explorer
- Look for the
ai-usage-dbdatabase - Review the two important containers:
llm-usage-container— stores usage records generated by notebook callsmodel-pricing— stores model pricing data imported by the script
- Select
ai-usage-dbdatabase →llm-usage-container -
Run a query to see usage records generated by your notebook calls:
SELECT * FROM c WHERE c._ts > (GetCurrentTimestamp()/1000 - 7200) ORDER BY c._ts DESC - Examine a usage record — note fields like:
- Model name and deployment
- Token counts (prompt, completion, total)
- Product that made the call
- Timestamp
💡 Tip: These usage records are generated by the
set-llm-usageAPIM policy fragment you reviewed in Lab 2 (Section 4.3). That policy fragment captures token counts and model metadata from each LLM call and emits them to Event Hub, which the Logic App workflow then ingests into Cosmos DB. - Also explore the
model-pricingcontainer to see how model pricing data is stored
ℹ️ Note: If you get a warning that the request is blocked by your Cosmos DB account firewall settings, go to your Cosmos DB resource → Settings → Networking, choose Selected networks, and add your current IP address.
Optional — visualize usage data in Power BI
You can now visualize the data in Cosmos DB using the provided Power BI template. See the Power BI Dashboard guide for instructions.
Clean up
After the lab, remove all deployed resources to avoid ongoing costs. Perform these steps in order — tear down the optional MCP lab resources first, then the Citadel hub, then any spokes.
Tear Down MCP Lab Resources (only if you ran optional notebook 9)
If you ran optional notebook 9 (HR MCP via APIM), tear down the HR MCP resources before running azd down for the Citadel hub. The MCP deployment creates resources inside the shared hub VNet (a dedicated ACA subnet, private DNS zone link, etc.), so removing them first avoids leaving orphaned dependencies that can block hub deletion. Run the teardown script from the workshop/ folder:
💻 Bash
bash ./mcp-hr/scripts/teardown-hr-mcp.sh
🪟 PowerShell
pwsh -File ./mcp-hr/scripts/teardown-hr-mcp.ps1
The teardown is best-effort and idempotent — resources that are already gone are skipped.
⏭️ Skip this step if you did not run notebook 9.
Tear Down the Citadel Hub (azd down)
Remove the hub resources that were deployed via azd up:
# This will delete ALL resources in the resource group and purge soft-deleted resources
azd down --purge --force
🛠️ If
azd downfails: Go to the Azure Portal, navigate to your hub resource group, and delete the deployment manually.
⚠️ Important: The
--purgeflag ensures soft-deleted resources (Key Vault, Cognitive Services) are permanently removed. The--forceflag skips confirmation prompts.
Tear Down the Spoke(s) (via Azure Portal)
azd down only removes the hub resources that were deployed via azd up. You must also manually delete any spoke resource groups in the Azure Portal because they were deployed separately by script.
- Go to portal.azure.com
- Locate each spoke resource group (e.g.
rg-citadel-demo-1-spoke-1) - Delete the resource group
Verify Cleanup
Verify in the Azure Portal that the hub resource group and any spoke resource groups have been deleted.
Troubleshooting
Deployment Issues
azd up fails with provider not registered
# Register the missing provider
az provider register --namespace <ProviderNamespace>
# Wait a minute, then retry
azd up
Quota errors for AI model deployments
- Check your subscription’s Azure OpenAI quota in the target region
- Try a different region:
azd env set AZURE_LOCATION eastus2 - Reduce model capacity in the parameter file
APIM deployment takes very long
- StandardV2 APIM typically deploys in 15-25 minutes
- Classic tier (Developer/Premium) can take 30-45 minutes
- This is normal — wait for completion
Notebook Issues
az CLI not authenticated in notebook
# Run in your terminal
az login
# Restart the Jupyter kernel in VS Code
Module not found errors
# Ensure you activated the correct virtual environment
# For uv:
uv sync
# For pip:
pip install -r requirements.txt
APIM returns 401/403 errors
- Verify your subscription key is correct (re-run the “Retrieve API key” cell)
- Check that the APIM product is published and the subscription is active
- For JWT tests: ensure Entra ID setup has been completed
Notebook cells fail with timeout
- Some operations (Bicep deployments) can take 2-5 minutes
- Re-run the cell if it timed out
- Check Azure Portal → Resource Group → Deployments for deployment status
Observability Issues
No data in Application Insights
- There is typically a 2-5 minute ingestion delay
- Ensure you are looking at the correct time range
- Verify the Application Insights resource is connected to your APIM instance
No usage records in Cosmos DB
- Usage records are processed by the Logic App workflow
- Check Logic App → Run history for any failed runs
- Allow a few minutes for events to flow through Event Hub → Logic App → Cosmos DB
Appendix
Key Azure Resources Deployed
| Service | SKU (Default) | Purpose |
|---|---|---|
| API Management | StandardV2 | Unified AI gateway |
| AI Foundry (x2) | Standard | AI model hosting (2 regions) |
| Cosmos DB | 400 RU/s | Usage analytics storage |
| Event Hub | 1 capacity unit | Usage event streaming |
| Key Vault | Standard | Secrets management |
| Language Service | S | PII detection |
| Content Safety | S0 | Content safety scanning |
| Log Analytics | Per-GB | Centralized logging |
| Application Insights | — | APIM monitoring |
| Logic App (Standard) | WS1 | Usage ingestion workflows |
| Virtual Network | — | Network isolation |
| Storage Account | Standard LRS | Logic App backing storage |
Useful azd Commands
azd env get-values # View all deployment outputs
azd env list # List all environments
azd monitor --overview # Open Application Insights in browser
azd monitor --live # Open Live Metrics in browser
azd monitor --logs # Open Log Analytics in browser
azd down --purge --force # Delete all resources