diff --git a/.github/linters/.powershell-psscriptanalyzer.psd1 b/.github/linters/.powershell-psscriptanalyzer.psd1 index 7edeaab6..8535acbd 100644 --- a/.github/linters/.powershell-psscriptanalyzer.psd1 +++ b/.github/linters/.powershell-psscriptanalyzer.psd1 @@ -14,6 +14,7 @@ 'PSAvoidUsingPlainTextForPassword' 'PSAvoidUsingConvertToSecureStringWithPlainText' 'PSPossibleIncorrectUsageOfAssignmentOperator' + 'PSUseSingularNouns' ) #IncludeRules = @( ) } diff --git a/healthcare/solutions/dataverseIntegration/Helper.ps1 b/healthcare/solutions/dataverseIntegration/Helper.ps1 new file mode 100644 index 00000000..a0f7011d --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/Helper.ps1 @@ -0,0 +1,471 @@ +function Update-OrganizationDetails { + <# + .SYNOPSIS + Updates Organization details for the Power Platform environment. + .DESCRIPTION + Update-OrganizationDetails creates a new Data Lake configuration in the Power Platform environment. + .PARAMETER PowerPlatformEnvironmentId + Function expects the power platform environment id in which the Data Lake configuration + will be created. + .PARAMETER OrganizationUrl + Function expects the organization url (e.g. 'https://org111aa111.crm.dynamics.com/'). + .PARAMETER OrganizationId + Function expects the organization Id (e.g. 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'). + .EXAMPLE + Update-OrganizationDetails -PowerPlatformEnvironmentId "" -OrganizationUrl "" -OrganizationId "" + .NOTES + Author: Marvin Buss + GitHub: @marvinbuss + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PowerPlatformEnvironmentId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $OrganizationUrl, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $OrganizationId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $AccessToken + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/updateorganizationdetails?organizationUrl=${OrganizationUrl}&organizationId=${OrganizationId}" + Write-Verbose "Uri: '${powerPlatformUri}'" + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${AccessToken}" + } + + # Define parameters for REST method + Write-Verbose "Defining parameters for pscore method" + $parameters = @{ + "Uri" = $powerPlatformUri + "Method" = "Post" + "Headers" = $headers + "ContentType" = "application/json" + } + + # Invoke REST API + Write-Verbose "Invoking REST API" + try { + $response = Invoke-RestMethod @parameters + Write-Verbose "Response: ${response}" + } + catch { + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } + throw "REST API call failed" + } + return $response +} + + +function New-FileSystem { + <# + .SYNOPSIS + Creates a FileSystem for the Power Platform environment. + .DESCRIPTION + New-FileSystem creates a Data Lake file system for the Power Platform environment. + .PARAMETER PowerPlatformEnvironmentId + Function expects the power platform environment id in which the Data Lake configuration + will be created. + .PARAMETER DataLakeFileSystemId + Function expects the data lake file system resource id which should be + connected to the power platform. + .EXAMPLE + New-FileSystem -PowerPlatformEnvironmentId "" -DataLakeFileSystemId "" + .NOTES + Author: Marvin Buss + GitHub: @marvinbuss + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PowerPlatformEnvironmentId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DataLakeFileSystemId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $AccessToken + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/createfilesystem" + Write-Verbose "Uri: '${powerPlatformUri}'" + + # Define parameters based on input parameters + Write-Verbose "Defining parameters based on input parameters" + $tenantId = (Get-AzTenant).Id + $dataLakeSubscriptionId = $DataLakeFileSystemId.Split("/")[2] + $dataLakeResourceGroupName = $DataLakeFileSystemId.Split("/")[4] + $dataLakeName = $DataLakeFileSystemId.Split("/")[8] + $dataLakeFileSystemName = $DataLakeFileSystemId.Split("/")[-1] + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${AccessToken}" + } + + # Set body for REST call + Write-Verbose "Setting body for REST call" + $body = @{ + "TenantId" = $tenantId + "SubscriptionId" = $dataLakeSubscriptionId + "ResourceGroupName" = $dataLakeResourceGroupName + "StorageAccountName" = $dataLakeName + "FileSystemEndpoint" = "https://${dataLakeName}.dfs.core.windows.net/" + "FileSystemName" = $dataLakeFileSystemName + } | ConvertTo-Json + Write-Verbose "Body: '${body}'" + + # Define parameters for REST method + Write-Verbose "Defining parameters for pscore method" + $parameters = @{ + "Uri" = $powerPlatformUri + "Method" = "Post" + "Headers" = $headers + "Body" = $body + "ContentType" = "application/json" + } + + # Invoke REST API + Write-Verbose "Invoking REST API" + try { + $response = Invoke-RestMethod @parameters + Write-Verbose "Response: ${response}" + } + catch { + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } + throw "REST API call failed" + } + return $response +} + + +function New-LakeDetails { + <# + .SYNOPSIS + Creates New Lake Configuration in the Power Platform environment. + .DESCRIPTION + New-LakeDetails creates a new Data Lake configuration in the Power Platform environment. + .PARAMETER PowerPlatformEnvironmentId + Function expects the power platform environment id in which the Data Lake configuration + will be created. + .PARAMETER DataLakeFileSystemId + Function expects the data lake file system resource id which should be + connected to the power platform. + .EXAMPLE + New-LakeDetails -PowerPlatformEnvironmentId "" -DataLakeFileSystemId "" + .NOTES + Author: Marvin Buss + GitHub: @marvinbuss + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PowerPlatformEnvironmentId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DataLakeFileSystemId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $SynapseId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $AccessToken + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/lakedetails" + Write-Verbose "Uri: '${powerPlatformUri}'" + + # Define parameters based on input parameters + Write-Verbose "Defining parameters based on input parameters" + $tenantId = (Get-AzTenant).Id + $dataLakeSubscriptionId = $DataLakeFileSystemId.Split("/")[2] + $dataLakeResourceGroupName = $DataLakeFileSystemId.Split("/")[4] + $dataLakeName = $DataLakeFileSystemId.Split("/")[8] + $dataLakeFileSystemName = $DataLakeFileSystemId.Split("/")[-1] + + $synapseSubscriptionId = $SynapseId.Split("/")[2] + $synapseResourceGroupName = $SynapseId.Split("/")[4] + $synapseName = $SynapseId.Split("/")[-1] + + if (($synapseSubscriptionId -ne $dataLakeSubscriptionId) -or ($synapseResourceGroupName -ne $dataLakeResourceGroupName)) { + Write-Error "Synapse workspace and Data Lake are not in the same Subscription and/or Resource Group" + throw "Synapse workspace and Data Lake are not in the same Subscription and/or Resource Group" + } + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${AccessToken}" + } + + # Set body for REST call + Write-Verbose "Setting body for REST call" + $body = @{ + "TenantId" = $tenantId + "SubscriptionId" = $dataLakeSubscriptionId + "ResourceGroupName" = $dataLakeResourceGroupName + "StorageAccountName" = $dataLakeName + "BlobEndpoint" = "https://${dataLakeName}.blob.core.windows.net/" + "FileEndpoint" = "https://${dataLakeName}.file.core.windows.net/" + "QueueEndpoint" = "https://${dataLakeName}.queue.core.windows.net/" + "TableEndpoint" = "https://${dataLakeName}.table.core.windows.net/" + "FileSystemEndpoint" = "https://${dataLakeName}.dfs.core.windows.net/" + "FileSystemName" = $dataLakeFileSystemName + "SqlODEndpoint" = "${synapseName}-ondemand.sql.azuresynapse.net" + "WorkspaceDevEndpoint" = "https://${synapseName}.dev.azuresynapse.net" + "IsDefault" = $true + } | ConvertTo-Json + Write-Verbose "Body: '${body}'" + + # Define parameters for REST method + Write-Verbose "Defining parameters for pscore method" + $parameters = @{ + "Uri" = $powerPlatformUri + "Method" = "Post" + "Headers" = $headers + "Body" = $body + "ContentType" = "application/json" + } + + # Invoke REST API + Write-Verbose "Invoking REST API" + try { + $response = Invoke-RestMethod @parameters + Write-Verbose "Response: ${response}" + } + catch { + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } + throw "REST API call failed" + } + return $response +} + + +function New-LakeProfile { + <# + .SYNOPSIS + Creates New Lake Profile based on the Data Lake Configuration in the Power Platform environment. + .DESCRIPTION + New-LakeProfile creates a new Data Lake profile in the Power Platform environment. + .PARAMETER PowerPlatformEnvironmentId + Function expects the power platform environment id in which the Data Lake configuration + will be created. + .PARAMETER DataLakeFileSystemId + Function expects the data lake file system resource id which should be + connected to the power platform. + .PARAMETER LakeDetailsId + Function expects the ID of the Lake Details/Lake configuration object. + .PARAMETER Entities + Function expects a definition of the entities that should be synched to the Data Lake. + .EXAMPLE + New-LakeProfile -PowerPlatformEnvironmentId "" -DataLakeFileSystemId "" -LakeDetailsId "" -Entities @[@{"Type": "msdyn_actual", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false, "Settings": @{}}] + .NOTES + Author: Marvin Buss + GitHub: @marvinbuss + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PowerPlatformEnvironmentId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DataLakeFileSystemId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $LakeDetailsId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Array] + $Entities, # Sample Input: [{"Type": "msdyn_actual", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false}, {"Type": "adx_ad", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false}] + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $AccessToken + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/lakeprofile/${LakeDetailsId}" + Write-Verbose "Uri: '${powerPlatformUri}'" + + # Define parameters based on input parameters + Write-Verbose "Defining parameters based on input parameters" + $dataLakeName = $DataLakeFileSystemId.Split("/")[8] + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${AccessToken}" + } + + # Set body for REST call + Write-Verbose "Setting body for REST call" + $body = @{ + "DestinationType" = 4 + "Entities" = $Entities + "IsOdiEnabled" = $false + "Name" = $dataLakeName + "RetryPolicy" = @{ + "IntervalInSeconds" = 5 + "MaxRetryCount" = 12 + } + "SchedulerIntervalInMinutes" = 60 + "WriteDeleteLog" = $true + } | ConvertTo-Json + Write-Verbose "Body: '${body}'" + + # Define parameters for REST method + Write-Verbose "Defining parameters for pscore method" + $parameters = @{ + "Uri" = $powerPlatformUri + "Method" = "Post" + "Headers" = $headers + "Body" = $body + "ContentType" = "application/json" + } + + # Invoke REST API + Write-Verbose "Invoking REST API" + try { + $response = Invoke-RestMethod @parameters + Write-Verbose "Response: ${response}" + } + catch { + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } + throw "REST API call failed" + } + return $response +} + + +function New-LakeProfileActivation { + <# + .SYNOPSIS + Activates previously created Lake Profile in the Power Platform environment. + .DESCRIPTION + New-LakeProfileActivation activates a Data Lake profile in the Power Platform environment. + .PARAMETER PowerPlatformEnvironmentId + Function expects the power platform environment id in which the Data Lake configuration + will be created. + .PARAMETER LakeDetailsId + Function expects the ID of the Lake Details/Lake configuration object. + .EXAMPLE + New-LakeProfileActivation -PowerPlatformEnvironmentId "" -LakeDetailsId "" + .NOTES + Author: Marvin Buss + GitHub: @marvinbuss + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PowerPlatformEnvironmentId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $LakeDetailsId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $AccessToken + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/lakeprofile/${LakeDetailsId}/activate" + Write-Verbose "Uri: '${powerPlatformUri}'" + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${AccessToken}" + } + + # Define parameters for REST method + Write-Verbose "Defining parameters for pscore method" + $parameters = @{ + "Uri" = $powerPlatformUri + "Method" = "Post" + "Headers" = $headers + "ContentType" = "application/json" + } + + # Invoke REST API + Write-Verbose "Invoking REST API" + try { + $response = Invoke-RestMethod @parameters + Write-Verbose "Response: ${response}" + } + catch { + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } + throw "REST API call failed" + } + return $response +} diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md new file mode 100644 index 00000000..851ceaea --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -0,0 +1,129 @@ +# Linking Dataverse with Azure Data Lake Gen2 and Azure Synapse + +In a data platform, disparate data sources and datasets are integrated onto a Data Lake in order to allow the development of new data products as well as the generation of new insights by using machine learning and other techniques. One of such data sources can be Dataverse, which is the standard storage option for all business applications running inside a Power Platform enviornment. Dataverse stores datasets in a tabular format and allows them to be extracted to a Data Lake Gen2 via a feature called "Azure Synapse Link for Dataverse". + +Simply put, this feature sets up a connection between Dataverse and a Storage Account in Azure with Hierarchical Namespaces (HNS) enabled. In addition, it allows to automatically generate a the metadata for the extracted tables in a Synapse workspace. If this option is enabled, a Spark metastore with table definitions for the extracted datasets is generated and can be used for loading the datasets and further data processing. The "Azure Synapse Link" feature in Power Apps also allows users to configure how specific tables are extracted. Options include the specification of the partitioning mode and in-place vs. append-only writes. More details about these options can be found [here](https://docs.microsoft.com/en-us/powerapps/maker/data-platform/azure-synapse-link-advanced-configuration). + +## Service Requirements + +When setting this feature up, the Storage Account as well as the Synapse workspace must be configured correctly. Otherwise, the setup of the feature or the actual dataset extraction will fail. Therefore, the below sections will describe how the two services must be configured in order for the automatic data extraction to work. + +### Storage Account + +The storage Account must have hierarchical namespaces (HNS) enabled. This is a strict requirement, since the Power Platform uses the dfs endpoint of the storage account for data extraction. In addition, the firewall of the storage account needs to be opened so that the power platform cann access the storage account and update datasets within the data lake file systems. Today, it is not possible to rely on private endpoints or service endpoints for the export feature to work. Hence, the `defaultAction` in the `networkAcls` property bag needs to be set to `Allow`. Enabling `AzureServices` to bypass the firewall was not sufficient when testing the setup of the feature. + +The storage account requires two containers/file systems. One is used for the actual export of data and the second one is used for power platform dataflows. In addition to that, multiple role assignments are required as outlined below: + +| Service Principle | Role Name | Scope | +|:---------------------------------------------------|:------------------------------|---------------------------| +| 'Microsoft Power Query' (Power Platform Dataflows) | Reader and Data Access | Storage Account | +| 'Microsoft Power Query' (Power Platform Dataflows) | Storage Blob Data Owner | Storage Account Container | +| 'Export to data lake' (Dataverse) | Owner | Storage Account | +| 'Export to data lake' (Dataverse) | Storage Blob Data Owner | Storage Account | +| 'Export to data lake' (Dataverse) | Storage Blob Data Contributor | Storage Account | +| 'Export to data lake' (Dataverse) | Storage Account Contributor | Storage Account | +| 'Export to data lake' (Dataverse) | Storage Account Contributor | Storage Account Container | +| 'Export to data lake' (Dataverse) | Owner | Storage Account Container | +| 'Export to data lake' (Dataverse) | Storage Blob Data Owner | Storage Account Container | +| 'Export to data lake' (Dataverse) | Storage Blob Data Contributor | Storage Account Container | + +### Synapse workspace + +Our tests have shown that similar requirements are existing for the Synapse workspace. Disabling traffic on the public endpoint of Synase is not possible and private endpoints can also not be used today. The Synapse workspace firewall needs to be opened up to allow traffic from the Power Platform environment. The following role assignment is required to enable the creation of the metadata tables in Synapse: + +| Service Principle | Role Name | Scope | +|:---------------------------------------------------|:------------------------------|---------------------------| +| 'Export to data lake' (Dataverse) | Synapse Administrator | Synapse Workspace | + +### Other + +The Storage Account, the Synapse Workspace and the Power Platform Environment must be in the same region. Otherwise, the "Azure Synapse Link" feature in Power Apps will not work. Also, all services need to be in the same tenant, subscription and resource group. + +The user creating the connection requires Owner or User Access Administrator rights on the two Azure resources and Synapse Administrator rights in the Synapse workspace in order to be able to assign RBAC roles to the Service Principles of the two Enterprise Applications. In addition, the user needs to have the Dataverse system administrator role in the environment to connect Azure and Dataverse successfully. + +## Reference Implementation + +To accelerate the integration of datasets between Dataverse and a data platform, a reference implementation has been developed to set this up much more quickly. The code consists of Infrastructure as Code (IaC) templates and a "Deploy To Azure" Button to setup everything related to Azure including the following: + +- Azure Services: Storage Account, Synapse workspace (including Spark pool), Key Vault +- All Role assigments ([see role assignments above](#service-requirements)) + +Also, the reference implementation includes a set of powershell scripts to automate the first setup of "Azure Synapse Link". Afterwards, modifications can be made with respect to the tables that get synchronized as well as the settings for each table. + +### Azure Deployment + +First, use the "Deploy To Azure" Button to setup all Azure related services. Go through the portal experience and specify the details of your environment to successfully deploy the setup: + +[![Deploy To Microsoft Cloud](/docs/deploytomicrosoftcloud.svg)](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Findustry%2Fmain%2Fhealthcare%2Fsolutions%2FdataverseIntegration%2Fmain.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Findustry%2Fmain%2Fhealthcare%2Fsolutions%2FdataverseIntegration%2Fportal.json) + +Please look at the outputs of the Azure deployment and take note of the Synapse Workspace ID as well as the Dataverse data lake file system ID, as they are required in the next step. + +### Connecting Dataverse and Azure Services + +For the next step, you will need the following PowerShell scripts included in this folder: "SetupSynapseLink.ps1" and "Helper.ps1". This step cannot be automated, because the Power Platform APIs do not support the on-behalf workflow. Therefore, this script needs to be executed by a user. Before executing "SetupSynapseLink.ps1", please follow the steps below: + +1. [Install the Azure Az PowerShell module](https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-6.4.0) including the Az.Synapse module. If the Az.Synapse module did not get installed, please execute the following command in your PowerShell environment: + +```powershell +Install-Module -Name Az.Synapse -Scope CurrentUser -Repository PSGallery -Force +``` + +2. Connect to your Azure environment using the following command: + +```powershell +Connect-AzAccount +``` + +3. Collect all inputs required for the "SetupSynapseLink.ps1": + +| Input | Description | Sample | +|:------------------------------|:------------------------------|---------------------------| +| PowerPlatformEnvironmentId | Specifies the ID of the Power Platform environment. | `0000aa0a-aaa0-0a00-aa00000a0000` | +| OrganizationUrl | Specifies the Organization URL. | `https://org111aa111.crm.dynamics.com/` | +| OrganizationId | Specifies the Organization ID. | `aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa` | +| SynapseId | Specifies the Synapse Workspace resource ID. This is specified as output in the Azure deployment. | `/subscriptions/{subscription-id}/resourceGroups/{rg-name}/providers/Microsoft.Synapse/workspaces/{synapse-workspace-name}` | +| DataverseDataLakeFileSystemId | Specifies the resource ID of the Storage Account Container. This is specified as output in the Azure deployment. | `/subscriptions/{subscription-id}/resourceGroups/{rg-name}/providers/Microsoft.Storage/storageAccounts/{storage-name}/blobServices/default/containers/{container-name}` | +| Entities | Specifies the tables that will be synched to the data lake. | `[{"Type": "msdyn_actual", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false}, {"Type": "adx_ad", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false}]` | + +4. Run "SetupSynapseLink.ps1" in PowerShell by running the following command (please update the parameters): + +```powershell +./SetupSynapseLink.ps1 ` + -PowerPlatformEnvironmentId "" ` + -OrganizationUrl "" ` + -OrganizationId "" ` + -SynapseId "" + -DataverseDataLakeFileSystemId "" ` + -Entities "" +``` + +The following can be used as an example for the "Entities" parameter: + +```powershell +$entities = @( + @{ + "Type" = "account" + "RecordCountPerBlock" = 0 + "PartitionStrategy" = "Month" + "AppendOnlyMode" = $false + } +) +``` + +### Review Deployment + +After running the PowerShell script, you should be able to see the "Azure Synapse Link" in Power Platform: + +![Azure Synapse Link Connection](./docs/media/AzureSynapseLinkConnection.png) + +In Azure, you will find the following resources inside teh specified resource group: + +![Azure Resources](./docs/media/AzureResources.png) + +In the Synapse workspace, you should see a database with the tables that you selected for the synch to the data lake: + +![Azure Synapse](./docs/media/AzureSynapse.png) + +Lastly, in the Storage Account users will find the synched tables inside the "dataverse" container: + +![Azure Storage](./docs/media/AzureStorage.png) diff --git a/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 new file mode 100644 index 00000000..43becf6c --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 @@ -0,0 +1,116 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +# Define script arguments +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [String] + $PowerPlatformEnvironmentId, + + [Parameter(Mandatory = $true)] + [String] + $OrganizationUrl, + + [Parameter(Mandatory = $true)] + [String] + $OrganizationId, + + [Parameter(Mandatory = $true)] + [String] + $SynapseId, + + [Parameter(Mandatory = $true)] + [String] + $DataverseDataLakeFileSystemId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Array] + $Entities, + + [Parameter(DontShow)] + [String] + $ExportDataLakeApplicationId = "7f15f9d9-cad0-44f1-bbba-d36650e07765", + + [Parameter(DontShow)] + [String] + $PowerPlatformApplicationId = "f3b07414-6bf4-46e6-b63f-56941f3f4128" +) + +# Import Helper Functions +Write-Output "Importing Helper Functions" +. "$PSScriptRoot\Helper.ps1" + +# Check existence of Enterprise Applications: 'Export to data lake' and 'Microsoft Power Query' +Write-Output "Checking existence of Enterprise Applications: 'Export to data lake' and 'Microsoft Power Query'" +$exportDataLakeServicePrincipal = Get-AzADServicePrincipal ` + -ApplicationId $ExportDataLakeApplicationId +Write-Output "'Expor Data Lake' Service Principle Details: '${exportDataLakeServicePrincipal}'" + +$powerPlatformServicePrincipal = Get-AzADServicePrincipal ` + -ApplicationId $PowerPlatformApplicationId +Write-Output "'Microsoft Power Query' Service Principle Details: '${powerPlatformServicePrincipal}'" + +# Add Synapse Role Assignment +Write-Output "Adding Synapse Role Assignment" +$synapseSubscriptionId = $SynapseId.Split("/")[2] +$synapseName = $SynapseId.Split("/")[-1] + +Set-AzContext ` + -Subscription $synapseSubscriptionId + +# More role details: "{"id": "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78", "isBuiltIn": true, "name": "Synapse Administrator"}," +New-AzSynapseRoleAssignment ` + -WorkspaceName $synapseName ` + -RoleDefinitionName "Synapse Administrator" ` + -ObjectId $exportDataLakeServicePrincipal.Id + +# Get Power App Access Token +Write-Output "Getting Power App Access Token" +$powerAppAccessToken = (Get-AzAccessToken -ResourceUrl "${ExportDataLakeApplicationId}").Token + +# Update Organization Details +Write-Output "Updating Organization Details" +Update-OrganizationDetails ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -OrganizationUrl $OrganizationUrl ` + -OrganizationId $OrganizationId ` + -AccessToken $powerAppAccessToken + +# Create New Data Lake Details +Write-Output "Creating New Data Lake Details" +$datalakeDetails = New-LakeDetails ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -DataLakeFileSystemId $DataverseDataLakeFileSystemId ` + -SynapseId $SynapseId ` + -AccessToken $powerAppAccessToken +Write-Output "New Data Lake Details: '${datalakeDetails}'" + +# Sleep for X Seconds to give the Backend Process some time to Finish +$seconds = 10 +Write-Output "Sleeping for ${seconds} Seconds to give the Backend Process some time to Finish" +Start-Sleep -Seconds $seconds + +# Create New Data Lake Profile +Write-Output "Creating New Data Lake Profile" +$datalakeProfile = New-LakeProfile ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -DataLakeFileSystemId $DataverseDataLakeFileSystemId ` + -LakeDetailsId $datalakeDetails.Id ` + -Entities $Entities ` + -AccessToken $powerAppAccessToken +Write-Output "New Data Lake Profile: '${datalakeProfile}'" + +# Sleep for X Seconds to give the Backend Process some time to Finish +$seconds = 10 +Write-Output "Sleeping for ${seconds} Seconds to give the Backend Process some time to Finish" +Start-Sleep -Seconds $seconds + +# Activate Lake Profile +Write-Output "Activating Lake Profile" +$datalakeProfileActivation = New-LakeProfileActivation ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -LakeDetailsId $datalakeDetails.Id ` + -AccessToken $powerAppAccessToken +Write-Output "New Data Lake Profile Activation: '${datalakeProfileActivation}'" diff --git a/healthcare/solutions/dataverseIntegration/docs/media/AzureResources.png b/healthcare/solutions/dataverseIntegration/docs/media/AzureResources.png new file mode 100644 index 00000000..9339595a Binary files /dev/null and b/healthcare/solutions/dataverseIntegration/docs/media/AzureResources.png differ diff --git a/healthcare/solutions/dataverseIntegration/docs/media/AzureStorage.png b/healthcare/solutions/dataverseIntegration/docs/media/AzureStorage.png new file mode 100644 index 00000000..8e0aef51 Binary files /dev/null and b/healthcare/solutions/dataverseIntegration/docs/media/AzureStorage.png differ diff --git a/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapse.png b/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapse.png new file mode 100644 index 00000000..73b9b2bc Binary files /dev/null and b/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapse.png differ diff --git a/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapseLinkConnection.png b/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapseLinkConnection.png new file mode 100644 index 00000000..fc0149a6 Binary files /dev/null and b/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapseLinkConnection.png differ diff --git a/healthcare/solutions/dataverseIntegration/main.bicep b/healthcare/solutions/dataverseIntegration/main.bicep new file mode 100644 index 00000000..ccfd70b4 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/main.bicep @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +targetScope = 'resourceGroup' + +// General parameters +@description('Specifies the location for all resources.') +param location string +@allowed([ + 'dev' + 'tst' + 'prd' +]) +@description('Specifies the environment of the deployment.') +param environment string = 'dev' +@minLength(2) +@maxLength(10) +@description('Specifies the prefix for all resources created in this deployment.') +param prefix string +@description('Specifies the tags that you want to apply to all resources.') +param tags object = {} + +// Resource parameters +@secure() +@description('Specifies the administrator password of the sql servers in Synapse.') +param administratorPassword string = '' +@description('Specifies the object ID of the Enterprise Application "Microsoft Power Query".') +param powerPlatformServicePrincipalObjectId string = '' +@description('Specifies the object ID of the Enterprise Application "Export to data lake".') +param dataverseServicePrincipalObjectId string = '' +@description('Specifies the resource ID of the central purview instance to connect Purviw with Data Factory or Synapse. If you do not want to setup a connection to Purview, leave this value empty as is.') +param purviewId string = '' +@description('Specifies whether role assignments should be enabled for Synapse (Blob Storage Contributor to default storage account).') +param enableRoleAssignments bool = false + +// Network parameters +@description('Specifies the resource ID of the subnet to which all services will connect.') +param subnetId string + +// Private DNS Zone parameters +@description('Specifies the resource ID of the private DNS zone for Blob Storage.') +param privateDnsZoneIdBlob string = '' +@description('Specifies the resource ID of the private DNS zone for Datalake Storage.') +param privateDnsZoneIdDfs string = '' +@description('Specifies the resource ID of the private DNS zone for KeyVault.') +param privateDnsZoneIdKeyVault string = '' +@description('Specifies the resource ID of the private DNS zone for Synapse Dev.') +param privateDnsZoneIdSynapseDev string = '' +@description('Specifies the resource ID of the private DNS zone for Synapse Sql.') +param privateDnsZoneIdSynapseSql string = '' + +// Variables +var name = toLower('${prefix}-${environment}') +var tagsDefault = { + Project: 'Dataverse - Data Integration' + Environment: environment + Toolkit: 'bicep' + Name: name +} +var tagsJoined = union(tagsDefault, tags) +var administratorUsername = 'SqlServerMainUser' +var keyvault001Name = '${name}-vault001' +var storage001Name = '${name}-storage001' +var synapse001Name = '${name}-synapse001' + +// Resources +module keyVault001 'modules/services/keyvault.bicep' = { + name: 'keyVault001' + scope: resourceGroup() + params: { + location: location + tags: tagsJoined + subnetId: subnetId + keyvaultName: keyvault001Name + privateDnsZoneIdKeyVault: privateDnsZoneIdKeyVault + } +} + +module storage001 'modules/services/storage.bicep' = { + name: 'storage001' + scope: resourceGroup() + params: { + location: location + tags: tagsJoined + fileSystemNames: [ + 'synapse' + 'power-platform-dataflows' + 'dataverse' + ] + storageName: storage001Name + subnetId: subnetId + privateDnsZoneIdBlob: privateDnsZoneIdBlob + privateDnsZoneIdDfs: privateDnsZoneIdDfs + } +} + +module synapse001 'modules/services/synapse.bicep' = { + name: 'synapse001' + scope: resourceGroup() + params: { + location: location + tags: tagsJoined + subnetId: subnetId + synapseName: synapse001Name + administratorUsername: administratorUsername + administratorPassword: administratorPassword + synapseSqlAdminGroupName: '' + synapseSqlAdminGroupObjectID: '' + privateDnsZoneIdSynapseDev: privateDnsZoneIdSynapseDev + privateDnsZoneIdSynapseSql: privateDnsZoneIdSynapseSql + purviewId: purviewId + synapseComputeSubnetId: '' + synapseDefaultStorageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0].storageFileSystemId + } +} + +module synapse001RoleAssignmentStorageFileSystem 'modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { + name: 'synapse001RoleAssignmentStorageFileSystem' + scope: resourceGroup() + params: { + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0].storageFileSystemId + synapseId: synapse001.outputs.synapseId + } +} + +module powerPlatformRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(powerPlatformServicePrincipalObjectId)) { + name: 'powerPlatformRoleAssignmentStorage001' + scope: resourceGroup() + params: { + servicePrincipalObjectId: powerPlatformServicePrincipalObjectId + storageAccountId: storage001.outputs.storageId + roleId: 'c12c1c16-33a1-487b-954d-41c89c60f349' // Reader and Data Access + } +} + +module powerPlatformRoleAssignmentStorageFileSystem001 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(powerPlatformServicePrincipalObjectId)) { + name: 'powerPlatformRoleAssignmentStorageFileSystem001' + scope: resourceGroup() + params: { + servicePrincipalObjectId: powerPlatformServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[1].storageFileSystemId + roleId: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner + } +} + +module dataverseRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorage001' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountId: storage001.outputs.storageId + roleId: '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' // Owner + } +} + +module dataverseRoleAssignmentStorage002 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorage002' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountId: storage001.outputs.storageId + roleId: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner + } +} + +module dataverseRoleAssignmentStorage003 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorage003' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountId: storage001.outputs.storageId + roleId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor + } +} + +module dataverseRoleAssignmentStorage004 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorage004' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountId: storage001.outputs.storageId + roleId: '17d1049b-9a84-46fb-8f53-869881c3d3ab' // Storage Account Contributor + } +} + +module dataverseRoleAssignmentStorageFileSystem001 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorageFileSystem001' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId + roleId: '17d1049b-9a84-46fb-8f53-869881c3d3ab' // Storage Account Contributor + } +} + +module dataverseRoleAssignmentStorageFileSystem002 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorageFileSystem002' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId + roleId: '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' // Owner + } +} + +module dataverseRoleAssignmentStorageFileSystem003 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorageFileSystem003' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId + roleId: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner + } +} + +module dataverseRoleAssignmentStorageFileSystem004 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { + name: 'dataverseRoleAssignmentStorageFileSystem004' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId + roleId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor + } +} + +// Outputs +output synapseId string = synapse001.outputs.synapseId +output dataverseDataLakeFileSystemId string = storage001.outputs.storageFileSystemIds[2].storageFileSystemId diff --git a/healthcare/solutions/dataverseIntegration/main.json b/healthcare/solutions/dataverseIntegration/main.json new file mode 100644 index 00000000..33490576 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -0,0 +1,1784 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "7729186315494720054" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Specifies the location for all resources." + } + }, + "environment": { + "type": "string", + "defaultValue": "dev", + "metadata": { + "description": "Specifies the environment of the deployment." + }, + "allowedValues": [ + "dev", + "tst", + "prd" + ] + }, + "prefix": { + "type": "string", + "metadata": { + "description": "Specifies the prefix for all resources created in this deployment." + }, + "maxLength": 10, + "minLength": 2 + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Specifies the tags that you want to apply to all resources." + } + }, + "administratorPassword": { + "type": "secureString", + "defaultValue": "", + "metadata": { + "description": "Specifies the administrator password of the sql servers in Synapse." + } + }, + "powerPlatformServicePrincipalObjectId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the object ID of the Enterprise Application \"Microsoft Power Query\"." + } + }, + "dataverseServicePrincipalObjectId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the object ID of the Enterprise Application \"Export to data lake\"." + } + }, + "purviewId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the resource ID of the central purview instance to connect Purviw with Data Factory or Synapse. If you do not want to setup a connection to Purview, leave this value empty as is." + } + }, + "enableRoleAssignments": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Specifies whether role assignments should be enabled for Synapse (Blob Storage Contributor to default storage account)." + } + }, + "subnetId": { + "type": "string", + "metadata": { + "description": "Specifies the resource ID of the subnet to which all services will connect." + } + }, + "privateDnsZoneIdBlob": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the resource ID of the private DNS zone for Blob Storage." + } + }, + "privateDnsZoneIdDfs": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the resource ID of the private DNS zone for Datalake Storage." + } + }, + "privateDnsZoneIdKeyVault": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the resource ID of the private DNS zone for KeyVault." + } + }, + "privateDnsZoneIdSynapseDev": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the resource ID of the private DNS zone for Synapse Dev." + } + }, + "privateDnsZoneIdSynapseSql": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the resource ID of the private DNS zone for Synapse Sql." + } + } + }, + "functions": [], + "variables": { + "name": "[toLower(format('{0}-{1}', parameters('prefix'), parameters('environment')))]", + "tagsDefault": { + "Project": "Dataverse - Data Integration", + "Environment": "[parameters('environment')]", + "Toolkit": "bicep", + "Name": "[variables('name')]" + }, + "tagsJoined": "[union(variables('tagsDefault'), parameters('tags'))]", + "administratorUsername": "SqlServerMainUser", + "keyvault001Name": "[format('{0}-vault001', variables('name'))]", + "storage001Name": "[format('{0}-storage001', variables('name'))]", + "synapse001Name": "[format('{0}-synapse001', variables('name'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "keyVault001", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tagsJoined')]" + }, + "subnetId": { + "value": "[parameters('subnetId')]" + }, + "keyvaultName": { + "value": "[variables('keyvault001Name')]" + }, + "privateDnsZoneIdKeyVault": { + "value": "[parameters('privateDnsZoneIdKeyVault')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "11855677596500727104" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "subnetId": { + "type": "string" + }, + "keyvaultName": { + "type": "string" + }, + "privateDnsZoneIdKeyVault": { + "type": "string", + "defaultValue": "" + } + }, + "functions": [], + "variables": { + "keyVaultPrivateEndpointName": "[format('{0}-private-endpoint', parameters('keyvaultName'))]" + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2021-04-01-preview", + "name": "[parameters('keyvaultName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "accessPolicies": [], + "createMode": "default", + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": false, + "enablePurgeProtection": true, + "enableRbacAuthorization": true, + "enableSoftDelete": true, + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Deny", + "ipRules": [], + "virtualNetworkRules": [] + }, + "sku": { + "family": "A", + "name": "standard" + }, + "softDeleteRetentionInDays": 7, + "tenantId": "[subscription().tenantId]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2020-11-01", + "name": "[variables('keyVaultPrivateEndpointName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "manualPrivateLinkServiceConnections": [], + "privateLinkServiceConnections": [ + { + "name": "[variables('keyVaultPrivateEndpointName')]", + "properties": { + "groupIds": [ + "vault" + ], + "privateLinkServiceId": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName'))]", + "requestMessage": "" + } + } + ], + "subnet": { + "id": "[parameters('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName'))]" + ] + }, + { + "condition": "[not(empty(parameters('privateDnsZoneIdKeyVault')))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2020-11-01", + "name": "[format('{0}/{1}', variables('keyVaultPrivateEndpointName'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-arecord', variables('keyVaultPrivateEndpointName'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIdKeyVault')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('keyVaultPrivateEndpointName'))]" + ] + } + ], + "outputs": { + "keyvaultId": { + "type": "string", + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "storage001", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tagsJoined')]" + }, + "fileSystemNames": { + "value": [ + "synapse", + "power-platform-dataflows", + "dataverse" + ] + }, + "storageName": { + "value": "[variables('storage001Name')]" + }, + "subnetId": { + "value": "[parameters('subnetId')]" + }, + "privateDnsZoneIdBlob": { + "value": "[parameters('privateDnsZoneIdBlob')]" + }, + "privateDnsZoneIdDfs": { + "value": "[parameters('privateDnsZoneIdDfs')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "11213770733626231232" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "subnetId": { + "type": "string" + }, + "storageName": { + "type": "string" + }, + "privateDnsZoneIdDfs": { + "type": "string", + "defaultValue": "" + }, + "privateDnsZoneIdBlob": { + "type": "string", + "defaultValue": "" + }, + "fileSystemNames": { + "type": "array" + } + }, + "functions": [], + "variables": { + "storageNameCleaned": "[replace(parameters('storageName'), '-', '')]", + "storagePrivateEndpointNameBlob": "[format('{0}-blob-private-endpoint', variables('storageNameCleaned'))]", + "storagePrivateEndpointNameDfs": "[format('{0}-dfs-private-endpoint', variables('storageNameCleaned'))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-02-01", + "name": "[variables('storageNameCleaned')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned" + }, + "sku": { + "name": "Standard_ZRS" + }, + "kind": "StorageV2", + "properties": { + "accessTier": "Hot", + "allowBlobPublicAccess": false, + "allowSharedKeyAccess": true, + "encryption": { + "keySource": "Microsoft.Storage", + "requireInfrastructureEncryption": false, + "services": { + "blob": { + "enabled": true, + "keyType": "Account" + }, + "file": { + "enabled": true, + "keyType": "Account" + }, + "queue": { + "enabled": true, + "keyType": "Service" + }, + "table": { + "enabled": true, + "keyType": "Service" + } + } + }, + "isHnsEnabled": true, + "isNfsV3Enabled": false, + "largeFileSharesState": "Disabled", + "minimumTlsVersion": "TLS1_2", + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Allow", + "ipRules": [], + "virtualNetworkRules": [], + "resourceAccessRules": [] + }, + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.Storage/storageAccounts/managementPolicies", + "apiVersion": "2021-02-01", + "name": "[format('{0}/{1}', variables('storageNameCleaned'), 'default')]", + "properties": { + "policy": { + "rules": [ + { + "enabled": true, + "name": "default", + "type": "Lifecycle", + "definition": { + "actions": { + "baseBlob": { + "tierToCool": { + "daysAfterModificationGreaterThan": 90 + } + }, + "snapshot": { + "tierToCool": { + "daysAfterCreationGreaterThan": 90 + } + }, + "version": { + "tierToCool": { + "daysAfterCreationGreaterThan": 90 + } + } + }, + "filters": { + "blobTypes": [ + "blockBlob" + ], + "prefixMatch": [] + } + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2021-02-01", + "name": "[format('{0}/{1}', variables('storageNameCleaned'), 'default')]", + "properties": { + "containerDeleteRetentionPolicy": { + "enabled": true, + "days": 7 + }, + "cors": { + "corsRules": [] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + ] + }, + { + "copy": { + "name": "storageFileSystems", + "count": "[length(parameters('fileSystemNames'))]" + }, + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2021-02-01", + "name": "[format('{0}/{1}/{2}', variables('storageNameCleaned'), 'default', parameters('fileSystemNames')[copyIndex()])]", + "properties": { + "publicAccess": "None", + "metadata": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]", + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageNameCleaned'), 'default')]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2020-11-01", + "name": "[variables('storagePrivateEndpointNameBlob')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "manualPrivateLinkServiceConnections": [], + "privateLinkServiceConnections": [ + { + "name": "[variables('storagePrivateEndpointNameBlob')]", + "properties": { + "groupIds": [ + "blob" + ], + "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]", + "requestMessage": "" + } + } + ], + "subnet": { + "id": "[parameters('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + ] + }, + { + "condition": "[not(empty(parameters('privateDnsZoneIdBlob')))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2020-11-01", + "name": "[format('{0}/{1}', variables('storagePrivateEndpointNameBlob'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-arecord', variables('storagePrivateEndpointNameBlob'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIdBlob')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('storagePrivateEndpointNameBlob'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2020-11-01", + "name": "[variables('storagePrivateEndpointNameDfs')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "manualPrivateLinkServiceConnections": [], + "privateLinkServiceConnections": [ + { + "name": "[variables('storagePrivateEndpointNameDfs')]", + "properties": { + "groupIds": [ + "dfs" + ], + "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]", + "requestMessage": "" + } + } + ], + "subnet": { + "id": "[parameters('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + ] + }, + { + "condition": "[not(empty(parameters('privateDnsZoneIdDfs')))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2020-11-01", + "name": "[format('{0}/{1}', variables('storagePrivateEndpointNameDfs'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-arecord', variables('storagePrivateEndpointNameDfs'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIdDfs')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('storagePrivateEndpointNameDfs'))]" + ] + } + ], + "outputs": { + "storageId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + }, + "storageFileSystemIds": { + "type": "array", + "copy": { + "count": "[length(parameters('fileSystemNames'))]", + "input": { + "storageFileSystemId": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageNameCleaned'), 'default', parameters('fileSystemNames')[copyIndex()])]" + } + } + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "synapse001", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tagsJoined')]" + }, + "subnetId": { + "value": "[parameters('subnetId')]" + }, + "synapseName": { + "value": "[variables('synapse001Name')]" + }, + "administratorUsername": { + "value": "[variables('administratorUsername')]" + }, + "administratorPassword": { + "value": "[parameters('administratorPassword')]" + }, + "synapseSqlAdminGroupName": { + "value": "" + }, + "synapseSqlAdminGroupObjectID": { + "value": "" + }, + "privateDnsZoneIdSynapseDev": { + "value": "[parameters('privateDnsZoneIdSynapseDev')]" + }, + "privateDnsZoneIdSynapseSql": { + "value": "[parameters('privateDnsZoneIdSynapseSql')]" + }, + "purviewId": { + "value": "[parameters('purviewId')]" + }, + "synapseComputeSubnetId": { + "value": "" + }, + "synapseDefaultStorageAccountFileSystemId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[0].storageFileSystemId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "13182141804118079952" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "subnetId": { + "type": "string" + }, + "synapseName": { + "type": "string" + }, + "administratorUsername": { + "type": "string", + "defaultValue": "SqlServerMainUser" + }, + "administratorPassword": { + "type": "secureString" + }, + "synapseSqlAdminGroupName": { + "type": "string", + "defaultValue": "" + }, + "synapseSqlAdminGroupObjectID": { + "type": "string", + "defaultValue": "" + }, + "synapseDefaultStorageAccountFileSystemId": { + "type": "string" + }, + "synapseComputeSubnetId": { + "type": "string", + "defaultValue": "" + }, + "privateDnsZoneIdSynapseSql": { + "type": "string", + "defaultValue": "" + }, + "privateDnsZoneIdSynapseDev": { + "type": "string", + "defaultValue": "" + }, + "purviewId": { + "type": "string", + "defaultValue": "" + } + }, + "functions": [], + "variables": { + "synapseDefaultStorageAccountFileSystemName": "[if(greaterOrEquals(length(split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')), 13), last(split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')), 'incorrectSegmentLength')]", + "synapseDefaultStorageAccountName": "[if(greaterOrEquals(length(split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')), 13), split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')[8], 'incorrectSegmentLength')]", + "synapsePrivateEndpointNameSql": "[format('{0}-sql-private-endpoint', parameters('synapseName'))]", + "synapsePrivateEndpointNameSqlOnDemand": "[format('{0}-sqlondemand-private-endpoint', parameters('synapseName'))]", + "synapsePrivateEndpointNameDev": "[format('{0}-dev-private-endpoint', parameters('synapseName'))]" + }, + "resources": [ + { + "type": "Microsoft.Synapse/workspaces", + "apiVersion": "2021-03-01", + "name": "[parameters('synapseName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "defaultDataLakeStorage": { + "accountUrl": "[format('https://{0}.dfs.{1}', variables('synapseDefaultStorageAccountName'), environment().suffixes.storage)]", + "filesystem": "[variables('synapseDefaultStorageAccountFileSystemName')]" + }, + "managedResourceGroupName": "[parameters('synapseName')]", + "managedVirtualNetwork": "default", + "managedVirtualNetworkSettings": { + "allowedAadTenantIdsForLinking": [], + "linkedAccessCheckOnTargetResource": true, + "preventDataExfiltration": true + }, + "publicNetworkAccess": "Enabled", + "purviewConfiguration": { + "purviewResourceId": "[parameters('purviewId')]" + }, + "sqlAdministratorLogin": "[parameters('administratorUsername')]", + "sqlAdministratorLoginPassword": "[parameters('administratorPassword')]", + "virtualNetworkProfile": { + "computeSubnetId": "[parameters('synapseComputeSubnetId')]" + } + } + }, + { + "type": "Microsoft.Synapse/workspaces/bigDataPools", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', parameters('synapseName'), 'bigDataPool001')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "autoPause": { + "enabled": true, + "delayInMinutes": 15 + }, + "autoScale": { + "enabled": true, + "minNodeCount": 3, + "maxNodeCount": 10 + }, + "customLibraries": [], + "defaultSparkLogFolder": "logs/", + "dynamicExecutorAllocation": { + "enabled": true, + "minExecutors": 1, + "maxExecutors": 9 + }, + "nodeSize": "Small", + "nodeSizeFamily": "MemoryOptimized", + "sessionLevelPackagesEnabled": true, + "sparkEventsFolder": "events/", + "sparkVersion": "3.1" + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "type": "Microsoft.Synapse/workspaces/managedIdentitySqlControlSettings", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('synapseName'), 'default')]", + "properties": { + "grantSqlControlToManagedIdentity": { + "desiredState": "Enabled" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "condition": "[and(not(empty(parameters('synapseSqlAdminGroupName'))), not(empty(parameters('synapseSqlAdminGroupObjectID'))))]", + "type": "Microsoft.Synapse/workspaces/administrators", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('synapseName'), 'activeDirectory')]", + "properties": { + "administratorType": "ActiveDirectory", + "login": "[parameters('synapseSqlAdminGroupName')]", + "sid": "[parameters('synapseSqlAdminGroupObjectID')]", + "tenantId": "[subscription().tenantId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "type": "Microsoft.Synapse/workspaces/firewallRules", + "apiVersion": "2021-06-01-preview", + "name": "[format('{0}/{1}', parameters('synapseName'), 'allowAll')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "255.255.255.255" + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2020-11-01", + "name": "[variables('synapsePrivateEndpointNameSql')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "manualPrivateLinkServiceConnections": [], + "privateLinkServiceConnections": [ + { + "name": "[variables('synapsePrivateEndpointNameSql')]", + "properties": { + "groupIds": [ + "Sql" + ], + "privateLinkServiceId": "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]", + "requestMessage": "" + } + } + ], + "subnet": { + "id": "[parameters('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "condition": "[not(empty(parameters('privateDnsZoneIdSynapseSql')))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2020-11-01", + "name": "[format('{0}/{1}', variables('synapsePrivateEndpointNameSql'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-arecord', variables('synapsePrivateEndpointNameSql'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIdSynapseSql')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('synapsePrivateEndpointNameSql'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2020-11-01", + "name": "[variables('synapsePrivateEndpointNameSqlOnDemand')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "manualPrivateLinkServiceConnections": [], + "privateLinkServiceConnections": [ + { + "name": "[variables('synapsePrivateEndpointNameSqlOnDemand')]", + "properties": { + "groupIds": [ + "SqlOnDemand" + ], + "privateLinkServiceId": "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]", + "requestMessage": "" + } + } + ], + "subnet": { + "id": "[parameters('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "condition": "[not(empty(parameters('privateDnsZoneIdSynapseSql')))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2020-11-01", + "name": "[format('{0}/{1}', variables('synapsePrivateEndpointNameSqlOnDemand'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-arecord', variables('synapsePrivateEndpointNameSqlOnDemand'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIdSynapseSql')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('synapsePrivateEndpointNameSqlOnDemand'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2020-11-01", + "name": "[variables('synapsePrivateEndpointNameDev')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "manualPrivateLinkServiceConnections": [], + "privateLinkServiceConnections": [ + { + "name": "[variables('synapsePrivateEndpointNameDev')]", + "properties": { + "groupIds": [ + "Dev" + ], + "privateLinkServiceId": "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]", + "requestMessage": "" + } + } + ], + "subnet": { + "id": "[parameters('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "condition": "[not(empty(parameters('privateDnsZoneIdSynapseDev')))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2020-11-01", + "name": "[format('{0}/{1}', variables('synapsePrivateEndpointNameDev'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[format('{0}-arecord', variables('synapsePrivateEndpointNameDev'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIdSynapseDev')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('synapsePrivateEndpointNameDev'))]" + ] + } + ], + "outputs": { + "synapseId": { + "type": "string", + "value": "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + }, + "synapseBigDataPool001Id": { + "type": "string", + "value": "[resourceId('Microsoft.Synapse/workspaces/bigDataPools', parameters('synapseName'), 'bigDataPool001')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[parameters('enableRoleAssignments')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "synapse001RoleAssignmentStorageFileSystem", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountFileSystemId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[0].storageFileSystemId]" + }, + "synapseId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'synapse001'), '2019-10-01').outputs.synapseId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "11818523926389760461" + } + }, + "parameters": { + "storageAccountFileSystemId": { + "type": "string" + }, + "synapseId": { + "type": "string" + } + }, + "functions": [], + "variables": { + "storageAccountFileSystemName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), last(split(parameters('storageAccountFileSystemId'), '/')), 'incorrectSegmentLength')]", + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), split(parameters('storageAccountFileSystemId'), '/')[8], 'incorrectSegmentLength')]", + "synapseSubscriptionId": "[if(greaterOrEquals(length(split(parameters('synapseId'), '/')), 9), split(parameters('synapseId'), '/')[2], subscription().subscriptionId)]", + "synapseResourceGroupName": "[if(greaterOrEquals(length(split(parameters('synapseId'), '/')), 9), split(parameters('synapseId'), '/')[4], resourceGroup().name)]", + "synapseName": "[if(greaterOrEquals(length(split(parameters('synapseId'), '/')), 9), last(split(parameters('synapseId'), '/')), 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName'))]", + "name": "[guid(uniqueString(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('synapseSubscriptionId'), variables('synapseResourceGroupName')), 'Microsoft.Synapse/workspaces', variables('synapseName'))))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('synapseSubscriptionId'), variables('synapseResourceGroupName')), 'Microsoft.Synapse/workspaces', variables('synapseName')), '2021-03-01', 'full').identity.principalId]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]", + "[resourceId('Microsoft.Resources/deployments', 'synapse001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('powerPlatformServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "powerPlatformRoleAssignmentStorage001", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('powerPlatformServicePrincipalObjectId')]" + }, + "storageAccountId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageId.value]" + }, + "roleId": { + "value": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "907671850504535586" + } + }, + "parameters": { + "storageAccountId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountId'), '/')), 9), last(split(parameters('storageAccountId'), '/')), 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageAccountName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('powerPlatformServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "powerPlatformRoleAssignmentStorageFileSystem001", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('powerPlatformServicePrincipalObjectId')]" + }, + "storageAccountFileSystemId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[1].storageFileSystemId]" + }, + "roleId": { + "value": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "13299270416843528420" + } + }, + "parameters": { + "storageAccountFileSystemId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountFileSystemName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), last(split(parameters('storageAccountFileSystemId'), '/')), 'incorrectSegmentLength')]", + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), split(parameters('storageAccountFileSystemId'), '/')[8], 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorage001", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageId.value]" + }, + "roleId": { + "value": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "907671850504535586" + } + }, + "parameters": { + "storageAccountId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountId'), '/')), 9), last(split(parameters('storageAccountId'), '/')), 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageAccountName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorage002", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageId.value]" + }, + "roleId": { + "value": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "907671850504535586" + } + }, + "parameters": { + "storageAccountId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountId'), '/')), 9), last(split(parameters('storageAccountId'), '/')), 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageAccountName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorage003", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageId.value]" + }, + "roleId": { + "value": "ba92f5b4-2d11-453d-a403-e96b0029c9fe" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "907671850504535586" + } + }, + "parameters": { + "storageAccountId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountId'), '/')), 9), last(split(parameters('storageAccountId'), '/')), 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageAccountName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorage004", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageId.value]" + }, + "roleId": { + "value": "17d1049b-9a84-46fb-8f53-869881c3d3ab" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "907671850504535586" + } + }, + "parameters": { + "storageAccountId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountId'), '/')), 9), last(split(parameters('storageAccountId'), '/')), 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageAccountName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorageFileSystem001", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountFileSystemId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" + }, + "roleId": { + "value": "17d1049b-9a84-46fb-8f53-869881c3d3ab" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "13299270416843528420" + } + }, + "parameters": { + "storageAccountFileSystemId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountFileSystemName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), last(split(parameters('storageAccountFileSystemId'), '/')), 'incorrectSegmentLength')]", + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), split(parameters('storageAccountFileSystemId'), '/')[8], 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorageFileSystem002", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountFileSystemId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" + }, + "roleId": { + "value": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "13299270416843528420" + } + }, + "parameters": { + "storageAccountFileSystemId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountFileSystemName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), last(split(parameters('storageAccountFileSystemId'), '/')), 'incorrectSegmentLength')]", + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), split(parameters('storageAccountFileSystemId'), '/')[8], 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorageFileSystem003", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountFileSystemId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" + }, + "roleId": { + "value": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "13299270416843528420" + } + }, + "parameters": { + "storageAccountFileSystemId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountFileSystemName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), last(split(parameters('storageAccountFileSystemId'), '/')), 'incorrectSegmentLength')]", + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), split(parameters('storageAccountFileSystemId'), '/')[8], 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + }, + { + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "dataverseRoleAssignmentStorageFileSystem004", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "servicePrincipalObjectId": { + "value": "[parameters('dataverseServicePrincipalObjectId')]" + }, + "storageAccountFileSystemId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" + }, + "roleId": { + "value": "ba92f5b4-2d11-453d-a403-e96b0029c9fe" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.4.613.9944", + "templateHash": "13299270416843528420" + } + }, + "parameters": { + "storageAccountFileSystemId": { + "type": "string" + }, + "servicePrincipalObjectId": { + "type": "string" + }, + "roleId": { + "type": "string", + "metadata": { + "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Storage Blob Data Owner": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "Storage Blob Data Contributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "Reader and Data Access": "c12c1c16-33a1-487b-954d-41c89c60f349" + } + } + }, + "functions": [], + "variables": { + "storageAccountFileSystemName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), last(split(parameters('storageAccountFileSystemId'), '/')), 'incorrectSegmentLength')]", + "storageAccountName": "[if(greaterOrEquals(length(split(parameters('storageAccountFileSystemId'), '/')), 13), split(parameters('storageAccountFileSystemId'), '/')[8], 'incorrectSegmentLength')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName'))]", + "name": "[guid(uniqueString(parameters('roleId'), resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('storageAccountFileSystemName')), parameters('servicePrincipalObjectId')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleId'))]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] + } + ], + "outputs": { + "synapseId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'synapse001'), '2019-10-01').outputs.synapseId.value]" + }, + "dataverseDataLakeFileSystemId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" + } + } +} \ No newline at end of file diff --git a/healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep b/healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep new file mode 100644 index 00000000..2c7094a6 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// The module contains a template to create a role assignment of a Service Principle to a storage file system. +targetScope = 'resourceGroup' + +// Parameters +param storageAccountId string +param servicePrincipalObjectId string +@metadata({ + 'Owner': '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + 'Storage Blob Data Owner': 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' + 'Storage Blob Data Contributor': 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + 'Storage Account Contributor': '17d1049b-9a84-46fb-8f53-869881c3d3ab' + 'Reader and Data Access': 'c12c1c16-33a1-487b-954d-41c89c60f349' +}) +param roleId string + +// Variables +var storageAccountName = length(split(storageAccountId, '/')) >= 9 ? last(split(storageAccountId, '/')) : 'incorrectSegmentLength' + +// Resources +resource storage 'Microsoft.Storage/storageAccounts@2021-04-01' existing = { + name: storageAccountName +} + +resource synapseRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(uniqueString(roleId, storage.id, servicePrincipalObjectId)) + scope: storage + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleId) + principalId: servicePrincipalObjectId + principalType: 'ServicePrincipal' + } +} + +// Outputs diff --git a/healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep b/healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep new file mode 100644 index 00000000..169e420c --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// The module contains a template to create a role assignment of a Service Principle to a storage file system. +targetScope = 'resourceGroup' + +// Parameters +param storageAccountFileSystemId string +param servicePrincipalObjectId string +@metadata({ + 'Owner': '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + 'Storage Blob Data Owner': 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' + 'Storage Blob Data Contributor': 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + 'Storage Account Contributor': '17d1049b-9a84-46fb-8f53-869881c3d3ab' + 'Reader and Data Access': 'c12c1c16-33a1-487b-954d-41c89c60f349' +}) +param roleId string + +// Variables +var storageAccountFileSystemName = length(split(storageAccountFileSystemId, '/')) >= 13 ? last(split(storageAccountFileSystemId, '/')) : 'incorrectSegmentLength' +var storageAccountName = length(split(storageAccountFileSystemId, '/')) >= 13 ? split(storageAccountFileSystemId, '/')[8] : 'incorrectSegmentLength' + +// Resources +resource storage 'Microsoft.Storage/storageAccounts@2021-04-01' existing = { + name: storageAccountName +} + +resource storageBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2021-04-01' existing = { + name: 'default' + parent: storage +} + +resource storageFileSystem 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-02-01' existing = { + name: storageAccountFileSystemName + parent: storageBlobServices +} + +resource synapseRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(uniqueString(roleId, storageFileSystem.id, servicePrincipalObjectId)) + scope: storageFileSystem + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleId) + principalId: servicePrincipalObjectId + principalType: 'ServicePrincipal' + } +} + +// Outputs diff --git a/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep b/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep new file mode 100644 index 00000000..2447cd95 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// The module contains a template to create a role assignment of the Synase MSI to a storage file system. +targetScope = 'resourceGroup' + +// Parameters +param storageAccountFileSystemId string +param synapseId string + +// Variables +var storageAccountFileSystemName = length(split(storageAccountFileSystemId, '/')) >= 13 ? last(split(storageAccountFileSystemId, '/')) : 'incorrectSegmentLength' +var storageAccountName = length(split(storageAccountFileSystemId, '/')) >= 13 ? split(storageAccountFileSystemId, '/')[8] : 'incorrectSegmentLength' +var synapseSubscriptionId = length(split(synapseId, '/')) >= 9 ? split(synapseId, '/')[2] : subscription().subscriptionId +var synapseResourceGroupName = length(split(synapseId, '/')) >= 9 ? split(synapseId, '/')[4] : resourceGroup().name +var synapseName = length(split(synapseId, '/')) >= 9 ? last(split(synapseId, '/')) : 'incorrectSegmentLength' + +// Resources +resource storage 'Microsoft.Storage/storageAccounts@2021-04-01' existing = { + name: storageAccountName +} + +resource storageBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2021-04-01' existing = { + name: 'default' + parent: storage +} + +resource storageFileSystem 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-02-01' existing = { + name: storageAccountFileSystemName + parent: storageBlobServices +} + +resource synapse 'Microsoft.Synapse/workspaces@2021-03-01' existing = { + name: synapseName + scope: resourceGroup(synapseSubscriptionId, synapseResourceGroupName) +} + +resource synapseRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(uniqueString(storageFileSystem.id, synapse.id)) + scope: storageFileSystem + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalId: synapse.identity.principalId + } +} + +// Outputs diff --git a/healthcare/solutions/dataverseIntegration/modules/services/keyvault.bicep b/healthcare/solutions/dataverseIntegration/modules/services/keyvault.bicep new file mode 100644 index 00000000..daeaa10c --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/services/keyvault.bicep @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// This template is used to create a KeyVault. +targetScope = 'resourceGroup' + +// Parameters +param location string +param tags object +param subnetId string +param keyvaultName string +param privateDnsZoneIdKeyVault string = '' + +// Variables +var keyVaultPrivateEndpointName = '${keyVault.name}-private-endpoint' + +// Resources +resource keyVault 'Microsoft.KeyVault/vaults@2021-04-01-preview' = { + name: keyvaultName + location: location + tags: tags + properties: { + accessPolicies: [] + createMode: 'default' + enabledForDeployment: false + enabledForDiskEncryption: false + enabledForTemplateDeployment: false + enablePurgeProtection: true + enableRbacAuthorization: true + enableSoftDelete: true + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + ipRules: [] + virtualNetworkRules: [] + } + sku: { + family: 'A' + name: 'standard' + } + softDeleteRetentionInDays: 7 + tenantId: subscription().tenantId + } +} + +resource keyVaultPrivateEndpoint 'Microsoft.Network/privateEndpoints@2020-11-01' = { + name: keyVaultPrivateEndpointName + location: location + tags: tags + properties: { + manualPrivateLinkServiceConnections: [] + privateLinkServiceConnections: [ + { + name: keyVaultPrivateEndpointName + properties: { + groupIds: [ + 'vault' + ] + privateLinkServiceId: keyVault.id + requestMessage: '' + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource keyVaultPrivateEndpointARecord 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-11-01' = if (!empty(privateDnsZoneIdKeyVault)) { + parent: keyVaultPrivateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: '${keyVaultPrivateEndpoint.name}-arecord' + properties: { + privateDnsZoneId: privateDnsZoneIdKeyVault + } + } + ] + } +} + +// Outputs +output keyvaultId string = keyVault.id diff --git a/healthcare/solutions/dataverseIntegration/modules/services/storage.bicep b/healthcare/solutions/dataverseIntegration/modules/services/storage.bicep new file mode 100644 index 00000000..4bf83fb5 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/services/storage.bicep @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// This template is used to create a datalake. +targetScope = 'resourceGroup' + +// Parameters +param location string +param tags object +param subnetId string +param storageName string +param privateDnsZoneIdDfs string = '' +param privateDnsZoneIdBlob string = '' +param fileSystemNames array + +// Variables +var storageNameCleaned = replace(storageName, '-', '') +var storagePrivateEndpointNameBlob = '${storage.name}-blob-private-endpoint' +var storagePrivateEndpointNameDfs = '${storage.name}-dfs-private-endpoint' + +// Resources +resource storage 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: storageNameCleaned + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + sku: { + name: 'Standard_ZRS' + } + kind: 'StorageV2' + properties: { + accessTier: 'Hot' + allowBlobPublicAccess: false + allowSharedKeyAccess: true + encryption: { + keySource: 'Microsoft.Storage' + requireInfrastructureEncryption: false + services: { + blob: { + enabled: true + keyType: 'Account' + } + file: { + enabled: true + keyType: 'Account' + } + queue: { + enabled: true + keyType: 'Service' + } + table: { + enabled: true + keyType: 'Service' + } + } + } + isHnsEnabled: true + isNfsV3Enabled: false + largeFileSharesState: 'Disabled' + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + ipRules: [] + virtualNetworkRules: [] + resourceAccessRules: [] + } + // routingPreference: { // Not supported for thsi account + // routingChoice: 'MicrosoftRouting' + // publishInternetEndpoints: false + // publishMicrosoftEndpoints: false + // } + supportsHttpsTrafficOnly: true + } +} + +resource storageManagementPolicies 'Microsoft.Storage/storageAccounts/managementPolicies@2021-02-01' = { + parent: storage + name: 'default' + properties: { + policy: { + rules: [ + { + enabled: true + name: 'default' + type: 'Lifecycle' + definition: { + actions: { + baseBlob: { + // enableAutoTierToHotFromCool: true // Not available for HNS storage yet + tierToCool: { + // daysAfterLastAccessTimeGreaterThan: 90 // Not available for HNS storage yet + daysAfterModificationGreaterThan: 90 + } + // tierToArchive: { // Not available for HNS storage yet + // // daysAfterLastAccessTimeGreaterThan: 365 // Not available for HNS storage yet + // daysAfterModificationGreaterThan: 365 + // } + // delete: { // Uncomment, if you also want to delete assets after a certain timeframe + // // daysAfterLastAccessTimeGreaterThan: 730 // Not available for HNS storage yet + // daysAfterModificationGreaterThan: 730 + // } + } + snapshot: { + tierToCool: { + daysAfterCreationGreaterThan: 90 + } + // tierToArchive: { // Not available for HNS storage yet + // daysAfterCreationGreaterThan: 365 + // } + // delete: { // Uncomment, if you also want to delete assets after a certain timeframe + // daysAfterCreationGreaterThan: 730 + // } + } + version: { + tierToCool: { + daysAfterCreationGreaterThan: 90 + } + // tierToArchive: { // Not available for HNS storage yet + // daysAfterCreationGreaterThan: 365 + // } + // delete: { // Uncomment, if you also want to delete assets after a certain timeframe + // daysAfterCreationGreaterThan: 730 + // } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + prefixMatch: [] + } + } + } + ] + } + } +} + +resource storageBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2021-02-01' = { + parent: storage + name: 'default' + properties: { + containerDeleteRetentionPolicy: { + enabled: true + days: 7 + } + cors: { + corsRules: [] + } + // automaticSnapshotPolicyEnabled: true // Not available for HNS storage yet + // changeFeed: { + // enabled: true + // retentionInDays: 7 + // } + // defaultServiceVersion: '' + // deleteRetentionPolicy: { + // enabled: true + // days: 7 + // } + // isVersioningEnabled: true + // lastAccessTimeTrackingPolicy: { + // name: 'AccessTimeTracking' + // enable: true + // blobType: [ + // 'blockBlob' + // ] + // trackingGranularityInDays: 1 + // } + // restorePolicy: { + // enabled: true + // days: 7 + // } + } +} + +resource storageFileSystems 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-02-01' = [for fileSystemName in fileSystemNames: { + parent: storageBlobServices + name: fileSystemName + properties: { + publicAccess: 'None' + metadata: {} + } +}] + +resource storagePrivateEndpointBlob 'Microsoft.Network/privateEndpoints@2020-11-01' = { + name: storagePrivateEndpointNameBlob + location: location + tags: tags + properties: { + manualPrivateLinkServiceConnections: [] + privateLinkServiceConnections: [ + { + name: storagePrivateEndpointNameBlob + properties: { + groupIds: [ + 'blob' + ] + privateLinkServiceId: storage.id + requestMessage: '' + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource storagePrivateEndpointBlobARecord 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-11-01' = if (!empty(privateDnsZoneIdBlob)) { + parent: storagePrivateEndpointBlob + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: '${storagePrivateEndpointBlob.name}-arecord' + properties: { + privateDnsZoneId: privateDnsZoneIdBlob + } + } + ] + } +} + +resource storagePrivateEndpointDfs 'Microsoft.Network/privateEndpoints@2020-11-01' = { + name: storagePrivateEndpointNameDfs + location: location + tags: tags + properties: { + manualPrivateLinkServiceConnections: [] + privateLinkServiceConnections: [ + { + name: storagePrivateEndpointNameDfs + properties: { + groupIds: [ + 'dfs' + ] + privateLinkServiceId: storage.id + requestMessage: '' + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource storagePrivateEndpointDfsARecord 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-11-01' = if (!empty(privateDnsZoneIdDfs)) { + parent: storagePrivateEndpointDfs + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: '${storagePrivateEndpointDfs.name}-arecord' + properties: { + privateDnsZoneId: privateDnsZoneIdDfs + } + } + ] + } +} + +// Outputs +output storageId string = storage.id +output storageFileSystemIds array = [for fileSystemName in fileSystemNames: { + storageFileSystemId: resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', storageNameCleaned, 'default', fileSystemName) +}] diff --git a/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep new file mode 100644 index 00000000..cf64fd83 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// This template is used to create a Synapse workspace. +targetScope = 'resourceGroup' + +// Parameters +param location string +param tags object +param subnetId string +param synapseName string +param administratorUsername string = 'SqlServerMainUser' +@secure() +param administratorPassword string +param synapseSqlAdminGroupName string = '' +param synapseSqlAdminGroupObjectID string = '' +param synapseDefaultStorageAccountFileSystemId string +param synapseComputeSubnetId string = '' +param privateDnsZoneIdSynapseSql string = '' +param privateDnsZoneIdSynapseDev string = '' +param purviewId string = '' + +// Variables +var synapseDefaultStorageAccountFileSystemName = length(split(synapseDefaultStorageAccountFileSystemId, '/')) >= 13 ? last(split(synapseDefaultStorageAccountFileSystemId, '/')) : 'incorrectSegmentLength' +var synapseDefaultStorageAccountName = length(split(synapseDefaultStorageAccountFileSystemId, '/')) >= 13 ? split(synapseDefaultStorageAccountFileSystemId, '/')[8] : 'incorrectSegmentLength' +var synapsePrivateEndpointNameSql = '${synapse.name}-sql-private-endpoint' +var synapsePrivateEndpointNameSqlOnDemand = '${synapse.name}-sqlondemand-private-endpoint' +var synapsePrivateEndpointNameDev = '${synapse.name}-dev-private-endpoint' + +// Resources +resource synapse 'Microsoft.Synapse/workspaces@2021-03-01' = { + name: synapseName + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + defaultDataLakeStorage: { + accountUrl: 'https://${synapseDefaultStorageAccountName}.dfs.${environment().suffixes.storage}' + filesystem: synapseDefaultStorageAccountFileSystemName + } + managedResourceGroupName: synapseName + managedVirtualNetwork: 'default' + managedVirtualNetworkSettings: { + allowedAadTenantIdsForLinking: [] + linkedAccessCheckOnTargetResource: true + preventDataExfiltration: true + } + publicNetworkAccess: 'Enabled' + purviewConfiguration: { + purviewResourceId: purviewId + } + sqlAdministratorLogin: administratorUsername + sqlAdministratorLoginPassword: administratorPassword + virtualNetworkProfile: { + computeSubnetId: synapseComputeSubnetId + } + } +} + +resource synapseBigDataPool001 'Microsoft.Synapse/workspaces/bigDataPools@2021-05-01' = { + parent: synapse + name: 'bigDataPool001' + location: location + tags: tags + properties: { + autoPause: { + enabled: true + delayInMinutes: 15 + } + autoScale: { + enabled: true + minNodeCount: 3 + maxNodeCount: 10 + } + // cacheSize: 100 // Uncomment to set a specific cache size + customLibraries: [] + defaultSparkLogFolder: 'logs/' + dynamicExecutorAllocation: { + enabled: true + minExecutors: 1 + maxExecutors: 9 + } + // isComputeIsolationEnabled: true // Uncomment to enable compute isolation (only available in selective regions) + // libraryRequirements: { // Uncomment to install pip dependencies on the Spark cluster + // content: '' + // filename: 'requirements.txt' + // } + nodeSize: 'Small' + nodeSizeFamily: 'MemoryOptimized' + sessionLevelPackagesEnabled: true + // sparkConfigProperties: { // Uncomment to set spark conf on the Spark cluster + // content: '' + // filename: 'spark.conf' + // } + sparkEventsFolder: 'events/' + sparkVersion: '3.1' + } +} + +resource synapseManagedIdentitySqlControlSettings 'Microsoft.Synapse/workspaces/managedIdentitySqlControlSettings@2021-03-01' = { + parent: synapse + name: 'default' + properties: { + grantSqlControlToManagedIdentity: { + desiredState: 'Enabled' + } + } +} + +resource synapseAadAdministrators 'Microsoft.Synapse/workspaces/administrators@2021-03-01' = if (!empty(synapseSqlAdminGroupName) && !empty(synapseSqlAdminGroupObjectID)) { + parent: synapse + name: 'activeDirectory' + properties: { + administratorType: 'ActiveDirectory' + login: synapseSqlAdminGroupName + sid: synapseSqlAdminGroupObjectID + tenantId: subscription().tenantId + } +} + +resource synapseFirewallRule001 'Microsoft.Synapse/workspaces/firewallRules@2021-06-01-preview' = { + parent: synapse + name: 'allowAll' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } +} + +resource synapsePrivateEndpointSql 'Microsoft.Network/privateEndpoints@2020-11-01' = { + name: synapsePrivateEndpointNameSql + location: location + tags: tags + properties: { + manualPrivateLinkServiceConnections: [] + privateLinkServiceConnections: [ + { + name: synapsePrivateEndpointNameSql + properties: { + groupIds: [ + 'Sql' + ] + privateLinkServiceId: synapse.id + requestMessage: '' + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource synapsePrivateEndpointSqlARecord 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-11-01' = if (!empty(privateDnsZoneIdSynapseSql)) { + parent: synapsePrivateEndpointSql + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: '${synapsePrivateEndpointSql.name}-arecord' + properties: { + privateDnsZoneId: privateDnsZoneIdSynapseSql + } + } + ] + } +} + +resource synapsePrivateEndpointSqlOnDemand 'Microsoft.Network/privateEndpoints@2020-11-01' = { + name: synapsePrivateEndpointNameSqlOnDemand + location: location + tags: tags + properties: { + manualPrivateLinkServiceConnections: [] + privateLinkServiceConnections: [ + { + name: synapsePrivateEndpointNameSqlOnDemand + properties: { + groupIds: [ + 'SqlOnDemand' + ] + privateLinkServiceId: synapse.id + requestMessage: '' + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource synapsePrivateEndpointSqlOnDemandARecord 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-11-01' = if (!empty(privateDnsZoneIdSynapseSql)) { + parent: synapsePrivateEndpointSqlOnDemand + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: '${synapsePrivateEndpointSqlOnDemand.name}-arecord' + properties: { + privateDnsZoneId: privateDnsZoneIdSynapseSql + } + } + ] + } +} + +resource synapsePrivateEndpointDev 'Microsoft.Network/privateEndpoints@2020-11-01' = { + name: synapsePrivateEndpointNameDev + location: location + tags: tags + properties: { + manualPrivateLinkServiceConnections: [] + privateLinkServiceConnections: [ + { + name: synapsePrivateEndpointNameDev + properties: { + groupIds: [ + 'Dev' + ] + privateLinkServiceId: synapse.id + requestMessage: '' + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource synapsePrivateEndpointDevARecord 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-11-01' = if (!empty(privateDnsZoneIdSynapseDev)) { + parent: synapsePrivateEndpointDev + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: '${synapsePrivateEndpointDev.name}-arecord' + properties: { + privateDnsZoneId: privateDnsZoneIdSynapseDev + } + } + ] + } +} + +// Outputs +output synapseId string = synapse.id +output synapseBigDataPool001Id string = synapseBigDataPool001.id diff --git a/healthcare/solutions/dataverseIntegration/params.json b/healthcare/solutions/dataverseIntegration/params.json new file mode 100644 index 00000000..30fde1fb --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/params.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "value": "eastus" + }, + "environment": { + "value": "dev" + }, + "prefix": { + "value": "mydverse" + }, + "tags": { + "value": {} + }, + "administratorPassword": { + "value": "" + }, + "powerPlatformServicePrincipalObjectId": { + "value": "" + }, + "dataverseServicePrincipalObjectId": { + "value": "" + }, + "purviewId": { + "value": "" + }, + "enableRoleAssignments": { + "value": true + }, + "subnetId": { + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/virtualNetworks/mydverse-dev-vnet/subnets/default" + }, + "privateDnsZoneIdKeyVault": { + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net" + }, + "privateDnsZoneIdSynapseDev": { + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.dev.azuresynapse.net" + }, + "privateDnsZoneIdSynapseSql": { + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.sql.azuresynapse.net" + }, + "privateDnsZoneIdBlob": { + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net" + }, + "privateDnsZoneIdDfs": { + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.dfs.core.windows.net" + } + } +} \ No newline at end of file diff --git a/healthcare/solutions/dataverseIntegration/portal.json b/healthcare/solutions/dataverseIntegration/portal.json new file mode 100644 index 00000000..80b0daa6 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/portal.json @@ -0,0 +1,741 @@ +{ + "$schema": "", + "view": { + "kind": "Form", + "properties": { + "title": "Dataverse-Azure - Data Integration", + "steps": [ + { + "name": "basics", + "label": "Deployment Location", + "elements": [ + { + "name": "deploymentDetails", + "label": "Deployment Details", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "deploymentDetailsText", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Select the subscription and resource group as well as the location to specify the scope of your deployment.", + "link": { + "label": "", + "uri": "" + } + } + }, + { + "name": "subscriptionApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "subscriptions?api-version=2020-01-01" + } + }, + { + "name": "subscriptionId", + "label": "Subscription", + "type": "Microsoft.Common.DropDown", + "visible": true, + "defaultValue": "", + "toolTip": "Select the Subscription for your deployment.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(steps('basics').deploymentDetails.subscriptionApi.value, (item) => parse(concat('{\"label\":\"', item.displayName, '\",\"value\":\"', item.id, '\",\"description\":\"', 'ID: ', item.subscriptionId, '\"}')))]", + "required": true + } + }, + { + "name": "resourceGroupApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/resourcegroups?api-version=2020-01-01')]" + } + }, + { + "name": "resourceGroupId", + "label": "Resource Group", + "type": "Microsoft.Common.DropDown", + "visible": true, + "defaultValue": "", + "toolTip": "Select the Resource Group for your deployment.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(steps('basics').deploymentDetails.resourceGroupApi.value, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Subscription ID: ', last(take(split(item.id, '/'), 3)), '\"}')))]", + "required": true + } + }, + { + "name": "infoBoxLocation", + "type": "Microsoft.Common.InfoBox", + "visible": true, + "options": { + "text": "Your Power Platform environment and Azure services must be in the same region.", + "style": "Info" + } + }, + { + "name": "locationsApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "locations?api-version=2019-11-01" + } + }, + { + "name": "locationName", + "label": "Location", + "type": "Microsoft.Common.DropDown", + "visible": true, + "defaultValue": "", + "toolTip": "Select the Location for your Deployment.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(steps('basics').deploymentDetails.locationsApi.value,(item) => parse(concat('{\"label\":\"', item.regionalDisplayName, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + } + } + ] + }, + { + "name": "deploymentName", + "label": "Deployment Name", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "deploymentNameText", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Specify a prefix and select an environment (Development, Test, Production) which will both be used as a prefix for all resource names. Independent of the environment, the same resources get deployed.", + "link": { + "label": "", + "uri": "" + } + } + }, + { + "name": "environment", + "label": "Environment", + "type": "Microsoft.Common.DropDown", + "visible": true, + "defaultValue": "Development", + "toolTip": "Select the environment for the deployment. This is currently only used for the naming of resources.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": [ + { + "label": "Development", + "description": "Select if you want to deploy a development environment.", + "value": "dev" + }, + { + "label": "Test", + "description": "Select if you want to deploy a test environment.", + "value": "tst" + }, + { + "label": "Production", + "description": "Select if you want to deploy a production environment.", + "value": "prd" + } + ], + "required": true + } + }, + { + "name": "deploymentPrefix", + "label": "Deployment Prefix", + "type": "Microsoft.Common.TextBox", + "visible": true, + "defaultValue": "", + "toolTip": "Specify a prefix (min 1 and max 10 lowercase characters and numbers).", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^[a-z0-9]{1,10}$", + "message": "The prefix must be between 1-10 lowercase characters and numbers." + }, + { + "isValid": "[not(equals(steps('basics').deploymentName.keyVaultNameApi.nameAvailable, false))]", + "message": "Prefix currently unavailable. Please choose a different one." + }, + { + "isValid": "[not(equals(steps('basics').deploymentName.storageAccountNameApi.nameAvailable, false))]", + "message": "Prefix currently unavailable. Please choose a different one." + }, + { + "isValid": "[not(equals(steps('basics').deploymentName.synapseNameApi.available, false))]", + "message": "Prefix currently unavailable. Please choose a different one." + } + ] + } + }, + { + "name": "keyVaultNameApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "POST", + "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.KeyVault/checkNameAvailability?api-version=2019-09-01')]", + "body": { + "name": "[concat(steps('basics').deploymentName.deploymentPrefix, '-', steps('basics').deploymentName.environment, '-vault001')]", + "type": "Microsoft.KeyVault/vaults" + } + } + }, + { + "name": "storageAccountNameApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "POST", + "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.Storage/checkNameAvailability?api-version=2021-04-01')]", + "body": { + "name": "[concat(steps('basics').deploymentName.deploymentPrefix, steps('basics').deploymentName.environment, 'storage001')]", + "type": "Microsoft.Storage/storageAccounts" + } + } + }, + { + "name": "synapseNameApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "POST", + "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.Synapse/checkNameAvailability?api-version=2021-03-01')]", + "body": { + "name": "[concat(steps('basics').deploymentName.deploymentPrefix, '-', steps('basics').deploymentName.environment, '-synapse001')]", + "type": "Microsoft.Synapse/workspaces" + } + } + } + ] + } + ] + }, + { + "name": "generalSettings", + "label": "General Settings", + "subLabel": { + "preValidation": "Provide settings for your deployment.", + "postValidation": "Done" + }, + "bladeTitle": "General Settings", + "bladeSubtitle": "General Settings", + "elements": [ + { + "name": "servicePrincipleSettings", + "label": "Service Principle Settings", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "servicePrincipleSettingsText", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Specify the Object IDs of the required Enterprise Applications.", + "link": { + "label": "", + "uri": "" + } + } + }, + { + "name": "powerPlatformServicePrincipalObjectId", + "label": { + "password": "Password", + "certificateThumbprint": "Certificate thumbprint", + "authenticationType": "Authentication Type", + "sectionHeader": "Power Platform Service Principal" + }, + "type": "Microsoft.Common.ServicePrincipalSelector", + "toolTip": { + "password": "Password", + "certificateThumbprint": "Certificate thumbprint", + "authenticationType": "Authentication Type" + }, + "defaultValue": { + "principalId": "f3b07414-6bf4-46e6-b63f-56941f3f4128", + "name": "Microsoft Power Query" + }, + "constraints": {}, + "options": { + "hideCertificate": true + }, + "visible": true + }, + { + "name": "dataverseServicePrincipalObjectId", + "label": { + "password": "Password", + "certificateThumbprint": "Certificate thumbprint", + "authenticationType": "Authentication Type", + "sectionHeader": "Dataverse Service Principal" + }, + "type": "Microsoft.Common.ServicePrincipalSelector", + "toolTip": { + "password": "Password", + "certificateThumbprint": "Certificate thumbprint", + "authenticationType": "Authentication Type" + }, + "defaultValue": { + "principalId": "7f15f9d9-cad0-44f1-bbba-d36650e07765", + "name": "Export to data lake" + }, + "constraints": {}, + "options": { + "hideCertificate": true + }, + "visible": true + } + ] + }, + { + "name": "synapseDeploymentSettings", + "label": "Synapse Deployment Settings", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "infoBoxsynapseDeploymentSettings", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Specify the settings for your Synapse workspace.", + "link": { + "label": "", + "uri": "" + } + } + }, + { + "name": "administratorPassword", + "label": { + "password": "Password", + "confirmPassword": "Confirm password" + }, + "type": "Microsoft.Compute.CredentialsCombo", + "visible": true, + "defaultValue": "", + "toolTip": { + "password": "Specify an administrator password for the Synapse workspace." + }, + "constraints": { + "required": true, + "customPasswordRegex": "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,128}$", + "customValidationMessage": "The password must be alphanumeric, contain at least 8 characters, and have at least 1 letter, 1 number and one special character." + }, + "options": { + "hideConfirmation": false + }, + "osPlatform": "Windows" + } + ] + }, + { + "name": "dataGovernanceSettings", + "label": "Data Governance Settings", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "dataGovernanceSettingsText", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Select the Purview account to which you want to connect the Synapse workspace.", + "link": { + "label": "Learn more", + "uri": "https://docs.microsoft.com/en-us/azure/purview/overview" + } + } + }, + { + "name": "purviewId", + "label": "Connect to Purview Account", + "type": "Microsoft.Solutions.ResourceSelector", + "visible": true, + "defaultValue": "", + "toolTip": "Select the Purview account to which you want to connect the Synapse workspace.", + "resourceType": "Microsoft.Purview/accounts", + "required": true, + "options": { + "filter": { + "subscription": "all", + "location": "all" + } + } + } + ] + }, + { + "name": "generalSettings", + "label": "General Settings", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "generalSettingsText", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Specify general settings for this deployment.", + "link": { + "label": "", + "uri": "" + } + } + }, + { + "name": "infoBoxRoleAssignment", + "type": "Microsoft.Common.InfoBox", + "visible": true, + "options": { + "text": "The role assignment is required for setting up a connection between Dataverse and Azure (Storage Account and Synapse). Multiple role assignment will be added to the storage account at account and container level. Please visit the link to learn more about the required role assignments.", + "style": "Info", + "uri": "https://github.com/microsoft/industry/blob/marvinbuss/dataverse-integration-template/healthcare/solutions/dataverseIntegration/README.md" + } + }, + { + "name": "enableRoleAssignments", + "label": "Enable role assignments", + "type": "Microsoft.Common.CheckBox", + "visible": true, + "defaultValue": false, + "toolTip": "Enable role assignments.", + "constraints": { + "required": false, + "validationMessage": "Enable role assignments of Power Platform and Dataverse Enterprise Applications. Please read infobox above for more details." + } + } + ] + } + ] + }, + { + "name": "connectivitySettings", + "label": "Connectivity Settings", + "subLabel": { + "preValidation": "Provide the connectivity settings that should be used for your deployment.", + "postValidation": "Done" + }, + "bladeTitle": "Connectivity Settings", + "bladeSubtitle": "Connectivity Settings", + "elements": [ + { + "name": "virtualNetwork", + "label": "Virtual Network", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "virtualNetworkText", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Select the Virtual Network and Subnet for your deployment.", + "link": { + "label": "", + "uri": "" + } + } + }, + { + "name": "infoBoxVirtualNetwork", + "type": "Microsoft.Common.InfoBox", + "visible": true, + "options": { + "text": "Please select a free Subnet within the Virtual Network with 'privateEndpointNetworkPolicies' and 'privateLinkServiceNetworkPolicies' set to disabled.", + "style": "Info" + } + }, + { + "name": "virtualNetworkApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.Network/virtualNetworks?api-version=2020-11-01')]" + } + }, + { + "name": "virtualNetworkId", + "label": "Virtual Network", + "type": "Microsoft.Common.DropDown", + "visible": true, + "defaultValue": "", + "toolTip": "Select the Virtual Network for your Deployment.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('connectivitySettings').virtualNetwork.virtualNetworkApi.value,(item) => equals(item.location, steps('basics').deploymentDetails.locationName)),(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + }, + { + "name": "subnetApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('connectivitySettings').virtualNetwork.virtualNetworkId, '/subnets?api-version=2020-11-01')]" + } + }, + { + "name": "subnetId", + "label": "Subnet", + "type": "Microsoft.Common.DropDown", + "visible": true, + "defaultValue": "", + "toolTip": "Select the Subnet for your Deployment. The subnet must have 'privateEndpointNetworkPolicies' and 'privateLinkServiceNetworkPolicies' set to disabled.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('connectivitySettings').virtualNetwork.subnetApi.value,(item) => and(equals(item.properties.privateEndpointNetworkPolicies, 'Disabled'), equals(item.properties.privateLinkServiceNetworkPolicies, 'Disabled'), lessOrEquals(int(last(split(item.properties.addressPrefix, '/'))), 27))),(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + } + ] + }, + { + "name": "privateDnsZones", + "label": "Private DNS Zones", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "privateDnsZonesText", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Select the Private DNS Zone settings for your deployment.", + "link": { + "label": "", + "uri": "" + } + } + }, + { + "name": "infoBoxPrivateDnsZone", + "type": "Microsoft.Common.InfoBox", + "visible": true, + "options": { + "text": "We are deploying all services with private endpoints and disabled public network access where possible to reduce the data exfiltration risk. For each private endpoint, DNS A-records need to be created in a Private DNS Zones. Therefore, these either need to deployed through Azure Policies or you have to provide the Private DNS Zones that should be used for this deployment. We are assuming that all Private DNS Zones are created in the same subscription. Deploying DNS A-Records through Private Endpoints is the recommended solution.", + "style": "Info", + "uri": "https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/private-link-and-dns-integration-at-scale" + } + }, + { + "name": "automatedPrivateDnsZoneGroups", + "label": "DNS A-Records are deployed through Azure Policy", + "type": "Microsoft.Common.OptionsGroup", + "visible": true, + "toolTip": "If 'No' is selected, you will have to choose private DNS Zones that will be used for the A-Record deployment of the private DNS Zones.", + "defaultValue": "Yes", + "constraints": { + "allowedValues": [ + { + "label": "Yes", + "value": "yes" + }, + { + "label": "No", + "value": "no" + } + ] + } + }, + { + "name": "subscriptionApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "subscriptions?api-version=2020-01-01" + } + }, + { + "name": "privateDnsZonesSub", + "label": "Private DNS Zone Subscription", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('connectivitySettings').privateDnsZones.automatedPrivateDnsZoneGroups, 'no')]", + "defaultValue": "", + "toolTip": "Select the Subscription of your Private DNS Zones.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(steps('connectivitySettings').privateDnsZones.subscriptionApi.value, (item) => parse(concat('{\"label\":\"', item.displayName, '\",\"value\":\"', item.id, '\",\"description\":\"', 'ID: ', item.subscriptionId, '\"}')))]", + "required": true + } + }, + { + "name": "privateDnsZonesApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('connectivitySettings').privateDnsZones.privateDnsZonesSub, '/providers/Microsoft.Network/privateDnsZones?api-version=2018-09-01')]" + } + }, + { + "name": "privateDnsZoneIdKeyVault", + "label": "Private DNS Zone Key Vault (privatelink.vaultcore.azure.net)", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('connectivitySettings').privateDnsZones.automatedPrivateDnsZoneGroups, 'no')]", + "defaultValue": "", + "toolTip": "Private DNS Zone for Key Vault (privatelink.vaultcore.azure.net).", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('connectivitySettings').privateDnsZones.privateDnsZonesApi.value,(item) => contains(item.name, 'privatelink.vaultcore.azure.net')),(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + }, + { + "name": "privateDnsZoneIdSynapseDev", + "label": "Private DNS Zone Synapse Dev (privatelink.dev.azuresynapse.net)", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('connectivitySettings').privateDnsZones.automatedPrivateDnsZoneGroups, 'no')]", + "defaultValue": "", + "toolTip": "Private DNS Zone for Synapse Dev (privatelink.dev.azuresynapse.net).", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('connectivitySettings').privateDnsZones.privateDnsZonesApi.value,(item) => contains(item.name, 'privatelink.dev.azuresynapse.net')),(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + }, + { + "name": "privateDnsZoneIdSynapseSql", + "label": "Private DNS Zone Synapse Sql (privatelink.sql.azuresynapse.net)", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('connectivitySettings').privateDnsZones.automatedPrivateDnsZoneGroups, 'no')]", + "defaultValue": "", + "toolTip": "Private DNS Zone for Synapse Sql (privatelink.sql.azuresynapse.net).", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('connectivitySettings').privateDnsZones.privateDnsZonesApi.value,(item) => contains(item.name, 'privatelink.sql.azuresynapse.net')),(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + }, + { + "name": "privateDnsZoneIdBlob", + "label": "Private DNS Zone Blob Storage (privatelink.blob.core.windows.net)", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('connectivitySettings').privateDnsZones.automatedPrivateDnsZoneGroups, 'no')]", + "defaultValue": "", + "toolTip": "Private DNS Zone for Blob Storage (privatelink.blob.core.windows.net).", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('connectivitySettings').privateDnsZones.privateDnsZonesApi.value,(item) => contains(item.name, 'privatelink.blob.core.windows.net')),(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + }, + { + "name": "privateDnsZoneIdDfs", + "label": "Private DNS Zone DFS Storage (privatelink.dfs.core.windows.net)", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('connectivitySettings').privateDnsZones.automatedPrivateDnsZoneGroups, 'no')]", + "defaultValue": "", + "toolTip": "Private DNS Zone for DFS Storage (privatelink.dfs.core.windows.net).", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('connectivitySettings').privateDnsZones.privateDnsZonesApi.value,(item) => contains(item.name, 'privatelink.dfs.core.windows.net')),(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + } + ] + } + ] + }, + { + "name": "tags", + "label": "Tags", + "subLabel": { + "preValidation": "Provide tags that will be used for all resources.", + "postValidation": "Done" + }, + "bladeTitle": "Tags", + "bladeSubtitle": "Tags", + "elements": [ + { + "name": "tagsByResource", + "label": "Tags by Resource", + "type": "Microsoft.Common.TagsByResource", + "visible": true, + "resources": [ + "DeploymentTags" + ] + } + ] + } + ] + }, + "outputs": { + "kind": "ResourceGroup", + "location": "[steps('basics').deploymentDetails.locationName]", + "resourceGroupId": "[steps('basics').deploymentDetails.resourceGroupId]", + "parameters": { + "location": "[if(empty(steps('basics').deploymentDetails.locationName), '', steps('basics').deploymentDetails.locationName)]", + "environment": "[if(empty(steps('basics').deploymentName.environment), '', steps('basics').deploymentName.environment)]", + "prefix": "[if(empty(steps('basics').deploymentName.deploymentPrefix), '', steps('basics').deploymentName.deploymentPrefix)]", + "administratorPassword": "[if(empty(steps('generalSettings').synapseDeploymentSettings.administratorPassword.password), '', steps('generalSettings').synapseDeploymentSettings.administratorPassword.password)]", + "powerPlatformServicePrincipalObjectId": "[if(empty(steps('generalSettings').servicePrincipleSettings.powerPlatformServicePrincipalObjectId.objectId), '', first(steps('generalSettings').servicePrincipleSettings.powerPlatformServicePrincipalObjectId.objectId))]", + "dataverseServicePrincipalObjectId": "[if(empty(steps('generalSettings').servicePrincipleSettings.dataverseServicePrincipalObjectId.objectId), '', first(steps('generalSettings').servicePrincipleSettings.dataverseServicePrincipalObjectId.objectId))]", + "purviewId": "[if(empty(steps('generalSettings').dataGovernanceSettings.purviewId.id), '', steps('generalSettings').dataGovernanceSettings.purviewId.id)]", + "enableRoleAssignments": "[steps('generalSettings').generalSettings.enableRoleAssignments]", + "subnetId": "[if(empty(steps('connectivitySettings').virtualNetwork.subnetId), '', steps('connectivitySettings').virtualNetwork.subnetId)]", + "privateDnsZoneIdBlob": "[if(empty(steps('connectivitySettings').privateDnsZones.privateDnsZoneIdBlob), '', steps('connectivitySettings').privateDnsZones.privateDnsZoneIdBlob)]", + "privateDnsZoneIdDfs": "[if(empty(steps('connectivitySettings').privateDnsZones.privateDnsZoneIdDfs), '', steps('connectivitySettings').privateDnsZones.privateDnsZoneIdDfs)]", + "privateDnsZoneIdKeyVault": "[if(empty(steps('connectivitySettings').privateDnsZones.privateDnsZoneIdKeyVault), '', steps('connectivitySettings').privateDnsZones.privateDnsZoneIdKeyVault)]", + "privateDnsZoneIdSynapseDev": "[if(empty(steps('connectivitySettings').privateDnsZones.privateDnsZoneIdSynapseDev), '', steps('connectivitySettings').privateDnsZones.privateDnsZoneIdSynapseDev)]", + "privateDnsZoneIdSynapseSql": "[if(empty(steps('connectivitySettings').privateDnsZones.privateDnsZoneIdSynapseSql), '', steps('connectivitySettings').privateDnsZones.privateDnsZoneIdSynapseSql)]", + "tags": "[if(not(contains(steps('tags').tagsByResource, 'DeploymentTags')), parse('{}'), first(map(parse(concat('[', string(steps('tags').tagsByResource), ']')), (item) => item.DeploymentTags)))]" + } + } + } +} \ No newline at end of file