|
4 | 4 | "cell_type": "markdown",
|
5 | 5 | "metadata": {},
|
6 | 6 | "source": [
|
7 |
| - "### 🛠️ 1. Initialize notebook variables\n", |
| 7 | + "### 🛠️ Configure Infrastructure Parameters & Create the Infrastructure\n", |
8 | 8 | "\n",
|
9 |
| - "Configures everything that's needed for deployment. \n", |
| 9 | + "Set your desired parameters for the AFD-APIM-PE infrastructure deployment.\n", |
10 | 10 | "\n",
|
11 |
| - "❗️ **Modify entries under _1) User-defined parameters_**." |
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 |
| - "apim_sku = APIM_SKU.STANDARDV2\n", |
27 |
| - "deployment = INFRASTRUCTURE.AFD_APIM_PE\n", |
28 |
| - "use_ACA = True\n", |
29 |
| - "reveal_backend = True # Set to True to reveal the backend details in the API operations\n", |
30 |
| - "\n", |
31 |
| - "# 2) Service-defined parameters (please do not change these unless you know what you're doing)\n", |
32 |
| - "rg_name = utils.get_infra_rg_name(deployment, index)\n", |
33 |
| - "rg_tags = utils.build_infrastructure_tags(deployment)\n", |
34 |
| - "apim_network_mode = APIMNetworkMode.EXTERNAL_VNET\n", |
35 |
| - "\n", |
36 |
| - "# 3) Set up the policy fragments\n", |
37 |
| - "pfs: List[PolicyFragment] = [\n", |
38 |
| - " PolicyFragment('AuthZ-Match-All', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-all.xml')), 'Authorizes if all of the specified roles match the JWT role claims.'),\n", |
39 |
| - " PolicyFragment('AuthZ-Match-Any', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-any.xml')), 'Authorizes if any of the specified roles match the JWT role claims.'),\n", |
40 |
| - " PolicyFragment('Http-Response-200', utils.read_policy_xml(utils.determine_shared_policy_path('pf-http-response-200.xml')), 'Returns a 200 OK response for the current HTTP method.'),\n", |
41 |
| - " PolicyFragment('Product-Match-Any', utils.read_policy_xml(utils.determine_shared_policy_path('pf-product-match-any.xml')), 'Proceeds if any of the specified products match the context product name.'),\n", |
42 |
| - " PolicyFragment('Remove-Request-Headers', utils.read_policy_xml(utils.determine_shared_policy_path('pf-remove-request-headers.xml')), 'Removes request headers from the incoming request.')\n", |
43 |
| - "]\n", |
44 |
| - "\n", |
45 |
| - "# 4) Define the APIs and their operations and policies\n", |
46 |
| - "\n", |
47 |
| - "# Policies\n", |
48 |
| - "pol_hello_world = utils.read_policy_xml(HELLO_WORLD_XML_POLICY_PATH)\n", |
49 |
| - "\n", |
50 |
| - "# Hello World (Root)\n", |
51 |
| - "api_hwroot_get = GET_APIOperation('This is a GET for API 1', pol_hello_world)\n", |
52 |
| - "api_hwroot = API('hello-world', 'Hello World', '', 'This is the root API for Hello World', operations = [api_hwroot_get])\n", |
53 |
| - "\n", |
54 |
| - "apis: List[API] = [api_hwroot]\n", |
55 |
| - "\n", |
56 |
| - "# If Container Apps is enabled, create the ACA APIs in APIM\n", |
57 |
| - "if use_ACA:\n", |
58 |
| - " utils.print_info('ACA APIs will be created.')\n", |
59 |
| - "\n", |
60 |
| - " pol_backend = utils.read_policy_xml(BACKEND_XML_POLICY_PATH)\n", |
61 |
| - " pol_aca_backend_1 = pol_backend.format(backend_id = 'aca-backend-1')\n", |
62 |
| - " pol_aca_backend_2 = pol_backend.format(backend_id = 'aca-backend-2')\n", |
63 |
| - " pol_aca_backend_pool = pol_backend.format(backend_id = 'aca-backend-pool')\n", |
64 |
| - "\n", |
65 |
| - " # Hello World (ACA Backend 1)\n", |
66 |
| - " api_hwaca_1_get = GET_APIOperation('This is a GET for Hello World on ACA Backend 1')\n", |
67 |
| - " api_hwaca_1 = API('hello-world-aca-1', 'Hello World (ACA 1)', '/aca-1', 'This is the ACA API for Backend 1', policyXml = pol_aca_backend_1, operations = [api_hwaca_1_get])\n", |
68 |
| - "\n", |
69 |
| - " # Hello World (ACA Backend 2)\n", |
70 |
| - " api_hwaca_2_get = GET_APIOperation('This is a GET for Hello World on ACA Backend 2')\n", |
71 |
| - " api_hwaca_2 = API('hello-world-aca-2', 'Hello World (ACA 2)', '/aca-2', 'This is the ACA API for Backend 2', policyXml = pol_aca_backend_2, operations = [api_hwaca_2_get])\n", |
72 |
| - "\n", |
73 |
| - " # Hello World (ACA Backend Pool)\n", |
74 |
| - " api_hwaca_pool_get = GET_APIOperation('This is a GET for Hello World on ACA Backend Pool')\n", |
75 |
| - " api_hwaca_pool = API('hello-world-aca-pool', 'Hello World (ACA Pool)', '/aca-pool', 'This is the ACA API for Backend Pool', policyXml = pol_aca_backend_pool, operations = [api_hwaca_pool_get])\n", |
76 |
| - "\n", |
77 |
| - " # Add ACA APIs to the existing apis array\n", |
78 |
| - " apis += [api_hwaca_1, api_hwaca_2, api_hwaca_pool]\n", |
79 |
| - "\n", |
80 |
| - "utils.print_ok('Notebook initialized')" |
81 |
| - ] |
82 |
| - }, |
83 |
| - { |
84 |
| - "cell_type": "markdown", |
85 |
| - "metadata": {}, |
86 |
| - "source": [ |
87 |
| - "### 🚀 2. Create deployment using Bicep\n", |
88 |
| - "\n", |
89 |
| - "Creates the bicep deployment into the previously-specified resource group. A bicep parameters file will be created prior to execution." |
90 |
| - ] |
91 |
| - }, |
92 |
| - { |
93 |
| - "cell_type": "code", |
94 |
| - "execution_count": null, |
95 |
| - "metadata": {}, |
96 |
| - "outputs": [], |
97 |
| - "source": [ |
98 |
| - "import utils\n", |
99 |
| - "from apimtypes import *\n", |
100 |
| - "\n", |
101 |
| - "# 1) Define the Bicep parameters with serialized APIs and networking mode\n", |
102 |
| - "bicep_parameters = {\n", |
103 |
| - " 'apimSku' : {'value': apim_sku.value},\n", |
104 |
| - " 'apis' : {'value': [api.to_dict() for api in apis]},\n", |
105 |
| - " 'policyFragments' : {'value': [pf.to_dict() for pf in pfs]},\n", |
106 |
| - " 'apimPublicAccess' : {'value': apim_network_mode in [APIMNetworkMode.PUBLIC, APIMNetworkMode.EXTERNAL_VNET]},\n", |
107 |
| - " 'useACA' : {'value': use_ACA}\n", |
108 |
| - "}\n", |
109 |
| - "\n", |
110 |
| - "# 2) Run the deployment\n", |
111 |
| - "output = utils.create_bicep_deployment_group(rg_name, rg_location, deployment, bicep_parameters, rg_tags = rg_tags)\n", |
112 |
| - "\n", |
113 |
| - "# 3) Print a deployment summary, if successful; otherwise, exit with an error\n", |
114 |
| - "if not output.success:\n", |
115 |
| - " raise SystemExit('Deployment failed')\n", |
116 |
| - "\n", |
117 |
| - "if output.success and output.json_data:\n", |
118 |
| - " apim_service_id = output.get('apimServiceId', 'APIM Service Id')\n", |
119 |
| - " apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n", |
120 |
| - " afd_endpoint_url = output.get('fdeSecureUrl', 'Front Door Endpoint URL')\n", |
121 |
| - " apim_apis = output.getJson('apiOutputs', 'APIs')\n", |
122 |
| - "\n", |
123 |
| - "utils.print_ok('Deployment completed')\n" |
124 |
| - ] |
125 |
| - }, |
126 |
| - { |
127 |
| - "cell_type": "markdown", |
128 |
| - "metadata": {}, |
129 |
| - "source": [ |
130 |
| - "### 🔗 3. Approve Front Door private link connection to APIM\n", |
131 |
| - "\n", |
132 |
| - "In the deployed Bicep template, Azure Front Door will establish a private link connection to the API Management service. This connection should be approved. Run the following command to approve the connection." |
133 |
| - ] |
134 |
| - }, |
135 |
| - { |
136 |
| - "cell_type": "code", |
137 |
| - "execution_count": null, |
138 |
| - "metadata": {}, |
139 |
| - "outputs": [], |
140 |
| - "source": [ |
141 |
| - "import utils\n", |
| 11 | + "❗️ **Modify entries under _User-defined parameters_**.\n", |
142 | 12 | "\n",
|
143 |
| - "# Get all pending private endpoint connections as JSON\n", |
144 |
| - "output = utils.run(f\"az network private-endpoint-connection list --id {apim_service_id} --query \\\"[?contains(properties.privateLinkServiceConnectionState.status, 'Pending')]\\\" -o json\")\n", |
145 |
| - "\n", |
146 |
| - "# Handle both a single object and a list of objects\n", |
147 |
| - "pending_connections = output.json_data if output.success and output.is_json else []\n", |
148 |
| - "\n", |
149 |
| - "if isinstance(pending_connections, dict):\n", |
150 |
| - " pending_connections = [pending_connections]\n", |
151 |
| - "\n", |
152 |
| - "total = len(pending_connections)\n", |
153 |
| - "utils.print_info(f\"Found {total} pending private link service connection(s).\")\n", |
154 |
| - "\n", |
155 |
| - "if total > 0:\n", |
156 |
| - " for i, conn in enumerate(pending_connections, 1):\n", |
157 |
| - " conn_id = conn.get('id')\n", |
158 |
| - " conn_name = conn.get('name', '<unknown>')\n", |
159 |
| - " utils.print_info(f\"{i}/{total}: {conn_name}\", True)\n", |
160 |
| - "\n", |
161 |
| - " approve_result = utils.run(\n", |
162 |
| - " f\"az network private-endpoint-connection approve --id {conn_id} --description 'Approved'\",\n", |
163 |
| - " f\"Private Link Connection approved: {conn_name}\",\n", |
164 |
| - " f\"Failed to approve Private Link Connection: {conn_name}\"\n", |
165 |
| - " )\n", |
166 |
| - "\n", |
167 |
| - " utils.print_ok('Private link approvals completed')\n", |
168 |
| - "else:\n", |
169 |
| - " utils.print_info('No pending private link service connection was found. There is nothing to approve.')" |
170 |
| - ] |
171 |
| - }, |
172 |
| - { |
173 |
| - "cell_type": "markdown", |
174 |
| - "metadata": {}, |
175 |
| - "source": [ |
176 |
| - "### ✅ 4. Verify API Request Success via API Management\n", |
177 |
| - "\n", |
178 |
| - "As we have not yet disabled public access to APIM, this request should succeed with a **200**." |
179 |
| - ] |
180 |
| - }, |
181 |
| - { |
182 |
| - "cell_type": "code", |
183 |
| - "execution_count": null, |
184 |
| - "metadata": {}, |
185 |
| - "outputs": [], |
186 |
| - "source": [ |
187 |
| - "import utils\n", |
188 |
| - "from apimrequests import ApimRequests\n", |
189 |
| - "from apimtesting import ApimTesting\n", |
190 |
| - "\n", |
191 |
| - "tests = ApimTesting(\"AFD-APIM-PE Tests (Pre-Lockdown)\", deployment, deployment)\n", |
192 |
| - "\n", |
193 |
| - "api_subscription_key = apim_apis[0]['subscriptionPrimaryKey']\n", |
194 |
| - "reqs = ApimRequests(apim_gateway_url, api_subscription_key)\n", |
195 |
| - "\n", |
196 |
| - "utils.print_message('Calling Hello World (Root) API via API Management Gateway URL. Expect 200 (if run before disabling API Management public network access).')\n", |
197 |
| - "output = reqs.singleGet('/')\n", |
198 |
| - "tests.verify(output, 'Hello World from API Management!')\n", |
199 |
| - "\n", |
200 |
| - "tests.print_summary()\n", |
201 |
| - "\n", |
202 |
| - "utils.print_ok('API request via API Management completed')" |
203 |
| - ] |
204 |
| - }, |
205 |
| - { |
206 |
| - "cell_type": "markdown", |
207 |
| - "metadata": {}, |
208 |
| - "source": [ |
209 |
| - "### 🔒 5. Disabling API Management public network access\n", |
210 |
| - "\n", |
211 |
| - "The initial `APIM` service deployment above cannot disable public network access. It must be disabled subsequently below." |
| 13 | + "**Note:** This infrastructure includes Azure Front Door with API Management using private endpoints. The creation process includes two phases: initial deployment with public access, private link approval, and then disabling public access." |
212 | 14 | ]
|
213 | 15 | },
|
214 | 16 | {
|
|
220 | 22 | "import utils\n",
|
221 | 23 | "from apimtypes import *\n",
|
222 | 24 | "\n",
|
223 |
| - "# 1) Update the Bicep parameters to disable public access to APIM (we only want private endpoint ingress)\n", |
224 |
| - "bicep_parameters['apimPublicAccess']['value'] = False\n", |
| 25 | + "# User-defined parameters (change these as needed)\n", |
| 26 | + "rg_location = 'eastus2' # Azure region for deployment\n", |
| 27 | + "index = 1 # Infrastructure index (use different numbers for multiple environments)\n", |
| 28 | + "apim_sku = APIM_SKU.STANDARDV2 # Options: 'STANDARDV2', 'PREMIUMV2' (Basic not supported for private endpoints)\n", |
| 29 | + "use_aca = True # Include Azure Container Apps backends\n", |
225 | 30 | "\n",
|
226 |
| - "# 2) Run the deployment\n", |
227 |
| - "output = utils.create_bicep_deployment_group(rg_name, rg_location, deployment, bicep_parameters)\n", |
228 |
| - "\n", |
229 |
| - "# 3) Print a single, clear deployment summary if successful\n", |
230 |
| - "if not output.success:\n", |
231 |
| - " raise SystemExit('Deployment failed')\n", |
232 |
| - " \n", |
233 |
| - "if output.success and output.json_data:\n", |
234 |
| - " afd_endpoint_url = output.get('fdeSecureUrl', 'Front Door Endpoint URL')\n", |
235 |
| - " apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n", |
236 |
| - " apim_apis = output.getJson('apiOutputs', 'APIs')\n", |
237 |
| - "\n", |
238 |
| - "utils.print_ok('Deployment completed')\n" |
239 |
| - ] |
240 |
| - }, |
241 |
| - { |
242 |
| - "cell_type": "markdown", |
243 |
| - "metadata": {}, |
244 |
| - "source": [ |
245 |
| - "### ✅ 6. Verify API Request Success via Azure Front Door & Failure with API Management\n", |
| 31 | + "# Create an instance of the desired infrastructure\n", |
| 32 | + "inb_helper = utils.InfrastructureNotebookHelper(rg_location, INFRASTRUCTURE.AFD_APIM_PE, index, apim_sku) \n", |
| 33 | + "success = inb_helper.create_infrastructure()\n", |
246 | 34 | "\n",
|
247 |
| - "At this time only requests through Front Door should be successful and return a **200**. Requests to APIM that worked previously should result in a **403**." |
248 |
| - ] |
249 |
| - }, |
250 |
| - { |
251 |
| - "cell_type": "code", |
252 |
| - "execution_count": null, |
253 |
| - "metadata": {}, |
254 |
| - "outputs": [], |
255 |
| - "source": [ |
256 |
| - "import utils\n", |
257 |
| - "from apimrequests import ApimRequests\n", |
258 |
| - "from apimtesting import ApimTesting\n", |
259 |
| - "\n", |
260 |
| - "tests = ApimTesting(\"AFD-APIM-PE Tests (Post-Lockdown)\", deployment, deployment)\n", |
261 |
| - "\n", |
262 |
| - "api_subscription_key = apim_apis[0]['subscriptionPrimaryKey']\n", |
263 |
| - "reqsApim = ApimRequests(apim_gateway_url, api_subscription_key)\n", |
264 |
| - "reqsAfd = ApimRequests(afd_endpoint_url, api_subscription_key)\n", |
265 |
| - "\n", |
266 |
| - "# 1) Unsuccessful call to APIM Gateway URL (should fail with 403 Forbidden)\n", |
267 |
| - "output = reqsApim.singleGet('/', msg = '1) Calling Hello World (Root) API via API Management Gateway URL. Expect 403 as APIM public access is disabled now.')\n", |
268 |
| - "outputJson = utils.get_json(output)\n", |
269 |
| - "tests.verify(outputJson['statusCode'], 403)\n", |
270 |
| - "\n", |
271 |
| - "# 2) Successful call to Front Door (200)\n", |
272 |
| - "output = reqsAfd.singleGet('/', msg = '2) Calling Hello World (Root) API via Azure Front Door. Expect 200.')\n", |
273 |
| - "tests.verify(output, 'Hello World from API Management!')\n", |
274 |
| - "\n", |
275 |
| - "# 3) Successful calls to Front Door -> APIM -> ACA (200)\n", |
276 |
| - "if use_ACA:\n", |
277 |
| - " reqsAfd = ApimRequests(afd_endpoint_url, apim_apis[1]['subscriptionPrimaryKey'])\n", |
278 |
| - " output = reqsAfd.singleGet('/aca-1', msg = '3) Calling Hello World (ACA 1) API via Azure Front Door. Expect 200.')\n", |
279 |
| - " tests.verify(output, 'Hello World!')\n", |
280 |
| - "\n", |
281 |
| - " reqsAfd = ApimRequests(afd_endpoint_url, apim_apis[2]['subscriptionPrimaryKey'])\n", |
282 |
| - " output = reqsAfd.singleGet('/aca-2', msg = '4) Calling Hello World (ACA 2) API via Azure Front Door. Expect 200.')\n", |
283 |
| - " tests.verify(output, 'Hello World!')\n", |
284 |
| - "\n", |
285 |
| - " reqsAfd = ApimRequests(afd_endpoint_url, apim_apis[3]['subscriptionPrimaryKey'])\n", |
286 |
| - " output = reqsAfd.singleGet('/aca-pool', msg = '5) Calling Hello World (ACA Pool) API via Azure Front Door. Expect 200.')\n", |
287 |
| - " tests.verify(output, 'Hello World!')\n", |
| 35 | + "if success:\n", |
| 36 | + " utils.print_ok('Infrastructure creation completed successfully!')\n", |
288 | 37 | "else:\n",
|
289 |
| - " utils.print_message('ACA APIs were not created. Skipping ACA API calls.', blank_above = True)\n", |
290 |
| - "\n", |
291 |
| - "# 4) Unsuccessful call to Front Door without API subscription key (should fail with 401 Unauthorized)\n", |
292 |
| - "reqsNoApiSubscription = ApimRequests(afd_endpoint_url)\n", |
293 |
| - "output = reqsNoApiSubscription.singleGet('/', msg = 'Calling Hello World (Root) API without API subscription key. Expect 401.')\n", |
294 |
| - "outputJson = utils.get_json(output)\n", |
295 |
| - "tests.verify(outputJson['statusCode'], 401)\n", |
296 |
| - "tests.verify(outputJson['message'], 'Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API.')\n", |
297 |
| - "\n", |
298 |
| - "tests.print_summary()\n", |
299 |
| - "\n", |
300 |
| - "utils.print_ok('All done!')" |
| 38 | + " print(\"❌ Infrastructure creation failed!\")\n", |
| 39 | + " raise SystemExit(1)" |
301 | 40 | ]
|
302 | 41 | },
|
303 | 42 | {
|
|
313 | 52 | ],
|
314 | 53 | "metadata": {
|
315 | 54 | "kernelspec": {
|
316 |
| - "display_name": "APIM Samples Python 3.12", |
| 55 | + "display_name": ".venv (3.12.10)", |
317 | 56 | "language": "python",
|
318 |
| - "name": "apim-samples" |
| 57 | + "name": "python3" |
319 | 58 | },
|
320 | 59 | "language_info": {
|
321 | 60 | "codemirror_mode": {
|
|
0 commit comments