Skip to content

πŸ”„ Seamlessly migrate Azure API Management & AI Foundry resources between subscriptions with a powerful Python toolkit

Notifications You must be signed in to change notification settings

glennbezanson/azure_foundry_migrations

Repository files navigation

Azure Migration Toolkit

A Python-based toolkit for migrating Azure API Management (APIM) and Azure AI Foundry resources between subscriptions.

Overview

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

Prerequisites

Software Requirements

  • 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)

Azure 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

Install Dependencies

# 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.txt

Understanding Azure AI Foundry Project Types

Azure AI Foundry has two distinct project architectures:

1. Hub-based Projects (Legacy)

  • 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

2. New Foundry Projects

  • 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.

Usage

Quick Start - Full Migration

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-all

Export Only

Export resources without deploying:

python export.py \
  --subscription-id <subscription-id> \
  --resource-group <resource-group> \
  --apim-name <apim-name> \
  --output-dir ./my-export

Deploy from Export

Deploy 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-apim

Generate Configuration Report

Create 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.md

Command Reference

export.py

Exports 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

deploy.py

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 Lists

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-run

Name Mapping

Transform 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.json

name-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.

Resource Tags

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.json

tags.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.

migrate.py

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

What Gets Migrated

API Management

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

Azure AI Foundry

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

Output Structure

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

Post-Migration Steps

After running the migration, you'll need to:

1. Developer Portal Configuration (Entra ID / AAD)

If using Entra ID (Azure AD) authentication for the Developer Portal:

Identity Provider Setup

  • Configure the AAD identity provider in APIM β†’ Developer Portal β†’ Identities
  • Users sign in with their Azure AD accounts - no separate signup required

Product-Group Associations

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 "{}"

User Provisioning

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.

User Subscriptions

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}"
    }
  }'

Home Page Content Updates

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.

2. Re-enter Secrets

Check SECRETS-REQUIRED.md for:

  • Named value secrets (APIM)
  • Backend credentials (APIM)
  • Authorization server client secrets (APIM)
  • Connection credentials (AI Foundry)

2. Configure Networking

  • Set up VNet integration if required
  • Create private endpoints
  • Configure firewall rules/IP restrictions

3. Set Up Identity

  • Configure managed identity permissions
  • Set up Key Vault access policies
  • Update RBAC assignments

4. Create Model Deployments

For Azure OpenAI accounts:

  • Create model deployments in the target
  • Note capacity/quota requirements

5. Test Everything

  • Verify API endpoints respond correctly
  • Test authentication flows
  • Validate Developer Portal access
  • Test AI service connections

Troubleshooting

Developer Portal Export Fails

Ensure:

  1. Node.js 18+ is installed: node --version
  2. Azure CLI is logged in: az account show
  3. Git is available: git --version

APIM Provisioning Takes Long

Creating a new APIM instance takes 30-45 minutes. The script will wait for completion.

Connection Credentials Missing

Connections are exported without credentials for security. After deployment, manually update each connection with the correct credentials.

Prompt Flows Not Appearing

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.

Permission Errors

Ensure your Azure account has:

  • Reader access on source resources
  • Contributor access on target resource group

Exit Codes

Code Meaning
0 Success
1 Partial success with errors
2 Fatal error

Logging

All operations are logged to migration.log in the current directory.

Security Notes

  • Secrets are never exported - they're marked as MASKED - NEEDS_RE-ENTRY
  • Always review SECRETS-REQUIRED.md and MANUAL-STEPS.md after migration
  • Consider rotating secrets during migration
  • Use Key Vault references where possible

License

MIT License

Contributing

Contributions welcome! Please submit issues and pull requests.

About

πŸ”„ Seamlessly migrate Azure API Management & AI Foundry resources between subscriptions with a powerful Python toolkit

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages