Skip to content

Commit 4eeb443

Browse files
authored
Optional deployment of Private Networks, Private Endpoints plus optional configuration of an ACL rule for all backend services (#864)
Add `AZURE_USE_PRIVATE_ENDPOINT` and `AZURE_PUBLIC_NETWORK_ACCESS` for private connectivity. Add options for provisioning VM to connect to private chat app
1 parent a4d9386 commit 4eeb443

25 files changed

+845
-59
lines changed

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ These are advanced topics that are not necessary for a basic deployment.
1212
* [Debugging the app on App Service](appservice.md)
1313
* [Local development](localdev.md)
1414
* [App customization](customization.md)
15+
* [Private access](private.md)
1516
* [Data ingestion](data_ingestion.md)
1617
* [Productionizing](productionizing.md)
1718
* [Alternative RAG chat samples](other_samples.md)

docs/deploy_private.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
# Deploying with private access
3+
4+
If you want to disable public access when deploying the Chat App, you can do so by setting `azd` environment values.
5+
6+
## Before you begin
7+
8+
Deploying with public access disabled adds additional cost to your deployment. Please see pricing for the following products:
9+
10+
1. [Private Endpoints](https://azure.microsoft.com/pricing/details/private-link/)
11+
1. The exact number of private endpoints created depends on the [optional features](./deploy_features.md) used.
12+
1. [Private DNS Zones](https://azure.microsoft.com/pricing/details/dns/)
13+
1. (Optional, but recommended)[Azure Virtual Machines](https://azure.microsoft.com/pricing/details/virtual-machines/windows/)
14+
1. (Optional, but recommended)[Azure Bastion](https://azure.microsoft.com/pricing/details/azure-bastion/)
15+
16+
## Environment variables controlling private access
17+
18+
1. `AZURE_PUBLIC_NETWORK_ACCESS`: Controls the value of public network access on supported Azure resources. Valid values are 'Enabled' or 'Disabled'.
19+
1. When public network access is 'Enabled', Azure resources are open to the internet.
20+
1. When public network access is 'Disabled', Azure resources are only accessible over a virtual network.
21+
1. `AZURE_USE_PRIVATE_ENDPOINT`: Controls deployment of [private endpoints](https://learn.microsoft.com/azure/private-link/private-endpoint-overview) which connect Azure resources to the virtual network.
22+
1. When set to 'true', ensures private endpoints are deployed for connectivity even when `AZURE_PUBLIC_NETWORK_ACCESS` is 'Disabled'.
23+
1. Note that private endpoints do not make the chat app accessible from the internet. Connections must be initiated from inside the virtual network.
24+
1. `AZURE_PROVISION_VM`: Controls deployment of a [virtual machine](https://learn.microsoft.com/azure/virtual-machines/overview) and [Azure Bastion](https://learn.microsoft.com/azure/bastion/bastion-overview). Azure Bastion allows you to securely connect to the virtual machine, without being connected virtual network. Since the virtual machine is connected to the virtual network, you are able to access the chat app.
25+
1. You must set `AZURE_VM_USERNAME` and `AZURE_VM_PASSWORD` to provision the built-in administrator account with the virtual machine so you can log in through Azure Bastion.
26+
1. By default, a server version of Windows is used for the VM. If you need to [enroll your device in Microsoft Intune](https://learn.microsoft.com/mem/intune/user-help/enroll-windows-10-device), you should use a desktop version of Windows by setting the following environment variables:
27+
* `azd env set AZURE_VM_OS_PUBLISHER MicrosoftWindowsDesktop`
28+
* `azd env set AZURE_VM_OS_OFFER Windows-11`
29+
* `azd env set AZURE_VM_OS_VERSION win11-23h2-pro`
30+
31+
## Recommended deployment strategy for private access
32+
33+
1. Deploy the app with private endpoints enabled and public access enabled.
34+
```
35+
azd env set AZURE_USE_PRIVATE_ENDPOINT true
36+
azd env set AZURE_PUBLIC_NETWORK_ACCESS Enabled
37+
azd up
38+
```
39+
2. Validate that you can connect to the chat app and it's working as expected from the internet.
40+
3. Re-provision the app with public access disabled.
41+
```
42+
azd env set AZURE_PUBLIC_NETWORK_ACCESS Disabled
43+
azd env set AZURE_PROVISION_VM true # Optional but recommended
44+
azd env set AZURE_VM_USERNAME myadminusername # https://learn.microsoft.com/azure/virtual-machines/windows/faq#what-are-the-username-requirements-when-creating-a-vm-
45+
azd env set AZURE_VM_PASSWORD mypassword # https://learn.microsoft.com/azure/virtual-machines/windows/faq#what-are-the-password-requirements-when-creating-a-vm-
46+
azd provision
47+
```
48+
4. Log into your new VM using [Azure Bastion](https://learn.microsoft.com/azure/bastion/tutorial-create-host-portal#connect). Validate the chat app is accessible from the virtual machine using a web browser.

docs/login_and_acl.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ In both the chat and ask a question modes, under **Developer settings** optional
143143

144144
The sample supports 2 main strategies for adding data with document level access control.
145145

146-
* [Using the Add Documents API](#using-the-add-documents-api). Sample scripts are provided which use the Azure AI Search Service [Add Documents API](https://learn.microsoft.com/rest/api/searchservice/documents/?view=rest-searchservice-2023-11-01&tabs=HTTP) to directly manage access control information on _existing documents_ in the index.
146+
* [Using the Add Documents API](#using-the-add-documents-api). Sample scripts are provided which use the Azure AI Search Service Add Documents API to directly manage access control information on _existing documents_ in the index.
147147
* [Using prepdocs and Azure Data Lake Storage Gen 2](#azure-data-lake-storage-gen2-setup). Sample scripts are provided which set up an [Azure Data Lake Storage Gen 2](https://learn.microsoft.com/azure/storage/blobs/data-lake-storage-introduction) account, set the [access control information](https://learn.microsoft.com/azure/storage/blobs/data-lake-storage-access-control) on files and folders stored there, and ingest those documents into the search index with their access control information.
148148

149149
### Using the Add Documents API

docs/productionizing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ and scale up the maximum/minimum based on load.
5959
* **Authentication**: By default, the deployed app is publicly accessible.
6060
We recommend restricting access to authenticated users.
6161
See [Enabling authentication](./deploy_features.md#enabling-authentication) to learn how to enable authentication.
62-
* **Networking**: We recommend deploying inside a Virtual Network. If the app is only for
62+
* **Networking**: We recommend [deploying inside a Virtual Network](./deploy_private.md). If the app is only for
6363
internal enterprise use, use a private DNS zone. Also consider using Azure API Management (APIM)
6464
for firewalls and other forms of protection.
6565
For more details, read [Azure OpenAI Landing Zone reference architecture](https://techcommunity.microsoft.com/t5/azure-architecture-blog/azure-openai-landing-zone-reference-architecture/ba-p/3882102).

infra/abbreviations.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@
105105
"operationalInsightsWorkspaces": "log-",
106106
"portalDashboards": "dash-",
107107
"powerBIDedicatedCapacities": "pbi-",
108+
"privateEndpoint": "pe-",
109+
"privateLink": "pl-",
108110
"purviewAccounts": "pview-",
109111
"recoveryServicesVaults": "rsv-",
110112
"resourcesResourceGroups": "rg-",
@@ -129,6 +131,7 @@
129131
"synapseWorkspacesSqlPoolsDedicated": "syndp",
130132
"synapseWorkspacesSqlPoolsSpark": "synsp",
131133
"timeSeriesInsightsEnvironments": "tsi-",
134+
"virtualNetworks": "vnet-",
132135
"webServerFarms": "plan-",
133136
"webSitesAppService": "app-",
134137
"webSitesAppServiceEnvironment": "ase-",

infra/core/ai/cognitiveservices.bicep

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,29 @@ param publicNetworkAccess string = 'Enabled'
1313
param sku object = {
1414
name: 'S0'
1515
}
16+
param ipRules array = []
17+
@allowed([ 'None', 'AzureServices' ])
18+
param bypass string = 'None'
1619

17-
param allowedIpRules array = []
18-
param networkAcls object = empty(allowedIpRules) ? {
20+
var networkAcls = {
1921
defaultAction: 'Allow'
20-
} : {
21-
ipRules: allowedIpRules
22-
defaultAction: 'Deny'
2322
}
2423

25-
resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
24+
var networkAclsWithBypass = {
25+
defaultAction: 'Allow'
26+
bypass: bypass
27+
}
28+
29+
resource account 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = {
2630
name: name
2731
location: location
2832
tags: tags
2933
kind: kind
3034
properties: {
3135
customSubDomainName: customSubDomainName
3236
publicNetworkAccess: publicNetworkAccess
33-
networkAcls: networkAcls
37+
// Document Intelligence (FormRecognizer) does not support bypass in network acls
38+
networkAcls: kind == 'FormRecognizer' ? networkAcls : networkAclsWithBypass
3439
disableLocalAuth: disableLocalAuth
3540
}
3641
sku: sku
@@ -52,4 +57,4 @@ resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01
5257

5358
output endpoint string = account.properties.endpoint
5459
output id string = account.id
55-
output name string = account.name
60+
output name string = account.name

infra/core/host/appservice.bicep

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ param applicationInsightsName string = ''
88
param appServicePlanId string
99
param keyVaultName string = ''
1010
param managedIdentity bool = !empty(keyVaultName)
11+
param virtualNetworkSubnetId string = ''
1112

1213
// Runtime Properties
1314
@allowed([
@@ -44,44 +45,54 @@ param serverAppId string = ''
4445
@secure()
4546
param clientSecretSettingName string = ''
4647
param authenticationIssuerUri string = ''
48+
@allowed([ 'Enabled', 'Disabled' ])
49+
param publicNetworkAccess string = 'Enabled'
4750
param enableUnauthenticatedAccess bool = false
4851

4952
var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ]
5053
var loginEndpoint = environment().authentication.loginEndpoint
5154
var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint
52-
var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [loginEndpointFixed]) : msftAllowedOrigins
55+
var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins
5356

5457
// .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly
5558
// Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent
56-
var requiredScopes = ['api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access']
57-
var requiredAudiences = ['api://${serverAppId}']
59+
var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ]
60+
var requiredAudiences = [ 'api://${serverAppId}' ]
61+
62+
var coreConfig = {
63+
linuxFxVersion: linuxFxVersion
64+
alwaysOn: alwaysOn
65+
ftpsState: ftpsState
66+
appCommandLine: appCommandLine
67+
numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null
68+
minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null
69+
minTlsVersion: '1.2'
70+
use32BitWorkerProcess: use32BitWorkerProcess
71+
functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
72+
healthCheckPath: healthCheckPath
73+
cors: {
74+
allowedOrigins: union(allMsftAllowedOrigins, allowedOrigins)
75+
}
76+
}
77+
78+
var appServiceProperties = {
79+
serverFarmId: appServicePlanId
80+
siteConfig: coreConfig
81+
clientAffinityEnabled: clientAffinityEnabled
82+
httpsOnly: true
83+
// Always route traffic through the vnet
84+
// See https://learn.microsoft.com/azure/app-service/configure-vnet-integration-routing#configure-application-routing
85+
vnetRouteAllEnabled: !empty(virtualNetworkSubnetId)
86+
virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null
87+
publicNetworkAccess: publicNetworkAccess
88+
}
5889

5990
resource appService 'Microsoft.Web/sites@2022-03-01' = {
6091
name: name
6192
location: location
6293
tags: tags
6394
kind: kind
64-
properties: {
65-
serverFarmId: appServicePlanId
66-
siteConfig: {
67-
linuxFxVersion: linuxFxVersion
68-
alwaysOn: alwaysOn
69-
ftpsState: ftpsState
70-
minTlsVersion: '1.2'
71-
appCommandLine: appCommandLine
72-
numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null
73-
minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null
74-
use32BitWorkerProcess: use32BitWorkerProcess
75-
functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
76-
healthCheckPath: healthCheckPath
77-
cors: {
78-
allowedOrigins: union(allMsftAllowedOrigins, allowedOrigins)
79-
}
80-
}
81-
clientAffinityEnabled: clientAffinityEnabled
82-
httpsOnly: true
83-
}
84-
95+
properties: appServiceProperties
8596
identity: { type: managedIdentity ? 'SystemAssigned' : 'None' }
8697

8798
resource configAppSettings 'config' = {
@@ -91,7 +102,7 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = {
91102
SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment)
92103
ENABLE_ORYX_BUILD: string(enableOryxBuild)
93104
},
94-
runtimeName == 'python' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {},
105+
runtimeName == 'python' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true' } : {},
95106
!empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {},
96107
!empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {})
97108
}
@@ -140,7 +151,7 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = {
140151
openIdIssuer: authenticationIssuerUri
141152
}
142153
login: {
143-
loginParameters: ['scope=${join(union(requiredScopes, additionalScopes), ' ')}']
154+
loginParameters: [ 'scope=${join(union(requiredScopes, additionalScopes), ' ')}' ]
144155
}
145156
validation: {
146157
allowedAudiences: union(requiredAudiences, additionalAllowedAudiences)
@@ -167,6 +178,7 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing
167178
name: applicationInsightsName
168179
}
169180

181+
output id string = appService.id
170182
output identityPrincipalId string = managedIdentity ? appService.identity.principalId : ''
171183
output name string = appService.name
172184
output uri string = 'https://${appService.properties.defaultHostName}'

infra/core/host/vm.bicep

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
param name string
2+
param location string
3+
param vmSize string = 'Standard_DS1_v2'
4+
param adminUsername string
5+
@secure()
6+
param adminPassword string
7+
param osVersion string = '2022-datacenter-azure-edition'
8+
param osPublisher string = 'MicrosoftWindowsServer'
9+
param osOffer string = 'WindowsServer'
10+
param nicId string
11+
param securityType string = 'TrustedLaunch'
12+
13+
var securityProfileJson = {
14+
uefiSettings: {
15+
secureBootEnabled: true
16+
vTpmEnabled: true
17+
}
18+
securityType: securityType
19+
}
20+
21+
resource vm 'Microsoft.Compute/virtualMachines@2022-03-01' = {
22+
name: name
23+
location: location
24+
properties: {
25+
hardwareProfile: {
26+
vmSize: vmSize
27+
}
28+
osProfile: {
29+
computerName: name
30+
adminUsername: adminUsername
31+
adminPassword: adminPassword
32+
}
33+
storageProfile: {
34+
imageReference: {
35+
publisher: osPublisher
36+
offer: osOffer
37+
sku: osVersion
38+
version: 'latest'
39+
}
40+
osDisk: {
41+
createOption: 'FromImage'
42+
managedDisk: {
43+
storageAccountType: 'StandardSSD_LRS'
44+
}
45+
}
46+
dataDisks: [
47+
{
48+
diskSizeGB: 1023
49+
lun: 0
50+
createOption: 'Empty'
51+
}
52+
]
53+
}
54+
networkProfile: {
55+
networkInterfaces: [
56+
{
57+
id: nicId
58+
}
59+
]
60+
}
61+
diagnosticsProfile: {
62+
bootDiagnostics: {
63+
enabled: true
64+
}
65+
}
66+
securityProfile: ((securityType == 'TrustedLaunch') ? securityProfileJson : null)
67+
}
68+
}

infra/core/monitor/applicationinsights.bicep

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ param dashboardName string = ''
44
param location string = resourceGroup().location
55
param tags object = {}
66
param logAnalyticsWorkspaceId string
7+
@allowed([ 'Enabled', 'Disabled' ])
8+
param publicNetworkAccessForIngestion string = 'Enabled'
9+
@allowed([ 'Enabled', 'Disabled' ])
10+
param publicNetworkAccessForQuery string = 'Enabled'
711

812
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
913
name: name
@@ -13,10 +17,12 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
1317
properties: {
1418
Application_Type: 'web'
1519
WorkspaceResourceId: logAnalyticsWorkspaceId
20+
publicNetworkAccessForIngestion: publicNetworkAccessForIngestion
21+
publicNetworkAccessForQuery: publicNetworkAccessForQuery
1622
}
1723
}
1824

19-
module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) {
25+
module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) {
2026
name: 'application-insights-dashboard'
2127
params: {
2228
name: dashboardName
@@ -28,3 +34,4 @@ module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if
2834
output connectionString string = applicationInsights.properties.ConnectionString
2935
output instrumentationKey string = applicationInsights.properties.InstrumentationKey
3036
output name string = applicationInsights.name
37+
output id string = applicationInsights.id

infra/core/monitor/loganalytics.bicep

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@ metadata description = 'Creates a Log Analytics workspace.'
22
param name string
33
param location string = resourceGroup().location
44
param tags object = {}
5+
@allowed([ 'Enabled', 'Disabled' ])
6+
param publicNetworkAccessForIngestion string = 'Enabled'
7+
@allowed([ 'Enabled', 'Disabled' ])
8+
param publicNetworkAccessForQuery string = 'Enabled'
59

610
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
711
name: name
812
location: location
913
tags: tags
10-
properties: any({
14+
properties: {
1115
retentionInDays: 30
1216
features: {
1317
searchVersion: 1
1418
}
1519
sku: {
1620
name: 'PerGB2018'
1721
}
18-
})
22+
publicNetworkAccessForIngestion: publicNetworkAccessForIngestion
23+
publicNetworkAccessForQuery: publicNetworkAccessForQuery
24+
}
1925
}
2026

2127
output id string = logAnalytics.id
22-
output name string = logAnalytics.name
28+
output name string = logAnalytics.name

0 commit comments

Comments
 (0)