Skip to content

Commit ccda532

Browse files
Add proxy to Azure Maps (#63)
Co-authored-by: Simon Kurtz <[email protected]>
1 parent c2219ee commit ccda532

File tree

11 files changed

+711
-4
lines changed

11 files changed

+711
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ For detailed troubleshooting of setup issues, see [Import Troubleshooting Guide]
147147
| [Load Balancing](./samples/load-balancing/README.md) | Priority and weighted load balancing across backends. | apim-aca, afd-apim (with ACA) |
148148
| [Secure Blob Access](./samples/secure-blob-access/README.md) | Secure blob access via the [valet key pattern](https://learn.microsoft.com/azure/architecture/patterns/valet-key). | All infrastructures |
149149
| [Credential Manager (with Spotify)](./samples/oauth-3rd-party/README.md) | Authenticate with APIM which then uses its Credential Manager with Spotify's REST API. | All infrastructures |
150+
| [Azure Maps](./samples/azure-maps/README.md) | Proxying calls to Azure Maps with APIM policies. | All infrastructures |
150151

151152
### ▶️ Running a Sample
152153

samples/azure-maps/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Samples: Api Management proxying calls to Azure Maps
2+
3+
This sample demonstrates how to use Azure API Management (APIM) to proxy calls to Azure Maps service using **three different authentication methods**. This setup allows you to manage, secure, and monitor access to Azure Maps through APIM while showcasing various authentication patterns for different use cases.
4+
5+
⚙️ **Supported infrastructures**: All infrastructures
6+
7+
👟 **Expected *Run All* runtime (excl. infrastructure prerequisite): ~2 minutes**
8+
9+
## 🎯 Objectives
10+
11+
1. **Demonstrate three Azure Maps authentication patterns:**
12+
- **Shared Key Authentication** - Using Azure Maps subscription keys
13+
- **Azure Entra ID (Managed Identity)** - Recommended approach for production scenarios
14+
- **SAS Token Authentication** - Dynamic token generation with fine-grained control
15+
2. Learn path-to-operation mapping vs. generic proxy patterns in APIM
16+
3. Understand how APIM can enable chargeback/cost allocation scenarios for Azure Maps usage
17+
4. Show integration with both v1 and v2 Azure Maps API endpoints
18+
19+
## 📝 Scenario
20+
21+
Organizations migrating from services like Bing Maps to Azure Maps often need flexible authentication and billing models. This sample addresses common questions about:
22+
23+
- **Authentication flexibility**: While Azure Entra ID with Managed Identity is the recommended production approach, some scenarios require shared keys or SAS tokens
24+
- **Cost allocation**: Using APIM subscription keys to enable chargeback models and usage tracking per department/application
25+
- **Migration patterns**: Supporting different authentication methods during transition periods
26+
- **API management**: Centralizing access control, rate limiting, and monitoring for Azure Maps
27+
28+
### Authentication Scenarios Demonstrated:
29+
30+
1. **🔑 Shared Key (Subscription Key)**: Direct use of Azure Maps primary/secondary keys - simpler but less granular control
31+
2. **🛡️ Azure Entra ID (Managed Identity)**: Recommended for production - leverages Azure RBAC and eliminates key management
32+
3. **🎫 SAS Token**: Dynamic token generation with configurable expiration, rate limits, and regional restrictions - ideal for fine-grained access control
33+
34+
> **Note**: In production scenarios, SAS token generation would typically be handled by a separate Azure Function or API service. This sample demonstrates in-policy generation for simplicity and educational purposes.
35+
36+
## 🛩️ Lab Components
37+
38+
This lab sets up:
39+
40+
- **Azure Maps Account** with Gen2 pricing tier
41+
- **APIM Managed Identity** with roles:
42+
- **Azure Maps Data Reader**: Read access to Maps APIs
43+
- **Azure Maps Contributor**: Ability to generate SAS tokens
44+
- **User Assigned Managed Identity (UAMI)** for SAS token principal, with:
45+
- **Azure Maps Data Reader**: Used as the identity for SAS token operations
46+
- **Three API Operations** demonstrating each authentication method:
47+
- `/geocode` - Azure Entra ID authentication
48+
- `/geocode/batch/async` - Shared key authentication
49+
- `/default/*` - SAS token authentication with caching
50+
51+
## ⚙️ Configuration
52+
53+
1. Decide which of the [Infrastructure Architectures](../../README.md#infrastructure-architectures) you wish to use.
54+
1. If the infrastructure _does not_ yet exist, navigate to the desired [infrastructure](../../infrastructure/) folder and follow its README.md.
55+
1. If the infrastructure _does_ exist, adjust the `user-defined parameters` in the _Initialize notebook variables_ below. Please ensure that all parameters match your infrastructure.

samples/azure-maps/create.ipynb

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"### 🛠️ 1. Initialize notebook variables\n",
8+
"\n",
9+
"Configures everything that's needed for deployment. \n",
10+
"\n",
11+
"**Modify entries under _1) User-defined parameters_ and _3) Define the APIs and their operations and policies_**."
12+
]
13+
},
14+
{
15+
"cell_type": "code",
16+
"execution_count": null,
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"import utils\n",
21+
"from apimtypes import *\n",
22+
"\n",
23+
"# 1) User-defined parameters (change these as needed)\n",
24+
"rg_location = 'eastus2'\n",
25+
"index = 1\n",
26+
"deployment = INFRASTRUCTURE.SIMPLE_APIM\n",
27+
"tags = ['azure-maps'] # ENTER DESCRIPTIVE TAG(S)\n",
28+
"api_prefix = 'am-' # OPTIONAL: ENTER A PREFIX FOR THE APIS TO REDUCE COLLISION POTENTIAL WITH OTHER SAMPLES\n",
29+
"azure_maps_url = 'https://atlas.microsoft.com' # OPTIONAL: ENTER THE AZURE MAPS URL IF DIFFERENT FROM DEFAULT\n",
30+
"\n",
31+
"# 2) Service-defined parameters (please do not change these)\n",
32+
"rg_name = utils.get_infra_rg_name(deployment, index)\n",
33+
"sample_folder = \"azure-maps\"\n",
34+
"nb_helper = utils.NotebookHelper(sample_folder, rg_name, rg_location, deployment, [INFRASTRUCTURE.AFD_APIM_PE, INFRASTRUCTURE.APIM_ACA, INFRASTRUCTURE.SIMPLE_APIM])\n",
35+
"\n",
36+
"# 3) Define the APIs and their operations and policies\n",
37+
"\n",
38+
"# Policies\n",
39+
"# Named values must be set up a bit differently as they need to have two surrounding curly braces\n",
40+
"map_async_geocode_batch_v1_keyauth_post_xml = utils.read_policy_xml('map_async_geocode_batch_v1_keyauth_post.xml', sample_name=sample_folder)\n",
41+
"map_default_route_v2_aad_get_xml = utils.read_policy_xml('map_default_route_v2_aad_get.xml', sample_name=sample_folder)\n",
42+
"map_geocode_v2_aad_get_xml = utils.read_policy_xml('map_geocode_v2_aad_get.xml', sample_name=sample_folder)\n",
43+
"\n",
44+
"# Map API \n",
45+
"mapApi_v2_default_get = GET_APIOperation2('get-default-route','Get default route','/default/*','This is the default route that will allow all requests to go through to the backend api',map_default_route_v2_aad_get_xml)\n",
46+
"mapApi_v1_async_post = APIOperation('async-geocode-batch','Async Geocode Batch','/geocode/batch/async',HTTP_VERB.POST, 'Post geocode batch async endpoint',map_async_geocode_batch_v1_keyauth_post_xml)\n",
47+
"mapApi_v2_geocode_get = GET_APIOperation2('get-geocode','Get Geocode','/geocode','Get geocode endpoint',map_geocode_v2_aad_get_xml)\n",
48+
"api1 = API('map-api', 'Map API', '/map', 'This is the proxy for Azure Maps', operations=[mapApi_v2_default_get, mapApi_v1_async_post,mapApi_v2_geocode_get], tags = tags, serviceUrl=azure_maps_url)\n",
49+
"\n",
50+
"# APIs Array\n",
51+
"apis: List[API] = [api1]\n",
52+
"\n",
53+
"# 4) Set up the named values, for this specific sample, we are using some of the named values in the API policies defined above that can't be known at this point in the process. For those named values, we are setting them in the main.bicep file.\n",
54+
"nvs: List[NamedValue] = [\n",
55+
" NamedValue('azure-maps-arm-api-version','2023-06-01')\n",
56+
"]\n",
57+
"\n",
58+
"utils.print_ok('Notebook initialized')"
59+
]
60+
},
61+
{
62+
"cell_type": "markdown",
63+
"metadata": {},
64+
"source": [
65+
"### 🚀 2. Create deployment using Bicep\n",
66+
"\n",
67+
"Creates the bicep deployment into the previously-specified resource group. A bicep parameters file will be created prior to execution."
68+
]
69+
},
70+
{
71+
"cell_type": "code",
72+
"execution_count": null,
73+
"metadata": {},
74+
"outputs": [],
75+
"source": [
76+
"import utils\n",
77+
"\n",
78+
"# 1) Define the Bicep parameters with serialized APIs\n",
79+
"bicep_parameters = {\n",
80+
" 'apis': {'value': [api.to_dict() for api in apis]},\n",
81+
" 'namedValues': {'value': [nv.to_dict() for nv in nvs]}\n",
82+
"}\n",
83+
"\n",
84+
"# 2) Deploy the bicep template\n",
85+
"output = nb_helper.deploy_bicep(bicep_parameters)\n",
86+
"\n",
87+
"if output.json_data:\n",
88+
" apim_name = output.get('apimServiceName', 'APIM Service Name')\n",
89+
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
90+
"\n",
91+
"utils.print_ok('Deployment completed')"
92+
]
93+
},
94+
{
95+
"cell_type": "markdown",
96+
"metadata": {},
97+
"source": [
98+
"### ✅ 3. Verify API Request Success\n",
99+
"\n",
100+
"Assert that the deployment was successful by making simple calls to APIM. \n",
101+
"\n",
102+
"❗️ If the infrastructure shields APIM and requires a different ingress (e.g. Azure Front Door), the request to the APIM gateway URl will fail by design. Obtain the Front Door endpoint hostname and try that instead."
103+
]
104+
},
105+
{
106+
"cell_type": "code",
107+
"execution_count": null,
108+
"metadata": {},
109+
"outputs": [],
110+
"source": [
111+
"import utils\n",
112+
"from apimtesting import ApimTesting\n",
113+
"from apimrequests import ApimRequests\n",
114+
"import json\n",
115+
"\n",
116+
"tests = ApimTesting(\"Azure Maps Sample Tests\", sample_folder, deployment)\n",
117+
"\n",
118+
"# Preflight: Check if the infrastructure architecture deployment uses Azure Front Door. If so, assume that APIM is not directly accessible and use the Front Door URL instead.\n",
119+
"endpoint_url = utils.test_url_preflight_check(deployment, rg_name, apim_gateway_url)\n",
120+
"\n",
121+
"reqs = ApimRequests(endpoint_url)\n",
122+
"\n",
123+
"# 1) Issue a direct request to API Management\n",
124+
"output = reqs.singleGet('/', msg = 'Calling Hello World (Root) API. Expect 200.')\n",
125+
"tests.verify(output, 'Hello World from API Management!')\n",
126+
"\n",
127+
"# 2) Issue requests to API Management with Azure Maps APIs\n",
128+
"output = reqs.singleGet('/map/default/geocode?query=15127%20NE%2024th%20Street%20Redmond%20WA', msg = 'Calling Default Route API with SAS Token Auth. Expect 200.')\n",
129+
"tests.verify('address' in output, True)\n",
130+
"\n",
131+
"output = reqs.singleGet('/map/geocode?query=15127%20NE%2024th%20Street%20Redmond%20WA', msg = 'Calling Geocode v2 API with AAD Auth. Expect 200.')\n",
132+
"tests.verify('address' in output, True)\n",
133+
"\n",
134+
"output = reqs.singlePostAsync('/map/geocode/batch/async', data={\n",
135+
" \"batchItems\": [\n",
136+
" {\"query\": \"?query=400 Broad St, Seattle, WA 98109&limit=3\"},\n",
137+
" {\"query\": \"?query=One, Microsoft Way, Redmond, WA 98052&limit=3\"},\n",
138+
" {\"query\": \"?query=350 5th Ave, New York, NY 10118&limit=1\"},\n",
139+
" {\"query\": \"?query=Pike Pl, Seattle, WA 98101&lat=47.610970&lon=-122.342469&radius=1000\"},\n",
140+
" {\"query\": \"?query=Champ de Mars, 5 Avenue Anatole France, 75007 Paris, France&limit=1\"}\n",
141+
" ]\n",
142+
"}, msg = 'Calling Async Geocode Batch v1 API with Share Key Auth. Expect initial 202, then a 200 on the polling response', timeout=120, poll_interval=3)\n",
143+
"\n",
144+
"# confirm the response contains \"summary\": { \"successfulRequests\": 5, \"totalRequests\": 5}\n",
145+
"tests.verify('summary' in output and 'successfulRequests' in output and json.loads(output)['summary']['successfulRequests'] == 5, True)\n",
146+
"\n",
147+
"tests.print_summary()\n",
148+
"\n",
149+
"utils.print_ok('All done!')"
150+
]
151+
}
152+
],
153+
"metadata": {
154+
"kernelspec": {
155+
"display_name": "APIM Samples Python 3.12",
156+
"language": "python",
157+
"name": "apim-samples"
158+
},
159+
"language_info": {
160+
"codemirror_mode": {
161+
"name": "ipython",
162+
"version": 3
163+
},
164+
"file_extension": ".py",
165+
"mimetype": "text/x-python",
166+
"name": "python",
167+
"nbconvert_exporter": "python",
168+
"pygments_lexer": "ipython3",
169+
"version": "3.12.10"
170+
}
171+
},
172+
"nbformat": 4,
173+
"nbformat_minor": 2
174+
}

0 commit comments

Comments
 (0)