diff --git a/labs/access-controlling/access-controlling.ipynb b/labs/access-controlling/access-controlling.ipynb index f83e5f8..cc5ee4e 100644 --- a/labs/access-controlling/access-controlling.ipynb +++ b/labs/access-controlling/access-controlling.ipynb @@ -47,36 +47,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "metadata": {} }, "outputs": [], "source": [ "import os\n", - "import json\n", - "import datetime\n", - "import requests\n", "\n", "deployment_name = os.path.basename(os.path.dirname(globals()['__vsc_ipynb_file__']))\n", "resource_group_name = f\"lab-{deployment_name}\" # change the name to match your naming style\n", "resource_group_location = \"westeurope\"\n", - "apim_resource_name = \"apim\"\n", - "apim_resource_location = \"westeurope\"\n", - "apim_resource_sku = \"Basicv2\"\n", - "openai_resources = [ {\"name\": \"openai1\", \"location\": \"swedencentral\"}, {\"name\": \"openai2\", \"location\": \"francecentral\"} ] # list of OpenAI resources to deploy. Clear this list to use only the mock resources\n", - "openai_resources_sku = \"S0\"\n", + "\n", + "openai_resources = [\n", + " {\"name\": \"openai1\", \"location\": \"swedencentral\"},\n", + " {\"name\": \"openai2\", \"location\": \"francecentral\"}\n", + "]\n", "openai_model_name = \"gpt-35-turbo\"\n", "openai_model_version = \"0613\"\n", "openai_deployment_name = \"gpt-35-turbo\"\n", "openai_api_version = \"2024-02-01\"\n", - "openai_specification_url='https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/stable/' + openai_api_version + '/inference.json'\n", - "openai_backend_pool = \"openai-backend-pool\"\n", - "mock_backend_pool = \"mock-backend-pool\"\n", - "mock_webapps = [ {\"name\": \"openaimock1\", \"endpoint\": \"https://openaimock1.azurewebsites.net\"}, {\"name\": \"openaimock2\", \"endpoint\": \"https://openaimock2.azurewebsites.net\"} ]\n", - "\n", - "log_analytics_name = \"workspace\"\n", - "app_insights_name = 'insights'\n", "\n", "app_registration_name = \"ai-gateway-openai-app\"\n" ] @@ -96,11 +86,20 @@ "metadata": {}, "outputs": [], "source": [ + "# type: ignore\n", + "\n", "cmd_stdout = ! az account show --query homeTenantId --output tsv\n", "tenant_id = cmd_stdout.n\n", + "print(f\"👉🏻 Tenant Id: {tenant_id}\")\n", + "\n", + "client_id = ! az ad app list --filter \"displayName eq '{app_registration_name}'\" --query [0].appId --output tsv\n", + "\n", + "if not client_id:\n", + " client_id = ! az ad app create --display-name {app_registration_name} --query appId --is-fallback-public-client true --output tsv\n", "\n", - "cmd_stdout = ! az ad app create --display-name {app_registration_name} --query appId --is-fallback-public-client true --output tsv\n", - "client_id = cmd_stdout.n\n" + "client_id = client_id[0] if client_id else None\n", + "\n", + "print(f\"👉🏻 Client Id: {client_id}\")\n" ] }, { @@ -118,11 +117,17 @@ "metadata": {}, "outputs": [], "source": [ + "# %load ../../shared/snippets/create-az-resource-group.py\n", + "# type: ignore\n", + "\n", + "import datetime\n", + "\n", "resource_group_stdout = ! az group create --name {resource_group_name} --location {resource_group_location}\n", + "\n", "if resource_group_stdout.n.startswith(\"ERROR\"):\n", " print(resource_group_stdout)\n", "else:\n", - " print(\"✅ Azure Resource Group \", resource_group_name, \" created ⌚ \", datetime.datetime.now().time())" + " print(f\"✅ Azure Resource Group {resource_group_name} created ⌚ {datetime.datetime.now().time()}\")\n" ] }, { @@ -141,43 +146,44 @@ "metadata": {}, "outputs": [], "source": [ - "if len(openai_resources) > 0:\n", - " backend_id = openai_backend_pool if len(openai_resources) > 1 else openai_resources[0].get(\"name\")\n", - "elif len(mock_webapps) > 0:\n", - " backend_id = mock_backend_pool if len(mock_backend_pool) > 1 else mock_webapps[0].get(\"name\")\n", + "# %load ../../shared/snippets/create-az-deployment.py\n", + "# type: ignore\n", + "\n", + "import json\n", + "\n", + "backend_id = \"openai-backend-pool\" if len(openai_resources) > 1 else openai_resources[0].get(\"name\")\n", "\n", "with open(\"policy.xml\", 'r') as policy_xml_file:\n", - " policy_template_xml = policy_xml_file.read()\n", - " policy_xml = policy_template_xml.replace(\"{backend-id}\", backend_id).replace(\"{aad-client-application-id}\", client_id).replace(\"{aad-tenant-id}\", tenant_id)\n", + " policy_xml = policy_xml_file.read()\n", + "\n", + " if \"{backend-id}\" in policy_xml:\n", + " policy_xml = policy_xml.replace(\"{backend-id}\", backend_id)\n", + "\n", + " if \"{aad-client-application-id}\" in policy_xml:\n", + " policy_xml = policy_xml.replace(\"{aad-client-application-id}\", client_id)\n", + "\n", + " if \"{aad-tenant-id}\" in policy_xml:\n", + " policy_xml = policy_xml.replace(\"{aad-tenant-id}\", tenant_id)\n", + "\n", " policy_xml_file.close()\n", - "open(\"policy.xml\", 'w').write(policy_xml)\n", + "open(\"policy-updated.xml\", 'w').write(policy_xml)\n", "\n", "bicep_parameters = {\n", - " \"$schema\": \"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#\",\n", - " \"contentVersion\": \"1.0.0.0\",\n", - " \"parameters\": {\n", - " \"mockWebApps\": { \"value\": mock_webapps },\n", - " \"mockBackendPoolName\": { \"value\": mock_backend_pool },\n", - " \"openAIBackendPoolName\": { \"value\": openai_backend_pool },\n", - " \"openAIConfig\": { \"value\": openai_resources },\n", - " \"openAIDeploymentName\": { \"value\": openai_deployment_name },\n", - " \"openAISku\": { \"value\": openai_resources_sku },\n", - " \"openAIModelName\": { \"value\": openai_model_name },\n", - " \"openAIModelVersion\": { \"value\": openai_model_version },\n", - " \"openAIAPISpecURL\": { \"value\": openai_specification_url },\n", - " \"apimResourceName\": { \"value\": apim_resource_name},\n", - " \"apimResourceLocation\": { \"value\": apim_resource_location},\n", - " \"apimSku\": { \"value\": apim_resource_sku},\n", - " \"logAnalyticsName\": { \"value\": log_analytics_name },\n", - " \"applicationInsightsName\": { \"value\": app_insights_name }\n", - " }\n", + " \"$schema\": \"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#\",\n", + " \"contentVersion\": \"1.0.0.0\",\n", + " \"parameters\": {\n", + " \"openAIConfig\": { \"value\": openai_resources },\n", + " \"openAIDeploymentName\": { \"value\": openai_deployment_name },\n", + " \"openAIModelName\": { \"value\": openai_model_name },\n", + " \"openAIModelVersion\": { \"value\": openai_model_version },\n", + " \"openAIAPIVersion\": { \"value\": openai_api_version }\n", + " }\n", "}\n", + "\n", "with open('params.json', 'w') as bicep_parameters_file:\n", " bicep_parameters_file.write(json.dumps(bicep_parameters))\n", "\n", - "! az deployment group create --name {deployment_name} --resource-group {resource_group_name} --template-file \"main.bicep\" --parameters \"params.json\"\n", - "\n", - "open(\"policy.xml\", 'w').write(policy_template_xml)\n" + "! az deployment group create --name {deployment_name} --resource-group {resource_group_name} --template-file \"main.bicep\" --parameters \"params.json\"\n" ] }, { @@ -196,23 +202,55 @@ "metadata": {}, "outputs": [], "source": [ - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.apimServiceId.value -o tsv\n", - "apim_service_id = deployment_stdout.n\n", - "print(\"👉🏻 APIM Service Id: \", apim_service_id)\n", - "\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.apimSubscriptionKey.value -o tsv\n", - "apim_subscription_key = deployment_stdout.n\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.apimResourceGatewayURL.value -o tsv\n", - "apim_resource_gateway_url = deployment_stdout.n\n", - "print(\"👉🏻 API Gateway URL: \", apim_resource_gateway_url)\n", - "\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.logAnalyticsWorkspaceId.value -o tsv\n", - "workspace_id = deployment_stdout.n\n", - "print(\"👉🏻 Workspace ID: \", workspace_id)\n", - "\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.applicationInsightsAppId.value -o tsv\n", - "app_id = deployment_stdout.n\n", - "print(\"👉🏻 App ID: \", app_id)" + "# %load ../../shared/snippets/deployment-outputs.py\n", + "# type: ignore\n", + "\n", + "# Obtain all of the outputs from the deployment\n", + "stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs -o json\n", + "outputs = json.loads(stdout.n)\n", + "\n", + "# Extract the individual properties\n", + "apim_service_id = outputs.get('apimServiceId', {}).get('value', '')\n", + "apim_subscription_key = outputs.get('apimSubscriptionKey', {}).get('value', '')\n", + "apim_subscription1_key = outputs.get('apimSubscription1Key', {}).get('value', '')\n", + "apim_subscription2_key = outputs.get('apimSubscription2Key', {}).get('value', '')\n", + "apim_subscription3_key = outputs.get('apimSubscription3Key', {}).get('value', '')\n", + "apim_resource_gateway_url = outputs.get('apimResourceGatewayURL', {}).get('value', '')\n", + "workspace_id = outputs.get('logAnalyticsWorkspaceId', {}).get('value', '')\n", + "app_id = outputs.get('applicationInsightsAppId', {}).get('value', '')\n", + "function_app_resource_name = outputs.get('functionAppResourceName', {}).get('value', '')\n", + "cosmosdb_connection_string = outputs.get('cosmosDBConnectionString', {}).get('value', '')\n", + "\n", + "# Print the extracted properties if they are not empty\n", + "if apim_service_id:\n", + " print(f\"👉🏻 APIM Service Id: {apim_service_id}\")\n", + "\n", + "if apim_subscription_key:\n", + " print(f\"👉🏻 APIM Subscription Key (masked): ****{apim_subscription_key[-4:]}\")\n", + "\n", + "if apim_subscription1_key:\n", + " print(f\"👉🏻 APIM Subscription Key 1 (masked): ****{apim_subscription1_key[-4:]}\")\n", + "\n", + "if apim_subscription2_key:\n", + " print(f\"👉🏻 APIM Subscription Key 2 (masked): ****{apim_subscription2_key[-4:]}\")\n", + "\n", + "if apim_subscription3_key:\n", + " print(f\"👉🏻 APIM Subscription Key 3 (masked): ****{apim_subscription3_key[-4:]}\")\n", + "\n", + "if apim_resource_gateway_url:\n", + " print(f\"👉🏻 APIM API Gateway URL: {apim_resource_gateway_url}\")\n", + "\n", + "if workspace_id:\n", + " print(f\"👉🏻 Workspace ID: {workspace_id}\")\n", + "\n", + "if app_id:\n", + " print(f\"👉🏻 App ID: {app_id}\")\n", + "\n", + "if function_app_resource_name:\n", + " print(f\"👉🏻 Function Name: {function_app_resource_name}\")\n", + "\n", + "if cosmosdb_connection_string:\n", + " print(f\"👉🏻 Cosmos DB Connection String: {cosmosdb_connection_string}\")\n" ] }, { @@ -247,21 +285,17 @@ "outputs": [], "source": [ "import json\n", - "import logging\n", - "\n", - "import requests\n", "import msal\n", "\n", - "app = msal.PublicClientApplication(\n", - " client_id, authority=\"https://login.microsoftonline.com/\" + tenant_id)\n", + "app = msal.PublicClientApplication(client_id, authority = \"https://login.microsoftonline.com/\" + tenant_id)\n", + "\n", + "flow = app.initiate_device_flow(scopes = [\"User.Read\"])\n", "\n", - "flow = app.initiate_device_flow(scopes=[\"User.Read\"])\n", "if \"user_code\" not in flow:\n", " raise ValueError(\n", - " \"Fail to create device flow. Err: %s\" % json.dumps(flow, indent=4))\n", + " \"Fail to create device flow. Err: %s\" % json.dumps(flow, indent = 4))\n", "\n", - "print(flow[\"message\"])\n", - "\n" + "print(flow[\"message\"])" ] }, { @@ -278,6 +312,8 @@ "metadata": {}, "outputs": [], "source": [ + "import requests\n", + "\n", "result = app.acquire_token_by_device_flow(flow)\n", "\n", "if \"access_token\" in result:\n", @@ -286,7 +322,7 @@ " graph_data = requests.get( # Use token to call downstream service\n", " \"https://graph.microsoft.com/v1.0/me\",\n", " headers={'Authorization': 'Bearer ' + access_token},).json()\n", - " print(\"Graph API call result: %s\" % json.dumps(graph_data, indent=2))\n", + " print(\"Graph API call result: %s\" % json.dumps(graph_data, indent = 2))\n", " # print(access_token) # Use a tool like https://jwt.io/ to decode the access token and see its contents\n", "else:\n", " print(result.get(\"error\"))\n", @@ -311,15 +347,17 @@ "source": [ "url = apim_resource_gateway_url + \"/openai/deployments/\" + openai_deployment_name + \"/chat/completions?api-version=\" + openai_api_version\n", "\n", - "messages={\"messages\":[\n", - " {\"role\": \"system\", \"content\": \"You are a sarcastic, unhelpful assistant.\"},\n", + "messages = { \"messages\": [\n", + " {\"role\": \"system\", \"content\": \"You are a sarcastic unhelpful assistant.\"},\n", " {\"role\": \"user\", \"content\": \"Can you tell me the time, please?\"}\n", "]}\n", - "response = requests.post(url, headers = {'api-key':apim_subscription_key, 'Authorization': 'Bearer ' + access_token}, json = messages)\n", - "print(\"status code: \", response.status_code)\n", + "\n", + "response = requests.post(url, headers = {'api-key': apim_subscription_key, 'Authorization': 'Bearer ' + access_token}, json = messages)\n", + "print(f\"status code: {response.status_code}\")\n", + "\n", "if (response.status_code == 200):\n", " data = json.loads(response.text)\n", - " print(\"response: \", data.get(\"choices\")[0].get(\"message\").get(\"content\"))\n", + " print(f\"response: {data.get(\"choices\")[0].get(\"message\").get(\"content\")}\")\n", "else:\n", " print(response.text)\n" ] @@ -340,18 +378,21 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "from openai import AzureOpenAI\n", - "messages=[\n", - " {\"role\": \"system\", \"content\": \"You are a sarcastic, unhelpful assistant.\"},\n", + "\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a sarcastic unhelpful assistant.\"},\n", " {\"role\": \"user\", \"content\": \"Can you tell me the time, please?\"}\n", "]\n", + "\n", "client = AzureOpenAI(\n", - " azure_endpoint=apim_resource_gateway_url,\n", - " api_key=apim_subscription_key,\n", - " api_version=openai_api_version \n", + " azure_endpoint = apim_resource_gateway_url,\n", + " api_key = apim_subscription_key,\n", + " api_version = openai_api_version\n", ")\n", - "response = client.chat.completions.create(model=openai_model_name, messages=messages, extra_headers={\"Authorization\": \"Bearer \" + access_token})\n", + "\n", + "response = client.chat.completions.create(model = openai_model_name, messages = messages, extra_headers = {\"Authorization\": \"Bearer \" + access_token}) # type: ignore\n", + "\n", "print(response.choices[0].message.content)\n" ] }, @@ -369,7 +410,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -383,7 +424,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.12.0" } }, "nbformat": 4, diff --git a/labs/access-controlling/main.bicep b/labs/access-controlling/main.bicep index b8e0431..c62d455 100644 --- a/labs/access-controlling/main.bicep +++ b/labs/access-controlling/main.bicep @@ -1,432 +1,91 @@ -@description('List of Mock webapp names used to simulate OpenAI behavior.') -param mockWebApps array = [] +// ------------------ +// PARAMETERS +// ------------------ -@description('The name of the OpenAI mock backend pool') -param mockBackendPoolName string = 'openai-backend-pool' +// Typically, parameters would be decorated with appropriate metadata and attributes, but as they are very repetetive in these labs we omit them for brevity. -@description('The description of the OpenAI mock backend pool') -param mockBackendPoolDescription string = 'Load balancer for multiple OpenAI Mocking endpoints' - -@description('List of OpenAI resources to create. Add pairs of name and location.') param openAIConfig array = [] - -@description('Deployment Name') -param openAIDeploymentName string - -@description('Azure OpenAI Sku') -@allowed([ - 'S0' -]) -param openAISku string = 'S0' - -@description('Model Name') param openAIModelName string - -@description('Model Version') param openAIModelVersion string +param openAIDeploymentName string +param openAIAPIVersion string -@description('Model Capacity') -param openAIModelCapacity int = 20 - -@description('The name of the API Management resource') -param apimResourceName string - -@description('Location for the APIM resource') -param apimResourceLocation string = resourceGroup().location - -@description('The pricing tier of this API Management service') -@allowed([ - 'Consumption' - 'Developer' - 'Basic' - 'Basicv2' - 'Standard' - 'Standardv2' - 'Premium' -]) -param apimSku string = 'Consumption' - -@description('The instance size of this API Management service.') -@allowed([ - 0 - 1 - 2 -]) -param apimSkuCount int = 1 - -@description('The email address of the owner of the service') -param apimPublisherEmail string = 'noreply@microsoft.com' - -@description('The name of the owner of the service') -param apimPublisherName string = 'Microsoft' - -@description('The name of the APIM API for OpenAI API') -param openAIAPIName string = 'openai' - -@description('The relative path of the APIM API for OpenAI API') -param openAIAPIPath string = 'openai' - -@description('The display name of the APIM API for OpenAI API') -param openAIAPIDisplayName string = 'OpenAI' - -@description('The description of the APIM API for OpenAI API') -param openAIAPIDescription string = 'Azure OpenAI API inferencing API' - -@description('Full URL for the OpenAI API spec') -param openAIAPISpecURL string = 'https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/stable/2024-02-01/inference.json' - -@description('The name of the APIM Subscription for OpenAI API') -param openAISubscriptionName string = 'openai-subscription' - -@description('The description of the APIM Subscription for OpenAI API') -param openAISubscriptionDescription string = 'OpenAI Subscription' - -@description('The name of the OpenAI backend pool') -param openAIBackendPoolName string = 'openai-backend-pool' - -@description('The description of the OpenAI backend pool') -param openAIBackendPoolDescription string = 'Load balancer for multiple OpenAI endpoints' - -// buult-in logging: additions BEGIN - -@description('Name of the Log Analytics resource') -param logAnalyticsName string = 'workspace' - -@description('Location of the Log Analytics resource') -param logAnalyticsLocation string = resourceGroup().location - -@description('Name of the Application Insights resource') -param applicationInsightsName string = 'insights' - -@description('Location of the Application Insights resource') -param applicationInsightsLocation string = resourceGroup().location - -@description('Name of the APIM Logger') -param apimLoggerName string = 'apim-logger' - -@description('Description of the APIM Logger') -param apimLoggerDescription string = 'APIM Logger for OpenAI API' - -@description('Number of bytes to log for API diagnostics') -param apiDiagnosticsLogBytes int = 8192 - -@description(' Name for the Workbook') -param workbookName string = 'OpenAIUsageAnalysis' - -@description('Location for the Workbook') -param workbookLocation string = resourceGroup().location - -@description('Display Name for the Workbook') -param workbookDisplayName string = 'OpenAI Usage Analysis' - -// buult-in logging: additions END - - - -var resourceSuffix = uniqueString(subscription().id, resourceGroup().id) - -resource cognitiveServices 'Microsoft.CognitiveServices/accounts@2021-10-01' = [for config in openAIConfig: if(length(openAIConfig) > 0) { - name: '${config.name}-${resourceSuffix}' - location: config.location - sku: { - name: openAISku - } - kind: 'OpenAI' - properties: { - apiProperties: { - statisticsEnabled: false - } - customSubDomainName: toLower('${config.name}-${resourceSuffix}') - } -}] - -resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for (config, i) in openAIConfig: if(length(openAIConfig) > 0) { - name: openAIDeploymentName - parent: cognitiveServices[i] - properties: { - model: { - format: 'OpenAI' - name: openAIModelName - version: openAIModelVersion - } - } - sku: { - name: 'Standard' - capacity: openAIModelCapacity - } -}] - -resource apimService 'Microsoft.ApiManagement/service@2023-05-01-preview' = { - name: '${apimResourceName}-${resourceSuffix}' - location: apimResourceLocation - sku: { - name: apimSku - capacity: (apimSku == 'Consumption') ? 0 : ((apimSku == 'Developer') ? 1 : apimSkuCount) - } - properties: { - publisherEmail: apimPublisherEmail - publisherName: apimPublisherName - } - identity: { - type: 'SystemAssigned' - } -} +// ------------------ +// VARIABLES +// ------------------ -var roleDefinitionID = resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') -resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (config, i) in openAIConfig: if(length(openAIConfig) > 0) { - scope: cognitiveServices[i] - name: guid(subscription().id, resourceGroup().id, config.name, roleDefinitionID) - properties: { - roleDefinitionId: roleDefinitionID - principalId: apimService.identity.principalId - principalType: 'ServicePrincipal' - } -}] +var updatedPolicyXml = loadTextContent('policy-updated.xml') +var azureRoles = loadJsonContent('../../modules/azure-roles.json') +var cognitiveServicesOpenAIUserRoleDefinitionID = resourceId('Microsoft.Authorization/roleDefinitions', azureRoles.CognitiveServicesOpenAIUser) -resource api 'Microsoft.ApiManagement/service/apis@2023-05-01-preview' = { - name: openAIAPIName - parent: apimService - properties: { - apiType: 'http' - description: openAIAPIDescription - displayName: openAIAPIDisplayName - format: 'openapi-link' - path: openAIAPIPath - protocols: [ - 'https' - ] - subscriptionKeyParameterNames: { - header: 'api-key' - query: 'api-key' - } - subscriptionRequired: true - type: 'http' - value: openAIAPISpecURL - } - } +// ------------------ +// RESOURCES +// ------------------ -resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2021-12-01-preview' = { - name: 'policy' - parent: api - properties: { - format: 'rawxml' - value: loadTextContent('policy.xml') - } +// 1. Log Analytics Workspace +module lawModule '../../modules/operational-insights/v1/workspaces.bicep' = { + name: 'lawModule' } -resource backendOpenAI 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = [for (config, i) in openAIConfig: if(length(openAIConfig) > 0) { - name: config.name - parent: apimService - properties: { - description: 'backend description' - url: '${cognitiveServices[i].properties.endpoint}/openai' - protocol: 'http' - circuitBreaker: { - rules: [ - { - failureCondition: { - count: 3 - errorReasons: [ - 'Server errors' - ] - interval: 'PT5M' - statusCodeRanges: [ - { - min: 429 - max: 429 - } - ] - } - name: 'openAIBreakerRule' - tripDuration: 'PT1M' - } - ] - } - } -}] +var lawId = lawModule.outputs.id -resource backendMock 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = [for (mock, i) in mockWebApps: if(length(openAIConfig) == 0 && length(mockWebApps) > 0) { - name: mock.name - parent: apimService - properties: { - description: 'backend description' - url: '${mock.endpoint}/openai' - protocol: 'http' - circuitBreaker: { - rules: [ - { - failureCondition: { - count: 3 - errorReasons: [ - 'Server errors' - ] - interval: 'PT5M' - statusCodeRanges: [ - { - min: 429 - max: 429 - } - ] - } - name: 'mockBreakerRule' - tripDuration: 'PT1M' - } - ] - } - } -}] - -resource backendPoolOpenAI 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = if(length(openAIConfig) > 1) { - name: openAIBackendPoolName - parent: apimService - properties: { - description: openAIBackendPoolDescription - type: 'Pool' -// protocol: 'http' // the protocol is not needed in the Pool type -// url: '${cognitiveServices[0].properties.endpoint}/openai' // the url is not needed in the Pool type - pool: { - services: [for (config, i) in openAIConfig: { - id: '/backends/${backendOpenAI[i].name}' - } - ] - } +// 2. Application Insights +module appInsightsModule '../../modules/monitor/v1/appinsights.bicep' = { + name: 'appInsightsModule' + params: { + workbookJson: loadTextContent('openai-usage-analysis-workbook.json') + lawId: lawId } } -resource backendPoolMock 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = if(length(openAIConfig) == 0 && length(mockWebApps) > 1) { - name: mockBackendPoolName - parent: apimService - properties: { - description: mockBackendPoolDescription - type: 'Pool' -// protocol: 'http' // the protocol is not needed in the Pool type -// url: '${mockWebApps[0].endpoint}/openai' // the url is not needed in the Pool type - pool: { - services: [for (webApp, i) in mockWebApps: { - id: '/backends/${backendMock[i].name}' - } - ] - } - } -} +var appInsightsId = appInsightsModule.outputs.id +var appInsightsInstrumentationKey = appInsightsModule.outputs.instrumentationKey -resource apimSubscription 'Microsoft.ApiManagement/service/subscriptions@2023-05-01-preview' = { - name: openAISubscriptionName - parent: apimService - properties: { - allowTracing: true - displayName: openAISubscriptionDescription - scope: '/apis/${api.id}' - state: 'active' +// 3. Cognitive Services +module openAIModule '../../modules/cognitive-services/v1/openai.bicep' = { + name: 'openAIModule' + params: { + openAIConfig: openAIConfig + openAIDeploymentName: openAIDeploymentName + openAIModelName: openAIModelName + openAIModelVersion: openAIModelVersion + lawId: lawId } } -// buult-in logging: additions BEGIN +var extendedOpenAIConfig = openAIModule.outputs.extendedOpenAIConfig -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: '${logAnalyticsName}-${resourceSuffix}' - location: logAnalyticsLocation - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -/* -resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [for (config, i) in openAIConfig: if(length(openAIConfig) > 0) { - name: '${cognitiveServices[i].name}-diagnostics' - scope: cognitiveServices[i] - properties: { - workspaceId: logAnalytics.id - logs: [] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -}] -*/ - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: '${applicationInsightsName}-${resourceSuffix}' - location: applicationInsightsLocation - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: logAnalytics.id +// 4. API Management +module apimModule '../../modules/apim/v1/apim.bicep' = { + name: 'apimModule' + params: { + policyXml: updatedPolicyXml + openAIConfig: extendedOpenAIConfig + appInsightsInstrumentationKey: appInsightsInstrumentationKey + appInsightsId: appInsightsId + openAIAPIVersion: openAIAPIVersion } } -resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = { - name: apimLoggerName - parent: apimService - properties: { - credentials: { - instrumentationKey: applicationInsights.properties.InstrumentationKey - } - description: apimLoggerDescription - isBuffered: false - loggerType: 'applicationInsights' - resourceId: applicationInsights.id - } -} +var apimPrincipalId = apimModule.outputs.principalId -var logSettings = { - headers: [ 'Content-type', 'User-agent', 'x-ms-region', 'x-ratelimit-remaining-tokens' , 'x-ratelimit-remaining-requests' ] - body: { bytes: apiDiagnosticsLogBytes } -} -resource apiDiagnostics 'Microsoft.ApiManagement/service/apis/diagnostics@2022-08-01' = if (!empty(apimLogger.name)) { - name: 'applicationinsights' - parent: api - properties: { - alwaysLog: 'allErrors' - httpCorrelationProtocol: 'W3C' - logClientIp: true - loggerId: apimLogger.id - metrics: true - verbosity: 'verbose' - sampling: { - samplingType: 'fixed' - percentage: 100 - } - frontend: { - request: logSettings - response: logSettings - } - backend: { - request: logSettings - response: logSettings +// 5. RBAC Assignment +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if(length(openAIConfig) > 0) { + scope: resourceGroup() + name: guid(subscription().id, resourceGroup().id, openAIConfig[0].name, cognitiveServicesOpenAIUserRoleDefinitionID) + properties: { + roleDefinitionId: cognitiveServicesOpenAIUserRoleDefinitionID + principalId: apimPrincipalId + principalType: 'ServicePrincipal' } - } -} - -resource workbook 'Microsoft.Insights/workbooks@2022-04-01' = { - name: guid(resourceGroup().id, workbookName) - location: workbookLocation - kind: 'shared' - properties: { - displayName: workbookDisplayName - serializedData: loadTextContent('openai-usage-analysis-workbook.json') - sourceId: applicationInsights.id - category: 'OpenAI' - } } -output applicationInsightsAppId string = applicationInsights.properties.AppId - -output logAnalyticsWorkspaceId string = logAnalytics.properties.customerId - -// buult-in logging: additions END - -output apimServiceId string = apimService.id -output apimResourceGatewayURL string = apimService.properties.gatewayUrl +// ------------------ +// OUTPUTS +// ------------------ -#disable-next-line outputs-should-not-contain-secrets -output apimSubscriptionKey string = apimSubscription.listSecrets().primaryKey +output applicationInsightsAppId string = appInsightsModule.outputs.appId +output logAnalyticsWorkspaceId string = lawModule.outputs.customerId +output apimServiceId string = apimModule.outputs.id +output apimResourceGatewayURL string = apimModule.outputs.gatewayUrl +output apimSubscriptionKey string = apimModule.outputs.subscriptionPrimaryKey diff --git a/labs/access-controlling/policy-updated.xml b/labs/access-controlling/policy-updated.xml new file mode 100644 index 0000000..ae2b89f --- /dev/null +++ b/labs/access-controlling/policy-updated.xml @@ -0,0 +1,3 @@ +Placeholder file to suppress bicep error. The file is replaced when the lab is executed. + +Do not check in updated versions of this file.