├── .github └── workflows │ └── build-dotnetcore.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── private-function-diagram.jpg ├── src ├── MyFunctions.cs ├── func-vnet-storage.csproj ├── global.json ├── host.json ├── local.settings.sample.json └── sample.csv ├── template ├── azuredeploy.json ├── azuredeploy.parameters.json ├── deploy.azcli └── deploy.sh └── terraform └── README.md /.github/workflows/build-dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: Build .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - ./template 8 | pull_request: 9 | branches: [ master ] 10 | paths-ignore: ./template 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build .NET Core 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: 3.1.x 23 | - name: Install dependencies 24 | working-directory: ./src 25 | run: dotnet restore 26 | - name: Build 27 | working-directory: ./src 28 | run: dotnet build --configuration Release --no-restore 29 | - name: Test 30 | working-directory: ./src 31 | run: dotnet test --no-restore --verbosity normal 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc 265 | src/azure-functions-core-tools/* 266 | template/azure-functions-core-tools/* 267 | azure-functions-core-tools/* 268 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureFunctions.pickProcess}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "src/bin/Release/netcoreapp3.1/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~3", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish" 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "options": { 4 | "cwd": "src" 5 | }, 6 | "tasks": [ 7 | { 8 | "label": "clean", 9 | "command": "dotnet", 10 | "args": [ 11 | "clean", 12 | "/property:GenerateFullPaths=true", 13 | "/consoleloggerparameters:NoSummary" 14 | ], 15 | "type": "process", 16 | "problemMatcher": "$msCompile" 17 | }, 18 | { 19 | "label": "build", 20 | "command": "dotnet", 21 | "args": [ 22 | "build", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "type": "process", 27 | "dependsOn": "clean", 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | }, 32 | "problemMatcher": "$msCompile" 33 | }, 34 | { 35 | "label": "clean release", 36 | "command": "dotnet", 37 | "args": [ 38 | "clean", 39 | "--configuration", 40 | "Release", 41 | "/property:GenerateFullPaths=true", 42 | "/consoleloggerparameters:NoSummary" 43 | ], 44 | "type": "process", 45 | "problemMatcher": "$msCompile" 46 | }, 47 | { 48 | "label": "publish", 49 | "command": "dotnet", 50 | "args": [ 51 | "publish", 52 | "--configuration", 53 | "Release", 54 | "/property:GenerateFullPaths=true", 55 | "/consoleloggerparameters:NoSummary" 56 | ], 57 | "type": "process", 58 | "dependsOn": "clean release", 59 | "problemMatcher": "$msCompile" 60 | }, 61 | { 62 | "type": "func", 63 | "dependsOn": "build", 64 | "options": { 65 | "cwd": "${workspaceFolder}/src/bin/Debug/netcoreapp3.1" 66 | }, 67 | "command": "host start", 68 | "isBackground": true, 69 | "problemMatcher": "$func-watch" 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Functions with Private Endpoints 2 | 3 | :construction: This is still under development! :construction: 4 | 5 | ![Build .NET Core](https://github.com/mcollier/azure-functions-private-storage/workflows/Build%20.NET%20Core/badge.svg) 6 | 7 | ## Summary 8 | 9 | This sample shows how to use Azure Functions with [private endpoints](https://docs.microsoft.com/azure/private-link/private-endpoint-overview) for Azure Storage and CosmosDB. The use of private endpoints enables private (virtual network only) access to designated Azure resources. 10 | 11 | One of the key scenarios in this sample is the use of Azure Storage private endpoints with the storage account required for use with Azure Functions. Azure Functions uses a storage account for metadata related to the runtime and various triggers, as well as application code. The storage account is referenced in the [AzureWebJobsStorage](https://docs.microsoft.com/azure/azure-functions/functions-app-settings#azurewebjobsstorage) application setting. The *AzureWebJobsStorage* account will be configured for access via private endpoints. 12 | 13 | ## Deployment 14 | 15 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgithub.com%2Fmcollier%2Fazure-functions-private-storage.git) 16 | 17 | ### Prerequisites 18 | 19 | - Azure subscription. Get a free Azure account at [https://azure.microsoft.com/free/](https://azure.microsoft.com/free/). 20 | - [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) 21 | - [Azure Functions Core Tools](https://docs.microsoft.com/azure/azure-functions/functions-run-local) 22 | 23 | ### Resource Manager Template 24 | 25 | Execute the [ARM template in the template directory](./template/azuredeploy.json). A [script](./template/deploy.sh) is provided to deploy the template. 26 | 27 | The template will provision all the necessary Azure resources. The template will also create the application settings needed by the included Azure Function sample code. The function can optionally (disabled by default) publish the function. 28 | 29 | The function can be published manually by using the [Azure Functions Core Tools](https://docs.microsoft.com/azure/azure-functions/functions-run-local?tabs=linux%2Ccsharp%2Cbash#publish). 30 | 31 | ```bash 32 | func azure functionapp publish 33 | ``` 34 | 35 | ## Architecture Overview 36 | 37 | This sample will demonstrate an Azure Function which retrieves files from an Azure Storage blob container, performs simple operations against the retrieved file data, and finally persists the data to an Azure CosmosDB collection. The function will communicate with the source Azure Storage account and the destination CosmosDB collection via private endpoints. 38 | 39 | ![Architecture diagram](private-function-diagram.jpg) 40 | 41 | ### Azure Function 42 | 43 | The Azure Function app provisioned in this sample uses an [Azure Functions Premium plan](https://docs.microsoft.com/azure/azure-functions/functions-premium-plan#features). The Premium plan is used to enable virtual network integration. Virtual network integration is significant in this sample as the storage accounts used by the function app can only be accessed via private endpoints within the virtual network. 44 | 45 | There are a few important details about the configuration of the function: 46 | 47 | - Virtual network trigger support must be enabled in order for the function to trigger based on resources using a private endpoint 48 | - In order to make [calls to a resource using a private endpoint](https://docs.microsoft.com/azure/azure-functions/functions-networking-options#azure-dns-private-zones), it is necessary to integrate with Azure DNS Private Zones. Therefore, it is necessary to configure the app to use a specific Azure DNS server. This is accomplished by setting `WEBSITE_DNS_SERVER` to 168.63.129.16 and `WEBSITE_VNET_ROUTE_ALL` to 1. 49 | - Enable the application content to be accessible over the virtual network. This is accomplished by setting `WEBSITE_CONTENTOVERVNET` to 1. 50 | 51 | The function is configured to [run from a deployment package](https://docs.microsoft.com/azure/azure-functions/run-functions-from-deployment-package). As such, the package is persisted in an Azure File share referenced by the [WEBSITE_CONTENTAZUREFILECONNECTIONSTRING](https://docs.microsoft.com/azure/azure-functions/functions-app-settings#website_contentazurefileconnectionstring) application setting. 52 | 53 | For more information on restricting an Azure storage account to a virtual network for use with Azure Functions, please [refer to this official documentation](https://docs.microsoft.com/azure/azure-functions/configure-networking-how-to#restrict-your-storage-account-to-a-virtual-network). 54 | 55 | ### Azure Storage accounts 56 | 57 | There are three Azure Storage accounts used in this sample: 58 | 59 | - a storage accounts which use a private endpoint for the Azure Functions runtime 60 | - a storage account with a private endpoint, which is set up with a blob container (created by the ARM template). This is the storage account on which the function triggers (blob trigger). 61 | - one storage account used by the VM for diagnostics 62 | 63 | ### Azure CosmosDB 64 | 65 | Azure CosmosDB is used to persist the data processed by the Azure Function. An Azure Function output binding is used for writing the data to the configured database and collection. The ARM template will create the CosmosDB database account and collection. 66 | 67 | A [private endpoint is created and configured for use with CosmosDB](https://docs.microsoft.com/azure/cosmos-db/how-to-configure-private-endpoints). 68 | 69 | ### Azure VM and Bastion 70 | 71 | An Azure VM is created as a way to access the Azure resources from within the virtual network. The VM has no public IP address nor port access (e.g. RDP). [Azure Bastion](https://docs.microsoft.com/azure/bastion/bastion-overview) is used to connect to the VM. 72 | 73 | The included ARM template configures the VM to shut down each evening at 7pm UTC. This is done as a cost-savings measure. 74 | 75 | ### Virtual Network 76 | 77 | Azure resources in this sample either integrate with or are placed within a virtual network. The use of private endpoints keeps network traffic contained with the virtual network. 78 | 79 | The sample uses four subnets: 80 | 81 | - Subnet for Azure Function virtual network integration. This subnet is delegated to the function. 82 | - Subnet for private endpoints. Private IP addresses are allocated from this subnet. 83 | - Subnet for the virtual machine. 84 | - Subnet for the Azure Bastion host. 85 | 86 | ### Private Endpoints 87 | 88 | [Azure Private Endpoints](https://docs.microsoft.com/azure/private-link/private-endpoint-overview) are used to connect to specific Azure resources using a private IP address. This ensures that network traffic remains within the designated virtual network, and access is available only for specific resources. This sample configures private endpoints for the following Azure resources: 89 | 90 | - [CosmosDB](https://docs.microsoft.com/azure/cosmos-db/how-to-configure-private-endpoints) 91 | - [Azure Storage](https://docs.microsoft.com/azure/storage/common/storage-private-endpoints) 92 | - Azure File storage 93 | - Azure Blob storage 94 | - Azure Queue storage 95 | - Azure Table storage 96 | 97 | ### Private DNS Zones 98 | 99 | Using a private endpoint to connect to Azure resources means connecting to a private IP address instead of the public endpoint. Existing Azure services are configured to use existing DNS to connect to the public endpoint. The DNS configuration will need to be overridden to connect to the private endpoint. 100 | 101 | A private DNS zone will be created for each Azure resource configured with a private endpoint. A DNS A record is created for each private IP address associated with the private endpoint. 102 | 103 | The following DNS zones are created in this sample: 104 | 105 | - privatelink.queue.core.windows.net 106 | - privatelink.blob.core.windows.net 107 | - privatelink.table.core.windows.net 108 | - privatelink.file.core.windows.net 109 | - privatelink.documents.azure.com 110 | 111 | ### Application Insights 112 | 113 | [Application Insights](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview) is used to [monitor the Azure Function](https://docs.microsoft.com/azure/azure-functions/functions-monitoring). 114 | -------------------------------------------------------------------------------- /private-function-diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollier/azure-functions-private-storage/6a400a42bc86284c1467def2f9c53dd6b6feb054/private-function-diagram.jpg -------------------------------------------------------------------------------- /src/MyFunctions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Extensions.Logging; 5 | using Newtonsoft.Json; 6 | using System.Collections.Generic; 7 | using Newtonsoft.Json.Linq; 8 | using System.Linq; 9 | 10 | namespace Company.Function 11 | { 12 | public static class MyFunctions 13 | { 14 | [FunctionName("CensusDataFilesFunction")] 15 | public static async Task ProcessCensusDataFiles( 16 | [BlobTrigger("%ContainerName%/{blobName}", Connection = "CensusResultsAzureStorageConnection")] Stream myBlobStream, 17 | string blobName, 18 | [CosmosDB( 19 | databaseName: "%CosmosDbName%", 20 | collectionName: "%CosmosDbCollectionName%", 21 | ConnectionStringSetting = "CosmosDBConnection")] IAsyncCollector items, 22 | ILogger logger) 23 | { 24 | logger.LogInformation($"C# Blob trigger function processed blob of name '{blobName}' with size of {myBlobStream.Length} bytes"); 25 | 26 | var jsonObject = await ConvertCsvToJsonAsync(myBlobStream); 27 | 28 | foreach (var item in jsonObject) 29 | { 30 | await items.AddAsync(JsonConvert.SerializeObject(item)); 31 | } 32 | } 33 | 34 | private static async Task> ConvertCsvToJsonAsync(Stream csvStream) 35 | { 36 | string[] header = { }; 37 | 38 | List jsonObjects = new List(); 39 | 40 | using (var streamReader = new StreamReader(csvStream)) 41 | { 42 | string line = null; 43 | 44 | while ((line = await streamReader.ReadLineAsync()) != null) 45 | { 46 | // Assume the first line contains the headers. Dangerous assumption? 47 | if (header.Length == 0) 48 | { 49 | header = line.Split(',').Select(h => h.Trim()).ToArray(); 50 | } 51 | else 52 | { 53 | // Assume all other lines are data elements. 54 | 55 | // https://stackoverflow.com/a/50741265 56 | 57 | string[] lineItem = line.Split(',').Select(i => i.Trim()).ToArray(); 58 | var itemWithHeader = header.Zip(lineItem, (h, v) => new KeyValuePair(h, v)); 59 | var jsonItem = new JObject(itemWithHeader.Select(j => new JProperty(j.Key, j.Value))); 60 | 61 | jsonObjects.Add(jsonItem); 62 | } 63 | } 64 | } 65 | 66 | return jsonObjects; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/func-vnet-storage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | func_vnet_storage 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | PreserveNewest 18 | Never 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "3.1.407" 4 | } 5 | } -------------------------------------------------------------------------------- /src/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingExcludedTypes": "Request", 6 | "samplingSettings": { 7 | "isEnabled": true 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/local.settings.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | "ContainerName":"", 7 | "CosmosDbName":"", 8 | "CosmosDbCollectionName":"", 9 | "CensusResultsAzureStorageConnection":"", 10 | "CosmosDBConnection":"" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/sample.csv: -------------------------------------------------------------------------------- 1 | postal-code, name, age 2 | 43040, michael, 41 3 | 43040, sonja, 40 4 | 43322, joe, 54 5 | 56873, sam, 21 -------------------------------------------------------------------------------- /template/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "virtualMachineAdminUsername": { 6 | "type": "string" 7 | }, 8 | "virtualMachineAdminPassword": { 9 | "type": "securestring" 10 | }, 11 | "cosmosDbDatabaseName": { 12 | "type": "string" 13 | }, 14 | "cosmosDbContainerName": { 15 | "type": "string" 16 | }, 17 | "blobContainerName": { 18 | "type": "string" 19 | }, 20 | "deploySourceCode": { 21 | "type": "bool", 22 | "defaultValue": false 23 | }, 24 | "location": { 25 | "type": "string", 26 | "defaultValue": "[resourceGroup().location]" 27 | }, 28 | "function_repo_url": { 29 | "type": "string" 30 | }, 31 | "virtualNetworkAddressPrefix": { 32 | "type": "string", 33 | "defaultValue": "10.100.0.0/16" 34 | }, 35 | "bastionSubnetAddressPrefix": { 36 | "type": "string", 37 | "defaultValue": "10.100.3.0/27" 38 | }, 39 | "virtualMachineSubnetAddressPrefix": { 40 | "type": "string", 41 | "defaultValue": "10.100.2.0/24" 42 | }, 43 | "functionSubnetAddressPrefix": { 44 | "type": "string", 45 | "defaultValue": "10.100.0.0/24" 46 | }, 47 | "privateEndpointSubnetAddressPrefix": { 48 | "type": "string", 49 | "defaultValue": "10.100.1.0/24" 50 | } 51 | }, 52 | "variables": { 53 | "uniqueStringId": "[uniqueString(resourceGroup().id)]", 54 | "appServicePlanName": "[concat(variables('uniqueStringId'), '-asp')]", 55 | "functionAppName": "[concat(variables('uniqueStringId'), '-funcapp')]", 56 | "vnetName": "[concat(variables('uniqueStringId'), '-vnet')]", 57 | "functionWebJobsStorageAccountName": "[toLower(concat(variables('uniqueStringId'), 'wjsa'))]", 58 | "censusDataStorageAccountName": "[toLower(concat(variables('uniqueStringId'), 'pe'))]", 59 | "applicationInsightsName": "[concat(variables('uniqueStringId'), '-ai')]", 60 | "functionsSubnetName": "[concat(variables('uniqueStringId'), '-subnet-functions')]", 61 | "privateEndpointSubnetName": "[concat(variables('uniqueStringId'), '-subnet-privateendpoint')]", 62 | "privateEndpointStorageBlobName": "[concat(variables('uniqueStringId'), '-blob-private-endpoint')]", 63 | "privateEndpointCosmosDbName": "[concat(variables('uniqueStringId'), '-cosmosdb-private-endpoint')]", 64 | "privateEndpointWebJobsQueueStorageName": "[concat(variables('uniqueStringId'), '-wjsa-queue-private-endpoint')]", 65 | "privateEndpointWebJobsTableStorageName": "[concat(variables('uniqueStringId'), '-wjsa-table-private-endpoint')]", 66 | "privateEndpointWebJobsBlobStorageName": "[concat(variables('uniqueStringId'), '-wjsa-blob-private-endpoint')]", 67 | "privateEndpointWebJobsFileStorageName": "[concat(variables('uniqueStringId'), '-wjsa-file-private-endpoint')]", 68 | "privateStorageQueueDnsZoneName": "privatelink.queue.core.windows.net", 69 | "privateStorageBlobDnsZoneName": "privatelink.blob.core.windows.net", 70 | "privateStorageTableDnsZoneName": "privatelink.table.core.windows.net", 71 | "privateStorageFileDnsZoneName": "privatelink.file.core.windows.net", 72 | "privateCosmosDbDnsZoneName": "privatelink.documents.azure.com", 73 | "vmDiagnosticStorageAccountName": "[concat(variables('uniqueStringId'), 'vmdiag')]", 74 | "virtualMachineName": "[concat(variables('uniqueStringId'), 'vm')]", 75 | "vmNicName": "[concat(variables('uniqueStringId'), '-vm-nic')]", 76 | "vmSubnetName": "[concat(variables('uniqueStringId'), '-subnet-vm')]", 77 | "vmNsgName": "[concat(variables('uniqueStringId'), '-vm-nsg')]", 78 | "privateCosmosDbAccountName": "[concat(variables('uniqueStringId'), '-cosmosdb-private')]", 79 | "bastionPublicIPAddressName": "[concat(variables('uniqueStringId'), '-bastion-pip')]", 80 | "dnsLabelPrefix": "[concat('a', variables('uniqueStringId'), '-vm')]", 81 | "bastionHostName": "[concat(variables('uniqueStringId'), '-bastion-host')]", 82 | "bastionSubnetName": "AzureBastionSubnet", 83 | "appInsightsResourceId": "[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]" 84 | }, 85 | "resources": [ 86 | { 87 | "type": "Microsoft.Network/virtualNetworks", 88 | "apiVersion": "2019-11-01", 89 | "location": "[parameters('location')]", 90 | "name": "[variables('vnetName')]", 91 | "dependsOn": [ 92 | "[resourceId('Microsoft.Network/networkSecurityGroups/', variables('vmNsgName'))]" 93 | ], 94 | "properties": { 95 | "addressSpace": { 96 | "addressPrefixes": [ 97 | "[parameters('virtualNetworkAddressPrefix')]" 98 | ] 99 | }, 100 | "subnets": [ 101 | { 102 | "name": "[variables('functionsSubnetName')]", 103 | "properties": { 104 | "addressPrefix": "[parameters('functionSubnetAddressPrefix')]", 105 | "privateEndpointNetworkPolicies": "Enabled", 106 | "privateLinkServiceNetworkPolicies": "Enabled", 107 | "delegations": [ 108 | { 109 | "name": "webapp", 110 | "properties": { 111 | "serviceName": "Microsoft.Web/serverFarms", 112 | "actions": [ 113 | "Microsoft.Network/virtualNetworks/subnets/action" 114 | ] 115 | } 116 | } 117 | ] 118 | } 119 | }, 120 | { 121 | "name": "[variables('privateEndpointSubnetName')]", 122 | "properties": { 123 | "addressPrefix": "[parameters('privateEndpointSubnetAddressPrefix')]", 124 | "privateLinkServiceNetworkPolicies": "Enabled", 125 | "privateEndpointNetworkPolicies": "Disabled" 126 | } 127 | }, 128 | { 129 | "name": "[variables('vmSubnetName')]", 130 | "properties": { 131 | "addressPrefix": "[parameters('virtualMachineSubnetAddressPrefix')]", 132 | "networkSecurityGroup": { 133 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('vmNsgName'))]" 134 | }, 135 | "delegations": [ 136 | ], 137 | "serviceEndpoints": [ 138 | ], 139 | "privateLinkServiceNetworkPolicies": "Enabled", 140 | "privateEndpointNetworkPolicies": "Disabled" 141 | } 142 | }, 143 | { 144 | "name": "[variables('bastionSubnetName')]", 145 | "properties": { 146 | "addressPrefix": "[parameters('bastionSubnetAddressPrefix')]", 147 | "delegations": [ 148 | ], 149 | "privateEndpointNetworkPolicies": "Enabled", 150 | "privateLinkServiceNetworkPolicies": "Enabled" 151 | } 152 | } 153 | ], 154 | "virtualNetworkPeerings": [ 155 | ], 156 | "enableDdosProtection": false, 157 | "enableVmProtection": false 158 | } 159 | }, 160 | { 161 | "type": "Microsoft.Storage/storageAccounts", 162 | "apiVersion": "2019-06-01", 163 | "location": "[parameters('location')]", 164 | "name": "[variables('censusDataStorageAccountName')]", 165 | "sku": { 166 | "name": "Standard_LRS", 167 | "tier": "Standard" 168 | }, 169 | "kind": "StorageV2", 170 | "properties": { 171 | "networkAcls": { 172 | "bypass": "AzureServices", 173 | "virtualNetworkRules": [ 174 | ], 175 | "ipRules": [ 176 | ], 177 | "defaultAction": "Deny" 178 | }, 179 | "supportsHttpsTrafficOnly": true, 180 | "encryption": { 181 | "services": { 182 | "file": { 183 | "keyType": "Account", 184 | "enabled": true 185 | }, 186 | "blob": { 187 | "keyType": "Account", 188 | "enabled": true 189 | } 190 | }, 191 | "keySource": "Microsoft.Storage" 192 | } 193 | }, 194 | "resources": [ 195 | { 196 | "name": "[concat('default/', parameters('blobContainerName'))]", 197 | "type": "blobServices/containers", 198 | "apiVersion": "2018-07-01", 199 | "dependsOn": [ 200 | "[variables('censusDataStorageAccountName')]" 201 | ] 202 | } 203 | ] 204 | }, 205 | { 206 | "type": "Microsoft.Storage/storageAccounts", 207 | "apiVersion": "2019-06-01", 208 | "location": "[parameters('location')]", 209 | "dependsOn": [ 210 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 211 | ], 212 | "name": "[variables('functionWebJobsStorageAccountName')]", 213 | "sku": { 214 | "name": "Standard_LRS", 215 | "tier": "Standard" 216 | }, 217 | "kind": "StorageV2", 218 | "properties": { 219 | "networkAcls": { 220 | "bypass": "AzureServices", 221 | "virtualNetworkRules": [ 222 | ], 223 | "ipRules": [ 224 | ], 225 | "defaultAction": "Deny" 226 | }, 227 | "supportsHttpsTrafficOnly": true, 228 | "encryption": { 229 | "services": { 230 | "file": { 231 | "keyType": "Account", 232 | "enabled": true 233 | }, 234 | "blob": { 235 | "keyType": "Account", 236 | "enabled": true 237 | } 238 | }, 239 | "keySource": "Microsoft.Storage" 240 | } 241 | } 242 | }, 243 | { 244 | "type": "Microsoft.Storage/storageAccounts/fileServices/shares", 245 | "apiVersion": "2019-06-01", 246 | "name": "[concat(variables('functionWebJobsStorageAccountName'), '/default/', 'myfunctionfiles')]", 247 | "dependsOn": [ 248 | "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]" 249 | ] 250 | }, 251 | { 252 | "type": "Microsoft.Storage/storageAccounts", 253 | "apiVersion": "2019-04-01", 254 | "name": "[variables('vmDiagnosticStorageAccountName')]", 255 | "location": "[parameters('location')]", 256 | "sku": { 257 | "name": "Standard_LRS" 258 | }, 259 | "kind": "Storage", 260 | "properties": { 261 | } 262 | }, 263 | { 264 | "type": "Microsoft.DocumentDB/databaseAccounts", 265 | "name": "[variables('privateCosmosDbAccountName')]", 266 | "apiVersion": "2021-01-15", 267 | "kind": "GlobalDocumentDB", 268 | "location": "[parameters('location')]", 269 | "properties": { 270 | "consistencyPolicy": { 271 | "defaultConsistencyLevel": "Session" 272 | }, 273 | "locations": [ 274 | { 275 | "locationName": "[parameters('location')]" 276 | } 277 | ], 278 | "databaseAccountOfferType": "Standard", 279 | "enableAutomaticFailover": false, 280 | "enableMultipleWriteLocations": false, 281 | "publicNetworkAccess": "Disabled" 282 | } 283 | }, 284 | { 285 | "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", 286 | "name": "[concat(variables('privateCosmosDbAccountName'), '/', parameters('cosmosDbDatabaseName'))]", 287 | "apiVersion": "2021-01-15", 288 | "dependsOn": [ 289 | "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('privateCosmosDbAccountName'))]" 290 | ], 291 | "properties": { 292 | "resource": { 293 | "id": "[ parameters('cosmosDbDatabaseName')]" 294 | }, 295 | "options": { 296 | } 297 | } 298 | }, 299 | { 300 | "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", 301 | "name": "[concat(variables('privateCosmosDbAccountName'), '/', parameters('cosmosDbDatabaseName'), '/', parameters('cosmosDbContainerName'))]", 302 | "apiVersion": "2021-01-15", 303 | "dependsOn": [ 304 | "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', variables('privateCosmosDbAccountName'), parameters('cosmosDbDatabaseName'))]", 305 | "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('privateCosmosDbAccountName'))]" 306 | ], 307 | "properties": { 308 | "resource": { 309 | "id": "[parameters('cosmosDbContainerName')]", 310 | "partitionKey": { 311 | "paths": [ 312 | "/id" 313 | ], 314 | "kind": "Hash" 315 | }, 316 | "indexingPolicy": { 317 | "indexingMode": "consistent", 318 | "includedPaths": [ 319 | { 320 | "path": "/*" 321 | } 322 | ], 323 | "excludedPaths": [ 324 | { 325 | "path": "/\"_etag\"/?" 326 | } 327 | ] 328 | } 329 | }, 330 | "options": { 331 | "throughput": 400 332 | } 333 | } 334 | }, 335 | { 336 | "type": "Microsoft.Compute/virtualMachines", 337 | "apiVersion": "2019-03-01", 338 | "name": "[variables('virtualMachineName')]", 339 | "location": "[parameters('location')]", 340 | "dependsOn": [ 341 | "[resourceId('Microsoft.Storage/storageAccounts/', variables('vmDiagnosticStorageAccountName'))]", 342 | "[resourceId('Microsoft.Network/networkInterfaces/', variables('vmNicName'))]" 343 | ], 344 | "properties": { 345 | "hardwareProfile": { 346 | "vmSize": "Standard_D2_v3" 347 | }, 348 | "osProfile": { 349 | "computerName": "[variables('virtualMachineName')]", 350 | "adminUsername": "[parameters('virtualMachineAdminUsername')]", 351 | "adminPassword": "[parameters('virtualMachineAdminPassword')]" 352 | }, 353 | "storageProfile": { 354 | "imageReference": { 355 | "publisher": "MicrosoftWindowsDesktop", 356 | "offer": "Windows-10", 357 | "sku": "rs5-pro", 358 | "version": "latest" 359 | }, 360 | "osDisk": { 361 | "createOption": "FromImage" 362 | } 363 | }, 364 | "networkProfile": { 365 | "networkInterfaces": [ 366 | { 367 | "id": "[resourceId('Microsoft.Nework/networkInterfaces', variables('vmNicName'))]" 368 | } 369 | ] 370 | }, 371 | "diagnosticsProfile": { 372 | "bootDiagnostics": { 373 | "enabled": true, 374 | "storageUri": "[reference(resourceId('Microsoft.Storage/storageAccounts/', variables('vmDiagnosticStorageAccountName'))).primaryEndpoints.blob]" 375 | } 376 | } 377 | } 378 | }, 379 | { 380 | "type": "Microsoft.DevTestLab/schedules", 381 | "name": "[concat('shutdown-computevm-', variables('virtualMachineName'))]", 382 | "apiVersion": "2018-09-15", 383 | "location": "[parameters('location')]", 384 | "dependsOn": [ 385 | "[concat('Microsoft.Compute/virtualMachines/', variables('virtualMachineName'))]" 386 | ], 387 | "properties": { 388 | "status": "Enabled", 389 | "taskType": "ComputeVmShutdownTask", 390 | "dailyRecurrence": { 391 | "time": "1900" 392 | }, 393 | "timeZoneId": "UTC", 394 | "notificationSettings": { 395 | "status": "Disabled" 396 | }, 397 | "targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines',variables('virtualMachineName'))]" 398 | } 399 | }, 400 | { 401 | "type": "Microsoft.Insights/components", 402 | "apiVersion": "2015-05-01", 403 | "location": "[parameters('location')]", 404 | "name": "[variables('applicationInsightsName')]", 405 | "kind": "web", 406 | "properties": { 407 | "Application_Type": "web" 408 | } 409 | }, 410 | { 411 | "type": "Microsoft.Network/privateDnsZones", 412 | "apiVersion": "2018-09-01", 413 | "name": "[variables('privateStorageQueueDnsZoneName')]", 414 | "location": "global", 415 | "properties": "" 416 | }, 417 | { 418 | "type": "Microsoft.Network/privateDnsZones", 419 | "apiVersion": "2018-09-01", 420 | "name": "[variables('privateStorageBlobDnsZoneName')]", 421 | "location": "global", 422 | "properties": "" 423 | }, 424 | { 425 | "type": "Microsoft.Network/privateDnsZones", 426 | "apiVersion": "2018-09-01", 427 | "name": "[variables('privateStorageTableDnsZoneName')]", 428 | "location": "global", 429 | "properties": "" 430 | }, 431 | { 432 | "type": "Microsoft.Network/privateDnsZones", 433 | "apiVersion": "2018-09-01", 434 | "name": "[variables('privateStorageFileDnsZoneName')]", 435 | "location": "global", 436 | "properties": "" 437 | }, 438 | { 439 | "type": "Microsoft.Network/privateDnsZones", 440 | "apiVersion": "2018-09-01", 441 | "name": "[variables('privateCosmosDbDnsZoneName')]", 442 | "location": "global", 443 | "properties": "" 444 | }, 445 | { 446 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 447 | "apiVersion": "2018-09-01", 448 | "name": "[concat(variables('privateStorageQueueDnsZoneName'), '/', variables('privateStorageQueueDnsZoneName'), '-link')]", 449 | "location": "global", 450 | "dependsOn": [ 451 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageQueueDnsZoneName'))]", 452 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 453 | ], 454 | "properties": { 455 | "registrationEnabled": false, 456 | "virtualNetwork": { 457 | "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 458 | } 459 | } 460 | }, 461 | { 462 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 463 | "apiVersion": "2018-09-01", 464 | "name": "[concat(variables('privateStorageTableDnsZoneName'), '/', variables('privateStorageTableDnsZoneName'), '-link')]", 465 | "location": "global", 466 | "dependsOn": [ 467 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageTableDnsZoneName'))]", 468 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 469 | ], 470 | "properties": { 471 | "registrationEnabled": false, 472 | "virtualNetwork": { 473 | "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 474 | } 475 | } 476 | }, 477 | { 478 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 479 | "apiVersion": "2018-09-01", 480 | "name": "[concat(variables('privateStorageBlobDnsZoneName'), '/', variables('privateStorageBlobDnsZoneName'), '-link')]", 481 | "location": "global", 482 | "dependsOn": [ 483 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageBlobDnsZoneName'))]", 484 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 485 | ], 486 | "properties": { 487 | "registrationEnabled": false, 488 | "virtualNetwork": { 489 | "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 490 | } 491 | } 492 | }, 493 | { 494 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 495 | "apiVersion": "2018-09-01", 496 | "name": "[concat(variables('privateStorageFileDnsZoneName'), '/', variables('privateStorageFileDnsZoneName'), '-link')]", 497 | "location": "global", 498 | "dependsOn": [ 499 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageFileDnsZoneName'))]", 500 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 501 | ], 502 | "properties": { 503 | "registrationEnabled": false, 504 | "virtualNetwork": { 505 | "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 506 | } 507 | } 508 | }, 509 | { 510 | "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", 511 | "apiVersion": "2018-09-01", 512 | "name": "[concat(variables('privateCosmosDbDnsZoneName'), '/', variables('privateCosmosDbDnsZoneName'), '-link')]", 513 | "location": "global", 514 | "dependsOn": [ 515 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateCosmosDbDnsZoneName'))]", 516 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 517 | ], 518 | "properties": { 519 | "registrationEnabled": false, 520 | "virtualNetwork": { 521 | "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 522 | } 523 | } 524 | }, 525 | { 526 | "type": "Microsoft.Network/privateEndpoints", 527 | "name": "[variables('privateEndpointStorageBlobName')]", 528 | "apiVersion": "2019-11-01", 529 | "location": "[parameters('location')]", 530 | "dependsOn": [ 531 | "[resourceId('Microsoft.Storage/storageAccounts', variables('censusDataStorageAccountName'))]", 532 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 533 | ], 534 | "properties": { 535 | "subnet": { 536 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('privateEndpointSubnetName') )]" 537 | }, 538 | "privateLinkServiceConnections": [ 539 | { 540 | "name": "MyStorageBlobPrivateLinkConnection", 541 | "properties": { 542 | "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('censusDataStorageAccountName'))]", 543 | "groupIds": [ 544 | "blob" 545 | ] 546 | } 547 | } 548 | ] 549 | } 550 | }, 551 | { 552 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 553 | "apiVersion": "2020-03-01", 554 | "location": "[parameters('location')]", 555 | "name": "[concat(variables('privateEndpointStorageBlobName'), '/default')]", 556 | "dependsOn": [ 557 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageBlobDnsZoneName'))]", 558 | "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointStorageBlobName'))]" 559 | ], 560 | "properties": { 561 | "privateDnsZoneConfigs": [ 562 | { 563 | "name": "config1", 564 | "properties": { 565 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageBlobDnsZoneName'))]" 566 | } 567 | } 568 | ] 569 | } 570 | }, 571 | { 572 | "type": "Microsoft.Network/privateEndpoints", 573 | "name": "[variables('privateEndpointWebJobsQueueStorageName')]", 574 | "apiVersion": "2019-11-01", 575 | "location": "[parameters('location')]", 576 | "dependsOn": [ 577 | "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 578 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 579 | ], 580 | "properties": { 581 | "subnet": { 582 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('privateEndpointSubnetName') )]" 583 | }, 584 | "privateLinkServiceConnections": [ 585 | { 586 | "name": "MyStorageQueuePrivateLinkConnection", 587 | "properties": { 588 | "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 589 | "groupIds": [ 590 | "queue" 591 | ] 592 | } 593 | } 594 | ] 595 | } 596 | }, 597 | { 598 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 599 | "apiVersion": "2020-03-01", 600 | "location": "[parameters('location')]", 601 | "name": "[concat(variables('privateEndpointWebJobsQueueStorageName'), '/default')]", 602 | "dependsOn": [ 603 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageQueueDnsZoneName'))]", 604 | "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointWebJobsQueueStorageName'))]" 605 | ], 606 | "properties": { 607 | "privateDnsZoneConfigs": [ 608 | { 609 | "name": "config1", 610 | "properties": { 611 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageQueueDnsZoneName'))]" 612 | } 613 | } 614 | ] 615 | } 616 | }, 617 | { 618 | "type": "Microsoft.Network/privateEndpoints", 619 | "name": "[variables('privateEndpointWebJobsTableStorageName')]", 620 | "apiVersion": "2019-11-01", 621 | "location": "[parameters('location')]", 622 | "dependsOn": [ 623 | "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 624 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 625 | ], 626 | "properties": { 627 | "subnet": { 628 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('privateEndpointSubnetName') )]" 629 | }, 630 | "privateLinkServiceConnections": [ 631 | { 632 | "name": "MyStorageQueuePrivateLinkConnection", 633 | "properties": { 634 | "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 635 | "groupIds": [ 636 | "table" 637 | ] 638 | } 639 | } 640 | ] 641 | } 642 | }, 643 | { 644 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 645 | "apiVersion": "2020-03-01", 646 | "location": "[parameters('location')]", 647 | "name": "[concat(variables('privateEndpointWebJobsTableStorageName'), '/default')]", 648 | "dependsOn": [ 649 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageTableDnsZoneName'))]", 650 | "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointWebJobsTableStorageName'))]" 651 | ], 652 | "properties": { 653 | "privateDnsZoneConfigs": [ 654 | { 655 | "name": "config1", 656 | "properties": { 657 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageTableDnsZoneName'))]" 658 | } 659 | } 660 | ] 661 | } 662 | }, 663 | { 664 | "type": "Microsoft.Network/privateEndpoints", 665 | "name": "[variables('privateEndpointWebJobsBlobStorageName')]", 666 | "apiVersion": "2019-11-01", 667 | "location": "[parameters('location')]", 668 | "dependsOn": [ 669 | "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 670 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 671 | ], 672 | "properties": { 673 | "subnet": { 674 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('privateEndpointSubnetName') )]" 675 | }, 676 | "privateLinkServiceConnections": [ 677 | { 678 | "name": "MyStorageQueuePrivateLinkConnection", 679 | "properties": { 680 | "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 681 | "groupIds": [ 682 | "blob" 683 | ] 684 | } 685 | } 686 | ] 687 | } 688 | }, 689 | { 690 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 691 | "apiVersion": "2020-03-01", 692 | "location": "[parameters('location')]", 693 | "name": "[concat(variables('privateEndpointWebJobsBlobStorageName'), '/default')]", 694 | "dependsOn": [ 695 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageBlobDnsZoneName'))]", 696 | "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointWebJobsBlobStorageName'))]" 697 | ], 698 | "properties": { 699 | "privateDnsZoneConfigs": [ 700 | { 701 | "name": "config1", 702 | "properties": { 703 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageBlobDnsZoneName'))]" 704 | } 705 | } 706 | ] 707 | } 708 | }, 709 | { 710 | "type": "Microsoft.Network/privateEndpoints", 711 | "name": "[variables('privateEndpointWebJobsFileStorageName')]", 712 | "apiVersion": "2019-11-01", 713 | "location": "[parameters('location')]", 714 | "dependsOn": [ 715 | "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 716 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 717 | ], 718 | "properties": { 719 | "subnet": { 720 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('privateEndpointSubnetName') )]" 721 | }, 722 | "privateLinkServiceConnections": [ 723 | { 724 | "name": "MyStorageQueuePrivateLinkConnection", 725 | "properties": { 726 | "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]", 727 | "groupIds": [ 728 | "file" 729 | ] 730 | } 731 | } 732 | ] 733 | } 734 | }, 735 | { 736 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 737 | "apiVersion": "2020-03-01", 738 | "location": "[parameters('location')]", 739 | "name": "[concat(variables('privateEndpointWebJobsFileStorageName'), '/default')]", 740 | "dependsOn": [ 741 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageFileDnsZoneName'))]", 742 | "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointWebJobsFileStorageName'))]" 743 | ], 744 | "properties": { 745 | "privateDnsZoneConfigs": [ 746 | { 747 | "name": "config1", 748 | "properties": { 749 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', variables('privateStorageFileDnsZoneName'))]" 750 | } 751 | } 752 | ] 753 | } 754 | }, 755 | { 756 | "type": "Microsoft.Network/privateEndpoints", 757 | "name": "[variables('privateEndpointCosmosDbName')]", 758 | "apiVersion": "2019-11-01", 759 | "location": "[parameters('location')]", 760 | "dependsOn": [ 761 | "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('privateCosmosDbAccountName'))]", 762 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 763 | ], 764 | "properties": { 765 | "subnet": { 766 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('privateEndpointSubnetName') )]" 767 | }, 768 | "privateLinkServiceConnections": [ 769 | { 770 | "name": "MyCosmosDbPrivateLinkConnection", 771 | "properties": { 772 | "privateLinkServiceId": "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('privateCosmosDbAccountName'))]", 773 | "groupIds": [ 774 | "Sql" 775 | ] 776 | } 777 | } 778 | ] 779 | } 780 | }, 781 | { 782 | "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", 783 | "apiVersion": "2020-03-01", 784 | "location": "[parameters('location')]", 785 | "name": "[concat(variables('privateEndpointCosmosDbName'), '/default')]", 786 | "dependsOn": [ 787 | "[resourceId('Microsoft.Network/privateDnsZones', variables('privateCosmosDbDnsZoneName'))]", 788 | "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointCosmosDbName'))]" 789 | ], 790 | "properties": { 791 | "privateDnsZoneConfigs": [ 792 | { 793 | "name": "config1", 794 | "properties": { 795 | "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', variables('privateCosmosDbDnsZoneName'))]" 796 | } 797 | } 798 | ] 799 | } 800 | }, 801 | { 802 | "type": "Microsoft.Network/bastionHosts", 803 | "name": "[variables('bastionHostName')]", 804 | "apiVersion": "2019-11-01", 805 | "location": "[parameters('location')]", 806 | "dependsOn": [ 807 | "[resourceId('Microsoft.Network/publicIpAddresses', variables('bastionPublicIPAddressName'))]", 808 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 809 | ], 810 | "properties": { 811 | "ipConfigurations": [ 812 | { 813 | "name": "IpConf", 814 | "properties": { 815 | "subnet": { 816 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('BastionSubnetName'))]" 817 | }, 818 | "publicIPAddress": { 819 | "id": "[resourceId('Microsoft.Network/publicIpAddresses', variables('bastionPublicIPAddressName'))]" 820 | } 821 | } 822 | } 823 | ] 824 | } 825 | }, 826 | { 827 | "type": "Microsoft.Network/networkInterfaces", 828 | "apiVersion": "2018-11-01", 829 | "name": "[variables('vmNicName')]", 830 | "location": "[parameters('location')]", 831 | "dependsOn": [ 832 | "[resourceId('Microsoft.Network/virtualNetworks/', variables('vnetName'))]" 833 | ], 834 | "properties": { 835 | "ipConfigurations": [ 836 | { 837 | "name": "ipconfig1", 838 | "properties": { 839 | "privateIPAllocationMethod": "Dynamic", 840 | "subnet": { 841 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('vmSubnetName'))]" 842 | } 843 | } 844 | } 845 | ] 846 | } 847 | }, 848 | { 849 | "type": "Microsoft.Network/publicIPAddresses", 850 | "apiVersion": "2018-11-01", 851 | "name": "[variables('bastionPublicIPAddressName')]", 852 | "location": "[parameters('location')]", 853 | "sku": { 854 | "name": "Standard" 855 | }, 856 | "properties": { 857 | "publicIPAllocationMethod": "Static", 858 | "dnsSettings": { 859 | "domainNameLabel": "[variables('dnsLabelPrefix')]" 860 | } 861 | } 862 | }, 863 | { 864 | "apiVersion": "2019-04-01", 865 | "type": "Microsoft.Network/networkSecurityGroups", 866 | "name": "[variables('vmNsgName')]", 867 | "location": "[parameters('location')]", 868 | "properties": { 869 | "securityRules": [ 870 | { 871 | "name": "Block_RDP_Internet", 872 | "properties": { 873 | "description": "Block RDP", 874 | "protocol": "Tcp", 875 | "sourcePortRange": "*", 876 | "destinationPortRange": "3389", 877 | "sourceAddressPrefix": "Internet", 878 | "destinationAddressPrefix": "*", 879 | "access": "Deny", 880 | "priority": 101, 881 | "direction": "Inbound" 882 | } 883 | } 884 | ] 885 | } 886 | }, 887 | { 888 | "type": "Microsoft.Web/serverfarms", 889 | "apiVersion": "2018-02-01", 890 | "name": "[variables('appServicePlanName')]", 891 | "location": "[parameters('location')]", 892 | "sku": { 893 | "name": "EP1", 894 | "tier": "ElasticPremium", 895 | "size": "EP1", 896 | "family": "EP", 897 | "capacity": 1 898 | }, 899 | "kind": "elastic", 900 | "properties": { 901 | "maximumElasticWorkerCount": 20 902 | } 903 | }, 904 | { 905 | "type": "Microsoft.Web/sites", 906 | "apiVersion": "2018-11-01", 907 | "name": "[variables('functionAppName')]", 908 | "location": "[parameters('location')]", 909 | "dependsOn": [ 910 | "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", 911 | "[resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName'))]" 912 | ], 913 | "kind": "functionapp", 914 | "properties": { 915 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", 916 | "siteConfig": { 917 | "vnetName": "[concat('bead47e1-d65e-4cb4-b907-f74674d32c09_', variables('functionsSubnetName'))]", 918 | "appSettings": [ 919 | { 920 | "name": "APPINSIGHTS_INSTRUMENTATIONKEY", 921 | "value": "[reference(variables('appInsightsResourceId'), '2018-05-01-preview').instrumentationKey]" 922 | }, 923 | { 924 | "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", 925 | "value": "[concat('InstrumentationKey=', reference(variables('appInsightsResourceId'), '2018-05-01-preview').instrumentationKey)]" 926 | }, 927 | { 928 | "name": "AzureWebJobsStorage", 929 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('functionWebJobsStorageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName')), '2018-11-01').keys[0].value,';')]" 930 | }, 931 | { 932 | "name": "FUNCTIONS_EXTENSION_VERSION", 933 | "value": "~3" 934 | }, 935 | { 936 | "name": "FUNCTIONS_WORKER_RUNTIME", 937 | "value": "dotnet" 938 | }, 939 | { 940 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 941 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('functionWebJobsStorageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('functionWebJobsStorageAccountName')), '2018-11-01').keys[0].value,';')]" 942 | }, 943 | { 944 | "name": "WEBSITE_CONTENTSHARE", 945 | "value": "myfunctionfiles" 946 | }, 947 | { 948 | "name": "WEBSITE_DNS_SERVER", 949 | "value": "168.63.129.16" 950 | }, 951 | { 952 | "name": "WEBSITE_CONTENTOVERVNET", 953 | "value": "1" 954 | }, 955 | { 956 | "name": "WEBSITE_VNET_ROUTE_ALL", 957 | "value": "1" 958 | }, 959 | { 960 | "name": "CensusResultsAzureStorageConnection", 961 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('censusDataStorageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('censusDataStorageAccountName')), '2018-11-01').keys[0].value,';')]" 962 | }, 963 | { 964 | "name": "ContainerName", 965 | "value": "[parameters('blobContainerName')]" 966 | }, 967 | { 968 | "name": "CosmosDbName", 969 | "value": "[parameters('cosmosDbDatabaseName')]" 970 | }, 971 | { 972 | "name": "CosmosDbCollectionName", 973 | "value": "[parameters('cosmosDbContainerName')]" 974 | }, 975 | { 976 | "name": "CosmosDBConnection", 977 | "value": "[listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('privateCosmosDbAccountName')), '2019-12-12').connectionStrings[0].connectionString]" 978 | }, 979 | { 980 | "name": "Project", 981 | "value": "src" 982 | } 983 | ] 984 | } 985 | }, 986 | "resources": [ 987 | { 988 | "type": "networkConfig", 989 | "apiVersion": "2019-08-01", 990 | "name": "virtualNetwork", 991 | "dependsOn": [ 992 | "[concat('Microsoft.Web/sites/', variables('functionAppName'))]", 993 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 994 | ], 995 | "properties": { 996 | "subnetResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('functionsSubnetName'))]", 997 | "isSwift": true 998 | } 999 | }, 1000 | { 1001 | "condition": "[parameters('deploySourceCode')]", 1002 | "type": "sourcecontrols", 1003 | "apiVersion": "2019-08-01", 1004 | "name": "web", 1005 | "dependsOn": [ 1006 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" 1007 | ], 1008 | "properties": { 1009 | "RepoUrl": "[parameters('function_repo_url')]", 1010 | "branch": "master", 1011 | "IsManualIntegration": true 1012 | } 1013 | }, 1014 | { 1015 | "type": "config", 1016 | "name": "web", 1017 | "apiVersion": "2019-08-01", 1018 | "dependsOn": [ 1019 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]", 1020 | "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" 1021 | ], 1022 | "properties": { 1023 | "functionsRuntimeScaleMonitoringEnabled": true 1024 | } 1025 | } 1026 | ] 1027 | } 1028 | ], 1029 | "outputs": { 1030 | } 1031 | } 1032 | -------------------------------------------------------------------------------- /template/azuredeploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "virtualMachineAdminUsername": { 6 | "value": "[USERNAME GOES HERE]" 7 | }, 8 | "virtualMachineAdminPassword": { 9 | "value": "[PASSWORD GOES HERE]" 10 | }, 11 | "cosmosDbDatabaseName": { 12 | "value": "Census" 13 | }, 14 | "cosmosDbContainerName": { 15 | "value": "census2020" 16 | }, 17 | "blobContainerName": { 18 | "value": "census-results" 19 | }, 20 | "function_repo_url": { 21 | "value": "https://github.com/mcollier/azure-functions-private-storage.git" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /template/deploy.azcli: -------------------------------------------------------------------------------- 1 | az login 2 | 3 | az account set -s [YOUR-AZURE-SUBSCRIPTION-ID] 4 | 5 | az group create --name [YOUR-RESOURCE-GROUP-NAME] --location eastus 6 | 7 | az deployment group create -g [YOUR-RESOURCE-GROUP-NAME] --template-file azuredeploy.json --parameters azuredeploy.parameters.json 8 | 9 | 10 | 11 | #### 12 | cd ..\src 13 | dotnet build 14 | func azure functionapp publish [YOURE-FUNCTION-APP-NAME] 15 | -------------------------------------------------------------------------------- /template/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #az login 4 | az account set -s [YOUR-SUBSCRIPTION-ID] 5 | 6 | 7 | resourceGroupName="functions-private-endpoints" 8 | location="eastus" 9 | now=`date +%Y%m%d-%H%M%S` 10 | deploymentName="azuredeploy-$now" 11 | 12 | echo "Creating resource group '$resourceGroupName' in region '$location' . . ." 13 | az group create --name $resourceGroupName --location $location 14 | 15 | # echo "Validating the template . . ." 16 | # az deployment group validate -g $resourceGroupName --template-file azuredeploy.json --parameters azuredeploy.parameters.json --debug 17 | 18 | echo "Deploying main template . . ." 19 | az deployment group create -g $resourceGroupName --template-file azuredeploy.json --parameters azuredeploy.parameters.json --name $deploymentName 20 | 21 | -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | put terraform scripts here --------------------------------------------------------------------------------