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.
+
+
+
+## 🎯 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 @@
+
\ 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