diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a4ac096..1da2619 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,27 @@ -ARG VARIANT=bullseye -FROM --platform=amd64 mcr.microsoft.com/devcontainers/python:0-${VARIANT} +FROM mcr.microsoft.com/vscode/devcontainers/universal:latest + +# Copy custom first notice message. +COPY first-run-notice.txt /tmp/staging/ +RUN sudo mv -f /tmp/staging/first-run-notice.txt /usr/local/etc/vscode-dev-containers/ \ + && sudo rm -rf /tmp/staging + +# Install PowerShell 7.x +RUN sudo apt-get update \ + && sudo apt-get install -y wget apt-transport-https software-properties-common \ + && wget -q https://packages.microsoft.com/config/ubuntu/$(. /etc/os-release && echo $VERSION_ID)/packages-microsoft-prod.deb \ + && sudo dpkg -i packages-microsoft-prod.deb \ + && sudo apt-get update \ + && sudo apt-get install -y powershell + +# Install Azure Functions Core Tools RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \ - && mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \ - && 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' \ - && apt-get update && apt-get install -y azure-functions-core-tools-4 \ No newline at end of file + && sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \ + && sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' \ + && sudo apt-get update \ + && sudo apt-get install -y azure-functions-core-tools-4 + +# Install Azure Developer CLI +RUN curl -fsSL https://aka.ms/install-azd.sh | bash + +# Install mechanical-markdown for quickstart validations +RUN pip install mechanical-markdown \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cc060d5..de9cfee 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,34 +1,35 @@ { - "name": "FastAPI on Azure Functions", - "build": { - "dockerfile": "Dockerfile", - "args": { - "VARIANT": "3.10-bullseye" - } - }, - "forwardPorts": [8000, 7071], + "name": "Functions Quickstarts Codespace", + "dockerFile": "Dockerfile", "features": { - "ghcr.io/devcontainers/features/node:1": { - "version": "16", - "nodeGypDependencies": false - }, - "ghcr.io/azure/azure-dev/azd:latest": {} + "azure-cli": "latest" }, "customizations": { - "vscode": { - "extensions": [ - "ms-azuretools.azure-dev", - "ms-azuretools.vscode-bicep", - "ms-vscode.vscode-node-azure-pack", - "ms-python.python", - "ms-azuretools.vscode-azurefunctions" - ] - } + "vscode": { + "extensions": [ + "ms-azuretools.vscode-bicep", + "ms-azuretools.vscode-docker", + "ms-azuretools.vscode-azurefunctions", + "GitHub.copilot", + "humao.rest-client" + ] + } }, - "postCreateCommand": "python3 -m venv .venv", - "postAttachCommand": ". .venv/bin/activate", - "remoteUser": "vscode", - "hostRequirements": { - "memory": "8gb" - } -} + "mounts": [ + // Mount docker-in-docker library volume + "source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume" + ], + // Always run image-defined docker-init.sh to enable docker-in-docker + "overrideCommand": false, + "remoteUser": "codespace", + "runArgs": [ + // Enable ptrace-based debugging for Go in container + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined", + + // Enable docker-in-docker configuration + "--init", + "--privileged" + ] + } diff --git a/.devcontainer/first-run-notice.txt b/.devcontainer/first-run-notice.txt new file mode 100644 index 0000000..2dc5474 --- /dev/null +++ b/.devcontainer/first-run-notice.txt @@ -0,0 +1,4 @@ +👋 Welcome to the Functions Codespace! You are on the Functions Quickstarts image. +It includes everything needed to run through our tutorials and quickstart applications. + +📚 Functions docs can be found at: https://learn.microsoft.com/en-us/azure/azure-functions/ \ No newline at end of file diff --git a/.github/workflows/security-scans.yml b/.github/workflows/security-scans.yml new file mode 100644 index 0000000..8a5f711 --- /dev/null +++ b/.github/workflows/security-scans.yml @@ -0,0 +1,108 @@ +name: Security Scans + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '0 0 * * 0' # Weekly scan + +permissions: + actions: read + contents: read + security-events: write + +jobs: + codeql: + name: CodeQL Analysis + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v3 + + secret-scan: + name: Secret Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + python-security: + name: Python Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install bandit[toml] safety + + - name: Run Bandit security linter + run: | + bandit -r . -f json -o bandit-results.json || true + + - name: Run Safety security scanner + run: | + safety check --json --output safety-results.json || true + + - name: Upload security scan results + uses: actions/upload-artifact@v3 + if: always() + with: + name: security-scan-results + path: | + bandit-results.json + safety-results.json \ No newline at end of file diff --git a/README.md b/README.md index d801b02..4229e5e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # Using FastAPI Framework with Azure Functions -Azure Functions supports WSGI and ASGI-compatible frameworks with HTTP-triggered Python functions. This can be helpful if you are familiar with a particular framework, or if you have existing code you would like to reuse to create the Function app. The following is an example of creating an Azure Function app using FastAPI. +Azure Functions supports WSGI and ASGI-compatible frameworks with HTTP-triggered Python functions. This sample demonstrates creating an Azure Function app using FastAPI with the Azure Functions Python v2 programming model and modern infrastructure including Flex Consumption plan, managed identity, and optional VNet integration. + +## Features + +- **Azure Functions Python v2 Programming Model**: Uses direct function decorators, no function.json files needed +- **Native FastAPI Support**: Leverages Azure Functions' built-in FastAPI integration +- **Flex Consumption Plan**: Modern, scalable hosting plan replacing the deprecated Y1 plan +- **Managed Identity**: Secure authentication without connection strings +- **Optional VNet Integration**: Enhanced security with virtual network isolation +- **Modern Infrastructure**: Uses Azure Verified Modules (AVM) for secure and compliant resource deployment ## Prerequisites -You can develop and deploy a function app using either Visual Studio Code or the Azure CLI. Make sure you have the required prerequisites for your preferred environment: ++ [Python 3.12](https://www.python.org/) ++ [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local?pivots=programming-language-python#install-the-azure-functions-core-tools) ++ [Azure Developer CLI (AZD)](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd) ++ To use Visual Studio Code to run and debug locally: + + [Visual Studio Code](https://code.visualstudio.com/) + + [Azure Functions extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) -* [Prerequisites for VS Code](https://docs.microsoft.com/azure/azure-functions/create-first-function-vs-code-python#configure-your-environment) -* [Prerequisites for Azure CLI](https://docs.microsoft.com/azure/azure-functions/create-first-function-cli-python#configure-your-local-environment) +## Getting Started -## Setup +You can initialize a project from this template in one of these ways: -Clone or download [this sample's repository](https://github.com/Azure-Samples/fastapi-on-azure-functions/), and open the `fastapi-on-azure-functions` folder in Visual Studio Code or your preferred editor (if you're using the Azure CLI). ++ Use this `azd init` command from an empty local (root) folder: -## Using FastAPI Framework in an Azure Function App + ```shell + azd init --template fastapi-on-azure-functions + ``` -The code in the sample folder has already been updated to support use of the FastAPI. Let's walk through the changed files. + Supply an environment name, such as `fastapiapp` when prompted. In `azd`, the environment is used to maintain a unique deployment context for your app. -The `requirements.txt` file has an additional dependency of the `fastapi` module: ++ Clone the GitHub template repository locally using the `git clone` command: -``` -azure-functions -fastapi -``` + ```shell + git clone https://github.com/Azure-Samples/fastapi-on-azure-functions.git + cd fastapi-on-azure-functions + ``` + +## Local Development +### Setup Local Environment -The file host.json includes the a `routePrefix` key with a value of empty string. +Add a file named `local.settings.json` in the root of your project with the following contents: ```json { - "version": "2.0", - "extensions": { - "http": { - "routePrefix": "" + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "python" } - } } ``` +### Create Virtual Environment -The root folder contains `function_app.py` which initializes an `AsgiFunctionApp` using the imported `FastAPI` app: +The way that you create your virtual environment depends on your operating system. +Open the terminal, navigate to the project folder, and run these commands: -```python -import azure.functions as func +#### Linux/macOS/bash + +```bash +python -m venv .venv +source .venv/bin/activate +``` -from WrapperFunction import app as fastapi_app +#### Windows (Cmd) -app = func.AsgiFunctionApp(app=fastapi_app, http_auth_level=func.AuthLevel.ANONYMOUS) +```shell +py -m venv .venv +.venv\scripts\activate ``` -In the `WrapperFunction` folder, the `__init__.py` file defines a FastAPI app in the typical way (no changes needed): +### Run Local Development Server + +1. Install the necessary requirements: + + ```shell + pip install -r requirements.txt + ``` + +2. Start the Functions host locally: + + ```shell + func start + ``` + +3. Test the endpoints: + + - GET endpoint: + - Parameterized endpoint: + +## Azure Functions Python v2 Model + +This sample uses the Azure Functions Python v2 programming model with direct FastAPI integration: ```python import azure.functions as func +from fastapi import FastAPI -import fastapi - -app = fastapi.FastAPI() +# Initialize FastAPI app +fastapi_app = FastAPI() -@app.get("/sample") +@fastapi_app.get("/sample") async def index(): return { "info": "Try /hello/Shivani for parameterized route.", } - -@app.get("/hello/{name}") +@fastapi_app.get("/hello/{name}") async def get_name(name: str): return { "name": name, } -``` -## Running the sample - -### Testing locally - -1. Create a [Python virtual environment](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments) and activate it. +# Azure Functions app using ASGI with FastAPI +app = func.AsgiFunctionApp(app=fastapi_app, http_auth_level=func.AuthLevel.ANONYMOUS) +``` -2. Run the command below to install the necessary requirements. +Key advantages of this approach: +- **No function.json files**: Configuration is done through decorators +- **Native FastAPI support**: No custom wrappers needed +- **Simplified project structure**: Single file for function definitions +- **Better development experience**: Full FastAPI features supported - ```log - python -m pip install -r requirements.txt - ``` +## Deployment to Azure -3. If you are using VS Code for development, click the "Run and Debug" button or follow [the instructions for running a function locally](https://docs.microsoft.com/azure/azure-functions/create-first-function-vs-code-python#run-the-function-locally). Outside of VS Code, follow [these instructions for using Core Tools commands directly to run the function locally](https://docs.microsoft.com/azure/azure-functions/functions-run-local?tabs=v4%2Cwindows%2Cpython%2Cportal%2Cbash#start). +Deploy using Azure Developer CLI: -4. Once the function is running, test the function at the local URL displayed in the Terminal panel: -======= -```log -Functions: - http_app_func: [GET,POST,DELETE,HEAD,PATCH,PUT,OPTIONS] http://localhost:7071//{*route} +```shell +azd up ``` - ```log - Functions: - WrapperFunction: [GET,POST] http://localhost:7071/{*route} - ``` +This command provisions the function app with required Azure resources and deploys your code. By default, the deployment includes: - Try out URLs corresponding to the handlers in the app, both the simple path and the parameterized path: +- **Flex Consumption Plan**: Modern scaling with FC1 tier +- **Managed Identity**: Secure authentication for Azure resources +- **Virtual Network**: Optional network isolation (enabled by default) +- **Application Insights**: Monitoring and diagnostics +- **Storage Account**: Required for Functions runtime - ``` - http://localhost:7071/sample - http://localhost:7071/hello/YourName - ``` +### VNet Configuration -### Deploying to Azure +By default, this sample deploys with a virtual network (VNet) for enhanced security. The `VNET_ENABLED` parameter controls this: +- When `VNET_ENABLED` is `true` (default), resources are deployed with VNet isolation +- When `VNET_ENABLED` is `false`, resources are deployed with public access -There are three main ways to deploy this to Azure: +To disable VNet for this sample: +```bash +azd env set VNET_ENABLED false +azd up +``` -* [Deploy with the VS Code Azure Functions extension](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-python#publish-the-project-to-azure). -* [Deploy with the Azure CLI](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-python?tabs=azure-cli%2Cbash%2Cbrowser#create-supporting-azure-resources-for-your-function). -* Deploy with the Azure Developer CLI: After [installing the `azd` tool](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd?tabs=localinstall%2Cwindows%2Cbrew), run `azd up` in the root of the project. You can also run `azd pipeline config` to set up a CI/CD pipeline for deployment. +## Infrastructure -All approaches will provision a Function App, Storage account (to store the code), and a Log Analytics workspace. +The infrastructure uses Azure Verified Modules (AVM) for secure, compliant deployments: -![Azure resources created by the deployment: Function App, Storage Account, Log Analytics workspace](./readme_diagram.png) +- **Function App**: Flex Consumption plan with Python 3.12 runtime +- **Storage Account**: Secure storage with managed identity authentication +- **Application Insights**: Monitoring with AAD authentication +- **Virtual Network**: Optional network isolation +- **Private Endpoints**: Secure connectivity when VNet is enabled -### Testing in Azure +## Testing in Azure After deployment, test these different paths on the deployed URL: ``` -http://.azurewebsites.net/sample -http://.azurewebsites.net/hello/Foo +https://.azurewebsites.net/sample +https://.azurewebsites.net/hello/FastAPI ``` -You can call the URL endpoints using your browser (GET requests) or one one of these HTTP test tools: -- [Visual Studio Code](https://code.visualstudio.com/download) with an [extension from Visual Studio Marketplace](https://marketplace.visualstudio.com/vscode) +You can test using: +- [Visual Studio Code with REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) - [PowerShell Invoke-RestMethod](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/invoke-restmethod) -- [Microsoft Edge - Network Console tool](https://learn.microsoft.com/microsoft-edge/devtools-guide-chromium/network-console/network-console-tool) -- [Bruno](https://www.usebruno.com/) - [curl](https://curl.se/) +- [Bruno](https://www.usebruno.com/) > [!CAUTION] > For scenarios where you have sensitive data, such as credentials, secrets, access tokens, > API keys, and other similar information, make sure to use a tool that protects your data > with the necessary security features, works offline or locally, doesn't sync your data to -> the cloud, and doesn't require that you sign in to an online account. This way, you reduce -> the risk around exposing sensitive data to the public. +> the cloud, and doesn't require that you sign in to an online account. + +## Clean Up Resources + +When you're done, you can delete the function app and related resources from Azure to avoid incurring further costs: + +```shell +azd down +``` ## Next Steps -Now you have a simple Azure Function App using the FastAPI framework, and you can continue building on it to develop more sophisticated applications. +Now you have a modern Azure Function App using the FastAPI framework with Azure Functions Python v2 programming model. You can continue building on it to develop more sophisticated applications. -To learn more about leveraging WSGI and ASGI-compatible frameworks, see [Web frameworks](https://docs.microsoft.com/azure/azure-functions/functions-reference-python?tabs=asgi%2Cazurecli-linux%2Capplication-level#web-frameworks). +To learn more: +- [Azure Functions Python v2 Programming Model](https://learn.microsoft.com/azure/azure-functions/functions-reference-python?tabs=get-started%2Casgi%2Capplication-level&pivots=python-mode-decorators) +- [FastAPI Integration with Azure Functions](https://learn.microsoft.com/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-python#example) +- [Flex Consumption Plan](https://learn.microsoft.com/azure/azure-functions/flex-consumption-plan) diff --git a/WrapperFunction/__init__.py b/WrapperFunction/__init__.py deleted file mode 100644 index 940ceaf..0000000 --- a/WrapperFunction/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -import azure.functions as func - -import fastapi - -app = fastapi.FastAPI() - -@app.get("/sample") -async def index(): - return { - "info": "Try /hello/Shivani for parameterized route.", - } - - -@app.get("/hello/{name}") -async def get_name(name: str): - return { - "name": name, - } diff --git a/azure.yaml b/azure.yaml index 331f478..c4ec3f8 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,8 +1,6 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json name: fastapi-on-azure-functions -metadata: - template: fastapi-on-azure-functions@0.0.1-beta services: api: project: . diff --git a/function_app.py b/function_app.py index 9e57e5e..882a56f 100644 --- a/function_app.py +++ b/function_app.py @@ -1,5 +1,20 @@ import azure.functions as func +from fastapi import FastAPI -from WrapperFunction import app as fastapi_app +# Initialize FastAPI app +fastapi_app = FastAPI() +@fastapi_app.get("/sample") +async def index(): + return { + "info": "Try /hello/Shivani for parameterized route.", + } + +@fastapi_app.get("/hello/{name}") +async def get_name(name: str): + return { + "name": name, + } + +# Azure Functions app using ASGI with FastAPI app = func.AsgiFunctionApp(app=fastapi_app, http_auth_level=func.AuthLevel.ANONYMOUS) diff --git a/host.json b/host.json index 77e4fae..b7b8192 100644 --- a/host.json +++ b/host.json @@ -10,7 +10,7 @@ }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[2.*, 3.0.0)" + "version": "[3.*, 4.0.0)" }, "extensions": { diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 0000000..a4fc9df --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,135 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} \ No newline at end of file diff --git a/infra/app/api.bicep b/infra/app/api.bicep new file mode 100644 index 0000000..df85d9f --- /dev/null +++ b/infra/app/api.bicep @@ -0,0 +1,109 @@ +param name string +@description('Primary location for all resources & Flex Consumption Function App') +param location string = resourceGroup().location +param tags object = {} +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param runtimeName string +param runtimeVersion string +param serviceName string = 'api' +param storageAccountName string +param deploymentStorageContainerName string +param virtualNetworkSubnetId string = '' +param instanceMemoryMB int = 2048 +param maximumInstanceCount int = 100 +param identityId string = '' +param identityClientId string = '' +param enableBlob bool = true +param enableQueue bool = false +param enableTable bool = false +param enableFile bool = false + +@allowed(['SystemAssigned', 'UserAssigned']) +param identityType string = 'UserAssigned' + +var applicationInsightsIdentity = 'ClientId=${identityClientId};Authorization=AAD' +var kind = 'functionapp,linux' + +// Create base application settings +var baseAppSettings = { + // Only include required credential settings unconditionally + AzureWebJobsStorage__credential: 'managedidentity' + AzureWebJobsStorage__clientId: identityClientId + + // Application Insights settings are always included + APPLICATIONINSIGHTS_AUTHENTICATION_STRING: applicationInsightsIdentity + APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString +} + +// Dynamically build storage endpoint settings based on feature flags +var blobSettings = enableBlob ? { AzureWebJobsStorage__blobServiceUri: stg.properties.primaryEndpoints.blob } : {} +var queueSettings = enableQueue ? { AzureWebJobsStorage__queueServiceUri: stg.properties.primaryEndpoints.queue } : {} +var tableSettings = enableTable ? { AzureWebJobsStorage__tableServiceUri: stg.properties.primaryEndpoints.table } : {} +var fileSettings = enableFile ? { AzureWebJobsStorage__fileServiceUri: stg.properties.primaryEndpoints.file } : {} + +// Merge all app settings +var allAppSettings = union( + appSettings, + blobSettings, + queueSettings, + tableSettings, + fileSettings, + baseAppSettings +) + +resource stg 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { + name: storageAccountName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { + name: applicationInsightsName +} + +// Create a Flex Consumption Function App to host the API +module api 'br/public:avm/res/web/site:0.15.1' = { + name: '${serviceName}-flex-consumption' + params: { + kind: kind + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + serverFarmResourceId: appServicePlanId + managedIdentities: { + systemAssigned: identityType == 'SystemAssigned' + userAssignedResourceIds: [ + '${identityId}' + ] + } + functionAppConfig: { + deployment: { + storage: { + type: 'blobContainer' + value: '${stg.properties.primaryEndpoints.blob}${deploymentStorageContainerName}' + authentication: { + type: identityType == 'SystemAssigned' ? 'SystemAssignedIdentity' : 'UserAssignedIdentity' + userAssignedIdentityResourceId: identityType == 'UserAssigned' ? identityId : '' + } + } + } + scaleAndConcurrency: { + instanceMemoryMB: instanceMemoryMB + maximumInstanceCount: maximumInstanceCount + } + runtime: { + name: runtimeName + version: runtimeVersion + } + } + siteConfig: { + alwaysOn: false + } + virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null + appSettingsKeyValuePairs: allAppSettings + } +} + +output SERVICE_API_NAME string = api.outputs.name +// Ensure output is always string, handle potential null from module output if SystemAssigned is not used +output SERVICE_API_IDENTITY_PRINCIPAL_ID string = identityType == 'SystemAssigned' ? api.outputs.?systemAssignedMIPrincipalId ?? '' : '' \ No newline at end of file diff --git a/infra/app/rbac.bicep b/infra/app/rbac.bicep new file mode 100644 index 0000000..d9c11eb --- /dev/null +++ b/infra/app/rbac.bicep @@ -0,0 +1,110 @@ +param storageAccountName string +param appInsightsName string +param managedIdentityPrincipalId string // Principal ID for the Managed Identity +param userIdentityPrincipalId string = '' // Principal ID for the User Identity +param allowUserIdentityPrincipal bool = false // Flag to enable user identity role assignments +param enableBlob bool = true +param enableQueue bool = false +param enableTable bool = false + +// Define Role Definition IDs internally +var storageRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' //Storage Blob Data Owner role +var queueRoleDefinitionId = '974c5e8b-45b9-4653-ba55-5f855dd0fb88' // Storage Queue Data Contributor role +var tableRoleDefinitionId = '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3' // Storage Table Data Contributor role +var monitoringRoleDefinitionId = '3913510d-42f4-4e42-8a64-420c390055eb' // Monitoring Metrics Publisher role ID + +resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { + name: storageAccountName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: appInsightsName +} + +// Role assignment for Storage Account (Blob) - Managed Identity +resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableBlob) { + name: guid(storageAccount.id, managedIdentityPrincipalId, storageRoleDefinitionId) // Use managed identity ID + scope: storageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageRoleDefinitionId) + principalId: managedIdentityPrincipalId // Use managed identity ID + principalType: 'ServicePrincipal' // Managed Identity is a Service Principal + } +} + +// Role assignment for Storage Account (Blob) - User Identity +resource storageRoleAssignment_User 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableBlob && allowUserIdentityPrincipal && !empty(userIdentityPrincipalId)) { + name: guid(storageAccount.id, userIdentityPrincipalId, storageRoleDefinitionId) + scope: storageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageRoleDefinitionId) + principalId: userIdentityPrincipalId // Use user identity ID + principalType: 'User' // User Identity is a User Principal + } +} + +// Role assignment for Storage Account (Queue) - Managed Identity +resource queueRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableQueue) { + name: guid(storageAccount.id, managedIdentityPrincipalId, queueRoleDefinitionId) // Use managed identity ID + scope: storageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', queueRoleDefinitionId) + principalId: managedIdentityPrincipalId // Use managed identity ID + principalType: 'ServicePrincipal' // Managed Identity is a Service Principal + } +} + +// Role assignment for Storage Account (Queue) - User Identity +resource queueRoleAssignment_User 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableQueue && allowUserIdentityPrincipal && !empty(userIdentityPrincipalId)) { + name: guid(storageAccount.id, userIdentityPrincipalId, queueRoleDefinitionId) + scope: storageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', queueRoleDefinitionId) + principalId: userIdentityPrincipalId // Use user identity ID + principalType: 'User' // User Identity is a User Principal + } +} + +// Role assignment for Storage Account (Table) - Managed Identity +resource tableRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableTable) { + name: guid(storageAccount.id, managedIdentityPrincipalId, tableRoleDefinitionId) // Use managed identity ID + scope: storageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', tableRoleDefinitionId) + principalId: managedIdentityPrincipalId // Use managed identity ID + principalType: 'ServicePrincipal' // Managed Identity is a Service Principal + } +} + +// Role assignment for Storage Account (Table) - User Identity +resource tableRoleAssignment_User 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableTable && allowUserIdentityPrincipal && !empty(userIdentityPrincipalId)) { + name: guid(storageAccount.id, userIdentityPrincipalId, tableRoleDefinitionId) + scope: storageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', tableRoleDefinitionId) + principalId: userIdentityPrincipalId // Use user identity ID + principalType: 'User' // User Identity is a User Principal + } +} + +// Role assignment for Application Insights - Managed Identity +resource appInsightsRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(applicationInsights.id, managedIdentityPrincipalId, monitoringRoleDefinitionId) // Use managed identity ID + scope: applicationInsights + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', monitoringRoleDefinitionId) + principalId: managedIdentityPrincipalId // Use managed identity ID + principalType: 'ServicePrincipal' // Managed Identity is a Service Principal + } +} + +// Role assignment for Application Insights - User Identity +resource appInsightsRoleAssignment_User 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (allowUserIdentityPrincipal && !empty(userIdentityPrincipalId)) { + name: guid(applicationInsights.id, userIdentityPrincipalId, monitoringRoleDefinitionId) + scope: applicationInsights + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', monitoringRoleDefinitionId) + principalId: userIdentityPrincipalId // Use user identity ID + principalType: 'User' // User Identity is a User Principal + } +} \ No newline at end of file diff --git a/infra/app/storage-PrivateEndpoint.bicep b/infra/app/storage-PrivateEndpoint.bicep new file mode 100644 index 0000000..69ebce2 --- /dev/null +++ b/infra/app/storage-PrivateEndpoint.bicep @@ -0,0 +1,178 @@ +param virtualNetworkName string +param subnetName string +@description('Specifies the storage account resource name') +param resourceName string +param location string = resourceGroup().location +param tags object = {} +param enableBlob bool = true +param enableQueue bool = false +param enableTable bool = false + +resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { + name: virtualNetworkName +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { + name: resourceName +} + +// Storage DNS zone names +var blobPrivateDNSZoneName = 'privatelink.blob.${environment().suffixes.storage}' +var queuePrivateDNSZoneName = 'privatelink.queue.${environment().suffixes.storage}' +var tablePrivateDNSZoneName = 'privatelink.table.${environment().suffixes.storage}' + +// AVM module for Blob Private Endpoint with private DNS zone +module blobPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.0' = if (enableBlob) { + name: 'blob-private-endpoint-deployment' + params: { + name: 'blob-private-endpoint' + location: location + tags: tags + subnetResourceId: '${vnet.id}/subnets/${subnetName}' + privateLinkServiceConnections: [ + { + name: 'blobPrivateLinkConnection' + properties: { + privateLinkServiceId: storageAccount.id + groupIds: [ + 'blob' + ] + } + } + ] + customDnsConfigs: [] + // Creates private DNS zone and links + privateDnsZoneGroup: { + name: 'blobPrivateDnsZoneGroup' + privateDnsZoneGroupConfigs: [ + { + name: 'storageBlobARecord' + privateDnsZoneResourceId: enableBlob ? privateDnsZoneBlobDeployment.outputs.resourceId : '' + } + ] + } + } +} + +// AVM module for Queue Private Endpoint with private DNS zone +module queuePrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.0' = if (enableQueue) { + name: 'queue-private-endpoint-deployment' + params: { + name: 'queue-private-endpoint' + location: location + tags: tags + subnetResourceId: '${vnet.id}/subnets/${subnetName}' + privateLinkServiceConnections: [ + { + name: 'queuePrivateLinkConnection' + properties: { + privateLinkServiceId: storageAccount.id + groupIds: [ + 'queue' + ] + } + } + ] + customDnsConfigs: [] + // Creates private DNS zone and links + privateDnsZoneGroup: { + name: 'queuePrivateDnsZoneGroup' + privateDnsZoneGroupConfigs: [ + { + name: 'storageQueueARecord' + privateDnsZoneResourceId: enableQueue ? privateDnsZoneQueueDeployment.outputs.resourceId : '' + } + ] + } + } +} + +// AVM module for Table Private Endpoint with private DNS zone +module tablePrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.0' = if (enableTable) { + name: 'table-private-endpoint-deployment' + params: { + name: 'table-private-endpoint' + location: location + tags: tags + subnetResourceId: '${vnet.id}/subnets/${subnetName}' + privateLinkServiceConnections: [ + { + name: 'tablePrivateLinkConnection' + properties: { + privateLinkServiceId: storageAccount.id + groupIds: [ + 'table' + ] + } + } + ] + customDnsConfigs: [] + // Creates private DNS zone and links + privateDnsZoneGroup: { + name: 'tablePrivateDnsZoneGroup' + privateDnsZoneGroupConfigs: [ + { + name: 'storageTableARecord' + privateDnsZoneResourceId: enableTable ? privateDnsZoneTableDeployment.outputs.resourceId : '' + } + ] + } + } +} + +// AVM module for Blob Private DNS Zone +module privateDnsZoneBlobDeployment 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (enableBlob) { + name: 'blob-private-dns-zone-deployment' + params: { + name: blobPrivateDNSZoneName + location: 'global' + tags: tags + virtualNetworkLinks: [ + { + name: '${resourceName}-blob-link-${take(toLower(uniqueString(resourceName, virtualNetworkName)), 4)}' + virtualNetworkResourceId: vnet.id + registrationEnabled: false + location: 'global' + tags: tags + } + ] + } +} + +// AVM module for Queue Private DNS Zone +module privateDnsZoneQueueDeployment 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (enableQueue) { + name: 'queue-private-dns-zone-deployment' + params: { + name: queuePrivateDNSZoneName + location: 'global' + tags: tags + virtualNetworkLinks: [ + { + name: '${resourceName}-queue-link-${take(toLower(uniqueString(resourceName, virtualNetworkName)), 4)}' + virtualNetworkResourceId: vnet.id + registrationEnabled: false + location: 'global' + tags: tags + } + ] + } +} + +// AVM module for Table Private DNS Zone +module privateDnsZoneTableDeployment 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (enableTable) { + name: 'table-private-dns-zone-deployment' + params: { + name: tablePrivateDNSZoneName + location: 'global' + tags: tags + virtualNetworkLinks: [ + { + name: '${resourceName}-table-link-${take(toLower(uniqueString(resourceName, virtualNetworkName)), 4)}' + virtualNetworkResourceId: vnet.id + registrationEnabled: false + location: 'global' + tags: tags + } + ] + } +} \ No newline at end of file diff --git a/infra/app/vnet.bicep b/infra/app/vnet.bicep new file mode 100644 index 0000000..a1b6a92 --- /dev/null +++ b/infra/app/vnet.bicep @@ -0,0 +1,48 @@ +@description('Specifies the name of the virtual network.') +param vNetName string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the name of the subnet for the Service Bus private endpoint.') +param peSubnetName string = 'private-endpoints-subnet' + +@description('Specifies the name of the subnet for Function App virtual network integration.') +param appSubnetName string = 'app' + +param tags object = {} + +// Migrated to use AVM module instead of direct resource declaration +module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = { + name: 'vnet-deployment' + params: { + // Required parameters + name: vNetName + addressPrefixes: [ + '10.0.0.0/16' + ] + // Non-required parameters + location: location + tags: tags + subnets: [ + { + name: peSubnetName + addressPrefix: '10.0.1.0/24' + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } + { + name: appSubnetName + addressPrefix: '10.0.2.0/24' + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + delegation: 'Microsoft.App/environments' + } + ] + } +} + +output peSubnetName string = peSubnetName +output peSubnetID string = '${virtualNetwork.outputs.resourceId}/subnets/${peSubnetName}' +output appSubnetName string = appSubnetName +output appSubnetID string = '${virtualNetwork.outputs.resourceId}/subnets/${appSubnetName}' \ No newline at end of file diff --git a/infra/core/host/app-diagnostics.bicep b/infra/core/host/app-diagnostics.bicep deleted file mode 100644 index f084c87..0000000 --- a/infra/core/host/app-diagnostics.bicep +++ /dev/null @@ -1,56 +0,0 @@ -param appName string = '' - -@description('The kind of the app.') -@allowed([ - 'functionapp' - 'webapp' -]) -param kind string - -@description('Resource ID of log analytics workspace.') -param diagnosticWorkspaceId string - -param diagnosticLogCategoriesToEnable array = kind == 'functionapp' ? [ - 'FunctionAppLogs' -] : [ - 'AppServiceHTTPLogs' - 'AppServiceConsoleLogs' - 'AppServiceAppLogs' - 'AppServiceAuditLogs' - 'AppServiceIPSecAuditLogs' - 'AppServicePlatformLogs' -] - -@description('Optional. The name of metrics that will be streamed.') -@allowed([ - 'AllMetrics' -]) -param diagnosticMetricsToEnable array = [ - 'AllMetrics' -] - - -var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: { - category: category - enabled: true -}] - -var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: { - category: metric - timeGrain: null - enabled: true -}] - -resource app 'Microsoft.Web/sites@2022-03-01' existing = { - name: appName -} - -resource app_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: '${appName}-diagnostics' - scope: app - properties: { - workspaceId: diagnosticWorkspaceId - metrics: diagnosticsMetrics - logs: diagnosticsLogs - } -} diff --git a/infra/core/host/appservice-appsettings.bicep b/infra/core/host/appservice-appsettings.bicep deleted file mode 100644 index f4b22f8..0000000 --- a/infra/core/host/appservice-appsettings.bicep +++ /dev/null @@ -1,17 +0,0 @@ -metadata description = 'Updates app settings for an Azure App Service.' -@description('The name of the app service resource within the current resource group scope') -param name string - -@description('The app settings to be applied to the app service') -@secure() -param appSettings object - -resource appService 'Microsoft.Web/sites@2022-03-01' existing = { - name: name -} - -resource settings 'Microsoft.Web/sites/config@2022-03-01' = { - name: 'appsettings' - parent: appService - properties: appSettings -} diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep deleted file mode 100644 index bef4d2b..0000000 --- a/infra/core/host/appservice.bicep +++ /dev/null @@ -1,123 +0,0 @@ -metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param applicationInsightsName string = '' -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) - -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' -param runtimeVersion string - -// Microsoft.Web/sites Properties -param kind string = 'app,linux' - -// Microsoft.Web/sites/config -param allowedOrigins array = [] -param alwaysOn bool = true -param appCommandLine string = '' -@secure() -param appSettings object = {} -param clientAffinityEnabled bool = false -param enableOryxBuild bool = contains(kind, 'linux') -param functionAppScaleLimit int = -1 -param linuxFxVersion string = runtimeNameAndVersion -param minimumElasticInstanceCount int = -1 -param numberOfWorkers int = -1 -param scmDoBuildDuringDeployment bool = false -param use32BitWorkerProcess bool = false -param ftpsState string = 'FtpsOnly' -param healthCheckPath string = '' - -resource appService 'Microsoft.Web/sites@2022-03-01' = { - name: name - location: location - tags: tags - kind: kind - properties: { - serverFarmId: appServicePlanId - siteConfig: { - linuxFxVersion: linuxFxVersion - alwaysOn: alwaysOn - ftpsState: ftpsState - minTlsVersion: '1.2' - appCommandLine: appCommandLine - numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null - minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null - use32BitWorkerProcess: use32BitWorkerProcess - functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null - healthCheckPath: healthCheckPath - cors: { - allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) - } - } - clientAffinityEnabled: clientAffinityEnabled - httpsOnly: true - } - - identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } - - resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { - name: 'ftp' - properties: { - allow: false - } - } - - resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { - name: 'scm' - properties: { - allow: false - } - } -} - -// Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially -// sites/web/config 'appsettings' -module configAppSettings 'appservice-appsettings.bicep' = { - name: '${name}-appSettings' - params: { - name: appService.name - appSettings: union(appSettings, - { - SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) - ENABLE_ORYX_BUILD: string(enableOryxBuild) - }, - runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {}, - !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, - !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) - } -} - -// sites/web/config 'logs' -resource configLogs 'Microsoft.Web/sites/config@2022-03-01' = { - name: 'logs' - parent: appService - properties: { - applicationLogs: { fileSystem: { level: 'Verbose' } } - detailedErrorMessages: { enabled: true } - failedRequestsTracing: { enabled: true } - httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } - } - dependsOn: [configAppSettings] -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { - name: keyVaultName -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { - name: applicationInsightsName -} - -output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' -output name string = appService.name -output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep deleted file mode 100644 index 2e37e04..0000000 --- a/infra/core/host/appserviceplan.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates an Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param kind string = '' -param reserved bool = true -param sku object - -resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - kind: kind - properties: { - reserved: reserved - } -} - -output id string = appServicePlan.id -output name string = appServicePlan.name diff --git a/infra/core/host/functions.bicep b/infra/core/host/functions.bicep deleted file mode 100644 index 7070a2c..0000000 --- a/infra/core/host/functions.bicep +++ /dev/null @@ -1,86 +0,0 @@ -metadata description = 'Creates an Azure Function in an existing Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param applicationInsightsName string = '' -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) -param storageAccountName string - -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' -param runtimeVersion string - -// Function Settings -@allowed([ - '~4', '~3', '~2', '~1' -]) -param extensionVersion string = '~4' - -// Microsoft.Web/sites Properties -param kind string = 'functionapp,linux' - -// Microsoft.Web/sites/config -param allowedOrigins array = [] -param alwaysOn bool = true -param appCommandLine string = '' -@secure() -param appSettings object = {} -param clientAffinityEnabled bool = false -param enableOryxBuild bool = contains(kind, 'linux') -param functionAppScaleLimit int = -1 -param linuxFxVersion string = runtimeNameAndVersion -param minimumElasticInstanceCount int = -1 -param numberOfWorkers int = -1 -param scmDoBuildDuringDeployment bool = true -param use32BitWorkerProcess bool = false -param healthCheckPath string = '' - -module functions 'appservice.bicep' = { - name: '${name}-functions' - params: { - name: name - location: location - tags: tags - allowedOrigins: allowedOrigins - alwaysOn: alwaysOn - appCommandLine: appCommandLine - applicationInsightsName: applicationInsightsName - appServicePlanId: appServicePlanId - appSettings: union(appSettings, { - AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' - FUNCTIONS_EXTENSION_VERSION: extensionVersion - FUNCTIONS_WORKER_RUNTIME: runtimeName - }) - clientAffinityEnabled: clientAffinityEnabled - enableOryxBuild: enableOryxBuild - functionAppScaleLimit: functionAppScaleLimit - healthCheckPath: healthCheckPath - keyVaultName: keyVaultName - kind: kind - linuxFxVersion: linuxFxVersion - managedIdentity: managedIdentity - minimumElasticInstanceCount: minimumElasticInstanceCount - numberOfWorkers: numberOfWorkers - runtimeName: runtimeName - runtimeVersion: runtimeVersion - runtimeNameAndVersion: runtimeNameAndVersion - scmDoBuildDuringDeployment: scmDoBuildDuringDeployment - use32BitWorkerProcess: use32BitWorkerProcess - } -} - -resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { - name: storageAccountName -} - -output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' -output name string = functions.outputs.name -output uri string = functions.outputs.uri diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep deleted file mode 100644 index b7af2c1..0000000 --- a/infra/core/monitor/applicationinsights-dashboard.bicep +++ /dev/null @@ -1,1235 +0,0 @@ -param name string -param applicationInsightsName string -param location string = resourceGroup().location -param tags object = {} - -// 2020-09-01-preview because that is the latest valid version -resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { - name: name - location: location - tags: tags - properties: { - lenses: [ - { - order: 0 - parts: [ - { - position: { - x: 0 - y: 0 - colSpan: 2 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'id' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' - asset: { - idInputName: 'id' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'overview' - } - } - { - position: { - x: 2 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'ProactiveDetection' - } - } - { - position: { - x: 3 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:20:33.345Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 5 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-08T18:47:35.237Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'ConfigurationId' - value: '78ce933e-e864-4b05-a27b-71fd55a6afad' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 0 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Usage' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 3 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:22:35.782Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Reliability' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 7 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:42:40.072Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'failures' - } - } - { - position: { - x: 8 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Responsiveness\r\n' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 11 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:43:37.804Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'performance' - } - } - { - position: { - x: 12 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Browser' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 15 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'MetricsExplorerJsonDefinitionId' - value: 'BrowserPerformanceTimelineMetrics' - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - createdTime: '2018-05-08T12:16:27.534Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'CurrentFilter' - value: { - eventTypes: [ - 4 - 1 - 3 - 5 - 2 - 6 - 13 - ] - typeFacets: {} - isPermissive: false - } - } - { - name: 'id' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'browser' - } - } - { - position: { - x: 0 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'sessions/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Sessions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'users/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Users' - color: '#7E58FF' - } - } - ] - title: 'Unique sessions and users' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'segmentationUsers' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Failed requests' - color: '#EC008C' - } - } - ] - title: 'Failed requests' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'failures' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/duration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server response time' - color: '#00BCF2' - } - } - ] - title: 'Server response time' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'performance' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/networkDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Page load network connect time' - color: '#7E58FF' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/processingDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Client processing time' - color: '#44F1C8' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/sendDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Send request time' - color: '#EB9371' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/receiveDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Receiving response time' - color: '#0672F1' - } - } - ] - title: 'Average page load time breakdown' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/availabilityPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability' - color: '#47BDF5' - } - } - ] - title: 'Average availability' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'availability' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/server' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server exceptions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'dependencies/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Dependency failures' - color: '#7E58FF' - } - } - ] - title: 'Server exceptions and Dependency failures' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processorCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Processor time' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process CPU' - color: '#7E58FF' - } - } - ] - title: 'Average processor and process CPU utilization' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/browser' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Browser exceptions' - color: '#47BDF5' - } - } - ] - title: 'Browser exceptions' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/count' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability test results count' - color: '#47BDF5' - } - } - ] - title: 'Availability test results count' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processIOBytesPerSecond' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process IO rate' - color: '#47BDF5' - } - } - ] - title: 'Average process I/O rate' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/memoryAvailableBytes' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Available memory' - color: '#47BDF5' - } - } - ] - title: 'Average available memory' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - ] - } - ] - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep deleted file mode 100644 index f76b292..0000000 --- a/infra/core/monitor/applicationinsights.bicep +++ /dev/null @@ -1,30 +0,0 @@ -param name string -param dashboardName string -param location string = resourceGroup().location -param tags object = {} - -param logAnalyticsWorkspaceId string - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: name - location: location - tags: tags - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: logAnalyticsWorkspaceId - } -} - -module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = { - name: 'application-insights-dashboard' - params: { - name: dashboardName - location: location - applicationInsightsName: applicationInsights.name - } -} - -output connectionString string = applicationInsights.properties.ConnectionString -output instrumentationKey string = applicationInsights.properties.InstrumentationKey -output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep deleted file mode 100644 index 770544c..0000000 --- a/infra/core/monitor/loganalytics.bicep +++ /dev/null @@ -1,21 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: name - location: location - tags: tags - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -output id string = logAnalytics.id -output name string = logAnalytics.name diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep deleted file mode 100644 index 96ba11e..0000000 --- a/infra/core/monitor/monitoring.bicep +++ /dev/null @@ -1,31 +0,0 @@ -param logAnalyticsName string -param applicationInsightsName string -param applicationInsightsDashboardName string -param location string = resourceGroup().location -param tags object = {} - -module logAnalytics 'loganalytics.bicep' = { - name: 'loganalytics' - params: { - name: logAnalyticsName - location: location - tags: tags - } -} - -module applicationInsights 'applicationinsights.bicep' = { - name: 'applicationinsights' - params: { - name: applicationInsightsName - location: location - tags: tags - dashboardName: applicationInsightsDashboardName - logAnalyticsWorkspaceId: logAnalytics.outputs.id - } -} - -output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString -output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey -output applicationInsightsName string = applicationInsights.outputs.name -output logAnalyticsWorkspaceId string = logAnalytics.outputs.id -output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep deleted file mode 100644 index a41972c..0000000 --- a/infra/core/storage/storage-account.bicep +++ /dev/null @@ -1,38 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param allowBlobPublicAccess bool = false -param containers array = [] -param kind string = 'StorageV2' -param minimumTlsVersion string = 'TLS1_2' -param sku object = { name: 'Standard_LRS' } - -resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { - name: name - location: location - tags: tags - kind: kind - sku: sku - properties: { - minimumTlsVersion: minimumTlsVersion - allowBlobPublicAccess: allowBlobPublicAccess - networkAcls: { - bypass: 'AzureServices' - defaultAction: 'Allow' - } - } - - resource blobServices 'blobServices' = if (!empty(containers)) { - name: 'default' - resource container 'containers' = [for container in containers: { - name: container.name - properties: { - publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' - } - }] - } -} - -output name string = storage.name -output primaryEndpoints object = storage.properties.primaryEndpoints diff --git a/infra/main.bicep b/infra/main.bicep index a54a032..8bf259d 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -2,86 +2,232 @@ targetScope = 'subscription' @minLength(1) @maxLength(64) -@description('Name which is used to generate a short unique hash for each resource') -param name string +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string @minLength(1) -@description('Primary location for all resources') +@description('Primary location for all resources & Flex Consumption Function App') +@allowed([ + 'australiaeast' + 'australiasoutheast' + 'brazilsouth' + 'canadacentral' + 'centralindia' + 'centralus' + 'eastasia' + 'eastus' + 'eastus2' + 'eastus2euap' + 'francecentral' + 'germanywestcentral' + 'italynorth' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'northeurope' + 'norwayeast' + 'southafricanorth' + 'southcentralus' + 'southeastasia' + 'southindia' + 'spaincentral' + 'swedencentral' + 'uaenorth' + 'uksouth' + 'ukwest' + 'westcentralus' + 'westeurope' + 'westus' + 'westus2' + 'westus3' +]) +@metadata({ + azd: { + type: 'location' + } +}) param location string +param vnetEnabled bool = true +param apiServiceName string = '' +param apiUserAssignedIdentityName string = '' +param applicationInsightsName string = '' +param appServicePlanName string = '' +param logAnalyticsName string = '' +param resourceGroupName string = '' +param storageAccountName string = '' +param vNetName string = '' +@description('Id of the user identity to be used for testing and debugging. This is not required in production. Leave empty if not needed.') +param principalId string = '' -var resourceToken = toLower(uniqueString(subscription().id, name, location)) -var tags = { 'azd-env-name': name } +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } +var functionAppName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' +var deploymentStorageContainerName = 'app-package-${take(functionAppName, 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' -resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { - name: '${name}-rg' +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' location: location tags: tags } -var prefix = '${name}-${resourceToken}' - -module monitoring './core/monitor/monitoring.bicep' = { - name: 'monitoring' - scope: resourceGroup +// User assigned managed identity to be used by the function app to reach storage and other dependencies +// Assign specific roles to this identity in the RBAC module +module apiUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = { + name: 'apiUserAssignedIdentity' + scope: rg params: { location: location tags: tags - logAnalyticsName: '${prefix}-logworkspace' - applicationInsightsName: '${prefix}-appinsights' - applicationInsightsDashboardName: '${prefix}-appinsights-dashboard' + name: !empty(apiUserAssignedIdentityName) ? apiUserAssignedIdentityName : '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' } } -module storageAccount 'core/storage/storage-account.bicep' = { - name: 'storage' - scope: resourceGroup +// Create an App Service Plan to group applications under the same payment plan and SKU +module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.1' = { + name: 'appserviceplan' + scope: rg params: { - name: '${toLower(take(replace(prefix, '-', ''), 17))}storage' + name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' + sku: { + name: 'FC1' + tier: 'FlexConsumption' + } + reserved: true location: location tags: tags } } -module appServicePlan './core/host/appserviceplan.bicep' = { - name: 'appserviceplan' - scope: resourceGroup +module api './app/api.bicep' = { + name: 'api' + scope: rg params: { - name: '${prefix}-plan' + name: functionAppName location: location tags: tags - sku: { - name: 'Y1' - tier: 'Dynamic' + applicationInsightsName: monitoring.outputs.name + appServicePlanId: appServicePlan.outputs.resourceId + runtimeName: 'python' + runtimeVersion: '3.12' + storageAccountName: storage.outputs.name + enableBlob: storageEndpointConfig.enableBlob + enableQueue: storageEndpointConfig.enableQueue + enableTable: storageEndpointConfig.enableTable + deploymentStorageContainerName: deploymentStorageContainerName + identityId: apiUserAssignedIdentity.outputs.resourceId + identityClientId: apiUserAssignedIdentity.outputs.clientId + appSettings: { } + virtualNetworkSubnetId: vnetEnabled ? serviceVirtualNetwork.outputs.appSubnetID : '' } } -module functionApp 'core/host/functions.bicep' = { - name: 'function' - scope: resourceGroup +// Backing storage for Azure functions backend API +module storage 'br/public:avm/res/storage/storage-account:0.8.3' = { + name: 'storage' + scope: rg params: { - name: '${prefix}-function-app' - location: location - tags: union(tags, { 'azd-service-name': 'api' }) - alwaysOn: false - appSettings: { - AzureWebJobsFeatureFlags: 'EnableWorkerIndexing' + name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + allowBlobPublicAccess: false + allowSharedKeyAccess: false // Disable local authentication methods as per policy + dnsEndpointType: 'Standard' + publicNetworkAccess: vnetEnabled ? 'Disabled' : 'Enabled' + networkAcls: vnetEnabled ? { + defaultAction: 'Deny' + bypass: 'None' + } : { + defaultAction: 'Allow' + bypass: 'AzureServices' } - applicationInsightsName: monitoring.outputs.applicationInsightsName - appServicePlanId: appServicePlan.outputs.id - runtimeName: 'python' - runtimeVersion: '3.10' - storageAccountName: storageAccount.outputs.name + blobServices: { + containers: [{name: deploymentStorageContainerName}] + } + minimumTlsVersion: 'TLS1_2' // Enforcing TLS 1.2 for better security + location: location + tags: tags + } +} + +// Define the configuration object locally to pass to the modules +var storageEndpointConfig = { + enableBlob: true // Required for AzureWebJobsStorage, .zip deployment, Event Hubs trigger and Timer trigger checkpointing + enableQueue: false // Required for Durable Functions and MCP trigger + enableTable: false // Required for Durable Functions and OpenAI triggers and bindings + enableFiles: false // Not required, used in legacy scenarios + allowUserIdentityPrincipal: true // Allow interactive user identity to access for testing and debugging +} + +// Consolidated Role Assignments +module rbac 'app/rbac.bicep' = { + name: 'rbacAssignments' + scope: rg + params: { + storageAccountName: storage.outputs.name + appInsightsName: monitoring.outputs.name + managedIdentityPrincipalId: apiUserAssignedIdentity.outputs.principalId + userIdentityPrincipalId: principalId + enableBlob: storageEndpointConfig.enableBlob + enableQueue: storageEndpointConfig.enableQueue + enableTable: storageEndpointConfig.enableTable + allowUserIdentityPrincipal: storageEndpointConfig.allowUserIdentityPrincipal } } +// Virtual Network & private endpoint to blob storage +module serviceVirtualNetwork 'app/vnet.bicep' = if (vnetEnabled) { + name: 'serviceVirtualNetwork' + scope: rg + params: { + location: location + tags: tags + vNetName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' + } +} -module diagnostics 'core/host/app-diagnostics.bicep' = { - name: '${name}-functions-diagnostics' - scope: resourceGroup +module storagePrivateEndpoint 'app/storage-PrivateEndpoint.bicep' = if (vnetEnabled) { + name: 'servicePrivateEndpoint' + scope: rg params: { - appName: functionApp.outputs.name - kind: 'functionapp' - diagnosticWorkspaceId: monitoring.outputs.logAnalyticsWorkspaceId + location: location + tags: tags + virtualNetworkName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' + subnetName: vnetEnabled ? serviceVirtualNetwork.outputs.peSubnetName : '' // Keep conditional check for safety, though module won't run if !vnetEnabled + resourceName: storage.outputs.name + enableBlob: storageEndpointConfig.enableBlob + enableQueue: storageEndpointConfig.enableQueue + enableTable: storageEndpointConfig.enableTable } } + +// Monitor application with Azure Monitor - Log Analytics and Application Insights +module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.7.0' = { + name: '${uniqueString(deployment().name, location)}-loganalytics' + scope: rg + params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + location: location + tags: tags + dataRetention: 30 + } +} + +module monitoring 'br/public:avm/res/insights/component:0.4.1' = { + name: '${uniqueString(deployment().name, location)}-appinsights' + scope: rg + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + location: location + tags: tags + workspaceResourceId: logAnalytics.outputs.resourceId + disableLocalAuth: true + } +} + +// App outputs +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.connectionString +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME +output AZURE_FUNCTION_NAME string = api.outputs.SERVICE_API_NAME diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 0b01821..dd97cbd 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -1,12 +1,18 @@ { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "name": { - "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - } + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "vnetEnabled": { + "value": "${VNET_ENABLED}" } - } \ No newline at end of file + } +} \ No newline at end of file