8000 GitHub - AlahmadiQ8/apim-onprem-vnet-poc · GitHub
[go: up one dir, main page]

Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

Repository files navigation

POC: Expose an on-prem API through Azure API Management over a private connection

This proof of concept demonstrates Azure API Management (APIM) privately calling an API that lives "on-premises" — specifically, an on-prem Data API Builder (DAB) MCP server exposed through APIM as a passthrough Model Context Protocol (MCP) server. The on-prem network is simulated as a second Azure VNet joined to the Azure VNet by an IPsec VNet-to-VNet (Site-to-Site style) VPN. Everything is authored in Bicep and driven from your machine with the Azure CLI.

📐 See docs/ARCHITECTURE.md for the full architecture, a per‑module Bicep reference, and Azure‑icon Mermaid diagrams (validated with mmdc).

One-click deploy

Deploy the entire POC into your own subscription — no clone, no CLI, no scripts. Click the button, pick a subscription/region, and select Create:

Deploy to Azure Visualize

The button opens the Azure Portal's custom-deployment form driven by azuredeploy.json (a compiled ARM template) and createUiDefinition.json. A random VPN pre-shared key is generated automatically. The VPN gateways take ~30–45 minutes to provision (the long pole); APIM Standard v2 and the ACI backend come up in a few minutes.

Before you click — the deployment is fully self-contained (the dummy MCP server is embedded in the template, so no clone/build/registry is needed), but a few subscription-level things are outside the template's control:

  • Region: pick one that supports APIM Standard v2 and zone-redundant VPN gateways (e.g. swedencentral, eastus). The form filters obviously-unsupported regions, but SKU availability isn't fully filterable.
  • Resource providers: Microsoft.Network, Microsoft.ContainerInstance, and Microsoft.ApiManagement are auto-registered by the portal. Microsoft.Web (needed for the APIM subnet delegation) usually is already registered; if the deploy fails with MissingSubscriptionRegistration, run az provider register --namespace Microsoft.Web once.
  • Container image: the on-prem dummy MCP server runs on python:3.12-slim (Docker Hub). If your subscription blocks Docker Hub, set Backend container image in the form's Advanced tab to an MCR image such as mcr.microsoft.com/azurelinux/base/python:3.12.

The button requires the template to be reachable at a public URL, which it is because this repository is public. To deploy from a fork, replace AlahmadiQ8/apim-onprem-vnet-poc and main in the two links above with your owner/repo and branch.

Architecture

flowchart LR
  client["Client<br/>curl / browser"]

  subgraph AZ["Azure VNet &middot; vnet-azure &middot; 10.0.0.0/16"]
    direction TB
    subgraph AZAPIM["snet-apim &middot; 10.0.1.0/24"]
      apim@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/api_management_services_48_color.svg", label: "APIM Standard v2 &middot; MCP passthrough", w: 60, h: 60, pos: "b", constraint: "on" }
      nsgA@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/network_security_groups_48_color.svg", label: "nsg-apim", w: 42, h: 42, pos: "b", constraint: "on" }
    end
    subgraph AZGW["GatewaySubnet &middot; 10.0.0.0/27"]
      gwA@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/virtual_network_gateways_48_color.svg", label: "vpngw-azure (VpnGw1AZ)", w: 60, h: 60, pos: "b", constraint: "on" }
      pipA@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/public_ip_addresses_48_color.svg", label: "pip-vpngw-azure", w: 42, h: 42, pos: "b", constraint: "on" }
    end
  end

  subgraph OP["On-prem VNet (simulated) &middot; vnet-onprem &middot; 10.100.0.0/16"]
    direction TB
    subgraph OPGW["GatewaySubnet &middot; 10.100.0.0/27"]
      gwO@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/virtual_network_gateways_48_color.svg", label: "vpngw-onprem (VpnGw1AZ)", w: 60, h: 60, pos: "b", constraint: "on" }
      pipO@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/public_ip_addresses_48_color.svg", label: "pip-vpngw-onprem", w: 42, h: 42, pos: "b", constraint: "on" }
    end
    subgraph OPACI["snet-aci &middot; 10.100.1.0/24"]
      aci@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/container_instances_48_color.svg", label: "on-prem MCP (dummy) &middot; 10.100.1.4:5000/mcp", w: 60, h: 60, pos: "b", constraint: "on" }
      nsgO@{ img: "https://raw.githubusercontent.com/AlahmadiQ8/icons/main/icons/network_security_groups_48_color.svg", label: "nsg-aci", w: 42, h: 42, pos: "b", constraint: "on" }
    end
  end

  client -->|"HTTPS &middot; MCP &middot; public gateway"| apim
  apim -->|"outbound VNet integration"| gwA
  gwA <-->|"IPsec VNet-to-VNet tunnel"| gwO
  gwO -->|"private IP 10.100.1.4:5000"| aci

  nsgA -.->|protects| apim
  nsgO -.->|protects| aci
  pipA -.-> gwA
  pipO -.-> gwO

  classDef vnet fill:#eef5ff,stroke:#3b82f6,color:#1e3a8a;
  classDef subnet fill:#f8fafc,stroke:#94a3b8,color:#334155;
  class AZ,OP vnet;
  class AZAPIM,AZGW,OPGW,OPACI subnet;
Loading

Request path: you -> APIM public gateway (MCP) -> APIM VNet integration (snet-apim) -> vpngw-azure -> IPsec tunnel -> vpngw-onprem -> snet-aci -> on-prem DAB MCP, and back. The client-to-APIM hop is public (Standard v2 integration); the APIM-to-backend hop is fully private — which is what "expose an on-prem MCP server over a private connection" means. APIM runs in passthrough MCP mode (streamable HTTP), proxying MCP calls straight through to DAB's /mcp endpoint rather than modeling it as a REST API.

Why these choices

Decision Choice Notes
On-prem simulation 2nd Azure VNet + its own VPN gateway Fully reproducible, no physical hardware
Connectivity VNet-to-VNet IPsec VPN Same IPsec/IKE tunnels as a real Site-to-Site link
APIM Standard v2 + VNet integration Officially supports "external access to private and on-premises backends"; gateway stays public, backend reached privately
APIM subnet delegated Microsoft.Web/serverFarms, /27+, NSG required v2 integration requirement
API shape Passthrough MCP server (type: mcp, streamable HTTP) DAB already speaks MCP, so APIM proxies MCP; requires API version 2025-09-01-preview and backend MCP 2025-06-18+
Backend Azure Container Instances (private IP, port 5000) Runs a dummy MCP server (infra/mcp-dummy/server.py, streamable HTTP, tools echo/add) so the path is testable now; swap the image/command for real DAB when ready
VPN SKU VpnGw1AZ Non-AZ SKUs (VpnGw1) are no longer accepted for new gateways

Prerequisites

  • Azure CLI logged in (az login) with rights to create the resources.
  • Bicep CLI (az bicep), and the resource providers Microsoft.Network, Microsoft.Web, Microsoft.ContainerInstance, Microsoft.ApiManagement (the deploy script registers them).

Deploy

Two ways to deploy the same thing — both use the single orchestrator infra/main.bicep:

A. One click (recommended for customers). Use the Deploy to Azure button above.

B. Command line.

./deploy.sh

deploy.sh submits infra/main.bicep as one resource-group deployment and prints the MCP endpoint plus ready-to-run test commands and the VPN status command at the end. Optional overrides via environment variables: RG, LOCATION, PUBLISHER_EMAIL, PUBLISHER_NAME, GATEWAY_SKU, API_PATH, BACKEND_PORT (the on-prem DAB port, default 5000). ARM provisions the independent modules (VPN, APIM, ACI) concurrently; VPN gateway provisioning is the long pole (~30–45 min).

You can also deploy the template directly without the script:

az group create -n rg-apim-onprem-poc -l swedencentral
az deployment group create -g rg-apim-onprem-poc -f infra/main.bicep

Validate

  1. VPN tunnel connected (both directions):

    az network vpn-connection show -g rg-apim-onprem-poc -n conn-azure-to-onprem --query connectionStatus -o tsv
    az network vpn-connection show -g rg-apim-onprem-poc -n conn-onprem-to-azure --query connectionStatus -o tsv

    Expect Connected.

  2. End-to-end through APIM (client -> public gateway -> VNet integration -> VPN -> on-prem DAB). APIM exposes the MCP server at https://<apim>.azure-api.net/<apiPath>/mcp (default path onprem).

    APIM=$(az apim list -g rg-apim-onprem-poc --query "[0].name" -o tsv)
    SUB=$(az account show --query id -o tsv)
    KEY=$(az rest --method post \
      --url "https://management.azure.com/subscriptions/$SUB/resourceGroups/rg-apim-onprem-poc/providers/Microsoft.ApiManagement/service/$APIM/subscriptions/master/listSecrets?api-version=2024-05-01" \
      --query primaryKey -o tsv)
    
    # Point any MCP client at the endpoint, passing the subscription key as a header:
    npx -y @modelcontextprotocol/inspector \
      --header "Ocp-Apim-Subscription-Key: $KEY" \
      "https://$APIM.azure-api.net/onprem/mcp"
    
    # ...or send a raw MCP initialize handshake with curl:
    curl -s "https://$APIM.azure-api.net/onprem/mcp" \
      -H "Ocp-Apim-Subscription-Key: $KEY" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

    A successful MCP initialize response (server info + capabilities) proves the private path. Then list and call the dummy server's tools (echo, add):

    curl -s "https://$APIM.azure-api.net/onprem/mcp" \
      -H "Ocp-Apim-Subscription-Key: $KEY" \
      -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello over the VPN"}}}'

    Note: the on-prem ACI runs the dummy MCP server in infra/mcp-dummy/server.py, so initialize, tools/list, and tools/call all return real responses. To use a real backend, swap backend-aci.bicep's image + command for a Data API Builder container (with its dab-config.json + database), or point APIM's dabBaseUrl at your actual on-prem DAB.

  3. (Optional) Isolate the VPN path from APIM — deploy a throwaway container in the Azure VNet snet-test subnet and curl the on-prem MCP private IP directly across the VPN.

Files

infra/main.bicep          # one-deployment orchestrator (wires the 5 modules below)
infra/network.bicep       # 2 VNets, subnets, delegations, NSGs
infra/vpn.bicep           # 2 public IPs, 2 VpnGw1AZ gateways, 2 Vnet2Vnet connections
infra/backend-aci.bicep   # on-prem dummy MCP server on ACI (private IP :5000), outputs the IP
infra/mcp-dummy/server.py # the dummy MCP server (stdlib-only, streamable HTTP, embedded via loadFileAsBase64)
infra/apim.bicep          # APIM Standard v2 + outbound VNet integration
infra/apim-mcp.bicep      # passthrough MCP server (type: mcp, serviceUrl = on-prem MCP private IP:port)
azuredeploy.json          # compiled ARM template (target of the Deploy to Azure button)
createUiDefinition.json   # portal form for the Deploy to Azure button
deploy.sh                 # thin CLI wrapper around infra/main.bicep
teardown.sh               # deletes the resource group

azuredeploy.json is generated from infra/main.bicep. After editing any Bicep, regenerate it with az bicep build -f infra/main.bicep --outfile azuredeploy.json and commit the result so the Deploy to Azure button stays in sync.

Tear down (control cost)

./teardown.sh

VPN gateways and APIM Standard v2 accrue hourly cost, so tear down when finished.

Notes & troubleshooting

  • Data API Builder MCP: DAB (1.7+, use 2.0) natively hosts an MCP server over streamable HTTP at /mcp, protocol version 2025-06-18. APIM's "expose an existing MCP server" (passthrough) mode requires backend MCP 2025-06-18+ and API version 2025-09-01-preview — both satisfied here.
  • Don't buffer MCP responses: never read context.Response.Body in any APIM policy on the MCP API — it forces response buffering and breaks MCP streaming. Rate-limit / auth policies are fine. Avoid a response-timeout shorter than your longest tool call.
  • Backend port: nsg-aci opens inbound TCP on the DAB port (BACKEND_PORT, default 5000) from the VirtualNetwork tag. If you run DAB on a different port, set BACKEND_PORT so the NSG rule, the ACI container, and the APIM backend URL stay aligned.
  • Making the client-to-APIM hop private too is an optional extension: add an APIM inbound private endpoint and call the gateway from inside a VNet.
  • If APIM can't reach the backend: confirm the VPN connections are Connected, that the APIM subnet keeps default gateway route propagation (no route table disabling BGP propagation), and that nsg-aci allows inbound TCP on the DAB port from the VirtualNetwork tag.
  • Internal virtual network type is not supported on Standard v2 (that needs Premium v2 injection); this POC uses External integration by design.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0