Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ below are the high-level steps to deploy the use case onboarding Bicep package b
5. **Deploy template with the prepared parameter file**:
```bash
# This can be executed in CLI or through a DevOps pipeline
az deployment sub create --name <use-case-contract-name> --location <location> --template-file main.bicep --parameters contracts/<businessunit-usecasename>/<environment>/main.bicepparam
az deployment group create --name <use-case-contract-name> --resource-group <your-resource-group> --template-file main.bicep --parameters contracts/<businessunit-usecasename>/<environment>/main.bicepparam
```

>NOTE: Ensure that you are updating values according to your environment and folder structure.
Expand Down Expand Up @@ -355,17 +355,17 @@ az deployment sub what-if `
### Step 4: Deploy

```powershell
# Deploy at subscription scope (run from the environment folder)
az deployment sub create `
# Deploy at resource-group scope (run from the environment folder)
az deployment group create `
--name healthcare-chatbot-dev-onboarding `
--location swedencentral `
--resource-group <your-resource-group> `
--template-file ../../../main.bicep `
--parameters main.bicepparam

# Or run from the citadel-access-contracts root folder:
az deployment sub create `
az deployment group create `
--name healthcare-chatbot-dev-onboarding `
--location swedencentral `
--resource-group <your-resource-group> `
--template-file main.bicep `
--parameters contracts/healthcare-chatbot/dev/main.bicepparam
```
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
targetScope = 'subscription'
targetScope = 'resourceGroup'

@description('APIM resource coordinates')
param apim object
Expand Down Expand Up @@ -67,30 +67,22 @@ var productPostfix = '${useCase.businessUnit}-${useCase.useCaseName}-${useCase.e
// Default APIM product policy (applied when a service item does not provide policyXmlPath)
var defaultProductPolicyXml = loadTextContent('./policies/default-ai-product-policy.xml')

resource apimRg 'Microsoft.Resources/resourceGroups@2022-09-01' existing = {
scope: subscription(apim.subscriptionId)
name: apim.resourceGroupName
}
// Resource-group scoped: APIM, Key Vault and Foundry all live in the resource
// group this deployment targets. The subscriptionId / resourceGroupName fields
// on the apim, keyVault and foundry parameters are kept for backwards
// compatibility but are no longer used for scoping.

resource apimSvc 'Microsoft.ApiManagement/service@2024-05-01' existing = {
scope: apimRg
name: apim.name
}

resource kvRg 'Microsoft.Resources/resourceGroups@2022-09-01' existing = if (useTargetAzureKeyVault) {
scope: subscription(keyVault.subscriptionId)
name: keyVault.resourceGroupName
}

resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = if (useTargetAzureKeyVault) {
scope: kvRg
name: keyVault.name
}

// Onboard each requested service into APIM
module onboard 'modules/apimOnboardService.bicep' = [for s in services: {
name: 'onboard-${s.code}-${productPostfix}'
scope: apimRg
params: {
apimName: apim.name
productId: '${s.code}-${productPostfix}'
Expand All @@ -109,7 +101,6 @@ module onboard 'modules/apimOnboardService.bicep' = [for s in services: {
// Create/update KV secrets; normalize names (Key Vault does not allow underscores)
module kvWrites 'modules/kvSecrets.bicep' = [for (s, i) in services: if (useTargetAzureKeyVault) {
name: 'kv-${s.code}-${productPostfix}'
scope: kvRg
params: {
keyVaultName: kv.name
secretNames: [ toLower(replace(s.endpointSecretName, '_', '-')), toLower(replace(s.apiKeySecretName, '_', '-')) ]
Expand All @@ -126,17 +117,11 @@ module kvWrites 'modules/kvSecrets.bicep' = [for (s, i) in services: if (useTarg
// Create Foundry APIM connections per service (only if useTargetFoundry is true)
// This enables Foundry agents to access AI models through the APIM gateway

resource foundryRg 'Microsoft.Resources/resourceGroups@2022-09-01' existing = if (useTargetFoundry) {
scope: subscription(foundry.subscriptionId)
name: foundry.resourceGroupName
}

// Generate connection name based on config or use case naming
var foundryConnectionPrefix = !empty(foundryConfig.connectionNamePrefix) ? foundryConfig.connectionNamePrefix : 'Hub-${useCase.businessUnit}-${useCase.useCaseName}-${useCase.environment}'

module foundryConnections 'modules/foundryConnection.bicep' = [for (s, i) in services: if (useTargetFoundry) {
name: 'foundry-${s.code}-${productPostfix}'
scope: foundryRg
params: {
aiFoundryAccountName: foundry.accountName
aiFoundryProjectName: foundry.projectName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,9 @@ param foundryConfig = {
// - Foundry Resource Group: Contributor (if using Foundry)
//
// Deployment command:
// az deployment sub create \
// az deployment group create \
// --name <deployment-name> \
// --location <region> \
// --resource-group <your-resource-group> \
// --template-file main.bicep \
// --parameters main.bicepparam
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ param llmBackendConfig = [
### 3. Deploy

```bash
az deployment sub create --name llm-backend-onboarding --location swedencentral --template-file main.bicep --parameters llm-backends-generated-local.bicepparam
az deployment group create --name llm-backend-onboarding --resource-group <your-resource-group> --template-file main.bicep --parameters llm-backends-generated-local.bicepparam
```

## Configuration Reference
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
targetScope = 'subscription'
targetScope = 'resourceGroup'

/**
* @module LLM Backend Onboarding
Expand All @@ -10,9 +10,14 @@ targetScope = 'subscription'
* - Automatic failover when backends are unavailable
* - Flexible authentication schemes per backend
*
* MicroHack note: this template is resource-group scoped. Lab participants hold
* Owner on their own resource group and only Reader on the subscription, so a
* subscription-scoped deployment would fail authorization. APIM and its managed
* identity are expected to live in the resource group being deployed to.
*
* Usage:
* az deployment sub create \
* --location <location> \
* az deployment group create \
* --resource-group <resource-group> \
* --template-file main.bicep \
* --parameters main.bicepparam
*/
Expand Down Expand Up @@ -106,24 +111,16 @@ param keyVaultName string = ''
// ============================================================================
// EXISTING RESOURCES
// ============================================================================

resource apimRg 'Microsoft.Resources/resourceGroups@2022-09-01' existing = {
scope: subscription(apim.subscriptionId)
name: apim.resourceGroupName
}
// Resource-group scoped: APIM and the managed identity are resolved in the
// resource group this deployment targets. The subscriptionId /
// resourceGroupName fields on the `apim` and `apimManagedIdentity` parameters
// are retained for backwards compatibility but are no longer used for scoping.

resource apimService 'Microsoft.ApiManagement/service@2024-06-01-preview' existing = {
scope: apimRg
name: apim.name
}

resource identityRg 'Microsoft.Resources/resourceGroups@2022-09-01' existing = {
scope: subscription(apimManagedIdentity.subscriptionId)
name: apimManagedIdentity.resourceGroupName
}

resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
scope: identityRg
name: apimManagedIdentity.name
}

Expand All @@ -137,7 +134,6 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-
*/
module llmBackends 'modules/llm-backends.bicep' = {
name: 'llm-backends-deployment-${uniqueString(deployment().name)}'
scope: apimRg
params: {
apimServiceName: apim.name
managedIdentityClientId: managedIdentity.properties.clientId
Expand All @@ -152,7 +148,6 @@ module llmBackends 'modules/llm-backends.bicep' = {
*/
module llmBackendPools 'modules/llm-backend-pools.bicep' = {
name: 'llm-backend-pools-deployment-${uniqueString(deployment().name)}'
scope: apimRg
params: {
apimServiceName: apim.name
backendDetails: llmBackends.outputs.backendDetails
Expand All @@ -165,7 +160,6 @@ module llmBackendPools 'modules/llm-backend-pools.bicep' = {
*/
module llmPolicyFragments 'modules/llm-policy-fragments.bicep' = {
name: 'llm-policy-fragments-deployment-${uniqueString(deployment().name)}'
scope: apimRg
params: {
apimServiceName: apim.name
policyFragmentConfig: llmBackendPools.outputs.policyFragmentConfig
Expand All @@ -185,7 +179,6 @@ module llmPolicyFragments 'modules/llm-policy-fragments.bicep' = {
// */
// module universalLlmApi 'modules/universal-llm-api.bicep' = if (deployUniversalLlmApi) {
// name: 'universal-llm-api-deployment-${uniqueString(deployment().name)}'
// scope: apimRg
// params: {
// apimServiceName: apim.name
// apiPath: universalLlmApiPath
Expand Down
161 changes: 134 additions & 27 deletions 03-Azure/01-04-AI/06_AI_Governance/challenges/challenge-01.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,151 @@ fragments, then prove it works through multiple API formats.
## 🧭 Context

This challenge runs entirely against **Hub** resources — the APIM instance and its
backend pools sit in front of every model your governance hub serves. Before you start,
confirm your attendee credentials are available to the notebooks (see Part A below);
every workshop notebook reads its configuration from the environment, falling back to
`azd env get-value`.
backend pools sit in front of every model your governance hub serves. Your lab resources
are already deployed for you, so Part A below is about pointing your machine at them:
sign in to Azure, then write a `.env` file that every workshop notebook reads.

## ✅ Tasks

### Part A — Confirm your notebook environment is ready (5 min)
### Part A — Set up your notebook environment (10 min)

Every notebook in this MicroHack reads its settings from the environment, so do this
once and it applies to all nine challenges. Pick **either** route:
Your lab resources are **already deployed**. You do not run `azd up`. This section
connects your machine to them, and it applies to all nine challenges — do it once.

**Route 1 — `.env` file (recommended, no extra tooling):**
#### A.1 — Sign in to Azure

1. From the `challenges/workshop/` folder, copy the template:
Your MicroHack dashboard's **Credentials** tab gives you an Entra username and a
**Temporary Access Pass (TAP)**, which is used instead of a password and expires at the
end of the event.

1. Open the **Azure Portal** in a **private/incognito** window (so it doesn't collide
with your corporate account) and sign in with the dashboard's *Entra ID Username*,
entering the *Entra ID TAP* when asked for a password. Complete any prompts.
2. Sign in from your terminal — the notebooks use `DefaultAzureCredential`, so this
session is what authenticates their Azure calls:

```bash
cd challenges/workshop
cp .env.template .env
az login --tenant emea.microhack.cloud
az account set --subscription <SubscriptionId from the dashboard>
az account show --output table
```

2. Open `.env` and paste in the matching `HackboxCredential` values from your MicroHack
dashboard. The template names the exact dashboard credential for every key.
For challenges 1-6 you only need `AZURE_RESOURCE_GROUP`, `AZURE_LOCATION`,
`AZURE_SUBSCRIPTION_ID` and `LLM_BACKEND_CONFIG`.
3. Paste `LLM_BACKEND_CONFIG` as a **single line**, exactly as the dashboard shows it —
it's a JSON document, so a stray line break will break it.
If the browser handoff misbehaves, use `az login --use-device-code --tenant emea.microhack.cloud`.

> [!WARNING]
> If you are already signed in to another Azure account, `DefaultAzureCredential` may
> pick it up instead. If notebook cells return 401/403, run `az account show` and
> confirm you are the lab user.

#### A.2 — Find your resource token

Every resource in your lab is named with the same random token, and the `.env` values
are built from it. In the portal, open your resource group (`rg-labuser-00XX`) and find
the AI Foundry account named **`aif-hub-<TOKEN>`** — everything after the prefix is your
token. For example `aif-hub-fd79f0b6fee89` means the token is `fd79f0b6fee89`.

From the CLI:

```bash
az cognitiveservices account list -g <your-rg> \
--query "[?starts_with(name,'aif-hub-')].name" -o tsv
```

#### A.3 — Fill in `.env`

```bash
cd challenges/workshop
cp .env.template .env
```

Open `.env` and set the values below, replacing `<TOKEN>` throughout. The first three
come straight from the dashboard / portal Overview blade; the rest are derived from the
token.

```bash
AZURE_RESOURCE_GROUP=rg-labuser-00XX
AZURE_LOCATION=swedencentral # compact form, not "Sweden Central"
AZURE_SUBSCRIPTION_ID=<SubscriptionId>

SPOKE_RESOURCE_GROUP=rg-labuser-00XX # same RG — this lab uses one resource group
SPOKE_KEY_VAULT_NAME=kv-spoke-<TOKEN>
SPOKE_AI_FOUNDRY_ACCOUNT_NAME=aif-spoke-<TOKEN>
SPOKE_AI_FOUNDRY_PROJECT_NAME=citadel-agents-project
SPOKE_ACR_NAME=acrcitadel<TOKEN>
SPOKE_ACR_LOGIN_SERVER=acrcitadel<TOKEN>.azurecr.io
A2A_FOUNDRY_ACCOUNT_NAME=aif-spoke-<TOKEN>
```

Challenges 1-6 need only the first four values plus `LLM_BACKEND_CONFIG`.

#### A.4 — Fill in `LLM_BACKEND_CONFIG`

This one is a single line of JSON describing your gateway's backend and its six models.
Paste the block below as **one line**, with `<TOKEN>` replaced in **both** places:

```
LLM_BACKEND_CONFIG=[{"backendId":"aif-hub-<TOKEN>","backendType":"ai-foundry","endpoint":"https://aif-hub-<TOKEN>.openai.azure.com","authScheme":"managedIdentity","authType":"managed-identity","priority":1,"weight":100,"supportedModels":[{"name":"gpt-4.1","sku":"GlobalStandard","capacity":20,"modelFormat":"OpenAI","modelVersion":"2025-04-14","retirementDate":"2027-04-14"},{"name":"gpt-5.4-mini","sku":"GlobalStandard","capacity":20,"modelFormat":"OpenAI","modelVersion":"2026-03-17","retirementDate":"2027-09-21"},{"name":"gpt-5.2","sku":"GlobalStandard","capacity":20,"modelFormat":"OpenAI","modelVersion":"2025-12-11","retirementDate":"2027-06-08"},{"name":"text-embedding-3-large","sku":"GlobalStandard","capacity":20,"modelFormat":"OpenAI","modelVersion":"1","retirementDate":"2028-02-09"},{"name":"Mistral-Large-3","sku":"GlobalStandard","capacity":20,"modelFormat":"Mistral AI","modelVersion":"1","retirementDate":"2099-12-31"},{"name":"Phi-4","sku":"GlobalStandard","capacity":1,"modelFormat":"Microsoft","modelVersion":"7","retirementDate":"2099-12-31"}]}]
```

> [!IMPORTANT]
> Three things break this value, and all three fail with a confusing error pointing
> ~1000 characters away from the real mistake:
>
> - **Line breaks.** It must be one unbroken line. Turn off word wrap before pasting.
> - **Surrounding quotes.** Do not wrap the value in `"` or `'`.
> - **A missing closing bracket.** The value must end with `}]}]` — four closers, for
> the model, `supportedModels`, the backend object, and the outer array.
>
> If your editor underlines the line as invalid, ignore it: `.env` is not a JSON file,
> and the `LLM_BACKEND_CONFIG=` prefix alone is enough to confuse a JSON linter.

Alternatively, read the exact value straight out of your deployment outputs and skip the
typing entirely:

```bash
az deployment group list -g <your-rg> -o json \
| python3 -c "
import json,sys
for d in json.load(sys.stdin):
for k,v in ((d.get('properties') or {}).get('outputs') or {}).items():
if k.upper() == 'LLM_BACKEND_CONFIG':
print('LLM_BACKEND_CONFIG=' + v['value']); raise SystemExit
"
```

#### A.5 — Validate before you start

```bash
cd challenges/workshop
python3 -c "
import json
from pathlib import Path
v=[l.partition('=')[2].strip() for l in Path('.env').read_text().splitlines()
if l.startswith('LLM_BACKEND_CONFIG=') and l.partition('=')[2].strip()][0]
c=json.loads(v)
print('OK -', len(c), 'backend,', len(c[0]['supportedModels']), 'models')"
```

Expect `OK - 1 backend, 6 models`. If it raises instead, re-read the warning above —
a missing final `]` is by far the most common cause.

`.env` is gitignored, so your credentials stay out of source control.

**Route 2 — `azd` bridge (the original workshop workflow):**
> [!TIP]
> **If you edit `.env` after starting Jupyter, restart the kernel.** The notebook only
> sets a variable if it isn't already present, so a bad value read at startup will
> persist for the whole session no matter what you change on disk. VS Code also injects
> `.env` into the kernel at start, which has the same effect.

1. Install `azd`, then run
[`setup-notebook-env.ps1`](../labautomation/README.md#notebook-environment-setup)
with your dashboard's `HackboxCredential` values. It writes a local `azd`
environment that the notebooks fall back to for any key not already set.
> [!NOTE]
> **Using `azd` instead (optional).** If you prefer the original workshop workflow,
> install `azd` and run
> [`setup-notebook-env.ps1`](../labautomation/README.md#notebook-environment-setup).
> It writes a local `azd` environment that the notebooks fall back to for any key not
> already set in `.env`. This is not required.

> [!TIP]
> To confirm you're ready, run the first setup cell of notebook 1. It prints your
> resource group, location, and the number of configured LLM backends. If it raises
> instead, the message tells you which key is missing and whether a `.env` was found.
To confirm you're ready, run the first setup cell of notebook 1. It should print your
resource group, your location, and `LLM Backends: 1 backend(s) configured`.

### Part B — Run the LLM backend onboarding notebook (30 min)

Expand Down Expand Up @@ -89,7 +193,10 @@ once and it applies to all nine challenges. Pick **either** route:
| Symptom | Fix |
|---------|-----|
| Setup cell raises a missing-key error | That key isn't in your `.env` (and no `azd` environment supplied it) — see Part A. The error message tells you which key is missing and whether a `.env` file was found. |
| `LLM_BACKEND_CONFIG` fails to parse | It must be on a single line, pasted exactly as the dashboard shows it. A line break or a truncated copy will break the JSON. |
| `LLM_BACKEND_CONFIG` fails to parse | It must be one unbroken line, with no surrounding quotes, ending in `}]}]`. A missing final bracket is the usual cause — run the validation snippet in Part A.5, which reports the exact position. |
| You fixed `.env` but the notebook reports the old error | The kernel caches environment values from startup. Restart the Jupyter kernel and re-run. |
| `ResourceGroupNotFound` for a resource group you can see in the portal | Your `az` CLI is signed in to a *different* tenant (commonly your corporate account). The portal session and the CLI session are independent. Run `az account show` — if the user isn't `labuser-XXXX@emea.microhack.cloud`, redo the `az login` in Part A.1. |
| `AuthorizationFailed` on `Microsoft.Resources/deployments/...` over a **subscription** scope | You're running an older copy of the notebooks. All deployments must be resource-group scoped (`az deployment group create`); you hold Owner on your own resource group but only Reader on the subscription. Re-download the challenge files. |
| Backend deployment succeeds but test calls 401/403 | APIM RBAC / managed-identity propagation can take a minute after a fresh deployment — wait and retry. |
| A model call fails with a region/quota error | Model availability is region-specific; double-check the model/SKU you configured is available in your lab's region. |

Expand Down
Loading