├── .azdo └── pipelines │ └── azure-dev.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitattributes ├── .github └── workflows │ └── azure-dev.yml ├── .gitignore ├── .vscode └── apim_policy.code-snippets ├── LICENSE ├── README.md ├── azure.yaml ├── del-soft-delete-apim.ps1 ├── docs ├── images │ └── arch.png └── raw │ └── arch.drawio ├── infra ├── abbreviations.json ├── main.bicep ├── main.parameters.json └── modules │ ├── ai │ └── cognitiveservices.bicep │ ├── apim │ ├── .DS_Store │ ├── apim.bicep │ ├── openapi │ │ └── openai-openapiv3.json │ └── policies │ │ ├── api_operation_policy.xml │ │ └── api_policy.xml │ ├── monitor │ ├── applicationinsights-dashboard.bicep │ ├── applicationinsights.bicep │ ├── loganalytics.bicep │ └── monitoring.bicep │ ├── networking │ ├── dns.bicep │ ├── private-endpoint.bicep │ └── vnet.bicep │ └── security │ ├── assignment.bicep │ ├── key-vault.bicep │ ├── keyvault-secret.bicep │ └── managed-identity.bicep └── tests.http /.azdo/pipelines/azure-dev.yml: -------------------------------------------------------------------------------- 1 | # Run when commits are pushed to mainline branch (main or master) 2 | # Set this to the mainline branch you are using 3 | trigger: 4 | - main 5 | - master 6 | 7 | # Azure Pipelines workflow to deploy to Azure using azd 8 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config --provider azdo` 9 | 10 | pool: 11 | vmImage: ubuntu-latest 12 | 13 | # Use azd provided container image that has azd, infra, multi-language build tools pre-installed. 14 | container: mcr.microsoft.com/azure-dev-cli-apps:latest 15 | 16 | steps: 17 | - pwsh: | 18 | azd config set auth.useAzCliAuth "true" 19 | displayName: Configure AZD to Use AZ CLI Authentication. 20 | 21 | - task: AzureCLI@2 22 | displayName: Provision Infrastructure 23 | inputs: 24 | azureSubscription: azconnection 25 | scriptType: bash 26 | scriptLocation: inlineScript 27 | inlineScript: | 28 | azd provision --no-prompt 29 | env: 30 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 31 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 32 | AZURE_LOCATION: $(AZURE_LOCATION) 33 | 34 | - task: AzureCLI@2 35 | displayName: Deploy Application 36 | inputs: 37 | azureSubscription: azconnection 38 | scriptType: bash 39 | scriptLocation: inlineScript 40 | inlineScript: | 41 | azd deploy --no-prompt 42 | env: 43 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 44 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 45 | AZURE_LOCATION: $(AZURE_LOCATION) 46 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE=bullseye 2 | FROM --platform=amd64 mcr.microsoft.com/devcontainers/${IMAGE} 3 | RUN export DEBIAN_FRONTEND=noninteractive \ 4 | && apt-get update && apt-get install -y xdg-utils \ 5 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 6 | RUN curl -fsSL https://aka.ms/install-azd.sh | bash 7 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure Developer CLI", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | // List of images: https://github.com/devcontainers/images/tree/main/src 7 | "IMAGE": "python:3.10" 8 | } 9 | }, 10 | "features": { 11 | // See https://containers.dev/features for list of features 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "extensions": [ 16 | "GitHub.vscode-github-actions", 17 | "ms-azuretools.azure-dev", 18 | "ms-azuretools.vscode-azurefunctions", 19 | "ms-azuretools.vscode-bicep", 20 | "ms-azuretools.vscode-docker", 21 | "humao.rest-client", 22 | "ms-vscode.vscode-node-azure-pack", 23 | "ms-azuretools.vscode-apimanagement", 24 | "GitHub.copilot", 25 | "DotJoshJohnson.xml" 26 | // Include other VSCode language extensions if needed 27 | // Right click on an extension inside VSCode to add directly to devcontainer.json, or copy the extension ID 28 | ] 29 | } 30 | }, 31 | "forwardPorts": [ 32 | // Forward ports if needed for local development 33 | ], 34 | "postCreateCommand": "", 35 | "remoteUser": "vscode", 36 | "hostRequirements": { 37 | "memory": "8gb" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | # Run when commits are pushed to mainline branch (main or master) 5 | # Set this to the mainline branch you are using 6 | branches: 7 | - main 8 | - master 9 | 10 | # GitHub Actions workflow to deploy to Azure using azd 11 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config` 12 | 13 | # Set up permissions for deploying with secretless Azure federated credentials 14 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 15 | permissions: 16 | id-token: write 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: mcr.microsoft.com/azure-dev-cli-apps:latest 24 | env: 25 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 26 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 27 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 28 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | 33 | - name: Log in with Azure (Federated Credentials) 34 | if: ${{ env.AZURE_CLIENT_ID != '' }} 35 | run: | 36 | azd auth login ` 37 | --client-id "$Env:AZURE_CLIENT_ID" ` 38 | --federated-credential-provider "github" ` 39 | --tenant-id "$Env:AZURE_TENANT_ID" 40 | shell: pwsh 41 | 42 | - name: Log in with Azure (Client Credentials) 43 | if: ${{ env.AZURE_CREDENTIALS != '' }} 44 | run: | 45 | $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; 46 | Write-Host "::add-mask::$($info.clientSecret)" 47 | 48 | azd auth login ` 49 | --client-id "$($info.clientId)" ` 50 | --client-secret "$($info.clientSecret)" ` 51 | --tenant-id "$($info.tenantId)" 52 | shell: pwsh 53 | env: 54 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 55 | 56 | - name: Provision Infrastructure 57 | run: azd provision --no-prompt 58 | env: 59 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 60 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 61 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 62 | 63 | - name: Deploy Application 64 | run: azd deploy --no-prompt 65 | env: 66 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 67 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 68 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .azure 2 | local.tests.http 3 | local.deploy.ps1 -------------------------------------------------------------------------------- /.vscode/apim_policy.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your ais-apim-snippets workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "policy-document": { 19 | "prefix": "policy-document", 20 | "body": [ 21 | "", 22 | "\t", 23 | "\t\t", 24 | "\t\t$0", 25 | "\t", 26 | "\t", 27 | "\t\t", 28 | "\t", 29 | "\t", 30 | "\t\t", 31 | "\t", 32 | "\t", 33 | "\t\t", 34 | "\t", 35 | "" 36 | ], 37 | "description": "Policy document boilerplate for scopes below Global" 38 | }, 39 | "policy-document-global": { 40 | "prefix": "policy-document-global", 41 | "body": [ 42 | "", 43 | "\t", 44 | "\t\t$0", 45 | "\t", 46 | "\t", 47 | "\t\t", 48 | "\t", 49 | "\t", 50 | "\t", 51 | "\t", 52 | "\t", 53 | "" 54 | ], 55 | "description": "Policy document boilerplate for Global scope" 56 | }, 57 | "authentication-basic": { 58 | "prefix": "authentication-basic", 59 | "body": [ 60 | "" 61 | ], 62 | "description": "Authenticate with the backend service using Basic authentication. Use in the inbound section at API scope." 63 | }, 64 | "authentication-certificate": { 65 | "prefix": "authentication-certificate", 66 | "description": "Authenticate with the backend service using a client certificate. Use in the inbound section at API scope.", 67 | "body": [ 68 | "" 69 | ] 70 | }, 71 | "base": { 72 | "prefix": "base", 73 | "body": [ 74 | "" 75 | ] 76 | }, 77 | "cache-lookup": { 78 | "prefix": "cache-lookup", 79 | "description": "Perform cache lookup and return a cached response when available. Appropriately respond to cache validation requests from callers. Use anywhere in the inbound section at Product, API, or Operation scopes.", 80 | "body": [ 81 | "", 82 | "\t${7:header name}", 83 | "\t${8:query parameter}", 84 | "" 85 | ] 86 | }, 87 | "cache-lookup-value": { 88 | "prefix": "cache-lookup-value", 89 | "description": "Perform cache lookup and return value under the key, if available, or default. If value is not present and no default is specified, variable will not be set. Use at any scope in any section except .", 90 | "body": [ 91 | "" 92 | ] 93 | }, 94 | "cache-remove-value": { 95 | "prefix": "cache-remove-value", 96 | "description": "Remove value from cache under the key. Use at any scope in any section except .", 97 | "body": [ 98 | "" 99 | ] 100 | }, 101 | "cache-store": { 102 | "prefix": "cache-store", 103 | "description": "Cache responses according to the specified cache configuration. Use anywhere in the outbound section at Product, API, or Operation scopes.", 104 | "body": [ 105 | "" 106 | ] 107 | }, 108 | "cache-store-value": { 109 | "prefix": "cache-store-value", 110 | "description": "Store value in cache under a key for duration. Use at any scope in any section except .", 111 | "body": [ 112 | "" 113 | ] 114 | }, 115 | "check-header": { 116 | "prefix": "check-header", 117 | "description": "Check header and return specified HTTP status code if it doesn't exist or match expected value. Works for both response and request headers – policy can be applied in inbound or outbound sections at any scope.", 118 | "body": [ 119 | "", 120 | "\t$5", 121 | "" 122 | ] 123 | }, 124 | "choose": { 125 | "prefix": "choose", 126 | "description": "Conditionally apply policy statements based on the results of the evaluation of Boolean expressions. Use at any scope in the inbound and outbound sections.", 127 | "body": [ 128 | "", 129 | "\t", 130 | "\t\t$0", 131 | "\t", 132 | "\t", 133 | "\t", 134 | "" 135 | ] 136 | }, 137 | "cors": { 138 | "prefix": "cors", 139 | "description": "CORS stands for cross-origin resource sharing. Add CORS support to an operation or an API to allow cross-domain calls from browser-based clients. Use in the inbound section only.", 140 | "body": [ 141 | "", 142 | "\t", 143 | "\t\t${2:*}", 144 | "\t", 145 | "\t", 146 | "\t\t${3:*}", 147 | "\t", 148 | "\t", 149 | "\t\t
${4:*}
", 150 | "\t
", 151 | "\t", 152 | "\t\t
${5:*}
", 153 | "\t
", 154 | "
" 155 | ] 156 | }, 157 | "cross-domain": { 158 | "prefix": "cross-domain", 159 | "description": "Make the API accessible from Adobe Flash and Microsoft Silverlight browser-based clients. Use in the inbound section at Global scope.", 160 | "body": [ 161 | "", 162 | "\t", 163 | "\t\t", 164 | "\t", 165 | "" 166 | ] 167 | }, 168 | "find-and-replace": { 169 | "prefix": "find-and-replace", 170 | "description": "Find a request or response substring and replace it with a different substring. Use in the inbound and outbound sections at any scope.", 171 | "body": [ 172 | "" 173 | ] 174 | }, 175 | "forward-request": { 176 | "prefix": "forward-request", 177 | "description": "Forward request to the backend service using information in the context and receive a response, waiting no longer then specified timeout value. Use at any scope in the backend section.", 178 | "body": [ 179 | "" 180 | ] 181 | }, 182 | "ip-filter": { 183 | "prefix": "ip-filter", 184 | "description": "Allow calls only from specific IP addresses and/or address ranges. Forbid calls from specific IP addresses and/or address ranges. Use in the inbound section at any scope.", 185 | "body": [ 186 | "", 187 | "\t", 188 | "" 189 | ] 190 | }, 191 | "jsonp": { 192 | "prefix": "jsonp", 193 | "description": "Add support for JSONP to an operation or an API to allow cross-domain calls from JavaScript browser-based clients. Use in the outbound section only.", 194 | "body": [ 195 | "" 196 | ] 197 | }, 198 | "json-to-xml": { 199 | "prefix": "json-to-xml", 200 | "description": "Convert request or response body from JSON to XML. Use in the inbound or outbound sections at API or Operation scopes.", 201 | "body": [ 202 | "" 203 | ] 204 | }, 205 | "limit-concurrency": { 206 | "prefix": "limit-concurrency", 207 | "description": "Limit how many calls may be processed in parallel for the duration of this policy's body.", 208 | "body": [ 209 | "", 210 | "" 211 | ] 212 | }, 213 | "log-to-eventhub": { 214 | "prefix": "log-to-eventhub", 215 | "description": "Send custom messages to Event Hub. Use at any scope in the inbound or outbound sections.", 216 | "body": [ 217 | "", 218 | "\t@($2)", 219 | "" 220 | ] 221 | }, 222 | "mock-response": { 223 | "prefix": "mock-response", 224 | "description": "Mock response based on operation responses samples/schemas. Use at any scope in the inbound or outbound sections.", 225 | "body": [ 226 | "" 227 | ] 228 | }, 229 | "proxy": { 230 | "prefix": "proxy", 231 | "description": "Route requests forwarded to backends via an HTTP proxy. Use at any scope in the inbound section.", 232 | "body": [ 233 | "" 234 | ] 235 | }, 236 | "quota": { 237 | "prefix": "quota", 238 | "description": "Enforce a renewable or lifetime call volume and/or bandwidth quota per subscription. Use in the inbound section at Product scope.", 239 | "body": [ 240 | "", 241 | "\t", 242 | "\t\t", 243 | "\t", 244 | "" 245 | ] 246 | }, 247 | "quota-by-key": { 248 | "prefix": "quota-by-key", 249 | "description": "Enforce a renewable or lifetime call volume and/or bandwidth quota per calculated key. Use in the inbound section at any scope.", 250 | "body": [ 251 | "" 252 | ] 253 | }, 254 | "rate-limit": { 255 | "prefix": "rate-limit", 256 | "description": "Arrest usage spikes by limiting calls and/or bandwidth consumption rate per subscription. Use in the inbound section at Product scope.", 257 | "body": [ 258 | "", 259 | "\t", 260 | "\t\t", 261 | "\t", 262 | "" 263 | ] 264 | }, 265 | "rate-limit-by-key": { 266 | "prefix": "rate-limit-by-key", 267 | "description": "Arrest usage spikes by limiting calls and/or bandwidth consumption rate per calculated key. Use in the inbound section at any scope.", 268 | "body": [ 269 | "" 270 | ] 271 | }, 272 | "redirect-content-urls": { 273 | "prefix": "redirect-content-urls", 274 | "description": "Use in the outbound section to re-write response body links and Location header values making them point to the proxy. Use in the inbound section for an opposite effect. Apply at API or Operation scopes.", 275 | "body": [ 276 | "" 277 | ] 278 | }, 279 | "retry": { 280 | "prefix": "retry", 281 | "description": "Retry execution of the enclosed policy statements, if and until the condition is met. Execution will repeat at the specified time interval, up to the specified count.", 282 | "body": [ 283 | "", 284 | "\t$0", 285 | "" 286 | ] 287 | }, 288 | "return-response": { 289 | "prefix": "return-response", 290 | "description": "Abort pipeline execution and return the specified response directly to the caller. Use at any scope in the inbound and outbound sections.", 291 | "body": [ 292 | "", 293 | "\t", 294 | "\t", 295 | "\t\t$5", 296 | "\t", 297 | "\t$6", 298 | "" 299 | ] 300 | }, 301 | "rewrite-uri": { 302 | "prefix": "rewrite-uri", 303 | "description": "Convert request URL from its public form to the form expected by the web service. Use anywhere in the inbound section at Operation scope only.", 304 | "body": [ 305 | "" 306 | ] 307 | }, 308 | "send-one-way-request": { 309 | "prefix": "send-one-way-request", 310 | "description": "Send provided request to the specified URL, without waiting for response. Use at any scope in the inbound and outbound sections.", 311 | "body": [ 312 | "", 313 | "\t$2", 314 | "\t${3|GET,PUT,PATCH,DELETE|}", 315 | "\t", 316 | "\t\t$6", 317 | "\t", 318 | "\t$7", 319 | "" 320 | ] 321 | }, 322 | "send-request": { 323 | "prefix": "send-request", 324 | "description": "Send provided request to the specified URL, waiting no longer then set timeout value. Use at any scope in the inbound and outbound sections.", 325 | "body": [ 326 | "", 327 | "\t$5", 328 | "\t${6|GET,PUT,PATCH,DELETE|}", 329 | "\t", 330 | "\t\t$9", 331 | "\t", 332 | "\t$10", 333 | "" 334 | ] 335 | }, 336 | "set-backend-service": { 337 | "prefix": "set-backend-service", 338 | "description": "Change backend service where the incoming calls will be directed. Use in the inbound section only at any scope.", 339 | "body": [ 340 | "" 341 | ] 342 | }, 343 | "set-body": { 344 | "prefix": "set-body", 345 | "description": "Set message body to a specific string value. The policy has no effect on the Content-Type header value. Use at any scope in the inbound or outbound sections.", 346 | "body": [ 347 | "$2" 348 | ] 349 | }, 350 | "set-header": { 351 | "prefix": "set-header", 352 | "description": "Add a new header, change the value of an existing header or remove a header. Works for both response and request headers – policy can be applied in inbound or outbound sections at any scope.", 353 | "body": [ 354 | "", 355 | "\t$3", 356 | "" 357 | ] 358 | }, 359 | "set-method": { 360 | "prefix": "set-method", 361 | "description": "Change HTTP method to the specified value", 362 | "body": [ 363 | "${1|GET,PUT,PATCH,DELETE|}" 364 | ] 365 | }, 366 | "set-query-parameter": { 367 | "prefix": "set-query-parameter", 368 | "description": "Add a new query string parameter, change the value of an existing parameter or remove a parameter. Can be applied in the inbound section at any scope.", 369 | "body": [ 370 | "", 371 | "\t$3", 372 | "" 373 | ] 374 | }, 375 | "set-status": { 376 | "prefix": "set-status", 377 | "description": "Change HTTP status code to the specified value. Use at any scope in the outbound sections.", 378 | "body": [ 379 | "" 380 | ] 381 | }, 382 | "set-variable": { 383 | "prefix": "set-variable", 384 | "description": "Persist a value in a named context variable for later access from expressions. Use at any scope in the inbound and outbound sections.", 385 | "body": [ 386 | "" 387 | ] 388 | }, 389 | "trace": { 390 | "prefix": "trace", 391 | "description": "Output information into trace logs, if request is executed with tracing enabled.", 392 | "body": [ 393 | "", 394 | "\t@($2)", 395 | "" 396 | ] 397 | }, 398 | "validate-jwt": { 399 | "prefix": "validate-jwt", 400 | "description": "Check and validate a JWT in a header or query parameter. Use in the inbound section at any scope.", 401 | "body": [ 402 | "", 403 | "\t", 404 | "\t", 405 | "\t\t${9:Base64 Encoded Key}", 406 | "\t", 407 | "\t", 408 | "\t\t$10", 409 | "\t", 410 | "\t", 411 | "\t\t$11", 412 | "\t", 413 | "\t", 414 | "\t\t", 415 | "\t\t\t$14", 416 | "\t\t", 417 | "\t", 418 | "" 419 | ] 420 | }, 421 | "wait": { 422 | "prefix": "wait", 423 | "description": "Wait for all or any of the send request policies to complete before proceeding. Use at any scope in the inbound and outbound sections.", 424 | "body": [ 425 | "", 426 | "\t$0", 427 | "" 428 | ] 429 | }, 430 | "xml-to-json": { 431 | "prefix": "xml-to-json", 432 | "description": "Convert request or response body from XML to either \"JSON friendly\" or \"XML faithful\" form of JSON. Use in the inbound or outbound sections at API or Operation scopes.", 433 | "body": [ 434 | "" 435 | ] 436 | }, 437 | "xsl-transform": { 438 | "prefix": "xsl-transform", 439 | "description": "Transform request or response body using XSLTransform. Use in the inbound, outbound and on-error sections at any scope.", 440 | "body": [ 441 | "", 442 | "\t@($2)", 443 | "\t", 444 | "\t\t", 445 | "\t\t", 446 | "\t\t", 447 | "\t\t\t", 448 | "\t\t\t\t", 449 | "\t\t\t", 450 | "\t\t", 451 | "\t", 452 | "" 453 | ] 454 | }, 455 | "authentication-managed-identity": { 456 | "prefix": "authentication-managed-identity", 457 | "description": "Obtain a token to a resource from Azure AD using managed identity of the Azure API Management service and send it to a backend in the Authorization header.", 458 | "body": [ 459 | "" 460 | ] 461 | } 462 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pascal van der Heiden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure API Management with Azure OpenAI 2 | 3 | Unleash the power of Azure OpenAI to your application developers in a secure & manageable way with Azure API Management and Azure Developer CLI(`azd`). 4 | 5 | [![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=lightgrey&logo=github)](https://codespaces.new/pascalvanderheiden/ais-apim-openai) 6 | [![Open in Dev Container](https://img.shields.io/static/v1?style=for-the-badge&label=Dev+Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/pascalvanderheiden/ais-apim-openai) 7 | 8 | Available as template on: 9 | [![Awesome Badge](https://awesome.re/badge-flat2.svg)](https://aka.ms/awesome-azd) 10 | `azd` 11 | 12 | ## Build Status 13 | 14 | | GitHub Action | Status | 15 | | ----------- | ----------- | 16 | | `azd` Deploy | [![Deploy](https://github.com/pascalvanderheiden/ais-apim-openai/actions/workflows/azure-dev.yml/badge.svg?branch=main)](https://github.com/pascalvanderheiden/ais-apim-openai/actions/workflows/azure-dev.yml) | 17 | 18 | ## About 19 | I've used the Azure Developer CLI Bicep Starter template to create this repository. With `azd` you can create a new repository with a fully functional CI/CD pipeline in minutes. You can find more information about `azd` [here](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/). 20 | 21 | One of the key points of `azd` templates is that we can implement best practices together with our solution when it comes to security, network isolation, monitoring, etc. Users are free to define their own best practices for their dev teams & organization, so all deployments are followed by the same standards. The best practices I followed for this architecture are: [Azure Integration Service Landingzone Accelerator](https://github.com/Azure/Integration-Services-Landing-Zone-Accelerator/tree/main) and for Azure OpenAI I used the blog post [Azure OpenAI Landing Zone reference architecture](https://techcommunity.microsoft.com/t5/azure-architecture-blog/azure-openai-landing-zone-reference-architecture/ba-p/3882102). 22 | 23 | When it comes to security, there are recommendations mentioned for securing your Azure API Management instance in the Azure Integration Service Landingzone Accelerator. For example, with the use of Front Door or Application Gateway, proving Layer 7 protection and WAF capabilities, and by implementing OAuth authentication on the API Management instance. How to implement OAuth authentication on the API Management instance is described in another repository I've created: [OAuth flow with Azure AD and Azure API Management.](https://github.com/pascalvanderheiden/ais-apim-oauth-flow). Because it really depends on the use case, I didn't implement Front Door or Application Gateway in this repository. But you can easily add it to the Bicep files if you want to, see [this](https://github.com/pascalvanderheiden/ais-sync-pattern-la-std-vnet) repository for as an example. 24 | 25 | I'm also using [Azure Monitor Private Link Scope](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/private-link-security#configure-access-to-your-resources). This allows me to define the boundaries of my monitoring network, and only allow traffic from within that network to my Log Analytics workspace. This is a great way to secure your monitoring network. 26 | 27 | I've simplified / transformed the output of OpenAI service with a Azure API Management policy using [Liquid](https://learn.microsoft.com/en-us/azure/api-management/set-body-policy#transform-json-using-a-liquid-template). 28 | 29 | The following assets have been provided: 30 | 31 | - Infrastructure-as-code (IaC) Bicep files under the `infra` folder that demonstrate how to provision resources and setup resource tagging for azd. 32 | - A [dev container](https://containers.dev) configuration file under the `.devcontainer` directory that installs infrastructure tooling by default. This can be readily used to create cloud-hosted developer environments such as [GitHub Codespaces](https://aka.ms/codespaces). 33 | - Continuous deployment workflows for CI providers such as GitHub Actions under the `.github` directory, and Azure Pipelines under the `.azdo` directory that work for most use-cases. 34 | 35 | ## Architecture 36 | 37 | ![ais-apim-openai](docs/images/arch.png) 38 | 39 | ## Prerequisites 40 | 41 | - [Azure Developer CLI](https://docs.microsoft.com/en-us/azure/developer/azure-developer-cli/) 42 | 43 | ## Next Steps 44 | 45 | ### Step 1: Initialize a new `azd` environment 46 | 47 | ```shell 48 | azd init 49 | ``` 50 | 51 | It will prompt you to provide a name that will later be used in the name of the deployed resources. 52 | 53 | ### Step 2: Provision and deploy all the resources 54 | 55 | ```shell 56 | azd up 57 | ``` 58 | 59 | It will prompt you to login, pick a subscription, and provide a location (like "eastus"). Then it will provision the resources in your account and deploy the latest code. 60 | 61 | For more details on the deployed services, see [additional details](#additional-details) below. 62 | 63 | > Note. Because Azure OpenAI isn't available yet in all regions, you might get an error when you deploy the resources. You can find more information about the availability of Azure OpenAI [here](https://docs.microsoft.com/en-us/azure/openai/overview/regions). 64 | 65 | > Note. It will take about 45 minutes to deploy Azure API Management. 66 | 67 | > Note. Sometimes the dns zones for the private endpoints aren't created correctly / in time. If you get an error when you deploy the resources, you can try to deploy the resources again. 68 | 69 | ## CI/CD pipeline 70 | 71 | This project includes a Github workflow and a Azure DevOps Pipeline for deploying the resources to Azure on every push to main. That workflow requires several Azure-related authentication secrets to be stored as Github action secrets. To set that up, run: 72 | 73 | ```shell 74 | azd pipeline config 75 | ``` 76 | 77 | ## Monitoring 78 | 79 | The deployed resources include a Log Analytics workspace with an Application Insights dashboard to measure metrics like server response time. 80 | 81 | To open that dashboard, run this command once you've deployed: 82 | 83 | ```shell 84 | azd monitor --overview 85 | ``` 86 | 87 | ## Remove the APIM Soft-delete 88 | 89 | If you deleted the deployment via the Azure Portal, and you want to run this deployment again, you might run into the issue that the APIM name is still reserved because of the soft-delete feature. You can remove the soft-delete by using this script: 90 | 91 | ```ps1 92 | $subscriptionId = "" 93 | $apimName = "" 94 | Connect-AzAccount 95 | Set-AzContext -Subscription $subscriptionId 96 | .\del-soft-delete-apim.ps1 -subscriptionId $subscriptionId -apimName $apimName 97 | ``` 98 | 99 | ## Testing 100 | 101 | I've included a [tests.http](tests.http) file with relevant tests you can perform, to check if your deployment is successful. I've also included a sample test if you implemented OAuth authentication on the API in API Management. You need a subcription key in API Management in order to test the API. You can find more information about how to get a subscription key [here](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-create-subscriptions#add-a-subscription-key-to-a-user). 102 | 103 | ## Additional Details 104 | 105 | The following section examines different concepts that help tie in application and infrastructure. 106 | 107 | ### Azure API Management 108 | 109 | [Azure API Management](https://azure.microsoft.com/en-us/services/api-management/) is a fully managed service that enables customers to publish, secure, transform, maintain, and monitor APIs. It is a great way to expose your APIs to the outside world in a secure and manageable way. 110 | 111 | ### Azure OpenAI 112 | 113 | [Azure OpenAI](https://azure.microsoft.com/en-us/services/openai/) is a service that provides AI models that are trained on a large amount of data. You can use these models to generate text, images, and more. 114 | 115 | ### Managed identities 116 | 117 | [Managed identities](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) allows you to secure communication between services. This is done without having the need for you to manage any credentials. 118 | 119 | ### Virtual Network 120 | 121 | [Azure Virtual Network](https://azure.microsoft.com/en-us/services/virtual-network/) allows you to create a private network in Azure. You can use this to secure communication between services. 122 | 123 | ### Azure Private DNS Zone 124 | 125 | [Azure Private DNS Zone](https://docs.microsoft.com/en-us/azure/dns/private-dns-overview) allows you to create a private DNS zone in Azure. You can use this to resolve hostnames in your private network. 126 | 127 | ### Azure Key Vault 128 | 129 | [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) allows you to store secrets securely. Your application can access these secrets securely through the use of managed identities. 130 | 131 | ### Application Insights 132 | 133 | [Application Insights](https://azure.microsoft.com/en-us/services/monitor/) allows you to monitor your application. You can use this to monitor the performance of your application. 134 | 135 | ### Log Analytics 136 | 137 | [Log Analytics](https://azure.microsoft.com/en-us/services/monitor/) allows you to collect and analyze telemetry data from your application. You can use this to monitor the performance of your application. 138 | 139 | ### Azure Monitor Private Link Scope 140 | 141 | [Azure Monitor Private Link Scope](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/private-link-security#configure-access-to-your-resources) allows you to define the boundaries of your monitoring network, and only allow traffic from within that network to your Log Analytics workspace. This is a great way to secure your monitoring network. 142 | 143 | ### Private Endpoint 144 | 145 | [Azure Private Endpoint](https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview) allows you to connect privately to a service powered by Azure Private Link. Private Endpoint uses a private IP address from your VNet, effectively bringing the service into your VNet. -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | # This is an example starter azure.yaml file containing several example services in comments below. 4 | # Make changes as needed to describe your application setup. 5 | # To learn more about the azure.yaml file, visit https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/azd-schema 6 | 7 | # Name of the application. 8 | name: ais-apim-openai 9 | metadata: 10 | template: ais-apim-openai@1.0.0 11 | # services: 12 | # ## An example for a python API service. 13 | # ## The service is named 'python-api'. 14 | # ## The language is 'python'. 15 | # ## The source code is located in the project (azure.yaml) directory. 16 | # ## The service will be hosted on Azure App Service. 17 | # python-api: 18 | # language: python 19 | # project: ./ 20 | # host: appservice 21 | # ## An example for a NodeJS API, located in src/api. 22 | # nodejs-api: 23 | # language: js 24 | # project: ./src/api 25 | # host: appservice 26 | # ## An example for a React front-end app. 27 | # ## The src/react-app/build folder is where the app is built to after `npm run build`. 28 | # react-web: 29 | # language: js 30 | # project: ./src/react-app 31 | # host: appservice 32 | # dist: build 33 | -------------------------------------------------------------------------------- /del-soft-delete-apim.ps1: -------------------------------------------------------------------------------- 1 | param ($subscriptionId, $apimName) 2 | 3 | $location = "West Europe" 4 | $token = Get-AzAccessToken 5 | $uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.ApiManagement/locations/$location/deletedservices/$apimName/?api-version=2020-12-01" 6 | 7 | $request = @{ 8 | Method = "DELETE" 9 | Uri = $uri 10 | Headers = @{ 11 | Authorization = "Bearer $($token.Token)" 12 | } 13 | } 14 | 15 | Invoke-RestMethod @request -------------------------------------------------------------------------------- /docs/images/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalvanderheiden/ais-apim-openai/66b1e07e135d1a24b7433491589623da1523e507/docs/images/arch.png -------------------------------------------------------------------------------- /docs/raw/arch.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des", 20 | "computeDisks": "disk", 21 | "computeDisksOs": "osdisk", 22 | "computeGalleries": "gal", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci", 27 | "containerRegistryRegistries": "cr", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla", 32 | "dataLakeStoreAccounts": "dls", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopics": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck", 56 | "kustoClusters": "dec", 57 | "kustoClustersDatabases": "dedb", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf", 73 | "networkFirewallPoliciesRuleGroups": "wafrg", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "privateEndpoints": "pe-", 108 | "recoveryServicesVaults": "rsv-", 109 | "resourcesResourceGroups": "rg-", 110 | "searchSearchServices": "srch-", 111 | "serviceBusNamespaces": "sb-", 112 | "serviceBusNamespacesQueues": "sbq-", 113 | "serviceBusNamespacesTopics": "sbt-", 114 | "serviceEndPointPolicies": "se-", 115 | "serviceFabricClusters": "sf-", 116 | "signalRServiceSignalR": "sigr", 117 | "sqlManagedInstances": "sqlmi-", 118 | "sqlServers": "sql-", 119 | "sqlServersDataWarehouse": "sqldw-", 120 | "sqlServersDatabases": "sqldb-", 121 | "sqlServersDatabasesStretch": "sqlstrdb-", 122 | "storageStorageAccounts": "st", 123 | "storageStorageAccountsVm": "stvm", 124 | "storSimpleManagers": "ssimp", 125 | "streamAnalyticsCluster": "asa-", 126 | "synapseWorkspaces": "syn", 127 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 128 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 129 | "synapseWorkspacesSqlPoolsSpark": "synsp", 130 | "timeSeriesInsightsEnvironments": "tsi-", 131 | "webServerFarms": "plan-", 132 | "webSitesAppService": "app-", 133 | "webSitesAppServiceEnvironment": "ase-", 134 | "webSitesFunctions": "func-", 135 | "webStaticSites": "stapp-" 136 | } -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources (filtered on available regions for Azure Open AI Service).') 10 | @allowed(['westeurope','southcentralus','australiaeast', 'canadaeast', 'eastus', 'eastus2', 'francecentral', 'japaneast', 'northcentralus', 'swedencentral', 'switzerlandnorth', 'uksouth']) 11 | param location string 12 | 13 | //Leave blank to use default naming conventions 14 | param resourceGroupName string = '' 15 | param openAiServiceName string = '' 16 | param keyVaultName string = '' 17 | param identityName string = '' 18 | param apimServiceName string = '' 19 | param logAnalyticsName string = '' 20 | param applicationInsightsDashboardName string = '' 21 | param applicationInsightsName string = '' 22 | param vnetName string = '' 23 | param apimSubnetName string = '' 24 | param apimNsgName string = '' 25 | param privateEndpointSubnetName string = '' 26 | param privateEndpointNsgName string = '' 27 | 28 | //Determine the version of the chat model to deploy 29 | param arrayVersion0301Locations array = [ 30 | 'westeurope' 31 | 'southcentralus' 32 | ] 33 | param chatGptModelVersion string = ((contains(arrayVersion0301Locations, location)) ? '0301' : '0613') 34 | 35 | var abbrs = loadJsonContent('./abbreviations.json') 36 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 37 | var openAiSkuName = 'S0' 38 | var chatGptDeploymentName = 'chat' 39 | var chatGptModelName = 'gpt-35-turbo' 40 | var openaiApiKeySecretName = 'openai-apikey' 41 | var tags = { 'azd-env-name': environmentName } 42 | 43 | var openAiPrivateDnsZoneName = 'privatelink.openai.azure.com' 44 | var keyVaultPrivateDnsZoneName = 'privatelink.vaultcore.azure.net' 45 | var monitorPrivateDnsZoneName = 'privatelink.monitor.azure.com' 46 | 47 | var privateDnsZoneNames = [ 48 | openAiPrivateDnsZoneName 49 | keyVaultPrivateDnsZoneName 50 | monitorPrivateDnsZoneName 51 | ] 52 | 53 | // Organize resources in a resource group 54 | resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { 55 | name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' 56 | location: location 57 | tags: tags 58 | } 59 | 60 | module dnsDeployment './modules/networking/dns.bicep' = [for privateDnsZoneName in privateDnsZoneNames: { 61 | name: 'dns-deployment-${privateDnsZoneName}' 62 | scope: resourceGroup 63 | params: { 64 | name: privateDnsZoneName 65 | } 66 | }] 67 | 68 | module managedIdentity './modules/security/managed-identity.bicep' = { 69 | name: 'managed-identity' 70 | scope: resourceGroup 71 | params: { 72 | name: !empty(identityName) ? identityName : '${abbrs.managedIdentityUserAssignedIdentities}${resourceToken}' 73 | location: location 74 | tags: tags 75 | } 76 | } 77 | 78 | module keyVault './modules/security/key-vault.bicep' = { 79 | name: 'key-vault' 80 | scope: resourceGroup 81 | params: { 82 | name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' 83 | location: location 84 | tags: tags 85 | keyVaultPrivateEndpointName: '${abbrs.keyVaultVaults}${abbrs.privateEndpoints}${resourceToken}' 86 | vNetName: vnet.outputs.vnetName 87 | privateEndpointSubnetName: vnet.outputs.privateEndpointSubnetName 88 | logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName 89 | managedIdentityName: managedIdentity.outputs.managedIdentityName 90 | keyVaultDnsZoneName: keyVaultPrivateDnsZoneName 91 | } 92 | } 93 | 94 | module openaiKeyVaultSecret './modules/security/keyvault-secret.bicep' = { 95 | name: 'openai-keyvault-secret' 96 | scope: resourceGroup 97 | params: { 98 | keyVaultName: keyVault.outputs.keyVaultName 99 | secretName: openaiApiKeySecretName 100 | openAiName: openAi.outputs.openAiName 101 | } 102 | } 103 | 104 | module vnet './modules/networking/vnet.bicep' = { 105 | name: 'vnet' 106 | scope: resourceGroup 107 | dependsOn: [ 108 | dnsDeployment 109 | ] 110 | params: { 111 | name: !empty(vnetName) ? vnetName : '${abbrs.networkVirtualNetworks}${resourceToken}' 112 | apimSubnetName: !empty(apimSubnetName) ? apimSubnetName : '${abbrs.networkVirtualNetworksSubnets}${abbrs.apiManagementService}${resourceToken}' 113 | apimNsgName: !empty(apimNsgName) ? apimNsgName : '${abbrs.networkNetworkSecurityGroups}${abbrs.apiManagementService}${resourceToken}' 114 | privateEndpointSubnetName: !empty(privateEndpointSubnetName) ? privateEndpointSubnetName : '${abbrs.networkVirtualNetworksSubnets}${abbrs.privateEndpoints}${resourceToken}' 115 | privateEndpointNsgName: !empty(privateEndpointNsgName) ? privateEndpointNsgName : '${abbrs.networkNetworkSecurityGroups}${abbrs.privateEndpoints}${resourceToken}' 116 | location: location 117 | tags: tags 118 | privateDnsZoneNames: privateDnsZoneNames 119 | } 120 | } 121 | 122 | module monitoring './modules/monitor/monitoring.bicep' = { 123 | name: 'monitoring' 124 | scope: resourceGroup 125 | params: { 126 | location: location 127 | tags: tags 128 | logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' 129 | applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' 130 | applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' 131 | vNetName: vnet.outputs.vnetName 132 | privateEndpointSubnetName: vnet.outputs.privateEndpointSubnetName 133 | applicationInsightsDnsZoneName: monitorPrivateDnsZoneName 134 | applicationInsightsPrivateEndpointName: '${abbrs.insightsComponents}${abbrs.privateEndpoints}${resourceToken}' 135 | } 136 | } 137 | 138 | module apim './modules/apim/apim.bicep' = { 139 | name: 'apim' 140 | scope: resourceGroup 141 | params: { 142 | name: !empty(apimServiceName) ? apimServiceName : '${abbrs.apiManagementService}${resourceToken}' 143 | location: location 144 | tags: tags 145 | applicationInsightsName: monitoring.outputs.applicationInsightsName 146 | openaiKeyVaultSecretName: openaiKeyVaultSecret.outputs.keyVaultSecretName 147 | keyVaultEndpoint: keyVault.outputs.keyVaultEndpoint 148 | openAiUri: openAi.outputs.openAiEndpointUri 149 | managedIdentityName: managedIdentity.outputs.managedIdentityName 150 | apimSubnetId: vnet.outputs.apimSubnetId 151 | } 152 | } 153 | 154 | module openAi 'modules/ai/cognitiveservices.bicep' = { 155 | name: 'openai' 156 | scope: resourceGroup 157 | params: { 158 | name: !empty(openAiServiceName) ? openAiServiceName : '${abbrs.cognitiveServicesAccounts}${resourceToken}' 159 | location: location 160 | tags: tags 161 | openAiPrivateEndpointName: '${abbrs.cognitiveServicesAccounts}${abbrs.privateEndpoints}${resourceToken}' 162 | vNetName: vnet.outputs.vnetName 163 | privateEndpointSubnetName: vnet.outputs.privateEndpointSubnetName 164 | openAiDnsZoneName: openAiPrivateDnsZoneName 165 | sku: { 166 | name: openAiSkuName 167 | } 168 | deployments: [ 169 | { 170 | name: chatGptDeploymentName 171 | model: { 172 | format: 'OpenAI' 173 | name: chatGptModelName 174 | version: chatGptModelVersion 175 | } 176 | scaleSettings: { 177 | scaleType: 'Standard' 178 | } 179 | } 180 | ] 181 | } 182 | } 183 | 184 | output TENTANT_ID string = subscription().tenantId 185 | output AOI_DEPLOYMENTID string = chatGptDeploymentName 186 | output APIM_NAME string = apim.outputs.apimName 187 | output APIM_AOI_PATH string = apim.outputs.apimOpenaiApiPath 188 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /infra/modules/ai/cognitiveservices.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param customSubDomainName string = name 6 | param deployments array = [] 7 | param kind string = 'OpenAI' 8 | param publicNetworkAccess string = 'Disabled' 9 | param sku object = { 10 | name: 'S0' 11 | } 12 | param openAiPrivateEndpointName string 13 | param vNetName string 14 | param privateEndpointSubnetName string 15 | param openAiDnsZoneName string 16 | 17 | resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 18 | name: name 19 | location: location 20 | tags: union(tags, { 'azd-service-name': name }) 21 | kind: kind 22 | properties: { 23 | customSubDomainName: customSubDomainName 24 | publicNetworkAccess: publicNetworkAccess 25 | networkAcls: { 26 | defaultAction: 'Deny' 27 | } 28 | } 29 | sku: sku 30 | } 31 | 32 | @batchSize(1) 33 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 34 | parent: account 35 | name: deployment.name 36 | properties: { 37 | model: deployment.model 38 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 39 | } 40 | sku: contains(deployment, 'sku') ? deployment.sku : { 41 | name: 'Standard' 42 | capacity: 20 43 | } 44 | }] 45 | 46 | module privateEndpoint '../networking/private-endpoint.bicep' = { 47 | name: '${account.name}-privateEndpoint-deployment' 48 | params: { 49 | groupIds: [ 50 | 'account' 51 | ] 52 | dnsZoneName: openAiDnsZoneName 53 | name: openAiPrivateEndpointName 54 | subnetName: privateEndpointSubnetName 55 | privateLinkServiceId: account.id 56 | vNetName: vNetName 57 | location: location 58 | } 59 | } 60 | 61 | output openAiName string = account.name 62 | output openAiEndpointUri string = '${account.properties.endpoint}openai/' 63 | -------------------------------------------------------------------------------- /infra/modules/apim/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalvanderheiden/ais-apim-openai/66b1e07e135d1a24b7433491589623da1523e507/infra/modules/apim/.DS_Store -------------------------------------------------------------------------------- /infra/modules/apim/apim.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | @minLength(1) 6 | param publisherEmail string = 'noreply@microsoft.com' 7 | 8 | @minLength(1) 9 | param publisherName string = 'n/a' 10 | param sku string = 'Developer' 11 | param skuCount int = 1 12 | param applicationInsightsName string 13 | param openAiUri string 14 | param openaiKeyVaultSecretName string 15 | param keyVaultEndpoint string 16 | param managedIdentityName string 17 | param apimSubnetId string 18 | 19 | var openAiApiKeyNamedValue = 'openai-apikey' 20 | var openAiApiBackendId = 'openai-backend' 21 | 22 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 23 | name: applicationInsightsName 24 | } 25 | 26 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = { 27 | name: managedIdentityName 28 | } 29 | 30 | resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = { 31 | name: name 32 | location: location 33 | tags: union(tags, { 'azd-service-name': name }) 34 | sku: { 35 | name: sku 36 | capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount) 37 | } 38 | identity: { 39 | type: 'UserAssigned' 40 | userAssignedIdentities: { 41 | '${managedIdentity.id}': {} 42 | } 43 | } 44 | properties: { 45 | publisherEmail: publisherEmail 46 | publisherName: publisherName 47 | virtualNetworkType: 'External' 48 | virtualNetworkConfiguration: { 49 | subnetResourceId: apimSubnetId 50 | } 51 | // Custom properties are not supported for Consumption SKU 52 | customProperties: sku == 'Consumption' ? {} : { 53 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 'false' 54 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false' 55 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_GCM_SHA256': 'false' 56 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA256': 'false' 57 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA256': 'false' 58 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA': 'false' 59 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA': 'false' 60 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168': 'false' 61 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false' 62 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false' 63 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30': 'false' 64 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false' 65 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false' 66 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30': 'false' 67 | } 68 | } 69 | } 70 | 71 | resource apimOpenaiApi 'Microsoft.ApiManagement/service/apis@2022-08-01' = { 72 | name: 'azure-openai-service-api' 73 | parent: apimService 74 | properties: { 75 | path: 'openai' 76 | apiRevision: '1' 77 | displayName: 'Azure OpenAI Service API' 78 | subscriptionRequired: true 79 | format: 'openapi+json' 80 | value: loadJsonContent('./openapi/openai-openapiv3.json') 81 | protocols: [ 82 | 'https' 83 | ] 84 | } 85 | } 86 | 87 | resource openAiBackend 'Microsoft.ApiManagement/service/backends@2021-08-01' = { 88 | name: openAiApiBackendId 89 | parent: apimService 90 | properties: { 91 | description: openAiApiBackendId 92 | url: openAiUri 93 | protocol: 'http' 94 | tls: { 95 | validateCertificateChain: true 96 | validateCertificateName: true 97 | } 98 | } 99 | } 100 | 101 | resource apimOpenaiApiKeyNamedValue 'Microsoft.ApiManagement/service/namedValues@2022-08-01' = { 102 | name: openAiApiKeyNamedValue 103 | parent: apimService 104 | properties: { 105 | displayName: openAiApiKeyNamedValue 106 | secret: true 107 | keyVault:{ 108 | secretIdentifier: '${keyVaultEndpoint}secrets/${openaiKeyVaultSecretName}' 109 | identityClientId: apimService.identity.userAssignedIdentities[managedIdentity.id].clientId 110 | } 111 | } 112 | } 113 | 114 | resource openaiApiPolicy 'Microsoft.ApiManagement/service/apis/policies@2022-08-01' = { 115 | name: 'policy' 116 | parent: apimOpenaiApi 117 | properties: { 118 | value: loadTextContent('./policies/api_policy.xml') 119 | format: 'rawxml' 120 | } 121 | dependsOn: [ 122 | openAiBackend 123 | apimOpenaiApiKeyNamedValue 124 | ] 125 | } 126 | 127 | resource apiOperationCompletions 'Microsoft.ApiManagement/service/apis/operations@2020-06-01-preview' existing = { 128 | name: 'ChatCompletions_Create' 129 | parent: apimOpenaiApi 130 | } 131 | 132 | resource chatCompletionsCreatePolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2022-08-01' = { 133 | name: 'policy' 134 | parent: apiOperationCompletions 135 | properties: { 136 | value: loadTextContent('./policies/api_operation_policy.xml') 137 | format: 'rawxml' 138 | } 139 | } 140 | 141 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = { 142 | name: 'appinsights-logger' 143 | parent: apimService 144 | properties: { 145 | credentials: { 146 | instrumentationKey: applicationInsights.properties.InstrumentationKey 147 | } 148 | description: 'Logger to Azure Application Insights' 149 | isBuffered: false 150 | loggerType: 'applicationInsights' 151 | resourceId: applicationInsights.id 152 | } 153 | } 154 | 155 | output apimName string = apimService.name 156 | output apimOpenaiApiPath string = apimOpenaiApi.properties.path 157 | -------------------------------------------------------------------------------- /infra/modules/apim/openapi/openai-openapiv3.json: -------------------------------------------------------------------------------- 1 | //https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/stable/2023-05-15/inference.json 2 | { 3 | "openapi": "3.0.0", 4 | "info": { 5 | "title": "Azure OpenAI Service API", 6 | "description": "Azure OpenAI APIs for completions and search", 7 | "version": "2023-05-15" 8 | }, 9 | "servers": [ 10 | { 11 | "url": "https://{endpoint}/openai", 12 | "variables": { 13 | "endpoint": { 14 | "default": "your-resource-name.openai.azure.com" 15 | } 16 | } 17 | } 18 | ], 19 | "security": [ 20 | { 21 | "bearer": [ 22 | "api.read" 23 | ] 24 | }, 25 | { 26 | "apiKey": [] 27 | } 28 | ], 29 | "paths": { 30 | "/deployments/{deployment-id}/completions": { 31 | "post": { 32 | "summary": "Creates a completion for the provided prompt, parameters and chosen model.", 33 | "operationId": "Completions_Create", 34 | "parameters": [ 35 | { 36 | "in": "path", 37 | "name": "deployment-id", 38 | "required": true, 39 | "schema": { 40 | "type": "string", 41 | "example": "davinci", 42 | "description": "Deployment id of the model which was deployed." 43 | } 44 | }, 45 | { 46 | "in": "query", 47 | "name": "api-version", 48 | "required": true, 49 | "schema": { 50 | "type": "string", 51 | "example": "2023-05-15", 52 | "description": "api version" 53 | } 54 | } 55 | ], 56 | "requestBody": { 57 | "required": true, 58 | "content": { 59 | "application/json": { 60 | "schema": { 61 | "type": "object", 62 | "properties": { 63 | "prompt": { 64 | "description": "The prompt(s) to generate completions for, encoded as a string or array of strings.\nNote that <|endoftext|> is the document separator that the model sees during training, so if a prompt is not specified the model will generate as if from the beginning of a new document. Maximum allowed size of string list is 2048.", 65 | "oneOf": [ 66 | { 67 | "type": "string", 68 | "default": "", 69 | "example": "This is a test.", 70 | "nullable": true 71 | }, 72 | { 73 | "type": "array", 74 | "items": { 75 | "type": "string", 76 | "default": "", 77 | "example": "This is a test.", 78 | "nullable": false 79 | }, 80 | "description": "Array size minimum of 1 and maximum of 2048" 81 | } 82 | ] 83 | }, 84 | "max_tokens": { 85 | "description": "The token count of your prompt plus max_tokens cannot exceed the model's context length. Most models have a context length of 2048 tokens (except for the newest models, which support 4096). Has minimum of 0.", 86 | "type": "integer", 87 | "default": 16, 88 | "example": 16, 89 | "nullable": true 90 | }, 91 | "temperature": { 92 | "description": "What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer.\nWe generally recommend altering this or top_p but not both.", 93 | "type": "number", 94 | "default": 1, 95 | "example": 1, 96 | "nullable": true 97 | }, 98 | "top_p": { 99 | "description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\nWe generally recommend altering this or temperature but not both.", 100 | "type": "number", 101 | "default": 1, 102 | "example": 1, 103 | "nullable": true 104 | }, 105 | "logit_bias": { 106 | "description": "Defaults to null. Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool (which works for both GPT-2 and GPT-3) to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass {\"50256\" : -100} to prevent the <|endoftext|> token from being generated.", 107 | "type": "object", 108 | "nullable": false 109 | }, 110 | "user": { 111 | "description": "A unique identifier representing your end-user, which can help monitoring and detecting abuse", 112 | "type": "string", 113 | "nullable": false 114 | }, 115 | "n": { 116 | "description": "How many completions to generate for each prompt. Minimum of 1 and maximum of 128 allowed.\nNote: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop.", 117 | "type": "integer", 118 | "default": 1, 119 | "example": 1, 120 | "nullable": true 121 | }, 122 | "stream": { 123 | "description": "Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.", 124 | "type": "boolean", 125 | "nullable": true, 126 | "default": false 127 | }, 128 | "logprobs": { 129 | "description": "Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. For example, if logprobs is 5, the API will return a list of the 5 most likely tokens. The API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response.\nMinimum of 0 and maximum of 5 allowed.", 130 | "type": "integer", 131 | "default": null, 132 | "nullable": true 133 | }, 134 | "model": { 135 | "type": "string", 136 | "example": "davinci", 137 | "nullable": true, 138 | "description": "ID of the model to use. You can use the Models_List operation to see all of your available models, or see our Models_Get overview for descriptions of them." 139 | }, 140 | "suffix": { 141 | "type": "string", 142 | "nullable": true, 143 | "description": "The suffix that comes after a completion of inserted text." 144 | }, 145 | "echo": { 146 | "description": "Echo back the prompt in addition to the completion", 147 | "type": "boolean", 148 | "default": false, 149 | "nullable": true 150 | }, 151 | "stop": { 152 | "description": "Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.", 153 | "oneOf": [ 154 | { 155 | "type": "string", 156 | "default": "<|endoftext|>", 157 | "example": "\n", 158 | "nullable": true 159 | }, 160 | { 161 | "type": "array", 162 | "items": { 163 | "type": "string", 164 | "example": [ 165 | "\n" 166 | ], 167 | "nullable": false 168 | }, 169 | "description": "Array minimum size of 1 and maximum of 4" 170 | } 171 | ] 172 | }, 173 | "completion_config": { 174 | "type": "string", 175 | "nullable": true 176 | }, 177 | "cache_level": { 178 | "description": "can be used to disable any server-side caching, 0=no cache, 1=prompt prefix enabled, 2=full cache", 179 | "type": "integer", 180 | "nullable": true 181 | }, 182 | "presence_penalty": { 183 | "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", 184 | "type": "number", 185 | "default": 0 186 | }, 187 | "frequency_penalty": { 188 | "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", 189 | "type": "number", 190 | "default": 0 191 | }, 192 | "best_of": { 193 | "description": "Generates best_of completions server-side and returns the \"best\" (the one with the highest log probability per token). Results cannot be streamed.\nWhen used with n, best_of controls the number of candidate completions and n specifies how many to return – best_of must be greater than n.\nNote: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop. Has maximum value of 128.", 194 | "type": "integer" 195 | } 196 | } 197 | }, 198 | "example": { 199 | "prompt": "Negate the following sentence.The price for bubblegum increased on thursday.\n\n Negated Sentence:", 200 | "max_tokens": 50 201 | } 202 | } 203 | } 204 | }, 205 | "responses": { 206 | "200": { 207 | "description": "OK", 208 | "content": { 209 | "application/json": { 210 | "schema": { 211 | "type": "object", 212 | "properties": { 213 | "id": { 214 | "type": "string" 215 | }, 216 | "object": { 217 | "type": "string" 218 | }, 219 | "created": { 220 | "type": "integer" 221 | }, 222 | "model": { 223 | "type": "string" 224 | }, 225 | "choices": { 226 | "type": "array", 227 | "items": { 228 | "type": "object", 229 | "properties": { 230 | "text": { 231 | "type": "string" 232 | }, 233 | "index": { 234 | "type": "integer" 235 | }, 236 | "logprobs": { 237 | "type": "object", 238 | "properties": { 239 | "tokens": { 240 | "type": "array", 241 | "items": { 242 | "type": "string" 243 | } 244 | }, 245 | "token_logprobs": { 246 | "type": "array", 247 | "items": { 248 | "type": "number" 249 | } 250 | }, 251 | "top_logprobs": { 252 | "type": "array", 253 | "items": { 254 | "type": "object", 255 | "additionalProperties": { 256 | "type": "number" 257 | } 258 | } 259 | }, 260 | "text_offset": { 261 | "type": "array", 262 | "items": { 263 | "type": "integer" 264 | } 265 | } 266 | } 267 | }, 268 | "finish_reason": { 269 | "type": "string" 270 | } 271 | } 272 | } 273 | }, 274 | "usage": { 275 | "type": "object", 276 | "properties": { 277 | "completion_tokens": { 278 | "type": "number", 279 | "format": "int32" 280 | }, 281 | "prompt_tokens": { 282 | "type": "number", 283 | "format": "int32" 284 | }, 285 | "total_tokens": { 286 | "type": "number", 287 | "format": "int32" 288 | } 289 | }, 290 | "required": [ 291 | "prompt_tokens", 292 | "total_tokens", 293 | "completion_tokens" 294 | ] 295 | } 296 | }, 297 | "required": [ 298 | "id", 299 | "object", 300 | "created", 301 | "model", 302 | "choices" 303 | ] 304 | }, 305 | "example": { 306 | "model": "davinci", 307 | "object": "text_completion", 308 | "id": "cmpl-4509KAos68kxOqpE2uYGw81j6m7uo", 309 | "created": 1637097562, 310 | "choices": [ 311 | { 312 | "index": 0, 313 | "text": "The price for bubblegum decreased on thursday.", 314 | "logprobs": null, 315 | "finish_reason": "stop" 316 | } 317 | ] 318 | } 319 | } 320 | }, 321 | "headers": { 322 | "apim-request-id": { 323 | "description": "Request ID for troubleshooting purposes", 324 | "schema": { 325 | "type": "string" 326 | } 327 | } 328 | } 329 | }, 330 | "default": { 331 | "description": "Service unavailable", 332 | "content": { 333 | "application/json": { 334 | "schema": { 335 | "$ref": "#/components/schemas/errorResponse" 336 | } 337 | } 338 | }, 339 | "headers": { 340 | "apim-request-id": { 341 | "description": "Request ID for troubleshooting purposes", 342 | "schema": { 343 | "type": "string" 344 | } 345 | } 346 | } 347 | } 348 | } 349 | } 350 | }, 351 | "/deployments/{deployment-id}/embeddings": { 352 | "post": { 353 | "summary": "Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms.", 354 | "operationId": "embeddings_create", 355 | "parameters": [ 356 | { 357 | "in": "path", 358 | "name": "deployment-id", 359 | "required": true, 360 | "schema": { 361 | "type": "string", 362 | "example": "ada-search-index-v1" 363 | }, 364 | "description": "The deployment id of the model which was deployed." 365 | }, 366 | { 367 | "in": "query", 368 | "name": "api-version", 369 | "required": true, 370 | "schema": { 371 | "type": "string", 372 | "example": "2023-05-15", 373 | "description": "api version" 374 | } 375 | } 376 | ], 377 | "requestBody": { 378 | "required": true, 379 | "content": { 380 | "application/json": { 381 | "schema": { 382 | "type": "object", 383 | "additionalProperties": true, 384 | "properties": { 385 | "input": { 386 | "description": "Input text to get embeddings for, encoded as a string. To get embeddings for multiple inputs in a single request, pass an array of strings. Each input must not exceed 2048 tokens in length.\nUnless you are embedding code, we suggest replacing newlines (\\n) in your input with a single space, as we have observed inferior results when newlines are present.", 387 | "oneOf": [ 388 | { 389 | "type": "string", 390 | "default": "", 391 | "example": "This is a test.", 392 | "nullable": true 393 | }, 394 | { 395 | "type": "array", 396 | "minItems": 1, 397 | "maxItems": 2048, 398 | "items": { 399 | "type": "string", 400 | "minLength": 1, 401 | "example": "This is a test.", 402 | "nullable": false 403 | } 404 | } 405 | ] 406 | }, 407 | "user": { 408 | "description": "A unique identifier representing your end-user, which can help monitoring and detecting abuse.", 409 | "type": "string", 410 | "nullable": false 411 | }, 412 | "input_type": { 413 | "description": "input type of embedding search to use", 414 | "type": "string", 415 | "example": "query" 416 | }, 417 | "model": { 418 | "type": "string", 419 | "description": "ID of the model to use. You can use the Models_List operation to see all of your available models, or see our Models_Get overview for descriptions of them.", 420 | "nullable": false 421 | } 422 | }, 423 | "required": [ 424 | "input" 425 | ] 426 | } 427 | } 428 | } 429 | }, 430 | "responses": { 431 | "200": { 432 | "description": "OK", 433 | "content": { 434 | "application/json": { 435 | "schema": { 436 | "type": "object", 437 | "properties": { 438 | "object": { 439 | "type": "string" 440 | }, 441 | "model": { 442 | "type": "string" 443 | }, 444 | "data": { 445 | "type": "array", 446 | "items": { 447 | "type": "object", 448 | "properties": { 449 | "index": { 450 | "type": "integer" 451 | }, 452 | "object": { 453 | "type": "string" 454 | }, 455 | "embedding": { 456 | "type": "array", 457 | "items": { 458 | "type": "number" 459 | } 460 | } 461 | }, 462 | "required": [ 463 | "index", 464 | "object", 465 | "embedding" 466 | ] 467 | } 468 | }, 469 | "usage": { 470 | "type": "object", 471 | "properties": { 472 | "prompt_tokens": { 473 | "type": "integer" 474 | }, 475 | "total_tokens": { 476 | "type": "integer" 477 | } 478 | }, 479 | "required": [ 480 | "prompt_tokens", 481 | "total_tokens" 482 | ] 483 | } 484 | }, 485 | "required": [ 486 | "object", 487 | "model", 488 | "data", 489 | "usage" 490 | ] 491 | } 492 | } 493 | } 494 | } 495 | } 496 | } 497 | }, 498 | "/deployments/{deployment-id}/chat/completions": { 499 | "post": { 500 | "summary": "Creates a completion for the chat message", 501 | "operationId": "ChatCompletions_Create", 502 | "parameters": [ 503 | { 504 | "in": "path", 505 | "name": "deployment-id", 506 | "required": true, 507 | "schema": { 508 | "type": "string", 509 | "description": "Deployment id of the model which was deployed." 510 | } 511 | }, 512 | { 513 | "in": "query", 514 | "name": "api-version", 515 | "required": true, 516 | "schema": { 517 | "type": "string", 518 | "example": "2023-05-15", 519 | "description": "api version" 520 | } 521 | } 522 | ], 523 | "requestBody": { 524 | "required": true, 525 | "content": { 526 | "application/json": { 527 | "schema": { 528 | "type": "object", 529 | "properties": { 530 | "messages": { 531 | "description": "The messages to generate chat completions for, in the chat format.", 532 | "type": "array", 533 | "minItems": 1, 534 | "items": { 535 | "type": "object", 536 | "properties": { 537 | "role": { 538 | "type": "string", 539 | "enum": [ 540 | "system", 541 | "user", 542 | "assistant" 543 | ], 544 | "description": "The role of the author of this message." 545 | }, 546 | "content": { 547 | "type": "string", 548 | "description": "The contents of the message" 549 | }, 550 | "name": { 551 | "type": "string", 552 | "description": "The name of the user in a multi-user chat" 553 | } 554 | }, 555 | "required": [ 556 | "role", 557 | "content" 558 | ] 559 | } 560 | }, 561 | "temperature": { 562 | "description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.", 563 | "type": "number", 564 | "minimum": 0, 565 | "maximum": 2, 566 | "default": 1, 567 | "example": 1, 568 | "nullable": true 569 | }, 570 | "top_p": { 571 | "description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\nWe generally recommend altering this or `temperature` but not both.", 572 | "type": "number", 573 | "minimum": 0, 574 | "maximum": 1, 575 | "default": 1, 576 | "example": 1, 577 | "nullable": true 578 | }, 579 | "n": { 580 | "description": "How many chat completion choices to generate for each input message.", 581 | "type": "integer", 582 | "minimum": 1, 583 | "maximum": 128, 584 | "default": 1, 585 | "example": 1, 586 | "nullable": true 587 | }, 588 | "stream": { 589 | "description": "If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a `data: [DONE]` message.", 590 | "type": "boolean", 591 | "nullable": true, 592 | "default": false 593 | }, 594 | "stop": { 595 | "description": "Up to 4 sequences where the API will stop generating further tokens.", 596 | "oneOf": [ 597 | { 598 | "type": "string", 599 | "nullable": true 600 | }, 601 | { 602 | "type": "array", 603 | "items": { 604 | "type": "string", 605 | "nullable": false 606 | }, 607 | "minItems": 1, 608 | "maxItems": 4, 609 | "description": "Array minimum size of 1 and maximum of 4" 610 | } 611 | ], 612 | "default": null 613 | }, 614 | "max_tokens": { 615 | "description": "The maximum number of tokens allowed for the generated answer. By default, the number of tokens the model can return will be (4096 - prompt tokens).", 616 | "type": "integer", 617 | "default": "inf" 618 | }, 619 | "presence_penalty": { 620 | "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", 621 | "type": "number", 622 | "default": 0, 623 | "minimum": -2, 624 | "maximum": 2 625 | }, 626 | "frequency_penalty": { 627 | "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", 628 | "type": "number", 629 | "default": 0, 630 | "minimum": -2, 631 | "maximum": 2 632 | }, 633 | "logit_bias": { 634 | "description": "Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.", 635 | "type": "object", 636 | "nullable": true 637 | }, 638 | "user": { 639 | "description": "A unique identifier representing your end-user, which can help Azure OpenAI to monitor and detect abuse.", 640 | "type": "string", 641 | "example": "user-1234", 642 | "nullable": false 643 | } 644 | }, 645 | "required": [ 646 | "messages" 647 | ] 648 | }, 649 | "example": { 650 | "model": "gpt-35-turbo", 651 | "messages": [ 652 | { 653 | "role": "user", 654 | "content": "Hello!" 655 | } 656 | ] 657 | } 658 | } 659 | } 660 | }, 661 | "responses": { 662 | "200": { 663 | "description": "OK", 664 | "content": { 665 | "application/json": { 666 | "schema": { 667 | "type": "object", 668 | "properties": { 669 | "id": { 670 | "type": "string" 671 | }, 672 | "object": { 673 | "type": "string" 674 | }, 675 | "created": { 676 | "type": "integer", 677 | "format": "unixtime" 678 | }, 679 | "model": { 680 | "type": "string" 681 | }, 682 | "choices": { 683 | "type": "array", 684 | "items": { 685 | "type": "object", 686 | "properties": { 687 | "index": { 688 | "type": "integer" 689 | }, 690 | "message": { 691 | "type": "object", 692 | "properties": { 693 | "role": { 694 | "type": "string", 695 | "enum": [ 696 | "system", 697 | "user", 698 | "assistant" 699 | ], 700 | "description": "The role of the author of this message." 701 | }, 702 | "content": { 703 | "type": "string", 704 | "description": "The contents of the message" 705 | } 706 | }, 707 | "required": [ 708 | "role", 709 | "content" 710 | ] 711 | }, 712 | "finish_reason": { 713 | "type": "string" 714 | } 715 | } 716 | } 717 | }, 718 | "usage": { 719 | "type": "object", 720 | "properties": { 721 | "prompt_tokens": { 722 | "type": "integer" 723 | }, 724 | "completion_tokens": { 725 | "type": "integer" 726 | }, 727 | "total_tokens": { 728 | "type": "integer" 729 | } 730 | }, 731 | "required": [ 732 | "prompt_tokens", 733 | "completion_tokens", 734 | "total_tokens" 735 | ] 736 | } 737 | }, 738 | "required": [ 739 | "id", 740 | "object", 741 | "created", 742 | "model", 743 | "choices" 744 | ] 745 | }, 746 | "example": { 747 | "id": "chatcmpl-123", 748 | "object": "chat.completion", 749 | "created": 1677652288, 750 | "choices": [ 751 | { 752 | "index": 0, 753 | "message": { 754 | "role": "assistant", 755 | "content": "\n\nHello there, how may I assist you today?" 756 | }, 757 | "finish_reason": "stop" 758 | } 759 | ], 760 | "usage": { 761 | "prompt_tokens": 9, 762 | "completion_tokens": 12, 763 | "total_tokens": 21 764 | } 765 | } 766 | } 767 | } 768 | } 769 | } 770 | } 771 | } 772 | }, 773 | "components": { 774 | "schemas": { 775 | "errorResponse": { 776 | "type": "object", 777 | "properties": { 778 | "error": { 779 | "type": "object", 780 | "properties": { 781 | "code": { 782 | "type": "string" 783 | }, 784 | "message": { 785 | "type": "string" 786 | }, 787 | "param": { 788 | "type": "string" 789 | }, 790 | "type": { 791 | "type": "string" 792 | } 793 | } 794 | } 795 | } 796 | } 797 | }, 798 | "securitySchemes": { 799 | "bearer": { 800 | "type": "oauth2", 801 | "flows": { 802 | "implicit": { 803 | "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", 804 | "scopes": {} 805 | } 806 | }, 807 | "x-tokenInfoFunc": "api.middleware.auth.bearer_auth", 808 | "x-scopeValidateFunc": "api.middleware.auth.validate_scopes" 809 | }, 810 | "apiKey": { 811 | "type": "apiKey", 812 | "name": "api-key", 813 | "in": "header" 814 | } 815 | } 816 | } 817 | } -------------------------------------------------------------------------------- /infra/modules/apim/policies/api_operation_policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | { 12 | "response": "{{body.choices[0].message.content}}" 13 | } 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /infra/modules/apim/policies/api_policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{openai-apikey}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /infra/modules/monitor/applicationinsights-dashboard.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param applicationInsightsName string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | // 2020-09-01-preview because that is the latest valid version 7 | resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { 8 | name: name 9 | location: location 10 | tags: tags 11 | properties: { 12 | lenses: [ 13 | { 14 | order: 0 15 | parts: [ 16 | { 17 | position: { 18 | x: 0 19 | y: 0 20 | colSpan: 2 21 | rowSpan: 1 22 | } 23 | metadata: { 24 | inputs: [ 25 | { 26 | name: 'id' 27 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 28 | } 29 | { 30 | name: 'Version' 31 | value: '1.0' 32 | } 33 | ] 34 | #disable-next-line BCP036 35 | type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' 36 | asset: { 37 | idInputName: 'id' 38 | type: 'ApplicationInsights' 39 | } 40 | defaultMenuItemId: 'overview' 41 | } 42 | } 43 | { 44 | position: { 45 | x: 2 46 | y: 0 47 | colSpan: 1 48 | rowSpan: 1 49 | } 50 | metadata: { 51 | inputs: [ 52 | { 53 | name: 'ComponentId' 54 | value: { 55 | Name: applicationInsights.name 56 | SubscriptionId: subscription().subscriptionId 57 | ResourceGroup: resourceGroup().name 58 | } 59 | } 60 | { 61 | name: 'Version' 62 | value: '1.0' 63 | } 64 | ] 65 | #disable-next-line BCP036 66 | type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' 67 | asset: { 68 | idInputName: 'ComponentId' 69 | type: 'ApplicationInsights' 70 | } 71 | defaultMenuItemId: 'ProactiveDetection' 72 | } 73 | } 74 | { 75 | position: { 76 | x: 3 77 | y: 0 78 | colSpan: 1 79 | rowSpan: 1 80 | } 81 | metadata: { 82 | inputs: [ 83 | { 84 | name: 'ComponentId' 85 | value: { 86 | Name: applicationInsights.name 87 | SubscriptionId: subscription().subscriptionId 88 | ResourceGroup: resourceGroup().name 89 | } 90 | } 91 | { 92 | name: 'ResourceId' 93 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 94 | } 95 | ] 96 | #disable-next-line BCP036 97 | type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' 98 | asset: { 99 | idInputName: 'ComponentId' 100 | type: 'ApplicationInsights' 101 | } 102 | } 103 | } 104 | { 105 | position: { 106 | x: 4 107 | y: 0 108 | colSpan: 1 109 | rowSpan: 1 110 | } 111 | metadata: { 112 | inputs: [ 113 | { 114 | name: 'ComponentId' 115 | value: { 116 | Name: applicationInsights.name 117 | SubscriptionId: subscription().subscriptionId 118 | ResourceGroup: resourceGroup().name 119 | } 120 | } 121 | { 122 | name: 'TimeContext' 123 | value: { 124 | durationMs: 86400000 125 | endTime: null 126 | createdTime: '2018-05-04T01:20:33.345Z' 127 | isInitialTime: true 128 | grain: 1 129 | useDashboardTimeRange: false 130 | } 131 | } 132 | { 133 | name: 'Version' 134 | value: '1.0' 135 | } 136 | ] 137 | #disable-next-line BCP036 138 | type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' 139 | asset: { 140 | idInputName: 'ComponentId' 141 | type: 'ApplicationInsights' 142 | } 143 | } 144 | } 145 | { 146 | position: { 147 | x: 5 148 | y: 0 149 | colSpan: 1 150 | rowSpan: 1 151 | } 152 | metadata: { 153 | inputs: [ 154 | { 155 | name: 'ComponentId' 156 | value: { 157 | Name: applicationInsights.name 158 | SubscriptionId: subscription().subscriptionId 159 | ResourceGroup: resourceGroup().name 160 | } 161 | } 162 | { 163 | name: 'TimeContext' 164 | value: { 165 | durationMs: 86400000 166 | endTime: null 167 | createdTime: '2018-05-08T18:47:35.237Z' 168 | isInitialTime: true 169 | grain: 1 170 | useDashboardTimeRange: false 171 | } 172 | } 173 | { 174 | name: 'ConfigurationId' 175 | value: '78ce933e-e864-4b05-a27b-71fd55a6afad' 176 | } 177 | ] 178 | #disable-next-line BCP036 179 | type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' 180 | asset: { 181 | idInputName: 'ComponentId' 182 | type: 'ApplicationInsights' 183 | } 184 | } 185 | } 186 | { 187 | position: { 188 | x: 0 189 | y: 1 190 | colSpan: 3 191 | rowSpan: 1 192 | } 193 | metadata: { 194 | inputs: [] 195 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 196 | settings: { 197 | content: { 198 | settings: { 199 | content: '# Usage' 200 | title: '' 201 | subtitle: '' 202 | } 203 | } 204 | } 205 | } 206 | } 207 | { 208 | position: { 209 | x: 3 210 | y: 1 211 | colSpan: 1 212 | rowSpan: 1 213 | } 214 | metadata: { 215 | inputs: [ 216 | { 217 | name: 'ComponentId' 218 | value: { 219 | Name: applicationInsights.name 220 | SubscriptionId: subscription().subscriptionId 221 | ResourceGroup: resourceGroup().name 222 | } 223 | } 224 | { 225 | name: 'TimeContext' 226 | value: { 227 | durationMs: 86400000 228 | endTime: null 229 | createdTime: '2018-05-04T01:22:35.782Z' 230 | isInitialTime: true 231 | grain: 1 232 | useDashboardTimeRange: false 233 | } 234 | } 235 | ] 236 | #disable-next-line BCP036 237 | type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' 238 | asset: { 239 | idInputName: 'ComponentId' 240 | type: 'ApplicationInsights' 241 | } 242 | } 243 | } 244 | { 245 | position: { 246 | x: 4 247 | y: 1 248 | colSpan: 3 249 | rowSpan: 1 250 | } 251 | metadata: { 252 | inputs: [] 253 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 254 | settings: { 255 | content: { 256 | settings: { 257 | content: '# Reliability' 258 | title: '' 259 | subtitle: '' 260 | } 261 | } 262 | } 263 | } 264 | } 265 | { 266 | position: { 267 | x: 7 268 | y: 1 269 | colSpan: 1 270 | rowSpan: 1 271 | } 272 | metadata: { 273 | inputs: [ 274 | { 275 | name: 'ResourceId' 276 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 277 | } 278 | { 279 | name: 'DataModel' 280 | value: { 281 | version: '1.0.0' 282 | timeContext: { 283 | durationMs: 86400000 284 | createdTime: '2018-05-04T23:42:40.072Z' 285 | isInitialTime: false 286 | grain: 1 287 | useDashboardTimeRange: false 288 | } 289 | } 290 | isOptional: true 291 | } 292 | { 293 | name: 'ConfigurationId' 294 | value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' 295 | isOptional: true 296 | } 297 | ] 298 | #disable-next-line BCP036 299 | type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' 300 | isAdapter: true 301 | asset: { 302 | idInputName: 'ResourceId' 303 | type: 'ApplicationInsights' 304 | } 305 | defaultMenuItemId: 'failures' 306 | } 307 | } 308 | { 309 | position: { 310 | x: 8 311 | y: 1 312 | colSpan: 3 313 | rowSpan: 1 314 | } 315 | metadata: { 316 | inputs: [] 317 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 318 | settings: { 319 | content: { 320 | settings: { 321 | content: '# Responsiveness\r\n' 322 | title: '' 323 | subtitle: '' 324 | } 325 | } 326 | } 327 | } 328 | } 329 | { 330 | position: { 331 | x: 11 332 | y: 1 333 | colSpan: 1 334 | rowSpan: 1 335 | } 336 | metadata: { 337 | inputs: [ 338 | { 339 | name: 'ResourceId' 340 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 341 | } 342 | { 343 | name: 'DataModel' 344 | value: { 345 | version: '1.0.0' 346 | timeContext: { 347 | durationMs: 86400000 348 | createdTime: '2018-05-04T23:43:37.804Z' 349 | isInitialTime: false 350 | grain: 1 351 | useDashboardTimeRange: false 352 | } 353 | } 354 | isOptional: true 355 | } 356 | { 357 | name: 'ConfigurationId' 358 | value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' 359 | isOptional: true 360 | } 361 | ] 362 | #disable-next-line BCP036 363 | type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' 364 | isAdapter: true 365 | asset: { 366 | idInputName: 'ResourceId' 367 | type: 'ApplicationInsights' 368 | } 369 | defaultMenuItemId: 'performance' 370 | } 371 | } 372 | { 373 | position: { 374 | x: 12 375 | y: 1 376 | colSpan: 3 377 | rowSpan: 1 378 | } 379 | metadata: { 380 | inputs: [] 381 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 382 | settings: { 383 | content: { 384 | settings: { 385 | content: '# Browser' 386 | title: '' 387 | subtitle: '' 388 | } 389 | } 390 | } 391 | } 392 | } 393 | { 394 | position: { 395 | x: 15 396 | y: 1 397 | colSpan: 1 398 | rowSpan: 1 399 | } 400 | metadata: { 401 | inputs: [ 402 | { 403 | name: 'ComponentId' 404 | value: { 405 | Name: applicationInsights.name 406 | SubscriptionId: subscription().subscriptionId 407 | ResourceGroup: resourceGroup().name 408 | } 409 | } 410 | { 411 | name: 'MetricsExplorerJsonDefinitionId' 412 | value: 'BrowserPerformanceTimelineMetrics' 413 | } 414 | { 415 | name: 'TimeContext' 416 | value: { 417 | durationMs: 86400000 418 | createdTime: '2018-05-08T12:16:27.534Z' 419 | isInitialTime: false 420 | grain: 1 421 | useDashboardTimeRange: false 422 | } 423 | } 424 | { 425 | name: 'CurrentFilter' 426 | value: { 427 | eventTypes: [ 428 | 4 429 | 1 430 | 3 431 | 5 432 | 2 433 | 6 434 | 13 435 | ] 436 | typeFacets: {} 437 | isPermissive: false 438 | } 439 | } 440 | { 441 | name: 'id' 442 | value: { 443 | Name: applicationInsights.name 444 | SubscriptionId: subscription().subscriptionId 445 | ResourceGroup: resourceGroup().name 446 | } 447 | } 448 | { 449 | name: 'Version' 450 | value: '1.0' 451 | } 452 | ] 453 | #disable-next-line BCP036 454 | type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' 455 | asset: { 456 | idInputName: 'ComponentId' 457 | type: 'ApplicationInsights' 458 | } 459 | defaultMenuItemId: 'browser' 460 | } 461 | } 462 | { 463 | position: { 464 | x: 0 465 | y: 2 466 | colSpan: 4 467 | rowSpan: 3 468 | } 469 | metadata: { 470 | inputs: [ 471 | { 472 | name: 'options' 473 | value: { 474 | chart: { 475 | metrics: [ 476 | { 477 | resourceMetadata: { 478 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 479 | } 480 | name: 'sessions/count' 481 | aggregationType: 5 482 | namespace: 'microsoft.insights/components/kusto' 483 | metricVisualization: { 484 | displayName: 'Sessions' 485 | color: '#47BDF5' 486 | } 487 | } 488 | { 489 | resourceMetadata: { 490 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 491 | } 492 | name: 'users/count' 493 | aggregationType: 5 494 | namespace: 'microsoft.insights/components/kusto' 495 | metricVisualization: { 496 | displayName: 'Users' 497 | color: '#7E58FF' 498 | } 499 | } 500 | ] 501 | title: 'Unique sessions and users' 502 | visualization: { 503 | chartType: 2 504 | legendVisualization: { 505 | isVisible: true 506 | position: 2 507 | hideSubtitle: false 508 | } 509 | axisVisualization: { 510 | x: { 511 | isVisible: true 512 | axisType: 2 513 | } 514 | y: { 515 | isVisible: true 516 | axisType: 1 517 | } 518 | } 519 | } 520 | openBladeOnClick: { 521 | openBlade: true 522 | destinationBlade: { 523 | extensionName: 'HubsExtension' 524 | bladeName: 'ResourceMenuBlade' 525 | parameters: { 526 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 527 | menuid: 'segmentationUsers' 528 | } 529 | } 530 | } 531 | } 532 | } 533 | } 534 | { 535 | name: 'sharedTimeRange' 536 | isOptional: true 537 | } 538 | ] 539 | #disable-next-line BCP036 540 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 541 | settings: {} 542 | } 543 | } 544 | { 545 | position: { 546 | x: 4 547 | y: 2 548 | colSpan: 4 549 | rowSpan: 3 550 | } 551 | metadata: { 552 | inputs: [ 553 | { 554 | name: 'options' 555 | value: { 556 | chart: { 557 | metrics: [ 558 | { 559 | resourceMetadata: { 560 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 561 | } 562 | name: 'requests/failed' 563 | aggregationType: 7 564 | namespace: 'microsoft.insights/components' 565 | metricVisualization: { 566 | displayName: 'Failed requests' 567 | color: '#EC008C' 568 | } 569 | } 570 | ] 571 | title: 'Failed requests' 572 | visualization: { 573 | chartType: 3 574 | legendVisualization: { 575 | isVisible: true 576 | position: 2 577 | hideSubtitle: false 578 | } 579 | axisVisualization: { 580 | x: { 581 | isVisible: true 582 | axisType: 2 583 | } 584 | y: { 585 | isVisible: true 586 | axisType: 1 587 | } 588 | } 589 | } 590 | openBladeOnClick: { 591 | openBlade: true 592 | destinationBlade: { 593 | extensionName: 'HubsExtension' 594 | bladeName: 'ResourceMenuBlade' 595 | parameters: { 596 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 597 | menuid: 'failures' 598 | } 599 | } 600 | } 601 | } 602 | } 603 | } 604 | { 605 | name: 'sharedTimeRange' 606 | isOptional: true 607 | } 608 | ] 609 | #disable-next-line BCP036 610 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 611 | settings: {} 612 | } 613 | } 614 | { 615 | position: { 616 | x: 8 617 | y: 2 618 | colSpan: 4 619 | rowSpan: 3 620 | } 621 | metadata: { 622 | inputs: [ 623 | { 624 | name: 'options' 625 | value: { 626 | chart: { 627 | metrics: [ 628 | { 629 | resourceMetadata: { 630 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 631 | } 632 | name: 'requests/duration' 633 | aggregationType: 4 634 | namespace: 'microsoft.insights/components' 635 | metricVisualization: { 636 | displayName: 'Server response time' 637 | color: '#00BCF2' 638 | } 639 | } 640 | ] 641 | title: 'Server response time' 642 | visualization: { 643 | chartType: 2 644 | legendVisualization: { 645 | isVisible: true 646 | position: 2 647 | hideSubtitle: false 648 | } 649 | axisVisualization: { 650 | x: { 651 | isVisible: true 652 | axisType: 2 653 | } 654 | y: { 655 | isVisible: true 656 | axisType: 1 657 | } 658 | } 659 | } 660 | openBladeOnClick: { 661 | openBlade: true 662 | destinationBlade: { 663 | extensionName: 'HubsExtension' 664 | bladeName: 'ResourceMenuBlade' 665 | parameters: { 666 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 667 | menuid: 'performance' 668 | } 669 | } 670 | } 671 | } 672 | } 673 | } 674 | { 675 | name: 'sharedTimeRange' 676 | isOptional: true 677 | } 678 | ] 679 | #disable-next-line BCP036 680 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 681 | settings: {} 682 | } 683 | } 684 | { 685 | position: { 686 | x: 12 687 | y: 2 688 | colSpan: 4 689 | rowSpan: 3 690 | } 691 | metadata: { 692 | inputs: [ 693 | { 694 | name: 'options' 695 | value: { 696 | chart: { 697 | metrics: [ 698 | { 699 | resourceMetadata: { 700 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 701 | } 702 | name: 'browserTimings/networkDuration' 703 | aggregationType: 4 704 | namespace: 'microsoft.insights/components' 705 | metricVisualization: { 706 | displayName: 'Page load network connect time' 707 | color: '#7E58FF' 708 | } 709 | } 710 | { 711 | resourceMetadata: { 712 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 713 | } 714 | name: 'browserTimings/processingDuration' 715 | aggregationType: 4 716 | namespace: 'microsoft.insights/components' 717 | metricVisualization: { 718 | displayName: 'Client processing time' 719 | color: '#44F1C8' 720 | } 721 | } 722 | { 723 | resourceMetadata: { 724 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 725 | } 726 | name: 'browserTimings/sendDuration' 727 | aggregationType: 4 728 | namespace: 'microsoft.insights/components' 729 | metricVisualization: { 730 | displayName: 'Send request time' 731 | color: '#EB9371' 732 | } 733 | } 734 | { 735 | resourceMetadata: { 736 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 737 | } 738 | name: 'browserTimings/receiveDuration' 739 | aggregationType: 4 740 | namespace: 'microsoft.insights/components' 741 | metricVisualization: { 742 | displayName: 'Receiving response time' 743 | color: '#0672F1' 744 | } 745 | } 746 | ] 747 | title: 'Average page load time breakdown' 748 | visualization: { 749 | chartType: 3 750 | legendVisualization: { 751 | isVisible: true 752 | position: 2 753 | hideSubtitle: false 754 | } 755 | axisVisualization: { 756 | x: { 757 | isVisible: true 758 | axisType: 2 759 | } 760 | y: { 761 | isVisible: true 762 | axisType: 1 763 | } 764 | } 765 | } 766 | } 767 | } 768 | } 769 | { 770 | name: 'sharedTimeRange' 771 | isOptional: true 772 | } 773 | ] 774 | #disable-next-line BCP036 775 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 776 | settings: {} 777 | } 778 | } 779 | { 780 | position: { 781 | x: 0 782 | y: 5 783 | colSpan: 4 784 | rowSpan: 3 785 | } 786 | metadata: { 787 | inputs: [ 788 | { 789 | name: 'options' 790 | value: { 791 | chart: { 792 | metrics: [ 793 | { 794 | resourceMetadata: { 795 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 796 | } 797 | name: 'availabilityResults/availabilityPercentage' 798 | aggregationType: 4 799 | namespace: 'microsoft.insights/components' 800 | metricVisualization: { 801 | displayName: 'Availability' 802 | color: '#47BDF5' 803 | } 804 | } 805 | ] 806 | title: 'Average availability' 807 | visualization: { 808 | chartType: 3 809 | legendVisualization: { 810 | isVisible: true 811 | position: 2 812 | hideSubtitle: false 813 | } 814 | axisVisualization: { 815 | x: { 816 | isVisible: true 817 | axisType: 2 818 | } 819 | y: { 820 | isVisible: true 821 | axisType: 1 822 | } 823 | } 824 | } 825 | openBladeOnClick: { 826 | openBlade: true 827 | destinationBlade: { 828 | extensionName: 'HubsExtension' 829 | bladeName: 'ResourceMenuBlade' 830 | parameters: { 831 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 832 | menuid: 'availability' 833 | } 834 | } 835 | } 836 | } 837 | } 838 | } 839 | { 840 | name: 'sharedTimeRange' 841 | isOptional: true 842 | } 843 | ] 844 | #disable-next-line BCP036 845 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 846 | settings: {} 847 | } 848 | } 849 | { 850 | position: { 851 | x: 4 852 | y: 5 853 | colSpan: 4 854 | rowSpan: 3 855 | } 856 | metadata: { 857 | inputs: [ 858 | { 859 | name: 'options' 860 | value: { 861 | chart: { 862 | metrics: [ 863 | { 864 | resourceMetadata: { 865 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 866 | } 867 | name: 'exceptions/server' 868 | aggregationType: 7 869 | namespace: 'microsoft.insights/components' 870 | metricVisualization: { 871 | displayName: 'Server exceptions' 872 | color: '#47BDF5' 873 | } 874 | } 875 | { 876 | resourceMetadata: { 877 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 878 | } 879 | name: 'dependencies/failed' 880 | aggregationType: 7 881 | namespace: 'microsoft.insights/components' 882 | metricVisualization: { 883 | displayName: 'Dependency failures' 884 | color: '#7E58FF' 885 | } 886 | } 887 | ] 888 | title: 'Server exceptions and Dependency failures' 889 | visualization: { 890 | chartType: 2 891 | legendVisualization: { 892 | isVisible: true 893 | position: 2 894 | hideSubtitle: false 895 | } 896 | axisVisualization: { 897 | x: { 898 | isVisible: true 899 | axisType: 2 900 | } 901 | y: { 902 | isVisible: true 903 | axisType: 1 904 | } 905 | } 906 | } 907 | } 908 | } 909 | } 910 | { 911 | name: 'sharedTimeRange' 912 | isOptional: true 913 | } 914 | ] 915 | #disable-next-line BCP036 916 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 917 | settings: {} 918 | } 919 | } 920 | { 921 | position: { 922 | x: 8 923 | y: 5 924 | colSpan: 4 925 | rowSpan: 3 926 | } 927 | metadata: { 928 | inputs: [ 929 | { 930 | name: 'options' 931 | value: { 932 | chart: { 933 | metrics: [ 934 | { 935 | resourceMetadata: { 936 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 937 | } 938 | name: 'performanceCounters/processorCpuPercentage' 939 | aggregationType: 4 940 | namespace: 'microsoft.insights/components' 941 | metricVisualization: { 942 | displayName: 'Processor time' 943 | color: '#47BDF5' 944 | } 945 | } 946 | { 947 | resourceMetadata: { 948 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 949 | } 950 | name: 'performanceCounters/processCpuPercentage' 951 | aggregationType: 4 952 | namespace: 'microsoft.insights/components' 953 | metricVisualization: { 954 | displayName: 'Process CPU' 955 | color: '#7E58FF' 956 | } 957 | } 958 | ] 959 | title: 'Average processor and process CPU utilization' 960 | visualization: { 961 | chartType: 2 962 | legendVisualization: { 963 | isVisible: true 964 | position: 2 965 | hideSubtitle: false 966 | } 967 | axisVisualization: { 968 | x: { 969 | isVisible: true 970 | axisType: 2 971 | } 972 | y: { 973 | isVisible: true 974 | axisType: 1 975 | } 976 | } 977 | } 978 | } 979 | } 980 | } 981 | { 982 | name: 'sharedTimeRange' 983 | isOptional: true 984 | } 985 | ] 986 | #disable-next-line BCP036 987 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 988 | settings: {} 989 | } 990 | } 991 | { 992 | position: { 993 | x: 12 994 | y: 5 995 | colSpan: 4 996 | rowSpan: 3 997 | } 998 | metadata: { 999 | inputs: [ 1000 | { 1001 | name: 'options' 1002 | value: { 1003 | chart: { 1004 | metrics: [ 1005 | { 1006 | resourceMetadata: { 1007 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1008 | } 1009 | name: 'exceptions/browser' 1010 | aggregationType: 7 1011 | namespace: 'microsoft.insights/components' 1012 | metricVisualization: { 1013 | displayName: 'Browser exceptions' 1014 | color: '#47BDF5' 1015 | } 1016 | } 1017 | ] 1018 | title: 'Browser exceptions' 1019 | visualization: { 1020 | chartType: 2 1021 | legendVisualization: { 1022 | isVisible: true 1023 | position: 2 1024 | hideSubtitle: false 1025 | } 1026 | axisVisualization: { 1027 | x: { 1028 | isVisible: true 1029 | axisType: 2 1030 | } 1031 | y: { 1032 | isVisible: true 1033 | axisType: 1 1034 | } 1035 | } 1036 | } 1037 | } 1038 | } 1039 | } 1040 | { 1041 | name: 'sharedTimeRange' 1042 | isOptional: true 1043 | } 1044 | ] 1045 | #disable-next-line BCP036 1046 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1047 | settings: {} 1048 | } 1049 | } 1050 | { 1051 | position: { 1052 | x: 0 1053 | y: 8 1054 | colSpan: 4 1055 | rowSpan: 3 1056 | } 1057 | metadata: { 1058 | inputs: [ 1059 | { 1060 | name: 'options' 1061 | value: { 1062 | chart: { 1063 | metrics: [ 1064 | { 1065 | resourceMetadata: { 1066 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1067 | } 1068 | name: 'availabilityResults/count' 1069 | aggregationType: 7 1070 | namespace: 'microsoft.insights/components' 1071 | metricVisualization: { 1072 | displayName: 'Availability test results count' 1073 | color: '#47BDF5' 1074 | } 1075 | } 1076 | ] 1077 | title: 'Availability test results count' 1078 | visualization: { 1079 | chartType: 2 1080 | legendVisualization: { 1081 | isVisible: true 1082 | position: 2 1083 | hideSubtitle: false 1084 | } 1085 | axisVisualization: { 1086 | x: { 1087 | isVisible: true 1088 | axisType: 2 1089 | } 1090 | y: { 1091 | isVisible: true 1092 | axisType: 1 1093 | } 1094 | } 1095 | } 1096 | } 1097 | } 1098 | } 1099 | { 1100 | name: 'sharedTimeRange' 1101 | isOptional: true 1102 | } 1103 | ] 1104 | #disable-next-line BCP036 1105 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1106 | settings: {} 1107 | } 1108 | } 1109 | { 1110 | position: { 1111 | x: 4 1112 | y: 8 1113 | colSpan: 4 1114 | rowSpan: 3 1115 | } 1116 | metadata: { 1117 | inputs: [ 1118 | { 1119 | name: 'options' 1120 | value: { 1121 | chart: { 1122 | metrics: [ 1123 | { 1124 | resourceMetadata: { 1125 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1126 | } 1127 | name: 'performanceCounters/processIOBytesPerSecond' 1128 | aggregationType: 4 1129 | namespace: 'microsoft.insights/components' 1130 | metricVisualization: { 1131 | displayName: 'Process IO rate' 1132 | color: '#47BDF5' 1133 | } 1134 | } 1135 | ] 1136 | title: 'Average process I/O rate' 1137 | visualization: { 1138 | chartType: 2 1139 | legendVisualization: { 1140 | isVisible: true 1141 | position: 2 1142 | hideSubtitle: false 1143 | } 1144 | axisVisualization: { 1145 | x: { 1146 | isVisible: true 1147 | axisType: 2 1148 | } 1149 | y: { 1150 | isVisible: true 1151 | axisType: 1 1152 | } 1153 | } 1154 | } 1155 | } 1156 | } 1157 | } 1158 | { 1159 | name: 'sharedTimeRange' 1160 | isOptional: true 1161 | } 1162 | ] 1163 | #disable-next-line BCP036 1164 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1165 | settings: {} 1166 | } 1167 | } 1168 | { 1169 | position: { 1170 | x: 8 1171 | y: 8 1172 | colSpan: 4 1173 | rowSpan: 3 1174 | } 1175 | metadata: { 1176 | inputs: [ 1177 | { 1178 | name: 'options' 1179 | value: { 1180 | chart: { 1181 | metrics: [ 1182 | { 1183 | resourceMetadata: { 1184 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1185 | } 1186 | name: 'performanceCounters/memoryAvailableBytes' 1187 | aggregationType: 4 1188 | namespace: 'microsoft.insights/components' 1189 | metricVisualization: { 1190 | displayName: 'Available memory' 1191 | color: '#47BDF5' 1192 | } 1193 | } 1194 | ] 1195 | title: 'Average available memory' 1196 | visualization: { 1197 | chartType: 2 1198 | legendVisualization: { 1199 | isVisible: true 1200 | position: 2 1201 | hideSubtitle: false 1202 | } 1203 | axisVisualization: { 1204 | x: { 1205 | isVisible: true 1206 | axisType: 2 1207 | } 1208 | y: { 1209 | isVisible: true 1210 | axisType: 1 1211 | } 1212 | } 1213 | } 1214 | } 1215 | } 1216 | } 1217 | { 1218 | name: 'sharedTimeRange' 1219 | isOptional: true 1220 | } 1221 | ] 1222 | #disable-next-line BCP036 1223 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1224 | settings: {} 1225 | } 1226 | } 1227 | ] 1228 | } 1229 | ] 1230 | } 1231 | } 1232 | 1233 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 1234 | name: applicationInsightsName 1235 | } 1236 | -------------------------------------------------------------------------------- /infra/modules/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param dashboardName string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param logAnalyticsWorkspaceId string 7 | param privateLinkScopeName string 8 | param vNetName string 9 | param privateEndpointSubnetName string 10 | param dnsZoneName string 11 | param privateEndpointName string 12 | 13 | resource privateLinkScope 'microsoft.insights/privateLinkScopes@2021-07-01-preview' existing = { 14 | name: privateLinkScopeName 15 | } 16 | 17 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 18 | name: name 19 | location: location 20 | tags: union(tags, { 'azd-service-name': name }) 21 | kind: 'web' 22 | properties: { 23 | Application_Type: 'web' 24 | WorkspaceResourceId: logAnalyticsWorkspaceId 25 | publicNetworkAccessForIngestion: 'Disabled' 26 | publicNetworkAccessForQuery: 'Enabled' 27 | } 28 | } 29 | 30 | module privateEndpoint '../networking/private-endpoint.bicep' = { 31 | name: '${applicationInsights.name}-privateEndpoint-deployment' 32 | params: { 33 | groupIds: [ 34 | 'azuremonitor' 35 | ] 36 | dnsZoneName: dnsZoneName 37 | name: privateEndpointName 38 | subnetName: privateEndpointSubnetName 39 | privateLinkServiceId: privateLinkScope.id 40 | vNetName: vNetName 41 | location: location 42 | } 43 | } 44 | 45 | resource appInsightsScopedResource 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { 46 | parent: privateLinkScope 47 | name: '${applicationInsights.name}-connection' 48 | properties: { 49 | linkedResourceId: applicationInsights.id 50 | } 51 | } 52 | 53 | module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = { 54 | name: 'application-insights-dashboard' 55 | params: { 56 | name: dashboardName 57 | location: location 58 | applicationInsightsName: applicationInsights.name 59 | } 60 | } 61 | 62 | output connectionString string = applicationInsights.properties.ConnectionString 63 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 64 | output name string = applicationInsights.name 65 | -------------------------------------------------------------------------------- /infra/modules/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param privateLinkScopeName string 5 | 6 | resource privateLinkScope 'microsoft.insights/privateLinkScopes@2021-07-01-preview' existing = { 7 | name: privateLinkScopeName 8 | } 9 | 10 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 11 | name: name 12 | location: location 13 | tags: union(tags, { 'azd-service-name': name }) 14 | properties: any({ 15 | retentionInDays: 30 16 | features: { 17 | searchVersion: 1 18 | } 19 | sku: { 20 | name: 'PerGB2018' 21 | } 22 | publicNetworkAccessForIngestion: 'Disabled' 23 | publicNetworkAccessForQuery: 'Enabled' 24 | }) 25 | } 26 | 27 | resource logAnalyticsScopedResource 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { 28 | parent: privateLinkScope 29 | name: '${logAnalytics.name}-connection' 30 | properties: { 31 | linkedResourceId: logAnalytics.id 32 | } 33 | } 34 | 35 | output id string = logAnalytics.id 36 | output name string = logAnalytics.name 37 | -------------------------------------------------------------------------------- /infra/modules/monitor/monitoring.bicep: -------------------------------------------------------------------------------- 1 | param logAnalyticsName string 2 | param applicationInsightsName string 3 | param applicationInsightsDashboardName string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | param vNetName string 7 | param privateEndpointSubnetName string 8 | param applicationInsightsDnsZoneName string 9 | param applicationInsightsPrivateEndpointName string 10 | 11 | var privateLinkScopeName = 'private-link-scope' 12 | 13 | resource privateLinkScope 'microsoft.insights/privateLinkScopes@2021-07-01-preview' = { 14 | name: privateLinkScopeName 15 | location: 'global' 16 | properties: { 17 | accessModeSettings: { 18 | ingestionAccessMode: 'Open' 19 | queryAccessMode: 'Open' 20 | } 21 | } 22 | } 23 | 24 | module logAnalytics 'loganalytics.bicep' = { 25 | name: 'log-analytics' 26 | params: { 27 | name: logAnalyticsName 28 | location: location 29 | tags: tags 30 | privateLinkScopeName: privateLinkScopeName 31 | } 32 | } 33 | 34 | module applicationInsights 'applicationinsights.bicep' = { 35 | name: 'application-insights' 36 | params: { 37 | name: applicationInsightsName 38 | location: location 39 | tags: tags 40 | dashboardName: applicationInsightsDashboardName 41 | logAnalyticsWorkspaceId: logAnalytics.outputs.id 42 | privateLinkScopeName: privateLinkScopeName 43 | vNetName: vNetName 44 | privateEndpointSubnetName: privateEndpointSubnetName 45 | dnsZoneName: applicationInsightsDnsZoneName 46 | privateEndpointName: applicationInsightsPrivateEndpointName 47 | } 48 | } 49 | 50 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString 51 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey 52 | output applicationInsightsName string = applicationInsights.outputs.name 53 | output logAnalyticsWorkspaceId string = logAnalytics.outputs.id 54 | output logAnalyticsWorkspaceName string = logAnalytics.outputs.name 55 | -------------------------------------------------------------------------------- /infra/modules/networking/dns.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param tags object = {} 3 | 4 | resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 5 | name: name 6 | location: 'global' 7 | tags: union(tags, { 'azd-service-name': name }) 8 | } 9 | 10 | output privateDnsZoneName string = privateDnsZone.name 11 | -------------------------------------------------------------------------------- /infra/modules/networking/private-endpoint.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param subnetName string 3 | param vNetName string 4 | param privateLinkServiceId string 5 | param groupIds array 6 | param dnsZoneName string 7 | param location string 8 | 9 | resource privateEndpointSubnet 'Microsoft.Network/virtualNetworks/subnets@2022-09-01' existing = { 10 | name: '${vNetName}/${subnetName}' 11 | } 12 | 13 | resource privateEndpointDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { 14 | name: dnsZoneName 15 | } 16 | 17 | resource privateEndpoint 'Microsoft.Network/privateEndpoints@2022-09-01' = { 18 | name: name 19 | location: location 20 | properties: { 21 | subnet: { 22 | id: privateEndpointSubnet.id 23 | } 24 | privateLinkServiceConnections: [ 25 | { 26 | name: name 27 | properties: { 28 | privateLinkServiceId: privateLinkServiceId 29 | groupIds: groupIds 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | 36 | resource privateEndpointDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-09-01' = { 37 | parent: privateEndpoint 38 | name: 'privateDnsZoneGroup' 39 | properties: { 40 | privateDnsZoneConfigs: [ 41 | { 42 | name: 'default' 43 | properties: { 44 | privateDnsZoneId: privateEndpointDnsZone.id 45 | } 46 | } 47 | ] 48 | } 49 | } 50 | 51 | output privateEndpointName string = privateEndpoint.name 52 | -------------------------------------------------------------------------------- /infra/modules/networking/vnet.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param apimSubnetName string 4 | param apimNsgName string 5 | param privateEndpointSubnetName string 6 | param privateEndpointNsgName string 7 | param privateDnsZoneNames array 8 | param tags object = {} 9 | 10 | resource apimNsg 'Microsoft.Network/networkSecurityGroups@2020-07-01' = { 11 | name: apimNsgName 12 | location: location 13 | tags: union(tags, { 'azd-service-name': apimNsgName }) 14 | properties: { 15 | securityRules: [ 16 | { 17 | name: 'AllowClientToGateway' 18 | properties: { 19 | protocol: 'Tcp' 20 | sourcePortRange: '*' 21 | destinationPortRange: '443' 22 | sourceAddressPrefix: 'Internet' 23 | destinationAddressPrefix: 'VirtualNetwork' 24 | access: 'Allow' 25 | priority: 2721 26 | direction: 'Inbound' 27 | } 28 | } 29 | { 30 | name: 'AllowAPIMPortal' 31 | properties: { 32 | protocol: 'Tcp' 33 | sourcePortRange: '*' 34 | destinationPortRange: '3443' 35 | sourceAddressPrefix: 'ApiManagement' 36 | destinationAddressPrefix: 'VirtualNetwork' 37 | access: 'Allow' 38 | priority: 2731 39 | direction: 'Inbound' 40 | } 41 | } 42 | { 43 | name: 'AllowAPIMLoadBalancer' 44 | properties: { 45 | protocol: '*' 46 | sourcePortRange: '*' 47 | destinationPortRange: '6390' 48 | sourceAddressPrefix: 'AzureLoadBalancer' 49 | destinationAddressPrefix: 'VirtualNetwork' 50 | access: 'Allow' 51 | priority: 2741 52 | direction: 'Inbound' 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | 59 | resource privateEndpointNsg 'Microsoft.Network/networkSecurityGroups@2020-07-01' = { 60 | name: privateEndpointNsgName 61 | location: location 62 | tags: union(tags, { 'azd-service-name': privateEndpointNsgName }) 63 | properties: { 64 | securityRules: [] 65 | } 66 | } 67 | 68 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' = { 69 | name: name 70 | location: location 71 | tags: union(tags, { 'azd-service-name': name }) 72 | properties: { 73 | addressSpace: { 74 | addressPrefixes: [ 75 | '10.0.0.0/16' 76 | ] 77 | } 78 | subnets: [ 79 | { 80 | name: 'default' 81 | properties: { 82 | addressPrefix: '10.0.0.0/24' 83 | } 84 | } 85 | { 86 | name: apimSubnetName 87 | properties: { 88 | addressPrefix: '10.0.1.0/24' 89 | networkSecurityGroup: apimNsg.id == '' ? null : { 90 | id: apimNsg.id 91 | } 92 | } 93 | } 94 | { 95 | name: privateEndpointSubnetName 96 | properties: { 97 | addressPrefix: '10.0.2.0/24' 98 | networkSecurityGroup: privateEndpointNsg.id == '' ? null : { 99 | id: privateEndpointNsg.id 100 | } 101 | } 102 | } 103 | ] 104 | } 105 | 106 | resource defaultSubnet 'subnets' existing = { 107 | name: 'default' 108 | } 109 | 110 | resource apimSubnet 'subnets' existing = { 111 | name: apimSubnetName 112 | } 113 | 114 | resource privateEndpointSubnet 'subnets' existing = { 115 | name: privateEndpointSubnetName 116 | } 117 | } 118 | 119 | resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = [for privateDnsZoneName in privateDnsZoneNames: { 120 | name: '${privateDnsZoneName}/privateDnsZoneLink' 121 | location: 'global' 122 | properties: { 123 | virtualNetwork: { 124 | id: virtualNetwork.id 125 | } 126 | registrationEnabled: false 127 | } 128 | }] 129 | 130 | output virtualNetworkId string = virtualNetwork.id 131 | output vnetName string = virtualNetwork.name 132 | output apimSubnetName string = virtualNetwork::apimSubnet.name 133 | output apimSubnetId string = virtualNetwork::apimSubnet.id 134 | output privateEndpointSubnetName string = virtualNetwork::privateEndpointSubnet.name 135 | output privateEndpointSubnetId string = virtualNetwork::privateEndpointSubnet.id 136 | -------------------------------------------------------------------------------- /infra/modules/security/assignment.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param policyAssignmentName string = 'audit-cogs-disable-public-access' 6 | param policyDefinitionID string = '/providers/Microsoft.Authorization/policyDefinitions/0725b4dd-7e76-479c-a735-68e7ee23d5ca' 7 | 8 | resource assignment 'Microsoft.Authorization/policyAssignments@2021-09-01' = { 9 | name: policyAssignmentName 10 | scope: subscriptionResourceId('Microsoft.Resources/resourceGroups', resourceGroup().name) 11 | tags: union(tags, { 'azd-service-name': name }) 12 | properties: { 13 | policyDefinitionId: policyDefinitionID 14 | } 15 | } 16 | 17 | output assignmentId string = assignment.id 18 | -------------------------------------------------------------------------------- /infra/modules/security/key-vault.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param logAnalyticsWorkspaceName string 4 | param managedIdentityName string 5 | param keyVaultPrivateEndpointName string 6 | param vNetName string 7 | param privateEndpointSubnetName string 8 | param keyVaultDnsZoneName string 9 | param publicNetworkAccess string = 'Disabled' 10 | param tags object = {} 11 | 12 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = { 13 | name: managedIdentityName 14 | } 15 | 16 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { 17 | name: name 18 | location: location 19 | tags: union(tags, { 'azd-service-name': name }) 20 | properties: { 21 | sku: { 22 | family: 'A' 23 | name: 'standard' 24 | } 25 | tenantId: subscription().tenantId 26 | enableRbacAuthorization: false 27 | enabledForTemplateDeployment: true 28 | publicNetworkAccess: publicNetworkAccess 29 | accessPolicies: [ 30 | { 31 | objectId: managedIdentity.properties.principalId 32 | tenantId: managedIdentity.properties.tenantId 33 | permissions: { 34 | secrets: [ 35 | 'get' 36 | 'list' 37 | ] 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | 44 | module privateEndpoint '../networking/private-endpoint.bicep' = { 45 | name: '${keyVault.name}-privateEndpoint-deployment' 46 | params: { 47 | groupIds: [ 48 | 'vault' 49 | ] 50 | dnsZoneName: keyVaultDnsZoneName 51 | name: keyVaultPrivateEndpointName 52 | subnetName: privateEndpointSubnetName 53 | privateLinkServiceId: keyVault.id 54 | vNetName: vNetName 55 | location: location 56 | } 57 | } 58 | 59 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' existing = { 60 | name: logAnalyticsWorkspaceName 61 | } 62 | 63 | resource diagnosticSettings 'Microsoft.Insights/diagnosticsettings@2017-05-01-preview' = { 64 | name: 'Logging' 65 | scope: keyVault 66 | properties: { 67 | workspaceId: logAnalyticsWorkspace.id 68 | logs: [ 69 | { 70 | category: 'AuditEvent' 71 | enabled: true 72 | } 73 | { 74 | category: 'AzurePolicyEvaluationDetails' 75 | enabled: true 76 | } 77 | ] 78 | metrics: [ 79 | { 80 | category: 'AllMetrics' 81 | enabled: true 82 | } 83 | ] 84 | } 85 | } 86 | 87 | output keyVaultName string = keyVault.name 88 | output keyVaultResourceId string = keyVault.id 89 | output keyVaultEndpoint string = keyVault.properties.vaultUri 90 | -------------------------------------------------------------------------------- /infra/modules/security/keyvault-secret.bicep: -------------------------------------------------------------------------------- 1 | param keyVaultName string = '' 2 | param secretName string 3 | param openAiName string 4 | 5 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 6 | name: keyVaultName 7 | } 8 | 9 | resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { 10 | name: openAiName 11 | } 12 | 13 | resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { 14 | parent: keyVault 15 | name: secretName 16 | properties: { 17 | value: account.listKeys().key1 18 | } 19 | } 20 | 21 | output keyVaultSecretName string = keyVaultSecret.name 22 | -------------------------------------------------------------------------------- /infra/modules/security/managed-identity.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { 6 | name: name 7 | location: location 8 | tags: union(tags, { 'azd-service-name': name }) 9 | } 10 | 11 | output managedIdentityName string = managedIdentity.name 12 | -------------------------------------------------------------------------------- /tests.http: -------------------------------------------------------------------------------- 1 | @tenantId = 2 | @scope = user_impersonation 3 | @clientId = 4 | @clientSecret = 5 | @apimName = 6 | @apiPath = openai 7 | @azureOpenAIDeploymentId = chat 8 | @azureOpenAIApiVersion = 2023-03-15-preview 9 | @prompt = "You are a helpful assistant." 10 | @subscriptionKey = 11 | 12 | "stream":true 13 | 14 | ### Test Azure API Management endpoint, chat completion without OAuth2 15 | POST https://{{apimName}}.azure-api.net/{{apiPath}}/deployments/{{azureOpenAIDeploymentId}}/chat/completions?api-version={{azureOpenAIApiVersion}} 16 | Content-Type: application/json 17 | Ocp-Apim-Subscription-Key: {{subscriptionKey}} 18 | 19 | { 20 | "messages": [ 21 | { 22 | "role": "user", 23 | "content": {{prompt}} 24 | } 25 | ] 26 | } 27 | 28 | ### Get Token for consumer for OAuth2 29 | # @name consumerToken 30 | 31 | POST https://login.microsoftonline.com/{{tenantId}}/oauth2/token HTTP/1.1 32 | Content-Type: application/x-www-form-urlencoded 33 | 34 | client_id={{clientId}} 35 | &scope={{scope}} 36 | &client_secret={{clientSecret}} 37 | &grant_type=client_credentials 38 | 39 | #### Get Token Response 40 | @accessToken = {{consumerToken.response.body.$.access_token}} 41 | 42 | ### Test Azure API Management endpoint, chat completion with OAuth2 43 | POST https://{{apimName}}.azure-api.net/{{apiPath}}/deployments/{{azureOpenAIDeploymentId}}/chat/completions?api-version={{azureOpenAIApiVersion}} 44 | Content-Type: application/json 45 | Authorization: Bearer {{accessToken}} 46 | 47 | { 48 | "messages": [ 49 | { 50 | "role": "user", 51 | "content": {{prompt}} 52 | } 53 | ] 54 | } --------------------------------------------------------------------------------