├── .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",
150 | "\t",
151 | "\t",
152 | "\t\t",
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 | [](https://codespaces.new/pascalvanderheiden/ais-apim-openai)
6 | [](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 | [](https://aka.ms/awesome-azd)
10 | `azd`
11 |
12 | ## Build Status
13 |
14 | | GitHub Action | Status |
15 | | ----------- | ----------- |
16 | | `azd` Deploy | [](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 | 
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 | }
--------------------------------------------------------------------------------