├── .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 | 
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 |
--------------------------------------------------------------------------------