Skip to content

Milestone 1.8.0 #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ _Try it out, learn from it, apply it in your setups._

---

## 🚀 Getting Started
## 🏗️ Infrastructure Setup

### Quick Start Options

Expand Down Expand Up @@ -159,11 +159,13 @@ For detailed troubleshooting of setup issues, see [Import Troubleshooting Guide]

📘 **For comprehensive troubleshooting including deployment errors, authentication issues, and more, see our main [Troubleshooting Guide](TROUBLESHOOTING.md).**

### ▶️ Running a Sample
## 🚀 Running a Sample

1. Locate the specific sample's `create.ipynb` file and adjust the parameters under the `User-defined Parameters` header as you see fit.
1. Ensure that the specified infrastructure already exists in your subscription. If not, proceed to the desired infrastructure folder and execute its `create.ipynb` file. Wait until this completes before continuing.
1. Execute the sample's `create.ipynb` file.
1. Open the desired sample's `create.ipynb` file.
1. Optional: Adjust the parameters under the `User-defined Parameters` header, if desired.
1. Execute the `create.ipynb` Jupyter notebook via `Run All`.

> A supported infrastructure does not yet need to exist before the sample is executed. The notebook will determine the current state and present you with options to create or select a supported infrastructure, if necessary.

Now that infrastructure and sample have been stood up, you can experiment with the policies, make requests against APIM, etc.

Expand Down
297 changes: 18 additions & 279 deletions infrastructure/afd-apim-pe/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4,211 +4,13 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### 🛠️ 1. Initialize notebook variables\n",
"### 🛠️ Configure Infrastructure Parameters & Create the Infrastructure\n",
"\n",
"Configures everything that's needed for deployment. \n",
"Set your desired parameters for the AFD-APIM-PE infrastructure deployment.\n",
"\n",
"❗️ **Modify entries under _1) User-defined parameters_**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import utils\n",
"from apimtypes import *\n",
"\n",
"# 1) User-defined parameters (change these as needed)\n",
"rg_location = 'eastus2'\n",
"index = 1\n",
"apim_sku = APIM_SKU.STANDARDV2\n",
"deployment = INFRASTRUCTURE.AFD_APIM_PE\n",
"use_ACA = True\n",
"reveal_backend = True # Set to True to reveal the backend details in the API operations\n",
"\n",
"# 2) Service-defined parameters (please do not change these unless you know what you're doing)\n",
"rg_name = utils.get_infra_rg_name(deployment, index)\n",
"rg_tags = utils.build_infrastructure_tags(deployment)\n",
"apim_network_mode = APIMNetworkMode.EXTERNAL_VNET\n",
"\n",
"# 3) Set up the policy fragments\n",
"pfs: List[PolicyFragment] = [\n",
" 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",
" 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",
" 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",
" 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",
" 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",
"]\n",
"\n",
"# 4) Define the APIs and their operations and policies\n",
"\n",
"# Policies\n",
"pol_hello_world = utils.read_policy_xml(HELLO_WORLD_XML_POLICY_PATH)\n",
"\n",
"# Hello World (Root)\n",
"api_hwroot_get = GET_APIOperation('This is a GET for API 1', pol_hello_world)\n",
"api_hwroot = API('hello-world', 'Hello World', '', 'This is the root API for Hello World', operations = [api_hwroot_get])\n",
"\n",
"apis: List[API] = [api_hwroot]\n",
"\n",
"# If Container Apps is enabled, create the ACA APIs in APIM\n",
"if use_ACA:\n",
" utils.print_info('ACA APIs will be created.')\n",
"\n",
" pol_backend = utils.read_policy_xml(BACKEND_XML_POLICY_PATH)\n",
" pol_aca_backend_1 = pol_backend.format(backend_id = 'aca-backend-1')\n",
" pol_aca_backend_2 = pol_backend.format(backend_id = 'aca-backend-2')\n",
" pol_aca_backend_pool = pol_backend.format(backend_id = 'aca-backend-pool')\n",
"\n",
" # Hello World (ACA Backend 1)\n",
" api_hwaca_1_get = GET_APIOperation('This is a GET for Hello World on ACA Backend 1')\n",
" 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",
"\n",
" # Hello World (ACA Backend 2)\n",
" api_hwaca_2_get = GET_APIOperation('This is a GET for Hello World on ACA Backend 2')\n",
" 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",
"\n",
" # Hello World (ACA Backend Pool)\n",
" api_hwaca_pool_get = GET_APIOperation('This is a GET for Hello World on ACA Backend Pool')\n",
" 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",
"\n",
" # Add ACA APIs to the existing apis array\n",
" apis += [api_hwaca_1, api_hwaca_2, api_hwaca_pool]\n",
"\n",
"utils.print_ok('Notebook initialized')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 🚀 2. Create deployment using Bicep\n",
"\n",
"Creates the bicep deployment into the previously-specified resource group. A bicep parameters file will be created prior to execution."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import utils\n",
"from apimtypes import *\n",
"\n",
"# 1) Define the Bicep parameters with serialized APIs and networking mode\n",
"bicep_parameters = {\n",
" 'apimSku' : {'value': apim_sku.value},\n",
" 'apis' : {'value': [api.to_dict() for api in apis]},\n",
" 'policyFragments' : {'value': [pf.to_dict() for pf in pfs]},\n",
" 'apimPublicAccess' : {'value': apim_network_mode in [APIMNetworkMode.PUBLIC, APIMNetworkMode.EXTERNAL_VNET]},\n",
" 'useACA' : {'value': use_ACA}\n",
"}\n",
"\n",
"# 2) Run the deployment\n",
"output = utils.create_bicep_deployment_group(rg_name, rg_location, deployment, bicep_parameters, rg_tags = rg_tags)\n",
"\n",
"# 3) Print a deployment summary, if successful; otherwise, exit with an error\n",
"if not output.success:\n",
" raise SystemExit('Deployment failed')\n",
"\n",
"if output.success and output.json_data:\n",
" apim_service_id = output.get('apimServiceId', 'APIM Service Id')\n",
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
" afd_endpoint_url = output.get('fdeSecureUrl', 'Front Door Endpoint URL')\n",
" apim_apis = output.getJson('apiOutputs', 'APIs')\n",
"\n",
"utils.print_ok('Deployment completed')\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 🔗 3. Approve Front Door private link connection to APIM\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import utils\n",
"❗️ **Modify entries under _User-defined parameters_**.\n",
"\n",
"# Get all pending private endpoint connections as JSON\n",
"output = utils.run(f\"az network private-endpoint-connection list --id {apim_service_id} --query \\\"[?contains(properties.privateLinkServiceConnectionState.status, 'Pending')]\\\" -o json\")\n",
"\n",
"# Handle both a single object and a list of objects\n",
"pending_connections = output.json_data if output.success and output.is_json else []\n",
"\n",
"if isinstance(pending_connections, dict):\n",
" pending_connections = [pending_connections]\n",
"\n",
"total = len(pending_connections)\n",
"utils.print_info(f\"Found {total} pending private link service connection(s).\")\n",
"\n",
"if total > 0:\n",
" for i, conn in enumerate(pending_connections, 1):\n",
" conn_id = conn.get('id')\n",
" conn_name = conn.get('name', '<unknown>')\n",
" utils.print_info(f\"{i}/{total}: {conn_name}\", True)\n",
"\n",
" approve_result = utils.run(\n",
" f\"az network private-endpoint-connection approve --id {conn_id} --description 'Approved'\",\n",
" f\"Private Link Connection approved: {conn_name}\",\n",
" f\"Failed to approve Private Link Connection: {conn_name}\"\n",
" )\n",
"\n",
" utils.print_ok('Private link approvals completed')\n",
"else:\n",
" utils.print_info('No pending private link service connection was found. There is nothing to approve.')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### ✅ 4. Verify API Request Success via API Management\n",
"\n",
"As we have not yet disabled public access to APIM, this request should succeed with a **200**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import utils\n",
"from apimrequests import ApimRequests\n",
"from apimtesting import ApimTesting\n",
"\n",
"tests = ApimTesting(\"AFD-APIM-PE Tests (Pre-Lockdown)\", deployment, deployment)\n",
"\n",
"api_subscription_key = apim_apis[0]['subscriptionPrimaryKey']\n",
"reqs = ApimRequests(apim_gateway_url, api_subscription_key)\n",
"\n",
"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",
"output = reqs.singleGet('/')\n",
"tests.verify(output, 'Hello World from API Management!')\n",
"\n",
"tests.print_summary()\n",
"\n",
"utils.print_ok('API request via API Management completed')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 🔒 5. Disabling API Management public network access\n",
"\n",
"The initial `APIM` service deployment above cannot disable public network access. It must be disabled subsequently below."
"**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."
]
},
{
Expand All @@ -220,84 +22,21 @@
"import utils\n",
"from apimtypes import *\n",
"\n",
"# 1) Update the Bicep parameters to disable public access to APIM (we only want private endpoint ingress)\n",
"bicep_parameters['apimPublicAccess']['value'] = False\n",
"# User-defined parameters (change these as needed)\n",
"rg_location = 'eastus2' # Azure region for deployment\n",
"index = 1 # Infrastructure index (use different numbers for multiple environments)\n",
"apim_sku = APIM_SKU.STANDARDV2 # Options: 'STANDARDV2', 'PREMIUMV2' (Basic not supported for private endpoints)\n",
"use_aca = True # Include Azure Container Apps backends\n",
"\n",
"# 2) Run the deployment\n",
"output = utils.create_bicep_deployment_group(rg_name, rg_location, deployment, bicep_parameters)\n",
"\n",
"# 3) Print a single, clear deployment summary if successful\n",
"if not output.success:\n",
" raise SystemExit('Deployment failed')\n",
" \n",
"if output.success and output.json_data:\n",
" afd_endpoint_url = output.get('fdeSecureUrl', 'Front Door Endpoint URL')\n",
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
" apim_apis = output.getJson('apiOutputs', 'APIs')\n",
"\n",
"utils.print_ok('Deployment completed')\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### ✅ 6. Verify API Request Success via Azure Front Door & Failure with API Management\n",
"# Create an instance of the desired infrastructure\n",
"inb_helper = utils.InfrastructureNotebookHelper(rg_location, INFRASTRUCTURE.AFD_APIM_PE, index, apim_sku) \n",
"success = inb_helper.create_infrastructure()\n",
"\n",
"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**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import utils\n",
"from apimrequests import ApimRequests\n",
"from apimtesting import ApimTesting\n",
"\n",
"tests = ApimTesting(\"AFD-APIM-PE Tests (Post-Lockdown)\", deployment, deployment)\n",
"\n",
"api_subscription_key = apim_apis[0]['subscriptionPrimaryKey']\n",
"reqsApim = ApimRequests(apim_gateway_url, api_subscription_key)\n",
"reqsAfd = ApimRequests(afd_endpoint_url, api_subscription_key)\n",
"\n",
"# 1) Unsuccessful call to APIM Gateway URL (should fail with 403 Forbidden)\n",
"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",
"outputJson = utils.get_json(output)\n",
"tests.verify(outputJson['statusCode'], 403)\n",
"\n",
"# 2) Successful call to Front Door (200)\n",
"output = reqsAfd.singleGet('/', msg = '2) Calling Hello World (Root) API via Azure Front Door. Expect 200.')\n",
"tests.verify(output, 'Hello World from API Management!')\n",
"\n",
"# 3) Successful calls to Front Door -> APIM -> ACA (200)\n",
"if use_ACA:\n",
" reqsAfd = ApimRequests(afd_endpoint_url, apim_apis[1]['subscriptionPrimaryKey'])\n",
" output = reqsAfd.singleGet('/aca-1', msg = '3) Calling Hello World (ACA 1) API via Azure Front Door. Expect 200.')\n",
" tests.verify(output, 'Hello World!')\n",
"\n",
" reqsAfd = ApimRequests(afd_endpoint_url, apim_apis[2]['subscriptionPrimaryKey'])\n",
" output = reqsAfd.singleGet('/aca-2', msg = '4) Calling Hello World (ACA 2) API via Azure Front Door. Expect 200.')\n",
" tests.verify(output, 'Hello World!')\n",
"\n",
" reqsAfd = ApimRequests(afd_endpoint_url, apim_apis[3]['subscriptionPrimaryKey'])\n",
" output = reqsAfd.singleGet('/aca-pool', msg = '5) Calling Hello World (ACA Pool) API via Azure Front Door. Expect 200.')\n",
" tests.verify(output, 'Hello World!')\n",
"if success:\n",
" utils.print_ok('Infrastructure creation completed successfully!')\n",
"else:\n",
" utils.print_message('ACA APIs were not created. Skipping ACA API calls.', blank_above = True)\n",
"\n",
"# 4) Unsuccessful call to Front Door without API subscription key (should fail with 401 Unauthorized)\n",
"reqsNoApiSubscription = ApimRequests(afd_endpoint_url)\n",
"output = reqsNoApiSubscription.singleGet('/', msg = 'Calling Hello World (Root) API without API subscription key. Expect 401.')\n",
"outputJson = utils.get_json(output)\n",
"tests.verify(outputJson['statusCode'], 401)\n",
"tests.verify(outputJson['message'], 'Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API.')\n",
"\n",
"tests.print_summary()\n",
"\n",
"utils.print_ok('All done!')"
" print(\"❌ Infrastructure creation failed!\")\n",
" raise SystemExit(1)"
]
},
{
Expand All @@ -313,9 +52,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "APIM Samples Python 3.12",
"display_name": ".venv (3.12.10)",
"language": "python",
"name": "apim-samples"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
Expand Down
Loading
Loading