Deploy to Foundry
Build the image, create an immutable agent version with deploy.py, grant the instance identity, and avoid the known deploy gotchas.
1. Build the image
docker build --platform linux/amd64 -f invoice-summarizer-agent/Dockerfile \
-t invoice-summarizer-agent:$(git rev-parse --short HEAD) .
# or build server-side, with no local push:
az acr build --registry <registry> --platform linux/amd64 \
--image invoice-summarizer-agent:<git-sha> \
-f invoice-summarizer-agent/Dockerfile --no-logs .linux/amd64is required: the platform is x86_64 only.- Tag by git SHA. Never `:latest`, never a reused tag. The platform caches by tag, so a tag rebuilt after a version points at it is a fix that never shipped.
deploy.pyrefuses:latest.
2. Configure .env
FOUNDRY_PROJECT_ENDPOINT=https://<account>.services.ai.azure.com/api/projects/<project>
ANTHROPIC_FOUNDRY_RESOURCE=<account>
AGENT_IMAGE=<registry>.azurecr.io/invoice-summarizer-agent:<git-sha>
ANTHROPIC_DEFAULT_OPUS_MODEL=<opus-deployment-name>
ANTHROPIC_DEFAULT_SONNET_MODEL=<sonnet-deployment-name>
ANTHROPIC_DEFAULT_HAIKU_MODEL=<haiku-deployment-name>
CLAUDE_AGENT_MODEL=sonnet
MAX_TURNS=<from a measured run>
MAX_USD_PER_RUN=<from a measured run>
RUN_TIMEOUT_SECONDS=3300
SESSION_IDLE_TIMEOUT_SECONDS=3600
STEP_TOKENS=on
EMIT_STEP_ACTIVITY_LOG=true
HOSTED_CPU=2
HOSTED_MEMORY=4Gi
# only used to print a ready-to-paste role assignment
AZURE_SUBSCRIPTION_ID=<subscription-id>
AZURE_RESOURCE_GROUP=<resource-group>There are no secrets in this file. ANTHROPIC_FOUNDRY_RESOURCE is the resource name only. Sandbox tiers are 0.5/1Gi, 1/2Gi and 2/4Gi; start on 2/4Gi. Guessing low doesn't give a slow run, it gives a container that dies at turn start with no envelope.
3. Run deploy.py
pip install "azure-ai-projects>=2.6.0" azure-identity python-dotenv
az login
python deploy.pydeploy.py doesn't build or push. It creates a version from the image, waits for it to become active, routes 100% of the endpoint to it, and prints both agent principals and the endpoint URL.
HostedAgentDefinition(
cpu=CPU,
memory=MEMORY,
container_configuration=ContainerConfiguration(image=image),
environment_variables=agent_environment(foundry_resource),
protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")],
session_configuration=SessionConfiguration(
idle_timeout_seconds=timedelta(seconds=SESSION_IDLE_TIMEOUT_SECONDS)),
)The version's environment sets CLAUDE_CODE_USE_FOUNDRY=1, ANTHROPIC_FOUNDRY_RESOURCE, the three pins, CLAUDE_AGENT_MODEL, MAX_TURNS, MAX_USD_PER_RUN (required, no defaults), RUN_TIMEOUT_SECONDS, CLAUDE_CODE_MAX_RETRIES (default 5), STEP_TOKENS, EMIT_STEP_ACTIVITY_LOG, and any integration settings you add. All of it is immutable per version, so "what limit was this run held to" is answerable from the deployment record.
Version payload gotchas
- Protocol `responses` version `2.0.0`, not `1.0.0`.
1.0.0gives an opaqueinternal server erroron every request while the container looks healthy. - Session idle timeout on every version. Without it every run longer than 15 minutes dies. It needs
SessionConfiguration, and the field is atimedelta, not an integer. - N
AGENT_*orFOUNDRY_*names** inenvironment_variables: the platform rejects the whole version as reserved. Container defaults for those names go in the Dockerfile. The platform injectsFOUNDRY_AGENT_VERSION,FOUNDRY_AGENT_NAMEandFOUNDRY_PROJECT_ENDPOINT, which you can read. - Rollback is a routing change, not a rebuild. Versions are immutable and retained; point the version selector at a previous one.
4. Grant the identities
A hosted agent carries two principals: a blueprint and an instance identity. The sandbox authenticates as the instance identity. Granting the blueprint looks right in the portal and produces 401 Principal does not have access to API/Operation. The instance identity is stable across versions, so you grant it once.
az role assignment create \
--assignee-object-id <instance-identity-principal-id> \
--assignee-principal-type ServicePrincipal \
--role "Foundry User" \
--scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CognitiveServices/accounts/<account>"- Account scope, not project. Claude Code calls the account's
/anthropic/v1/messagesendpoint itself, and account-level endpoints aren't proxied by the project. - Use `--assignee-object-id` with `--assignee-principal-type ServicePrincipal`. The plain
--assigneeform does a Graph lookup a non-privileged account can't perform on these principals. - Image pull uses the account and project managed identities, not the agent's.
image_pull_failed,UnauthorizedAcrPullorInvalidAcrPullCredentialsmean they lack classic AcrPull on the registry.Container Registry Repository Readeris inert on a registry in legacy permissions mode. The registry also needsazureADAuthenticationAsArmPolicyenabled. - Every outbound server the skill calls must allow the same instance identity: an app role and, if the server keeps one, its caller allowlist.
Outbound integrations
An unattended agent that must write to a Microsoft 365 document library or any Graph-backed store needs an app-to-app MCP server the brain calls. Foundry's built-in document-library tool is read-only grounding and refuses managed identities. When that API is app-only, no person can hold a token for it, so {"mode":"preflight"} is the only test. Adding a delegated scope so a person can probe it removes the server's security property.
Runtime notes
CLAUDE_CODE_MAX_RETRIESis bounded at 5. Foundry returns no rate-limit headers, and the CLI's default backoff makes a hard failure look like a hang.- There is no per-request caller authorization on the data plane. Any principal with project data-plane access can invoke a run and spend up to
MAX_USD_PER_RUN. Don't route a production agent to callers beyond the deploying team, and say so in its README. - Set an Azure budget and action group on the subscription. It's the only bound that survives restarts.
Windows workstations
- The Azure CLI can crash with
UnicodeEncodeError: 'charmap'while streaming output. Set$env:PYTHONIOENCODING = "utf-8"and useaz acr build --no-logs. The server-side operation usually succeeded, so check state before retrying. docker pushthrough Docker Desktop's proxy can die partway. Useaz acr build.- Git Bash rewrites a leading
/subscriptions/...argument into a Windows path. Run scope-bearingazcommands from PowerShell or setMSYS_NO_PATHCONV=1. JMESPath filters break in PowerShell; run those from Bash.
Related
Last updated 2026-10-21