├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── .funcignore
├── .github
├── dependabot-bot.yaml
├── dependabot.yaml
└── workflows
│ ├── azure-dev-validate.yaml
│ ├── azure-dev.yaml
│ └── python-check.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── LICENSE.md
├── README.md
├── api
├── __init__.py
├── fastapi_app.py
└── fastapi_routes.py
├── azure.yaml
├── function_app.py
├── host.json
├── infra
├── apimanagement.bicep
├── app-diagnostics.bicep
├── appinsightsdashboard.bicep
├── core
│ ├── database
│ │ ├── cosmos
│ │ │ ├── cosmos-account.bicep
│ │ │ ├── mongo
│ │ │ │ ├── cosmos-mongo-account.bicep
│ │ │ │ └── cosmos-mongo-db.bicep
│ │ │ └── sql
│ │ │ │ ├── cosmos-sql-account.bicep
│ │ │ │ ├── cosmos-sql-db.bicep
│ │ │ │ ├── cosmos-sql-role-assign.bicep
│ │ │ │ └── cosmos-sql-role-def.bicep
│ │ └── sqlserver
│ │ │ └── sqlserver.bicep
│ ├── gateway
│ │ ├── apim-api-policy.xml
│ │ └── apim.bicep
│ ├── host
│ │ ├── appservice-appsettings.bicep
│ │ ├── appservice.bicep
│ │ ├── appserviceplan.bicep
│ │ └── functions.bicep
│ ├── monitor
│ │ ├── applicationinsights-dashboard.bicep
│ │ ├── applicationinsights.bicep
│ │ ├── loganalytics.bicep
│ │ └── monitoring.bicep
│ ├── security
│ │ ├── keyvault-access.bicep
│ │ ├── keyvault-secret.bicep
│ │ └── keyvault.bicep
│ └── storage
│ │ └── storage-account.bicep
├── main.bicep
└── main.parameters.json
├── local.settings.json
├── pyproject.toml
├── readme_diagram_apim.png
├── requirements-dev.txt
├── requirements.txt
└── tests
├── test_env.py
└── test_fastapi.py
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG IMAGE=bullseye
2 | FROM --platform=amd64 mcr.microsoft.com/devcontainers/${IMAGE}
3 | RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \
4 | && mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \
5 | && sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/debian/$(lsb_release -rs | cut -d'.' -f 1)/prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' \
6 | && apt-get update && apt-get install -y azure-functions-core-tools-4 \
7 | && apt-get clean -y && rm -rf /var/lib/apt/lists/*
8 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fastapi-azure-function-apim",
3 | "build": {
4 | "dockerfile": "Dockerfile",
5 | "args": {
6 | "IMAGE": "python:3.10-bullseye"
7 | },
8 | "context": ".."
9 | },
10 | "features": {
11 | "ghcr.io/devcontainers/features/azure-cli:1": {
12 | "version": "2.38"
13 | },
14 | "ghcr.io/devcontainers/features/node:1": {
15 | "version": "16",
16 | "nodeGypDependencies": false
17 | },
18 | "ghcr.io/azure/azure-dev/azd:latest": {}
19 | },
20 | "customizations": {
21 | "vscode": {
22 | "extensions": [
23 | "ms-azuretools.azure-dev",
24 | "ms-azuretools.vscode-bicep",
25 | "ms-vscode.vscode-node-azure-pack",
26 | "ms-python.python",
27 | "ms-azuretools.vscode-azurefunctions"
28 | ]
29 | }
30 | },
31 | "forwardPorts": [
32 | 8000,
33 | 7071
34 | ],
35 | "postCreateCommand": "python3 -m pip install --user -r requirements-dev.txt && pre-commit install",
36 | "remoteUser": "vscode",
37 | "hostRequirements": {
38 | "memory": "8gb"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.funcignore:
--------------------------------------------------------------------------------
1 | .git*
2 | .vscode
3 | local.settings.json
4 | test
5 | .venv
6 |
--------------------------------------------------------------------------------
/.github/dependabot-bot.yaml:
--------------------------------------------------------------------------------
1 | safe:
2 | - azure-functions
3 | - fastapi
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 | - package-ecosystem: "pip" # See documentation for possible values
13 | directory: "/" # Location of package manifests
14 | schedule:
15 | interval: "weekly"
16 |
--------------------------------------------------------------------------------
/.github/workflows/azure-dev-validate.yaml:
--------------------------------------------------------------------------------
1 | name: Validate bicep scripts
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | - master
8 | pull_request:
9 | branches:
10 | - main
11 | - master
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Build Bicep for linting
21 | uses: azure/CLI@v2
22 | with:
23 | inlineScript: |
24 | export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
25 | az config set bicep.use_binary_from_path=false && az bicep build -f infra/main.bicep --stdout
26 |
--------------------------------------------------------------------------------
/.github/workflows/azure-dev.yaml:
--------------------------------------------------------------------------------
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 |
9 | # GitHub Actions workflow to deploy to Azure using azd
10 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config`
11 |
12 | # Set up permissions for deploying with secretless Azure federated credentials
13 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication
14 | permissions:
15 | id-token: write
16 | contents: read
17 |
18 | jobs:
19 | build:
20 | runs-on: ubuntu-latest
21 | env:
22 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
23 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
24 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
25 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v4
29 |
30 | - name: Install azd
31 | uses: Azure/setup-azd@v1.0.0
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 |
--------------------------------------------------------------------------------
/.github/workflows/python-check.yaml:
--------------------------------------------------------------------------------
1 | name: Python check
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | test_package:
11 | name: Test ${{ matrix.os }} Python ${{ matrix.python_version }}
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os: ["ubuntu-20.04"]
17 | python_version: ["3.10"]
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Setup python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: ${{ matrix.python_version }}
24 | architecture: x64
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install -r requirements-dev.txt
29 | - name: Lint with ruff
30 | run: python3 -m ruff check api/
31 | - name: Check formatting with black
32 | run: python3 -m black api/ --check --verbose
33 | - name: Run Pytest tests
34 | run: python3 -m pytest
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | .hypothesis/
51 | .pytest_cache/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 | db.sqlite3
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # IPython
79 | profile_default/
80 | ipython_config.py
81 |
82 | # pyenv
83 | .python-version
84 |
85 | # pipenv
86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not
89 | # install all needed dependencies.
90 | #Pipfile.lock
91 |
92 | # celery beat schedule file
93 | celerybeat-schedule
94 |
95 | # SageMath parsed files
96 | *.sage.py
97 |
98 | # Environments
99 | .env
100 | .venv
101 | env/
102 | venv/
103 | ENV/
104 | env.bak/
105 | venv.bak/
106 |
107 | # Spyder project settings
108 | .spyderproject
109 | .spyproject
110 |
111 | # Rope project settings
112 | .ropeproject
113 |
114 | # mkdocs documentation
115 | /site
116 |
117 | # mypy
118 | .mypy_cache/
119 | .dmypy.json
120 | dmypy.json
121 |
122 | # Pyre type checker
123 | .pyre/
124 |
125 | # Azure Functions artifacts
126 | bin
127 | obj
128 | appsettings.json
129 |
130 | # Azurite artifacts
131 | __blobstorage__
132 | __queuestorage__
133 | __azurite_db*__.json
134 | .python_packages
135 | .azure
136 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v2.3.0
4 | hooks:
5 | - id: check-yaml
6 | - id: end-of-file-fixer
7 | - id: trailing-whitespace
8 | - repo: https://github.com/astral-sh/ruff-pre-commit
9 | rev: v0.7.2
10 | hooks:
11 | - id: ruff
12 | - repo: https://github.com/psf/black
13 | rev: 24.10.0
14 | hooks:
15 | - id: black
16 | args: ['--config=./pyproject.toml']
17 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | ]
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Attach to Python Functions",
6 | "type": "python",
7 | "request": "attach",
8 | "port": 9091,
9 | "preLaunchTask": "func: host start"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "azureFunctions.scmDoBuildDuringDeployment": true,
3 | "azureFunctions.projectLanguage": "Python",
4 | "azureFunctions.projectRuntime": "~4",
5 | "debug.internalConsoleOptions": "neverOpen"
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
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 | # FastAPI + Azure API Management
2 |
3 | [](https://github.com/codespaces/new?hide_repo_select=true&repo=pamelafox%2Ffastapi-azure-function-apim)
4 |
5 | This repository includes a simple HTTP API powered by FastAPI, made for demonstration purposes only.
6 | This API is designed to be deployed as a secured Azure Function with an API Management service in front.
7 |
8 | 
9 |
10 | Thanks to the API Management policies, making calls to the actual API requires a subscription key,
11 | but viewing the auto-generated documentation or OpenAPI schema does not.
12 | The Azure Function has an authentication level of "function",
13 | so even if someone knows its endpoint, they can't make calls to it without a function key.
14 | The API Management service _does_ know the function key, and passes it on.
15 | [Learn more about the approach in this blog post.](http://blog.pamelafox.org/2022/11/fastapi-on-azure-functions-with-azure.html)
16 |
17 | ## Opening the project
18 |
19 | This project has devcontainer support, so it will be automatically setup if you open it in Github Codespaces or in local VS Code with the Dev Containers extension.
20 |
21 | If you're unable to open the devcontainer, then you'll need to:
22 |
23 | 1. Create a [Python virtual environment](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments) and activate it.
24 |
25 | 2. Install requirements:
26 |
27 | ```shell
28 | python3 -m pip install --user -r requirements-dev.txt
29 | ```
30 |
31 | 3. Install the [Azure Dev CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd).
32 |
33 | ## Local development
34 |
35 | Use the local emulator from Azure Functions Core Tools to test the function locally.
36 | (There is no local emulator for the API Management service).
37 |
38 | 1. Open this repository in Github Codespaces or VS Code with Remote Devcontainers extension.
39 | 2. Open the Terminal and make sure you're in the root folder.
40 | 3. Run `func host start`
41 | 4. Click [http://localhost:7071/](http://localhost:7071/) in the terminal, which should open the website in a new tab.
42 | 5. Change the URL to navigate to either the API at `/generate_name` or the docs at `/docs`.
43 |
44 | ## Deployment
45 |
46 | This repo is set up for deployment using the
47 | [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview),
48 | which relies on the `azure.yaml` file and the configuration files in the `infra` folder.
49 |
50 | [🎥 Watch a screencast of deploying and testing the app.](https://youtu.be/FPyq_aLzmIY)
51 |
52 | Steps for deployment:
53 |
54 | 1. Sign up for a [free Azure account](https://azure.microsoft.com/free/)
55 | 2. Login to your Azure account:
56 |
57 | ```shell
58 | azd auth login
59 | ```
60 |
61 | 3. Initialize a new `azd` environment:
62 |
63 | ```shell
64 | azd env new
65 | ```
66 |
67 | It will prompt you to provide a name (like "fast-func") that will later be used in the name of the deployed resources.
68 |
69 | 4. Provision and deploy all the resources:
70 |
71 | ```shell
72 | azd up
73 | ```
74 |
75 | 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.
76 |
77 | 5. Once it finishes deploying, navigate to the API endpoint URL from the output.
78 | 6. To get a subscription key for API calls, navigate to the portal URL from the output, open the _Subscriptions_ page from the side nav, and copy one of the built-in keys.
79 |
80 | ### CI/CD pipeline
81 |
82 | This project includes a Github workflow for deploying the resources to Azure
83 | 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:
84 |
85 | ```shell
86 | azd pipeline config
87 | ```
88 |
89 | ### Monitoring
90 |
91 | The deployed resources include a Log Analytics workspace with an Application Insights dashboard to measure metrics like server response time.
92 |
93 | To open that dashboard, run this command once you've deployed:
94 |
95 | ```shell
96 | azd monitor --overview
97 | ```
98 |
99 | ## Costs
100 |
101 | (only provided as an example, as of Nov-2022)
102 |
103 | Costs for this architecture are based on incoming traffic / usage, so cost should be near $0 if you're only testing it out, and otherwise increase based on your API usage.
104 |
105 | - API Management - Consumption tier: $3.50 per 1 million calls. The first 1 million calls per Azure subscription are free. [Pricing](https://azure.microsoft.com/pricing/details/api-management/)
106 | - Azure Functions - Consumption tier: $0.20 per 1 million calls. The first 1 million calls per Azure subscription are free. [Pricing](https://azure.microsoft.com/pricing/details/functions/)
107 | - Storage account - Standard tier (Hot): $0.0255 per used GiB, $0.065 per 10,000 write transactions. The account is only used to store the function code, so cost depends on size of function code and number of deploys (but should be quite low). [Pricing](https://azure.microsoft.com/pricing/details/storage/files/)
108 | - Application Insights: $2.88 per GB ingested data. The first 5 GB per billing account are included per month. [Pricing](https://azure.microsoft.com/pricing/details/monitor/)
109 |
110 | ## Getting help
111 |
112 | If you're working with this project and running into issues, please post in [Discussions](/discussions).
113 |
--------------------------------------------------------------------------------
/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pamelafox/fastapi-azure-function-apim/61a26416b647406d7fa059fd0abf7103e19d6f63/api/__init__.py
--------------------------------------------------------------------------------
/api/fastapi_app.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | import fastapi
5 |
6 | from . import fastapi_routes
7 |
8 | logger = logging.getLogger("fastapi_app")
9 |
10 |
11 | def create_app():
12 | logging.basicConfig(level=logging.INFO)
13 | # Check for an environment variable that's only set in production
14 | if os.getenv("RUNNING_IN_PRODUCTION"):
15 | logger.info("Running in production, using /public as root path")
16 | app = fastapi.FastAPI(
17 | servers=[{"url": "/api", "description": "API"}],
18 | root_path="/public",
19 | root_path_in_servers=False,
20 | )
21 | else:
22 | logger.info("Running in development, using / as root path")
23 | app = fastapi.FastAPI()
24 |
25 | app.include_router(fastapi_routes.router)
26 | return app
27 |
--------------------------------------------------------------------------------
/api/fastapi_routes.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | import fastapi
4 |
5 | router = fastapi.APIRouter()
6 |
7 |
8 | @router.get("/generate_name")
9 | async def generate_name(
10 | starts_with: str = None,
11 | subscription_key: str | None = fastapi.Query(default=None, alias="subscription-key"),
12 | ):
13 | names = ["Minnie", "Margaret", "Myrtle", "Noa", "Nadia"]
14 | if starts_with:
15 | names = [n for n in names if n.lower().startswith(starts_with)]
16 | random_name = random.choice(names)
17 | return {"name": random_name}
18 |
--------------------------------------------------------------------------------
/azure.yaml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
2 |
3 | name: fastapi-azure-function-apim
4 | metadata:
5 | template: fastapi-azure-function-apim@0.0.1-beta
6 | services:
7 | api:
8 | project: .
9 | language: py
10 | host: function
11 |
--------------------------------------------------------------------------------
/function_app.py:
--------------------------------------------------------------------------------
1 | import azure.functions as func
2 |
3 | from api.fastapi_app import create_app
4 |
5 | fastapi_app = create_app()
6 |
7 | app = func.AsgiFunctionApp(app=fastapi_app, http_auth_level=func.AuthLevel.FUNCTION)
8 |
--------------------------------------------------------------------------------
/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | }
10 | },
11 | "extensionBundle": {
12 | "id": "Microsoft.Azure.Functions.ExtensionBundle",
13 | "version": "[2.*, 3.0.0)"
14 | },
15 | "extensions": {
16 | "http": {
17 | "routePrefix": ""
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/infra/apimanagement.bicep:
--------------------------------------------------------------------------------
1 | param apimServiceName string
2 | param functionAppName string
3 |
4 | resource apimService 'Microsoft.ApiManagement/service@2021-08-01' existing = {
5 | name: apimServiceName
6 | }
7 |
8 | resource functionApp 'Microsoft.Web/sites@2022-03-01' existing = {
9 | name: functionAppName
10 | }
11 |
12 | resource apimBackend 'Microsoft.ApiManagement/service/backends@2021-12-01-preview' = {
13 | parent: apimService
14 | name: functionAppName
15 | properties: {
16 | description: functionAppName
17 | url: 'https://${functionApp.properties.hostNames[0]}'
18 | protocol: 'http'
19 | resourceId: '${environment().resourceManager}${functionApp.id}'
20 | credentials: {
21 | header: {
22 | 'x-functions-key': [
23 | '{{function-app-key}}'
24 | ]
25 | }
26 | }
27 | }
28 | dependsOn: [apimNamedValuesKey]
29 | }
30 |
31 | resource apimNamedValuesKey 'Microsoft.ApiManagement/service/namedValues@2021-12-01-preview' = {
32 | parent: apimService
33 | name: 'function-app-key'
34 | properties: {
35 | displayName: 'function-app-key'
36 | value: listKeys('${functionApp.id}/host/default', '2019-08-01').functionKeys.default
37 | tags: [
38 | 'key'
39 | 'function'
40 | 'auto'
41 | ]
42 | secret: true
43 | }
44 | }
45 |
46 | resource apimAPI 'Microsoft.ApiManagement/service/apis@2021-12-01-preview' = {
47 | parent: apimService
48 | name: 'simple-fastapi-api'
49 | properties: {
50 | displayName: 'Protected API Calls'
51 | apiRevision: '1'
52 | subscriptionRequired: true
53 | protocols: [
54 | 'https'
55 | ]
56 | path: 'api'
57 | }
58 | }
59 |
60 | resource apimAPIGet 'Microsoft.ApiManagement/service/apis/operations@2021-12-01-preview' = {
61 | parent: apimAPI
62 | name: 'generate-name'
63 | properties: {
64 | displayName: 'Generate Name'
65 | method: 'GET'
66 | urlTemplate: '/generate_name'
67 | }
68 | }
69 |
70 | resource apimAPIGetPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-12-01-preview' = {
71 | parent: apimAPIGet
72 | name: 'policy'
73 | properties: {
74 | format: 'xml'
75 | value: '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n*\r\n\r\n\r\nGET\r\nPOST\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n'
76 | }
77 | dependsOn: [apimBackend]
78 | }
79 |
80 | resource apimAPIPublic 'Microsoft.ApiManagement/service/apis@2021-12-01-preview' = {
81 | parent: apimService
82 | name: 'public-docs'
83 | properties: {
84 | displayName: 'Public Doc Paths'
85 | apiRevision: '1'
86 | subscriptionRequired: false
87 | protocols: [
88 | 'https'
89 | ]
90 | path: 'public'
91 | }
92 | }
93 |
94 | resource apimAPIDocsSwagger 'Microsoft.ApiManagement/service/apis/operations@2021-12-01-preview' = {
95 | parent: apimAPIPublic
96 | name: 'swagger-docs'
97 | properties: {
98 | displayName: 'Documentation'
99 | method: 'GET'
100 | urlTemplate: '/docs'
101 | }
102 | }
103 |
104 | resource apimAPIDocsSchema 'Microsoft.ApiManagement/service/apis/operations@2021-12-01-preview' = {
105 | parent: apimAPIPublic
106 | name: 'openapi-schema'
107 | properties: {
108 | displayName: 'OpenAPI Schema'
109 | method: 'GET'
110 | urlTemplate: '/openapi.json'
111 | }
112 | }
113 |
114 | var docsPolicy = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n'
115 |
116 | resource apimAPIDocsSwaggerPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-12-01-preview' = {
117 | parent: apimAPIDocsSwagger
118 | name: 'policy'
119 | properties: {
120 | format: 'xml'
121 | value: docsPolicy
122 | }
123 | dependsOn: [apimBackend]
124 | }
125 |
126 | resource apimAPIDocsSchemaPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-12-01-preview' = {
127 | parent: apimAPIDocsSchema
128 | name: 'policy'
129 | properties: {
130 | format: 'xml'
131 | value: docsPolicy
132 | }
133 | dependsOn: [apimBackend]
134 | }
135 |
136 | resource functionAppProperties 'Microsoft.Web/sites/config@2022-03-01' = {
137 | name: 'web'
138 | kind: 'string'
139 | parent: functionApp
140 | properties: {
141 | apiManagementConfig: {
142 | id: '${apimService.id}/apis/simple-fastapi-api'
143 | }
144 | }
145 | dependsOn: [
146 | apimService
147 | ]
148 | }
149 |
150 | output apimServiceUrl string = apimService.properties.gatewayUrl
151 |
--------------------------------------------------------------------------------
/infra/app-diagnostics.bicep:
--------------------------------------------------------------------------------
1 | param appName string = ''
2 |
3 | @description('The kind of the app.')
4 | @allowed([
5 | 'functionapp'
6 | 'webapp'
7 | ])
8 | param kind string
9 |
10 | @description('Resource ID of log analytics workspace.')
11 | param diagnosticWorkspaceId string
12 |
13 | param diagnosticLogCategoriesToEnable array = kind == 'functionapp' ? [
14 | 'FunctionAppLogs'
15 | ] : [
16 | 'AppServiceHTTPLogs'
17 | 'AppServiceConsoleLogs'
18 | 'AppServiceAppLogs'
19 | 'AppServiceAuditLogs'
20 | 'AppServiceIPSecAuditLogs'
21 | 'AppServicePlatformLogs'
22 | ]
23 |
24 | @description('Optional. The name of metrics that will be streamed.')
25 | @allowed([
26 | 'AllMetrics'
27 | ])
28 | param diagnosticMetricsToEnable array = [
29 | 'AllMetrics'
30 | ]
31 |
32 |
33 | var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: {
34 | category: category
35 | enabled: true
36 | }]
37 |
38 | var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: {
39 | category: metric
40 | timeGrain: null
41 | enabled: true
42 | }]
43 |
44 | resource app 'Microsoft.Web/sites@2022-03-01' existing = {
45 | name: appName
46 | }
47 |
48 | resource app_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
49 | name: '${appName}-diagnostics'
50 | scope: app
51 | properties: {
52 | workspaceId: diagnosticWorkspaceId
53 | metrics: diagnosticsMetrics
54 | logs: diagnosticsLogs
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/infra/appinsightsdashboard.bicep:
--------------------------------------------------------------------------------
1 | param prefix string
2 | param location string
3 | param tags object
4 | param appInsightsName string
5 |
6 | resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = {
7 | name: '${prefix}-dashboard'
8 | location: location
9 | tags: tags
10 | properties: {
11 | lenses: [
12 | {
13 | order: 0
14 | parts: [
15 | {
16 | position: {
17 | x: 0
18 | y: 0
19 | colSpan: 2
20 | rowSpan: 1
21 | }
22 | metadata: {
23 | inputs: [
24 | {
25 | name: 'id'
26 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
27 | }
28 | {
29 | name: 'Version'
30 | value: '1.0'
31 | }
32 | ]
33 | #disable-next-line BCP036
34 | type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart'
35 | asset: {
36 | idInputName: 'id'
37 | type: 'ApplicationInsights'
38 | }
39 | defaultMenuItemId: 'overview'
40 | }
41 | }
42 | {
43 | position: {
44 | x: 2
45 | y: 0
46 | colSpan: 1
47 | rowSpan: 1
48 | }
49 | metadata: {
50 | inputs: [
51 | {
52 | name: 'ComponentId'
53 | value: {
54 | Name: appInsightsName
55 | SubscriptionId: subscription().subscriptionId
56 | ResourceGroup: resourceGroup().name
57 | }
58 | }
59 | {
60 | name: 'Version'
61 | value: '1.0'
62 | }
63 | ]
64 | #disable-next-line BCP036
65 | type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart'
66 | asset: {
67 | idInputName: 'ComponentId'
68 | type: 'ApplicationInsights'
69 | }
70 | defaultMenuItemId: 'ProactiveDetection'
71 | }
72 | }
73 | {
74 | position: {
75 | x: 3
76 | y: 0
77 | colSpan: 1
78 | rowSpan: 1
79 | }
80 | metadata: {
81 | inputs: [
82 | {
83 | name: 'ComponentId'
84 | value: {
85 | Name: appInsightsName
86 | SubscriptionId: subscription().subscriptionId
87 | ResourceGroup: resourceGroup().name
88 | }
89 | }
90 | {
91 | name: 'ResourceId'
92 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
93 | }
94 | ]
95 | #disable-next-line BCP036
96 | type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart'
97 | asset: {
98 | idInputName: 'ComponentId'
99 | type: 'ApplicationInsights'
100 | }
101 | }
102 | }
103 | {
104 | position: {
105 | x: 4
106 | y: 0
107 | colSpan: 1
108 | rowSpan: 1
109 | }
110 | metadata: {
111 | inputs: [
112 | {
113 | name: 'ComponentId'
114 | value: {
115 | Name: appInsightsName
116 | SubscriptionId: subscription().subscriptionId
117 | ResourceGroup: resourceGroup().name
118 | }
119 | }
120 | {
121 | name: 'TimeContext'
122 | value: {
123 | durationMs: 86400000
124 | endTime: null
125 | createdTime: '2018-05-04T01:20:33.345Z'
126 | isInitialTime: true
127 | grain: 1
128 | useDashboardTimeRange: false
129 | }
130 | }
131 | {
132 | name: 'Version'
133 | value: '1.0'
134 | }
135 | ]
136 | #disable-next-line BCP036
137 | type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart'
138 | asset: {
139 | idInputName: 'ComponentId'
140 | type: 'ApplicationInsights'
141 | }
142 | }
143 | }
144 | {
145 | position: {
146 | x: 5
147 | y: 0
148 | colSpan: 1
149 | rowSpan: 1
150 | }
151 | metadata: {
152 | inputs: [
153 | {
154 | name: 'ComponentId'
155 | value: {
156 | Name: appInsightsName
157 | SubscriptionId: subscription().subscriptionId
158 | ResourceGroup: resourceGroup().name
159 | }
160 | }
161 | {
162 | name: 'TimeContext'
163 | value: {
164 | durationMs: 86400000
165 | endTime: null
166 | createdTime: '2018-05-08T18:47:35.237Z'
167 | isInitialTime: true
168 | grain: 1
169 | useDashboardTimeRange: false
170 | }
171 | }
172 | {
173 | name: 'ConfigurationId'
174 | value: '78ce933e-e864-4b05-a27b-71fd55a6afad'
175 | }
176 | ]
177 | #disable-next-line BCP036
178 | type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart'
179 | asset: {
180 | idInputName: 'ComponentId'
181 | type: 'ApplicationInsights'
182 | }
183 | }
184 | }
185 | {
186 | position: {
187 | x: 0
188 | y: 1
189 | colSpan: 3
190 | rowSpan: 1
191 | }
192 | metadata: {
193 | inputs: []
194 | type: 'Extension/HubsExtension/PartType/MarkdownPart'
195 | settings: {
196 | content: {
197 | settings: {
198 | content: '# Usage'
199 | title: ''
200 | subtitle: ''
201 | }
202 | }
203 | }
204 | }
205 | }
206 | {
207 | position: {
208 | x: 3
209 | y: 1
210 | colSpan: 1
211 | rowSpan: 1
212 | }
213 | metadata: {
214 | inputs: [
215 | {
216 | name: 'ComponentId'
217 | value: {
218 | Name: appInsightsName
219 | SubscriptionId: subscription().subscriptionId
220 | ResourceGroup: resourceGroup().name
221 | }
222 | }
223 | {
224 | name: 'TimeContext'
225 | value: {
226 | durationMs: 86400000
227 | endTime: null
228 | createdTime: '2018-05-04T01:22:35.782Z'
229 | isInitialTime: true
230 | grain: 1
231 | useDashboardTimeRange: false
232 | }
233 | }
234 | ]
235 | #disable-next-line BCP036
236 | type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart'
237 | asset: {
238 | idInputName: 'ComponentId'
239 | type: 'ApplicationInsights'
240 | }
241 | }
242 | }
243 | {
244 | position: {
245 | x: 4
246 | y: 1
247 | colSpan: 3
248 | rowSpan: 1
249 | }
250 | metadata: {
251 | inputs: []
252 | type: 'Extension/HubsExtension/PartType/MarkdownPart'
253 | settings: {
254 | content: {
255 | settings: {
256 | content: '# Reliability'
257 | title: ''
258 | subtitle: ''
259 | }
260 | }
261 | }
262 | }
263 | }
264 | {
265 | position: {
266 | x: 7
267 | y: 1
268 | colSpan: 1
269 | rowSpan: 1
270 | }
271 | metadata: {
272 | inputs: [
273 | {
274 | name: 'ResourceId'
275 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
276 | }
277 | {
278 | name: 'DataModel'
279 | value: {
280 | version: '1.0.0'
281 | timeContext: {
282 | durationMs: 86400000
283 | createdTime: '2018-05-04T23:42:40.072Z'
284 | isInitialTime: false
285 | grain: 1
286 | useDashboardTimeRange: false
287 | }
288 | }
289 | isOptional: true
290 | }
291 | {
292 | name: 'ConfigurationId'
293 | value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f'
294 | isOptional: true
295 | }
296 | ]
297 | #disable-next-line BCP036
298 | type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart'
299 | isAdapter: true
300 | asset: {
301 | idInputName: 'ResourceId'
302 | type: 'ApplicationInsights'
303 | }
304 | defaultMenuItemId: 'failures'
305 | }
306 | }
307 | {
308 | position: {
309 | x: 8
310 | y: 1
311 | colSpan: 3
312 | rowSpan: 1
313 | }
314 | metadata: {
315 | inputs: []
316 | type: 'Extension/HubsExtension/PartType/MarkdownPart'
317 | settings: {
318 | content: {
319 | settings: {
320 | content: '# Responsiveness\r\n'
321 | title: ''
322 | subtitle: ''
323 | }
324 | }
325 | }
326 | }
327 | }
328 | {
329 | position: {
330 | x: 11
331 | y: 1
332 | colSpan: 1
333 | rowSpan: 1
334 | }
335 | metadata: {
336 | inputs: [
337 | {
338 | name: 'ResourceId'
339 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
340 | }
341 | {
342 | name: 'DataModel'
343 | value: {
344 | version: '1.0.0'
345 | timeContext: {
346 | durationMs: 86400000
347 | createdTime: '2018-05-04T23:43:37.804Z'
348 | isInitialTime: false
349 | grain: 1
350 | useDashboardTimeRange: false
351 | }
352 | }
353 | isOptional: true
354 | }
355 | {
356 | name: 'ConfigurationId'
357 | value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865'
358 | isOptional: true
359 | }
360 | ]
361 | #disable-next-line BCP036
362 | type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart'
363 | isAdapter: true
364 | asset: {
365 | idInputName: 'ResourceId'
366 | type: 'ApplicationInsights'
367 | }
368 | defaultMenuItemId: 'performance'
369 | }
370 | }
371 | {
372 | position: {
373 | x: 15
374 | y: 1
375 | colSpan: 1
376 | rowSpan: 1
377 | }
378 | metadata: {
379 | inputs: [
380 | {
381 | name: 'ComponentId'
382 | value: {
383 | Name: appInsightsName
384 | SubscriptionId: subscription().subscriptionId
385 | ResourceGroup: resourceGroup().name
386 | }
387 | }
388 | {
389 | name: 'TimeContext'
390 | value: {
391 | durationMs: 86400000
392 | createdTime: '2018-05-08T12:16:27.534Z'
393 | isInitialTime: false
394 | grain: 1
395 | useDashboardTimeRange: false
396 | }
397 | }
398 | {
399 | name: 'CurrentFilter'
400 | value: {
401 | eventTypes: [
402 | 4
403 | 1
404 | 3
405 | 5
406 | 2
407 | 6
408 | 13
409 | ]
410 | typeFacets: {}
411 | isPermissive: false
412 | }
413 | }
414 | {
415 | name: 'id'
416 | value: {
417 | Name: appInsightsName
418 | SubscriptionId: subscription().subscriptionId
419 | ResourceGroup: resourceGroup().name
420 | }
421 | }
422 | {
423 | name: 'Version'
424 | value: '1.0'
425 | }
426 | ]
427 | #disable-next-line BCP036
428 | type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart'
429 | asset: {
430 | idInputName: 'ComponentId'
431 | type: 'ApplicationInsights'
432 | }
433 | }
434 | }
435 | {
436 | position: {
437 | x: 0
438 | y: 2
439 | colSpan: 4
440 | rowSpan: 3
441 | }
442 | metadata: {
443 | inputs: [
444 | {
445 | name: 'options'
446 | value: {
447 | chart: {
448 | metrics: [
449 | {
450 | resourceMetadata: {
451 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
452 | }
453 | name: 'sessions/count'
454 | aggregationType: 5
455 | namespace: 'microsoft.insights/components/kusto'
456 | metricVisualization: {
457 | displayName: 'Sessions'
458 | color: '#47BDF5'
459 | }
460 | }
461 | {
462 | resourceMetadata: {
463 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
464 | }
465 | name: 'users/count'
466 | aggregationType: 5
467 | namespace: 'microsoft.insights/components/kusto'
468 | metricVisualization: {
469 | displayName: 'Users'
470 | color: '#7E58FF'
471 | }
472 | }
473 | ]
474 | title: 'Unique sessions and users'
475 | visualization: {
476 | chartType: 2
477 | legendVisualization: {
478 | isVisible: true
479 | position: 2
480 | hideSubtitle: false
481 | }
482 | axisVisualization: {
483 | x: {
484 | isVisible: true
485 | axisType: 2
486 | }
487 | y: {
488 | isVisible: true
489 | axisType: 1
490 | }
491 | }
492 | }
493 | openBladeOnClick: {
494 | openBlade: true
495 | destinationBlade: {
496 | extensionName: 'HubsExtension'
497 | bladeName: 'ResourceMenuBlade'
498 | parameters: {
499 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
500 | menuid: 'segmentationUsers'
501 | }
502 | }
503 | }
504 | }
505 | }
506 | }
507 | {
508 | name: 'sharedTimeRange'
509 | isOptional: true
510 | }
511 | ]
512 | #disable-next-line BCP036
513 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
514 | settings: {}
515 | }
516 | }
517 | {
518 | position: {
519 | x: 4
520 | y: 2
521 | colSpan: 4
522 | rowSpan: 3
523 | }
524 | metadata: {
525 | inputs: [
526 | {
527 | name: 'options'
528 | value: {
529 | chart: {
530 | metrics: [
531 | {
532 | resourceMetadata: {
533 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
534 | }
535 | name: 'requests/failed'
536 | aggregationType: 7
537 | namespace: 'microsoft.insights/components'
538 | metricVisualization: {
539 | displayName: 'Failed requests'
540 | color: '#EC008C'
541 | }
542 | }
543 | ]
544 | title: 'Failed requests'
545 | visualization: {
546 | chartType: 3
547 | legendVisualization: {
548 | isVisible: true
549 | position: 2
550 | hideSubtitle: false
551 | }
552 | axisVisualization: {
553 | x: {
554 | isVisible: true
555 | axisType: 2
556 | }
557 | y: {
558 | isVisible: true
559 | axisType: 1
560 | }
561 | }
562 | }
563 | openBladeOnClick: {
564 | openBlade: true
565 | destinationBlade: {
566 | extensionName: 'HubsExtension'
567 | bladeName: 'ResourceMenuBlade'
568 | parameters: {
569 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
570 | menuid: 'failures'
571 | }
572 | }
573 | }
574 | }
575 | }
576 | }
577 | {
578 | name: 'sharedTimeRange'
579 | isOptional: true
580 | }
581 | ]
582 | #disable-next-line BCP036
583 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
584 | settings: {}
585 | }
586 | }
587 | {
588 | position: {
589 | x: 8
590 | y: 2
591 | colSpan: 4
592 | rowSpan: 3
593 | }
594 | metadata: {
595 | inputs: [
596 | {
597 | name: 'options'
598 | value: {
599 | chart: {
600 | metrics: [
601 | {
602 | resourceMetadata: {
603 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
604 | }
605 | name: 'requests/duration'
606 | aggregationType: 4
607 | namespace: 'microsoft.insights/components'
608 | metricVisualization: {
609 | displayName: 'Server response time'
610 | color: '#00BCF2'
611 | }
612 | }
613 | ]
614 | title: 'Server response time'
615 | visualization: {
616 | chartType: 2
617 | legendVisualization: {
618 | isVisible: true
619 | position: 2
620 | hideSubtitle: false
621 | }
622 | axisVisualization: {
623 | x: {
624 | isVisible: true
625 | axisType: 2
626 | }
627 | y: {
628 | isVisible: true
629 | axisType: 1
630 | }
631 | }
632 | }
633 | openBladeOnClick: {
634 | openBlade: true
635 | destinationBlade: {
636 | extensionName: 'HubsExtension'
637 | bladeName: 'ResourceMenuBlade'
638 | parameters: {
639 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
640 | menuid: 'performance'
641 | }
642 | }
643 | }
644 | }
645 | }
646 | }
647 | {
648 | name: 'sharedTimeRange'
649 | isOptional: true
650 | }
651 | ]
652 | #disable-next-line BCP036
653 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
654 | settings: {}
655 | }
656 | }
657 | {
658 | position: {
659 | x: 0
660 | y: 5
661 | colSpan: 4
662 | rowSpan: 3
663 | }
664 | metadata: {
665 | inputs: [
666 | {
667 | name: 'options'
668 | value: {
669 | chart: {
670 | metrics: [
671 | {
672 | resourceMetadata: {
673 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
674 | }
675 | name: 'availabilityResults/availabilityPercentage'
676 | aggregationType: 4
677 | namespace: 'microsoft.insights/components'
678 | metricVisualization: {
679 | displayName: 'Availability'
680 | color: '#47BDF5'
681 | }
682 | }
683 | ]
684 | title: 'Average availability'
685 | visualization: {
686 | chartType: 3
687 | legendVisualization: {
688 | isVisible: true
689 | position: 2
690 | hideSubtitle: false
691 | }
692 | axisVisualization: {
693 | x: {
694 | isVisible: true
695 | axisType: 2
696 | }
697 | y: {
698 | isVisible: true
699 | axisType: 1
700 | }
701 | }
702 | }
703 | openBladeOnClick: {
704 | openBlade: true
705 | destinationBlade: {
706 | extensionName: 'HubsExtension'
707 | bladeName: 'ResourceMenuBlade'
708 | parameters: {
709 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
710 | menuid: 'availability'
711 | }
712 | }
713 | }
714 | }
715 | }
716 | }
717 | {
718 | name: 'sharedTimeRange'
719 | isOptional: true
720 | }
721 | ]
722 | #disable-next-line BCP036
723 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
724 | settings: {}
725 | }
726 | }
727 | {
728 | position: {
729 | x: 4
730 | y: 5
731 | colSpan: 4
732 | rowSpan: 3
733 | }
734 | metadata: {
735 | inputs: [
736 | {
737 | name: 'options'
738 | value: {
739 | chart: {
740 | metrics: [
741 | {
742 | resourceMetadata: {
743 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
744 | }
745 | name: 'exceptions/server'
746 | aggregationType: 7
747 | namespace: 'microsoft.insights/components'
748 | metricVisualization: {
749 | displayName: 'Server exceptions'
750 | color: '#47BDF5'
751 | }
752 | }
753 | {
754 | resourceMetadata: {
755 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
756 | }
757 | name: 'dependencies/failed'
758 | aggregationType: 7
759 | namespace: 'microsoft.insights/components'
760 | metricVisualization: {
761 | displayName: 'Dependency failures'
762 | color: '#7E58FF'
763 | }
764 | }
765 | ]
766 | title: 'Server exceptions and Dependency failures'
767 | visualization: {
768 | chartType: 2
769 | legendVisualization: {
770 | isVisible: true
771 | position: 2
772 | hideSubtitle: false
773 | }
774 | axisVisualization: {
775 | x: {
776 | isVisible: true
777 | axisType: 2
778 | }
779 | y: {
780 | isVisible: true
781 | axisType: 1
782 | }
783 | }
784 | }
785 | }
786 | }
787 | }
788 | {
789 | name: 'sharedTimeRange'
790 | isOptional: true
791 | }
792 | ]
793 | #disable-next-line BCP036
794 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
795 | settings: {}
796 | }
797 | }
798 | {
799 | position: {
800 | x: 8
801 | y: 5
802 | colSpan: 4
803 | rowSpan: 3
804 | }
805 | metadata: {
806 | inputs: [
807 | {
808 | name: 'options'
809 | value: {
810 | chart: {
811 | metrics: [
812 | {
813 | resourceMetadata: {
814 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
815 | }
816 | name: 'performanceCounters/processorCpuPercentage'
817 | aggregationType: 4
818 | namespace: 'microsoft.insights/components'
819 | metricVisualization: {
820 | displayName: 'Processor time'
821 | color: '#47BDF5'
822 | }
823 | }
824 | {
825 | resourceMetadata: {
826 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
827 | }
828 | name: 'performanceCounters/processCpuPercentage'
829 | aggregationType: 4
830 | namespace: 'microsoft.insights/components'
831 | metricVisualization: {
832 | displayName: 'Process CPU'
833 | color: '#7E58FF'
834 | }
835 | }
836 | ]
837 | title: 'Average processor and process CPU utilization'
838 | visualization: {
839 | chartType: 2
840 | legendVisualization: {
841 | isVisible: true
842 | position: 2
843 | hideSubtitle: false
844 | }
845 | axisVisualization: {
846 | x: {
847 | isVisible: true
848 | axisType: 2
849 | }
850 | y: {
851 | isVisible: true
852 | axisType: 1
853 | }
854 | }
855 | }
856 | }
857 | }
858 | }
859 | {
860 | name: 'sharedTimeRange'
861 | isOptional: true
862 | }
863 | ]
864 | #disable-next-line BCP036
865 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
866 | settings: {}
867 | }
868 | }
869 | {
870 | position: {
871 | x: 0
872 | y: 8
873 | colSpan: 4
874 | rowSpan: 3
875 | }
876 | metadata: {
877 | inputs: [
878 | {
879 | name: 'options'
880 | value: {
881 | chart: {
882 | metrics: [
883 | {
884 | resourceMetadata: {
885 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
886 | }
887 | name: 'availabilityResults/count'
888 | aggregationType: 7
889 | namespace: 'microsoft.insights/components'
890 | metricVisualization: {
891 | displayName: 'Availability test results count'
892 | color: '#47BDF5'
893 | }
894 | }
895 | ]
896 | title: 'Availability test results count'
897 | visualization: {
898 | chartType: 2
899 | legendVisualization: {
900 | isVisible: true
901 | position: 2
902 | hideSubtitle: false
903 | }
904 | axisVisualization: {
905 | x: {
906 | isVisible: true
907 | axisType: 2
908 | }
909 | y: {
910 | isVisible: true
911 | axisType: 1
912 | }
913 | }
914 | }
915 | }
916 | }
917 | }
918 | {
919 | name: 'sharedTimeRange'
920 | isOptional: true
921 | }
922 | ]
923 | #disable-next-line BCP036
924 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
925 | settings: {}
926 | }
927 | }
928 | {
929 | position: {
930 | x: 4
931 | y: 8
932 | colSpan: 4
933 | rowSpan: 3
934 | }
935 | metadata: {
936 | inputs: [
937 | {
938 | name: 'options'
939 | value: {
940 | chart: {
941 | metrics: [
942 | {
943 | resourceMetadata: {
944 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
945 | }
946 | name: 'performanceCounters/processIOBytesPerSecond'
947 | aggregationType: 4
948 | namespace: 'microsoft.insights/components'
949 | metricVisualization: {
950 | displayName: 'Process IO rate'
951 | color: '#47BDF5'
952 | }
953 | }
954 | ]
955 | title: 'Average process I/O rate'
956 | visualization: {
957 | chartType: 2
958 | legendVisualization: {
959 | isVisible: true
960 | position: 2
961 | hideSubtitle: false
962 | }
963 | axisVisualization: {
964 | x: {
965 | isVisible: true
966 | axisType: 2
967 | }
968 | y: {
969 | isVisible: true
970 | axisType: 1
971 | }
972 | }
973 | }
974 | }
975 | }
976 | }
977 | {
978 | name: 'sharedTimeRange'
979 | isOptional: true
980 | }
981 | ]
982 | #disable-next-line BCP036
983 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
984 | settings: {}
985 | }
986 | }
987 | {
988 | position: {
989 | x: 8
990 | y: 8
991 | colSpan: 4
992 | rowSpan: 3
993 | }
994 | metadata: {
995 | inputs: [
996 | {
997 | name: 'options'
998 | value: {
999 | chart: {
1000 | metrics: [
1001 | {
1002 | resourceMetadata: {
1003 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${appInsightsName}'
1004 | }
1005 | name: 'performanceCounters/memoryAvailableBytes'
1006 | aggregationType: 4
1007 | namespace: 'microsoft.insights/components'
1008 | metricVisualization: {
1009 | displayName: 'Available memory'
1010 | color: '#47BDF5'
1011 | }
1012 | }
1013 | ]
1014 | title: 'Average available memory'
1015 | visualization: {
1016 | chartType: 2
1017 | legendVisualization: {
1018 | isVisible: true
1019 | position: 2
1020 | hideSubtitle: false
1021 | }
1022 | axisVisualization: {
1023 | x: {
1024 | isVisible: true
1025 | axisType: 2
1026 | }
1027 | y: {
1028 | isVisible: true
1029 | axisType: 1
1030 | }
1031 | }
1032 | }
1033 | }
1034 | }
1035 | }
1036 | {
1037 | name: 'sharedTimeRange'
1038 | isOptional: true
1039 | }
1040 | ]
1041 | #disable-next-line BCP036
1042 | type: 'Extension/HubsExtension/PartType/MonitorChartPart'
1043 | settings: {}
1044 | }
1045 | }
1046 | ]
1047 | }
1048 | ]
1049 | }
1050 | }
1051 |
--------------------------------------------------------------------------------
/infra/core/database/cosmos/cosmos-account.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING'
6 | param keyVaultName string
7 |
8 | @allowed([ 'GlobalDocumentDB', 'MongoDB', 'Parse' ])
9 | param kind string
10 |
11 | resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = {
12 | name: name
13 | kind: kind
14 | location: location
15 | tags: tags
16 | properties: {
17 | consistencyPolicy: { defaultConsistencyLevel: 'Session' }
18 | locations: [
19 | {
20 | locationName: location
21 | failoverPriority: 0
22 | isZoneRedundant: false
23 | }
24 | ]
25 | databaseAccountOfferType: 'Standard'
26 | enableAutomaticFailover: false
27 | enableMultipleWriteLocations: false
28 | apiProperties: (kind == 'MongoDB') ? { serverVersion: '4.0' } : {}
29 | capabilities: [ { name: 'EnableServerless' } ]
30 | }
31 | }
32 |
33 | resource cosmosConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
34 | parent: keyVault
35 | name: connectionStringKey
36 | properties: {
37 | value: cosmos.listConnectionStrings().connectionStrings[0].connectionString
38 | }
39 | }
40 |
41 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
42 | name: keyVaultName
43 | }
44 |
45 | output connectionStringKey string = connectionStringKey
46 | output endpoint string = cosmos.properties.documentEndpoint
47 | output id string = cosmos.id
48 | output name string = cosmos.name
49 |
--------------------------------------------------------------------------------
/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param keyVaultName string
6 | param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING'
7 |
8 | module cosmos '../../cosmos/cosmos-account.bicep' = {
9 | name: 'cosmos-account'
10 | params: {
11 | name: name
12 | location: location
13 | connectionStringKey: connectionStringKey
14 | keyVaultName: keyVaultName
15 | kind: 'MongoDB'
16 | tags: tags
17 | }
18 | }
19 |
20 | output connectionStringKey string = cosmos.outputs.connectionStringKey
21 | output endpoint string = cosmos.outputs.endpoint
22 | output id string = cosmos.outputs.id
23 |
--------------------------------------------------------------------------------
/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep:
--------------------------------------------------------------------------------
1 | param accountName string
2 | param databaseName string
3 | param location string = resourceGroup().location
4 | param tags object = {}
5 |
6 | param collections array = []
7 | param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING'
8 | param keyVaultName string
9 |
10 | module cosmos 'cosmos-mongo-account.bicep' = {
11 | name: 'cosmos-mongo-account'
12 | params: {
13 | name: accountName
14 | location: location
15 | keyVaultName: keyVaultName
16 | tags: tags
17 | connectionStringKey: connectionStringKey
18 | }
19 | }
20 |
21 | resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-08-15' = {
22 | name: '${accountName}/${databaseName}'
23 | tags: tags
24 | properties: {
25 | resource: { id: databaseName }
26 | }
27 |
28 | resource list 'collections' = [for collection in collections: {
29 | name: collection.name
30 | properties: {
31 | resource: {
32 | id: collection.id
33 | shardKey: { _id: collection.shardKey }
34 | indexes: [ { key: { keys: [ collection.indexKey ] } } ]
35 | }
36 | }
37 | }]
38 |
39 | dependsOn: [
40 | cosmos
41 | ]
42 | }
43 |
44 | output connectionStringKey string = connectionStringKey
45 | output databaseName string = databaseName
46 | output endpoint string = cosmos.outputs.endpoint
47 |
--------------------------------------------------------------------------------
/infra/core/database/cosmos/sql/cosmos-sql-account.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param keyVaultName string
6 |
7 | module cosmos '../../cosmos/cosmos-account.bicep' = {
8 | name: 'cosmos-account'
9 | params: {
10 | name: name
11 | location: location
12 | tags: tags
13 | keyVaultName: keyVaultName
14 | kind: 'GlobalDocumentDB'
15 | }
16 | }
17 |
18 | output connectionStringKey string = cosmos.outputs.connectionStringKey
19 | output endpoint string = cosmos.outputs.endpoint
20 | output id string = cosmos.outputs.id
21 | output name string = cosmos.outputs.name
22 |
--------------------------------------------------------------------------------
/infra/core/database/cosmos/sql/cosmos-sql-db.bicep:
--------------------------------------------------------------------------------
1 | param accountName string
2 | param databaseName string
3 | param location string = resourceGroup().location
4 | param tags object = {}
5 |
6 | param containers array = []
7 | param keyVaultName string
8 | param principalIds array = []
9 |
10 | module cosmos 'cosmos-sql-account.bicep' = {
11 | name: 'cosmos-sql-account'
12 | params: {
13 | name: accountName
14 | location: location
15 | tags: tags
16 | keyVaultName: keyVaultName
17 | }
18 | }
19 |
20 | resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = {
21 | name: '${accountName}/${databaseName}'
22 | properties: {
23 | resource: { id: databaseName }
24 | }
25 |
26 | resource list 'containers' = [for container in containers: {
27 | name: container.name
28 | properties: {
29 | resource: {
30 | id: container.id
31 | partitionKey: { paths: [ container.partitionKey ] }
32 | }
33 | options: {}
34 | }
35 | }]
36 |
37 | dependsOn: [
38 | cosmos
39 | ]
40 | }
41 |
42 | module roleDefintion 'cosmos-sql-role-def.bicep' = {
43 | name: 'cosmos-sql-role-definition'
44 | params: {
45 | accountName: accountName
46 | }
47 | dependsOn: [
48 | cosmos
49 | database
50 | ]
51 | }
52 |
53 | // We need batchSize(1) here because sql role assignments have to be done sequentially
54 | @batchSize(1)
55 | module userRole 'cosmos-sql-role-assign.bicep' = [for principalId in principalIds: if (!empty(principalId)) {
56 | name: 'cosmos-sql-user-role-${uniqueString(principalId)}'
57 | params: {
58 | accountName: accountName
59 | roleDefinitionId: roleDefintion.outputs.id
60 | principalId: principalId
61 | }
62 | dependsOn: [
63 | cosmos
64 | database
65 | ]
66 | }]
67 |
68 | output accountId string = cosmos.outputs.id
69 | output accountName string = cosmos.outputs.name
70 | output connectionStringKey string = cosmos.outputs.connectionStringKey
71 | output databaseName string = databaseName
72 | output endpoint string = cosmos.outputs.endpoint
73 | output roleDefinitionId string = roleDefintion.outputs.id
74 |
--------------------------------------------------------------------------------
/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep:
--------------------------------------------------------------------------------
1 | param accountName string
2 |
3 | param roleDefinitionId string
4 | param principalId string = ''
5 |
6 | resource role 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = {
7 | parent: cosmos
8 | name: guid(roleDefinitionId, principalId, cosmos.id)
9 | properties: {
10 | principalId: principalId
11 | roleDefinitionId: roleDefinitionId
12 | scope: cosmos.id
13 | }
14 | }
15 |
16 | resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
17 | name: accountName
18 | }
19 |
--------------------------------------------------------------------------------
/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep:
--------------------------------------------------------------------------------
1 | param accountName string
2 |
3 | resource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2022-08-15' = {
4 | parent: cosmos
5 | name: guid(cosmos.id, accountName, 'sql-role')
6 | properties: {
7 | assignableScopes: [
8 | cosmos.id
9 | ]
10 | permissions: [
11 | {
12 | dataActions: [
13 | 'Microsoft.DocumentDB/databaseAccounts/readMetadata'
14 | 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
15 | 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'
16 | ]
17 | notDataActions: []
18 | }
19 | ]
20 | roleName: 'Reader Writer'
21 | type: 'CustomRole'
22 | }
23 | }
24 |
25 | resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
26 | name: accountName
27 | }
28 |
29 | output id string = roleDefinition.id
30 |
--------------------------------------------------------------------------------
/infra/core/database/sqlserver/sqlserver.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param appUser string = 'appUser'
6 | param databaseName string
7 | param keyVaultName string
8 | param sqlAdmin string = 'sqlAdmin'
9 | param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING'
10 |
11 | @secure()
12 | param sqlAdminPassword string
13 | @secure()
14 | param appUserPassword string
15 |
16 | resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
17 | name: name
18 | location: location
19 | tags: tags
20 | properties: {
21 | version: '12.0'
22 | minimalTlsVersion: '1.2'
23 | publicNetworkAccess: 'Enabled'
24 | administratorLogin: sqlAdmin
25 | administratorLoginPassword: sqlAdminPassword
26 | }
27 |
28 | resource database 'databases' = {
29 | name: databaseName
30 | location: location
31 | }
32 |
33 | resource firewall 'firewallRules' = {
34 | name: 'Azure Services'
35 | properties: {
36 | // Allow all clients
37 | // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only".
38 | // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes.
39 | startIpAddress: '0.0.0.1'
40 | endIpAddress: '255.255.255.254'
41 | }
42 | }
43 | }
44 |
45 | resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
46 | name: '${name}-deployment-script'
47 | location: location
48 | kind: 'AzureCLI'
49 | properties: {
50 | azCliVersion: '2.37.0'
51 | retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running
52 | timeout: 'PT5M' // Five minutes
53 | cleanupPreference: 'OnSuccess'
54 | environmentVariables: [
55 | {
56 | name: 'APPUSERNAME'
57 | value: appUser
58 | }
59 | {
60 | name: 'APPUSERPASSWORD'
61 | secureValue: appUserPassword
62 | }
63 | {
64 | name: 'DBNAME'
65 | value: databaseName
66 | }
67 | {
68 | name: 'DBSERVER'
69 | value: sqlServer.properties.fullyQualifiedDomainName
70 | }
71 | {
72 | name: 'SQLCMDPASSWORD'
73 | secureValue: sqlAdminPassword
74 | }
75 | {
76 | name: 'SQLADMIN'
77 | value: sqlAdmin
78 | }
79 | ]
80 |
81 | scriptContent: '''
82 | wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2
83 | tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C .
84 |
85 | cat < ./initDb.sql
86 | drop user ${APPUSERNAME}
87 | go
88 | create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}'
89 | go
90 | alter role db_owner add member ${APPUSERNAME}
91 | go
92 | SCRIPT_END
93 |
94 | ./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql
95 | '''
96 | }
97 | }
98 |
99 | resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
100 | parent: keyVault
101 | name: 'sqlAdminPassword'
102 | properties: {
103 | value: sqlAdminPassword
104 | }
105 | }
106 |
107 | resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
108 | parent: keyVault
109 | name: 'appUserPassword'
110 | properties: {
111 | value: appUserPassword
112 | }
113 | }
114 |
115 | resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
116 | parent: keyVault
117 | name: connectionStringKey
118 | properties: {
119 | value: '${connectionString}; Password=${appUserPassword}'
120 | }
121 | }
122 |
123 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
124 | name: keyVaultName
125 | }
126 |
127 | var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}'
128 | output connectionStringKey string = connectionStringKey
129 | output databaseName string = sqlServer::database.name
130 |
--------------------------------------------------------------------------------
/infra/core/gateway/apim-api-policy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {origin}
9 |
10 |
11 | PUT
12 | GET
13 | POST
14 | DELETE
15 | PATCH
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Call to the @(context.Api.Name)
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 | Failed to process the @(context.Api.Name)
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | An unexpected error has occurred.
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/infra/core/gateway/apim.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | @description('The email address of the owner of the service')
6 | @minLength(1)
7 | param publisherEmail string = 'noreply@microsoft.com'
8 |
9 | @description('The name of the owner of the service')
10 | @minLength(1)
11 | param publisherName string = 'n/a'
12 |
13 | @description('The pricing tier of this API Management service')
14 | @allowed([
15 | 'Consumption'
16 | 'Developer'
17 | 'Standard'
18 | 'Premium'
19 | ])
20 | param sku string = 'Consumption'
21 |
22 | @description('The instance size of this API Management service.')
23 | @allowed([ 0, 1, 2 ])
24 | param skuCount int = 0
25 |
26 | @description('Azure Application Insights Name')
27 | param applicationInsightsName string
28 |
29 | resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = {
30 | name: name
31 | location: location
32 | tags: union(tags, { 'azd-service-name': name })
33 | sku: {
34 | name: sku
35 | capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount)
36 | }
37 | properties: {
38 | publisherEmail: publisherEmail
39 | publisherName: publisherName
40 | }
41 | }
42 |
43 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) {
44 | name: 'app-insights-logger'
45 | parent: apimService
46 | properties: {
47 | credentials: {
48 | instrumentationKey: applicationInsights.properties.InstrumentationKey
49 | }
50 | description: 'Logger to Azure Application Insights'
51 | isBuffered: false
52 | loggerType: 'applicationInsights'
53 | resourceId: applicationInsights.id
54 | }
55 | }
56 |
57 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) {
58 | name: applicationInsightsName
59 | }
60 |
61 | output apimServiceName string = apimService.name
62 |
--------------------------------------------------------------------------------
/infra/core/host/appservice-appsettings.bicep:
--------------------------------------------------------------------------------
1 | metadata description = 'Updates app settings for an Azure App Service.'
2 | @description('The name of the app service resource within the current resource group scope')
3 | param name string
4 |
5 | @description('The app settings to be applied to the app service')
6 | @secure()
7 | param appSettings object
8 |
9 | resource appService 'Microsoft.Web/sites@2022-03-01' existing = {
10 | name: name
11 | }
12 |
13 | resource settings 'Microsoft.Web/sites/config@2022-03-01' = {
14 | name: 'appsettings'
15 | parent: appService
16 | properties: appSettings
17 | }
18 |
--------------------------------------------------------------------------------
/infra/core/host/appservice.bicep:
--------------------------------------------------------------------------------
1 | metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.'
2 | param name string
3 | param location string = resourceGroup().location
4 | param tags object = {}
5 |
6 | // Reference Properties
7 | param applicationInsightsName string = ''
8 | param appServicePlanId string
9 | param keyVaultName string = ''
10 | param managedIdentity bool = !empty(keyVaultName)
11 |
12 | // Runtime Properties
13 | @allowed([
14 | 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom'
15 | ])
16 | param runtimeName string
17 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}'
18 | param runtimeVersion string
19 |
20 | // Microsoft.Web/sites Properties
21 | param kind string = 'app,linux'
22 |
23 | // Microsoft.Web/sites/config
24 | param allowedOrigins array = []
25 | param alwaysOn bool = true
26 | param appCommandLine string = ''
27 | @secure()
28 | param appSettings object = {}
29 | param clientAffinityEnabled bool = false
30 | param enableOryxBuild bool = contains(kind, 'linux')
31 | param functionAppScaleLimit int = -1
32 | param linuxFxVersion string = runtimeNameAndVersion
33 | param minimumElasticInstanceCount int = -1
34 | param numberOfWorkers int = -1
35 | param scmDoBuildDuringDeployment bool = false
36 | param use32BitWorkerProcess bool = false
37 | param ftpsState string = 'FtpsOnly'
38 | param healthCheckPath string = ''
39 |
40 | resource appService 'Microsoft.Web/sites@2022-03-01' = {
41 | name: name
42 | location: location
43 | tags: tags
44 | kind: kind
45 | properties: {
46 | serverFarmId: appServicePlanId
47 | siteConfig: {
48 | linuxFxVersion: linuxFxVersion
49 | alwaysOn: alwaysOn
50 | ftpsState: ftpsState
51 | minTlsVersion: '1.2'
52 | appCommandLine: appCommandLine
53 | numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null
54 | minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null
55 | use32BitWorkerProcess: use32BitWorkerProcess
56 | functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
57 | healthCheckPath: healthCheckPath
58 | cors: {
59 | allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
60 | }
61 | }
62 | clientAffinityEnabled: clientAffinityEnabled
63 | httpsOnly: true
64 | }
65 |
66 | identity: { type: managedIdentity ? 'SystemAssigned' : 'None' }
67 |
68 | resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = {
69 | name: 'ftp'
70 | properties: {
71 | allow: false
72 | }
73 | }
74 |
75 | resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = {
76 | name: 'scm'
77 | properties: {
78 | allow: false
79 | }
80 | }
81 | }
82 |
83 | // Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially
84 | // sites/web/config 'appsettings'
85 | module configAppSettings 'appservice-appsettings.bicep' = {
86 | name: '${name}-appSettings'
87 | params: {
88 | name: appService.name
89 | appSettings: union(appSettings,
90 | {
91 | SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment)
92 | ENABLE_ORYX_BUILD: string(enableOryxBuild)
93 | },
94 | runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {},
95 | !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {},
96 | !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {})
97 | }
98 | }
99 |
100 | // sites/web/config 'logs'
101 | resource configLogs 'Microsoft.Web/sites/config@2022-03-01' = {
102 | name: 'logs'
103 | parent: appService
104 | properties: {
105 | applicationLogs: { fileSystem: { level: 'Verbose' } }
106 | detailedErrorMessages: { enabled: true }
107 | failedRequestsTracing: { enabled: true }
108 | httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } }
109 | }
110 | dependsOn: [configAppSettings]
111 | }
112 |
113 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) {
114 | name: keyVaultName
115 | }
116 |
117 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) {
118 | name: applicationInsightsName
119 | }
120 |
121 | output identityPrincipalId string = managedIdentity ? appService.identity.principalId : ''
122 | output name string = appService.name
123 | output uri string = 'https://${appService.properties.defaultHostName}'
124 |
--------------------------------------------------------------------------------
/infra/core/host/appserviceplan.bicep:
--------------------------------------------------------------------------------
1 | metadata description = 'Creates an Azure App Service plan.'
2 | param name string
3 | param location string = resourceGroup().location
4 | param tags object = {}
5 |
6 | param kind string = ''
7 | param reserved bool = true
8 | param sku object
9 |
10 | resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
11 | name: name
12 | location: location
13 | tags: tags
14 | sku: sku
15 | kind: kind
16 | properties: {
17 | reserved: reserved
18 | }
19 | }
20 |
21 | output id string = appServicePlan.id
22 | output name string = appServicePlan.name
23 |
--------------------------------------------------------------------------------
/infra/core/host/functions.bicep:
--------------------------------------------------------------------------------
1 | metadata description = 'Creates an Azure Function (flex consumption) in an existing Azure App Service plan.'
2 | param name string
3 | param location string = resourceGroup().location
4 | param tags object = {}
5 |
6 | // Reference Properties
7 | param applicationInsightsName string = ''
8 | param appServicePlanId string
9 | param keyVaultName string = ''
10 |
11 | // Runtime Properties
12 | @allowed([
13 | 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom'
14 | ])
15 | param runtimeName string
16 | param runtimeVersion string
17 |
18 | // Microsoft.Web/sites Properties
19 | param kind string = 'functionapp,linux'
20 |
21 | // Microsoft.Web/sites/config
22 | param allowedOrigins array = []
23 | param alwaysOn bool = true
24 | param appCommandLine string = ''
25 | @secure()
26 | param appSettings object = {}
27 | param clientAffinityEnabled bool = false
28 | param maximumInstanceCount int = 800
29 | param instanceMemoryMB int = 2048
30 | param minimumElasticInstanceCount int = -1
31 | param numberOfWorkers int = -1
32 | param healthCheckPath string = ''
33 | param storageAccountName string
34 |
35 | resource appService 'Microsoft.Web/sites@2023-12-01' = {
36 | name: name
37 | location: location
38 | tags: tags
39 | kind: kind
40 | properties: {
41 | serverFarmId: appServicePlanId
42 | siteConfig: {
43 | alwaysOn: alwaysOn
44 | minTlsVersion: '1.2'
45 | appCommandLine: appCommandLine
46 | numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null
47 | minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null
48 | healthCheckPath: healthCheckPath
49 | cors: {
50 | allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
51 | }
52 | }
53 | functionAppConfig: {
54 | deployment: {
55 | storage: {
56 | type: 'blobContainer'
57 | value: '${storage.properties.primaryEndpoints.blob}${name}'
58 | authentication: {
59 | type: 'SystemAssignedIdentity'
60 | }
61 | }
62 | }
63 | scaleAndConcurrency: {
64 | maximumInstanceCount: maximumInstanceCount
65 | instanceMemoryMB: instanceMemoryMB
66 | }
67 | runtime: {
68 | name: runtimeName
69 | version: runtimeVersion
70 | }
71 | }
72 | clientAffinityEnabled: clientAffinityEnabled
73 | httpsOnly: true
74 | }
75 |
76 | identity: { type: 'SystemAssigned' }
77 | }
78 |
79 | // Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially
80 | // sites/web/config 'appsettings'
81 | module configAppSettings 'appservice-appsettings.bicep' = {
82 | name: '${name}-appSettings'
83 | params: {
84 | name: appService.name
85 | appSettings: union(appSettings,
86 | runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {},
87 | !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {},
88 | !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {})
89 | }
90 | }
91 |
92 | // sites/web/config 'logs'
93 | resource configLogs 'Microsoft.Web/sites/config@2022-03-01' = {
94 | name: 'logs'
95 | parent: appService
96 | properties: {
97 | applicationLogs: { fileSystem: { level: 'Verbose' } }
98 | detailedErrorMessages: { enabled: true }
99 | failedRequestsTracing: { enabled: true }
100 | httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } }
101 | }
102 | dependsOn: [configAppSettings]
103 | }
104 |
105 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) {
106 | name: keyVaultName
107 | }
108 |
109 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) {
110 | name: applicationInsightsName
111 | }
112 |
113 | resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = {
114 | name: storageAccountName
115 | }
116 |
117 | var storageContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
118 |
119 | resource storageContainer 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
120 | scope: storage // Use when specifying a scope that is different than the deployment scope
121 | name: guid(subscription().id, resourceGroup().id, appService.id, storageContributorRole)
122 | properties: {
123 | roleDefinitionId: storageContributorRole
124 | principalType: 'ServicePrincipal'
125 | principalId: appService.identity.principalId
126 | }
127 | }
128 |
129 |
130 | output identityPrincipalId string = appService.identity.principalId
131 | output name string = appService.name
132 | output uri string = 'https://${appService.properties.defaultHostName}'
133 |
--------------------------------------------------------------------------------
/infra/core/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/core/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 |
8 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
9 | name: name
10 | location: location
11 | tags: tags
12 | kind: 'web'
13 | properties: {
14 | Application_Type: 'web'
15 | WorkspaceResourceId: logAnalyticsWorkspaceId
16 | }
17 | }
18 |
19 | module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = {
20 | name: 'application-insights-dashboard'
21 | params: {
22 | name: dashboardName
23 | location: location
24 | applicationInsightsName: applicationInsights.name
25 | }
26 | }
27 |
28 | output connectionString string = applicationInsights.properties.ConnectionString
29 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey
30 | output name string = applicationInsights.name
31 |
--------------------------------------------------------------------------------
/infra/core/monitor/loganalytics.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
6 | name: name
7 | location: location
8 | tags: tags
9 | properties: any({
10 | retentionInDays: 30
11 | features: {
12 | searchVersion: 1
13 | }
14 | sku: {
15 | name: 'PerGB2018'
16 | }
17 | })
18 | }
19 |
20 | output id string = logAnalytics.id
21 | output name string = logAnalytics.name
22 |
--------------------------------------------------------------------------------
/infra/core/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 |
7 | module logAnalytics 'loganalytics.bicep' = {
8 | name: 'loganalytics'
9 | params: {
10 | name: logAnalyticsName
11 | location: location
12 | tags: tags
13 | }
14 | }
15 |
16 | module applicationInsights 'applicationinsights.bicep' = {
17 | name: 'applicationinsights'
18 | params: {
19 | name: applicationInsightsName
20 | location: location
21 | tags: tags
22 | dashboardName: applicationInsightsDashboardName
23 | logAnalyticsWorkspaceId: logAnalytics.outputs.id
24 | }
25 | }
26 |
27 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString
28 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey
29 | output applicationInsightsName string = applicationInsights.outputs.name
30 | output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
31 | output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
32 |
--------------------------------------------------------------------------------
/infra/core/security/keyvault-access.bicep:
--------------------------------------------------------------------------------
1 | param name string = 'add'
2 |
3 | param keyVaultName string = ''
4 | param permissions object = { secrets: [ 'get', 'list' ] }
5 | param principalId string
6 |
7 | resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = {
8 | parent: keyVault
9 | name: name
10 | properties: {
11 | accessPolicies: [ {
12 | objectId: principalId
13 | tenantId: subscription().tenantId
14 | permissions: permissions
15 | } ]
16 | }
17 | }
18 |
19 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
20 | name: keyVaultName
21 | }
22 |
--------------------------------------------------------------------------------
/infra/core/security/keyvault-secret.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param tags object = {}
3 | param keyVaultName string
4 | param contentType string = 'string'
5 | @description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates')
6 | @secure()
7 | param secretValue string
8 |
9 | param enabled bool = true
10 | param exp int = 0
11 | param nbf int = 0
12 |
13 | resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
14 | name: name
15 | tags: tags
16 | parent: keyVault
17 | properties: {
18 | attributes: {
19 | enabled: enabled
20 | exp: exp
21 | nbf: nbf
22 | }
23 | contentType: contentType
24 | value: secretValue
25 | }
26 | }
27 |
28 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
29 | name: keyVaultName
30 | }
31 |
--------------------------------------------------------------------------------
/infra/core/security/keyvault.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param principalId string = ''
6 |
7 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
8 | name: name
9 | location: location
10 | tags: tags
11 | properties: {
12 | tenantId: subscription().tenantId
13 | sku: { family: 'A', name: 'standard' }
14 | accessPolicies: !empty(principalId) ? [
15 | {
16 | objectId: principalId
17 | permissions: { secrets: [ 'get', 'list' ] }
18 | tenantId: subscription().tenantId
19 | }
20 | ] : []
21 | }
22 | }
23 |
24 | output endpoint string = keyVault.properties.vaultUri
25 | output name string = keyVault.name
26 |
--------------------------------------------------------------------------------
/infra/core/storage/storage-account.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param allowBlobPublicAccess bool = false
6 | param containers array = []
7 | param kind string = 'StorageV2'
8 | param minimumTlsVersion string = 'TLS1_2'
9 | param sku object = { name: 'Standard_LRS' }
10 |
11 | resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = {
12 | name: name
13 | location: location
14 | tags: tags
15 | kind: kind
16 | sku: sku
17 | properties: {
18 | minimumTlsVersion: minimumTlsVersion
19 | allowBlobPublicAccess: allowBlobPublicAccess
20 | networkAcls: {
21 | bypass: 'AzureServices'
22 | defaultAction: 'Allow'
23 | }
24 | }
25 |
26 | resource blobServices 'blobServices' = if (!empty(containers)) {
27 | name: 'default'
28 | resource container 'containers' = [for container in containers: {
29 | name: container.name
30 | properties: {
31 | publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None'
32 | }
33 | }]
34 | }
35 | }
36 |
37 | output name string = storage.name
38 | output primaryEndpoints object = storage.properties.primaryEndpoints
39 |
--------------------------------------------------------------------------------
/infra/main.bicep:
--------------------------------------------------------------------------------
1 | targetScope = 'subscription'
2 |
3 | @minLength(1)
4 | @maxLength(64)
5 | @description('Name which is used to generate a short unique hash for each resource')
6 | param name string
7 |
8 | // Constrained due to Flex plan limitations
9 | // https://learn.microsoft.com/azure/azure-functions/flex-consumption-how-to#view-currently-supported-regions
10 | @minLength(1)
11 | @description('Primary location for all resources')
12 | @allowed(['australiaeast', 'eastasia', 'eastus', 'eastus2', 'northeurope', 'southcentralus', 'southeastasia', 'swedencentral', 'uksouth', 'westus2', 'eastus2euap'])
13 | @metadata({
14 | azd: {
15 | type: 'location'
16 | }
17 | })
18 | param location string
19 |
20 | @description('The email address of the owner of the service')
21 | @minLength(1)
22 | param publisherEmail string
23 |
24 | @description('The name of the owner of the service')
25 | @minLength(1)
26 | param publisherName string
27 |
28 | var resourceToken = toLower(uniqueString(subscription().id, name, location))
29 | var tags = { 'azd-env-name': name }
30 |
31 | resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
32 | name: '${name}-rg'
33 | location: location
34 | tags: tags
35 | }
36 |
37 | var prefix = '${name}-${resourceToken}'
38 | var functionAppName = '${take(prefix, 19)}-funcapp'
39 |
40 | // Monitor application with Azure Monitor
41 | module monitoring './core/monitor/monitoring.bicep' = {
42 | name: 'monitoring'
43 | scope: resourceGroup
44 | params: {
45 | location: location
46 | tags: tags
47 | logAnalyticsName: '${prefix}-logworkspace'
48 | applicationInsightsName: '${prefix}-appinsights'
49 | applicationInsightsDashboardName: 'appinsights-dashboard'
50 | }
51 | }
52 |
53 | // Backing storage for Azure functions backend API
54 | var validStoragePrefix = toLower(take(replace(prefix, '-', ''), 17))
55 | module storageAccount 'core/storage/storage-account.bicep' = {
56 | name: 'storage'
57 | scope: resourceGroup
58 | params: {
59 | name: '${validStoragePrefix}storage'
60 | location: location
61 | tags: tags
62 | containers: [
63 | {name: functionAppName}
64 | ]
65 | }
66 | }
67 |
68 |
69 | // Create an App Service Plan to group applications under the same payment plan and SKU
70 | module appServicePlan './core/host/appserviceplan.bicep' = {
71 | name: 'appserviceplan'
72 | scope: resourceGroup
73 | params: {
74 | name: '${prefix}-plan'
75 | location: location
76 | tags: tags
77 | kind: 'functionapp'
78 | sku: {
79 | name: 'FC1'
80 | tier: 'FlexConsumption'
81 | }
82 | }
83 | }
84 |
85 | module functionApp 'core/host/functions.bicep' = {
86 | name: 'function-app'
87 | scope: resourceGroup
88 | params: {
89 | name: functionAppName
90 | location: location
91 | tags: union(tags, { 'azd-service-name': 'api' })
92 | alwaysOn: false
93 | appSettings: {
94 | FUNCTIONS_EXTENSION_VERSION: '~4'
95 | AzureWebJobsStorage__accountName: storageAccount.outputs.name
96 | AzureWebJobsStorage__blobServiceUri: storageAccount.outputs.primaryEndpoints.blob
97 | RUNNING_IN_PRODUCTION: 'true'
98 | }
99 | applicationInsightsName: monitoring.outputs.applicationInsightsName
100 | appServicePlanId: appServicePlan.outputs.id
101 | runtimeName: 'python'
102 | runtimeVersion: '3.10'
103 | storageAccountName: storageAccount.outputs.name
104 | }
105 | }
106 |
107 | module diagnostics 'app-diagnostics.bicep' = {
108 | name: 'function-diagnostics'
109 | scope: resourceGroup
110 | params: {
111 | appName: functionApp.outputs.name
112 | kind: 'functionapp'
113 | diagnosticWorkspaceId: monitoring.outputs.logAnalyticsWorkspaceId
114 | }
115 | }
116 |
117 | // Creates Azure API Management (APIM) service to mediate the requests between the frontend and the backend API
118 | module apim './core/gateway/apim.bicep' = {
119 | name: 'apim-deployment'
120 | scope: resourceGroup
121 | params: {
122 | name: '${take(prefix, 18)}-function-app-apim'
123 | location: location
124 | tags: tags
125 | applicationInsightsName: monitoring.outputs.applicationInsightsName
126 | publisherEmail: publisherEmail
127 | publisherName: publisherName
128 | }
129 | }
130 |
131 | // Configures the API in the Azure API Management (APIM) service
132 | module apimAPI 'apimanagement.bicep' = {
133 | name: 'apimanagement-resources'
134 | scope: resourceGroup
135 | params: {
136 | apimServiceName: apim.outputs.apimServiceName
137 | functionAppName: functionApp.outputs.name
138 | }
139 | dependsOn: [
140 | functionApp
141 | ]
142 | }
143 |
144 |
145 | output SERVICE_API_ENDPOINTS array = ['${apimAPI.outputs.apimServiceUrl}/public/docs']
146 |
--------------------------------------------------------------------------------
/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 | "name": {
6 | "value": "${AZURE_ENV_NAME}"
7 | },
8 | "location": {
9 | "value": "${AZURE_LOCATION}"
10 | },
11 | "publisherEmail": {
12 | "value": "your-email@your-domain.com"
13 | },
14 | "publisherName": {
15 | "value": "Your API Provider Organization Name"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "FUNCTIONS_WORKER_RUNTIME": "python",
5 | "AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 120
3 | target-version = ['py310']
4 |
5 | [tool.ruff]
6 | target-version = "py310"
7 | line-length = 120
8 |
9 | [tool.ruff.lint]
10 | select = ["E", "F", "I", "UP"]
11 |
12 | [tool.pytest.ini_options]
13 | addopts = "-ra --cov --cov-fail-under=100"
14 |
--------------------------------------------------------------------------------
/readme_diagram_apim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pamelafox/fastapi-azure-function-apim/61a26416b647406d7fa059fd0abf7103e19d6f63/readme_diagram_apim.png
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | fastapi[all]
3 | black
4 | ruff
5 | pytest
6 | coverage
7 | pytest-cov
8 | pre-commit
9 | uvicorn[standard]
10 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | azure-functions==1.20.0
2 | fastapi==0.111.0
3 |
--------------------------------------------------------------------------------
/tests/test_env.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | import pytest
4 | from fastapi.testclient import TestClient
5 |
6 | from api.fastapi_app import create_app
7 |
8 |
9 | @pytest.fixture
10 | def mock_functions_env(monkeypatch):
11 | monkeypatch.setenv("RUNNING_IN_PRODUCTION", "true")
12 |
13 |
14 | def test_functions_env(mock_functions_env):
15 | random.seed(1)
16 | client = TestClient(create_app())
17 | response = client.get("/generate_name")
18 | assert response.status_code == 200
19 | assert response.json() == {"name": "Margaret"}
20 |
21 |
22 | def test_functions_openapi(mock_functions_env):
23 | client = TestClient(create_app())
24 | response = client.get("/openapi.json")
25 | assert response.status_code == 200
26 | body = response.json()
27 | assert body["servers"][0]["url"] == "/api"
28 |
29 |
30 | def test_functions_docs(mock_functions_env):
31 | client = TestClient(create_app())
32 | response = client.get("/docs")
33 | assert response.status_code == 200
34 | assert b"/public/openapi.json" in response.content
35 |
--------------------------------------------------------------------------------
/tests/test_fastapi.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | import pytest
4 | from fastapi.testclient import TestClient
5 |
6 | from api.fastapi_app import create_app
7 |
8 |
9 | @pytest.fixture
10 | def client():
11 | return TestClient(create_app())
12 |
13 |
14 | def test_generate_name(client):
15 | random.seed(1)
16 | response = client.get("/generate_name")
17 | assert response.status_code == 200
18 | assert response.json() == {"name": "Margaret"}
19 |
20 |
21 | def test_generate_name_params(client):
22 | random.seed(1)
23 | response = client.get("/generate_name", params={"starts_with": "n"})
24 | assert response.status_code == 200
25 | assert response.json() == {"name": "Noa"}
26 |
27 |
28 | def test_docs(client):
29 | response = client.get("/docs")
30 | assert response.status_code == 200
31 |
32 |
33 | def test_openapi(client):
34 | response = client.get("/openapi.json")
35 | assert response.status_code == 200
36 |
--------------------------------------------------------------------------------