Skip to content

AI Setup

This page is for the platform team. It describes how to enable the AI agents for an environment: the model endpoint in Azure AI Foundry, the spoke agent (Azure DevOps repository) and the integration agent (GitHub repository). It is a reference design: your deployment may differ.

What is needed

Part Needed for
Azure AI Foundry account with model deployments All agents
Spoke agent configuration and repository permissions Spoke agent
Integration agent configuration, GitHub App and repository permissions Integration agent

The agent loop runs inside the portal backend. Foundry is used as a model endpoint (an OpenAI-compatible endpoint that the portal calls with its managed identity). The agents do not run in a Foundry agent service. This follows the accepted decision to move to an in-process agent loop (Decisions).

Subscription and providers

The subscription that hosts Foundry needs these resource providers registered: Microsoft.App, Microsoft.ContainerService, Microsoft.CognitiveServices, Microsoft.KeyVault, Microsoft.Network, Microsoft.Search, Microsoft.Storage. The foundry_platform module also registers providers through an action.

Needs verification

The delivery guidance lists all seven providers and a delegated subnet for AI agents. The Terraform module creates Cosmos DB, AI Search, storage and VNet injection only in bring-your-own mode (see below). In the default mode the search and storage providers and the delegated subnet may not be needed. Confirm with the engineer before you ask the customer to prepare them.

Deploy AI Foundry

Add the foundry_platform module to each environment root and pass the endpoint to the backstage module.

module "foundry_platform" {
  source        = "../../modules/foundry_platform"
  location      = module.canvas.location
  environment   = module.canvas.environment
  workload_name = module.canvas.workload_short_name

  add_data_suffix_to_storage_account = false

  ai_user_principal_ids = [
    module.backstage.production_slot_identity_principal_id,
  ]

  spoke_config = {
    owners_group_object_id                   = "<owners-group-object-id>"
    contributors_group_object_id             = "<contributors-group-object-id>"
    vnet_id                                  = module.canvas.virtual_network_id
    snet_pe_id                               = module.canvas.subnets.default.id
    snet_aif_id                              = module.canvas.subnets.<agents-subnet>.id
    log_analytics_workspace_id               = module.canvas.log_analytics_workspace_id
    application_insights_name                = module.azure_monitor.application_insights_name
    application_insights_resource_group_name = module.azure_monitor.application_insights_resource_group_name
  }
}
module "backstage" {
  # ...
  additional_app_settings = {
    services__foundryOpenAIEndpoint__https__0 = "${module.foundry_platform.platform.ai_foundry_openai_endpoint}/openai/v1"
  }
}
Input Meaning
owners_group_object_id, contributors_group_object_id Entra groups that receive the owner and contributor roles on the Foundry account. Most landing zones have these groups already. Ask the Entra administrator to create them if not.
ai_user_principal_ids Identities that may call the models. Pass the portal's production slot identity, which receives the Foundry user role.
byo_agent_resources_enabled Default false: Foundry uses its Microsoft-hosted default agent backend ("Basic"). true deploys Cosmos DB, AI Search and storage plus VNet injection ("Standard").
foundry_public_access Default false. A private endpoint is always created.
search_sku Default basic, used only with bring-your-own resources

Resources

Resource Created
AI Services account (S0) and project Always
Model deployments Always. Defaults are gpt-5.4 and gpt-5.6-terra, standard data zone deployment, capacity 250.
Private endpoint, log analytics workspace, role assignments Always
Cosmos DB, AI Search, storage, their private endpoints and role assignments Only when byo_agent_resources_enabled is true

Conflicting guidance

The older deployment guide and the module README list Cosmos DB, AI Search, a storage account, a Container App environment and GPT-4o-mini models as always created. The module code does not agree with that. This page follows the code. See Decisions for the status of the related records.

Agent configuration

The agents are enabled in the customer's app-config.<customer>.yaml. The agent definitions (instructions, tools, skills) ship with the code and are rendered from templates, so you set only the switches and the repository.

Spoke agent

integrations:
  azure:
    - host: dev.azure.com
      useForAI: false   # keep false when the agents use the managed identity for repository access
      credentials:
        - clientId: ${BACKSTAGE_CLIENT_ID}
          clientSecret: ${BACKSTAGE_CLIENT_SECRET}
          tenantId: ${BACKSTAGE_TENANT_ID}

agents:
  spoke:
    enabled: true
    repository:
      organization: '<organisation>'
      projectId: '<project>'
      repositoryId: '<repository>'
      branch: main
      paths:
        spoke: '/<spokes-folder>/'
        spokeSchema: '/<schema-file>.json'
        documentation: ''
Field Description
organization, projectId, repositoryId Where the spoke configuration repository lives
branch Branch to read from
paths.spoke Folder with the spoke configuration files
paths.spokeSchema JSON schema used for validation
paths.documentation Optional
additionalSchema, fileNameRules Optional extra schema and file naming rules

Permissions in Azure DevOps (done by a user with organisation-level rights):

  1. Add the portal's identity to the organisation with access level Basic. Without this step the identity does not appear in the repository security settings.
  2. On the spoke repository, grant Contribute to that identity.

Without Contribute, branch and pull request calls fail with HTTP 403. The meaning of the spoke files is in Spoke JSON reference and the process in Change workflow.

Integration agent

The integration agent works on a GitHub repository and authenticates with a GitHub App.

integrations:
  github:
    - host: <host>
      apiBaseUrl: https://<host>/api/v3
      rawBaseUrl: https://<host>/raw
      apps:
        - appId: ${GITHUB_APP_ID}
          installationId: ${GITHUB_APP_INSTALLATION_ID}
          webhookSecret: 'webhook'      # required by the config, not used
          privateKey: ${GITHUB_APP_PRIVATE_KEY}

agents:
  integration:
    enabled: true
    repository:
      host: '<host>'                    # must equal integrations.github[].host
      owner: '<organisation>'
      repositoryName: '<repository>'
      paths:
        serviceBus:
          schemas: '<schemas-folder>/'
          configuration: '<configuration-folder>/'
          documentation: ''

An agentsIntegrations.github block with the same shape lets you give the agents access to GitHub without enabling the other GitHub features of the portal (such as TechDocs scanning).

Setting Source
GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID Literal app settings in the Terraform root. They are not secret.
GITHUB_APP_PRIVATE_KEY Repository secret passed through app_secrets into Key Vault (Configuration)

Install the GitHub App on the target organisation, give it access to the integration repository and grant Contents: read and write and Pull requests: read and write. Without these, branch and pull request operations fail with HTTP 403 or 404. See Integration platform for the platform that this agent configures.

Deploy and verify

Deploy through the pipeline (Deployment and environments), test or acceptance first.

Check Expected
Foundry resources Exist in the expected resource group
Role The portal identity has the Foundry user role on the account
Agent configuration Active in the running portal (the agent shows in the AI Agents page)
Repository access The identity has Contribute on the spoke repository
App settings GITHUB_APP_* and the Foundry endpoint are set on the web app
Spoke agent Test prompt "List all spoke files" works
Integration agent Test prompt "Show me all existing workloads" works
MCP The tool list of the MCP server is not empty