Orpheus Systems Architecture

16. Azure target: API Management in front of Container Apps, provisioned with Terraform

16. Azure target: API Management in front of Container Apps, provisioned with Terraform

Date: 2026-10-22

Status

Proposed

This ADR records the target hosting for a client deployment. Nothing in it is built in this version: the demo runs the server locally behind a dev tunnel (0015-dev-tunnel-for-the-agent-demo.md).


Problem

For a client, the MCP server needs a host that is always reachable by Copilot, starts fast enough for the roughly 10 second action timeout (MCP and API plugins), keeps the OBO credential out of configuration files, ships audit events to a store with retention, and is reproducible per client tenant. The owner also asked for an API gateway in front and for infrastructure as code.

The inbound side has a constraint that rules out the usual "make it private" answer. The caller is Copilot itself. We work from the premise that a declarative agent's plugin call comes from Microsoft's cloud over the public internet, not from the client's network; the pages read for the research document no fixed IP range for it. A private endpoint would make the server unreachable to its only caller. Network injection, which keeps agent traffic inside a customer virtual network, is a feature of Foundry Agent Service (agents injected into a delegated subnet), not of Microsoft 365 declarative agents (Foundry agent networking). So the edge stays public and the protection moves to identity and the gateway.


Decision

Target topology, all of it described in Terraform (one module set, one state per client environment):

  1. API Management is the only public entry. An inbound validate-jwt (or validate-azure-ad-token) policy checks issuer and audience before anything reaches the server (validate-jwt, protect an API with Entra ID), and a per-user rate limit keyed on the token's oid caps runaway loops. The server keeps validating the token itself; the gateway is defence in depth, not a replacement.
  2. Azure Container Apps runs the server as a container image built from the same build output (lib/, package.json, production dependencies) and the same configuration variables as the local run. The image does not exist yet: locally, compose.yml runs the generic m365pnp/spfx development image with the source mounted. A Dockerfile for a slim Node 22 image is work for the target (see Consequences). Ingress allows only API Management (IP restriction rules on the gateway's outbound addresses, IP restrictions). minReplicas is 1, so there is no cold start inside the action timeout.
  3. A managed identity on the Container App pulls the image from the registry and reads Key Vault, which holds the OBO client secret or, preferably, a certificate. No secret in app settings.
  4. Log Analytics receives container logs and a dedicated audit table for the per-tool-call events (0014-three-layer-audit-for-agent-actions.md), with its own retention.

Deployment view of the Azure target

flowchart LR
  CP["Copilot Chat<br/>Microsoft cloud, public internet"] -->|"HTTPS, Entra SSO token"| APIM
  subgraph AZ["Azure subscription, provisioned with Terraform"]
    APIM["API Management<br/>validate-jwt: issuer, audience<br/>rate limit per user"] -->|"forward /mcp"| ACA
    ACA["Container App: MCP server image<br/>ingress allows APIM only<br/>min replicas 1"]
    MI["Managed identity"] -.-> ACA
    KV["Key Vault<br/>OBO client secret or certificate"]
    LA["Log Analytics<br/>diagnostics and audit table"]
    ACR["Container registry"]
    ACA -->|"managed identity"| KV
    ACA -->|"audit events"| LA
    ACR -->|"image pull, managed identity"| ACA
    APIM -->|"gateway logs"| LA
  end
  ACA -->|"OBO token, SharePoint REST"| SP[("SharePoint Online")]
  ACA -->|"OBO exchange"| ENTRA["Microsoft Entra ID"]

Considered Options

Compute

From the research's hosting comparison (choose an Azure service for MCP):

Option Summary Pros Cons
✅ Azure Container Apps, minReplicas 1 A container image of our server, Express + MCP SDK unchanged Same build output and configuration as the local Podman run; logs to Log Analytics out of the box; ingress IP restrictions; plain HTTP app, portable to App Service or AKS One small replica always billed
Azure Functions Flex Consumption Functions host with MCP extension or self-hosted SDK handler Official MCP hosting template (host MCP SDK servers) Second toolchain (Functions Core Tools); always-ready instances billed to avoid cold start (Functions scale)
App Service Container on a plan Familiar Plan sizing; fewer per-app ingress controls than Container Apps
AKS Kubernetes Full control Cluster operations for one small service

Edge

Option Summary Pros Cons
✅ API Management in front Gateway with token validation and rate limit Rejects bad tokens before our code; throttling per user; one place for headers and logging; a stable host name across server moves Cost of an APIM tier; one more hop inside the timeout
Container Apps ingress only, built-in auth Easy Auth on the app Cheaper, fewer parts No rate limiting; less control over token rules
Private endpoint or internal environment No public endpoint Strong network isolation Copilot cannot reach it: the caller comes from Microsoft's cloud over the public internet
Front Door with WAF Global edge WAF rules Adds cost and complexity; JWT validation still needed behind it

Infrastructure as code

Option Pros Cons
✅ Terraform Owner's choice (O6); same tool across Azure and Entra (azuread provider) for app registration, Key Vault and APIM; state per client State storage to manage
Bicep Azure-native, no state file Entra objects need Graph Bicep extension or scripts; the owner chose Terraform
Agents Toolkit m365agents.yml Already used for the agent package Covers the Microsoft 365 app and SSO registration, not Azure infrastructure

Consequences

Positive:

Negative / Trade-offs:

Risks: | Risk ID | Description | Impact (E/H/M/L) | Mitigation | |---------|-------------|------------------|------------| | R-AGT-9 | Cold path (APIM, OBO, SharePoint batch) exceeds the action timeout | M | minReplicas: 1; measure a five-day book_days on the dev tenant first (verification log) | | R-AGT-10 | A future Microsoft feature publishes fixed egress ranges and clients ask for IP allow-listing at APIM | L | Add an APIM ip-filter policy then; the design does not depend on it | | R-AGT-11 | Client has no Azure subscription for this (spec section 9, open item 2) | M | Functions Flex or App Service under an existing subscription; the server is portable |