CAMPUX learn / ai / 05
Project 05 of 09
Public network access: disabled.

Ship it like infrastructure

The whole Warranty Desk stack as Bicep, deployed from GitHub Actions with no secret in the repo, into a Foundry environment where nothing has a public endpoint and every model call goes through a gateway that counts tokens.

build time
8 to 10 hours
cost
Roughly $0.60 to $1.60 an hour while it exists, so about $25 if you leave it up for a working day3
services
Bicep, GitHub Actions, azd, Virtual Network, Private Link, Private DNS, Azure Firewall, API Management, Key Vault
assumes
Project 01, and that you have deployed anything with Bicep before. If not, the CAMPUX bootcamp covers it; this page does not.
exams
Overlaps AI-200, AZ-104, AZ-305
vocabulary
Standard setup, BYO resources, private endpoint, VNet injection, delegated subnet, federated credential, AI gateway

The problem

Projects 01 to 04 were built by hand, by you, in a subscription you own. Campux Retail's platform team will not run that. Their rules are short.

Everything is code and it deploys from a pipeline, not a laptop. Nothing the agent touches has a public endpoint: not the project, not the search index, not the order data. The pipeline holds no secret that could leak. Every model call goes through one gateway so the platform team can cap a team's tokens and see who spent what. And a second environment must be one command away, identical to the first. The platform lead's test is blunt: "if you can reach it from a coffee shop, it is not done."

definition
Standard setup. A Foundry project that brings its own Storage, Azure AI Search and Cosmos DB, so every byte the Agent Service stores lands in resources you own and can put behind private endpoints. The basic setup manages those for you and is the wrong choice here.

The architecture

One virtual network, private endpoints for everything, and a pipeline that holds no secret.

Ship it architecture: GitHub Actions with a federated credential runs Bicep through azd into a virtual network with public access disabled; API Management fronts the Foundry project as an AI gateway; the agent client is injected into a delegated subnet; Azure AI Search, Storage and Cosmos DB sit behind private endpoints with private DNS; Azure Firewall controls egress; Key Vault holds keys. public network access: disabled. If you can reach it from a coffee shop, it is not done. PIPELINE VIRTUAL NETWORK · PUBLIC ACCESS DISABLED GitHub Actionsazd provision + deployfederated credential Bicepstack as code API ManagementAI gateway, no keytoken limit, safety Foundry projectprivate endpointstandard setup, injected Agent clientinjected into your subnet/27, Microsoft.App delegated Azure Cosmos DBprivate endpoint Storageprivate endpoint Azure AI Searchprivate endpointindexer: private execution Private DNS zonesprivatelink records Azure Firewallegress allowlist, logged Key Vaultkeys, never in the pipeline Application Insightstokens per team Microsoft Entra IDGitHub to Azure: federated credential, OIDC · agent to services: managed identities · no secret in the repo, no key in the pipeline 1 2 3 4 5 6 7
livepress play to deploy one environment
Figure 8 The pipeline is outside the network on purpose and holds no secret. Everything the agent touches is inside it, behind a private endpoint, and the template does not create the three on the middle row for you.
  1. Sign in. GitHub Actions authenticates to Azure with a federated credential; there is no client secret to leak.
  2. Provision. azd runs the Bicep from Microsoft's private-network standard-setup template, plus your additions.
  3. Gateway. API Management fronts the model deployment with a per-team token limit, token metrics and content safety.
  4. Inject. The agent client runs in a subnet you own, delegated to Microsoft.App/environments, /27 or larger.
  5. Search privately. Azure AI Search is reached over a private endpoint; the indexer runs in the private execution environment.
  6. Store privately. Agent data lands in your own Storage and Cosmos DB over private endpoints you created.
  7. Egress. Outbound traffic passes an Azure Firewall allowlist: Entra, Application Insights ingestion, nothing else.
Service map, for readers coming from AWS
JobAWS versionThis buildSame idea?
Infrastructure as codeCloudFormation, CDKBicep, driven by azdYes
Pipeline identityGitHub OIDC to an IAM roleGitHub OIDC to an Entra federated credentialYes
Private accessVPC endpointsPrivate endpoints plus private DNS zonesYes
Agent in your networkLambda in a VPCAgent client VNet injection, delegated subnet1Yes
Egress controlNAT plus security groupsAzure Firewall with an FQDN allowlistYes
AI gatewayAPI Gateway plus custom codeAPI Management AI gateway policies2Different. Token limits and metrics are built in

The build

In this order. Decide the network before you create anything; outbound isolation cannot be added to an existing Foundry resource, only redeployed.

  1. Start from Microsoft's template

    Clone the Foundry samples repository and read the Bicep in infrastructure-setup-bicep/15-private-network-standard-agent-setup end to end before running it. Note what it creates and what it does not.

    You can list every resource the template creates from memory, and you noticed it does not create private endpoints for Search, Storage or Cosmos.
  2. The network

    A virtual network with three subnets: one for private endpoints, one delegated to Microsoft.App/environments at /27 or larger for the agent client, and one for a firewall. Private DNS zones for every privatelink domain you will need, linked to the network.

    nslookup for the Foundry endpoint from a VM in the network returns a private IP; from your laptop it returns a public one you cannot use.
  3. Foundry, standard setup, private

    Deploy the Foundry resource and project with public network access Disabled, a private endpoint, VNet injection into the delegated subnet, and your own Storage, AI Search and Cosmos DB as the standard setup. Then create the private endpoints for those three yourself.

    Four private endpoint connections show Approved. The Foundry portal from your laptop says you cannot reach the project. Good.
  4. The agent, in the network

    Deploy the Project 01 agent into this project. Its OpenAPI tool and file search now travel through your subnet and private endpoints; check the tool support table before you rely on any other tool.

    From a VM in the network, the agent answers a warranty question. The Function App's public endpoint is disabled and it still works.
  5. Egress

    Route the agent subnet through Azure Firewall with an allowlist of the FQDNs Foundry needs: Entra sign-in, the Application Insights ingestion endpoints, and nothing else.

    The agent still works. A curl to an unrelated site from the same subnet is blocked and the firewall log shows why.
  6. The AI gateway

    Put API Management in front of the model deployment with managed identity, and add three policies: llm-token-limit per team key, llm-emit-token-metric with a team dimension, and llm-content-safety. Point the agent at the gateway, not the model.

    Set a 200 tokens-per-minute limit for a test key and get a 429 on the third question. Ours did exactly that: 163 tokens consumed on the first call, 37 remaining; 142 on the second, 0 remaining; then 429 Token limit is exceeded. Try again in 55 seconds. Two things to check while you are there: the x-consumed-tokens and x-remaining-tokens headers come back on every call, and the gateway reaches the model with a managed identity, so there is no key anywhere in the policy.4
  7. The pipeline

    GitHub Actions with a federated credential to an Entra app: no client secret anywhere. azd provision then azd deploy on push to main, with a manual approval environment gate for production.

    grep -ri secret .github/ finds nothing. A push deploys; a second environment is one variable change away.
  8. The second environment

    Create staging from the same templates with different parameters. Run the Project 03 evaluation against it.

    Two identical stacks, and the evaluation scores match within noise.
  9. Tear it down
    azd down --purge

    Then check for the things Bicep does not own: the private DNS zone links, the firewall's public IP, and the API Management instance, which can take a while to delete.

    The resource group is empty. Today's cost is under $25, most of it the firewall and gateway hours.

Where it breaks

Cause each one on purpose. Private networking fails silently and blames DNS.

  • az apim create --sku-name Developer     # 35 minutes to Activating -> Succeeded
    then an API pointing at https://<account>.openai.azure.com/openai
    call 1: HTTP 404   Resource not found
    the backend was right. the API had no operation to match the path.
    PUT .../apis/aoai/operations/chat    # POST /deployments/{deployment}/chat/completions
    operation PUT: 201
    
    policy: llm-token-limit tokens-per-minute=200, managed identity to the backend
    6 identical calls through the gateway
    call 1: HTTP 200   consumed=163  remaining=37
    call 2: HTTP 200   consumed=142  remaining=0
    call 3: HTTP 429   Token limit is exceeded. Try again in 55 seconds.
    call 4: HTTP 429   Token limit is exceeded. Try again in 55 seconds.
    the cap held on the third call, and no key appears anywhere in the policy.
    
    ran 3 Sept 2026 One team key, a 200 token-per-minute cap, and the 429 that proves it.
  • the create call is accepted. that is not the same as it working.
    PUT .../accounts/campux-inj-agent28   # subnet 10.10.2.0/28, delegated
    HTTP 201  ACCEPTED
    PUT .../accounts/campux-inj-agent27   # subnet 10.10.3.0/27, delegated
    HTTP 201  ACCEPTED
    
    poll provisioningState
    poll 1:  /28=Creating   /27=Creating
    poll 3:  /28=Creating   /27=Creating
    poll 4:  /28=Failed     /27=Creating
    final:   /28=Failed     /27=Succeeded
    
    the resource itself carries no error. it is in the activity log:
    "code":"ResourceProviderExtensionError","message":"ExtendedErrorInfo:: Kind: AmlRp,
     Code: BadRequest, Message: Capability host creation failed with agent messages"
    nothing about subnet size. you would look at the agent config for an hour first.
    
    ran 3 Sept 2026 Same request twice, one subnet bit apart.
    private endpoints are not created for you. three by hand, then the
    dns zone group writes the A records.
    
    az network private-endpoint list -g rg-campux-net -o table
    Name        State      Ip         Fqdn
    pe-cosmos   Succeeded  10.20.1.6  campux-cosmos.documents.azure.com
    pe-search   Succeeded  10.20.1.5  campux-search.search.windows.net
    pe-storage  Succeeded  10.20.1.4  campuxnet.blob.core.windows.net
    az network private-dns record-set a list -z privatelink.documents.azure.com -g rg-campux-net
    campux-cosmos          10.20.1.6
    campux-cosmos-eastus   10.20.1.7   <- cosmos writes two, account and region
    
    public access off. now call storage from outside, with a good token.
    
    curl -H "Authorization: Bearer $TOKEN" "https://$SA.blob.core.windows.net/?comp=list"
    HTTP 403
    <Error><Code>AuthorizationFailure</Code><Message>This request is not authorized to perform this operation.
    nothing about the network. that is a role-assignment error message.
    az storage container list --account-name $SA --auth-mode login
    ERROR: The request may be blocked by network rules of storage account.
    the cli says it. the api does not.
    
    now egress, for real: a container in a subnet routed 0.0.0.0/0 -> 10.20.0.4
    
    az container logs -g rg-campux-net -n fwtest
    ALLOWED-login.microsoftonline.com
      HTTP 200
    BLOCKED-example.com
      HTTP 000
      curl-exit-35
    BLOCKED-pypi.org
      HTTP 000
      curl-exit-35
    
    exit 35 is "SSL connect error". the allowlist works, and it will
    cost you an hour the first time it stops a package install.
    
    ran 4 Sept 2026 Torn down the same hour. A blocked FQDN never says it was blocked.

The trade-offs

Standard setup, or basic?
Basic is faster and Microsoft manages the storage. Standard means you own every resource the agent writes to and can put each behind a private endpoint. Regulated data, or a platform team with rules, means standard.
Microsoft's template, or your own Bicep?
Their template encodes decisions that took a team months. Your own is smaller and you understand it. Start from theirs, strip what you do not use, and keep the commit history showing what you removed and why.
A gateway in front of the model, or direct?
Direct is one hop faster. The gateway gives per-team token limits, a metric per team, content safety, load balancing and a place to swap models without touching agents. From the second team onward, the gateway pays for itself.
Firewall, or NAT gateway?
A NAT gateway gives egress without inspection. A firewall gives an allowlist and a log. The allowlist is the compliance answer; the log is how you debug it.
Bicep, or Terraform?
Terraform works for Foundry too and is the right answer in a multi-cloud shop. Bicep is what the Foundry samples are written in, so you inherit the most tested path. Pick the one your platform team already reviews.

In the interview

"How would you deploy this for real?"

  • decisionBicep from Microsoft's private-network standard-setup template, driven by azd from GitHub Actions with a federated credential.
  • reasonNo secret in the pipeline, and a second environment is a parameter change.
  • watchedWhat the template does not create: the private endpoints for Search, Storage and Cosmos.

"How is it network-isolated?"

  • decisionPublic access disabled, private endpoints and DNS zones for every service, the agent injected into a delegated subnet, egress through a firewall allowlist.
  • reasonThe platform rule: nothing reachable from outside the network.
  • watchedThe indexer that silently indexed nothing until its execution environment was private.

"How do you control model spend across teams?"

  • decisionAPI Management as the AI gateway with a token limit and a token metric per team key.
  • reasonOne place to cap, count and swap models.
  • watchedA 429 at the limit, and the per-team chart in Application Insights.

Evidence

The two nslookups
The same hostname resolving private from inside and useless from outside.
The 429
The gateway refusing the third question under a test limit, and the per-team token chart.
The repo
Bicep, the workflow file with no secret in it, and a README that lists what the template does not create.
One sentence for the résumé
"Deployed a Microsoft Foundry agent platform as Bicep from GitHub Actions with federated credentials into a private-endpoint-only network with VNet injection and firewall egress control, fronted by an API Management AI gateway enforcing per-team token limits."

Next

Notes

  1. Checked against Microsoft Learn on 2 September 2026. The standard setup requires bring-your-own Storage, Azure AI Search and Cosmos DB; VNet injection needs a subnet delegated to Microsoft.App/environments at /27 or larger; private endpoints for the three BYO resources are not created by the Foundry deployment; outbound networking cannot be added to an existing resource. The sample templates are 11-private-network-basic-vnet, 15-private-network-standard-agent-setup and 19-private-network-agent-tools in the foundry-samples repository.
  2. API Management's AI gateway policies used here are llm-token-limit, llm-emit-token-metric and llm-content-safety; semantic caching (llm-semantic-cache-store and -lookup) needs a Redis-compatible cache and is left out to keep the bill down. The AI gateway inside the Foundry portal was in preview, and a gateway created from there is public until you isolate it yourself.
  3. Run in a real Azure subscription on 3 September 2026, in East US. The subnet test went through the ARM API at api-version=2025-06-01 with networkInjections set to scenario: agent; note the Azure CLI has no flag for this, so VNet injection is an ARM or Bicep property, which is part of why the samples are Bicep. The gateway test used a Developer-tier instance, an llm-token-limit policy and authentication-managed-identity against a gpt-5-mini deployment. Azure Firewall, the private endpoints and the DNS steps were finished on 4 September 2026 in East US, in one resource group torn down the same hour: a Standard-tier firewall with an FQDN allowlist, private endpoints on Storage, Azure AI Search and Cosmos DB, the three matching private DNS zones linked to the virtual network, and public network access disabled on all three resources. Egress was driven through the firewall for real, from a container in a subnet whose route table sent 0.0.0.0/0 to the firewall private address, not merely configured and inspected. The one step still not exercised end to end is the indexer running in the private execution environment, which needs documents and an index this run did not build.
  4. Hourly list prices in East US, checked against the Azure retail price API on 3 September 2026: Azure Firewall Basic $0.395 an hour and Standard $1.25, API Management Developer $0.066 and Standard $0.94, Azure AI Search Basic $0.101, private endpoints about a cent each an hour. Firewall is most of the bill, which is why the range above moves so much with the SKUs you pick. Everything here bills while it exists, not while it is used, so the number that matters is how many hours you leave it up: deploy in the morning and tear down before dinner, or this footnote is wrong by a lot.