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
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
Copy link

Copilot AI Dec 4, 2025

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
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: [email protected]

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: .
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
}
}

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
Copy link

Copilot AI Dec 4, 2025

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
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
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
}
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', '--', '-')
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/'
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