|
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", |
142 | | - "\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", |
| 11 | + "❗️ **Modify entries under _User-defined parameters_**.\n", |
177 | 12 | "\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", |
225 | | - "\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", |
246 | | - "\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", |
288 | | - "else:\n", |
289 | | - " utils.print_message('ACA APIs were not created. Skipping ACA API calls.', blank_above = True)\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", |
290 | 30 | "\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", |
| 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", |
297 | 34 | "\n", |
298 | | - "tests.print_summary()\n", |
| 35 | + "if not success:\n", |
| 36 | + " print(\"❌ Infrastructure creation failed!\")\n", |
| 37 | + " raise SystemExit(1)\n", |
299 | 38 | "\n", |
300 | 39 | "utils.print_ok('All done!')" |
301 | 40 | ] |
|
0 commit comments