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
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ _Try it out, learn from it, apply it in your setups._

We provide several common architectural approaches to integrating APIM into your Azure ecosystem. While these are high-fidelity setups, they are not production-ready. Please refer to the [Azure API Management landing zone accelerator](https://learn.microsoft.com/azure/cloud-adoption-framework/scenarios/app-platform/api-management/landing-zone-accelerator) for up-to-date production setups.

- [Simple API Management](./infrastructure/simple-apim)
- Just the basics with a publicly accessible API Management intance fronting your APIs. This is the innermost way to experience and experiment with the APIM policies.
- [API Management & Container Apps](./infrastructure/apim-aca)
- [Simple API Management](./infrastructure/simple-apim) (simple-apim)
- Just the basics with a publicly accessible API Management intance fronting your APIs. This is the innermost way to experience and experiment with the APIM policies.

- [API Management & Container Apps](./infrastructure/apim-aca) (apim-aca)
- APIs are often times implemented in containers that are running in Azure Container Apps. This architecture accesses the container apps publicly. It's beneficial to test both APIM and container app URLs here to contrast and compare experiences of API calls through and bypassing APIM. It is not intended to be a security baseline.
- [Secure Front Door & API Management & Container Apps](./infrastructure/afd-apim)

- [Secure Front Door & API Management & Container Apps](./infrastructure/afd-apim) (afd-apim)
- A higher-fidelity implementation of a secured setup in which Azure Front Door connects to APIM via the new private link integration. This traffic, once it traverses through Front Door, rides entirely on Microsoft-owned and operated networks. Similarly, the connection from APIM to Container Apps is secured but through a VNet configuration (it is also entirely possible to do this via private link). It's noteworthy that we are using APIM Standard V2 here as we need the ability to accept a private link from Front Door.

---
Expand Down Expand Up @@ -71,6 +73,13 @@ Run through the following steps to create a Python virtual environment before do

The first time you run a Jupyter notebook, you'll be asked to install the Jupyter kernel package (ipykernel).

### List of Samples

| Sample Name | Description | Supported Infrastructure(s) |
|------------------|-----------------------------------------------------------------------------|--------------------------------------------|
| [General](./samples/general) | Basic demonstration of APIM sample setup and policy usage. | All infrastructures |
| [Load Balancing](./samples/load-balancing) | Example of APIM policy-based load balancing across backends. | apim-aca, afd-apim (with ACA) |

### 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.
Expand Down
6 changes: 3 additions & 3 deletions infrastructure/afd-apim/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
"if use_ACA:\n",
" utils.print_info('ACA APIs will be created.')\n",
"\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",
" aca_backend_1_policy_xml = utils.read_policy_xml(ACA_BACKEND_1_XML_POLICY_PATH)\n",
" aca_backend_2_policy_xml = utils.read_policy_xml(ACA_BACKEND_2_XML_POLICY_PATH)\n",
" aca_backend_pool_policy_xml = utils.read_policy_xml(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')\n",
Expand Down
11 changes: 0 additions & 11 deletions infrastructure/afd-apim/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ module acaEnvModule '../../shared/bicep/modules/aca/v1/environment.bicep' = if (
name: 'acaEnvModule'
params: {
name: 'cae-${resourceSuffix}'
location: location
logAnalyticsWorkspaceCustomerId: lawModule.outputs.customerId
logAnalyticsWorkspaceSharedKey: lawModule.outputs.clientSecret
subnetResourceId: acaSubnetResourceId
Expand All @@ -148,24 +147,16 @@ module acaModule1 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (u
name: 'acaModule-1'
params: {
name: 'ca-${resourceSuffix}-mockwebapi-1'
location: location
containerImage: IMG_MOCK_WEB_API
environmentId: acaEnvModule.outputs.environmentId
cpu: '0.5'
memory: '1.0Gi'
ingressPort: 8080
}
}
module acaModule2 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (useACA) {
name: 'acaModule-2'
params: {
name: 'ca-${resourceSuffix}-mockwebapi-2'
location: location
containerImage: IMG_MOCK_WEB_API
environmentId: acaEnvModule.outputs.environmentId
cpu: '0.5'
memory: '1.0Gi'
ingressPort: 8080
}
}

Expand Down Expand Up @@ -267,7 +258,6 @@ module apimDnsPrivateLinkModule '../../shared/bicep/modules/dns/v1/dns-private-l
module acaDnsPrivateZoneModule '../../shared/bicep/modules/dns/v1/aca-dns-private-zone.bicep' = if (useACA && !empty(acaSubnetResourceId)) {
name: 'acaDnsPrivateZoneModule'
params: {
location: location
acaEnvironmentRandomSubdomain: acaEnvModule.outputs.environmentRandomSubdomain
acaEnvironmentStaticIp: acaEnvModule.outputs.environmentStaticIp
vnetId: vnetModule.outputs.vnetId
Expand All @@ -278,7 +268,6 @@ module acaDnsPrivateZoneModule '../../shared/bicep/modules/dns/v1/aca-dns-privat
module afdModule '../../shared/bicep/modules/afd/v1/afd.bicep' = {
name: 'afdModule'
params: {
location: 'global'
resourceSuffix: resourceSuffix
afdName: afdEndpointName
fdeName: afdEndpointName
Expand Down
8 changes: 4 additions & 4 deletions infrastructure/apim-aca/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@
"# 3) Define the APIs and their operations and policies\n",
"\n",
"# Policies\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",
"hello_world_policy_xml = utils.read_policy_xml(HELLO_WORLD_XML_POLICY_PATH)\n",
"aca_backend_1_policy_xml = utils.read_policy_xml(ACA_BACKEND_1_XML_POLICY_PATH)\n",
"aca_backend_2_policy_xml = utils.read_policy_xml(ACA_BACKEND_2_XML_POLICY_PATH)\n",
"aca_backend_pool_policy_xml = utils.read_policy_xml(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",
Expand Down
12 changes: 1 addition & 11 deletions infrastructure/apim-aca/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ param location string = resourceGroup().location
param resourceSuffix string = uniqueString(subscription().id, resourceGroup().id)

param apimName string = 'apim-${resourceSuffix}'

param apimSku string
param apis array = []

Expand Down Expand Up @@ -59,26 +58,17 @@ module acaModule1 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = {
name: 'acaModule-1'
params: {
name: 'ca-${resourceSuffix}-mockwebapi-1'
location: location
containerImage: IMG_MOCK_WEB_API
environmentId: acaEnvModule.outputs.environmentId
cpu: '0.5'
memory: '1.0Gi'
ingressPort: 8080
ingressExternal: true
}
}

module acaModule2 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = {
name: 'acaModule-2'
params: {
name: 'ca-${resourceSuffix}-mockwebapi-2'
location: location
containerImage: IMG_MOCK_WEB_API
environmentId: acaEnvModule.outputs.environmentId
cpu: '0.5'
memory: '1.0Gi'
ingressPort: 8080
ingressExternal: true
}
}

Expand Down
2 changes: 1 addition & 1 deletion infrastructure/simple-apim/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"# 3) Define the APIs and their operations and policies\n",
"\n",
"# Policies\n",
"hello_world_policy_xml = utils.policy_xml_replacement(HELLO_WORLD_XML_POLICY_PATH)\n",
"hello_world_policy_xml = 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', hello_world_policy_xml)\n",
Expand Down
10 changes: 8 additions & 2 deletions samples/_TEMPLATE/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"1. [LEARNING / EXPERIMENTATION OBJECTIVE 2]\n",
"1. ...\n",
"\n",
"## Lab Components\n",
"\n",
"[DESCRIBE IN MORE DETAIL WHAT THIS LAB SETS UP AND HOW THIS BENEFITS THE LEARNER/USER.]\n",
"\n",
"## Configuration\n",
"\n",
"1. Decide which of the [Infrastructure Architectures](../../README.md#infrastructure-architectures) you wish to use.\n",
Expand Down Expand Up @@ -51,7 +55,8 @@
"rg_location = 'eastus2'\n",
"index = 1\n",
"deployment = INFRASTRUCTURE.AFD_APIM_PE\n",
"tags = ['TAG1', 'TAG2', '...'] // [ENTER DESCRIPTIVE TAG(S)]\n",
"tags = ['tag1', 'tag2', '...'] # ENTER DESCRIPTIVE TAG(S)\n",
"api_prefix = '' # OPTIONAL: ENTER A PREFIX FOR THE APIS TO REDUCE COLLISION POTENTIAL WITH OTHER SAMPLES\n",
"\n",
"# 2) Service-defined parameters (please do not change these)\n",
"rg_name = utils.get_infra_rg_name(deployment, index)\n",
Expand All @@ -68,6 +73,7 @@
"\n",
"# APIs Array\n",
"# apis: List[API] = [api1, apin]\n",
"apis: List[API] = []\n",
"\n",
"utils.print_ok('Notebook initialized')"
]
Expand Down Expand Up @@ -132,7 +138,7 @@
"import utils\n",
"from apimrequests import ApimRequests\n",
"\n",
"[ADD RELEVANT TESTS HERE]\n",
"# [ADD RELEVANT TESTS HERE]\n",
"\n",
"# 1) Issue a direct request to API Management\n",
"# reqsApim = ApimRequests(apim_gateway_url)\n",
Expand Down
10 changes: 7 additions & 3 deletions samples/_TEMPLATE/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,27 @@ param apimName string = 'apim-${resourceSuffix}'
param appInsightsName string = 'appi-${resourceSuffix}'
param apis array = []

// [ADD RELEVANT PARAMETERS HERE]

// ------------------
// RESOURCES
// ------------------

// https://learn.microsoft.com/azure/templates/microsoft.insights/components
resource appInsightsModule 'Microsoft.Insights/components@2020-02-02' existing = {
resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = {
name: appInsightsName
}

var appInsightsId = appInsightsModule.id
var appInsightsInstrumentationKey = appInsightsModule.properties.InstrumentationKey
var appInsightsId = appInsights.id
var appInsightsInstrumentationKey = appInsights.properties.InstrumentationKey

// https://learn.microsoft.com/azure/templates/microsoft.apimanagement/service
resource apimService 'Microsoft.ApiManagement/service@2024-06-01-preview' existing = {
name: apimName
}

// [ADD RELEVANT BICEP MODULES HERE]

// APIM APIs
module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in apis: if(length(apis) > 0) {
name: '${api.name}-${resourceSuffix}'
Expand All @@ -50,3 +53,4 @@ module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in a
output apimServiceId string = apimService.id
output apimServiceName string = apimService.name
output apimResourceGatewayURL string = apimService.properties.gatewayUrl
// [ADD RELEVANT OUTPUTS HERE]
3 changes: 0 additions & 3 deletions shared/bicep/modules/afd/v1/afd.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
// PARAMETERS
// ------------------------------

@description('Location to be used for resources. Defaults to the resource group location')
param location string = resourceGroup().location

@description('The unique suffix to append. Defaults to a unique string based on subscription and resource group IDs.')
param resourceSuffix string = uniqueString(subscription().id, resourceGroup().id)

Expand Down
3 changes: 0 additions & 3 deletions shared/bicep/modules/apim/v1/api.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
// PARAMETERS
// ------------------------------

@description('Location to be used for resources. Defaults to the resource group location')
param location string = resourceGroup().location

@description('The unique suffix to append. Defaults to a unique string based on subscription and resource group IDs.')
param resourceSuffix string = uniqueString(subscription().id, resourceGroup().id)

Expand Down
4 changes: 2 additions & 2 deletions shared/bicep/modules/dns/v1/aca-dns-private-zone.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
// PARAMETERS
// ------------------------------

@description('The Azure region in which the Azure Container Apps environment resides (e.g., eastus2).')
param location string
@description('The Azure region in which the Azure Container Apps environment resides (e.g., eastus2). Defaults to the resource group location')
param location string = resourceGroup().location

@description('The unique subdomain of the ACA environment (used for the wildcard A record).')
param acaEnvironmentRandomSubdomain string
Expand Down
2 changes: 1 addition & 1 deletion shared/python/apimtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# PRIVATE METHODS
# ------------------------------

# Placing this here privately as putting it into the utils module would constitude a circular import
# Placing this here privately as putting it into the utils module would constitute a circular import
def _read_policy_xml(policy_xml_filepath: str) -> str:
"""
Read and return the contents of a policy XML file.
Expand Down
17 changes: 17 additions & 0 deletions shared/python/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,23 @@ def policy_xml_replacement(policy_xml_filepath: str) -> str:
# Convert the XML to JSON format
return policy_template_xml

def read_policy_xml(policy_xml_filepath: str) -> str:
"""
Read and return the contents of a policy XML file.

Args:
policy_xml_filepath (str): Path to the policy XML file.

Returns:
str: Contents of the policy XML file.
"""

# Read the specified policy XML file
with open(policy_xml_filepath, 'r') as policy_xml_file:
policy_template_xml = policy_xml_file.read()

return policy_template_xml

def _cleanup_resources(deployment_name: str, rg_name: str) -> None:
"""
Clean up resources associated with a deployment in a resource group.
Expand Down
28 changes: 28 additions & 0 deletions tests/python/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import builtins
import pytest
from io import StringIO
from unittest.mock import patch, MagicMock, mock_open
from shared.python import utils
from apimtypes import INFRASTRUCTURE
Expand Down Expand Up @@ -133,6 +134,33 @@ def test_create_resource_group(monkeypatch):
utils.create_resource_group('foo', 'bar')
assert called['info'] and called['run']

# ------------------------------
# read_policy_xml
# ------------------------------

def test_read_policy_xml_success(monkeypatch):
"""Test reading a valid XML file returns its contents."""
xml_content = '<policies><inbound><base /></inbound></policies>'
m = mock_open(read_data=xml_content)
monkeypatch.setattr(builtins, 'open', m)
result = utils.read_policy_xml('dummy.xml')
assert result == xml_content

def test_read_policy_xml_file_not_found(monkeypatch):
"""Test reading a missing XML file raises FileNotFoundError."""
def raise_fnf(*args, **kwargs):
raise FileNotFoundError('File not found')
monkeypatch.setattr(builtins, 'open', raise_fnf)
with pytest.raises(FileNotFoundError):
utils.read_policy_xml('missing.xml')

def test_read_policy_xml_empty_file(monkeypatch):
"""Test reading an empty XML file returns an empty string."""
m = mock_open(read_data='')
monkeypatch.setattr(builtins, 'open', m)
result = utils.read_policy_xml('empty.xml')
assert result == ''

# ------------------------------
# policy_xml_replacement
# ------------------------------
Expand Down