Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.azure
.venv
.logfire
.devcontainer
Comment thread
pamelafox marked this conversation as resolved.
37 changes: 37 additions & 0 deletions agents/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# ------------------- Stage 1: Build Stage ------------------------------
# We use Alpine for smaller image size (~329MB vs ~431MB for Debian slim).
# Trade-off: We must install build tools to compile native extensions (cryptography, etc.)
# since pre-built musl wheels aren't available. Debian -slim would skip compilation
# but produces a larger final image due to glibc wheel sizes.
FROM python:3.13-alpine AS build

# Install build dependencies for packages with native extensions (cryptography, etc.)
# https://cryptography.io/en/latest/installation/#building-cryptography-on-linux
RUN apk add --no-cache gcc g++ musl-dev python3-dev libffi-dev openssl-dev cargo pkgconfig

COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /uvx /bin/

WORKDIR /code

# Copy dependency files and install dependencies (for layer caching)
# Note: We avoid --mount=type=cache since Azure Container Apps remote build doesn't support BuildKit

Copilot AI Dec 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment states "Azure Container Apps remote build doesn't support BuildKit" but this may be outdated. As of 2024, Azure Container Apps supports BuildKit features in remote builds. Consider verifying this limitation with current Azure documentation, as using --mount=type=cache can significantly improve build performance by caching package downloads between builds.

Suggested change
# Note: We avoid --mount=type=cache since Azure Container Apps remote build doesn't support BuildKit
# Note: As of 2024, Azure Container Apps remote build supports BuildKit. Consider using --mount=type=cache to cache package downloads and improve build performance.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a link to open issue about it

COPY uv.lock pyproject.toml ./
RUN uv sync --locked --no-install-project

# Copy the project and sync
COPY . .
RUN uv sync --locked

# ------------------- Stage 2: Final Stage ------------------------------
FROM python:3.13-alpine AS final

RUN addgroup -S app && adduser -S app -G app

COPY --from=build --chown=app:app /code /code

WORKDIR /code/agents
USER app

ENV PATH="/code/.venv/bin:$PATH"

ENTRYPOINT ["python", "agentframework_http.py"]
15 changes: 12 additions & 3 deletions azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@
name: python-mcp-demo
metadata:
template: python-mcp-demo@0.0.1

services:
# Not using remoteBuild due to private endpoint usage
aca:
server:
project: .
language: docker
host: containerapp
docker:
remoteBuild: true
path: ./servers/Dockerfile
context: .
agent:
project: .
language: docker
host: containerapp
docker:
remoteBuild: true
path: ./agents/Dockerfile
context: .
Comment thread
pamelafox marked this conversation as resolved.
hooks:
postprovision:
posix:
Expand All @@ -21,4 +30,4 @@ hooks:
windows:
shell: pwsh
run: ./infra/write_env.ps1
continueOnError: true
continueOnError: true
33 changes: 13 additions & 20 deletions infra/aca.bicep → infra/agent.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ param tags object = {}
param identityName string
param containerAppsEnvironmentName string
param containerRegistryName string
param serviceName string = 'aca'
param serviceName string = 'agent'
param exists bool
param openAiDeploymentName string
param openAiEndpoint string
param cosmosDbAccount string
param cosmosDbDatabase string
param cosmosDbContainer string
param mcpServerUrl string

resource acaIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
resource agentIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: identityName
location: location
}
Expand All @@ -24,11 +22,11 @@ module app 'core/host/container-app-upsert.bicep' = {
name: name
location: location
tags: union(tags, { 'azd-service-name': serviceName })
identityName: acaIdentity.name
identityName: agentIdentity.name
exists: exists
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
ingressEnabled: true
ingressEnabled: false
env: [
{
name: 'AZURE_OPENAI_CHAT_DEPLOYMENT'
Expand All @@ -39,31 +37,26 @@ module app 'core/host/container-app-upsert.bicep' = {
value: openAiEndpoint
}
{
name: 'RUNNING_IN_PRODUCTION'
value: 'true'
name: 'API_HOST'
value: 'azure'
}
{
name: 'AZURE_CLIENT_ID'
value: acaIdentity.properties.clientId
}
{
name: 'AZURE_COSMOSDB_ACCOUNT'
value: cosmosDbAccount
value: agentIdentity.properties.clientId
}
{
name: 'AZURE_COSMOSDB_DATABASE'
value: cosmosDbDatabase
name: 'MCP_SERVER_URL'
value: mcpServerUrl
}
{
name: 'AZURE_COSMOSDB_CONTAINER'
value: cosmosDbContainer
name: 'RUNNING_IN_PRODUCTION'
value: 'true'
}
]
targetPort: 8000
}
Comment on lines +19 to +56

Copilot AI Dec 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The agent container runs agentframework_http.py, which executes a single query and then exits. With the default containerMinReplicas: 1, this will cause the container to continuously restart after completing its task, wasting resources and potentially causing cost issues.

Consider either:

  1. Setting containerMinReplicas: 0 and containerMaxReplicas: 0 for the agent if it's meant to be triggered manually
  2. Or modifying agentframework_http.py to run as a long-lived service with a loop or server endpoint
  3. Or using Azure Container Apps Jobs instead of a Container App for one-off task execution

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +56

Copilot AI Dec 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The agent container is missing the APPLICATIONINSIGHTS_CONNECTION_STRING environment variable that the server has (line 695 in main.bicep). This inconsistency means the agent won't have access to Application Insights for monitoring and telemetry, making it harder to debug issues when deployed.

Consider adding an applicationInsightsConnectionString parameter to agent.bicep and passing it as an environment variable, similar to how it's done in server.bicep.

Copilot uses AI. Check for mistakes.
}

output identityPrincipalId string = acaIdentity.properties.principalId
output identityPrincipalId string = agentIdentity.properties.principalId
output name string = app.outputs.name
output hostName string = app.outputs.hostName
output uri string = app.outputs.uri
Expand Down
4 changes: 4 additions & 0 deletions infra/core/host/container-app-upsert.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ param containerCpuCoreCount string = '0.5'
@description('Memory allocated to a single container instance, e.g. 1Gi')
param containerMemory string = '1.0Gi'

@description('Health probes for the container')
param probes array = []

resource existingApp 'Microsoft.App/containerApps@2025-01-01' existing = if (exists) {
name: name
}
Expand Down Expand Up @@ -67,6 +70,7 @@ module app 'container-app.bicep' = {
env: env
imageName: exists ? existingApp.properties.template.containers[0].image : ''
targetPort: targetPort
probes: probes
}
}

Expand Down
6 changes: 5 additions & 1 deletion infra/core/host/container-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ param containerCpuCoreCount string = '0.5'
@description('Memory allocated to a single container instance, e.g. 1Gi')
param containerMemory string = '1.0Gi'

@description('Health probes for the container')
param probes array = []

resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
name: identityName
}
Expand Down Expand Up @@ -102,6 +105,7 @@ resource app 'Microsoft.App/containerApps@2025-01-01' = {
cpu: json(containerCpuCoreCount)
memory: containerMemory
}
probes: probes
}
]
scale: {
Expand All @@ -124,5 +128,5 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-pr
output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
output imageName string = imageName
output name string = app.name
output hostName string = app.properties.configuration.ingress.fqdn
output hostName string = ingressEnabled ? app.properties.configuration.ingress.fqdn : ''
output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : ''
16 changes: 15 additions & 1 deletion infra/core/host/container-apps-environment.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,21 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.
location: location
tags: tags
zoneRedundant: false
publicNetworkAccess: 'Enabled'
publicNetworkAccess: usePrivateIngress ? 'Disabled' : 'Enabled'
workloadProfiles: usePrivateIngress
? [
{
name: 'Consumption'
workloadProfileType: 'Consumption'
}
{
name: 'Warm'
workloadProfileType: 'D4'
minimumCount: 1
maximumCount: 3
}
]
: []
Comment on lines +30 to +43

Copilot AI Dec 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When usePrivateIngress is true, workload profiles are defined but the container apps don't specify which profile to use. Azure Container Apps with workload profiles require the workloadProfileName property in the template section. Consider adding a parameter for workloadProfileName and setting it in the container app template, or removing the unused workload profiles to avoid unnecessary costs (the 'Warm' profile with D4 instances will incur charges even if unused).

Copilot uses AI. Check for mistakes.
appLogsConfiguration: useLogging
? {
destination: 'log-analytics'
Expand Down
2 changes: 1 addition & 1 deletion infra/core/host/container-registry.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ param encryption object = {
status: 'disabled'
}
param networkRuleBypassOptions string = 'AzureServices'
param publicNetworkAccess string = useVnet ? 'Disabled' : 'Enabled' // Public network access is disabled if VNet integration is enabled
param publicNetworkAccess string = 'Enabled' // Keep public access enabled for pushing images from local machine
param useVnet bool = false // Determines if VNet integration is enabled
Comment thread
pamelafox marked this conversation as resolved.
Outdated
param sku object = {
name: useVnet ? 'Premium' : 'Standard' // Use Premium if VNet is required, otherwise Standard
Expand Down
75 changes: 55 additions & 20 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ param location string
@description('Id of the user or app to assign application roles')
param principalId string = ''

param acaExists bool = false
param serverExists bool = false

param agentExists bool = false

@description('Location for the OpenAI resource group')
@allowed([
Expand Down Expand Up @@ -150,7 +152,7 @@ module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0
skuName: 'PerGB2018'
dataRetention: 30
publicNetworkAccessForIngestion: useVnet ? 'Disabled' : 'Enabled'
publicNetworkAccessForQuery: useVnet ? 'Disabled' : 'Enabled'
publicNetworkAccessForQuery: 'Enabled' // Keep public query access for debugging - change to 'Disabled' for more security
Comment thread
pamelafox marked this conversation as resolved.
Outdated
useResourcePermissions: true
}
}
Expand Down Expand Up @@ -538,7 +540,7 @@ module monitorPrivateLinkScope 'br/public:avm/res/insights/private-link-scope:0.
tags: tags
accessModeSettings: {
ingestionAccessMode: 'PrivateOnly'
queryAccessMode: 'PrivateOnly'
queryAccessMode: 'Open' // Allow public queries for debugging - change to 'PrivateOnly' for more security
Comment thread
pamelafox marked this conversation as resolved.
Outdated
}
scopedResources: [
{
Expand Down Expand Up @@ -653,23 +655,41 @@ module cosmosDbPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.
}
}

// Container app frontend
module aca 'aca.bicep' = {
name: 'aca'
// Container app for MCP server
module server 'server.bicep' = {
name: 'server'
scope: resourceGroup
params: {
name: replace('${take(prefix,19)}-ca', '--', '-')
name: replace('${take(prefix,15)}-server', '--', '-')
location: location
tags: tags
identityName: '${prefix}-id-aca'
identityName: '${prefix}-id-server'
containerAppsEnvironmentName: containerApps.outputs.environmentName
containerRegistryName: containerApps.outputs.registryName
openAiDeploymentName: openAiDeploymentName
openAiEndpoint: openAi.outputs.endpoint
cosmosDbAccount: cosmosDb.outputs.name
cosmosDbDatabase: cosmosDbDatabaseName
cosmosDbContainer: cosmosDbContainerName
exists: acaExists
exists: serverExists
}
}

// Container app for agent
module agent 'agent.bicep' = {
name: 'agent'
scope: resourceGroup
params: {
name: replace('${take(prefix,15)}-agent', '--', '-')
Comment on lines +684 to +705

Copilot AI Dec 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The container app names use take(prefix,15) which could lead to naming collisions if the first 15 characters of the prefix are identical for both "server" and "agent". For example, if prefix is "myapp-abc123456", both would start with "myapp-abc123456" and only differ by the suffix. While the suffixes "-server" and "-agent" make them unique in this case, the truncation to 15 characters is very conservative and could cause issues with very short environment names.

Consider using take(prefix,19) for both (to match the original pattern) or at least verify that 15 characters + suffix won't exceed Azure Container App name limits (which allow up to 32 characters).

Copilot uses AI. Check for mistakes.
location: location
tags: tags
identityName: '${prefix}-id-agent'
containerAppsEnvironmentName: containerApps.outputs.environmentName
containerRegistryName: containerApps.outputs.registryName
openAiDeploymentName: openAiDeploymentName
openAiEndpoint: openAi.outputs.endpoint
mcpServerUrl: '${server.outputs.uri}/mcp/'
Comment thread
pamelafox marked this conversation as resolved.
exists: agentExists
}
}

Expand All @@ -683,11 +703,21 @@ module openAiRoleUser 'core/security/role.bicep' = {
}
}

module openAiRoleBackend 'core/security/role.bicep' = {
module openAiRoleServer 'core/security/role.bicep' = {
scope: resourceGroup
name: 'openai-role-server'
params: {
principalId: server.outputs.identityPrincipalId
roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User
principalType: 'ServicePrincipal'
}
}

module openAiRoleAgent 'core/security/role.bicep' = {
scope: resourceGroup
name: 'openai-role-backend'
name: 'openai-role-agent'
params: {
principalId: aca.outputs.identityPrincipalId
principalId: agent.outputs.identityPrincipalId
roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User
principalType: 'ServicePrincipal'
}
Expand All @@ -704,13 +734,13 @@ module cosmosDbRoleUser 'core/security/documentdb-sql-role.bicep' = {
}
}

// Cosmos DB Data Contributor role for backend
module cosmosDbRoleBackend 'core/security/documentdb-sql-role.bicep' = {
// Cosmos DB Data Contributor role for server
module cosmosDbRoleServer 'core/security/documentdb-sql-role.bicep' = {
scope: resourceGroup
name: 'cosmosdb-role-backend'
name: 'cosmosdb-role-server'
params: {
databaseAccountName: cosmosDb.outputs.name
principalId: aca.outputs.identityPrincipalId
principalId: server.outputs.identityPrincipalId
roleDefinitionId: '/${subscription().id}/resourceGroups/${resourceGroup.name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosDb.outputs.name}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002'
}
}
Expand All @@ -725,10 +755,15 @@ output AZURE_OPENAI_ENDPOINT string = openAi.outputs.endpoint
output AZURE_OPENAI_RESOURCE string = openAi.outputs.name
output AZURE_OPENAI_RESOURCE_LOCATION string = openAi.outputs.location

output SERVICE_ACA_IDENTITY_PRINCIPAL_ID string = aca.outputs.identityPrincipalId
output SERVICE_ACA_NAME string = aca.outputs.name
output SERVICE_ACA_URI string = aca.outputs.uri
output SERVICE_ACA_IMAGE_NAME string = aca.outputs.imageName
output SERVICE_SERVER_IDENTITY_PRINCIPAL_ID string = server.outputs.identityPrincipalId
output SERVICE_SERVER_NAME string = server.outputs.name
output SERVICE_SERVER_URI string = server.outputs.uri
output SERVICE_SERVER_IMAGE_NAME string = server.outputs.imageName

output SERVICE_AGENT_IDENTITY_PRINCIPAL_ID string = agent.outputs.identityPrincipalId
output SERVICE_AGENT_NAME string = agent.outputs.name
output SERVICE_AGENT_URI string = agent.outputs.uri
output SERVICE_AGENT_IMAGE_NAME string = agent.outputs.imageName

output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer
Expand Down
9 changes: 6 additions & 3 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
"usePrivateIngress": {
"value": "${USE_PRIVATE_INGRESS=false}"
},
"acaExists": {
"value": "${SERVICE_ACA_RESOURCE_EXISTS=false}"
"serverExists": {
"value": "${SERVICE_SERVER_RESOURCE_EXISTS=false}"
},
"agentExists": {
"value": "${SERVICE_AGENT_RESOURCE_EXISTS=false}"
}
}
}
}
Loading