| name | foundry-vnet-deploy |
| description | Deploy Azure AI Foundry with **Agent Setup inside a private VNet** — the `15-private-network-standard-agent-setup` Bicep reference architecture. Supports new/existing VNets, resource reuse (CosmosDB / Storage / AI Search / private DNS zones), hosted-agent RBAC, App Insights, and spoke-side peering + APIM-DNS-zone link to a Citadel hub VNet. Read the full skill body for the guided interview, retry logic, and subnet sizing — do not deploy from this summary. USE FOR: foundry private vnet, network-secured foundry, agent injection, capability host, citadel hub peering, vnet-isolated citadel spoke, apim private dns zone link, central dns at scale, hub-spoke private dns, InvalidPrivateDnsZoneIds. DO NOT USE FOR: azd-based deploys (use threadlight-deploy or foundry-hosted-agents), public-network Foundry, APIM cross-resource (use foundry-cross-resource), tenant isolation (use azure-tenant-isolation), Citadel app-layer onboarding (use citadel-spoke-onboarding for APIM products + Foundry connection).
|
| metadata | {"version":"1.2.0"} |
Foundry VNet Deploy — Agent Setup inside a Private VNet
1. Goal
Guide the user step by step to deploy Azure AI Foundry with Agent in a private VNet using the Bicep files from the 15-private-network-standard-agent-setup reference project. The skill collects all required parameters, generates the .bicepparam file, and runs the deployment.
Optional scenario hint. When the user invokes the skill, they may pass a one-line scenario hint such as "new VNet in swedencentral" or "existing VNet with existing DNS zones". Use it to pre-fill defaults during the interview wherever it applies.
Day-2 lifecycle companion. This skill covers greenfield create only. For Day-2 capability host lifecycle (idempotent re-create, delete, soft-delete + purge of the parent account, Subnet already in use redeploy guard, soft-delete recovery) see foundry-caphost-lifecycle. You will need it any time you tear down and redeploy a Foundry caphost into the same VNet/subnet.
2. Architecture being deployed
The deployment creates the following secure network architecture:
┌─────────────────────────────────────────────────────────┐
│ Resource Group │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Virtual Network (VNet) │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Agent Subnet │ │ Private Endpoint Subnet │ │ │
│ │ │ (delegated to │ │ │ │ │
│ │ │ Container Apps) │ │ ● AI Services PE │ │ │
│ │ │ │ │ ● AI Search PE │ │ │
│ │ │ Network │ │ ● Storage PE (blob) │ │ │
│ │ │ Injection for │ │ ● CosmosDB PE (SQL) │ │ │
│ │ │ AI Agents │ │ │ │ │
│ │ └──────────────────┘ └──────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌────────────┐ ┌───────────────────┐ │
│ │ AI Services │ │ AI Search │ │ Storage Account │ │
│ │ (Foundry) │ │ (standard) │ │ (StorageV2, ZRS) │ │
│ │ ● SKU S0 │ │ ● disabled │ │ ● public access │ │
│ │ ● public │ │ public │ │ disabled │ │
│ │ disabled │ │ access │ │ ● shared key │ │
│ │ ● network │ │ │ │ disabled │ │
│ │ injection │ │ │ │ │ │
│ └──────────────┘ └────────────┘ └───────────────────┘ │
│ │
│ ┌──────────────┐ ┌────────────────────────────────────┐ │
│ │ CosmosDB │ │ Private DNS Zones (6 zones) │ │
│ │ (SQL API) │ │ ● privatelink.services.ai.azure.. │ │
│ │ ● public │ │ ● privatelink.openai.azure.com │ │
│ │ disabled │ │ ● privatelink.cognitiveservices.. │ │
│ │ ● local auth │ │ ● privatelink.search.windows.net │ │
│ │ disabled │ │ ● privatelink.blob.core.windows.. │ │
│ │ │ │ ● privatelink.documents.azure.com │ │
│ └──────────────┘ └────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ AI Foundry Account Capability Host │ │
│ │ ● capabilityHostKind: Agents │ │
│ │ ● customerSubnet → Agent Subnet (VNet injection) │ │
│ │ ● Replaces manual createCapHost.sh │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ AI Foundry Project │ │
│ │ ● System-Assigned Managed Identity │ │
│ │ ● Connections: CosmosDB, Storage, AI Search │ │
│ │ ● Project Capability Host (Agents) with: │ │
│ │ - vectorStoreConnections → AI Search │ │
│ │ - storageConnections → Blob Storage │ │
│ │ - threadStorageConnections → CosmosDB │ │
│ │ ● Depends on Account Capability Host │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Role Assignments (Project SMI): │
│ ● Storage Blob Data Contributor (account level) │
│ ● Storage Blob Data Owner (container level, scoped) │
│ ● Cosmos DB Operator (account level) │
│ ● Cosmos DB Built-in Data Contributor (enterprise_memory)│
│ ● AI Search Index Data Contributor │
│ ● AI Search Service Contributor │
│ └───────────────────────────────────────────────────────┘
└──────────────────────────────────────────────────────────┘
3. Workflow
Follow these steps IN ORDER. Use the ask_user tool for every question.
Step 0: Choose the template (Decision Guide)
This skill vendors two Foundry network-isolation templates. Pick one before
the interview — they share Steps 1-8b/12/13 but differ on BYO resources and DNS.
| You need… | Template | TEMPLATE_DIR |
|---|
| BYO Azure AI Search + Storage + Cosmos wired as project connections (vector store, thread storage, file storage) | standard-agent (template 15) | templates/standard-agent |
| Platform-managed storage — no BYO Search/Storage/Cosmos, smallest network surface, optional private ACR | basic-vnet (template 11) | templates/basic-vnet |
Ask the user:
- Do your agents need BYO Search / Storage / Cosmos connected to the project?
- Yes / not sure / need vector search + custom thread storage →
standard-agent.
- No — platform-managed storage is fine →
basic-vnet.
Set the working directory for every later deploy/verify command:
TEMPLATE_DIR="templates/standard-agent"
TEMPLATE_DIR="templates/basic-vnet"
What differs by fork (both keep the four awesome-gbb integrations —
hosted-agent developer RBAC, App Insights project-MI roles, Citadel spoke
peering, APIM DNS link):
| Aspect | standard-agent (15) | basic-vnet (11) |
|---|
| BYO resources | AI Search + Storage + Cosmos (create or reuse) | none (platform-managed) |
| Project connections | CosmosDb, CognitiveSearch, AzureStorageAccount (+ appinsights) | appinsights only |
| Private endpoints | AI Services, Search, Storage(blob), Cosmos(SQL) (4) | AI Services + Monitor PLS (2) + ACR (optional → 3) |
| Private DNS zones | 6 (services.ai, openai, cognitiveservices, search, blob, documents) | 3 app (services.ai, openai, cognitiveservices) + azurecr.io (if ACR) + 4 monitor zones |
| Optional private ACR | no | yes (enableContainerRegistry, default true; developerIpCidr) |
| Account capability host | yes (accountCapHost) | project caphost only (projectCapHost = caphostproj) |
| Role assignments | 6 (project-MI over BYO) + optional developer/telemetry | telemetry (LAW Reader + Azure AI User) + optional developer + ACR AcrPull |
| Deploy time | 45-90 min | 30-60 min (no BYO wiring) |
Steps 7 (BYO existing resources) and 8 (6-zone DNS map) apply to
standard-agent only. On basic-vnet, skip Step 7 entirely; in Step 8
the DNS map uses the basic zone set (existingDnsZones = 3 app zones +
optional azurecr.io; existingMonitorDnsZones = 4 monitor zones), and you
additionally ask about the optional private ACR (enableContainerRegistry,
developerIpCidr).
Step 1: Verify prerequisites
Before starting, verify:
- Run
az account show to confirm the user is logged in to Azure CLI.
- If NOT logged in, indicate that they must run
az login first.
- Show the current subscription and ask whether it is correct.
Step 2: Subscription and Resource Group
Ask the user:
- Subscription: Do they want to use the current subscription or change it? If they want to change, ask for the subscription ID.
- Resource Group: Does the resource group already exist or does it need to be created?
- If it needs to be created, ask for the name and location.
- If it already exists, ask for the name.
Step 3: Location
Ask for the deployment region. The allowed regions are:
- westus, eastus, eastus2, japaneast, francecentral, spaincentral, uaenorth
- southcentralus, italynorth, germanywestcentral, brazilsouth, southafricanorth
- australiaeast, swedencentral, canadaeast, westeurope, westus3, uksouth, southindia
- koreacentral, polandcentral, switzerlandnorth, norwayeast (Class B and C only)
Present the options as a list and offer swedencentral as the recommended one.
Step 4: AI service and project name
Ask:
- aiServices: Prefix for the AI Services resource (default:
foundry). A unique suffix will be appended automatically.
- firstProjectName: Project name (default:
project).
- displayName: Visible project name (default: same as the project name).
- projectDescription: Project description (default:
A project for the AI Foundry account with network secured deployed Agent).
- accountCapHost: Name of the account-level capability host (default:
caphostacct). This resource sets customerSubnet for the agent runtime network injection.
Step 5: Model configuration
Ask:
- modelName: Name of the model to deploy (default:
gpt-4.1). Common options: gpt-4.1, gpt-4o, gpt-4o-mini, gpt-4.1-mini, gpt-4.1-nano.
- modelFormat: Model provider (default:
OpenAI).
- modelVersion: Model version. Depends on the chosen model.
- modelSkuName: Deployment SKU (default:
GlobalStandard). Options: GlobalStandard, Standard, ProvisionedManaged.
- modelCapacity: TPM (tokens per minute) in thousands (default:
30 = 30K TPM).
Step 6: Network configuration (VNet)
Ask the user:
Are you going to use an existing VNet or create a new one?
Subnet sizing (agent injection). The agent subnet is delegated to
Microsoft.App/environments and consumes ~1 IP per ~10 running agent pods.
The platform caps concurrent agent sessions at 50 per subscription per
region — that ceiling does not scale with a bigger subnet. A larger
subnet buys project density (~250 projects at low traffic, as few as ~25
at full scale) and upgrade/scale headroom, not more sessions.
| Agent subnet CIDR | Usable IPs | Concurrent sessions | Use when |
|---|
| /24 | 251 | 50 (platform cap) + upgrade buffer | Production default (Microsoft-recommended) |
| /25 | 123 | 50 (platform cap) | Buffer between /26 and /24 |
| /26 | 59 | ~50 — minimum to reach the cap | Smallest that supports the full 50 |
| /27 | 27 | ~17 | Dev/test only — minimum, risky |
Rules: RFC-1918 only (10/8, 172.16/12, 192.168/16) — the CGNAT
100.64.0.0/10 range is not supported (routing failures). Avoid
172.17.0.0/16 (reserved by Docker bridge). Target < 80 % utilization.
Prompt-agent revisions do not consume subnet IPs; hosted-agent revisions
do (100 active / 1000 total per agent name; ~200 hosted agents per Foundry
instance). The PE subnet only needs one IP per private endpoint (a /27 is
plenty). Full IP math and exhaustion symptoms:
references/agent-networking.md.
Option A: New VNet
Ask:
- vnetName: VNet name (default:
agent-vnet).
- vnetAddressPrefix: Address space (default:
192.168.0.0/16). Also supports 10.x.x.x/16 or 172.16.x.x/16.
- agentSubnetPrefix: CIDR of the agent subnet (default: automatically calculated as the first /24 of the address space).
- peSubnetPrefix: CIDR of the private endpoint subnet (default: automatically calculated as the second /24).
- agentSubnetName: Name of the agent subnet (default:
agent-subnet).
- peSubnetName: Name of the PE subnet (default:
pe-subnet).
Option B: Existing VNet
Ask:
- existingVnetResourceId: Full resource ID of the VNet. Format:
/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{name}.
- agentSubnetName: Name of the agent subnet (will be created if it does not exist). IMPORTANT: This subnet needs delegation to
Microsoft.App/environments.
- peSubnetName: Name of the private endpoint subnet (will be created if it does not exist).
- agentSubnetPrefix: CIDR of the agent subnet (mandatory if the subnet does not exist). WARNING: Must not overlap with existing subnets.
- peSubnetPrefix: CIDR of the PE subnet (mandatory if the subnet does not exist).
Step 7: Existing resources (optional)
Ask whether the user has existing resources they want to reuse:
- Do you have an existing AI Search? If yes → ask for
aiSearchResourceId (full ARM format).
- Do you have an existing Storage Account? If yes → ask for
azureStorageAccountResourceId.
- Do you have an existing CosmosDB? If yes → ask for
azureCosmosDBAccountResourceId.
If none are provided, all of them will be created automatically. The new resources are created with:
- AI Search: Standard SKU, public access disabled, local auth enabled with AAD.
- Storage: StorageV2, ZRS (or GRS in southindia/westus), public access disabled, shared key disabled.
- CosmosDB: Global Document DB, Session consistency, public access disabled, local auth disabled.
Step 8: Private DNS zones (optional)
Ask:
Do you have existing private DNS zones that you want to reuse?
If NO → all zones will be created automatically (6 zones). Continue to step 9.
If YES → ask:
- dnsZonesSubscriptionId: Subscription where the DNS zones are located (leave empty if it is the same as the deployment).
- For each zone, ask whether it exists and which resource group it is in:
privatelink.services.ai.azure.com
privatelink.openai.azure.com
privatelink.cognitiveservices.azure.com
privatelink.search.windows.net
privatelink.blob.core.windows.net
privatelink.documents.azure.com
The format is an object where each key is the zone name and the value is the resource group (empty = create a new one).
Central DNS at scale (hub-and-spoke). When the spoke is deployed into an enterprise hub-and-spoke topology, the 6 zones above are typically owned by the platform team in a separate subscription (often the connectivity hub). The dnsZonesSubscriptionId parameter combined with the per-zone resource-group map in existingDnsZones is exactly the pattern described in Microsoft's Cloud Adoption Framework — see Private Link and DNS integration at scale for the canonical reference architecture. The deployment principal needs Private DNS Zone Contributor on each zone in the hub subscription to create the VNet links; without this you will hit InvalidPrivateDnsZoneIds at deploy time.
- For deploy-time DNS troubleshooting (including the
InvalidPrivateDnsZoneIds symptom → cause → fix table), see the upcoming foundry-network-runbook skill.
Step 8b: Hosted agent developers (optional but recommended)
Users / service principals that are going to create hosted agents in this Foundry need:
Managed Identity Operator on the Foundry account
Network Contributor on the agent injection subnet
Without these permissions, hosted agent creation will fail with 403 errors when provisioning the MIs or NICs. The template can assign them automatically.
Ask:
- Which users / groups / SPs are going to create hosted agents in this Foundry?
- Ask for a list of AAD objectIds (you can get them with
az ad signed-in-user show --query id -o tsv or az ad user show --id <upn> --query id -o tsv).
- If the user does not want to use it now, leave the list empty and it can be added manually later.
- Principal type (User / Group / ServicePrincipal). Default
User. All IDs must be of the same type (run the module twice if you need to mix types).
Step 8c: Application Insights + Log Analytics (recommended)
The official hosted agent permissions doc lists App Insights + Log Analytics Workspace as required resources:
- They enable agent traces, logs and metrics
- Required for the evaluations feature
- The template creates them + creates an
appinsights connection in the project + assigns to the project MI:
Log Analytics Reader on the workspace
Azure AI User on the account (recommended by the doc so the MI can infer via the project endpoint)
They usually do not need user input; names are auto-generated unless the user wants to customize them.
Optional question:
- Do you want custom names for the Log Analytics workspace and App Insights? If not, they are auto-generated with a unique suffix.
Step 8d: Citadel hub integration (optional)
If the spoke project will be onboarded into an AI Citadel Governance Hub as a spoke (see citadel-spoke-onboarding), this template can create the spoke-side network plumbing in the same deployment. Otherwise skip to Step 9.
Ask:
- Will this Foundry be a Citadel hub spoke? (yes/no — default no).
- If no, leave the four
hub* / apimDnsZone* parameters empty in Step 9 and continue. Citadel onboarding can also be added later via a re-deploy with the same deploymentTimestamp.
- If yes, ask the next questions.
hubVnetResourceId — full ARM ID of the Citadel hub VNet. Format: /subscriptions/{hubSub}/resourceGroups/{hubRg}/providers/Microsoft.Network/virtualNetworks/{hubVnet}.
- The deployment will create only the spoke→hub peering (your RBAC). The hub→spoke reverse peering is hub-team RBAC; the deployment will emit a one-line
az network vnet peering create in the hubReversePeeringCommand output for them to run.
- ⚠️ Address-space requirement: spoke and hub VNets must NOT overlap. Re-validate the values from Step 6 against the hub VNet's address space before continuing.
apimDnsZoneResourceId (optional) — full ARM ID of the existing privatelink.azure-api.net private DNS zone (typically owned by the hub team). Format: /subscriptions/{hubSub}/resourceGroups/{hubDnsRg}/providers/Microsoft.Network/privateDnsZones/privatelink.azure-api.net.
- Required when the Citadel hub APIM uses a private endpoint (most common). Skip when APIM is reachable on a public endpoint.
- The deployment will link the zone to the spoke VNet so the agent resolves
{apim}.azure-api.net to its private IP.
hubPeeringName (optional, default peering-to-hub) — friendly name for the spoke-side peering.
apimDnsZoneLinkName (optional, default foundry-spoke-link) — VNet-link name on the DNS zone (must be unique within the zone).
Pre-flight (run after Step 11 verification, before invoking citadel-spoke-onboarding):
# 1. Both peerings must be Connected
az network vnet peering show --resource-group {rg} --vnet-name {vnetName} `
--name {hubPeeringName} --query peeringState -o tsv # → "Connected"
# 2. DNS resolution from inside the spoke VNet (run from a peered VM/Bastion):
Resolve-DnsName "{apim}.azure-api.net" # must resolve to a 10.x / 192.168.x / 172.16.x private IP
# 3. End-to-end TCP reachability:
Test-NetConnection -ComputerName "{apim}.azure-api.net" -Port 443
If all three pass, the Foundry account in this VNet can now reach the Citadel hub APIM gateway, and you can run citadel-spoke-onboarding against the project to inject the APIM products + Foundry connection.
Step 9: Generate the .bicepparam file
With all the data collected, generate the main.bicepparam file. This skill vendors
two templates (Step 0): the block below is for standard-agent (template 15).
When TEMPLATE_DIR=templates/basic-vnet, use the basic-vnet fork block after the
IMPORTANT notes instead. The deploy-{rg}.bicepparam naming + the "never overwrite
main.bicepparam" rule apply to both.
standard-agent (template 15) — BYO Search / Storage / Cosmos:
using './main.bicep'
param location = '{location}'
param aiServices = '{aiServices}'
param modelName = '{modelName}'
param modelFormat = '{modelFormat}'
param modelVersion = '{modelVersion}'
param modelSkuName = '{modelSkuName}'
param modelCapacity = {modelCapacity}
param firstProjectName = '{firstProjectName}'
param projectDescription = '{projectDescription}'
param displayName = '{displayName}'
param peSubnetName = '{peSubnetName}'
param accountCapHost = '{accountCapHost}'
// Existing resources
param existingVnetResourceId = '{existingVnetResourceId}'
param vnetName = '{vnetName}'
param agentSubnetName = '{agentSubnetName}'
param aiSearchResourceId = '{aiSearchResourceId}'
param azureStorageAccountResourceId = '{azureStorageAccountResourceId}'
param azureCosmosDBAccountResourceId = '{azureCosmosDBAccountResourceId}'
// DNS configuration
param dnsZonesSubscriptionId = '{dnsZonesSubscriptionId}'
param existingDnsZones = {
'privatelink.services.ai.azure.com': '{rg_or_empty}'
'privatelink.openai.azure.com': '{rg_or_empty}'
'privatelink.cognitiveservices.azure.com': '{rg_or_empty}'
'privatelink.search.windows.net': '{rg_or_empty}'
'privatelink.blob.core.windows.net': '{rg_or_empty}'
'privatelink.documents.azure.com': '{rg_or_empty}'
}
param dnsZoneNames = [
'privatelink.services.ai.azure.com'
'privatelink.openai.azure.com'
'privatelink.cognitiveservices.azure.com'
'privatelink.search.windows.net'
'privatelink.blob.core.windows.net'
'privatelink.documents.azure.com'
]
// Network configuration
param vnetAddressPrefix = '{vnetAddressPrefix}'
param agentSubnetPrefix = '{agentSubnetPrefix}'
param peSubnetPrefix = '{peSubnetPrefix}'
// Hosted-agent developer RBAC (optional). Object IDs that will get
// 'Managed Identity Operator' on the account and 'Network Contributor'
// on the agent subnet. Leave empty to skip.
param agentDeveloperPrincipalIds = [
// '00000000-0000-0000-0000-000000000000'
]
param agentDeveloperPrincipalType = 'User'
// Application Insights + Log Analytics workspace (REQUIRED by hosted agent permissions doc).
// Leave empty to auto-name; project gets an `appinsights` connection and the
// project MI receives Log Analytics Reader + Azure AI User on the account.
param logAnalyticsWorkspaceName = ''
param appInsightsName = ''
// Citadel hub integration (optional). Set to non-empty values when the spoke
// will be onboarded as a Citadel hub spoke (Step 8d above + citadel-spoke-onboarding).
// Leave empty to skip — existing flows are unchanged.
param hubVnetResourceId = '{hubVnetResourceId}'
param hubPeeringName = '{hubPeeringName}' // default: 'peering-to-hub'
param apimDnsZoneResourceId = '{apimDnsZoneResourceId}'
param apimDnsZoneLinkName = '{apimDnsZoneLinkName}' // default: 'foundry-spoke-link'
IMPORTANT:
- If a value is empty (not provided by the user), use the empty string
''.
- The
dnsZoneNames parameter must always include the 6 zones.
- If using a new VNet without prefixes, leave the subnet prefixes empty (they will be calculated automatically).
- DO NOT include
deploymentTimestamp in the .bicepparam file. This parameter is always passed via CLI (--parameters deploymentTimestamp=...) so it can be reused on retries without modifying the file.
- Use a descriptive name for the
.bicepparam file (e.g. deploy-{resourceGroup}.bicepparam) so as not to overwrite the project's original main.bicepparam.
basic-vnet fork. When TEMPLATE_DIR=templates/basic-vnet, generate the
param file below instead of the standard block above (no BYO resource IDs,
no 6-zone dnsZoneNames). Before writing existingDnsZones /
existingMonitorDnsZones / enableContainerRegistry values, the key names
below are the vendored templates/basic-vnet/main.bicep defaults — match them
exactly; do not invent variants.
basic-vnet (template 11) — platform-managed storage, optional private ACR:
using './main.bicep'
param location = '{location}'
param aiServices = '{aiServices}'
param modelName = '{modelName}'
param modelFormat = '{modelFormat}'
param modelVersion = '{modelVersion}'
param modelSkuName = '{modelSkuName}'
param modelCapacity = {modelCapacity}
param firstProjectName = '{firstProjectName}'
param projectDescription = '{projectDescription}'
param displayName = '{displayName}'
param projectCapHost = '{projectCapHost}' // default 'caphostproj'
// Network (new or existing VNet — same questions as Step 6)
param existingVnetResourceId = '{existingVnetResourceId}'
param vnetName = '{vnetName}'
param agentSubnetName = '{agentSubnetName}'
param peSubnetName = '{peSubnetName}'
param vnetAddressPrefix = '{vnetAddressPrefix}'
param agentSubnetPrefix = '{agentSubnetPrefix}'
param peSubnetPrefix = '{peSubnetPrefix}'
// Optional private Azure Container Registry (basic-vnet only; default enabled)
param enableContainerRegistry = {true_or_false}
param developerIpCidr = '{developer_ip_cidr_or_empty}'
// DNS (basic zone set). Leave RG empty to create; set RG to reuse hub-owned zones.
param dnsZonesSubscriptionId = '{dnsZonesSubscriptionId}'
param existingDnsZones = {
'privatelink.services.ai.azure.com': '{rg_or_empty}'
'privatelink.openai.azure.com': '{rg_or_empty}'
'privatelink.cognitiveservices.azure.com': '{rg_or_empty}'
'privatelink.azurecr.io': '{rg_or_empty}' // only relevant if enableContainerRegistry
}
param existingMonitorDnsZones = {
'privatelink.monitor.azure.com': '{rg_or_empty}'
'privatelink.oms.opinsights.azure.com': '{rg_or_empty}'
'privatelink.ods.opinsights.azure.com': '{rg_or_empty}'
'privatelink.agentsvc.azure-automation.net': '{rg_or_empty}'
}
// Hosted-agent developer RBAC (optional)
param agentDeveloperPrincipalIds = [
// '00000000-0000-0000-0000-000000000000'
]
param agentDeveloperPrincipalType = 'User'
// Citadel hub integration (optional — same semantics as standard-agent)
param hubVnetResourceId = '{hubVnetResourceId}'
param hubPeeringName = '{hubPeeringName}' // default 'peering-to-hub'
param apimDnsZoneResourceId = '{apimDnsZoneResourceId}'
param apimDnsZoneLinkName = '{apimDnsZoneLinkName}' // default 'foundry-spoke-link'
Step 10: Confirm and deploy
-
Show a complete summary of the configuration to the user, including:
- Subscription and Resource Group
- Region
- AI service and project name
- Model and capacity
- VNet type (new/existing) and subnets
- Reused existing resources
- DNS zones (new/existing)
- Citadel hub integration (if Step 8d was completed): hub VNet ARM ID, APIM DNS zone ARM ID, peering name. Otherwise: "(none — local Foundry only)".
-
Ask the user whether to proceed with the deployment.
-
If they confirm, generate a fixed timestamp for the deployment and store it:
$deployTimestamp = Get-Date -Format 'yyyyMMddHHmmss'
This timestamp will be reused if the deployment has to be retried.
-
Run the deployment passing the timestamp as an explicit parameter:
az deployment group create \
--resource-group {resourceGroup} \
--template-file "$TEMPLATE_DIR/main.bicep" \
--parameters {bicepparam_file} \
--parameters deploymentTimestamp={deployTimestamp} \
--name "foundry-vnet-{deployTimestamp}"
⚠️ CRITICAL — Timestamp idempotency
The Bicep uses uniqueString(resourceGroup().id + deploymentTimestamp) to generate
a unique suffix (uniqueSuffix) that is appended to ALL resource names
(AI Services, CosmosDB, Storage, AI Search, Project). If the timestamp changes between
attempts, a different suffix will be generated and DUPLICATE resources will be created
instead of retrying the existing ones. That is why it is mandatory to fix the timestamp
and reuse it on any retry.
-
The deployment can take 45-90 minutes. The slowest step is the Account
Capability Host (network injection / Container Apps Environment) which can take
30-60+ minutes. Monitor the progress with:
az deployment operation group list --resource-group {resourceGroup} \
--name "foundry-vnet-{deployTimestamp}" \
--query "[].{name:properties.targetResource.resourceName, state:properties.provisioningState}" -o table
-
If the deployment fails, follow the retry procedure in Step 10b.
Step 10b: Safe retry (anti-duplication)
If the deployment fails (frequently due to a timeout of the Account Capability Host),
follow these steps to retry without duplicating resources:
-
Reuse the SAME timestamp ($deployTimestamp) as the original attempt. This is
the most important thing to avoid duplication.
-
Check whether the Account Capability Host was created internally despite the timeout.
Azure sometimes completes the operation after ARM reports a timeout:
# Try to create again — if it returns "Conflict" with provisioningState: Succeeded,
# it means it already exists and works correctly
az rest --method PUT \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/capabilityHosts/{capHostName}?api-version=2025-04-01-preview" \
--body "@caphost-body.json" \
--headers "Content-Type=application/json"
If the error is Conflict with provisioning state: Succeeded → the caphost already exists.
In that case, create the Project Capability Host directly via the REST API:
# Get the project's connection names
az rest --method GET \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}/connections?api-version=2025-04-01-preview" \
--query "value[].{name:name, category:properties.category}" -o table
# Create the project caphost with the connections
# Body: {"properties":{"capabilityHostKind":"Agents","vectorStoreConnections":["searchConn"],"storageConnections":["storageConn"],"threadStorageConnections":["cosmosConn"]}}
az rest --method PUT \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}/capabilityHosts/caphost?api-version=2025-04-01-preview" \
--body "@project-caphost-body.json" \
--headers "Content-Type=application/json"
-
If duplicate resources were created (because of retrying without fixing the timestamp),
identify and delete them:
# List resources — look for names with a suffix different from the original
az resource list --resource-group {rg} --query "[].name" -o tsv
# Delete duplicates (those that do NOT have the original suffix)
az resource delete --ids {duplicate_resource_id}
-
Re-run the full deployment with the original timestamp:
az deployment group create \
--resource-group {resourceGroup} \
--template-file "$TEMPLATE_DIR/main.bicep" \
--parameters {bicepparam_file} \
--parameters deploymentTimestamp={deployTimestamp} \
--name "foundry-vnet-retry-{deployTimestamp}"
-
If the Account Capability Host keeps failing with a timeout after 2 attempts,
use the direct REST API path (step 2 of this block) to create the capability
hosts manually and then run the remaining role assignments via Bicep or CLI.
Step 11: Post-deployment verification
After the deployment (successful or after completing the retry steps), run ALL these
checks and present the results to the user as a status table.
basic-vnet verification differences. On templates/basic-vnet:
- Private Endpoints: expect 2 (AI Services + Monitor PLS) or 3
when
enableContainerRegistry=true (adds the ACR PE). Not 4.
- DNS VNet links: 3 app zones +
azurecr.io (if ACR) + 4 monitor zones.
- Capability host: project caphost only (
caphostproj); there is no
account capability host to verify (skip the account-caphost PUT check).
- Project connections: expect appinsights only (no CosmosDb /
CognitiveSearch / AzureStorageAccount).
- Role assignments: project-MI Log Analytics Reader (LAW) + Azure AI
User (account); ACR AcrPull (if ACR); no Storage/Cosmos/Search roles.
- Skip Steps 11.9 (BYO public-access) and 11.10 (BYO roles) — no BYO resources.
11.1 — AI Services Account
az rest --method GET \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}?api-version=2025-04-01-preview" \
--query "{name:name, state:properties.provisioningState, publicAccess:properties.publicNetworkAccess, sku:sku.name, networkInjections:properties.networkInjections}" -o json
Verify:
provisioningState = Succeeded
publicNetworkAccess = Disabled
networkInjections[0].scenario = agent and points to the correct subnet
11.2 — Model deployment
az rest --method GET \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/deployments?api-version=2025-04-01-preview" \
--query "value[].{name:name, model:properties.model.name, version:properties.model.version, sku:sku.name, capacity:sku.capacity, state:properties.provisioningState}" -o table
Verify: provisioningState = Succeeded, model and capacity match what was requested.
11.3 — Project + Identity
az rest --method GET \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}?api-version=2025-04-01-preview" \
--query "{name:name, state:properties.provisioningState, identityType:identity.type, principalId:identity.principalId}" -o json
Verify: provisioningState = Succeeded, identity.type = SystemAssigned, principalId is not null.
11.4 — Capability Hosts
Account Capability Host — use PUT to verify (GET does not always work with this resource):
# If it returns "Conflict" with "provisioning state: Succeeded" → OK
az rest --method PUT \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/capabilityHosts/{capHostName}?api-version=2025-04-01-preview" \
--body "@caphost-body.json" --headers "Content-Type=application/json"
Project Capability Host:
az rest --method GET \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}/capabilityHosts/caphost?api-version=2025-04-01-preview" \
--query "{name:name, state:properties.provisioningState, kind:properties.capabilityHostKind, vectorStore:properties.vectorStoreConnections, storage:properties.storageConnections, threadStorage:properties.threadStorageConnections}" -o json
Verify:
provisioningState = Succeeded
capabilityHostKind = Agents
- The 3 connections (vectorStore, storage, threadStorage) point to the correct resources
11.5 — Project Connections
az rest --method GET \
--url "https://management.azure.com/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}/connections?api-version=2025-04-01-preview" \
--query "value[].{name:name, category:properties.category, target:properties.target}" -o table
Verify: 3 connections exist (CosmosDb, CognitiveSearch, AzureStorageAccount) with correct targets.
11.6 — VNet and Subnets
az network vnet show --resource-group {rg} --name {vnetName} \
--query "{name:name, addressSpace:addressSpace.addressPrefixes[0], subnets:subnets[].{name:name, prefix:addressPrefix, delegation:delegations[0].serviceName}}" -o json
Verify:
- Address space is correct
agent-subnet delegated to Microsoft.App/environments
pe-subnet without delegation
11.7 — Private Endpoints
az network private-endpoint list --resource-group {rg} \
--query "[].{name:name, status:privateLinkServiceConnections[0].privateLinkServiceConnectionState.status}" -o table
Verify: 4 Private Endpoints, all with status Approved.
11.8 — DNS Zone VNet Links
For each of the 6 DNS zones, verify that the deployment's VNet has an active link.
If reusing existing zones from another resource group:
# For each zone, list links and look for the one pointing to our VNet
$zones = @("privatelink.services.ai.azure.com","privatelink.openai.azure.com",
"privatelink.cognitiveservices.azure.com","privatelink.search.windows.net",
"privatelink.blob.core.windows.net","privatelink.documents.azure.com")
foreach ($z in $zones) {
$all = az network private-dns link vnet list --resource-group {dnsZonesRg} --zone-name $z -o json | ConvertFrom-Json
$mine = $all | Where-Object { $_.virtualNetwork.id -match '{rg}' }
if ($mine -and $mine.provisioningState -eq "Succeeded") {
Write-Host "✅ $z"
} else {
Write-Host "❌ $z → MISSING VNet link"
}
}
⚠️ IMPORTANT: If VNet links are missing, private DNS resolution will not work from the
deployment's VNet. Create them manually:
az network private-dns link vnet create \
--resource-group {dnsZonesRg} \
--zone-name {zoneName} \
--name "{vnetName}-link-{zoneSuffix}" \
--virtual-network {vnetResourceId} \
--registration-enabled false
11.9 — Private resources (public access disabled)
# CosmosDB
az cosmosdb show --name {cosmosName} --resource-group {rg} \
--query "{name:name, publicAccess:publicNetworkAccess}" -o json
# AI Search
az search service show --name {searchName} --resource-group {rg} \
--query "{name:name, publicAccess:publicNetworkAccess}" -o json
# Storage
az storage account show --name {storageName} --resource-group {rg} \
--query "{name:name, publicAccess:publicNetworkAccess, allowSharedKey:allowSharedKeyAccess}" -o json
Verify: publicNetworkAccess = Disabled on the 3 resources. Storage additionally: allowSharedKeyAccess = false.
11.10 — Role Assignments (6 total)
ARM RBAC (5 assignments):
az role assignment list --assignee {principalId} --all \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
Verify the following exist:
Storage Blob Data Contributor (scope: storage account)
Storage Blob Data Owner (scope: storage account, with ABAC condition)
Cosmos DB Operator (scope: cosmos account)
Search Index Data Contributor (scope: search service)
Search Service Contributor (scope: search service)
CosmosDB SQL Role (1 assignment):
az cosmosdb sql role assignment list --account-name {cosmosName} --resource-group {rg} \
--query "[?principalId=='{principalId}'].{scope:scope}" -o table
Verify: Built-in Data Contributor scoped to dbs/enterprise_memory.
11.11 — Summary and result
Present a summary table to the user with the status of each check:
| Component | Status | Detail |
|---|
| AI Services | ✅/❌ | name, SKU, network injection |
| Model | ✅/❌ | name, version, SKU, capacity |
| Project | ✅/❌ | name, identity |
| Account CapHost | ✅/❌ | kind, subnet |
| Project CapHost | ✅/❌ | kind, connections |
| VNet + Subnets | ✅/❌ | address space, delegation |
| Private Endpoints (4) | ✅/❌ | status Approved |
| DNS VNet Links (6) | ✅/❌ | all linked |
| Private resources (3) | ✅/❌ | publicAccess Disabled |
| Role Assignments (6) | ✅/❌ | all present |
If everything is ✅, inform that the deployment is 100% operational.
If there are ❌, indicate what is missing and offer to fix it automatically.
Step 12: VNet access configuration (optional)
Ask the user how they will access the private resources of the VNet. The options are:
- Existing VPN Gateway (P2S) in another VNet → Step 12A
- VM / Bastion / ExpressRoute already available in the same VNet or with peering → Step 12C
- No access configured → needs to create one → Step 12B
- AI Citadel hub integration (peering + DNS link were created in Step 8d / 9 / 10) → Step 12D post-deploy verification
- I will configure it later → skip to Step 13
Step 12 options are not mutually exclusive — a Citadel-spoke deployment commonly also needs operator access (Step 12A/B/C) for az-side validation.
Step 12A — Access via existing VPN Gateway (with peering)
If the user has a VPN Gateway with Point-to-Site configured in another VNet,
bidirectional peering with gateway transit is required so VPN clients can reach
the new VNet.
12A.1 — Identify the VPN Gateway
# Locate the gateway and its VNet
az network vnet-gateway list --resource-group {gatewayRg} \
--query "[].{name:name, vnet:ipConfigurations[0].subnet.id}" -o json
# Get P2S address pool
az network vnet-gateway show --resource-group {gatewayRg} --name {gatewayName} \
--query "{p2sPool:vpnClientConfiguration.vpnClientAddressPool.addressPrefixes}" -o json
--query "{p2sPool:vpnClientConfiguration.vpnClientAddressPool.addressPrefixes}" -o json
#### 12A.2 — Create bidirectional VNet Peering with gateway transit
TWO peerings are required: one from each side. The peering from the gateway's VNet
must enable `allowGatewayTransit`, and the peering from the new VNet must enable
`useRemoteGateways`. This allows P2S clients to reach the new VNet through the
existing gateway.
```powershell
$gatewayVnetId = "/subscriptions/{subId}/resourceGroups/{gatewayRg}/providers/Microsoft.Network/virtualNetworks/{gatewayVnet}"
$newVnetId = "/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}"
# Peering 1: Gateway VNet → New VNet (allows gateway transit)
az network vnet peering create \
--resource-group {gatewayRg} \
--vnet-name {gatewayVnet} \
--name "peering-to-{vnetName}" \
--remote-vnet $newVnetId \
--allow-vnet-access \
--allow-forwarded-traffic \
--allow-gateway-transit
# Peering 2: New VNet → Gateway VNet (uses the remote gateway)
az network vnet peering create \
--resource-group {rg} \
--vnet-name {vnetName} \
--name "peering-to-{gatewayVnet}" \
--remote-vnet $gatewayVnetId \
--allow-vnet-access \
--allow-forwarded-traffic \
--use-remote-gateways
Verify both peerings are in Connected state:
az network vnet peering show --resource-group {gatewayRg} --vnet-name {gatewayVnet} \
--name "peering-to-{vnetName}" --query "{state:peeringState}" -o tsv
az network vnet peering show --resource-group {rg} --vnet-name {vnetName} \
--name "peering-to-{gatewayVnet}" --query "{state:peeringState}" -o tsv
⚠️ REQUIREMENT: The address spaces of both VNets MUST NOT overlap.
If the gateway's VNet uses 10.0.0.0/16, the new VNet should use 10.1.0.0/16 or another
non-overlapping range (this is already validated in Step 6).
12A.3 — Get private IPs of the endpoints
$pes = az network private-endpoint list --resource-group {rg} --query "[].name" -o tsv
foreach ($pe in $pes) {
$nicId = az network private-endpoint show --name $pe --resource-group {rg} \
--query "networkInterfaces[0].id" -o tsv
az network nic show --ids $nicId \
--query "ipConfigurations[].{ip:privateIPAddress, fqdn:privateLinkConnectionProperties.fqdns[0]}" -o table
}
12A.4 — Generate hosts file entries
Generate the lines for C:\Windows\System32\drivers\etc\hosts (or /etc/hosts on Linux/Mac):
# Foundry VNet - {rg} ({vnetName})
{ip} {fqdn}
...
Present the lines to the user and remind them to:
- Edit the hosts file with administrator permissions
- Reconnect the VPN client (disconnect and reconnect) so it loads the routes
for the new IP range through the peering with gateway transit
- Verify connectivity:
Test-NetConnection -ComputerName {fqdn} -Port 443
Note: The hosts file entries are necessary because private DNS resolution
(Private DNS Zones) only works inside the VNet. P2S VPN clients resolve DNS
externally, so they need the hosts entries to point to the private IPs of
the endpoints.
Step 12B — No access configured (create a new VPN Gateway)
If the user has no means of accessing the VNet, they need to create one.
The most common option for development/testing is a VPN Gateway with Point-to-Site.
Inform the user of what they need and offer help to create it:
-
VPN Gateway in the deployment's VNet (or in another VNet with peering):
- Requires a
GatewaySubnet (minimum /27) in the VNet.
- Recommended SKU:
VpnGw1 (sufficient for development, ~30 min to deploy).
- Configure P2S with self-signed certificates or Azure AD auth.
# Create GatewaySubnet in the deployment's VNet
az network vnet subnet create \
--resource-group {rg} \
--vnet-name {vnetName} \
--name GatewaySubnet \
--address-prefixes {gatewaySubnetPrefix} # e.g. 10.1.2.0/27
# Create public IP for the gateway
az network public-ip create \
--resource-group {rg} \
--name "{vnetName}-gateway-pip" \
--allocation-method Static \
--sku Standard
# Create VPN Gateway (takes ~30 minutes)
az network vnet-gateway create \
--resource-group {rg} \
--name "{vnetName}-vpn-gateway" \
--vnet {vnetName} \
--public-ip-addresses "{vnetName}-gateway-pip" \
--gateway-type Vpn \
--vpn-type RouteBased \
--sku VpnGw1 \
--vpn-gateway-generation Generation1 \
--no-wait
-
Faster alternative — Azure Bastion + VM:
- Create a VM in the same VNet (in
pe-subnet or a new subnet).
- Configure Azure Bastion for web access to the VM desktop.
- From the VM, private resources are resolved directly via Private DNS Zones.
- No hosts file needed.
Ask the user which option they prefer and inform that both take ~30 min to deploy.
Step 12C — Already has access (VM, Bastion, ExpressRoute)
If the user already has access to the VNet (or to a peered VNet) via VM, Bastion,
ExpressRoute or other means:
-
If they access from inside the VNet (VM/Bastion): private DNS resolution
works automatically via Private DNS Zones → no hosts file needed.
-
If they access from a different VNet with peering: verify that:
- Bidirectional peering exists with
Connected state
- The DNS zones have VNet links to that VNet too
- If not, create the VNet links (see step 11.8)
-
If they access via ExpressRoute: verify that the ExpressRoute circuit is
connected to the deployment's VNet (directly or via peering) and that the
DNS zones have VNet links configured.
Step 12D — AI Citadel hub spoke (post-deploy verification)
If Step 8d enabled Citadel hub integration, the deployment has already created
the spoke-side peering and (optionally) linked the APIM private DNS zone to
the spoke VNet. Two things still need to happen before citadel-spoke-onboarding
can run successfully against the project:
12D.1 — Hub team creates the reverse peering
Retrieve the one-line az command from the deployment output and hand it to
the Citadel hub team (separate RBAC; they own the hub VNet):
$deploymentName = "foundry-vnet-{deployTimestamp}"
az deployment group show --resource-group {rg} --name $deploymentName `
--query "properties.outputs.hubReversePeeringCommand.value" -o tsv
The output looks like:
az network vnet peering create --resource-group {hubRg} --vnet-name {hubVnet} \
--name peering-from-{spokeVnet} \
--remote-vnet /subscriptions/{spokeSub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{spokeVnet} \
--allow-vnet-access --allow-forwarded-traffic --subscription {hubSub}
Paste it into a Teams/Slack handoff to the hub team.
12D.2 — Pre-flight checklist
Once the hub team confirms the reverse peering is in place, verify end-to-end
from inside the spoke VNet (a peered VM, Bastion, or VPN-connected client):
# 1. Both peerings must be in "Connected" state
az network vnet peering show --resource-group {rg} --vnet-name {vnetName} `
--name {hubPeeringName} --query peeringState -o tsv # → "Connected"
# 2. APIM hostname must resolve to a private IP (10.x / 192.168.x / 172.16.x)
# — this requires the privatelink.azure-api.net VNet link from Step 8d.
Resolve-DnsName "{apim}.azure-api.net"
# 3. End-to-end TCP reachability on 443
Test-NetConnection -ComputerName "{apim}.azure-api.net" -Port 443
NSG egress (manual) — if the customer attached a custom NSG to the agent
subnet (foundry-vnet-deploy never does this, since attaching an NSG to a
delegated subnet is destructive), ensure its outbound rules allow HTTPS (443)
to the hub VNet address space or to the AzureCloud service tag. The default
subnet NSG is permissive and needs no change.
12D.3 — Hand off to citadel-spoke-onboarding
Once 12D.1 + 12D.2 pass, the project is ready to be onboarded as a Citadel
spoke. Inform the operator:
- The Foundry account + project ARM IDs needed by
citadel-spoke-onboarding
are visible in the deployment outputs (spokeVnetId, agentSubnetName).
- Use Option B (Foundry Connection) in
citadel-spoke-onboarding —
Option A (Key Vault secret pull) violates the keyless-by-mandate posture
that VNet-isolated spokes require.
- The hub team-owned APIM gateway URL is what the Foundry connection routes
to; the agent's UAMI is the only credential threaded through the call.
Step 13: Final notes
After successful verification, remind the user:
- To use the Agents they need VNet access (VPN, ExpressRoute, Bastion, or a VM in the VNet).
- They can verify the private IPs of the endpoints with
get_ips_services.ps1.
- The account capability host (with
customerSubnet) and the project one are already created — there is NO need to run createCapHost.sh manually.
- If they have a VPN Gateway with peering configured, they must reconnect the VPN client after creating the peering to load the new routes.
4. Important rules
- Always use
ask_user for each question. DO NOT put multiple questions in a single one.
- Offer default values whenever possible to minimize user input.
- Validate Resource ID formats before continuing (they must start with
/subscriptions/).
- Do not generate the .bicepparam until you have ALL parameters.
- Save the .bicepparam file with a descriptive name (e.g.
deploy-{rg}.bicepparam) in the same directory as main.bicep, never overwrite main.bicepparam.
- The Bicep files live under the
templates/ subfolder of this skill, one directory per template: templates/standard-agent/ (BYO Search/Storage/Cosmos — template 15) and templates/basic-vnet/ (platform-managed storage — template 11). Step 0 selects which one; set TEMPLATE_DIR to that directory and use it (main.bicep, main.bicepparam, modules-network-secured/) as the deployment working directory — copy them out to a workspace folder first if you want to keep the originals pristine.
- If the user passed a one-line scenario hint when invoking the skill (see the Goal section), use it to pre-fill values whenever possible.
- Always generate and store a fixed
deploymentTimestamp before the first deployment. Pass it as --parameters deploymentTimestamp={timestamp} on every attempt (including retries). This guarantees that uniqueSuffix is identical and resources are not duplicated.
- On retries, first verify whether the Account Capability Host completed internally (via REST API PUT → look for "Conflict" error with "Succeeded"). If it already exists, create the Project Capability Host directly via the REST API and then re-run the full deployment with the same timestamp to complete the role assignments.
- Citadel hub integration is opt-in via Step 8d. When the spoke will be onboarded as a Citadel hub spoke, complete Step 12D (hub team creates the reverse peering, all three pre-flight checks pass) before running
citadel-spoke-onboarding. Otherwise the injected APIM Foundry connection's first call will time out — either no route to the hub, or no DNS resolution for {apim}.azure-api.net.
5. References
- Foundry Samples —
15-private-network-standard-agent-setup — Bicep template set under templates/ derives from this Foundry samples reference.
- Agent networking deep-dive — hosted vs prompt agent traffic paths, the ~1-IP-per-10-pods allocation model, subnet sizing against the 50-session platform cap (per subscription/region), hosted-vs-prompt revision IP behavior, and subnet-exhaustion signals:
references/agent-networking.md.
- Agent tools behind the VNet — the tool-by-tool reachability matrix (through-subnet / through-PE / backbone / public / unsupported), isolation feature limits, the public-ACR-for-hosted-agents and can't-change-the-delegated-subnet gotchas, the firewall FQDN allowlist, and troubleshooting:
references/agent-tools-network-isolation.md.
- Original interview/automation logic — Angel Sevillano (Microsoft),
asevillano/foundry-vnet-deploy.
- Related skills —
azure-tenant-isolation (set up first), foundry-hosted-agents (deploy agents into the host this skill creates), threadlight-deploy (azd-based public-network alternative), foundry-cross-resource (APIM cross-resource model wiring on top), citadel-spoke-onboarding — see Step 8d + Step 12D for the network plumbing this skill creates so the deployed Foundry can be onboarded as a Citadel hub spoke, foundry-observability (App Insights wiring if Step 8c was opted in).
- Template fork notice — the
templates/ set vendors two Foundry samples references: templates/standard-agent/ from 15-private-network-standard-agent-setup and templates/basic-vnet/ from 11-private-network-basic-vnet. awesome-gbb adds the same four optional integrations on top of each template: (1) modules-network-secured/spoke-hub-peering.bicep and (2) modules-network-secured/apim-dns-zone-link.bicep behind the hubVnetResourceId / apimDnsZoneResourceId params; (3) a hubReversePeeringCommand output; (4) hosted-agent developer RBAC (agent-developer-role-assignments.bicep + agent-developer-subnet-assignment.bicep) and project-MI telemetry roles (app-insights-role-assignments.bicep). Note App Insights + Log Analytics + private trace ingestion (Monitor Private Link Scope) are upstream-native in template 11 — only the project-MI role delta is awesome-gbb. Future upstream syncs must diff each vendored tree against its origin and re-apply these additions. Pinned SHA + revalidation contract: references/upstream-pin.md.