├── .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 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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 | ![Architecture diagram for API Management Service to Function App to FastAPI](readme_diagram_apim.png) 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 | --------------------------------------------------------------------------------