From 57ce3755f0f9bc580fd21e1034a870100b9ccf8c Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Thu, 16 Sep 2021 12:16:56 +0200 Subject: [PATCH 01/22] Added Dataverse-Azure IaC Required Services --- .../solutions/dataverseIntegration/main.bicep | 103 +++ .../solutions/dataverseIntegration/main.json | 709 ++++++++++++++++++ .../synapseRoleAssignmentStorage.bicep | 37 + .../modules/services/keyvault.bicep | 86 +++ .../modules/services/synapse.bicep | 255 +++++++ .../dataverseIntegration/params.json | 42 ++ 6 files changed, 1232 insertions(+) create mode 100644 healthcare/solutions/dataverseIntegration/main.bicep create mode 100644 healthcare/solutions/dataverseIntegration/main.json create mode 100644 healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorage.bicep create mode 100644 healthcare/solutions/dataverseIntegration/modules/services/keyvault.bicep create mode 100644 healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep create mode 100644 healthcare/solutions/dataverseIntegration/params.json diff --git a/healthcare/solutions/dataverseIntegration/main.bicep b/healthcare/solutions/dataverseIntegration/main.bicep new file mode 100644 index 00000000..ca04d151 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/main.bicep @@ -0,0 +1,103 @@ +// 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. If you selected dataFactory as processingService, leave this value empty as is.') +param administratorPassword string = '' +@description('Specifies the resource ID of the default storage account file system for Synapse. If you selected dataFactory as processingService, leave this value empty as is.') +param synapseDefaultStorageAccountFileSystemId 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 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 synapseDefaultStorageAccountSubscriptionId = length(split(synapseDefaultStorageAccountFileSystemId, '/')) >= 13 ? split(synapseDefaultStorageAccountFileSystemId, '/')[2] : subscription().subscriptionId +var synapseDefaultStorageAccountResourceGroupName = length(split(synapseDefaultStorageAccountFileSystemId, '/')) >= 13 ? split(synapseDefaultStorageAccountFileSystemId, '/')[4] : resourceGroup().name +var keyvault001Name = '${name}-vault001' +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 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: synapseDefaultStorageAccountFileSystemId + } +} + +module synapse001RoleAssignmentStorage 'modules/auxiliary/synapseRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { + name: 'synapse001RoleAssignmentStorage' + scope: resourceGroup(synapseDefaultStorageAccountSubscriptionId, synapseDefaultStorageAccountResourceGroupName) + params: { + storageAccountFileSystemId: synapseDefaultStorageAccountFileSystemId + synapseId: synapse001.outputs.synapseId + } +} + +// Outputs diff --git a/healthcare/solutions/dataverseIntegration/main.json b/healthcare/solutions/dataverseIntegration/main.json new file mode 100644 index 00000000..a8181e29 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -0,0 +1,709 @@ +{ + "$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": "7708067349362611098" + } + }, + "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. If you selected dataFactory as processingService, leave this value empty as is." + } + }, + "synapseDefaultStorageAccountFileSystemId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specifies the resource ID of the default storage account file system for Synapse. If you selected dataFactory as processingService, leave this value empty as is." + } + }, + "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." + } + }, + "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", + "synapseDefaultStorageAccountSubscriptionId": "[if(greaterOrEquals(length(split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')), 13), split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')[2], subscription().subscriptionId)]", + "synapseDefaultStorageAccountResourceGroupName": "[if(greaterOrEquals(length(split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')), 13), split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')[4], resourceGroup().name)]", + "keyvault001Name": "[format('{0}-vault001', 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": "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": "[parameters('synapseDefaultStorageAccountFileSystemId')]" + } + }, + "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": "18364487249710460520" + } + }, + "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/sqlPools", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('synapseName'), 'sqlPool001')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "DW100c" + }, + "properties": { + "collation": "SQL_Latin1_General_CP1_CI_AS", + "createMode": "Default", + "storageAccountType": "GRS" + }, + "dependsOn": [ + "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" + ] + }, + { + "type": "Microsoft.Synapse/workspaces/bigDataPools", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('synapseName'), 'bigDataPool001')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "autoPause": { + "enabled": true, + "delayInMinutes": 15 + }, + "autoScale": { + "enabled": true, + "maxNodeCount": 10, + "minNodeCount": 3 + }, + "customLibraries": [], + "defaultSparkLogFolder": "logs/", + "dynamicExecutorAllocation": { + "enabled": true + }, + "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.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')]" + } + } + } + } + }, + { + "condition": "[parameters('enableRoleAssignments')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "synapse001RoleAssignmentStorage", + "subscriptionId": "[variables('synapseDefaultStorageAccountSubscriptionId')]", + "resourceGroup": "[variables('synapseDefaultStorageAccountResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountFileSystemId": { + "value": "[parameters('synapseDefaultStorageAccountFileSystemId')]" + }, + "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": "16578949331457096017" + } + }, + "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}', split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[0], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[1], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[2])]", + "name": "[guid(uniqueString(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[0], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[1], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[2]), 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', 'synapse001')]" + ] + } + ] +} \ No newline at end of file diff --git a/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorage.bicep b/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorage.bicep new file mode 100644 index 00000000..09f73473 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorage.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 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 storageAccountFileSystem 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-02-01' existing = { + name: '${storageAccountName}/default/${storageAccountFileSystemName}' +} + +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(storageAccountFileSystem.id, synapse.id)) + scope: storageAccountFileSystem + 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/synapse.bicep b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep new file mode 100644 index 00000000..d07b5f6a --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep @@ -0,0 +1,255 @@ +// 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 synapseSqlPool001 'Microsoft.Synapse/workspaces/sqlPools@2021-03-01' = { + parent: synapse + name: 'sqlPool001' + location: location + tags: tags + sku: { + name: 'DW100c' + } + properties: { + collation: 'SQL_Latin1_General_CP1_CI_AS' + createMode: 'Default' + storageAccountType: 'GRS' + } +} + +resource synapseBigDataPool001 'Microsoft.Synapse/workspaces/bigDataPools@2021-03-01' = { + parent: synapse + name: 'bigDataPool001' + location: location + tags: tags + properties: { + autoPause: { + enabled: true + delayInMinutes: 15 + } + autoScale: { + enabled: true + maxNodeCount: 10 + minNodeCount: 3 + } + // cacheSize: 100 // Uncomment to set a specific cache size + customLibraries: [] + defaultSparkLogFolder: 'logs/' + dynamicExecutorAllocation: { + enabled: true + } + // 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 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..b2c62516 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/params.json @@ -0,0 +1,42 @@ +{ + "$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": "dataverse" + }, + "tags": { + "value": {} + }, + "administratorPassword": { + "value": "" + }, + "synapseDefaultStorageAccountFileSystemId": { + "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Storage/storageAccounts/dataversedl/blobServices/default/containers/synapse" + }, + "purviewId": { + "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/synapse-rg/providers/Microsoft.Purview/accounts/marvtest" + }, + "enableRoleAssignments": { + "value": true + }, + "subnetId": { + "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/virtualNetworks/dataverse-vnet/subnets/default" + }, + "privateDnsZoneIdKeyVault": { + "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net" + }, + "privateDnsZoneIdSynapseDev": { + "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/privateDnsZones/privatelink.dev.azuresynapse.net" + }, + "privateDnsZoneIdSynapseSql": { + "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/privateDnsZones/privatelink.sql.azuresynapse.net" + } + } +} \ No newline at end of file From 01dcb1bf87b5720e1ddbe0090b1f0fcaccef2753 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Thu, 16 Sep 2021 16:12:11 +0200 Subject: [PATCH 02/22] removed synpase pool --- .../solutions/dataverseIntegration/main.json | 22 ++----------------- .../modules/services/synapse.bicep | 15 ------------- 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/main.json b/healthcare/solutions/dataverseIntegration/main.json index a8181e29..2883bf79 100644 --- a/healthcare/solutions/dataverseIntegration/main.json +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "7708067349362611098" + "templateHash": "15609941857550792068" } }, "parameters": { @@ -317,7 +317,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "18364487249710460520" + "templateHash": "13804182594025419947" } }, "parameters": { @@ -409,24 +409,6 @@ } } }, - { - "type": "Microsoft.Synapse/workspaces/sqlPools", - "apiVersion": "2021-03-01", - "name": "[format('{0}/{1}', parameters('synapseName'), 'sqlPool001')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "sku": { - "name": "DW100c" - }, - "properties": { - "collation": "SQL_Latin1_General_CP1_CI_AS", - "createMode": "Default", - "storageAccountType": "GRS" - }, - "dependsOn": [ - "[resourceId('Microsoft.Synapse/workspaces', parameters('synapseName'))]" - ] - }, { "type": "Microsoft.Synapse/workspaces/bigDataPools", "apiVersion": "2021-03-01", diff --git a/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep index d07b5f6a..5f806691 100644 --- a/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep +++ b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep @@ -59,21 +59,6 @@ resource synapse 'Microsoft.Synapse/workspaces@2021-03-01' = { } } -resource synapseSqlPool001 'Microsoft.Synapse/workspaces/sqlPools@2021-03-01' = { - parent: synapse - name: 'sqlPool001' - location: location - tags: tags - sku: { - name: 'DW100c' - } - properties: { - collation: 'SQL_Latin1_General_CP1_CI_AS' - createMode: 'Default' - storageAccountType: 'GRS' - } -} - resource synapseBigDataPool001 'Microsoft.Synapse/workspaces/bigDataPools@2021-03-01' = { parent: synapse name: 'bigDataPool001' From 82c7633cf6404c3d6f791bbccb4fc868a73b0c0b Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Fri, 17 Sep 2021 00:40:58 +0200 Subject: [PATCH 03/22] updated params --- .../solutions/dataverseIntegration/params.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/params.json b/healthcare/solutions/dataverseIntegration/params.json index b2c62516..e7c00940 100644 --- a/healthcare/solutions/dataverseIntegration/params.json +++ b/healthcare/solutions/dataverseIntegration/params.json @@ -9,7 +9,7 @@ "value": "dev" }, "prefix": { - "value": "dataverse" + "value": "mydverse" }, "tags": { "value": {} @@ -18,25 +18,25 @@ "value": "" }, "synapseDefaultStorageAccountFileSystemId": { - "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Storage/storageAccounts/dataversedl/blobServices/default/containers/synapse" + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Storage/storageAccounts/mydversedevstorage/blobServices/default/containers/synapse" }, "purviewId": { - "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/synapse-rg/providers/Microsoft.Purview/accounts/marvtest" + "value": "" }, "enableRoleAssignments": { "value": true }, "subnetId": { - "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/virtualNetworks/dataverse-vnet/subnets/default" + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/virtualNetworks/mydverse-dev-vnet/subnets/default" }, "privateDnsZoneIdKeyVault": { - "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net" + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net" }, "privateDnsZoneIdSynapseDev": { - "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/privateDnsZones/privatelink.dev.azuresynapse.net" + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.dev.azuresynapse.net" }, "privateDnsZoneIdSynapseSql": { - "value": "/subscriptions/558bd446-4212-46a2-908c-9ab0a628705e/resourceGroups/dataverse-rg/providers/Microsoft.Network/privateDnsZones/privatelink.sql.azuresynapse.net" + "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Network/privateDnsZones/privatelink.sql.azuresynapse.net" } } } \ No newline at end of file From f1d1f4029d8a5110664b6659bea153d4461a903d Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Tue, 21 Sep 2021 19:07:18 +0200 Subject: [PATCH 04/22] * Added Role Assignments to ARM/Bicep for PP * Added pwsh scripts for automating the setup of synch * Added role assignment templates * Updated ARM --- .../solutions/dataverseIntegration/Helper.ps1 | 344 ++++++ .../dataverseIntegration/SetupSynapseLink.ps1 | 164 +++ .../solutions/dataverseIntegration/main.bicep | 137 ++- .../solutions/dataverseIntegration/main.json | 1093 ++++++++++++++++- ...ervicePrincipalRoleAssignmentStorage.bicep | 37 + ...cipalRoleAssignmentStorageFileSystem.bicep | 48 + ...apseRoleAssignmentStorageFileSystem.bicep} | 18 +- .../modules/services/storage.bicep | 270 ++++ .../modules/services/synapse.bicep | 9 + 9 files changed, 2090 insertions(+), 30 deletions(-) create mode 100644 healthcare/solutions/dataverseIntegration/Helper.ps1 create mode 100644 healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 create mode 100644 healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep create mode 100644 healthcare/solutions/dataverseIntegration/modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep rename healthcare/solutions/dataverseIntegration/modules/auxiliary/{synapseRoleAssignmentStorage.bicep => synapseRoleAssignmentStorageFileSystem.bicep} (73%) create mode 100644 healthcare/solutions/dataverseIntegration/modules/services/storage.bicep diff --git a/healthcare/solutions/dataverseIntegration/Helper.ps1 b/healthcare/solutions/dataverseIntegration/Helper.ps1 new file mode 100644 index 00000000..33ff5079 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/Helper.ps1 @@ -0,0 +1,344 @@ +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 + ) + # 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}" + + # Define parameters based on input parameters + Write-Verbose "Defining parameters based on input parameters" + $azureAccessToken = (Get-AzAccessToken).Token + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${azureAccessToken}" + } + + # 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 { + 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 + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/lakedetails" + + # Define parameters based on input parameters + Write-Verbose "Defining parameters based on input parameters" + $azureAccessToken = (Get-AzAccessToken).Token + $tenantId = (Get-AzTenant).Id + $dataLakeSubscriptionId = $DataLakeId.Split("/")[2] + $dataLakeResourceGroupName = $DataLakeId.Split("/")[4] + $dataLakeName = $DataLakeId.Split("/")[8] + $dataLakeFileSystemName = $DataLakeId.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 ${azureAccessToken}" + } + + # 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 + + # 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 { + 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, "Settings": {}}, {"Type": "adx_ad", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false, "Settings": {}}] + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/lakedetails/${LakeDetailsId}" + + # Define parameters based on input parameters + Write-Verbose "Defining parameters based on input parameters" + $azureAccessToken = (Get-AzAccessToken).Token + $dataLakeName = $DataLakeId.Split("/")[8] + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${azureAccessToken}" + } + + # 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 + + # 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 { + 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 + ) + # Set Graph API URI + Write-Verbose "Setting Power Platform URI" + $powerPlatformUri = "https://athenawebservice.eus-il105.gateway.prod.island.powerapps.com/environment/${PowerPlatformEnvironmentId}/lakedetails/${LakeDetailsId}/activate" + + # Define parameters based on input parameters + Write-Verbose "Defining parameters based on input parameters" + $azureAccessToken = (Get-AzAccessToken).Token + + # Set header for REST call + Write-Verbose "Setting header for REST call" + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer ${azureAccessToken}" + } + + # 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 { + Write-Error "REST API call failed" + throw "REST API call failed" + } + return $response +} diff --git a/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 new file mode 100644 index 00000000..4fe9a50f --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 @@ -0,0 +1,164 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +# Define script arguments +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [String] + $PowerPlatformEnvironmentId, + + [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 + +New-AzSynapseRoleAssignment ` + -WorkspaceName $synapseName ` + -RoleDefinitionName "Workspace Admin" ` # More role details: "{"id": "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78", "isBuiltIn": true, "name": "Workspace Admin"}," + -ObjectId $exportDataLakeServicePrincipal.Id + +# Create File Systems on Data Lake +Write-Output "Creating File Systems on Data Lake" +$dataLakeSubscriptionId = $DataLakeId.Split("/")[2] +$dataLakeName = $DataLakeId.Split("/")[-1] +$powerPlatformContainerName = "power-platform-dataflows" +$dataverseContainerName = "dataverse" + +Set-AzContext ` + -Subscription $dataLakeSubscriptionId + +$context = New-AzStorageContext ` + -StorageAccountName $dataLakeName + +New-AzureStorageContainer ` + -Context $context ` + -Name $powerPlatformContainerName + +New-AzureStorageContainer ` + -Context $context ` + -Name $dataverseContainerName + +#Add Role Assignments for 'Export to data lake' Enterprise Application +Write-Output "Adding Role Assignments for 'Export to data lake' Enterprise Application" +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Owner" ` + -Scope $DataLakeId + +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Storage Blob Data Owner" ` + -Scope $DataLakeId + +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Storage Blob Data Contributor" ` + -Scope $DataLakeId + +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Storage Account Contributor" ` + -Scope $DataLakeId + +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Storage Account Contributor" ` + -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" + +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Owner" ` + -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" + +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Storage Blob Data Owner" ` + -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" + +New-AzRoleAssignment ` + -ObjectId $exportDataLakeServicePrincipal.Id ` + -RoleDefinitionId "Storage Blob Data Contributor" ` + -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" + +# Add Role Assignments for 'Microsoft Power Query' Enterprise Application +Write-Output "Adding Role Assignments for 'Microsoft Power Query' Enterprise Application" +New-AzRoleAssignment ` + -ObjectId $powerPlatformServicePrincipal.Id ` + -RoleDefinitionId "Reader and Data Access" ` + -Scope $DataLakeId + +New-AzRoleAssignment ` + -ObjectId $powerPlatformServicePrincipal.Id ` + -RoleDefinitionId "Storage Blob Data Owner" ` + -Scope "${DataLakeId}/blobServices/default/containers/${powerPlatformContainerName}" + +# Update Organization Details +Write-Output "Creating New Data Lake Details" +Update-OrganizationDetails ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -OrganizationUrl $OrganizationUrl ` + -OrganizationId $OrganizationId + +# Create New Data Lake Details +Write-Output "Creating New Data Lake Details" +$datalakeDetails = New-LakeDetails ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -DataLakeFileSystemId $DataverseDataLakeFileSystemId ` + -SynapseId $SynapseId +Write-Output "New Data Lake Details: '${datalakeDetails}'" + +# Create New Data Lake Profile +Write-Output "Creating New Data Lake Profile" +$datalakeProfile = New-LakeProfile ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -DataLakeFileSystemId $DataverseDataLakeFileSystemId ` + -LakeDetailsId $datalakeDetails.Id ` + -Entities $Entities +Write-Output "New Data Lake Profile: '${datalakeProfile}'" + +# Activate Lake Profile +Write-Output "Activating Lake Profile" +$datalakeProfileActivation = New-LakeProfileActivation ` + -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` + -LakeDetailsId $datalakeDetails.Id +Write-Output "New Data Lake Profile Activation: '${datalakeProfileActivation}'" diff --git a/healthcare/solutions/dataverseIntegration/main.bicep b/healthcare/solutions/dataverseIntegration/main.bicep index ca04d151..4bab4947 100644 --- a/healthcare/solutions/dataverseIntegration/main.bicep +++ b/healthcare/solutions/dataverseIntegration/main.bicep @@ -22,10 +22,12 @@ param tags object = {} // Resource parameters @secure() -@description('Specifies the administrator password of the sql servers in Synapse. If you selected dataFactory as processingService, leave this value empty as is.') +@description('Specifies the administrator password of the sql servers in Synapse.') param administratorPassword string = '' -@description('Specifies the resource ID of the default storage account file system for Synapse. If you selected dataFactory as processingService, leave this value empty as is.') -param synapseDefaultStorageAccountFileSystemId 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).') @@ -53,9 +55,8 @@ var tagsDefault = { } var tagsJoined = union(tagsDefault, tags) var administratorUsername = 'SqlServerMainUser' -var synapseDefaultStorageAccountSubscriptionId = length(split(synapseDefaultStorageAccountFileSystemId, '/')) >= 13 ? split(synapseDefaultStorageAccountFileSystemId, '/')[2] : subscription().subscriptionId -var synapseDefaultStorageAccountResourceGroupName = length(split(synapseDefaultStorageAccountFileSystemId, '/')) >= 13 ? split(synapseDefaultStorageAccountFileSystemId, '/')[4] : resourceGroup().name var keyvault001Name = '${name}-vault001' +var storage001Name = '${name}-storage001' var synapse001Name = '${name}-synapse001' // Resources @@ -71,6 +72,22 @@ module keyVault001 'modules/services/keyvault.bicep' = { } } +module storage001 'modules/services/storage.bicep' = { + name: 'storage001' + scope: resourceGroup() + params: { + location: location + tags: tagsJoined + fileSystemNames: [ + 'Synapse' + 'PowerPlatformDataFlows' + 'Dataverse' + ] + storageName: storage001Name + subnetId: subnetId + } +} + module synapse001 'modules/services/synapse.bicep' = { name: 'synapse001' scope: resourceGroup() @@ -87,17 +104,117 @@ module synapse001 'modules/services/synapse.bicep' = { privateDnsZoneIdSynapseSql: privateDnsZoneIdSynapseSql purviewId: purviewId synapseComputeSubnetId: '' - synapseDefaultStorageAccountFileSystemId: synapseDefaultStorageAccountFileSystemId + synapseDefaultStorageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0] } } -module synapse001RoleAssignmentStorage 'modules/auxiliary/synapseRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { - name: 'synapse001RoleAssignmentStorage' - scope: resourceGroup(synapseDefaultStorageAccountSubscriptionId, synapseDefaultStorageAccountResourceGroupName) +module synapse001RoleAssignmentStorageFileSystem 'modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { + name: 'synapse001RoleAssignmentStorageFileSystem' + scope: resourceGroup() params: { - storageAccountFileSystemId: synapseDefaultStorageAccountFileSystemId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0] synapseId: synapse001.outputs.synapseId } } +module powerPlatformRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { + 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) { + name: 'powerPlatformRoleAssignmentStorageFileSystem001' + scope: resourceGroup() + params: { + servicePrincipalObjectId: powerPlatformServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[1] + roleId: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner + } +} + +module dataverseRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { + 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) { + 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) { + 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) { + 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) { + name: 'dataverseRoleAssignmentStorageFileSystem001' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + roleId: '17d1049b-9a84-46fb-8f53-869881c3d3ab' // Storage Account Contributor + } +} + +module dataverseRoleAssignmentStorageFileSystem002 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { + name: 'dataverseRoleAssignmentStorageFileSystem002' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + roleId: '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' // Owner + } +} + +module dataverseRoleAssignmentStorageFileSystem003 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { + name: 'dataverseRoleAssignmentStorageFileSystem003' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + roleId: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner + } +} + +module dataverseRoleAssignmentStorageFileSystem004 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { + name: 'dataverseRoleAssignmentStorageFileSystem004' + scope: resourceGroup() + params: { + servicePrincipalObjectId: dataverseServicePrincipalObjectId + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + roleId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor + } +} + // Outputs diff --git a/healthcare/solutions/dataverseIntegration/main.json b/healthcare/solutions/dataverseIntegration/main.json index 2883bf79..4846f41c 100644 --- a/healthcare/solutions/dataverseIntegration/main.json +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "15609941857550792068" + "templateHash": "18109601729406099985" } }, "parameters": { @@ -46,14 +46,21 @@ "type": "secureString", "defaultValue": "", "metadata": { - "description": "Specifies the administrator password of the sql servers in Synapse. If you selected dataFactory as processingService, leave this value empty as is." + "description": "Specifies the administrator password of the sql servers in Synapse." } }, - "synapseDefaultStorageAccountFileSystemId": { + "powerPlatformServicePrincipalObjectId": { "type": "string", "defaultValue": "", "metadata": { - "description": "Specifies the resource ID of the default storage account file system for Synapse. If you selected dataFactory as processingService, leave this value empty as is." + "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": { @@ -109,9 +116,8 @@ }, "tagsJoined": "[union(variables('tagsDefault'), parameters('tags'))]", "administratorUsername": "SqlServerMainUser", - "synapseDefaultStorageAccountSubscriptionId": "[if(greaterOrEquals(length(split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')), 13), split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')[2], subscription().subscriptionId)]", - "synapseDefaultStorageAccountResourceGroupName": "[if(greaterOrEquals(length(split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')), 13), split(parameters('synapseDefaultStorageAccountFileSystemId'), '/')[4], resourceGroup().name)]", "keyvault001Name": "[format('{0}-vault001', variables('name'))]", + "storage001Name": "[format('{0}-storage001', variables('name'))]", "synapse001Name": "[format('{0}-synapse001', variables('name'))]" }, "resources": [ @@ -260,6 +266,322 @@ } } }, + { + "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", + "PowerPlatformDataFlows", + "Dataverse" + ] + }, + "storageName": { + "value": "[variables('storage001Name')]" + }, + "subnetId": { + "value": "[parameters('subnetId')]" + } + }, + "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", @@ -307,7 +629,7 @@ "value": "" }, "synapseDefaultStorageAccountFileSystemId": { - "value": "[parameters('synapseDefaultStorageAccountFileSystemId')]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[0]]" } }, "template": { @@ -317,7 +639,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "13804182594025419947" + "templateHash": "7918254326984888691" } }, "parameters": { @@ -468,6 +790,18 @@ "[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", @@ -621,15 +955,16 @@ } } } - } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]" + ] }, { "condition": "[parameters('enableRoleAssignments')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", - "name": "synapse001RoleAssignmentStorage", - "subscriptionId": "[variables('synapseDefaultStorageAccountSubscriptionId')]", - "resourceGroup": "[variables('synapseDefaultStorageAccountResourceGroupName')]", + "name": "synapse001RoleAssignmentStorageFileSystem", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -637,7 +972,7 @@ "mode": "Incremental", "parameters": { "storageAccountFileSystemId": { - "value": "[parameters('synapseDefaultStorageAccountFileSystemId')]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[0]]" }, "synapseId": { "value": "[reference(resourceId('Microsoft.Resources/deployments', 'synapse001'), '2019-10-01').outputs.synapseId.value]" @@ -650,7 +985,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "16578949331457096017" + "templateHash": "11818523926389760461" } }, "parameters": { @@ -673,8 +1008,8 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2020-04-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[0], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[1], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[2])]", - "name": "[guid(uniqueString(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[0], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[1], split(format('{0}/default/{1}', variables('storageAccountName'), variables('storageAccountFileSystemName')), '/')[2]), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('synapseSubscriptionId'), variables('synapseResourceGroupName')), 'Microsoft.Synapse/workspaces', variables('synapseName'))))]", + "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]" @@ -684,8 +1019,734 @@ } }, "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storage001')]", "[resourceId('Microsoft.Resources/deployments', 'synapse001')]" ] + }, + { + "condition": "[parameters('enableRoleAssignments')]", + "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": "[parameters('enableRoleAssignments')]", + "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]]" + }, + "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": "[parameters('enableRoleAssignments')]", + "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": "[parameters('enableRoleAssignments')]", + "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": "[parameters('enableRoleAssignments')]", + "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": "[parameters('enableRoleAssignments')]", + "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": "[parameters('enableRoleAssignments')]", + "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]]" + }, + "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": "[parameters('enableRoleAssignments')]", + "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]]" + }, + "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": "[parameters('enableRoleAssignments')]", + "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]]" + }, + "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": "[parameters('enableRoleAssignments')]", + "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]]" + }, + "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')]" + ] } ] } \ 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/synapseRoleAssignmentStorage.bicep b/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep similarity index 73% rename from healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorage.bicep rename to healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep index 09f73473..2447cd95 100644 --- a/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorage.bicep +++ b/healthcare/solutions/dataverseIntegration/modules/auxiliary/synapseRoleAssignmentStorageFileSystem.bicep @@ -16,8 +16,18 @@ var synapseResourceGroupName = length(split(synapseId, '/')) >= 9 ? split(synaps var synapseName = length(split(synapseId, '/')) >= 9 ? last(split(synapseId, '/')) : 'incorrectSegmentLength' // Resources -resource storageAccountFileSystem 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-02-01' existing = { - name: '${storageAccountName}/default/${storageAccountFileSystemName}' +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 = { @@ -26,8 +36,8 @@ resource synapse 'Microsoft.Synapse/workspaces@2021-03-01' existing = { } resource synapseRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { - name: guid(uniqueString(storageAccountFileSystem.id, synapse.id)) - scope: storageAccountFileSystem + name: guid(uniqueString(storageFileSystem.id, synapse.id)) + scope: storageFileSystem properties: { roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') principalId: synapse.identity.principalId 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 index 5f806691..064e5f27 100644 --- a/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep +++ b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep @@ -118,6 +118,15 @@ resource synapseAadAdministrators 'Microsoft.Synapse/workspaces/administrators@2 } } +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 From 019d25aa7af7aac3b98bbb53cc278b60f70df705 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Tue, 21 Sep 2021 19:09:55 +0200 Subject: [PATCH 05/22] * Added missing parameters * Removed old role assignments * Removed Container Creation in Script --- .../dataverseIntegration/SetupSynapseLink.ps1 | 83 ++----------------- 1 file changed, 8 insertions(+), 75 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 index 4fe9a50f..8e7f5059 100644 --- a/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 +++ b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 @@ -8,6 +8,14 @@ param ( [String] $PowerPlatformEnvironmentId, + [Parameter(Mandatory = $true)] + [String] + $OrganizationUrl, + + [Parameter(Mandatory = $true)] + [String] + $OrganizationId, + [Parameter(Mandatory = $true)] [String] $SynapseId, @@ -57,81 +65,6 @@ New-AzSynapseRoleAssignment ` -RoleDefinitionName "Workspace Admin" ` # More role details: "{"id": "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78", "isBuiltIn": true, "name": "Workspace Admin"}," -ObjectId $exportDataLakeServicePrincipal.Id -# Create File Systems on Data Lake -Write-Output "Creating File Systems on Data Lake" -$dataLakeSubscriptionId = $DataLakeId.Split("/")[2] -$dataLakeName = $DataLakeId.Split("/")[-1] -$powerPlatformContainerName = "power-platform-dataflows" -$dataverseContainerName = "dataverse" - -Set-AzContext ` - -Subscription $dataLakeSubscriptionId - -$context = New-AzStorageContext ` - -StorageAccountName $dataLakeName - -New-AzureStorageContainer ` - -Context $context ` - -Name $powerPlatformContainerName - -New-AzureStorageContainer ` - -Context $context ` - -Name $dataverseContainerName - -#Add Role Assignments for 'Export to data lake' Enterprise Application -Write-Output "Adding Role Assignments for 'Export to data lake' Enterprise Application" -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Owner" ` - -Scope $DataLakeId - -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Storage Blob Data Owner" ` - -Scope $DataLakeId - -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Storage Blob Data Contributor" ` - -Scope $DataLakeId - -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Storage Account Contributor" ` - -Scope $DataLakeId - -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Storage Account Contributor" ` - -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" - -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Owner" ` - -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" - -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Storage Blob Data Owner" ` - -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" - -New-AzRoleAssignment ` - -ObjectId $exportDataLakeServicePrincipal.Id ` - -RoleDefinitionId "Storage Blob Data Contributor" ` - -Scope "${DataLakeId}/blobServices/default/containers/${dataverseContainerName}" - -# Add Role Assignments for 'Microsoft Power Query' Enterprise Application -Write-Output "Adding Role Assignments for 'Microsoft Power Query' Enterprise Application" -New-AzRoleAssignment ` - -ObjectId $powerPlatformServicePrincipal.Id ` - -RoleDefinitionId "Reader and Data Access" ` - -Scope $DataLakeId - -New-AzRoleAssignment ` - -ObjectId $powerPlatformServicePrincipal.Id ` - -RoleDefinitionId "Storage Blob Data Owner" ` - -Scope "${DataLakeId}/blobServices/default/containers/${powerPlatformContainerName}" - # Update Organization Details Write-Output "Creating New Data Lake Details" Update-OrganizationDetails ` From 2e41c7f5b63db7ccb906a124369369c97b0234e0 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Tue, 21 Sep 2021 21:05:17 +0200 Subject: [PATCH 06/22] update linting --- .github/linters/.powershell-psscriptanalyzer.psd1 | 1 + 1 file changed, 1 insertion(+) 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 = @( ) } From e8686f9c282251d2dbc909255bf3e9592c7535ba Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Tue, 21 Sep 2021 21:26:36 +0200 Subject: [PATCH 07/22] removing unnecessary parameter --- healthcare/solutions/dataverseIntegration/params.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/params.json b/healthcare/solutions/dataverseIntegration/params.json index e7c00940..d9334d0a 100644 --- a/healthcare/solutions/dataverseIntegration/params.json +++ b/healthcare/solutions/dataverseIntegration/params.json @@ -17,9 +17,6 @@ "administratorPassword": { "value": "" }, - "synapseDefaultStorageAccountFileSystemId": { - "value": "/subscriptions/1f6c9a98-a387-420d-853c-9d27cfda0f33/resourceGroups/mabuss-dataverse/providers/Microsoft.Storage/storageAccounts/mydversedevstorage/blobServices/default/containers/synapse" - }, "purviewId": { "value": "" }, From 219f04bfbc1165a539003c61acf0e7eb20d39575 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Tue, 21 Sep 2021 23:59:15 +0200 Subject: [PATCH 08/22] Fixed bugs --- .../solutions/dataverseIntegration/main.bicep | 40 +++++++++--------- .../solutions/dataverseIntegration/main.json | 42 +++++++++---------- .../dataverseIntegration/params.json | 6 +++ 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/main.bicep b/healthcare/solutions/dataverseIntegration/main.bicep index 4bab4947..3a178d40 100644 --- a/healthcare/solutions/dataverseIntegration/main.bicep +++ b/healthcare/solutions/dataverseIntegration/main.bicep @@ -79,9 +79,9 @@ module storage001 'modules/services/storage.bicep' = { location: location tags: tagsJoined fileSystemNames: [ - 'Synapse' - 'PowerPlatformDataFlows' - 'Dataverse' + 'synapse' + 'power-platform-dataflows' + 'dataverse' ] storageName: storage001Name subnetId: subnetId @@ -104,7 +104,7 @@ module synapse001 'modules/services/synapse.bicep' = { privateDnsZoneIdSynapseSql: privateDnsZoneIdSynapseSql purviewId: purviewId synapseComputeSubnetId: '' - synapseDefaultStorageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0] + synapseDefaultStorageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0].storageFileSystemId } } @@ -112,12 +112,12 @@ module synapse001RoleAssignmentStorageFileSystem 'modules/auxiliary/synapseRoleA name: 'synapse001RoleAssignmentStorageFileSystem' scope: resourceGroup() params: { - storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0] + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[0].storageFileSystemId synapseId: synapse001.outputs.synapseId } } -module powerPlatformRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { +module powerPlatformRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(powerPlatformServicePrincipalObjectId)) { name: 'powerPlatformRoleAssignmentStorage001' scope: resourceGroup() params: { @@ -127,17 +127,17 @@ module powerPlatformRoleAssignmentStorage001 'modules/auxiliary/servicePrincipal } } -module powerPlatformRoleAssignmentStorageFileSystem001 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { +module powerPlatformRoleAssignmentStorageFileSystem001 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(powerPlatformServicePrincipalObjectId)) { name: 'powerPlatformRoleAssignmentStorageFileSystem001' scope: resourceGroup() params: { servicePrincipalObjectId: powerPlatformServicePrincipalObjectId - storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[1] + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[1].storageFileSystemId roleId: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner } } -module dataverseRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorage001' scope: resourceGroup() params: { @@ -147,7 +147,7 @@ module dataverseRoleAssignmentStorage001 'modules/auxiliary/servicePrincipalRole } } -module dataverseRoleAssignmentStorage002 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorage002 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorage002' scope: resourceGroup() params: { @@ -157,7 +157,7 @@ module dataverseRoleAssignmentStorage002 'modules/auxiliary/servicePrincipalRole } } -module dataverseRoleAssignmentStorage003 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorage003 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorage003' scope: resourceGroup() params: { @@ -167,7 +167,7 @@ module dataverseRoleAssignmentStorage003 'modules/auxiliary/servicePrincipalRole } } -module dataverseRoleAssignmentStorage004 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorage004 'modules/auxiliary/servicePrincipalRoleAssignmentStorage.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorage004' scope: resourceGroup() params: { @@ -177,42 +177,42 @@ module dataverseRoleAssignmentStorage004 'modules/auxiliary/servicePrincipalRole } } -module dataverseRoleAssignmentStorageFileSystem001 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorageFileSystem001 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorageFileSystem001' scope: resourceGroup() params: { servicePrincipalObjectId: dataverseServicePrincipalObjectId - storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId roleId: '17d1049b-9a84-46fb-8f53-869881c3d3ab' // Storage Account Contributor } } -module dataverseRoleAssignmentStorageFileSystem002 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorageFileSystem002 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorageFileSystem002' scope: resourceGroup() params: { servicePrincipalObjectId: dataverseServicePrincipalObjectId - storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId roleId: '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' // Owner } } -module dataverseRoleAssignmentStorageFileSystem003 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorageFileSystem003 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorageFileSystem003' scope: resourceGroup() params: { servicePrincipalObjectId: dataverseServicePrincipalObjectId - storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId roleId: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner } } -module dataverseRoleAssignmentStorageFileSystem004 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments) { +module dataverseRoleAssignmentStorageFileSystem004 'modules/auxiliary/servicePrincipalRoleAssignmentStorageFileSystem.bicep' = if(enableRoleAssignments && !empty(dataverseServicePrincipalObjectId)) { name: 'dataverseRoleAssignmentStorageFileSystem004' scope: resourceGroup() params: { servicePrincipalObjectId: dataverseServicePrincipalObjectId - storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2] + storageAccountFileSystemId: storage001.outputs.storageFileSystemIds[2].storageFileSystemId roleId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor } } diff --git a/healthcare/solutions/dataverseIntegration/main.json b/healthcare/solutions/dataverseIntegration/main.json index 4846f41c..6b111de1 100644 --- a/healthcare/solutions/dataverseIntegration/main.json +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "18109601729406099985" + "templateHash": "4494666277324552804" } }, "parameters": { @@ -284,9 +284,9 @@ }, "fileSystemNames": { "value": [ - "Synapse", - "PowerPlatformDataFlows", - "Dataverse" + "synapse", + "power-platform-dataflows", + "dataverse" ] }, "storageName": { @@ -629,7 +629,7 @@ "value": "" }, "synapseDefaultStorageAccountFileSystemId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[0]]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[0].storageFileSystemId]" } }, "template": { @@ -972,7 +972,7 @@ "mode": "Incremental", "parameters": { "storageAccountFileSystemId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[0]]" + "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]" @@ -1024,7 +1024,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('powerPlatformServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "powerPlatformRoleAssignmentStorage001", @@ -1096,7 +1096,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('powerPlatformServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "powerPlatformRoleAssignmentStorageFileSystem001", @@ -1110,7 +1110,7 @@ "value": "[parameters('powerPlatformServicePrincipalObjectId')]" }, "storageAccountFileSystemId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[1]]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[1].storageFileSystemId]" }, "roleId": { "value": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b" @@ -1169,7 +1169,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorage001", @@ -1241,7 +1241,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorage002", @@ -1313,7 +1313,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorage003", @@ -1385,7 +1385,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorage004", @@ -1457,7 +1457,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorageFileSystem001", @@ -1471,7 +1471,7 @@ "value": "[parameters('dataverseServicePrincipalObjectId')]" }, "storageAccountFileSystemId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2]]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" }, "roleId": { "value": "17d1049b-9a84-46fb-8f53-869881c3d3ab" @@ -1530,7 +1530,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorageFileSystem002", @@ -1544,7 +1544,7 @@ "value": "[parameters('dataverseServicePrincipalObjectId')]" }, "storageAccountFileSystemId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2]]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" }, "roleId": { "value": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" @@ -1603,7 +1603,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorageFileSystem003", @@ -1617,7 +1617,7 @@ "value": "[parameters('dataverseServicePrincipalObjectId')]" }, "storageAccountFileSystemId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2]]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" }, "roleId": { "value": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b" @@ -1676,7 +1676,7 @@ ] }, { - "condition": "[parameters('enableRoleAssignments')]", + "condition": "[and(parameters('enableRoleAssignments'), not(empty(parameters('dataverseServicePrincipalObjectId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2019-10-01", "name": "dataverseRoleAssignmentStorageFileSystem004", @@ -1690,7 +1690,7 @@ "value": "[parameters('dataverseServicePrincipalObjectId')]" }, "storageAccountFileSystemId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2]]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage001'), '2019-10-01').outputs.storageFileSystemIds.value[2].storageFileSystemId]" }, "roleId": { "value": "ba92f5b4-2d11-453d-a403-e96b0029c9fe" diff --git a/healthcare/solutions/dataverseIntegration/params.json b/healthcare/solutions/dataverseIntegration/params.json index d9334d0a..b2051fa2 100644 --- a/healthcare/solutions/dataverseIntegration/params.json +++ b/healthcare/solutions/dataverseIntegration/params.json @@ -17,6 +17,12 @@ "administratorPassword": { "value": "" }, + "powerPlatformServicePrincipalObjectId": { + "value": "" + }, + "dataverseServicePrincipalObjectId": { + "value": "" + }, "purviewId": { "value": "" }, From 2b3bb858ba8384cb919b010964c38bbe6471ed95 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Wed, 22 Sep 2021 14:11:58 +0200 Subject: [PATCH 09/22] fix spark pool bug --- .../solutions/dataverseIntegration/main.json | 14 ++++++++------ .../modules/services/synapse.bicep | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/main.json b/healthcare/solutions/dataverseIntegration/main.json index 6b111de1..702e90b5 100644 --- a/healthcare/solutions/dataverseIntegration/main.json +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "4494666277324552804" + "templateHash": "8829416978934135310" } }, "parameters": { @@ -639,7 +639,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "7918254326984888691" + "templateHash": "13182141804118079952" } }, "parameters": { @@ -733,7 +733,7 @@ }, { "type": "Microsoft.Synapse/workspaces/bigDataPools", - "apiVersion": "2021-03-01", + "apiVersion": "2021-05-01", "name": "[format('{0}/{1}', parameters('synapseName'), 'bigDataPool001')]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", @@ -744,13 +744,15 @@ }, "autoScale": { "enabled": true, - "maxNodeCount": 10, - "minNodeCount": 3 + "minNodeCount": 3, + "maxNodeCount": 10 }, "customLibraries": [], "defaultSparkLogFolder": "logs/", "dynamicExecutorAllocation": { - "enabled": true + "enabled": true, + "minExecutors": 1, + "maxExecutors": 9 }, "nodeSize": "Small", "nodeSizeFamily": "MemoryOptimized", diff --git a/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep index 064e5f27..cf64fd83 100644 --- a/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep +++ b/healthcare/solutions/dataverseIntegration/modules/services/synapse.bicep @@ -59,7 +59,7 @@ resource synapse 'Microsoft.Synapse/workspaces@2021-03-01' = { } } -resource synapseBigDataPool001 'Microsoft.Synapse/workspaces/bigDataPools@2021-03-01' = { +resource synapseBigDataPool001 'Microsoft.Synapse/workspaces/bigDataPools@2021-05-01' = { parent: synapse name: 'bigDataPool001' location: location @@ -71,14 +71,16 @@ resource synapseBigDataPool001 'Microsoft.Synapse/workspaces/bigDataPools@2021-0 } autoScale: { enabled: true - maxNodeCount: 10 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 From 13e2c5c57f062f725dff5219bf325d49e4b69b54 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Wed, 22 Sep 2021 14:46:02 +0200 Subject: [PATCH 10/22] add outputs for seamless deployment across clouds --- .../solutions/dataverseIntegration/main.bicep | 2 ++ .../solutions/dataverseIntegration/main.json | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/main.bicep b/healthcare/solutions/dataverseIntegration/main.bicep index 3a178d40..e80cc8d2 100644 --- a/healthcare/solutions/dataverseIntegration/main.bicep +++ b/healthcare/solutions/dataverseIntegration/main.bicep @@ -218,3 +218,5 @@ module dataverseRoleAssignmentStorageFileSystem004 'modules/auxiliary/servicePri } // 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 index 702e90b5..319250d6 100644 --- a/healthcare/solutions/dataverseIntegration/main.json +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "8829416978934135310" + "templateHash": "15370992525510895090" } }, "parameters": { @@ -1750,5 +1750,15 @@ "[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 From 9de0afa6678291dce7188d8ccbc498094bab8a58 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Thu, 23 Sep 2021 14:19:57 +0200 Subject: [PATCH 11/22] * Added additional logging for troubleshooting * Added function for creating file systems * fixed bugs in workflow * Generate Access Token with correct scope * Added some sleep in between actions because of backend processes --- .../solutions/dataverseIntegration/Helper.ps1 | 179 +++++++++++++++--- .../dataverseIntegration/SetupSynapseLink.ps1 | 31 ++- 2 files changed, 178 insertions(+), 32 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/Helper.ps1 b/healthcare/solutions/dataverseIntegration/Helper.ps1 index 33ff5079..3bb77495 100644 --- a/healthcare/solutions/dataverseIntegration/Helper.ps1 +++ b/healthcare/solutions/dataverseIntegration/Helper.ps1 @@ -32,29 +32,126 @@ function Update-OrganizationDetails { [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] - $OrganizationId + $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" - $azureAccessToken = (Get-AzAccessToken).Token + $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 ${azureAccessToken}" + "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" } @@ -65,7 +162,11 @@ function Update-OrganizationDetails { Write-Verbose "Response: ${response}" } catch { - Write-Error "REST API call failed" + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } throw "REST API call failed" } return $response @@ -105,20 +206,25 @@ function New-LakeDetails { [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] - $SynapseId + $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" - $azureAccessToken = (Get-AzAccessToken).Token $tenantId = (Get-AzTenant).Id - $dataLakeSubscriptionId = $DataLakeId.Split("/")[2] - $dataLakeResourceGroupName = $DataLakeId.Split("/")[4] - $dataLakeName = $DataLakeId.Split("/")[8] - $dataLakeFileSystemName = $DataLakeId.Split("/")[-1] + $dataLakeSubscriptionId = $DataLakeFileSystemId.Split("/")[2] + $dataLakeResourceGroupName = $DataLakeFileSystemId.Split("/")[4] + $dataLakeName = $DataLakeFileSystemId.Split("/")[8] + $dataLakeFileSystemName = $DataLakeFileSystemId.Split("/")[-1] $synapseSubscriptionId = $SynapseId.Split("/")[2] $synapseResourceGroupName = $SynapseId.Split("/")[4] @@ -133,7 +239,7 @@ function New-LakeDetails { Write-Verbose "Setting header for REST call" $headers = @{ "Content-Type" = "application/json" - "Authorization" = "Bearer ${azureAccessToken}" + "Authorization" = "Bearer ${AccessToken}" } # Set body for REST call @@ -153,6 +259,7 @@ function New-LakeDetails { "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" @@ -171,7 +278,11 @@ function New-LakeDetails { Write-Verbose "Response: ${response}" } catch { - Write-Error "REST API call failed" + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } throw "REST API call failed" } return $response @@ -220,22 +331,27 @@ function New-LakeProfile { [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Array] - $Entities # Sample Input: [{"Type": "msdyn_actual", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false, "Settings": {}}, {"Type": "adx_ad", "RecordCountPerBlock": 0, "PartitionStrategy": "Month", "AppendOnlyMode": false, "Settings": {}}] + $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}/lakedetails/${LakeDetailsId}" + $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" - $azureAccessToken = (Get-AzAccessToken).Token - $dataLakeName = $DataLakeId.Split("/")[8] + $dataLakeName = $DataLakeFileSystemId.Split("/")[8] # Set header for REST call Write-Verbose "Setting header for REST call" $headers = @{ "Content-Type" = "application/json" - "Authorization" = "Bearer ${azureAccessToken}" + "Authorization" = "Bearer ${AccessToken}" } # Set body for REST call @@ -252,6 +368,7 @@ function New-LakeProfile { "SchedulerIntervalInMinutes" = 60 "WriteDeleteLog" = $true } | ConvertTo-Json + Write-Verbose "Body: '${body}'" # Define parameters for REST method Write-Verbose "Defining parameters for pscore method" @@ -270,7 +387,11 @@ function New-LakeProfile { Write-Verbose "Response: ${response}" } catch { - Write-Error "REST API call failed" + if($_.ErrorDetails.Message) { + Write-Error $_.ErrorDetails.Message; + } else { + Write-Error "REST API call failed" + } throw "REST API call failed" } return $response @@ -304,21 +425,23 @@ function New-LakeProfileActivation { [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] - $LakeDetailsId + $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}/lakedetails/${LakeDetailsId}/activate" - - # Define parameters based on input parameters - Write-Verbose "Defining parameters based on input parameters" - $azureAccessToken = (Get-AzAccessToken).Token + $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 ${azureAccessToken}" + "Authorization" = "Bearer ${AccessToken}" } # Define parameters for REST method @@ -337,7 +460,11 @@ function New-LakeProfileActivation { Write-Verbose "Response: ${response}" } catch { - Write-Error "REST API call failed" + 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/SetupSynapseLink.ps1 b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 index 8e7f5059..364f9b09 100644 --- a/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 +++ b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 @@ -60,38 +60,57 @@ $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 "Workspace Admin" ` # More role details: "{"id": "6e4bf58a-b8e1-4cc3-bbf9-d73143322b78", "isBuiltIn": true, "name": "Workspace Admin"}," + -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 "Creating New Data Lake Details" +Write-Output "Updating Organization Details" Update-OrganizationDetails ` -PowerPlatformEnvironmentId $PowerPlatformEnvironmentId ` -OrganizationUrl $OrganizationUrl ` - -OrganizationId $OrganizationId + -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 + -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-Host "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 + -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-Host "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 + -LakeDetailsId $datalakeDetails.Id ` + -AccessToken $powerAppAccessToken Write-Output "New Data Lake Profile Activation: '${datalakeProfileActivation}'" From 455a42b340f03df7d346bb87ba2ca011f4dddae4 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Thu, 23 Sep 2021 18:48:01 +0200 Subject: [PATCH 12/22] Update docs --- .../solutions/dataverseIntegration/README.md | 129 ++++++++++++++++++ .../docs/media/AzureResources.png | Bin 0 -> 60046 bytes .../docs/media/AzureStorage.png | Bin 0 -> 40696 bytes .../docs/media/AzureSynapse.png | Bin 0 -> 32461 bytes .../docs/media/AzureSynapseLinkConnection.png | Bin 0 -> 13024 bytes 5 files changed, 129 insertions(+) create mode 100644 healthcare/solutions/dataverseIntegration/README.md create mode 100644 healthcare/solutions/dataverseIntegration/docs/media/AzureResources.png create mode 100644 healthcare/solutions/dataverseIntegration/docs/media/AzureStorage.png create mode 100644 healthcare/solutions/dataverseIntegration/docs/media/AzureSynapse.png create mode 100644 healthcare/solutions/dataverseIntegration/docs/media/AzureSynapseLinkConnection.png diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md new file mode 100644 index 00000000..7e53ca9b --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -0,0 +1,129 @@ +# Link 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 comments + +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 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. + +## Accelerator + +To accelerate the integration of datasets between Dataverse and a data platform, an accelerator has been developed to set this up much more quickly. The accelerator 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 accelerator 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. + +### Deploy To Azure + +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 Azure](https://aka.ms/deploytoazurebutton)](#) + +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. + +### Connect Azure Services and Dataverse + +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/docs/media/AzureResources.png b/healthcare/solutions/dataverseIntegration/docs/media/AzureResources.png new file mode 100644 index 0000000000000000000000000000000000000000..9339595afa1af57b2c12e39067653168287e4815 GIT binary patch literal 60046 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 +fGx8~@Oq+|B&grOh}VP6q2VT^7D^Bczkr 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 literal 0 HcmV?d00001 diff --git a/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapseLinkConnection.png b/healthcare/solutions/dataverseIntegration/docs/media/AzureSynapseLinkConnection.png new file mode 100644 index 0000000000000000000000000000000000000000..fc0149a60b602ee4f6c594ececf4ec72277a8c38 GIT binary patch literal 13024 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 Date: Thu, 23 Sep 2021 18:53:19 +0200 Subject: [PATCH 13/22] linting --- healthcare/solutions/dataverseIntegration/Helper.ps1 | 2 +- healthcare/solutions/dataverseIntegration/README.md | 2 +- .../solutions/dataverseIntegration/SetupSynapseLink.ps1 | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/Helper.ps1 b/healthcare/solutions/dataverseIntegration/Helper.ps1 index 3bb77495..a0f7011d 100644 --- a/healthcare/solutions/dataverseIntegration/Helper.ps1 +++ b/healthcare/solutions/dataverseIntegration/Helper.ps1 @@ -117,7 +117,7 @@ function New-FileSystem { 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 diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md index 7e53ca9b..444273c6 100644 --- a/healthcare/solutions/dataverseIntegration/README.md +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -54,7 +54,7 @@ Also, the accelerator includes a set of powershell scripts to automate the first 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 Azure](https://aka.ms/deploytoazurebutton)](#) +[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](#TODO) 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. diff --git a/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 index 364f9b09..43becf6c 100644 --- a/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 +++ b/healthcare/solutions/dataverseIntegration/SetupSynapseLink.ps1 @@ -89,7 +89,7 @@ Write-Output "New Data Lake Details: '${datalakeDetails}'" # Sleep for X Seconds to give the Backend Process some time to Finish $seconds = 10 -Write-Host "Sleeping for ${seconds} Seconds to give the Backend Process some time to Finish" +Write-Output "Sleeping for ${seconds} Seconds to give the Backend Process some time to Finish" Start-Sleep -Seconds $seconds # Create New Data Lake Profile @@ -104,7 +104,7 @@ Write-Output "New Data Lake Profile: '${datalakeProfile}'" # Sleep for X Seconds to give the Backend Process some time to Finish $seconds = 10 -Write-Host "Sleeping for ${seconds} Seconds to give the Backend Process some time to Finish" +Write-Output "Sleeping for ${seconds} Seconds to give the Backend Process some time to Finish" Start-Sleep -Seconds $seconds # Activate Lake Profile From 4261f1a0d1000395155978b601a474ba64859fc7 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Mon, 27 Sep 2021 20:45:33 +0200 Subject: [PATCH 14/22] Updated to reference implementation --- healthcare/solutions/dataverseIntegration/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md index 444273c6..3a1ae1f1 100644 --- a/healthcare/solutions/dataverseIntegration/README.md +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -41,14 +41,14 @@ The Storage Account, the Synapse Workspace and the Power Platform Environment mu The user creating the connection requires Owner or User Access Administrator rights on the two Azure resources 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. -## Accelerator +## Reference Implementations -To accelerate the integration of datasets between Dataverse and a data platform, an accelerator has been developed to set this up much more quickly. The accelerator consists of Infrastructure as Code (IaC) templates and a "Deploy To Azure" Button to setup everything related to Azure including the following: +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 accelerator 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. +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. ### Deploy To Azure From b2347640fac91f331fb9689bdf8d1ff343bf9e36 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Wed, 29 Sep 2021 09:17:27 +0200 Subject: [PATCH 15/22] Updated private dns --- .../solutions/dataverseIntegration/main.bicep | 6 +++++ .../solutions/dataverseIntegration/main.json | 22 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/healthcare/solutions/dataverseIntegration/main.bicep b/healthcare/solutions/dataverseIntegration/main.bicep index e80cc8d2..ccfd70b4 100644 --- a/healthcare/solutions/dataverseIntegration/main.bicep +++ b/healthcare/solutions/dataverseIntegration/main.bicep @@ -38,6 +38,10 @@ param enableRoleAssignments bool = false 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.') @@ -85,6 +89,8 @@ module storage001 'modules/services/storage.bicep' = { ] storageName: storage001Name subnetId: subnetId + privateDnsZoneIdBlob: privateDnsZoneIdBlob + privateDnsZoneIdDfs: privateDnsZoneIdDfs } } diff --git a/healthcare/solutions/dataverseIntegration/main.json b/healthcare/solutions/dataverseIntegration/main.json index 319250d6..33490576 100644 --- a/healthcare/solutions/dataverseIntegration/main.json +++ b/healthcare/solutions/dataverseIntegration/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.4.613.9944", - "templateHash": "15370992525510895090" + "templateHash": "7729186315494720054" } }, "parameters": { @@ -83,6 +83,20 @@ "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": "", @@ -294,6 +308,12 @@ }, "subnetId": { "value": "[parameters('subnetId')]" + }, + "privateDnsZoneIdBlob": { + "value": "[parameters('privateDnsZoneIdBlob')]" + }, + "privateDnsZoneIdDfs": { + "value": "[parameters('privateDnsZoneIdDfs')]" } }, "template": { From 0fde0203c358fc4b9b8469bea4dac17e9cece7b3 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Wed, 29 Sep 2021 09:18:26 +0200 Subject: [PATCH 16/22] added portal exp --- .../dataverseIntegration/portal.json | 721 ++++++++++++++++++ 1 file changed, 721 insertions(+) create mode 100644 healthcare/solutions/dataverseIntegration/portal.json diff --git a/healthcare/solutions/dataverseIntegration/portal.json b/healthcare/solutions/dataverseIntegration/portal.json new file mode 100644 index 00000000..d15745b6 --- /dev/null +++ b/healthcare/solutions/dataverseIntegration/portal.json @@ -0,0 +1,721 @@ +{ + "$schema": "", + "view": { + "kind": "Form", + "properties": { + "title": "Dataverse ", + "steps": [ + { + "name": "basics", + "label": "Data Product", + "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": "Since not all service features are available in all regions, Enterprise Scale Analytics is available in a subset of regions.", + "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 Data Product.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(filter(steps('basics').deploymentDetails.locationsApi.value,(item) => contains(split('southafricanorth,southeastasia,japaneast,canadacentral,northeurope,westeurope,francecentral,germanywestcentral,uksouth,centralus,eastus,eastus2,southcentralus,westus2', ','), item.name)),(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": "dataProductPrefix", + "label": "Data Product 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').dataProductName.keyVaultNameApi.nameAvailable, false))]", + "message": "Prefix currently unavailable. Please choose a different one." + }, + { + "isValid": "[not(equals(steps('basics').dataProductName.storageAccountNameApi.nameAvailable, false))]", + "message": "Prefix currently unavailable. Please choose a different one." + }, + { + "isValid": "[not(equals(steps('basics').dataProductName.containerRegistryNameApi.nameAvailable, false))]", + "message": "Prefix currently unavailable. Please choose a different one." + }, + { + "isValid": "[not(equals(steps('basics').dataProductName.synapseNameApi.available, false))]", + "message": "Prefix currently unavailable. Please choose a different one." + }, + { + "isValid": "[not(equals(steps('basics').dataProductName.searchNameApi.nameAvailable, 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').dataProductName.dataProductPrefix, '-', steps('basics').dataProductName.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').dataProductName.dataProductPrefix, steps('basics').dataProductName.environment, 'storage001')]", + "type": "Microsoft.Storage/storageAccounts" + } + } + }, + { + "name": "containerRegistryNameApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "POST", + "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.ContainerRegistry/checkNameAvailability?api-version=2019-05-01')]", + "body": { + "name": "[concat(steps('basics').dataProductName.dataProductPrefix, steps('basics').dataProductName.environment, 'containerregistry001')]", + "type": "Microsoft.ContainerRegistry/registries" + } + } + }, + { + "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').dataProductName.dataProductPrefix, '-', steps('basics').dataProductName.environment, '-synapse001')]", + "type": "Microsoft.Synapse/workspaces" + } + } + }, + { + "name": "searchNameApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "POST", + "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.Search/checkNameAvailability?api-version=2021-04-01-preview')]", + "body": { + "name": "[concat(steps('basics').dataProductName.dataProductPrefix, '-', steps('basics').dataProductName.environment, '-search001')]", + "type": "searchServices" + } + } + } + ] + } + ] + }, + { + "name": "generalSettings", + "label": "General Settings", + "subLabel": { + "preValidation": "Provide settings for your data product 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": "Learn more", + "uri": "" + } + } + } + ] + }, + { + "name": "synapseDeploymentSettings", + "label": "Synapse Deployment Settings", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "infoBoxProcessingService", + "type": "Microsoft.Common.InfoBox", + "visible": true, + "options": { + "text": "Specify the settings for your Synapse workspace.", + "style": "Info" + } + }, + { + "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 or Data Factory.", + "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 Synapse or Data Factory.", + "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 optional and will assign the Service Principle of the Power Platform Enterprise Application and the Service Principle of the Dataverse Enterprise Application multiple roles on the Data Lake. Please visit the link to learn more about the required role assignments.", + "style": "Info" + } + }, + { + "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 Machine Learning MSI and Synapse MSI. Please read infobox above for more details." + } + } + ] + } + ] + }, + { + "name": "connectivitySettings", + "label": "Connectivity Settings", + "subLabel": { + "preValidation": "Provide the connectivity settings that should be used for the Data Product.", + "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 the Virtual Network of your Data Landing Zone and select a free Subnet within the Virtual Network to make sure that you have proper outbound connectivity as well as DNS name resolution in place. The subnet must have 'privateEndpointNetworkPolicies' and 'privateLinkServiceNetworkPolicies' set to disabled. This is already pre-configured for you in the DataProduct and DataIntegration subnets in the Data Landing Zone Vnet. In addition, Azure Machine Learning requires a subnet with the 'Microsoft.Storage' service endpoint enabled in the region of the Data Product deployment. This is not enabled by default and therefore must be setup manually.", + "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 Data Product.", + "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 Data Product. 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": [ + "EnterpriseScaleAnalytics" + ] + } + ] + } + ] + }, + "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').dataProductName.environment), '', steps('basics').dataProductName.environment)]", + "prefix": "[if(empty(steps('basics').dataProductName.dataProductPrefix), '', steps('basics').dataProductName.dataProductPrefix)]", + "administratorPassword": "[if(empty(steps('generalSettings').synapseDeploymentSettings.administratorPassword.password), '', steps('generalSettings').synapseDeploymentSettings.administratorPassword.password)]", + "powerPlatformServicePrincipalObjectId": "[if(empty(steps('generalSettings').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId), '', steps('generalSettings').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId)]", + "dataverseServicePrincipalObjectId": "[if(empty(steps('generalSettings').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId), '', steps('generalSettings').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId)]", + "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, 'EnterpriseScaleAnalytics')), parse('{}'), first(map(parse(concat('[', string(steps('tags').tagsByResource), ']')), (item) => item.EnterpriseScaleAnalytics)))]" + } + } + } +} \ No newline at end of file From dae400d8beebd2688961dcc5fee2ee3227a38e95 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Thu, 30 Sep 2021 17:49:41 +0200 Subject: [PATCH 17/22] updated header --- healthcare/solutions/dataverseIntegration/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md index 3a1ae1f1..6bca59c6 100644 --- a/healthcare/solutions/dataverseIntegration/README.md +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -1,4 +1,4 @@ -# Link Dataverse with Azure Data Lake Gen2 and Azure Synapse +# 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". @@ -35,13 +35,13 @@ Our tests have shown that similar requirements are existing for the Synapse work |:---------------------------------------------------|:------------------------------|---------------------------| | 'Export to data lake' (Dataverse) | Synapse Administrator | Synapse Workspace | -### Other comments +### 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 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 Implementations +## 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: @@ -50,7 +50,7 @@ To accelerate the integration of datasets between Dataverse and a data platform, 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. -### Deploy To Azure +### 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: @@ -58,7 +58,7 @@ First, use the "Deploy To Azure" Button to setup all Azure related services. Go 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. -### Connect Azure Services and Dataverse +### 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: From d33e45bcaa76f6b31eba30889024c198c29a8248 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Thu, 30 Sep 2021 19:59:18 +0200 Subject: [PATCH 18/22] Added Portal UI --- .../dataverseIntegration/portal.json | 150 ++++++++++-------- 1 file changed, 85 insertions(+), 65 deletions(-) diff --git a/healthcare/solutions/dataverseIntegration/portal.json b/healthcare/solutions/dataverseIntegration/portal.json index d15745b6..80b0daa6 100644 --- a/healthcare/solutions/dataverseIntegration/portal.json +++ b/healthcare/solutions/dataverseIntegration/portal.json @@ -3,11 +3,11 @@ "view": { "kind": "Form", "properties": { - "title": "Dataverse ", + "title": "Dataverse-Azure - Data Integration", "steps": [ { "name": "basics", - "label": "Data Product", + "label": "Deployment Location", "elements": [ { "name": "deploymentDetails", @@ -82,7 +82,7 @@ "type": "Microsoft.Common.InfoBox", "visible": true, "options": { - "text": "Since not all service features are available in all regions, Enterprise Scale Analytics is available in a subset of regions.", + "text": "Your Power Platform environment and Azure services must be in the same region.", "style": "Info" } }, @@ -100,14 +100,14 @@ "type": "Microsoft.Common.DropDown", "visible": true, "defaultValue": "", - "toolTip": "Select the Location for your Data Product.", + "toolTip": "Select the Location for your Deployment.", "multiselect": false, "selectAll": false, "filter": true, "filterPlaceholder": "Filter items ...", "multiLine": true, "constraints": { - "allowedValues": "[map(filter(steps('basics').deploymentDetails.locationsApi.value,(item) => contains(split('southafricanorth,southeastasia,japaneast,canadacentral,northeurope,westeurope,francecentral,germanywestcentral,uksouth,centralus,eastus,eastus2,southcentralus,westus2', ','), item.name)),(item) => parse(concat('{\"label\":\"', item.regionalDisplayName, '\",\"value\":\"', item.name, '\"}')))]", + "allowedValues": "[map(steps('basics').deploymentDetails.locationsApi.value,(item) => parse(concat('{\"label\":\"', item.regionalDisplayName, '\",\"value\":\"', item.name, '\"}')))]", "required": true } } @@ -165,8 +165,8 @@ } }, { - "name": "dataProductPrefix", - "label": "Data Product Prefix", + "name": "deploymentPrefix", + "label": "Deployment Prefix", "type": "Microsoft.Common.TextBox", "visible": true, "defaultValue": "", @@ -179,23 +179,15 @@ "message": "The prefix must be between 1-10 lowercase characters and numbers." }, { - "isValid": "[not(equals(steps('basics').dataProductName.keyVaultNameApi.nameAvailable, false))]", + "isValid": "[not(equals(steps('basics').deploymentName.keyVaultNameApi.nameAvailable, false))]", "message": "Prefix currently unavailable. Please choose a different one." }, { - "isValid": "[not(equals(steps('basics').dataProductName.storageAccountNameApi.nameAvailable, false))]", + "isValid": "[not(equals(steps('basics').deploymentName.storageAccountNameApi.nameAvailable, false))]", "message": "Prefix currently unavailable. Please choose a different one." }, { - "isValid": "[not(equals(steps('basics').dataProductName.containerRegistryNameApi.nameAvailable, false))]", - "message": "Prefix currently unavailable. Please choose a different one." - }, - { - "isValid": "[not(equals(steps('basics').dataProductName.synapseNameApi.available, false))]", - "message": "Prefix currently unavailable. Please choose a different one." - }, - { - "isValid": "[not(equals(steps('basics').dataProductName.searchNameApi.nameAvailable, false))]", + "isValid": "[not(equals(steps('basics').deploymentName.synapseNameApi.available, false))]", "message": "Prefix currently unavailable. Please choose a different one." } ] @@ -208,7 +200,7 @@ "method": "POST", "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.KeyVault/checkNameAvailability?api-version=2019-09-01')]", "body": { - "name": "[concat(steps('basics').dataProductName.dataProductPrefix, '-', steps('basics').dataProductName.environment, '-vault001')]", + "name": "[concat(steps('basics').deploymentName.deploymentPrefix, '-', steps('basics').deploymentName.environment, '-vault001')]", "type": "Microsoft.KeyVault/vaults" } } @@ -220,23 +212,11 @@ "method": "POST", "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.Storage/checkNameAvailability?api-version=2021-04-01')]", "body": { - "name": "[concat(steps('basics').dataProductName.dataProductPrefix, steps('basics').dataProductName.environment, 'storage001')]", + "name": "[concat(steps('basics').deploymentName.deploymentPrefix, steps('basics').deploymentName.environment, 'storage001')]", "type": "Microsoft.Storage/storageAccounts" } } }, - { - "name": "containerRegistryNameApi", - "type": "Microsoft.Solutions.ArmApiControl", - "request": { - "method": "POST", - "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.ContainerRegistry/checkNameAvailability?api-version=2019-05-01')]", - "body": { - "name": "[concat(steps('basics').dataProductName.dataProductPrefix, steps('basics').dataProductName.environment, 'containerregistry001')]", - "type": "Microsoft.ContainerRegistry/registries" - } - } - }, { "name": "synapseNameApi", "type": "Microsoft.Solutions.ArmApiControl", @@ -244,22 +224,10 @@ "method": "POST", "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.Synapse/checkNameAvailability?api-version=2021-03-01')]", "body": { - "name": "[concat(steps('basics').dataProductName.dataProductPrefix, '-', steps('basics').dataProductName.environment, '-synapse001')]", + "name": "[concat(steps('basics').deploymentName.deploymentPrefix, '-', steps('basics').deploymentName.environment, '-synapse001')]", "type": "Microsoft.Synapse/workspaces" } } - }, - { - "name": "searchNameApi", - "type": "Microsoft.Solutions.ArmApiControl", - "request": { - "method": "POST", - "path": "[concat(steps('basics').deploymentDetails.subscriptionId, '/providers/Microsoft.Search/checkNameAvailability?api-version=2021-04-01-preview')]", - "body": { - "name": "[concat(steps('basics').dataProductName.dataProductPrefix, '-', steps('basics').dataProductName.environment, '-search001')]", - "type": "searchServices" - } - } } ] } @@ -269,7 +237,7 @@ "name": "generalSettings", "label": "General Settings", "subLabel": { - "preValidation": "Provide settings for your data product deployment.", + "preValidation": "Provide settings for your deployment.", "postValidation": "Done" }, "bladeTitle": "General Settings", @@ -288,10 +256,58 @@ "options": { "text": "Specify the Object IDs of the required Enterprise Applications.", "link": { - "label": "Learn more", + "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 } ] }, @@ -302,12 +318,15 @@ "visible": true, "elements": [ { - "name": "infoBoxProcessingService", - "type": "Microsoft.Common.InfoBox", + "name": "infoBoxsynapseDeploymentSettings", + "type": "Microsoft.Common.TextBlock", "visible": true, "options": { "text": "Specify the settings for your Synapse workspace.", - "style": "Info" + "link": { + "label": "", + "uri": "" + } } }, { @@ -345,7 +364,7 @@ "type": "Microsoft.Common.TextBlock", "visible": true, "options": { - "text": "Select the Purview account to which you want to connect the Synapse workspace or Data Factory.", + "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" @@ -358,7 +377,7 @@ "type": "Microsoft.Solutions.ResourceSelector", "visible": true, "defaultValue": "", - "toolTip": "Select the Purview account to which you want to connect Synapse or Data Factory.", + "toolTip": "Select the Purview account to which you want to connect the Synapse workspace.", "resourceType": "Microsoft.Purview/accounts", "required": true, "options": { @@ -393,8 +412,9 @@ "type": "Microsoft.Common.InfoBox", "visible": true, "options": { - "text": "The role assignment is optional and will assign the Service Principle of the Power Platform Enterprise Application and the Service Principle of the Dataverse Enterprise Application multiple roles on the Data Lake. Please visit the link to learn more about the required role assignments.", - "style": "Info" + "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" } }, { @@ -406,7 +426,7 @@ "toolTip": "Enable role assignments.", "constraints": { "required": false, - "validationMessage": "Enable role assignments of Machine Learning MSI and Synapse MSI. Please read infobox above for more details." + "validationMessage": "Enable role assignments of Power Platform and Dataverse Enterprise Applications. Please read infobox above for more details." } } ] @@ -417,7 +437,7 @@ "name": "connectivitySettings", "label": "Connectivity Settings", "subLabel": { - "preValidation": "Provide the connectivity settings that should be used for the Data Product.", + "preValidation": "Provide the connectivity settings that should be used for your deployment.", "postValidation": "Done" }, "bladeTitle": "Connectivity Settings", @@ -446,7 +466,7 @@ "type": "Microsoft.Common.InfoBox", "visible": true, "options": { - "text": "Please select the Virtual Network of your Data Landing Zone and select a free Subnet within the Virtual Network to make sure that you have proper outbound connectivity as well as DNS name resolution in place. The subnet must have 'privateEndpointNetworkPolicies' and 'privateLinkServiceNetworkPolicies' set to disabled. This is already pre-configured for you in the DataProduct and DataIntegration subnets in the Data Landing Zone Vnet. In addition, Azure Machine Learning requires a subnet with the 'Microsoft.Storage' service endpoint enabled in the region of the Data Product deployment. This is not enabled by default and therefore must be setup manually.", + "text": "Please select a free Subnet within the Virtual Network with 'privateEndpointNetworkPolicies' and 'privateLinkServiceNetworkPolicies' set to disabled.", "style": "Info" } }, @@ -464,7 +484,7 @@ "type": "Microsoft.Common.DropDown", "visible": true, "defaultValue": "", - "toolTip": "Select the Virtual Network for your Data Product.", + "toolTip": "Select the Virtual Network for your Deployment.", "multiselect": false, "selectAll": false, "filter": true, @@ -489,7 +509,7 @@ "type": "Microsoft.Common.DropDown", "visible": true, "defaultValue": "", - "toolTip": "Select the Subnet for your Data Product. The subnet must have 'privateEndpointNetworkPolicies' and 'privateLinkServiceNetworkPolicies' set to disabled.", + "toolTip": "Select the Subnet for your Deployment. The subnet must have 'privateEndpointNetworkPolicies' and 'privateLinkServiceNetworkPolicies' set to disabled.", "multiselect": false, "selectAll": false, "filter": true, @@ -688,7 +708,7 @@ "type": "Microsoft.Common.TagsByResource", "visible": true, "resources": [ - "EnterpriseScaleAnalytics" + "DeploymentTags" ] } ] @@ -701,11 +721,11 @@ "resourceGroupId": "[steps('basics').deploymentDetails.resourceGroupId]", "parameters": { "location": "[if(empty(steps('basics').deploymentDetails.locationName), '', steps('basics').deploymentDetails.locationName)]", - "environment": "[if(empty(steps('basics').dataProductName.environment), '', steps('basics').dataProductName.environment)]", - "prefix": "[if(empty(steps('basics').dataProductName.dataProductPrefix), '', steps('basics').dataProductName.dataProductPrefix)]", + "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').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId), '', steps('generalSettings').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId)]", - "dataverseServicePrincipalObjectId": "[if(empty(steps('generalSettings').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId), '', steps('generalSettings').synapseDeploymentSettings.synapseDefaultStorageAccountFileSystemId)]", + "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)]", @@ -714,7 +734,7 @@ "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, 'EnterpriseScaleAnalytics')), parse('{}'), first(map(parse(concat('[', string(steps('tags').tagsByResource), ']')), (item) => item.EnterpriseScaleAnalytics)))]" + "tags": "[if(not(contains(steps('tags').tagsByResource, 'DeploymentTags')), parse('{}'), first(map(parse(concat('[', string(steps('tags').tagsByResource), ']')), (item) => item.DeploymentTags)))]" } } } From aa1a29a2290ee73eabae7501bfaeeddd95f4901c Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Sun, 3 Oct 2021 23:30:51 +0200 Subject: [PATCH 19/22] update deploy to msft button --- healthcare/solutions/dataverseIntegration/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md index 6bca59c6..9c7d1117 100644 --- a/healthcare/solutions/dataverseIntegration/README.md +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -54,7 +54,7 @@ Also, the reference implementation includes a set of powershell scripts to autom 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 Azure](https://aka.ms/deploytoazurebutton)](#TODO) +[![Deploy To Azure](/docs/deploytomicrosoftcloud.svg)](#TODO) 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. From 607f681bbfa2cedb0464b0d4dedd6bceebacd8e0 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Mon, 4 Oct 2021 11:20:09 +0200 Subject: [PATCH 20/22] Updated URI for deploy to MSFT button --- healthcare/solutions/dataverseIntegration/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md index 9c7d1117..0fb3506a 100644 --- a/healthcare/solutions/dataverseIntegration/README.md +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -54,7 +54,7 @@ Also, the reference implementation includes a set of powershell scripts to autom 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 Azure](/docs/deploytomicrosoftcloud.svg)](#TODO) +[![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. From f1d86fd75bff53cfbe706677230c6f65b5bbc502 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Wed, 13 Oct 2021 16:13:45 +0200 Subject: [PATCH 21/22] updated parameters and readme --- healthcare/solutions/dataverseIntegration/README.md | 2 +- healthcare/solutions/dataverseIntegration/params.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md index 0fb3506a..90d25740 100644 --- a/healthcare/solutions/dataverseIntegration/README.md +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -39,7 +39,7 @@ Our tests have shown that similar requirements are existing for the Synapse work 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 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. +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 diff --git a/healthcare/solutions/dataverseIntegration/params.json b/healthcare/solutions/dataverseIntegration/params.json index b2051fa2..30fde1fb 100644 --- a/healthcare/solutions/dataverseIntegration/params.json +++ b/healthcare/solutions/dataverseIntegration/params.json @@ -40,6 +40,12 @@ }, "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 From 0e49a214e86d6b4340c2ccce67d92443b3d5a0af Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Fri, 14 Oct 2022 15:57:14 +0200 Subject: [PATCH 22/22] Lint --- healthcare/solutions/dataverseIntegration/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/healthcare/solutions/dataverseIntegration/README.md b/healthcare/solutions/dataverseIntegration/README.md index 90d25740..851ceaea 100644 --- a/healthcare/solutions/dataverseIntegration/README.md +++ b/healthcare/solutions/dataverseIntegration/README.md @@ -56,7 +56,7 @@ First, use the "Deploy To Azure" Button to setup all Azure related services. Go [![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. +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