diff --git a/infrastructure/apim-workspaces/README.md b/infrastructure/apim-workspaces/README.md new file mode 100644 index 0000000..f82f5ff --- /dev/null +++ b/infrastructure/apim-workspaces/README.md @@ -0,0 +1,20 @@ +# 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. + +Diagram showing a simple Azure API Management architecture. API Management acts as a gateway for API consumers. Telemetry is sent to Azure Monitor. + +## 🎯 Objectives + +1. Provide the simplest Azure API Management infrastructure with a public ingress to allow for easy testing +1. Enable observability by sending telemetry to Azure Monitor + +## ⚙️ Configuration + +Adjust the `user-defined parameters` in this lab's Jupyter Notebook's [Initialize notebook variables](./create.ipynb#initialize-notebook-variables) section. + +## ▶️ Execution + +👟 **Expected *Run All* runtime: ~3 minutes** + +1. Execute this lab's [Jupyter Notebook](./create.ipynb) step-by-step or via _Run All_. diff --git a/infrastructure/apim-workspaces/Simple API Management Architecture.svg b/infrastructure/apim-workspaces/Simple API Management Architecture.svg new file mode 100644 index 0000000..52c286e --- /dev/null +++ b/infrastructure/apim-workspaces/Simple API Management Architecture.svg @@ -0,0 +1 @@ +Simple API Management Architecture API Management Application Insights Log AnalyticsAppsAPIsAPI ConsumersDeployed to Sendstelemetry Stores telemetry \ No newline at end of file diff --git a/infrastructure/apim-workspaces/clean-up.ipynb b/infrastructure/apim-workspaces/clean-up.ipynb new file mode 100644 index 0000000..9307142 --- /dev/null +++ b/infrastructure/apim-workspaces/clean-up.ipynb @@ -0,0 +1,49 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 🗑️ Clean up resources\n", + "\n", + "When you're finished with the lab, you should remove all your deployed resources from Azure to avoid extra charges and keep your Azure subscription uncluttered." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import utils\n", + "from apimtypes import INFRASTRUCTURE\n", + "\n", + "deployment = INFRASTRUCTURE.SIMPLE_APIM\n", + "indexes = [1]\n", + "\n", + "utils.cleanup_infra_deployments(deployment, indexes)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "APIM Samples Python 3.12", + "language": "python", + "name": "apim-samples" + }, + "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 +} diff --git a/infrastructure/apim-workspaces/create.ipynb b/infrastructure/apim-workspaces/create.ipynb new file mode 100644 index 0000000..a3247b9 --- /dev/null +++ b/infrastructure/apim-workspaces/create.ipynb @@ -0,0 +1,210 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 🛠️ 1. Initialize notebook variables\n", + "\n", + "Configures everything that's needed for deployment. \n", + "\n", + "❗️ **Modify entries under _1) User-defined parameters_**." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "👉🏽 \u001b[1;34mResource group name : apim-infra-apim-workspaces-1\u001b[0m \n", + "📄 Reading policy XML from : c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\shared\\apim-policies\\fragments\\pf-authz-match-all.xml\n", + "📄 Reading policy XML from : c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\shared\\apim-policies\\fragments\\pf-authz-match-any.xml\n", + "📄 Reading policy XML from : c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\shared\\apim-policies\\fragments\\pf-http-response-200.xml\n", + "📄 Reading policy XML from : c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\shared\\apim-policies\\fragments\\pf-remove-request-headers.xml\n", + "📄 Reading policy XML from : C:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\shared\\apim-policies\\hello-world.xml\n", + "\n", + "✅ \u001b[1;32mNotebook initialized\u001b[0m ⌚ 12:56:06.226056 \n" + ] + } + ], + "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.PREMIUM\n", + "deployment = INFRASTRUCTURE.APIM_WORKSPACES\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)\n", + "rg_name = utils.get_infra_rg_name(deployment, index)\n", + "rg_tags = utils.build_infrastructure_tags(deployment)\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('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", + "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", + "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", + "\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": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "⚙️ \u001b[1;34maz group show --name apim-infra-apim-workspaces-1\u001b[0m \n", + "📝 Updated the policy XML in the bicep parameters file 'params.json'\n", + "⚙️ \u001b[1;34maz deployment group create --name apim-workspaces --resource-group apim-infra-apim-workspaces-1 --template-file \"c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\infrastructure\\apim-workspaces\\main.bicep\" --parameters \"c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\infrastructure\\apim-workspaces\\params.json\" --query \"properties.outputs\"\u001b[0m \n", + "⛔ \u001b[1;31mCommand failed with error: WARNING: A new Bicep release is available: v0.36.1. Upgrade now by running \"az bicep upgrade\".\n", + "ERROR: {\"status\":\"Failed\",\"error\":{\"code\":\"DeploymentFailed\",\"target\":\"/subscriptions/3a2450c5-be2a-4171-bb9c-9e6c275104a2/resourceGroups/apim-infra-apim-workspaces-1/providers/Microsoft.Resources/deployments/apim-workspaces\",\"message\":\"At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/arm-deployment-operations for usage details.\",\"details\":[{\"code\":\"ResourceDeploymentFailure\",\"target\":\"/subscriptions/3a2450c5-be2a-4171-bb9c-9e6c275104a2/resourceGroups/apim-infra-apim-workspaces-1/providers/Microsoft.Resources/deployments/gatewayModule\",\"message\":\"The resource write operation failed to complete successfully, because it reached terminal provisioning state 'Failed'.\",\"details\":[{\"code\":\"DeploymentFailed\",\"target\":\"/subscriptions/3a2450c5-be2a-4171-bb9c-9e6c275104a2/resourceGroups/apim-infra-apim-workspaces-1/providers/Microsoft.Resources/deployments/gatewayModule\",\"message\":\"At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/arm-deployment-operations for usage details.\",\"details\":[{\"code\":\"InvalidConfigConnectionSourceId\",\"message\":\"Invalid sourceId /subscriptions/3a2450c5-be2a-4171-bb9c-9e6c275104a2/resourceGroups/apim-infra-apim-workspaces-1/providers/Microsoft.ApiManagement/service/apim-xysk7zo6uu33w/gateways/apim-xysk7zo6uu33w-gateway. Expected '/subscriptions//resourceGroups//providers/Microsoft.ApiManagement/service//workspaces/'\"}]}]}]}}\n", + "\u001b[0m ⌚ 15:52:54.843069 [1m:39s]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"C:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\shared\\python\\utils.py\", line 997, in run\n", + " output_text = subprocess.check_output(command, shell = True, stderr = subprocess.STDOUT).decode(\"utf-8\")\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"C:\\Python312\\Lib\\subprocess.py\", line 466, in check_output\n", + " return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"C:\\Python312\\Lib\\subprocess.py\", line 571, in run\n", + " raise CalledProcessError(retcode, process.args,\n", + "subprocess.CalledProcessError: Command 'az deployment group create --name apim-workspaces --resource-group apim-infra-apim-workspaces-1 --template-file \"c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\infrastructure\\apim-workspaces\\main.bicep\" --parameters \"c:\\Users\\robertlacher\\source\\Repos\\Apim-Samples\\infrastructure\\apim-workspaces\\params.json\" --query \"properties.outputs\"' returned non-zero exit status 1.\n" + ] + }, + { + "ename": "SystemExit", + "evalue": "Deployment failed", + "output_type": "error", + "traceback": [ + "An exception has occurred, use %tb to see the full traceback.\n", + "\u001b[31mSystemExit\u001b[39m\u001b[31m:\u001b[39m Deployment failed\n" + ] + } + ], + "source": [ + "import utils\n", + "\n", + "# 1) Define the Bicep parameters with serialized APIs\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", + " 'revealBackendApiInfo' : {'value:': reveal_backend}\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) Check the deployment outputs\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": [ + "### ✅ 3. Verify API Request Success\n", + "\n", + "Assert that the deployment was successful by making simple calls to APIM. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import utils\n", + "from apimrequests import ApimRequests\n", + "from apimtesting import ApimTesting\n", + "\n", + "reqs = ApimRequests(apim_gateway_url)\n", + "tests = ApimTesting(\"Simple APIM Tests\", deployment, deployment)\n", + "\n", + "output = reqs.singleGet('/', msg = 'Calling Hello World (Root) API')\n", + "tests.verify(output, 'Hello World from API Management!')\n", + "\n", + "tests.print_summary()\n", + "\n", + "utils.print_ok('All done!')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 🗑️ Clean up resources\n", + "\n", + "When you're finished experimenting, it's advisable to remove all associated resources from Azure to avoid unnecessary cost.\n", + "Use the [clean-up notebook](clean-up.ipynb) for that." + ] + } + ], + "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 +} diff --git a/infrastructure/apim-workspaces/main.bicep b/infrastructure/apim-workspaces/main.bicep new file mode 100644 index 0000000..93409d5 --- /dev/null +++ b/infrastructure/apim-workspaces/main.bicep @@ -0,0 +1,314 @@ +// ------------------ +// 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) + +param apimName string = 'apim-${resourceSuffix}' + +param apimSku string +param apis array = [] +param policyFragments array = [] + +@description('Reveals the backend API information. Defaults to true. *** WARNING: This will expose backend API information to the caller - For learning & testing only! ***') +param revealBackendApiInfo bool = true + +param vnetName string = 'vnet-${resourceSuffix}' +param vnetAddressPrefixes array = ['10.0.0.0/16'] +param apimSubnetName string = 'snet-apim' +param apimSubnetPrefix string = '10.0.0.0/24' + +@description('The name of the subnet for the API Management Workspace Gateway. Defaults to "snet-gateway".') +param gatewaySubnetName string = 'snet-gateway' +@description('The prefix for the gateway subnet. Defaults to 10.0.1.0/24.') +param gatewaySubnetPrefix string = '10.0.1.0/24' +@description('The capacity for the Workspace Gateway. Defaults to 1.') +param gatewayCapacity int = 1 +@description('The SKU for the Workspace Gateway. Defaults to "WorkspaceGatewayPremium".') +@allowed(['WorkspaceGatewayPremium', 'WorkspaceGatewayStandard', 'Standard']) +param gatewaySku string = 'WorkspaceGatewayPremium' +@description('The type of network for the Workspace Gateway. Defaults to "External".') +@allowed(['External', 'Internal', 'None']) +param gatewayNetworkType string = 'Internal' + +// ------------------ +// RESOURCES +// ------------------ + +// 1. Log Analytics Workspace +module lawModule '../../shared/bicep/modules/operational-insights/v1/workspaces.bicep' = { + name: 'lawModule' +} + +var lawId = lawModule.outputs.id + +// 2. Application Insights +module appInsightsModule '../../shared/bicep/modules/monitor/v1/appinsights.bicep' = { + name: 'appInsightsModule' + params: { + lawId: lawId + customMetricsOptedInType: 'WithDimensions' + } +} + +var appInsightsId = appInsightsModule.outputs.id +var appInsightsInstrumentationKey = appInsightsModule.outputs.instrumentationKey + +// 3. Virtual Networks and Subnets +resource nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { + name: 'nsg-default' + location: location + properties: { + securityRules: [ + // Inbound rules + { + name: 'Allow-ApiManagement-Management' + properties: { + priority: 100 + direction: 'Inbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'ApiManagement' + sourcePortRange: '*' + destinationAddressPrefix: 'VirtualNetwork' + destinationPortRange: '3443' + } + } + { + name: 'Allow-AzureLoadBalancer-6390' + properties: { + priority: 110 + direction: 'Inbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'AzureLoadBalancer' + sourcePortRange: '*' + destinationAddressPrefix: 'VirtualNetwork' + destinationPortRange: '6390' + } + } + // Outbound rules + { + name: 'Allow-VNet-Storage-443' + properties: { + priority: 200 + direction: 'Outbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: 'Storage' + destinationPortRange: '443' + } + } + { + name: 'Allow-VNet-Internet-80' + properties: { + priority: 210 + direction: 'Outbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: 'Internet' + destinationPortRange: '80' + } + } + { + name: 'Allow-VNet-Internet-443' + properties: { + priority: 220 + direction: 'Outbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: 'Internet' + destinationPortRange: '443' + } + } + { + name: 'Allow-VNet-Sql-1433' + properties: { + priority: 230 + direction: 'Outbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: 'Sql' + destinationPortRange: '1433' + } + } + { + name: 'Allow-VNet-AzureKeyVault-443' + properties: { + priority: 240 + direction: 'Outbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: 'AzureKeyVault' + destinationPortRange: '443' + } + } + { + name: 'Allow-VNet-AzureMonitor-1886-443' + properties: { + priority: 250 + direction: 'Outbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: 'AzureMonitor' + destinationPortRanges: [ '1886', '443' ] + } + } + { + name: 'Allow-VNet-AzureActiveDirectory-443' + properties: { + priority: 260 + direction: 'Outbound' + access: 'Allow' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + destinationAddressPrefix: 'AzureActiveDirectory' + destinationPortRange: '443' + } + } + ] + } +} +module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { + name: 'vnetModule' + params: { + vnetName: vnetName + vnetAddressPrefixes: vnetAddressPrefixes + subnets: [ + // APIM Subnet + { + name: apimSubnetName + properties: { + addressPrefix: apimSubnetPrefix + networkSecurityGroup: { + id: nsg.id + } + delegations: [] + } + } + gatewayNetworkType != 'None' ? { + // Gateway Subnet + name: gatewaySubnetName + properties: { + addressPrefix: gatewaySubnetPrefix + networkSecurityGroup: { + id: nsg.id + } + delegations: [ + { + name: gatewayNetworkType == 'External' ? 'Microsoft.Web/serverFarms' : 'Microsoft.Web/hostingEnvironments' + properties: { + serviceName: gatewayNetworkType == 'External' ? 'Microsoft.Web/serverFarms' : 'Microsoft.Web/hostingEnvironments' + } + } + ] + } + } : null + ] + } +} + +// TODO: We have a timing issue here in that we may get a null if this happens too quickly after the vnet module executes. +var apimSubnetResourceId = resourceId(resourceGroup().name, 'Microsoft.Network/virtualNetworks/subnets', vnetName, apimSubnetName) + + +// 3. API Management +module apimModule '../../shared/bicep/modules/apim/v1/apim.bicep' = { + name: 'apimModule' + params: { + apimSku: apimSku + apimSubnetResourceId: apimSubnetResourceId + appInsightsInstrumentationKey: appInsightsInstrumentationKey + appInsightsId: appInsightsId + globalPolicyXml: revealBackendApiInfo ? loadTextContent('../../shared/apim-policies/all-apis-reveal-backend.xml') : loadTextContent('../../shared/apim-policies/all-apis.xml') + } +} + +module workspaceModule '../../shared/bicep/modules/apim/v1/workspace.bicep' = { + name: 'workspaceModule' + params: { + apimName: apimName + apimWorkspaceName: '${apimName}-workspace' + apimWorkspaceDescription: 'API Management Workspace for ${apimName}' + apimWorkspaceDisplayName: '${apimName} Workspace' + } + dependsOn: [ + apimModule + ] +} + +module gatewayModule '../../shared/bicep/modules/apim/v1/workspace-gateway.bicep' = { + name: 'gatewayModule' + params: { + apimName: apimName + apimWorkspaceGatewayCapacity: gatewayCapacity + apimWorkspaceGatewaySku: gatewaySku + apimWorkspaceGatewaySubnetId: resourceId(resourceGroup().name, 'Microsoft.Network/virtualNetworks/subnets', vnetName, gatewaySubnetName) + apimWorkspaceGatewayNetworkType: gatewayNetworkType + apimWorkspaceName: '${apimName}-workspace' + } + dependsOn: [ + apimModule + workspaceModule + ] +} + +// 4. APIM Policy Fragments +module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment.bicep' = [for pf in policyFragments: { + name: 'pf-${pf.name}' + params:{ + apimName: apimName + policyFragmentName: pf.name + policyFragmentDescription: pf.description + policyFragmentValue: pf.policyXml + } + dependsOn: [ + apimModule + ] +}] + +// 5. APIM APIs +module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in apis: if(length(apis) > 0) { + name: 'api-${api.name}' + params: { + apimName: apimName + appInsightsInstrumentationKey: appInsightsInstrumentationKey + appInsightsId: appInsightsId + api: api + } + dependsOn: [ + apimModule + ] +}] + + +// ------------------ +// MARK: OUTPUTS +// ------------------ + +output applicationInsightsAppId string = appInsightsModule.outputs.appId +output applicationInsightsName string = appInsightsModule.outputs.applicationInsightsName +output logAnalyticsWorkspaceId string = lawModule.outputs.customerId +output apimServiceId string = apimModule.outputs.id +output apimServiceName string = apimModule.outputs.name +output apimResourceGatewayURL string = apimModule.outputs.gatewayUrl + +#disable-next-line outputs-should-not-contain-secrets +//output apimSubscription1Key string = apimModule.outputs.[0].listSecrets().primaryKey diff --git a/shared/bicep/modules/apim/v1/workspace-gateway.bicep b/shared/bicep/modules/apim/v1/workspace-gateway.bicep new file mode 100644 index 0000000..5a274b5 --- /dev/null +++ b/shared/bicep/modules/apim/v1/workspace-gateway.bicep @@ -0,0 +1,72 @@ +/** + * @module api-v1 + * @description This module defines the API resources using Bicep. + * It includes configurations for creating and managing APIs, products, and policies. + */ + + +// ------------------------------ +// PARAMETERS +// ------------------------------ + +@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) + +@description('The name of the API Management instance. Defaults to "apim-".') +param apimName string = 'apim-${resourceSuffix}' + +@description('The capacity for the Workspace Gateway. Defaults to 1.') +param apimWorkspaceGatewayCapacity int = 1 +@description('The SKU for the Workspace Gateway. Defaults to "WorkspaceGatewayPremium".') +@allowed(['WorkspaceGatewayPremium', 'WorkspaceGatewayStandard', 'Standard']) +param apimWorkspaceGatewaySku string = 'WorkspaceGatewayPremium' +@description('The resource id of the subnet for the Workspace Gateway.') +param apimWorkspaceGatewaySubnetId string +@description('The type of network for the Workspace Gateway. Defaults to "External".') +@allowed(['External', 'Internal', 'None']) +param apimWorkspaceGatewayNetworkType string +@description('The name of the Workspace Gateway. Defaults to "-gateway".') +param apimWorkspaceGatewayName string = '${apimName}-gateway' +@description('The name of the API Management Workspace. Defaults to "-workspace".') +param apimWorkspaceName string + +// ------------------------------ +// VARIABLES +// ------------------------------ +var apimWorkspaceGatewayNetworkConfig = 'config' + +// ------------------------------ +// RESOURCES +// ------------------------------ + +resource workspaceGateway 'Microsoft.ApiManagement/gateways@2024-06-01-preview' = { + name: apimWorkspaceGatewayName + location: resourceGroup().location + sku: { + name: apimWorkspaceGatewaySku + capacity: apimWorkspaceGatewayCapacity + } + properties: { + backend: apimWorkspaceGatewayNetworkType != 'None' ? { + subnet: { + id: apimWorkspaceGatewaySubnetId + } + } : null + configurationApi: {} + frontend: {} + virtualNetworkType: apimWorkspaceGatewayNetworkType + } +} + +resource workspaceGatewayConfigConnection 'Microsoft.ApiManagement/gateways/configConnections@2024-06-01-preview' = { + parent: workspaceGateway + name: apimWorkspaceGatewayNetworkConfig + properties: { + sourceId: '${resourceId('Microsoft.ApiManagement/service', apimName)}/workspaces/${apimWorkspaceName}' + } +} + +// ------------------------------ +// OUTPUTS +// ------------------------------ + diff --git a/shared/bicep/modules/apim/v1/workspace.bicep b/shared/bicep/modules/apim/v1/workspace.bicep new file mode 100644 index 0000000..c88d8c5 --- /dev/null +++ b/shared/bicep/modules/apim/v1/workspace.bicep @@ -0,0 +1,51 @@ +/** + * @module api-v1 + * @description This module defines the API resources using Bicep. + * It includes configurations for creating and managing APIs, products, and policies. + */ + + +// ------------------------------ +// PARAMETERS +// ------------------------------ + +@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) + +@description('The name of the API Management instance. Defaults to "apim-".') +param apimName string = 'apim-${resourceSuffix}' +@description('The name of the API Management Workspace. Defaults to "-workspace".') +param apimWorkspaceName string = '${apimName}-workspace' +@description('The description of the API Management Workspace. Defaults to "API Management Workspace for ".') +param apimWorkspaceDescription string = 'API Management Workspace for ${apimName}' +@description('The display name of the API Management Workspace. Defaults to " Workspace".') +param apimWorkspaceDisplayName string = '${apimName} Workspace' + +// ------------------------------ +// VARIABLES +// ------------------------------ +var apimWorkspaceGatewayNetworkConfig = 'config' + +// ------------------------------ +// RESOURCES +// ------------------------------ + + +resource apimService 'Microsoft.ApiManagement/service@2021-08-01' existing = { + name: apimName +} + +resource apimWorkspace 'Microsoft.ApiManagement/service/workspaces@2024-06-01-preview' = { + parent: apimService + name: apimWorkspaceName + properties: { + description: apimWorkspaceDescription + displayName: apimWorkspaceDisplayName + } +} + + +// ------------------------------ +// OUTPUTS +// ------------------------------ + diff --git a/shared/python/apimtypes.py b/shared/python/apimtypes.py index aa6bfb3..9d6a797 100644 --- a/shared/python/apimtypes.py +++ b/shared/python/apimtypes.py @@ -132,7 +132,7 @@ class INFRASTRUCTURE(StrEnum): SIMPLE_APIM = "simple-apim" # Simple API Management with no dependencies APIM_ACA = "apim-aca" # Azure API Management connected to Azure Container Apps AFD_APIM_PE = "afd-apim-pe" # Azure Front Door Premium connected to Azure API Management (Standard V2) via Private Link - + APIM_WORKSPACES = "apim-workspaces" # Azure API Management with Workspace Gateway # ------------------------------ # CLASSES