Foundry Tools Inventory
Deployment Runbook
Architecture Overview
The inventory job runs as a timer-triggered Azure Function (Python, Linux Consumption) inside the target subscription, on an hourly schedule. Using its system-assigned managed identity, it reads the AI Foundry data plane plus Azure Resource Manager and Resource Graph, then writes a single tools-inventory.json document (schema v1.5, with all eight sections inline) to a Blob Storage container. The OpenICF connector in PingOne IDM reads that blob (and the live AI Foundry APIs) to surface AI governance data during reconciliation.
Shell Variables
All commands reference shell variables. Set these in your terminal (or Cloud Shell) before running anything. They mirror the keys in deploy.env. Later steps capture additional values as resources are created.
export SUBSCRIPTION_ID=<your-subscription-id> export AIF_TENANT_ID=<your-tenant-id> export LOCATION=eastus # Where the Function infrastructure is created export FUNCTION_RESOURCE_GROUP=rg-tools-inventory export FUNCTION_APP_NAME=<globally-unique-name> export STORAGE_ACCOUNT_NAME=<3-24 lowercase alphanumerics> # The pre-existing AI Foundry project to inventory export AIF_RESOURCE_GROUP=<foundry-project-rg> export AIF_PROJECT_RESOURCE_ID=/subscriptions/.../projects/<project> export AIF_AGENT_SERVICE_ENDPOINT=https://<acct>.services.ai.azure.com/api/projects/<project> export AIF_AGENT_TYPE=AGENTS
deploy.sh script reads deploy.env and applies these for you.
Prerequisites
Before starting, ensure you have:
- An Azure account with Owner (or Contributor + User Access Administrator) on the subscription β the deploy creates resources and assigns roles
- An existing AI Foundry project to inventory (you supply its ARM resource ID and endpoint)
- The deployment package (
foundry-tools-inventory-deploy.tar.gz)
The two prerequisite steps (P1βP2) must complete before Step 1.
P1 Get the Azure CLI
The simplest path needs nothing installed locally β and crucially, no Python, because the function's dependencies are built on the server. Use Azure Cloud Shell, or install the CLI locally.
P1a β Azure Cloud Shell (recommended, zero install)
Open https://shell.azure.com and choose Bash. You are already signed in, and az, jq, and zip are pre-installed. Use the Upload button to upload the package, then extract:
tar xzf foundry-tools-inventory-deploy.tar.gz cd foundry-tools-inventory
P1b β Local CLI (alternative)
Install from https://learn.microsoft.com/cli/azure/install-azure-cli (bash 4+ recommended on macOS β brew install bash). Verify:
az version
P2 Sign In & Register Providers
Sign in (skip in Cloud Shell β already signed in), select the subscription, and ensure the resource providers used by the deploy are registered.
az login
az account set --subscription ${SUBSCRIPTION_ID}
# Register the providers the deploy needs (no-op if already registered)
az provider register -n Microsoft.Web --subscription ${SUBSCRIPTION_ID}
az provider register -n Microsoft.Storage --subscription ${SUBSCRIPTION_ID}
az provider register -n Microsoft.Insights --subscription ${SUBSCRIPTION_ID}
Registered state. preflight.sh checks this and offers to register them for you.
Step 1 Create the Resource Group
This resource group holds the Function App, its storage account, and Application Insights. It may differ from the AI Foundry project's resource group.
az group create -n ${FUNCTION_RESOURCE_GROUP} -l ${LOCATION}
Step 2 Create the Storage Account
One storage account serves both the Function runtime and the inventory output blob.
az storage account create -n ${STORAGE_ACCOUNT_NAME} -g ${FUNCTION_RESOURCE_GROUP} \
-l ${LOCATION} --sku Standard_LRS --kind StorageV2 --min-tls-version TLS1_2
STORAGE_ACCOUNT_NAME.
Step 3 Create Application Insights
Application Insights captures function logs and run telemetry. It is where you look first when a run fails.
az extension add --name application-insights --upgrade
az monitor app-insights component create --app ${FUNCTION_APP_NAME}-ai \
-g ${FUNCTION_RESOURCE_GROUP} -l ${LOCATION} --application-type web
# Capture the connection string for Step 5
export APPINSIGHTS_CONN=$(az monitor app-insights component show \
--app ${FUNCTION_APP_NAME}-ai -g ${FUNCTION_RESOURCE_GROUP} \
--query connectionString -o tsv)
Step 4 Create the Function App & Identity
Create a Linux Consumption Function App on Python 3.11, then enable its system-assigned managed identity and capture the principal ID for RBAC.
az functionapp create -n ${FUNCTION_APP_NAME} -g ${FUNCTION_RESOURCE_GROUP} \
--storage-account ${STORAGE_ACCOUNT_NAME} --consumption-plan-location ${LOCATION} \
--os-type Linux --runtime python --runtime-version 3.11 --functions-version 4 \
--disable-app-insights true
export PRINCIPAL_ID=$(az functionapp identity assign -n ${FUNCTION_APP_NAME} \
-g ${FUNCTION_RESOURCE_GROUP} --query principalId -o tsv)
echo "PRINCIPAL_ID=${PRINCIPAL_ID}"
Step 5 Apply App Settings
The function reads its configuration from app settings. The two build flags enable the server-side Oryx build that installs requirements.txt β this is why you do not need Python locally.
az functionapp config appsettings set -n ${FUNCTION_APP_NAME} -g ${FUNCTION_RESOURCE_GROUP} --settings \
AIF_TENANT_ID=${AIF_TENANT_ID} \
AIF_SUBSCRIPTION_ID=${SUBSCRIPTION_ID} \
AIF_DEFAULT_LOCATION=${LOCATION} \
AIF_AGENT_SERVICE_ENDPOINT=${AIF_AGENT_SERVICE_ENDPOINT} \
AIF_API_VERSION=2025-11-15-preview \
AIF_AGENT_TYPE=${AIF_AGENT_TYPE} \
AIF_RESOURCE_GROUP=${AIF_RESOURCE_GROUP} \
AIF_AUTH_MODE=MANAGED_IDENTITY \
TOOLS_INVENTORY_STORAGE_ACCOUNT_URL=https://${STORAGE_ACCOUNT_NAME}.blob.core.windows.net \
TOOLS_INVENTORY_CONTAINER=tools-inventory \
TOOLS_INVENTORY_BLOB=azure-ai-foundry/tools-inventory.json \
SCM_DO_BUILD_DURING_DEPLOYMENT=true \
ENABLE_ORYX_BUILD=true \
ACTIVITY_LOG_ENABLED=false \
APPLICATIONINSIGHTS_CONNECTION_STRING="${APPINSIGHTS_CONN}"
SCM_DO_BUILD_DURING_DEPLOYMENT=true and ENABLE_ORYX_BUILD=true, the dependencies in requirements.txt are installed on Azure's build server when you push the zip in Step 7.
Step 6 Assign RBAC
The managed identity needs three role assignments, each covering a distinct responsibility:
| Role | Scope | Purpose |
|---|---|---|
Azure AI User | the Foundry project | Read agents, tools, connections (data plane) |
Reader | the Foundry project's resource group | Read ARM identity and role assignments for identity bindings |
Storage Blob Data Contributor | the storage account | Write the inventory blob |
# Azure AI User on the project (role GUID 53ca6127-db72-4b80-b1b0-d745d6d5456d) az role assignment create --assignee-object-id ${PRINCIPAL_ID} \ --assignee-principal-type ServicePrincipal \ --role 53ca6127-db72-4b80-b1b0-d745d6d5456d \ --scope ${AIF_PROJECT_RESOURCE_ID} # Reader on the Foundry project's resource group az role assignment create --assignee-object-id ${PRINCIPAL_ID} \ --assignee-principal-type ServicePrincipal --role Reader \ --scope /subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${AIF_RESOURCE_GROUP} # Storage Blob Data Contributor on the storage account az role assignment create --assignee-object-id ${PRINCIPAL_ID} \ --assignee-principal-type ServicePrincipal --role "Storage Blob Data Contributor" \ --scope /subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${FUNCTION_RESOURCE_GROUP}/providers/Microsoft.Storage/storageAccounts/${STORAGE_ACCOUNT_NAME}
--assignee-principal-type ServicePrincipal avoids a Microsoft Graph lookup race for a freshly created identity. Re-creating an existing assignment is a no-op, so this block is safe to re-run.
Step 7 Deploy the Code
7a β From the package directory
You extracted the package in P1a. The shipped package.zip already has host.json and requirements.txt at its root, which the remote build requires.
7b β Deploy via zip (remote build)
az functionapp deployment source config-zip -n ${FUNCTION_APP_NAME} \
-g ${FUNCTION_RESOURCE_GROUP} --src package.zip
az functionapp function list -n ${FUNCTION_APP_NAME} -g ${FUNCTION_RESOURCE_GROUP} --query "[].name" -o tsv should list tools_inventory_job.
Step 8 Trigger Manually & Verify
8a β Trigger one run
The function is a timer trigger; invoke it on demand via the admin endpoint.
export MASTER_KEY=$(az functionapp keys list -n ${FUNCTION_APP_NAME} \ -g ${FUNCTION_RESOURCE_GROUP} --query masterKey -o tsv) curl -X POST "https://${FUNCTION_APP_NAME}.azurewebsites.net/admin/functions/tools_inventory_job" \ -H "x-functions-key: ${MASTER_KEY}" -H "Content-Type: application/json" -d '{}'
curl above. Also check logs: az webapp log tail -n ${FUNCTION_APP_NAME} -g ${FUNCTION_RESOURCE_GROUP}.
8b β Confirm the blob exists
az storage blob list --account-name ${STORAGE_ACCOUNT_NAME} \
--container-name tools-inventory --query "[].name" -o tsv
Expected:
8c β Download & check the version
export ACCOUNT_KEY=$(az storage account keys list -n ${STORAGE_ACCOUNT_NAME} \ -g ${FUNCTION_RESOURCE_GROUP} --query "[0].value" -o tsv) az storage blob download --account-name ${STORAGE_ACCOUNT_NAME} --account-key ${ACCOUNT_KEY} \ --container-name tools-inventory --name azure-ai-foundry/tools-inventory.json --file inv.json # Expect "1.5" jq -r '.version' inv.json
version is 1.5, the agents array is non-empty, each agent has a runtimeIdentity, and identityBindings are resolved to role names.
8d β Inspect per-section counts
jq '{agents:(.agents|length), tools:(.tools|length),
knowledgeBases:(.knowledgeBases|length), guardrails:(.guardrails|length),
connections:(.connections|length), toolCredentials:(.toolCredentials|length),
serviceAccounts:(.serviceAccounts|length),
identityBindings:(.identityBindings|length)}' inv.json
Zeros in agents usually mean the wrong AIF_AGENT_TYPE (classic projects answer on AGENTS, newer on ASSISTANTS; use BOTH if unsure), or RBAC had not propagated on the first run.
8e β Activity logs (optional)
If you enabled the collector (ACTIVITY_LOG_ENABLED=true), a second blob azure-ai-foundry/activity-logs.json is written, and the inventory gains activityLogCount and activityLogWarnings. Read activityLogWarnings β each entry names a missing telemetry source and how to fix it.
Updating the Function
Re-running ./deploy.sh re-deploys the current package.zip (idempotent). To ship changed source, rebuild the zip from src/ β host.json and requirements.txt must stay at the zip root β then deploy:
# Rebuild the deployment zip from source ( cd src && zip -r ../package.zip host.json requirements.txt tools_inventory_job -x '*__pycache__*' ) # Redeploy az functionapp deployment source config-zip -n ${FUNCTION_APP_NAME} \ -g ${FUNCTION_RESOURCE_GROUP} --src package.zip # Trigger to verify curl -X POST "https://${FUNCTION_APP_NAME}.azurewebsites.net/admin/functions/tools_inventory_job" \ -H "x-functions-key: ${MASTER_KEY}" -d '{}'
Teardown
The deployment package includes a teardown.sh script that removes all resources (role assignments, Function App, Application Insights, storage account, and optionally the resource group) with confirmation prompts. To run non-interactively:
# Interactive (confirms each resource) ./teardown.sh # Non-interactive ./teardown.sh --force
Resource Summary
| Resource | Variable / Name |
|---|---|
| Resource Group | ${FUNCTION_RESOURCE_GROUP} β rg-tools-inventory |
| Storage Account | ${STORAGE_ACCOUNT_NAME} β runtime + inventory blob |
| Application Insights | ${FUNCTION_APP_NAME}-ai |
| Function App | ${FUNCTION_APP_NAME} β Linux Consumption |
| Managed Identity | System-assigned (${PRINCIPAL_ID}) |
| Inventory Blob | tools-inventory/azure-ai-foundry/tools-inventory.json |
| Schedule | Hourly (timer trigger, built in) |
| Runtime | Python 3.11 (Functions v4) |
| Schema | Inventory v1.5 (8 sections inline) |
| Auth | Managed identity (no secrets) |