Skip to content
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
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ env/
# Jupyter
.ipynb_checkpoints/

# VS Code
.vscode/

# Azure
.azure/

Expand All @@ -26,7 +23,8 @@ labs-in-progress/
# Exclude sensitive or generated files
.env


# Coverage data and reports
.coverage
tests/python/htmlcov/

shared/bicep/modules/**/*.json
3 changes: 3 additions & 0 deletions .vscode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Fixing Pylance unresolved import warnings

Follow [this documentation](https://github.com/microsoft/pylance-release/blob/main/TROUBLESHOOTING.md#unresolved-import-warnings).
21 changes: 21 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"python.analysis.extraPaths": [
"${workspaceFolder}/shared/python"
],
"python.analysis.completeFunctionParens": true,
"python.analysis.autoIndent": true,
"python.analysis.diagnosticSeverityOverrides": {
"reportDuplicateImport": "warning",
"reportUndefinedVariable": "information",
"reportUnusedVariable": "information"
},
"python.envFile": "${workspaceFolder}/.env",
"plantuml.render": "Local",
"plantuml.exportFormat": "svg",
"terminal.integrated.env.windows": {
"PATH": "${env:PATH}"
},
"plantuml.java": "C:\\Program Files\\OpenJDK\\jdk-22.0.2\\bin\\java.exe",
"plantuml.diagramsRoot": "diagrams/src",
"plantuml.exportOutDir": "diagrams/out"
}
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ The first time you run a Jupyter notebook, you'll be asked to install the Jupyte

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

### Adding a Sample

Adding a new sample is relatively straight-forward.

1. Create a new feature branch for the new sample.
1. Copy the `/samples/_TEMPLATE` folder.
1. Rename the copied folder to a name representative of the sample (e.g. "load-balancing", "authX", etc.)
1. Change the `create.ipynb` and `main.bicep` files. Look for the brackets (`[ ]`) brackets for specific inputs.
1. Add any policy.xml files to the same folder if they are specific to this sample. If they are to be reused, place them into the `/shared/apim-policies` folder instead.
1. Test the sample with all supported infrastructures.
1. Create a pull request for merge.

---

## Development
Expand All @@ -89,7 +101,6 @@ The repo uses the bicep linter and has rules defined in `bicepconfig.json`. See

We welcome contributions! Please consider forking the repo and creating issues and pull requests to share your samples. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. Thank you!


### Testing & Code Coverage

Python modules in `shared/python` are covered by comprehensive unit tests located in `tests/python`. All tests use [pytest](https://docs.pytest.org/) and leverage modern pytest features, including custom markers for unit and HTTP tests.
Expand Down
1 change: 0 additions & 1 deletion infrastructure/afd-apim/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Front Door & API Management & Container Apps Infrastructure

Secure architecture that takes all traffic off the public Internet once Azure Front Door is traversed. This is due to Front Door's use of a private link to Azure API Management.
Expand Down
19 changes: 9 additions & 10 deletions infrastructure/afd-apim/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,11 @@
"# 3) Define the APIs and their operations and policies\n",
"\n",
"# Policies\n",
"default_policy_xml = utils.policy_xml_replacement(DEFAULT_XML_POLICY_PATH)\n",
"hello_world_policy_xml = utils.policy_xml_replacement(HELLO_WORLD_XML_POLICY_PATH)\n",
"\n",
"# Hello World (Root)\n",
"api_hwroot_get = GET_APIOperation('This is a GET for API 1', hello_world_policy_xml)\n",
"api_hwroot = API('hello-world', 'Hello World', '', 'This is the root API for Hello World', default_policy_xml, [api_hwroot_get])\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",
Expand All @@ -63,16 +62,16 @@
" aca_backend_pool_policy_xml = utils.policy_xml_replacement(ACA_BACKEND_POOL_XML_POLICY_PATH)\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', default_policy_xml)\n",
" api_hwaca_1 = API('hello-world-aca-1', 'Hello World (ACA 1)', '/aca-1', 'This is the ACA API for Backend 1', aca_backend_1_policy_xml, [api_hwaca_1_get])\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 = aca_backend_1_policy_xml, 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', default_policy_xml)\n",
" api_hwaca_2 = API('hello-world-aca-2', 'Hello World (ACA 2)', '/aca-2', 'This is the ACA API for Backend 2', aca_backend_2_policy_xml, [api_hwaca_2_get])\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 = aca_backend_2_policy_xml, 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', default_policy_xml)\n",
" api_hwaca_pool = API('hello-world-aca-pool', 'Hello World (ACA Pool)', '/aca-pool', 'This is the ACA API for Backend Pool', aca_backend_pool_policy_xml, [api_hwaca_pool_get])\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 = aca_backend_pool_policy_xml, 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",
Expand Down Expand Up @@ -111,7 +110,7 @@
"\n",
"# 3) Print a deployment summary, if successful; otherwise, exit with an error\n",
"if not output.success:\n",
" exit('Deployment failed')\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",
Expand Down Expand Up @@ -219,7 +218,7 @@
"\n",
"# 3) Print a single, clear deployment summary if successful\n",
"if not output.success:\n",
" exit('Deployment failed')\n",
" raise SystemExit('Deployment failed')\n",
" \n",
"if output.success and output.json_data:\n",
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
Expand Down
1 change: 0 additions & 1 deletion infrastructure/apim-aca/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# API Management & Container Apps Infrastructure

This architecture secures API traffic by routing requests through Azure API Management, which is integrated with Azure Container Apps for backend processing. Telemetry is sent to Azure Monitor for observability.
Expand Down
17 changes: 8 additions & 9 deletions infrastructure/apim-aca/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,26 @@
"# 3) Define the APIs and their operations and policies\n",
"\n",
"# Policies\n",
"default_policy_xml = utils.policy_xml_replacement(DEFAULT_XML_POLICY_PATH)\n",
"hello_world_policy_xml = utils.policy_xml_replacement(HELLO_WORLD_XML_POLICY_PATH)\n",
"aca_backend_1_policy_xml = utils.policy_xml_replacement(ACA_BACKEND_1_XML_POLICY_PATH)\n",
"aca_backend_2_policy_xml = utils.policy_xml_replacement(ACA_BACKEND_2_XML_POLICY_PATH)\n",
"aca_backend_pool_policy_xml = utils.policy_xml_replacement(ACA_BACKEND_POOL_XML_POLICY_PATH)\n",
"\n",
"# Hello World (Root)\n",
"api_hwroot_get = GET_APIOperation('This is a GET for Hello World in the root', hello_world_policy_xml)\n",
"api_hwroot = API('hello-world', 'Hello World', '', 'This is the root API for Hello World', default_policy_xml, [api_hwroot_get])\n",
"api_hwroot = API('hello-world', 'Hello World', '', 'This is the root API for Hello World', operations = [api_hwroot_get])\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', default_policy_xml)\n",
"api_hwaca_1 = API('hello-world-aca-1', 'Hello World (ACA 1)', '/aca-1', 'This is the ACA API for Backend 1', aca_backend_1_policy_xml, [api_hwaca_1_get])\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 = aca_backend_1_policy_xml, 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', default_policy_xml)\n",
"api_hwaca_2 = API('hello-world-aca-2', 'Hello World (ACA 2)', '/aca-2', 'This is the ACA API for Backend 2', aca_backend_2_policy_xml, [api_hwaca_2_get])\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 = aca_backend_2_policy_xml, 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', default_policy_xml)\n",
"api_hwaca_pool = API('hello-world-aca-pool', 'Hello World (ACA Pool)', '/aca-pool', 'This is the ACA API for Backend Pool', aca_backend_pool_policy_xml, [api_hwaca_pool_get])\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 = aca_backend_pool_policy_xml, operations = [api_hwaca_pool_get])\n",
"\n",
"# APIs Array\n",
"apis: List[API] = [api_hwroot, api_hwaca_1, api_hwaca_2, api_hwaca_pool]\n",
Expand Down Expand Up @@ -101,7 +100,7 @@
"\n",
"# 3) Check the deployment outputs\n",
"if not output.success:\n",
" exit('Deployment failed')\n",
" raise SystemExit('Deployment failed')\n",
"\n",
"if output.success and output.json_data:\n",
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
Expand Down
1 change: 0 additions & 1 deletion infrastructure/simple-apim/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Simple API Management Infrastructure

This architecture provides a basic API gateway using Azure API Management, suitable for simple scenarios where secure API exposure and basic observability are required.
Expand Down
5 changes: 2 additions & 3 deletions infrastructure/simple-apim/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@
"# 3) Define the APIs and their operations and policies\n",
"\n",
"# Policies\n",
"default_policy_xml = utils.policy_xml_replacement(DEFAULT_XML_POLICY_PATH)\n",
"hello_world_policy_xml = utils.policy_xml_replacement(HELLO_WORLD_XML_POLICY_PATH)\n",
"\n",
"# Hello World (Root)\n",
"api_hwroot_get = GET_APIOperation('This is a GET for API 1', hello_world_policy_xml)\n",
"api_hwroot = API('hello-world', 'Hello World', '', 'This is the root API for Hello World', default_policy_xml, [api_hwroot_get])\n",
"api_hwroot = API('hello-world', 'Hello World', '', 'This is the root API for Hello World', operations = [api_hwroot_get])\n",
"\n",
"# APIs Array\n",
"apis: List[API] = [api_hwroot]\n",
Expand Down Expand Up @@ -84,7 +83,7 @@
"\n",
"# 3) Check the deployment outputs\n",
"if not output.success:\n",
" exit('Deployment failed')\n",
" raise SystemExit('Deployment failed')\n",
"\n",
"if output.success and output.json_data:\n",
" apim_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
Expand Down
175 changes: 175 additions & 0 deletions samples/_TEMPLATE/create.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Samples: [TEMPLATE NAME]\n",
"\n",
"[BRIEF SAMPLE DESCRIPTION]\n",
"\n",
"⚙️ **Supported infrastructures**: [Comma-separated names of the supported infrastructures or \"All infrastructures\"]\n",
"\n",
"⌚ **Expected *Run All* runtime (excl. infrastructure prerequisite): ~[NOTEBOOK RUNTIME] minute**\n",
"\n",
"## Objectives\n",
"\n",
"1. [LEARNING / EXPERIMENTATION OBJECTIVE 1]\n",
"1. [LEARNING / EXPERIMENTATION OBJECTIVE 2]\n",
"1. ...\n",
"\n",
"## Configuration\n",
"\n",
"1. Decide which of the [Infrastructure Architectures](../../README.md#infrastructure-architectures) you wish to use.\n",
" 1. If the infrastructure _does not_ yet exist, navigate to the desired [infrastructure](../../infrastructure/) folder and follow its README.md.\n",
" 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."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Initialize notebook variables\n",
"\n",
"Configures everything that's needed for deployment. \n",
"\n",
"[ADD ANY SPECIAL INSTRUCTIONS]\n",
"\n",
"**Modify entries under _1) User-defined parameters_ and _3) Define the APIs and their operations and policies_**."
]
},
{
"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",
"deployment = INFRASTRUCTURE.AFD_APIM_PE\n",
"tags = ['TAG1', 'TAG2', '...'] // [ENTER DESCRIPTIVE TAG(S)]\n",
"\n",
"# 2) Service-defined parameters (please do not change these)\n",
"rg_name = utils.get_infra_rg_name(deployment, index)\n",
"\n",
"# 3) Define the APIs and their operations and policies\n",
"\n",
"# API 1\n",
"# api1_get = GET_APIOperation('This is a GET for API 1')\n",
"# api1_post = POST_APIOperation('This is a POST for API 1')\n",
"# api1 = API('API1', 'API 1', '/api1', 'This is API 1', operations = [api1_get, api1_post], tags = tags)\n",
"\n",
"# API n\n",
"# ...\n",
"\n",
"# APIs Array\n",
"# apis: List[API] = [api1, apin]\n",
"\n",
"utils.print_ok('Notebook initialized')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 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",
"\n",
"# 1) Define the Bicep parameters with serialized APIs\n",
"bicep_parameters = {\n",
" 'apis': {'value': [api.to_dict() for api in apis]}\n",
"}\n",
"\n",
"# 2) Infrastructure must be in place before samples can be layered on top\n",
"if not utils.does_resource_group_exist(rg_name):\n",
" utils.print_error(f'The specified infrastructure resource group and its resources must exist first. Please check that the user-defined parameters above are correctly referencing an existing infrastructure. If it does not yet exist, run the desired infrastructure in the /infra/ folder first.')\n",
" raise SystemExit(1)\n",
"\n",
"# 3) Run the deployment\n",
"output = utils.create_bicep_deployment_group(rg_name, rg_location, deployment, bicep_parameters)\n",
"\n",
"# 4) 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_gateway_url = output.get('apimResourceGatewayURL', 'APIM API Gateway URL')\n",
"\n",
"utils.print_ok('Deployment completed')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Verify API Request Success\n",
"\n",
"Assert that the deployment was successful by making simple calls to APIM. \n",
"\n",
"❗️ 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."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import utils\n",
"from apimrequests import ApimRequests\n",
"\n",
"[ADD RELEVANT TESTS HERE]\n",
"\n",
"# 1) Issue a direct request to API Management\n",
"# reqsApim = ApimRequests(apim_gateway_url)\n",
"# reqsApim.singleGet('/request-headers', msg = 'Calling Request Headers API via API Management Gateway URL. Response codes 200 and 403 are both valid depending on the infrastructure used.')\n",
"\n",
"# # 2) Issue requests against Front Door.\n",
"# # Check if the infrastructure architecture deployment uses Azure Front Door.\n",
"# utils.print_message('Checking if the infrastructure architecture deployment uses Azure Front Door.', blank_above = True)\n",
"# afd_endpoint_url = utils.get_frontdoor_url(deployment, rg_name)\n",
"\n",
"# if afd_endpoint_url:\n",
"# reqsAfd = ApimRequests(afd_endpoint_url)\n",
"# reqsAfd.singleGet('/request-headers', msg = 'Calling Request Headers API via via Azure Front Door. Expect 200.')\n",
"\n",
"utils.print_ok('All done!')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.10"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Loading