├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── docs └── media │ └── app-services-baseline-architecture.png └── infra-as-code └── bicep ├── database.bicep ├── gateway.bicep ├── main.bicep ├── modules └── keyvaultRoleAssignment.bicep ├── network.bicep ├── parameters.json ├── secrets.bicep ├── storage.bicep └── webapp.bicep /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # From .NET Core 3.0 you can use the command: `dotnet new gitignore` to generate a customizable .gitignore file 2 | 3 | *.swp 4 | *.*~ 5 | project.lock.json 6 | .DS_Store 7 | *.pyc 8 | 9 | # Visual Studio Code 10 | .vscode 11 | 12 | .config 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Oo]ut/ 32 | msbuild.log 33 | msbuild.err 34 | msbuild.wrn 35 | 36 | # Visual Studio 2015 37 | .vs/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App Services Baseline Architecture 2 | 3 | This repository contains the Bicep code to deploy an Azure App Services baseline architecture with zonal redundancy. For more information on this architecture, see the guidance in the [Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/web-apps/app-service/architectures/baseline-zone-redundant). 4 | 5 | ![Diagram of the app services baseline architecture.](docs/media/app-services-baseline-architecture.png) 6 | 7 | ## Deploy 8 | 9 | The following are prerequisites. 10 | 11 | ## Prerequisites 12 | 13 | 1. Ensure you have an [Azure Account](https://azure.microsoft.com/free/) 14 | 1. The deployment must be started by a user who has sufficient permissions to assign [roles](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles), such as a User Access Administrator or Owner. 15 | 1. Ensure you have the [Azure CLI installed](https://learn.microsoft.com/cli/azure/install-azure-cli) 16 | 1. Ensure you have the [az Bicep tools installed](https://learn.microsoft.com/azure/azure-resource-manager/bicep/install) 17 | 18 | Use the following to deploy the infrastructure. 19 | 20 | ### Deploy the infrastructure 21 | 22 | The following steps are required to deploy the infrastructure from the command line. 23 | 24 | 1. In your command-line tool where you have the Azure CLI and Bicep installed, navigate to the root directory of this repository (AppServicesRI) 25 | 26 | 1. Login and set subscription if it is needed 27 | 28 | ```bash 29 | az login 30 | az account set --subscription xxxxx 31 | ``` 32 | 33 | 1. Obtain App gateway certificate 34 | Azure Application Gateway support for secure TLS using Azure Key Vault and managed identities for Azure resources. This configuration enables end-to-end encryption of the network traffic using standard TLS protocols. For production systems you use a publicly signed certificate backed by a public root certificate authority (CA). Here, we are going to use a self signed certificate for demonstrational purposes. 35 | 36 | - Set a variable for the domain that will be used in the rest of this deployment. 37 | 38 | ```bash 39 | export DOMAIN_NAME_APPSERV_BASELINE="contoso.com" 40 | ``` 41 | 42 | - Generate a client-facing, self-signed TLS certificate. 43 | 44 | :warning: Do not use the certificate created by this script for actual deployments. The use of self-signed certificates are provided for ease of illustration purposes only. For your App Service solution, use your organization's requirements for procurement and lifetime management of TLS certificates, _even for development purposes_. 45 | 46 | Create the certificate that will be presented to web clients by Azure Application Gateway for your domain. 47 | 48 | ```bash 49 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -out appgw.crt -keyout appgw.key -subj "/CN=${DOMAIN_NAME_APPSERV_BASELINE}/O=Contoso" -addext "subjectAltName = DNS:${DOMAIN_NAME_APPSERV_BASELINE}" -addext "keyUsage = digitalSignature" -addext "extendedKeyUsage = serverAuth" 50 | openssl pkcs12 -export -out appgw.pfx -in appgw.crt -inkey appgw.key -passout pass: 51 | ``` 52 | 53 | - Base64 encode the client-facing certificate. 54 | 55 | :bulb: No matter if you used a certificate from your organization or you generated one from above, you'll need the certificate (as `.pfx`) to be Base64 encoded for proper storage in Key Vault later. 56 | 57 | ```bash 58 | export APP_GATEWAY_LISTENER_CERTIFICATE_APPSERV_BASELINE=$(cat appgw.pfx | base64 | tr -d '\n') 59 | echo APP_GATEWAY_LISTENER_CERTIFICATE_APPSERV_BASELINE: $APP_GATEWAY_LISTENER_CERTIFICATE_APPSERV_BASELINE 60 | ``` 61 | 62 | 1. Update the infra-as-code/parameters file 63 | 64 | ```json 65 | { 66 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 67 | "contentVersion": "1.0.0.0", 68 | "parameters": { 69 | "baseName": { 70 | "value": "" 71 | }, 72 | "sqlAdministratorLogin": { 73 | "value": "" 74 | }, 75 | "sqlAdministratorLoginPassword": { 76 | "value": "" 77 | }, 78 | "developmentEnvironment": { 79 | "value": true 80 | }, 81 | "appGatewayListenerCertificate": { 82 | "value": "[base64 cert data from $APP_GATEWAY_LISTENER_CERTIFICATE_APPSERV_BASELINE]" 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | Note: Take into account that sql database enforce [password complexity](https://learn.microsoft.com/sql/relational-databases/security/password-policy?view=sql-server-ver16#password-complexity) 89 | 90 | 1. Run the following command to create a resource group and deploy the infrastructure. Make sure: 91 | 92 | - The location you choose [supports availability zones](https://learn.microsoft.com/azure/reliability/availability-zones-service-support) 93 | - The BASE_NAME contains only lowercase letters and is between 6 and 12 characters. All resources will be named given this basename. 94 | - You choose a valid resource group name 95 | 96 | ```bash 97 | LOCATION=westus3 98 | BASE_NAME= 99 | 100 | RESOURCE_GROUP= 101 | az group create --location $LOCATION --resource-group $RESOURCE_GROUP 102 | 103 | az deployment group create --template-file ./infra-as-code/bicep/main.bicep \ 104 | --resource-group $RESOURCE_GROUP \ 105 | --parameters @./infra-as-code/bicep/parameters.json \ 106 | --parameters baseName=$BASE_NAME 107 | ``` 108 | 109 | ### Publish the web app 110 | 111 | The baseline architecture uses [run from zip file in App Services](https://learn.microsoft.com/azure/app-service/deploy-run-package). There are many benefits of using this approach, including eliminating file lock conflicts when deploying. 112 | 113 | To use run from zip, you do the following: 114 | 115 | 1. Create a [project zip package](https://learn.microsoft.com/azure/app-service/deploy-run-package#create-a-project-zip-package) which is a zip file of your project. 116 | 1. Upload that zip file to a location that is accessible to your web site. This implementation uses private endpoints to securely connect to the storage account. The web app has a managed identity that is authorized to access the blob. 117 | 1. Set the environment variable `WEBSITE_RUN_FROM_PACKAGE` to the URL of the zip file. 118 | 119 | In a production environment, you would likely use a CI/CD pipeline to: 120 | 121 | 1. Build your application 122 | 1. Create the project zip package 123 | 1. Upload the zip file to your storage account 124 | 125 | The CI/CD pipeline would likely use a [self-hosted agent](https://learn.microsoft.com/azure/devops/pipelines/agents/agents?view=azure-devops&tabs=browser#install) that is able to connect to the storage account through a private endpoint to upload the zip. We have not implemented that here. 126 | 127 | **Workaround** 128 | 129 | Because we have not implemented a CI/CD pipeline with a self-hosted agent, we need a workaround to upload the file to the storage account. There are two workaround steps you need to do in order to manually upload the zip file using the portal. 130 | 131 | 1. The deployed storage account does not allow public access, so you will need to temporarily allow access public access from your IP address. 132 | 1. You need to give your user permissions to upload a blob to the storage account. 133 | 134 | Run the following to: 135 | 136 | - Allow public access from your IP address, g 137 | - Give the logged in user permissions to upload a blob 138 | - Create the `deploy` container 139 | - Upload the zip file `./website/SimpleWebApp/SimpleWebApp.zip` to the `deploy` container 140 | - Tell the web app to restart 141 | 142 | ```bash 143 | CLIENT_IP_ADDRESS= 144 | 145 | STORAGE_ACCOUNT_PREFIX=st 146 | WEB_APP_PREFIX=app- 147 | NAME_OF_STORAGE_ACCOUNT="$STORAGE_ACCOUNT_PREFIX$BASE_NAME" 148 | NAME_OF_WEB_APP="$WEB_APP_PREFIX$BASE_NAME" 149 | LOGGED_IN_USER_ID=$(az ad signed-in-user show --query id -o tsv) 150 | RESOURCE_GROUP_ID=$(az group show --resource-group $RESOURCE_GROUP --query id -o tsv) 151 | STORAGE_BLOB_DATA_CONTRIBUTOR=ba92f5b4-2d11-453d-a403-e96b0029c9fe 152 | 153 | az storage account network-rule add -g $RESOURCE_GROUP --account-name "$NAME_OF_STORAGE_ACCOUNT" --ip-address $CLIENT_IP_ADDRESS 154 | az role assignment create --assignee-principal-type User --assignee-object-id $LOGGED_IN_USER_ID --role $STORAGE_BLOB_DATA_CONTRIBUTOR --scope $RESOURCE_GROUP_ID 155 | 156 | az storage container create \ 157 | --account-name $NAME_OF_STORAGE_ACCOUNT \ 158 | --auth-mode login \ 159 | --name deploy 160 | 161 | curl https://raw.githubusercontent.com/Azure-Samples/app-service-sample-workload/main/website/SimpleWebApp.zip -o SimpleWebApp.zip 162 | 163 | az storage blob upload -f ./SimpleWebApp.zip \ 164 | --account-name $NAME_OF_STORAGE_ACCOUNT \ 165 | --auth-mode login \ 166 | -c deploy -n SimpleWebApp.zip 167 | 168 | az webapp restart --name $NAME_OF_WEB_APP --resource-group $RESOURCE_GROUP 169 | ``` 170 | 171 | ### Validate the web app 172 | 173 | This section will help you to validate the workload is exposed correctly and responding to HTTP requests. 174 | 175 | ### Steps 176 | 177 | 1. Get the public IP address of Application Gateway. 178 | 179 | > :book: The app team conducts a final acceptance test to be sure that traffic is flowing end-to-end as expected, so they place a request against the Azure Application Gateway endpoint. 180 | 181 | ```bash 182 | # query the Azure Application Gateway Public Ip 183 | APPGW_PUBLIC_IP=$(az network public-ip show --resource-group $RESOURCE_GROUP --name "pip-$BASE_NAME" --query [ipAddress] --output tsv) 184 | echo APPGW_PUBLIC_IP: $APPGW_PUBLIC_IP 185 | ``` 186 | 187 | 1. Create an `A` record for DNS. 188 | 189 | > :bulb: You can simulate this via a local hosts file modification. You're welcome to add a real DNS entry for your specific deployment's application domain name, if you have access to do so. 190 | 191 | Map the Azure Application Gateway public IP address to the application domain name. To do that, please edit your hosts file (`C:\Windows\System32\drivers\etc\hosts` or `/etc/hosts`) and add the following record to the end: `${APPGW_PUBLIC_IP} www.${DOMAIN_NAME_APPSERV_BASELINE}` (e.g. `50.140.130.120 www.contoso.com`) 192 | 193 | 1. Browse to the site (e.g. ). 194 | 195 | > :bulb: Remember to include the protocol prefix `https://` in the URL you type in the address bar of your browser. A TLS warning will be present due to using a self-signed certificate. You can ignore it or import the self-signed cert (`appgw.pfx`) to your user's trusted root store. 196 | 197 | ## Clean Up 198 | 199 | After you are done exploring your deployed AppService refence implementation, you'll want to delete the created Azure resources to prevent undesired costs from accruing. 200 | 201 | ```bash 202 | az group delete --name $RESOURCE_GROUP -y 203 | az keyvault purge -n kv-${BASE_NAME} 204 | ``` 205 | -------------------------------------------------------------------------------- /docs/media/app-services-baseline-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/app-service-baseline-implementation/433137a33eb494f8364d531d3224d144989b6124/docs/media/app-services-baseline-architecture.png -------------------------------------------------------------------------------- /infra-as-code/bicep/database.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | Deploy a SQL server with a sample database, a private endpoint and a private DNS zone 3 | */ 4 | @description('This is the base name for each Azure resource name (6-12 chars)') 5 | param baseName string 6 | 7 | @description('The resource group location') 8 | param location string = resourceGroup().location 9 | 10 | @description('The administrator username of the SQL server') 11 | param sqlAdministratorLogin string 12 | @description('The administrator password of the SQL server.') 13 | @secure() 14 | param sqlAdministratorLoginPassword string 15 | 16 | // existing resource name params 17 | param vnetName string 18 | param privateEndpointsSubnetName string 19 | 20 | // variables 21 | var sqlServerName = 'sql-${baseName}' 22 | var sampleSqlDatabaseName = 'sqldb-adventureworks' 23 | var sqlPrivateEndpointName = 'pep-${sqlServerName}' 24 | var sqlDnsGroupName = '${sqlPrivateEndpointName}/default' 25 | var sqlDnsZoneName = 'privatelink${environment().suffixes.sqlServerHostname}' 26 | var sqlConnectionString = 'Server=tcp:${sqlServerName}${environment().suffixes.sqlServerHostname},1433;Initial Catalog=${sampleSqlDatabaseName};Persist Security Info=False;User ID=${sqlAdministratorLogin};Password=${sqlAdministratorLoginPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;' 27 | 28 | // ---- Existing resources ---- 29 | resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { 30 | name: vnetName 31 | 32 | resource privateEndpointsSubnet 'subnets' existing = { 33 | name: privateEndpointsSubnetName 34 | } 35 | } 36 | 37 | // ---- Sql resources ---- 38 | 39 | // sql server 40 | resource sqlServer 'Microsoft.Sql/servers@2021-11-01' = { 41 | name: sqlServerName 42 | location: location 43 | tags: { 44 | displayName: sqlServerName 45 | } 46 | properties: { 47 | administratorLogin: sqlAdministratorLogin 48 | administratorLoginPassword: sqlAdministratorLoginPassword 49 | version: '12.0' 50 | publicNetworkAccess: 'Disabled' 51 | } 52 | } 53 | 54 | //database 55 | resource slqDatabase 'Microsoft.Sql/servers/databases@2021-11-01' = { 56 | name: sampleSqlDatabaseName 57 | parent: sqlServer 58 | location: location 59 | 60 | sku: { 61 | name: 'Basic' 62 | tier: 'Basic' 63 | capacity: 5 64 | } 65 | tags: { 66 | displayName: sampleSqlDatabaseName 67 | } 68 | properties: { 69 | collation: 'SQL_Latin1_General_CP1_CI_AS' 70 | maxSizeBytes: 104857600 71 | sampleName: 'AdventureWorksLT' 72 | } 73 | } 74 | 75 | resource sqlServerPrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-11-01' = { 76 | name: sqlPrivateEndpointName 77 | location: location 78 | properties: { 79 | subnet: { 80 | id: vnet::privateEndpointsSubnet.id 81 | } 82 | privateLinkServiceConnections: [ 83 | { 84 | name: sqlPrivateEndpointName 85 | properties: { 86 | privateLinkServiceId: sqlServer.id 87 | groupIds: [ 88 | 'sqlServer' 89 | ] 90 | } 91 | } 92 | ] 93 | } 94 | } 95 | 96 | resource sqlServerDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 97 | parent: sqlServerDnsZone 98 | name: '${sqlDnsZoneName}-link' 99 | location: 'global' 100 | properties: { 101 | registrationEnabled: false 102 | virtualNetwork: { 103 | id: vnet.id 104 | } 105 | } 106 | } 107 | 108 | resource sqlServerDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 109 | name: sqlDnsZoneName 110 | location: 'global' 111 | properties: {} 112 | } 113 | 114 | resource sqlServerDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-11-01' = { 115 | name: sqlDnsGroupName 116 | properties: { 117 | privateDnsZoneConfigs: [ 118 | { 119 | name: sqlDnsZoneName 120 | properties: { 121 | privateDnsZoneId: sqlServerDnsZone.id 122 | } 123 | } 124 | ] 125 | } 126 | dependsOn: [ 127 | sqlServerPrivateEndpoint 128 | ] 129 | } 130 | 131 | @description('The connection string to the sample database.') 132 | output sqlConnectionString string = sqlConnectionString 133 | -------------------------------------------------------------------------------- /infra-as-code/bicep/gateway.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | Deploy an Azure Application Gateway with WAF v2 and a custom domain name. 3 | */ 4 | 5 | @description('This is the base name for each Azure resource name (6-12 chars)') 6 | param baseName string 7 | 8 | @description('The resource group location') 9 | param location string = resourceGroup().location 10 | 11 | @description('Optional. When true will deploy a cost-optimised environment for development purposes.') 12 | param developmentEnvironment bool 13 | 14 | @description('Domain name to use for App Gateway') 15 | param customDomainName string 16 | 17 | param availabilityZones array 18 | param gatewayCertSecretUri string 19 | 20 | // existing resource name params 21 | param vnetName string 22 | param appGatewaySubnetName string 23 | param appName string 24 | param keyVaultName string 25 | param logWorkspaceName string 26 | 27 | //variables 28 | var appGateWayName = 'agw-${baseName}' 29 | var appGatewayManagedIdentityName = 'id-${appGateWayName}' 30 | var appGatewayPublicIpName = 'pip-${baseName}' 31 | var appGateWayFqdn = 'fe-${baseName}' 32 | var wafPolicyName= 'waf-${baseName}' 33 | 34 | // ---- Existing resources ---- 35 | resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { 36 | name: vnetName 37 | 38 | resource appGatewaySubnet 'subnets' existing = { 39 | name: appGatewaySubnetName 40 | } 41 | } 42 | 43 | resource webApp 'Microsoft.Web/sites@2022-09-01' existing = { 44 | name: appName 45 | } 46 | 47 | resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 48 | name: logWorkspaceName 49 | } 50 | 51 | // Built-in Azure RBAC role that is applied to a Key Vault to grant with secrets content read privileges. Granted to both Key Vault and our workload's identity. 52 | resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 53 | name: '4633458b-17de-408a-b874-0445c86b69e6' 54 | scope: subscription() 55 | } 56 | 57 | // ---- App Gateway resources ---- 58 | 59 | // Managed Identity for App Gateway. 60 | resource appGatewayManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 61 | name: appGatewayManagedIdentityName 62 | location: location 63 | } 64 | 65 | // Grant the Azure Application Gateway managed identity with key vault secrets role permissions; this allows pulling certificates. 66 | module appGatewaySecretsUserRoleAssignmentModule './modules/keyvaultRoleAssignment.bicep' = { 67 | name: 'appGatewaySecretsUserRoleAssignmentDeploy' 68 | params: { 69 | roleDefinitionId: keyVaultSecretsUserRole.id 70 | principalId: appGatewayManagedIdentity.properties.principalId 71 | keyVaultName: keyVaultName 72 | } 73 | } 74 | 75 | //External IP for App Gateway 76 | resource appGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2022-11-01' = { 77 | name: appGatewayPublicIpName 78 | location: location 79 | zones: !developmentEnvironment ? availabilityZones : null 80 | sku: { 81 | name: 'Standard' 82 | } 83 | properties: { 84 | publicIPAddressVersion: 'IPv4' 85 | publicIPAllocationMethod: 'Static' 86 | idleTimeoutInMinutes: 4 87 | dnsSettings: { 88 | domainNameLabel: appGateWayFqdn 89 | } 90 | } 91 | } 92 | 93 | //WAF policy definition 94 | resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2022-11-01' = { 95 | name: wafPolicyName 96 | location: location 97 | properties: { 98 | policySettings: { 99 | fileUploadLimitInMb: 10 100 | state: 'Enabled' 101 | mode: 'Prevention' 102 | } 103 | managedRules: { 104 | managedRuleSets: [ 105 | { 106 | ruleSetType: 'OWASP' 107 | ruleSetVersion: '3.2' 108 | ruleGroupOverrides: [] 109 | } 110 | { 111 | ruleSetType: 'Microsoft_BotManagerRuleSet' 112 | ruleSetVersion: '0.1' 113 | ruleGroupOverrides: [] 114 | } 115 | ] 116 | } 117 | } 118 | } 119 | 120 | //App Gateway 121 | resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { 122 | name: appGateWayName 123 | location: location 124 | zones: !developmentEnvironment ? availabilityZones : null 125 | identity: { 126 | type: 'UserAssigned' 127 | userAssignedIdentities: { 128 | '${appGatewayManagedIdentity.id}': {} 129 | } 130 | } 131 | properties: { 132 | sku: { 133 | name: 'WAF_v2' 134 | tier: 'WAF_v2' 135 | } 136 | sslPolicy: { 137 | policyType: 'Custom' 138 | cipherSuites: [ 139 | 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384' 140 | 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' 141 | ] 142 | minProtocolVersion: 'TLSv1_2' 143 | } 144 | 145 | gatewayIPConfigurations: [ 146 | { 147 | name: 'appGatewayIpConfig' 148 | properties: { 149 | subnet: { 150 | id: vnet::appGatewaySubnet.id 151 | } 152 | } 153 | } 154 | ] 155 | frontendIPConfigurations: [ 156 | { 157 | name: 'appGwPublicFrontendIp' 158 | properties: { 159 | privateIPAllocationMethod: 'Dynamic' 160 | publicIPAddress: { 161 | id: appGatewayPublicIp.id 162 | } 163 | } 164 | } 165 | ] 166 | frontendPorts: [ 167 | { 168 | name: 'port-443' 169 | properties: { 170 | port: 443 171 | } 172 | } 173 | ] 174 | probes: [ 175 | { 176 | name: 'probe-web${baseName}' 177 | properties: { 178 | protocol: 'Https' 179 | path: '/favicon.ico' 180 | interval: 30 181 | timeout: 30 182 | unhealthyThreshold: 3 183 | pickHostNameFromBackendHttpSettings: true 184 | minServers: 0 185 | match: { 186 | statusCodes: [ 187 | '200-399' 188 | '401' 189 | '403' 190 | ] 191 | } 192 | } 193 | } 194 | ] 195 | firewallPolicy: { 196 | id: wafPolicy.id 197 | } 198 | enableHttp2: false 199 | sslCertificates: [ 200 | { 201 | name: '${appGateWayName}-ssl-certificate' 202 | properties: { 203 | keyVaultSecretId: gatewayCertSecretUri 204 | } 205 | } 206 | ] 207 | backendAddressPools: [ 208 | { 209 | name: 'pool-${appName}' 210 | properties: { 211 | backendAddresses: [ 212 | { 213 | fqdn: webApp.properties.defaultHostName 214 | } 215 | ] 216 | } 217 | } 218 | ] 219 | backendHttpSettingsCollection: [ 220 | { 221 | name: 'WebAppBackendHttpSettings' 222 | properties: { 223 | port: 443 224 | protocol: 'Https' 225 | cookieBasedAffinity: 'Disabled' 226 | pickHostNameFromBackendAddress: true 227 | requestTimeout: 20 228 | probe: { 229 | id: resourceId('Microsoft.Network/applicationGateways/probes', appGateWayName, 'probe-web${baseName}') 230 | } 231 | } 232 | } 233 | ] 234 | httpListeners: [ 235 | { 236 | name: 'WebAppListener' 237 | properties: { 238 | frontendIPConfiguration: { 239 | id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', appGateWayName, 'appGwPublicFrontendIp') 240 | } 241 | frontendPort: { 242 | id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', appGateWayName, 'port-443') 243 | } 244 | protocol: 'Https' 245 | sslCertificate: { 246 | id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', appGateWayName, '${appGateWayName}-ssl-certificate') 247 | } 248 | hostName: 'www.${customDomainName}' 249 | hostNames: [] 250 | requireServerNameIndication: true 251 | } 252 | } 253 | ] 254 | requestRoutingRules: [ 255 | { 256 | name: 'WebAppRoutingRule' 257 | properties: { 258 | ruleType: 'Basic' 259 | priority: 100 260 | httpListener: { 261 | id: resourceId('Microsoft.Network/applicationGateways/httpListeners', appGateWayName, 'WebAppListener') 262 | } 263 | backendAddressPool: { 264 | id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', appGateWayName, 'pool-${appName}') 265 | } 266 | backendHttpSettings: { 267 | id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGateWayName, 'WebAppBackendHttpSettings') 268 | } 269 | } 270 | } 271 | ] 272 | autoscaleConfiguration: { 273 | minCapacity: developmentEnvironment ? 2 : 3 274 | maxCapacity: developmentEnvironment ? 3 : 5 275 | } 276 | } 277 | dependsOn: [ 278 | appGatewaySecretsUserRoleAssignmentModule 279 | ] 280 | } 281 | 282 | // App Gateway diagnostics 283 | resource appGatewayDiagSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 284 | name: '${appGateWay.name}-diagnosticSettings' 285 | scope: appGateWay 286 | properties: { 287 | workspaceId: logWorkspace.id 288 | logs: [ 289 | { 290 | categoryGroup: 'allLogs' 291 | enabled: true 292 | } 293 | ] 294 | metrics: [ 295 | { 296 | category: 'AllMetrics' 297 | enabled: true 298 | } 299 | ] 300 | } 301 | } 302 | 303 | @description('The name of the app gateway resource.') 304 | output appGateWayName string = appGateWay.name 305 | -------------------------------------------------------------------------------- /infra-as-code/bicep/main.bicep: -------------------------------------------------------------------------------- 1 | @description('The location in which all resources should be deployed.') 2 | param location string = resourceGroup().location 3 | 4 | @description('This is the base name for each Azure resource name (6-12 chars)') 5 | @minLength(6) 6 | @maxLength(12) 7 | param baseName string 8 | 9 | @description('The administrator username of the SQL server') 10 | param sqlAdministratorLogin string 11 | 12 | @description('The administrator password of the SQL server.') 13 | @secure() 14 | param sqlAdministratorLoginPassword string 15 | 16 | @description('Domain name to use for App Gateway') 17 | param customDomainName string = 'contoso.com' 18 | 19 | @description('The certificate data for app gateway TLS termination. The value is base64 encoded') 20 | @secure() 21 | param appGatewayListenerCertificate string 22 | 23 | @description('Optional. When true will deploy a cost-optimised environment for development purposes. Note that when this param is true, the deployment is not suitable or recommended for Production environments. Default = false.') 24 | param developmentEnvironment bool = false 25 | 26 | @description('The name of the web deploy file. The file should reside in a deploy container in the storage account. Defaults to SimpleWebApp.zip') 27 | param publishFileName string = 'SimpleWebApp.zip' 28 | 29 | // ---- Availability Zones ---- 30 | var availabilityZones = [ '1', '2', '3' ] 31 | var logWorkspaceName = 'log-${baseName}' 32 | 33 | 34 | // ---- Log Analytics workspace ---- 35 | resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 36 | name: logWorkspaceName 37 | location: location 38 | properties: { 39 | sku: { 40 | name: 'PerGB2018' 41 | } 42 | retentionInDays: 30 43 | } 44 | } 45 | 46 | // Deploy vnet with subnets and NSGs 47 | module networkModule 'network.bicep' = { 48 | name: 'networkDeploy' 49 | params: { 50 | location: location 51 | baseName: baseName 52 | developmentEnvironment: developmentEnvironment 53 | } 54 | } 55 | 56 | // Deploy storage account with private endpoint and private DNS zone 57 | module storageModule 'storage.bicep' = { 58 | name: 'storageDeploy' 59 | params: { 60 | location: location 61 | baseName: baseName 62 | vnetName: networkModule.outputs.vnetNName 63 | privateEndpointsSubnetName: networkModule.outputs.privateEndpointsSubnetName 64 | } 65 | } 66 | 67 | // Deploy a SQL server with a sample database, a private endpoint and a DNS zone 68 | module databaseModule 'database.bicep' = { 69 | name: 'databaseDeploy' 70 | params: { 71 | location: location 72 | baseName: baseName 73 | sqlAdministratorLogin: sqlAdministratorLogin 74 | sqlAdministratorLoginPassword: sqlAdministratorLoginPassword 75 | vnetName: networkModule.outputs.vnetNName 76 | privateEndpointsSubnetName: networkModule.outputs.privateEndpointsSubnetName 77 | } 78 | } 79 | 80 | // Deploy a Key Vault with a private endpoint and DNS zone 81 | module secretsModule 'secrets.bicep' = { 82 | name: 'secretsDeploy' 83 | params: { 84 | location: location 85 | baseName: baseName 86 | vnetName: networkModule.outputs.vnetNName 87 | privateEndpointsSubnetName: networkModule.outputs.privateEndpointsSubnetName 88 | appGatewayListenerCertificate: appGatewayListenerCertificate 89 | sqlConnectionString: databaseModule.outputs.sqlConnectionString 90 | } 91 | } 92 | 93 | // Deploy a web app 94 | module webappModule 'webapp.bicep' = { 95 | name: 'webappDeploy' 96 | params: { 97 | location: location 98 | baseName: baseName 99 | developmentEnvironment: developmentEnvironment 100 | publishFileName: publishFileName 101 | keyVaultName: secretsModule.outputs.keyVaultName 102 | storageName: storageModule.outputs.storageName 103 | vnetName: networkModule.outputs.vnetNName 104 | appServicesSubnetName: networkModule.outputs.appServicesSubnetName 105 | privateEndpointsSubnetName: networkModule.outputs.privateEndpointsSubnetName 106 | logWorkspaceName: logWorkspace.name 107 | } 108 | } 109 | 110 | //Deploy an Azure Application Gateway with WAF v2 and a custom domain name. 111 | module gatewayModule 'gateway.bicep' = { 112 | name: 'gatewayDeploy' 113 | params: { 114 | location: location 115 | baseName: baseName 116 | developmentEnvironment: developmentEnvironment 117 | availabilityZones: availabilityZones 118 | customDomainName: customDomainName 119 | appName: webappModule.outputs.appName 120 | vnetName: networkModule.outputs.vnetNName 121 | appGatewaySubnetName: networkModule.outputs.appGatewaySubnetName 122 | keyVaultName: secretsModule.outputs.keyVaultName 123 | gatewayCertSecretUri: secretsModule.outputs.gatewayCertSecretUri 124 | logWorkspaceName: logWorkspace.name 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /infra-as-code/bicep/modules/keyvaultRoleAssignment.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | This template creates a role assignment for a managed identity to access secrets in key vault. 3 | 4 | To ensure that each deployment has a unique role assignment ID, you can use the guid() function with a seed value that is based in part on the 5 | managed identity's principal ID. However, because Azure Resource Manager requires each resource's name to be available at the beginning of the deployment, 6 | you can't use this approach in the same Bicep file that defines the managed identity. This sample uses a Bicep module to work around this issue. 7 | */ 8 | @description('The Id of the role definition.') 9 | param roleDefinitionId string 10 | 11 | @description('The principalId property of the managed identity.') 12 | param principalId string 13 | 14 | @description('The name of the key vault resource.') 15 | param keyVaultName string 16 | 17 | // ---- Existing resources ---- 18 | resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { 19 | name: keyVaultName 20 | } 21 | 22 | // ---- Role assignment ---- 23 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 24 | name: guid(resourceGroup().id, principalId, roleDefinitionId) 25 | scope: keyVault 26 | properties: { 27 | roleDefinitionId: roleDefinitionId 28 | principalId: principalId 29 | principalType: 'ServicePrincipal' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /infra-as-code/bicep/network.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | Deploy vnet with subnets and NSGs 3 | */ 4 | 5 | @description('This is the base name for each Azure resource name (6-12 chars)') 6 | param baseName string 7 | 8 | @description('The resource group location') 9 | param location string = resourceGroup().location 10 | 11 | param developmentEnvironment bool 12 | 13 | // variables 14 | var vnetName = 'vnet-${baseName}' 15 | var ddosPlanName = 'ddos-${baseName}' 16 | 17 | var vnetAddressPrefix = '10.0.0.0/16' 18 | var appGatewaySubnetPrefix = '10.0.1.0/24' 19 | var appServicesSubnetPrefix = '10.0.0.0/24' 20 | var privateEndpointsSubnetPrefix = '10.0.2.0/27' 21 | var agentsSubnetPrefix = '10.0.2.32/27' 22 | 23 | //Temp disable DDoS protection 24 | var enableDdosProtection = !developmentEnvironment 25 | 26 | // ---- Networking resources ---- 27 | 28 | // DDoS Protection Plan 29 | resource ddosProtectionPlan 'Microsoft.Network/ddosProtectionPlans@2022-11-01' = if (enableDdosProtection) { 30 | name: ddosPlanName 31 | location: location 32 | properties: {} 33 | } 34 | 35 | //vnet and subnets 36 | resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' = { 37 | name: vnetName 38 | location: location 39 | properties: { 40 | enableDdosProtection: enableDdosProtection 41 | ddosProtectionPlan: enableDdosProtection ? { id: ddosProtectionPlan.id } : null 42 | addressSpace: { 43 | addressPrefixes: [ 44 | vnetAddressPrefix 45 | ] 46 | } 47 | subnets: [ 48 | { 49 | //App services plan subnet 50 | name: 'snet-appServicePlan' 51 | properties: { 52 | addressPrefix: appServicesSubnetPrefix 53 | networkSecurityGroup: { 54 | id: appServiceSubnetNsg.id 55 | } 56 | delegations: [ 57 | { 58 | name: 'delegation' 59 | properties: { 60 | serviceName: 'Microsoft.Web/serverFarms' 61 | } 62 | } 63 | ] 64 | } 65 | } 66 | { 67 | //App Gateway subnet 68 | name: 'snet-appGateway' 69 | properties: { 70 | addressPrefix: appGatewaySubnetPrefix 71 | networkSecurityGroup: { 72 | id: appGatewaySubnetNsg.id 73 | } 74 | privateEndpointNetworkPolicies: 'Enabled' 75 | privateLinkServiceNetworkPolicies: 'Enabled' 76 | } 77 | } 78 | { 79 | //Private endpoints subnet 80 | name: 'snet-privateEndpoints' 81 | properties: { 82 | addressPrefix: privateEndpointsSubnetPrefix 83 | networkSecurityGroup: { 84 | id: privateEndpointsSubnetNsg.id 85 | } 86 | } 87 | } 88 | { 89 | // Build agents subnet 90 | name: 'snet-agents' 91 | properties: { 92 | addressPrefix: agentsSubnetPrefix 93 | networkSecurityGroup: { 94 | id: agentsSubnetNsg.id 95 | } 96 | } 97 | } 98 | ] 99 | } 100 | 101 | resource appGatewaySubnet 'subnets' existing = { 102 | name: 'snet-appGateway' 103 | } 104 | 105 | resource appServiceSubnet 'subnets' existing = { 106 | name: 'snet-appServicePlan' 107 | } 108 | 109 | resource privateEnpointsSubnet 'subnets' existing = { 110 | name: 'snet-privateEndpoints' 111 | } 112 | 113 | resource agentsSubnet 'subnets' existing = { 114 | name: 'snet-agents' 115 | } 116 | } 117 | 118 | //App Gateway subnet NSG 119 | resource appGatewaySubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { 120 | name: 'nsg-appGatewaySubnet' 121 | location: location 122 | properties: { 123 | securityRules: [ 124 | { 125 | name: 'AppGw.In.Allow.ControlPlane' 126 | properties: { 127 | description: 'Allow inbound Control Plane (https://docs.microsoft.com/azure/application-gateway/configuration-infrastructure#network-security-groups)' 128 | protocol: '*' 129 | sourcePortRange: '*' 130 | destinationPortRange: '65200-65535' 131 | sourceAddressPrefix: '*' 132 | destinationAddressPrefix: '*' 133 | access: 'Allow' 134 | priority: 100 135 | direction: 'Inbound' 136 | } 137 | } 138 | { 139 | name: 'AppGw.In.Allow443.Internet' 140 | properties: { 141 | description: 'Allow ALL inbound web traffic on port 443' 142 | protocol: 'Tcp' 143 | sourcePortRange: '*' 144 | destinationPortRange: '443' 145 | sourceAddressPrefix: 'Internet' 146 | destinationAddressPrefix: appGatewaySubnetPrefix 147 | access: 'Allow' 148 | priority: 110 149 | direction: 'Inbound' 150 | } 151 | } 152 | { 153 | name: 'AppGw.In.Allow.LoadBalancer' 154 | properties: { 155 | description: 'Allow inbound traffic from azure load balancer' 156 | protocol: '*' 157 | sourcePortRange: '*' 158 | destinationPortRange: '*' 159 | sourceAddressPrefix: 'AzureLoadBalancer' 160 | destinationAddressPrefix: '*' 161 | access: 'Allow' 162 | priority: 120 163 | direction: 'Inbound' 164 | } 165 | } 166 | { 167 | name: 'DenyAllInBound' 168 | properties: { 169 | protocol: '*' 170 | sourcePortRange: '*' 171 | sourceAddressPrefix: '*' 172 | destinationPortRange: '*' 173 | destinationAddressPrefix: '*' 174 | access: 'Deny' 175 | priority: 1000 176 | direction: 'Inbound' 177 | } 178 | } 179 | { 180 | name: 'AppGw.Out.Allow.PrivateEndpoints' 181 | properties: { 182 | description: 'Allow outbound traffic from the App Gateway subnet to the Private Endpoints subnet.' 183 | protocol: '*' 184 | sourcePortRange: '*' 185 | destinationPortRange: '*' 186 | sourceAddressPrefix: appGatewaySubnetPrefix 187 | destinationAddressPrefix: privateEndpointsSubnetPrefix 188 | access: 'Allow' 189 | priority: 100 190 | direction: 'Outbound' 191 | } 192 | } 193 | { 194 | name: 'AppPlan.Out.Allow.AzureMonitor' 195 | properties: { 196 | description: 'Allow outbound traffic from the App Gateway subnet to Azure Monitor' 197 | protocol: '*' 198 | sourcePortRange: '*' 199 | destinationPortRange: '*' 200 | sourceAddressPrefix: appGatewaySubnetPrefix 201 | destinationAddressPrefix: 'AzureMonitor' 202 | access: 'Allow' 203 | priority: 110 204 | direction: 'Outbound' 205 | } 206 | } 207 | ] 208 | } 209 | } 210 | 211 | //App service subnet nsg 212 | resource appServiceSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { 213 | name: 'nsg-appServicesSubnet' 214 | location: location 215 | properties: { 216 | securityRules: [ 217 | { 218 | name: 'AppPlan.Out.Allow.PrivateEndpoints' 219 | properties: { 220 | description: 'Allow outbound traffic from the app service subnet to the private endpoints subnet' 221 | protocol: 'Tcp' 222 | sourcePortRange: '*' 223 | destinationPortRange: '443' 224 | sourceAddressPrefix: appServicesSubnetPrefix 225 | destinationAddressPrefix: privateEndpointsSubnetPrefix 226 | access: 'Allow' 227 | priority: 100 228 | direction: 'Outbound' 229 | } 230 | } 231 | { 232 | name: 'AppPlan.Out.Allow.AzureMonitor' 233 | properties: { 234 | description: 'Allow outbound traffic from App service to the AzureMonitor ServiceTag.' 235 | protocol: '*' 236 | sourcePortRange: '*' 237 | destinationPortRange: '*' 238 | sourceAddressPrefix: appServicesSubnetPrefix 239 | destinationAddressPrefix: 'AzureMonitor' 240 | access: 'Allow' 241 | priority: 110 242 | direction: 'Outbound' 243 | } 244 | } 245 | ] 246 | } 247 | } 248 | 249 | //Private endpoints subnets NSG 250 | resource privateEndpointsSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { 251 | name: 'nsg-privateEndpointsSubnet' 252 | location: location 253 | properties: { 254 | securityRules: [ 255 | { 256 | name: 'PE.Out.Deny.All' 257 | properties: { 258 | description: 'Deny outbound traffic from the private endpoints subnet' 259 | protocol: '*' 260 | sourcePortRange: '*' 261 | destinationPortRange: '*' 262 | sourceAddressPrefix: privateEndpointsSubnetPrefix 263 | destinationAddressPrefix: '*' 264 | access: 'Deny' 265 | priority: 100 266 | direction: 'Outbound' 267 | } 268 | } 269 | ] 270 | } 271 | } 272 | 273 | //Build agents subnets NSG 274 | resource agentsSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { 275 | name: 'nsg-agentsSubnet' 276 | location: location 277 | properties: { 278 | securityRules: [ 279 | { 280 | name: 'DenyAllOutBound' 281 | properties: { 282 | description: 'Deny outbound traffic from the build agents subnet. Note: adjust rules as needed after adding resources to the subnet' 283 | protocol: '*' 284 | sourcePortRange: '*' 285 | destinationPortRange: '*' 286 | sourceAddressPrefix: appGatewaySubnetPrefix 287 | destinationAddressPrefix: '*' 288 | access: 'Deny' 289 | priority: 1000 290 | direction: 'Outbound' 291 | } 292 | } 293 | ] 294 | } 295 | } 296 | 297 | @description('The name of the vnet.') 298 | output vnetNName string = vnet.name 299 | 300 | @description('The name of the app service plan subnet.') 301 | output appServicesSubnetName string = vnet::appServiceSubnet.name 302 | 303 | @description('The name of the app gatewaysubnet.') 304 | output appGatewaySubnetName string = vnet::appGatewaySubnet.name 305 | 306 | @description('The name of the private endpoints subnet.') 307 | output privateEndpointsSubnetName string = vnet::privateEnpointsSubnet.name 308 | -------------------------------------------------------------------------------- /infra-as-code/bicep/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 | "baseName": { 6 | "value": "" 7 | }, 8 | "sqlAdministratorLogin": { 9 | "value": "" 10 | }, 11 | "sqlAdministratorLoginPassword": { 12 | "value": "" 13 | }, 14 | "developmentEnvironment": { 15 | "value": true 16 | }, 17 | "appGatewayListenerCertificate": { 18 | "value": "[base64 cert data from $APP_GATEWAY_LISTENER_CERTIFICATE_APPSERV_BASELINE]" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /infra-as-code/bicep/secrets.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | Deploy a Key Vault with a private endpoint and DNS zone 3 | */ 4 | 5 | @description('This is the base name for each Azure resource name (6-12 chars)') 6 | param baseName string 7 | 8 | @description('The resource group location') 9 | param location string = resourceGroup().location 10 | 11 | @description('The certificate data for app gateway TLS termination. The value is base64 encoded') 12 | @secure() 13 | param appGatewayListenerCertificate string 14 | param sqlConnectionString string 15 | 16 | // existing resource name params 17 | param vnetName string 18 | param privateEndpointsSubnetName string 19 | 20 | //variables 21 | var keyVaultName = 'kv-${baseName}' 22 | var keyVaultPrivateEndpointName = 'pep-${keyVaultName}' 23 | var keyVaultDnsGroupName = '${keyVaultPrivateEndpointName}/default' 24 | var keyVaultDnsZoneName = 'privatelink.vaultcore.azure.net' //Cannot use 'privatelink${environment().suffixes.keyvaultDns}', per https://github.com/Azure/bicep/issues/9708 25 | 26 | // ---- Existing resources ---- 27 | resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { 28 | name: vnetName 29 | 30 | resource privateEndpointsSubnet 'subnets' existing = { 31 | name: privateEndpointsSubnetName 32 | } 33 | } 34 | 35 | // ---- Key Vault resources ---- 36 | resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = { 37 | name: keyVaultName 38 | location: location 39 | properties: { 40 | enabledForDeployment: false 41 | enabledForDiskEncryption: false 42 | enabledForTemplateDeployment: false 43 | enableRbacAuthorization: true 44 | enableSoftDelete: true 45 | tenantId: tenant().tenantId 46 | sku: { 47 | name: 'standard' 48 | family: 'A' 49 | } 50 | networkAcls: { 51 | defaultAction: 'Deny' 52 | bypass: 'AzureServices' // Required for AppGW communication 53 | } 54 | } 55 | resource kvsGatewayPublicCert 'secrets' = { 56 | name: 'gateway-public-cert' 57 | properties: { 58 | value: appGatewayListenerCertificate 59 | contentType: 'application/x-pkcs12' 60 | } 61 | } 62 | } 63 | 64 | resource keyVaultPrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-11-01' = { 65 | name: keyVaultPrivateEndpointName 66 | location: location 67 | properties: { 68 | subnet: { 69 | id: vnet::privateEndpointsSubnet.id 70 | } 71 | privateLinkServiceConnections: [ 72 | { 73 | name: keyVaultPrivateEndpointName 74 | properties: { 75 | privateLinkServiceId: keyVault.id 76 | groupIds: [ 77 | 'vault' 78 | ] 79 | } 80 | } 81 | ] 82 | } 83 | } 84 | 85 | resource keyVaultDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 86 | name: keyVaultDnsZoneName 87 | location: 'global' 88 | properties: {} 89 | } 90 | 91 | resource keyVaultDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 92 | parent: keyVaultDnsZone 93 | name: '${keyVaultDnsZoneName}-link' 94 | location: 'global' 95 | properties: { 96 | registrationEnabled: false 97 | virtualNetwork: { 98 | id: vnet.id 99 | } 100 | } 101 | } 102 | 103 | resource keyVaultDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-11-01' = { 104 | name: keyVaultDnsGroupName 105 | properties: { 106 | privateDnsZoneConfigs: [ 107 | { 108 | name: keyVaultDnsZoneName 109 | properties: { 110 | privateDnsZoneId: keyVaultDnsZone.id 111 | } 112 | } 113 | ] 114 | } 115 | dependsOn: [ 116 | keyVaultPrivateEndpoint 117 | ] 118 | } 119 | 120 | resource sqlConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { 121 | parent: keyVault 122 | name: 'adWorksConnString' 123 | properties: { 124 | value: sqlConnectionString 125 | } 126 | } 127 | 128 | @description('The name of the key vault account.') 129 | output keyVaultName string= keyVault.name 130 | 131 | @description('Uri to the secret holding the cert.') 132 | output gatewayCertSecretUri string = keyVault::kvsGatewayPublicCert.properties.secretUri 133 | -------------------------------------------------------------------------------- /infra-as-code/bicep/storage.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | Deploy storage account with private endpoint and private DNS zone 3 | */ 4 | 5 | @description('This is the base name for each Azure resource name (6-12 chars)') 6 | param baseName string 7 | 8 | @description('The resource group location') 9 | param location string = resourceGroup().location 10 | 11 | // existing resource name params 12 | param vnetName string 13 | param privateEndpointsSubnetName string 14 | 15 | // variables 16 | var storageName = 'st${baseName}' 17 | var storageSkuName = 'Standard_LRS' 18 | var storageDnsGroupName = '${storagePrivateEndpointName}/default' 19 | var storagePrivateEndpointName = 'pep-${storageName}' 20 | var blobStorageDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}' 21 | 22 | // ---- Existing resources ---- 23 | resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { 24 | name: vnetName 25 | 26 | resource privateEndpointsSubnet 'subnets' existing = { 27 | name: privateEndpointsSubnetName 28 | } 29 | } 30 | 31 | // ---- Storage resources ---- 32 | resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' = { 33 | name: storageName 34 | location: location 35 | sku: { 36 | name: storageSkuName 37 | } 38 | kind: 'StorageV2' 39 | properties: { 40 | accessTier: 'Hot' 41 | allowBlobPublicAccess: false 42 | allowSharedKeyAccess: false 43 | encryption: { 44 | keySource: 'Microsoft.Storage' 45 | requireInfrastructureEncryption: false 46 | services: { 47 | blob: { 48 | enabled: true 49 | keyType: 'Account' 50 | } 51 | } 52 | } 53 | minimumTlsVersion: 'TLS1_2' 54 | networkAcls: { 55 | bypass: 'AzureServices' 56 | defaultAction: 'Deny' 57 | } 58 | supportsHttpsTrafficOnly: true 59 | } 60 | } 61 | 62 | resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-11-01' = { 63 | name: storagePrivateEndpointName 64 | location: location 65 | properties: { 66 | subnet: { 67 | id: vnet::privateEndpointsSubnet.id 68 | } 69 | privateLinkServiceConnections: [ 70 | { 71 | name: storagePrivateEndpointName 72 | properties: { 73 | groupIds: [ 74 | 'blob' 75 | ] 76 | privateLinkServiceId: storage.id 77 | } 78 | } 79 | ] 80 | } 81 | } 82 | 83 | resource storageDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 84 | name: blobStorageDnsZoneName 85 | location: 'global' 86 | properties: {} 87 | } 88 | 89 | resource storageDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 90 | parent: storageDnsZone 91 | name: '${blobStorageDnsZoneName}-link' 92 | location: 'global' 93 | properties: { 94 | registrationEnabled: false 95 | virtualNetwork: { 96 | id: vnet.id 97 | } 98 | } 99 | } 100 | 101 | resource storageDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-11-01' = { 102 | name: storageDnsGroupName 103 | properties: { 104 | privateDnsZoneConfigs: [ 105 | { 106 | name: blobStorageDnsZoneName 107 | properties: { 108 | privateDnsZoneId: storageDnsZone.id 109 | } 110 | } 111 | ] 112 | } 113 | dependsOn: [ 114 | storagePrivateEndpoint 115 | ] 116 | } 117 | 118 | @description('The name of the storage account.') 119 | output storageName string = storage.name 120 | -------------------------------------------------------------------------------- /infra-as-code/bicep/webapp.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | Deploy a web app with a managed identity, diagnostic, and a private endpoint 3 | */ 4 | 5 | @description('This is the base name for each Azure resource name (6-12 chars)') 6 | param baseName string 7 | 8 | @description('The resource group location') 9 | param location string = resourceGroup().location 10 | 11 | param developmentEnvironment bool 12 | param publishFileName string 13 | 14 | // existing resource name params 15 | param vnetName string 16 | param appServicesSubnetName string 17 | param privateEndpointsSubnetName string 18 | param storageName string 19 | param keyVaultName string 20 | param logWorkspaceName string 21 | 22 | // variables 23 | var appName = 'app-${baseName}' 24 | var appServicePlanName = 'asp-${appName}${uniqueString(subscription().subscriptionId)}' 25 | var appServiceManagedIdentityName = 'id-${appName}' 26 | var packageLocation = 'https://${storageName}.blob.${environment().suffixes.storage}/deploy/${publishFileName}' 27 | var appServicePrivateEndpointName = 'pep-${appName}' 28 | var appInsightsName= 'appinsights-${appName}' 29 | 30 | var appServicePlanPremiumSku = 'Premium' 31 | var appServicePlanStandardSku = 'Standard' 32 | var appServicePlanSettings = { 33 | Standard: { 34 | name: 'S1' 35 | capacity: 1 36 | } 37 | Premium: { 38 | name: 'P2v2' 39 | capacity: 3 40 | } 41 | } 42 | 43 | var appServicesDnsZoneName = 'privatelink.azurewebsites.net' 44 | var appServicesDnsGroupName = '${appServicePrivateEndpointName}/default' 45 | 46 | // ---- Existing resources ---- 47 | resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { 48 | name: vnetName 49 | 50 | resource appServicesSubnet 'subnets' existing = { 51 | name: appServicesSubnetName 52 | } 53 | resource privateEndpointsSubnet 'subnets' existing = { 54 | name: privateEndpointsSubnetName 55 | } 56 | } 57 | 58 | resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { 59 | name: keyVaultName 60 | } 61 | 62 | resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { 63 | name: storageName 64 | } 65 | 66 | resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 67 | name: logWorkspaceName 68 | } 69 | 70 | // Built-in Azure RBAC role that is applied to a Key Vault to grant secrets content read permissions. 71 | resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 72 | name: '4633458b-17de-408a-b874-0445c86b69e6' 73 | scope: subscription() 74 | } 75 | 76 | // Built-in Azure RBAC role that is applied to a Key storage to grant data reader permissions. 77 | resource blobDataReaderRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 78 | name: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' 79 | scope: subscription() 80 | } 81 | 82 | // ---- Web App resources ---- 83 | 84 | // Managed Identity for App Service 85 | resource appServiceManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 86 | name: appServiceManagedIdentityName 87 | location: location 88 | } 89 | 90 | // Grant the App Service managed identity key vault secrets role permissions 91 | module appServiceSecretsUserRoleAssignmentModule './modules/keyvaultRoleAssignment.bicep' = { 92 | name: 'appServiceSecretsUserRoleAssignmentDeploy' 93 | params: { 94 | roleDefinitionId: keyVaultSecretsUserRole.id 95 | principalId: appServiceManagedIdentity.properties.principalId 96 | keyVaultName: keyVaultName 97 | } 98 | } 99 | 100 | // Grant the App Service managed identity storage data reader role permissions 101 | resource blobDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 102 | scope: storage 103 | name: guid(resourceGroup().id, appServiceManagedIdentity.name, blobDataReaderRole.id) 104 | properties: { 105 | roleDefinitionId: blobDataReaderRole.id 106 | principalType: 'ServicePrincipal' 107 | principalId: appServiceManagedIdentity.properties.principalId 108 | } 109 | } 110 | 111 | //App service plan 112 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { 113 | name: appServicePlanName 114 | location: location 115 | sku: developmentEnvironment ? appServicePlanSettings[appServicePlanStandardSku] : appServicePlanSettings[appServicePlanPremiumSku] 116 | properties: { 117 | zoneRedundant: !developmentEnvironment 118 | } 119 | kind: 'app' 120 | } 121 | 122 | // Web App 123 | resource webApp 'Microsoft.Web/sites@2022-09-01' = { 124 | name: appName 125 | location: location 126 | kind: 'app' 127 | identity: { 128 | type: 'UserAssigned' 129 | userAssignedIdentities: { 130 | '${appServiceManagedIdentity.id}': {} 131 | } 132 | } 133 | properties: { 134 | serverFarmId: appServicePlan.id 135 | virtualNetworkSubnetId: vnet::appServicesSubnet.id 136 | httpsOnly: false 137 | keyVaultReferenceIdentity: appServiceManagedIdentity.id 138 | hostNamesDisabled: false 139 | siteConfig: { 140 | vnetRouteAllEnabled: true 141 | http20Enabled: true 142 | publicNetworkAccess: 'Disabled' 143 | alwaysOn: true 144 | } 145 | } 146 | dependsOn: [ 147 | appServiceSecretsUserRoleAssignmentModule 148 | blobDataReaderRoleAssignment 149 | ] 150 | } 151 | 152 | // App Settings 153 | resource appsettings 'Microsoft.Web/sites/config@2022-09-01' = { 154 | name: 'appsettings' 155 | parent: webApp 156 | properties: { 157 | WEBSITE_RUN_FROM_PACKAGE: packageLocation 158 | WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID: appServiceManagedIdentity.id 159 | AZURE_SQL_CONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=https://${keyVault.name}${environment().suffixes.keyvaultDns}/secrets/adWorksConnString)' 160 | APPINSIGHTS_INSTRUMENTATIONKEY: appInsights.properties.InstrumentationKey 161 | APPLICATIONINSIGHTS_CONNECTION_STRING: appInsights.properties.ConnectionString 162 | ApplicationInsightsAgent_EXTENSION_VERSION: '~2' 163 | } 164 | } 165 | 166 | resource appServicePrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-11-01' = { 167 | name: appServicePrivateEndpointName 168 | location: location 169 | properties: { 170 | subnet: { 171 | id: vnet::privateEndpointsSubnet.id 172 | } 173 | privateLinkServiceConnections: [ 174 | { 175 | name: appServicePrivateEndpointName 176 | properties: { 177 | privateLinkServiceId: webApp.id 178 | groupIds: [ 179 | 'sites' 180 | ] 181 | } 182 | } 183 | ] 184 | } 185 | } 186 | 187 | resource appServiceDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 188 | name: appServicesDnsZoneName 189 | location: 'global' 190 | properties: {} 191 | } 192 | 193 | resource appServiceDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 194 | parent: appServiceDnsZone 195 | name: '${appServicesDnsZoneName}-link' 196 | location: 'global' 197 | properties: { 198 | registrationEnabled: false 199 | virtualNetwork: { 200 | id: vnet.id 201 | } 202 | } 203 | } 204 | 205 | resource appServiceDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-11-01' = { 206 | name: appServicesDnsGroupName 207 | properties: { 208 | privateDnsZoneConfigs: [ 209 | { 210 | name: 'privatelink.azurewebsites.net' 211 | properties: { 212 | privateDnsZoneId: appServiceDnsZone.id 213 | } 214 | } 215 | ] 216 | } 217 | dependsOn: [ 218 | appServicePrivateEndpoint 219 | ] 220 | } 221 | 222 | // App service plan diagnostic settings 223 | resource appServicePlanDiagSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 224 | name: '${appServicePlan.name}-diagnosticSettings' 225 | scope: appServicePlan 226 | properties: { 227 | workspaceId: logWorkspace.id 228 | metrics: [ 229 | { 230 | category: 'AllMetrics' 231 | enabled: true 232 | } 233 | ] 234 | } 235 | } 236 | 237 | //Web App diagnostic settings 238 | resource webAppDiagSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 239 | name: '${webApp.name}-diagnosticSettings' 240 | scope: webApp 241 | properties: { 242 | workspaceId: logWorkspace.id 243 | logs: [ 244 | { 245 | category: 'AppServiceHTTPLogs' 246 | categoryGroup: null 247 | enabled: true 248 | } 249 | { 250 | category: 'AppServiceConsoleLogs' 251 | categoryGroup: null 252 | enabled: true 253 | } 254 | { 255 | category: 'AppServiceAppLogs' 256 | categoryGroup: null 257 | enabled: true 258 | } 259 | ] 260 | metrics: [ 261 | { 262 | category: 'AllMetrics' 263 | enabled: true 264 | } 265 | ] 266 | } 267 | } 268 | 269 | // App service plan auto scale settings 270 | resource appServicePlanAutoScaleSettings 'Microsoft.Insights/autoscalesettings@2022-10-01' = { 271 | name: '${appServicePlan.name}-autoscale' 272 | location: location 273 | properties: { 274 | enabled: true 275 | targetResourceUri: appServicePlan.id 276 | profiles: [ 277 | { 278 | name: 'Scale out condition' 279 | capacity: { 280 | maximum: '5' 281 | default: '1' 282 | minimum: '1' 283 | } 284 | rules: [ 285 | { 286 | scaleAction: { 287 | type: 'ChangeCount' 288 | direction: 'Increase' 289 | cooldown: 'PT5M' 290 | value: '1' 291 | } 292 | metricTrigger: { 293 | metricName: 'CpuPercentage' 294 | metricNamespace: 'microsoft.web/serverfarms' 295 | operator: 'GreaterThan' 296 | timeAggregation: 'Average' 297 | threshold: 70 298 | metricResourceUri: appServicePlan.id 299 | timeWindow: 'PT10M' 300 | timeGrain: 'PT1M' 301 | statistic: 'Average' 302 | } 303 | } 304 | ] 305 | } 306 | ] 307 | } 308 | dependsOn: [ 309 | webApp 310 | appServicePlanDiagSettings 311 | ] 312 | } 313 | 314 | // create application insights resource 315 | resource appInsights 'Microsoft.Insights/components@2020-02-02' = { 316 | name: appInsightsName 317 | location: location 318 | kind: 'web' 319 | properties: { 320 | Application_Type: 'web' 321 | WorkspaceResourceId: logWorkspace.id 322 | } 323 | } 324 | 325 | @description('The name of the app service plan.') 326 | output appServicePlanName string = appServicePlan.name 327 | 328 | @description('The name of the web app.') 329 | output appName string = webApp.name 330 | --------------------------------------------------------------------------------