Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.azure
.venv
.logfire
.devcontainer
infra

# Common Python and development files to exclude
__pycache__
*.pyc
*.pyo
*.egg-info
.pytest_cache
.ruff_cache
.env
.git
38 changes: 38 additions & 0 deletions agents/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ------------------- 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 can't use --mount=type=cache since Azure Container Apps remote build doesn't support BuildKit:
# https://github.com/Azure/acr/issues/721
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,16 +5,14 @@ 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
param applicationInsightsConnectionString string = ''

resource acaIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
resource agentIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: identityName
location: location
}
Expand All @@ -25,11 +23,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 @@ -40,36 +38,31 @@ 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'
}
// We typically store sensitive values in secrets, but App Insights connection strings are not considered highly sensitive
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: applicationInsightsConnectionString
}
]
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
3 changes: 3 additions & 0 deletions infra/core/host/container-apps.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ param subnetResourceId string = ''

param usePrivateIngress bool = true

param usePrivateAcr bool = false

module containerAppsEnvironment 'container-apps-environment.bicep' = {
name: '${name}-container-apps-environment'
params: {
Expand All @@ -36,6 +38,7 @@ module containerRegistry 'container-registry.bicep' = {
location: location
tags: tags
useVnet: !empty(vnetName)
usePrivateAcr: usePrivateAcr
}
}

Expand Down
4 changes: 2 additions & 2 deletions infra/core/host/container-registry.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ 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 useVnet bool = false // Determines if VNet integration is enabled
param usePrivateAcr bool = false // Determines if public network access should be disabled
param sku object = {
name: useVnet ? 'Premium' : 'Standard' // Use Premium if VNet is required, otherwise Standard
}
Expand All @@ -32,7 +32,7 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-pr
dataEndpointEnabled: dataEndpointEnabled
encryption: encryption
networkRuleBypassOptions: networkRuleBypassOptions
publicNetworkAccess: publicNetworkAccess
publicNetworkAccess: usePrivateAcr ? 'Disabled' : 'Enabled'
zoneRedundancy: zoneRedundancy
}
}
Expand Down
Loading