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.

Foundry inventory Azure Function architecture diagram Shows the Azure Function reading from the AI Foundry data plane, Azure Resource Manager, Azure Resource Graph, and Log Analytics, writing tools-inventory.json to Blob Storage, with the OpenICF connector consuming from both Blob Storage and live AI Foundry APIs. AZURE SUBSCRIPTION AI Foundry Data Plane agents Β· tools Β· connections Azure Resource Manager project / account identity Azure Resource Graph role assignments Log Analytics activity logs (optional) tools-inventory -func Azure Function Β· Python 3.11 Timer Β· hourly Β· MSI Blob Storage container: tools-inventory tools-inventory.json schema v1.5 Β· 8 sections inline activity-logs.json optional collector overwrite each run written by managed identity upload_blob (MSI) PINGONE IDM / OPENIDM HOST OpenICF Connector AzureAIFoundryConnector (Java) Reconciliation β†’ IGA platform read blob (AAD) Live: list agents, tools, connections, guardrails… LEGEND Function reads Blob write / read Connector live API

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.

bash β€” environment setup
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
Configuration via app settings
The Function receives its configuration as Function App settings (set in Step 5), not a mounted file. The deploy.sh script reads deploy.env and applies these for you.

Prerequisites

Before starting, ensure you have:

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:

bash
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:

bash
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.

bash
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}
Provider propagation
Provider registration can take a few minutes to reach the 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.

bash
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.

bash
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 name rules
Storage account names are globally unique and must be 3–24 lowercase letters and digits only (no hyphens). If the name is taken, choose another 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.

bash
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.

bash
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}"
System-assigned identity
The function authenticates to AI Foundry, ARM, and Blob Storage with this identity β€” no secrets, no Key Vault. The object ID changes if the app is recreated, so it is always captured, never hardcoded.

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.

bash
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}"
Why no local Python
With 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:

RoleScopePurpose
Azure AI Userthe Foundry projectRead agents, tools, connections (data plane)
Readerthe Foundry project's resource groupRead ARM identity and role assignments for identity bindings
Storage Blob Data Contributorthe storage accountWrite the inventory blob
bash β€” assign all three roles
# 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}
Idempotent & race-free
--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)

bash β€” ~2-5 minutes
az functionapp deployment source config-zip -n ${FUNCTION_APP_NAME} \
  -g ${FUNCTION_RESOURCE_GROUP} --src package.zip
Verify the deployment
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.

bash β€” 1-2 minutes
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 '{}'
First run wrote nothing?
Role assignments take up to a couple of minutes to propagate. Wait, then re-run the curl above. Also check logs: az webapp log tail -n ${FUNCTION_APP_NAME} -g ${FUNCTION_RESOURCE_GROUP}.

8b β€” Confirm the blob exists

bash
az storage blob list --account-name ${STORAGE_ACCOUNT_NAME} \
  --container-name tools-inventory --query "[].name" -o tsv

Expected:

azure-ai-foundry/tools-inventory.json

8c β€” Download & check the version

bash
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
Confirm
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

bash
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:

bash β€” rebuild & redeploy
# 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:

bash
# Interactive (confirms each resource)
./teardown.sh

# Non-interactive
./teardown.sh --force

Resource Summary

ResourceVariable / 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 IdentitySystem-assigned (${PRINCIPAL_ID})
Inventory Blobtools-inventory/azure-ai-foundry/tools-inventory.json
ScheduleHourly (timer trigger, built in)
RuntimePython 3.11 (Functions v4)
SchemaInventory v1.5 (8 sections inline)
AuthManaged identity (no secrets)