A Python-based toolkit for migrating Azure API Management (APIM) and Azure AI Foundry resources between subscriptions.
This toolkit exports resources from a source Azure subscription and deploys them to a target subscription. It handles:
- API Management: Service configuration, APIs, products, policies, backends, named values, and Developer Portal content
- Azure AI Foundry: AI Services accounts, hub-based ML workspaces, projects, connections, and prompt flows
- Python 3.9+
- Node.js 18+ (required for Developer Portal migration)
- npm (comes with Node.js)
- Git (for cloning Microsoft's portal scripts)
- Azure CLI (logged in with appropriate permissions)
The account running the migration needs:
- Source subscription: Reader access to all resources being exported
- Target subscription: Contributor access to the target resource group
# Create and activate virtual environment (recommended)
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install Python dependencies
pip install -r requirements.txtAzure AI Foundry has two distinct project architectures:
- Built on Microsoft.MachineLearningServices/workspaces
- Types: Hub (kind=Hub) and Project (kind=Project)
- Projects are connected to a Hub workspace
- Supports prompt flows stored in workspace file share
- Connections managed via azure-ai-ml SDK
- Built on Microsoft.CognitiveServices/accounts/projects
- Direct child resources of Cognitive Services accounts
- Does NOT support prompt flows (as of late 2024)
- More streamlined but fewer features
The toolkit detects both types automatically and exports them appropriately.
python migrate.py \
--source-subscription-id <source-subscription-id> \
--source-resource-group <source-resource-group> \
--source-apim-name <source-apim-name> \
--target-subscription-id <target-subscription-id> \
--target-resource-group <target-resource-group> \
--target-location eastus \
--target-apim-name <target-apim-name> \
--create-allExport resources without deploying:
python export.py \
--subscription-id <subscription-id> \
--resource-group <resource-group> \
--apim-name <apim-name> \
--output-dir ./my-exportDeploy from a previous export:
python deploy.py \
--source-dir ./azure-export-20240101-120000 \
--subscription-id <target-subscription-id> \
--resource-group <target-resource-group> \
--location eastus \
--apim-name <target-apim-name> \
--create-apimCreate a single markdown file with all exported configuration data:
python report.py --source-dir ./azure-export-20240101-120000
# Output: ./azure-export-20240101-120000/CONFIGURATION-REPORT.md
# Or specify custom output location
python report.py --source-dir ./azure-export-20240101-120000 --output ./my-report.mdExports APIM and AI Foundry resources from a source subscription.
| Parameter | Required | Description |
|---|---|---|
--subscription-id |
Yes | Source Azure subscription ID |
--resource-group |
Yes | Source resource group name |
--apim-name |
No | APIM instance name |
--ai-resource-group |
No | AI resources resource group (if different) |
--output-dir |
No | Output directory (default: ./azure-export-{timestamp}) |
--skip-apim |
No | Skip APIM export |
--skip-ai |
No | Skip AI Foundry export |
--skip-portal |
No | Skip Developer Portal export |
Deploys exported resources to a target subscription.
| Parameter | Required | Description |
|---|---|---|
--source-dir |
Yes | Path to export directory |
--subscription-id |
Yes | Target subscription ID |
--resource-group |
Yes | Target resource group (must exist) |
--location |
Yes | Azure region for new resources |
--apim-name |
No | Target APIM instance name |
--apim-sku |
No | APIM SKU (Developer, Basic, Standard, Premium) |
--create-apim |
No | Create APIM if it doesn't exist |
--ai-services-name |
No | Target AI Services account name |
--create-ai-services |
No | Create AI Services if doesn't exist |
--ai-hub-name |
No | Target AI Hub name |
--create-ai-hub |
No | Create AI Hub if doesn't exist |
--ai-project-name |
No | Target AI Project name |
--create-ai-project |
No | Create AI Project if doesn't exist |
--skip-apim |
No | Skip APIM deployment |
--skip-ai |
No | Skip AI Foundry deployment |
--skip-portal |
No | Skip Developer Portal deployment |
--skip-products |
No | Comma-separated product names to skip |
--skip-backends |
No | Comma-separated backend names to skip |
--skip-apis |
No | Comma-separated API names to skip |
--skip-ai-services |
No | Comma-separated AI Services names to skip |
--name-mapping |
No | Path to JSON file with name/URL transformations |
--tags-file |
No | Path to JSON file with resource tags to apply |
--dry-run |
No | Show what would be deployed |
Skip specific resources during deployment:
python deploy.py \
--source-dir ./azure-export-20240101-120000 \
--subscription-id <target-subscription-id> \
--resource-group <target-resource-group> \
--location eastus \
--apim-name <target-apim-name> \
--skip-products "starter,unlimited" \
--skip-ai-services "test-account" \
--dry-runTransform URLs and resource names during deployment using a JSON mapping file:
python deploy.py \
--source-dir ./azure-export-20240101-120000 \
--subscription-id <target-subscription-id> \
--resource-group <target-resource-group> \
--location eastus \
--apim-name <target-apim-name> \
--name-mapping ./name-mapping.jsonname-mapping.json schema:
{
"apim": {
"api_paths": {
"old/api/path": "new/api/path"
},
"backend_urls": {
"old-backend.domain.com": "new-backend.domain.com"
},
"service_urls": {
"old-service.domain.com": "new-service.domain.com"
}
},
"ai_foundry": {
"account_names": {
"old-account-name": "new-account-name"
}
}
}See name-mapping.example.json for a template.
Apply custom tags to created resources using a JSON tags file:
python deploy.py \
--source-dir ./azure-export-20240101-120000 \
--subscription-id <target-subscription-id> \
--resource-group <target-resource-group> \
--location eastus \
--apim-name <target-apim-name> \
--tags-file ./tags.jsontags.json schema:
{
"Environment": "prod",
"Project": "myproject",
"Owner": "team",
"CostCenter": "IT",
"ManagedBy": "migration-toolkit"
}Tags from the file are merged with any existing tags from the source resources. File tags take precedence when there are conflicts.
See tags.example.json for a template.
All-in-one orchestrator combining export and deploy.
Includes all parameters from export.py and deploy.py, plus:
| Parameter | Required | Description |
|---|---|---|
--export-only |
No | Only export, don't deploy |
--deploy-only |
No | Only deploy from existing export |
--export-dir |
No | Export directory (required for --deploy-only) |
--create-all |
No | Create all resources if they don't exist |
--yes / -y |
No | Skip confirmation prompts |
| Component | Migrated | Notes |
|---|---|---|
| Service configuration | Yes | SKU, networking, hostnames |
| APIs | Yes | Exported as OpenAPI specs |
| Policies | Yes | Global, product, API, operation levels |
| Products | Yes | Including API associations and 'developers' group |
| Backends | Yes | Credentials need re-entry |
| Named Values | Partial | Secret values need re-entry |
| Authorization Servers | Partial | Client secrets need re-entry |
| Tags | Yes | |
| API Version Sets | Yes | |
| Certificates | Metadata only | Certs need re-upload |
| Developer Portal | Yes | Via Microsoft's official scripts |
| Users/Groups | No | Not migrated |
| Subscriptions | No | Not migrated |
| Analytics/Logs | No | Not migrated |
| Component | Migrated | Notes |
|---|---|---|
| AI Services accounts | Yes | Config, SKU, networking |
| OpenAI accounts | Yes | Config only, not deployments |
| Hub workspaces | Yes | Via azure-ai-ml SDK |
| Project workspaces | Yes | Hub-based projects |
| Connections | Partial | Credentials need re-entry |
| Prompt Flows | Yes | Hub-based projects only |
| Datastores | Config only | Not data |
| Model deployments | No | Create fresh in target |
| New Foundry projects | Config only | Prompt flows not supported |
azure-export-{timestamp}/
βββ manifest.json # Export metadata
βββ SECRETS-REQUIRED.md # Secrets needing re-entry
βββ MANUAL-STEPS.md # Post-deployment steps
βββ MIGRATION-REPORT.md # Final report (after migrate.py)
β
βββ apim/
β βββ service.json # APIM configuration
β βββ apis/
β β βββ {api-name}/
β β βββ api.json # API metadata
β β βββ openapi.json # OpenAPI specification
β β βββ policy.xml # API-level policy
β β βββ operations/
β β βββ {op-id}.xml
β βββ products/
β β βββ {product}.json
β β βββ {product}-policy.xml
β βββ backends/
β βββ named-values/
β βββ policies/
β β βββ global.xml
β βββ auth-servers/
β βββ tags/
β βββ version-sets/
β βββ certificates/
β βββ portal/ # Developer Portal content
β
βββ ai-foundry/
βββ detected-resources.json
βββ ai-services/
β βββ {account}/
β βββ config.json
βββ hub-based/
β βββ {workspace}/
β βββ workspace.json
β βββ connections/
β βββ datastores/
β βββ prompt-flows/
β βββ {flow-name}/
β βββ flow.dag.yaml
β βββ *.py
β βββ requirements.txt
βββ foundry-projects/
βββ {project}/
βββ config.json
βββ connections.json
After running the migration, you'll need to:
If using Entra ID (Azure AD) authentication for the Developer Portal:
- Configure the AAD identity provider in APIM β Developer Portal β Identities
- Users sign in with their Azure AD accounts - no separate signup required
Published products need the developers group for signed-in users to see them:
# Add developers group to a product (required for visibility)
az rest --method PUT --uri "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/{apim}/products/{product-id}/groups/developers?api-version=2022-08-01" --body "{}"AAD users need to be added to APIM with their AAD Object ID (GUID):
# Get user's AAD Object ID
az ad user show --id "[email protected]" --query id -o tsv
# Create APIM user with AAD identity
az rest --method PUT \
--uri "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/{apim}/users/{user-id}?api-version=2022-08-01" \
--body '{
"properties": {
"firstName": "First",
"lastName": "Last",
"email": "[email protected]",
"identities": [{
"provider": "Aad",
"id": "<AAD-OBJECT-ID-GUID>"
}]
}
}'Important: The identity id must be the AAD Object ID (GUID), not the email address.
After creating users, create subscriptions for them to access products:
az rest --method PUT \
--uri "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/{apim}/subscriptions/{subscription-name}?api-version=2022-08-01" \
--body '{
"properties": {
"displayName": "User Subscription Name",
"scope": "/products/{product-id}",
"ownerId": "/users/{user-id}"
}
}'The Developer Portal home page is stored as a document. To update it (e.g., endpoint URLs, model names):
# Get the home page document
az rest --method GET --uri "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/{apim}/contentTypes/document/contentItems/e0987ca1-f458-b546-7697-7be594b35583?api-version=2022-08-01"
# Update and PUT back, then publish the portal revision
az rest --method POST --uri "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/{apim}/portalRevisions/master/publish?api-version=2022-08-01"See scripts/update_homepage.py for an automated example.
Check SECRETS-REQUIRED.md for:
- Named value secrets (APIM)
- Backend credentials (APIM)
- Authorization server client secrets (APIM)
- Connection credentials (AI Foundry)
- Set up VNet integration if required
- Create private endpoints
- Configure firewall rules/IP restrictions
- Configure managed identity permissions
- Set up Key Vault access policies
- Update RBAC assignments
For Azure OpenAI accounts:
- Create model deployments in the target
- Note capacity/quota requirements
- Verify API endpoints respond correctly
- Test authentication flows
- Validate Developer Portal access
- Test AI service connections
Ensure:
- Node.js 18+ is installed:
node --version - Azure CLI is logged in:
az account show - Git is available:
git --version
Creating a new APIM instance takes 30-45 minutes. The script will wait for completion.
Connections are exported without credentials for security. After deployment, manually update each connection with the correct credentials.
Prompt flows are only supported in hub-based projects. If migrating to new Foundry projects, prompt flows cannot be deployed - you'll need to recreate them manually or continue using hub-based projects.
Ensure your Azure account has:
- Reader access on source resources
- Contributor access on target resource group
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Partial success with errors |
| 2 | Fatal error |
All operations are logged to migration.log in the current directory.
- Secrets are never exported - they're marked as
MASKED - NEEDS_RE-ENTRY - Always review
SECRETS-REQUIRED.mdandMANUAL-STEPS.mdafter migration - Consider rotating secrets during migration
- Use Key Vault references where possible
MIT License
Contributions welcome! Please submit issues and pull requests.