diff --git a/101-synapse-poc/README.md b/101-synapse-poc/README.md new file mode 100644 index 000000000000..138bdfc6532d --- /dev/null +++ b/101-synapse-poc/README.md @@ -0,0 +1,79 @@ +# Azure Synapse Proof-of-Concept +![Synapse Analytics](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-poc/images/synapse1.png) + +![Azure Public Test Date](https://azurequickstartsservice.blob.core.windows.net/badges/101-synapse-poc/PublicLastTestDate.svg) +![Azure Public Test Result](https://azurequickstartsservice.blob.core.windows.net/badges/101-synapse-poc/PublicDeployment.svg) + +![Azure US Gov Last Test Date](https://azurequickstartsservice.blob.core.windows.net/badges/101-synapse-poc/FairfaxLastTestDate.svg) +![Azure US Gov Last Test Result](https://azurequickstartsservice.blob.core.windows.net/badges/101-synapse-poc/FairfaxDeployment.svg) + +![Best Practice Check](https://azurequickstartsservice.blob.core.windows.net/badges/101-synapse-poc/BestPracticeResult.svg) +![Cred Scan Check](https://azurequickstartsservice.blob.core.windows.net/badges/101-synapse-poc/CredScanResult.svg) + +[![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-quickstart-templates%2Fmaster%2F101-synapse-poc%2Fazuredeploy.json) + +[![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-quickstart-templates%2Fmaster%2F101-synapse-poc%2Fazuredeploy.json) + +This template deploys necessary resources to run an Azure Synapse Proof-of-Concept + +This template deploys the following: +- An Azure Synapse Workspace + - (OPTIONAL) Allows All connections in by default (Firewall IP Addresses) + - Allows Azure Services to access the workspace by default + - Managed Virtual Network is Enabled +- An Azure Synapse SQL Pool +- (OPTIONAL) Apache Spark Pool + - Auto-paused set to 15 minutes of idling +- Azure Data Lake Storage Gen2 account + - Azure Synapse Workspace identity given Storage Blob Data Contributor to the Storage Account + - A new File System inside the Storage Account to be used by Azure Synapse +- A Logic App to Pause the SQL Pool at defined schedule + - The Logic App will check for Active Queries. If there are active queries, it will wait 5 minutes and check again until there are none before pausing +- A Logic App to Resume the SQL Pool at defined schedule +- Both Logic App managed identities are given Contributor rights to the Resource Group +- Grants the Workspace identity CONTROL to all SQL pools and SQL on-demand pool + +# Index + +- [Purpose](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-poc#purpose) +- [Prerequisites](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-poc#prerequisites) +- [Deploy to Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-poc#deploy-to-azure) +- [Post Deployment](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-poc#post-deployment) + +## Purpose +This template allows the Administrator to deploy a Proof-of-Concept environment of Azure Synapse Analytics with some pre-set parameters. This allows more time to focus on the Proof-of-Concept at hand and test the service. + +Using the Getting Started wizard inside of the workspace is recommended to use sample data if you do not have your own with you to add to the Storage Account. + +## Prerequisites +- Owner to the Azure Subscription being deployed. This is for creation of a separate Proof-of-Concept Resource Group and to delegate roles necessary for this proof of concept + +## Post Deployment +Because the Synapse Workspace is using a Managed Virtual Network, the Storage Account requires a Managed Private Endpoint to be created into the Managed Virtual Network. + +You can create a Managed private endpoint to your data source from Azure Synapse Studio. Select the Overview tab in Azure portal and select Launch Synapse Studio. + +![Step 1](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-workspace-and-pools/images/9.png) + +In Azure Synapse Studio, select the Manage tab from the left navigation. Select Managed Virtual Networks and then select + New. + +![Step 2](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-workspace-and-pools/images/10.png) + +Select the data source type. In this case, the target data source is an ADLS Gen2 account. Select Continue. + +![Step 3](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-workspace-and-pools/images/11.png) + +In the next window, enter information about the data source. In this example, we're creating a Managed private endpoint to an ADLS Gen2 account. Enter a Name for the Managed private endpoint. Provide an Azure subscription and a Storage account name. Select Create. + +![Step 4](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-workspace-and-pools/images/12.png) + +After submitting the request, you'll see its status. To verify the successful creation of your Managed private endpoint was created, check its Provisioning State. You may need to wait 1 minute and select Refresh to update the provisioning state. You can see that the Managed private endpoint to the ADLS Gen2 account was successfully created. + +You can also see that the Approval State is Pending. The owner of the target resource can approve or deny the private endpoint connection request. If the owner approves the private endpoint connection request, then a private link is established. If denied, then a private link isn't established. + +![Step 5](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-workspace-and-pools/images/13.png) + +Further information can be found: + +[Create a Managed private endpoint to your data source](https://docs.microsoft.com/en-us/azure/synapse-analytics/security/how-to-create-managed-private-endpoints) + diff --git a/101-synapse-poc/azuredeploy.json b/101-synapse-poc/azuredeploy.json new file mode 100644 index 000000000000..66150f5ba692 --- /dev/null +++ b/101-synapse-poc/azuredeploy.json @@ -0,0 +1,457 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "_artifactsLocation": { + "type": "string", + "defaultValue": "https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-synapse-poc/" + }, + "_artifactsLocationSASToken": { + "type": "securestring", + "defaultValue": "" + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for your deployment." + }, + "defaultValue": "[resourceGroup().location]" + }, + "companyTla": { + "type": "string", + "metadata": { + "description": "This is a Three Letter Acronym for your company name. 'CON' for Contoso for example." + } + }, + "allowAllConnections": { + "type": "string", + "allowedValues": [ "true", "false" ], + "defaultValue": "true" + }, + "sparkDeployment": { + "type": "string", + "defaultValue": "true", + "allowedValues": [ "true", "false" ], + "metadata": { + "description": "'True' deploys an Apache Spark pool as well as a SQL pool. 'False' does not deploy an Apache Spark pool." + } + }, + "sparkNodeSize": { + "type": "string", + "metadata": { + "description": "This parameter will determine the node size if SparkDeployment is true" + }, + "defaultValue": "Medium", + "allowedValues": [ "Small", "Medium", "Large" ] + }, + "deploymentType": { + "type": "string", + "allowedValues": [ "devtest", "poc", "prod", "shared" ], + "defaultValue": "poc", + "metadata": { + "description": "Specify deployment type: DevTest, POC, Prod, Shared. This will also be used in the naming convention." + } + }, + "sqlAdministratorLogin": { + "type": "string", + "metadata": { + "description": "The username of the SQL Administrator" + } + }, + "sqlAdministratorLoginPassword": { + "type": "securestring", + "metadata": { + "description": "The password for the SQL Administrator" + } + }, + "sku": { + "type": "string", + "defaultValue": "DW100c", + "allowedValues": [ + "DW100c", + "DW200c", + "DW300c", + "DW400c", + "DW500c", + "DW1000c", + "DW1500c", + "DW2000c", + "DW2500c", + "DW3000c" + ], + "metadata": { + "description": "Select the SKU of the SQL pool." + } + }, + "metadataSync": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Choose whether you want to synchronise metadata." + } + }, + + "Frequency": { + "type": "string", + "metadata": "Choose whether to run schedule every day of the week, or only on weekdays", + "defaultValue": "Weekdays", + "allowedValues": [ "Daily", "Weekdays" ] + }, + "TIME_ZONE": { + "type": "string", + "metadata": "Timezone for the schedule. Consult https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx for more information", + "defaultValue": "Eastern Standard Time", + "allowedValues": [ + "Dateline Standard Time", + "Samoa Standard Time", + "Hawaiian Standard Time", + "Alaskan Standard Time", + "Pacific Standard Time", + "Mountain Standard Time", + "Mexico Standard Time 2", + "Central Standard Time", + "Canada Central Standard Time", + "Mexico Standard Time", + "Central America Standard Time", + "Eastern Standard Time", + "Atlantic Standard Time", + "Newfoundland and Labrador Standard Time", + "E. South America Standard Time", + "S.A. Eastern Standard Time", + "Greenland Standard Time", + "Mid-Atlantic Standard Time", + "Azores Standard Time", + "Cape Verde Standard Time", + "GMT Standard Time", + "Greenwich Standard Time", + "Central Europe Standard Time", + "Central European Standard Time", + "Romance Standard Time", + "W. Europe Standard Time", + "W. Central Africa Standard Time", + "E. Europe Standard Time", + "Egypt Standard Time", + "FLE Standard Time", + "GTB Standard Time", + "Israel Standard Time", + "South Africa Standard Time", + "Russian Standard Time", + "Arab Standard Time", + "E. Africa Standard Time", + "Arabic Standard Time", + "Iran Standard Time", + "Arabian Standard Time", + "Caucasus Standard Time", + "Transitional Islamic State of Afghanistan Standard Time", + "Ekaterinburg Standard Time", + "West Asia Standard Time", + "India Standard Time", + "Nepal Standard Time", + "Central Asia Standard Time", + "Sri Lanka Standard Time", + "Myanmar Standard Time", + "North Asia Standard Time", + "China Standard Time", + "Singapore Standard Time", + "Taipei Standard Time", + "North Asia East Standard Time", + "Korea Standard Time", + "Tokyo Standard Time", + "Yakutsk Standard Time", + "Tasmania Standard Time", + "Vladivostok Standard Time", + "West Pacific Standard Time", + "Central Pacific Standard Time", + "Fiji Islands Standard Time", + "New Zealand Standard Time", + "Tonga Standard Time" + ] + }, + "ResumeTime": { + "type": "string", + "metadata": "Time of day when the data warehouse will be resumed", + "allowedValues": [ + "12:00 AM ( 0:00 )", + "01:00 AM ( 1:00 )", + "02:00 AM ( 2:00 )", + "03:00 AM ( 3:00 )", + "04:00 AM ( 4:00 )", + "05:00 AM ( 5:00 )", + "06:00 AM ( 6:00 )", + "07:00 AM ( 7:00 )", + "08:00 AM ( 8:00 )", + "09:00 AM ( 9:00 )", + "10:00 AM ( 10:00 )", + "11:00 AM ( 11:00 )", + "12:00 PM ( 12:00 )", + "01:00 PM ( 13:00 )", + "02:00 PM ( 14:00 )", + "03:00 PM ( 15:00 )", + "04:00 PM ( 16:00 )", + "05:00 PM ( 17:00 )", + "06:00 PM ( 18:00 )", + "07:00 PM ( 19:00 )", + "08:00 PM ( 20:00 )", + "09:00 PM ( 21:00 )", + "10:00 PM ( 22:00 )", + "11:00 PM ( 23:00 )" + ], + "defaultValue": "09:00 PM ( 21:00 )" + }, + "PauseTime": { + "type": "string", + "metadata": "Time of day when the data warehouse will be paused", + "allowedValues": [ + "12:00 AM ( 0:00 )", + "01:00 AM ( 1:00 )", + "02:00 AM ( 2:00 )", + "03:00 AM ( 3:00 )", + "04:00 AM ( 4:00 )", + "05:00 AM ( 5:00 )", + "06:00 AM ( 6:00 )", + "07:00 AM ( 7:00 )", + "08:00 AM ( 8:00 )", + "09:00 AM ( 9:00 )", + "10:00 AM ( 10:00 )", + "11:00 AM ( 11:00 )", + "12:00 PM ( 12:00 )", + "01:00 PM ( 13:00 )", + "02:00 PM ( 14:00 )", + "03:00 PM ( 15:00 )", + "04:00 PM ( 16:00 )", + "05:00 PM ( 17:00 )", + "06:00 PM ( 18:00 )", + "07:00 PM ( 19:00 )", + "08:00 PM ( 20:00 )", + "09:00 PM ( 21:00 )", + "10:00 PM ( 22:00 )", + "11:00 PM ( 23:00 )" + ], + "defaultValue": "05:00 PM ( 17:00 )" + } + }, + "variables": { + "synapseName": "[toLower(concat(parameters('companyTla'),parameters('deploymentType')))]", + "dlsName": "[toLower(concat('dls',parameters('companyTla'),parameters('deploymentType')))]", + "dlsFsName": "[toLower(concat(variables('dlsName'),'fs1'))]", + "sqlPoolName": "[toLower(concat(variables('workspaceName'),'p1'))]", + "workspaceName": "[toLower(concat(variables('synapseName'),'ws1'))]", + "sparkPoolName": "[toLower('synasp1')]", + "logicApps": [ + { + "name": "SynapsePauseSchedule" + }, + { + "name": "SynapseResumeSchedule" + } + ] + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "logicAppPauseDeployment", + "properties": { + "mode": "Incremental", + "templatelink": { + "uri": "[concat(parameters('_artifactsLocation'),'nestedtemplates/pausetemplate.json',parameters('_artifactsLocationSASToken'))]" + }, + "parameters": { + "logicAppName": { "value": "[variables('logicApps')[0].name]" }, + "Frequency": { "value": "[parameters('Frequency')]" }, + "companyTla": { "value": "[parameters('companyTla')]" }, + "deploymentType": { "value": "[parameters('deploymentType')]" }, + "TIME_ZONE": { "value": "[parameters('TIME_ZONE')]" }, + "PauseTime": { "value": "[parameters('PauseTime')]" }, + "location": { "value": "[parameters('location')]" } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "logicAppResumeDeployment", + "properties": { + "mode": "Incremental", + "templatelink": { + "uri": "[concat(parameters('_artifactsLocation'),'nestedtemplates/resumetemplate.json',parameters('_artifactsLocationSASToken'))]" + }, + "parameters": { + "logicAppName": { "value": "[variables('logicApps')[1].name]" }, + "Frequency": { "value": "[parameters('Frequency')]" }, + "companyTla": { "value": "[parameters('companyTla')]" }, + "deploymentType": { "value": "[parameters('deploymentType')]" }, + "TIME_ZONE": { "value": "[parameters('TIME_ZONE')]" }, + "ResumeTime": { "value": "[parameters('ResumeTime')]" }, + "location": { "value": "[parameters('location')]" } + } + } + }, + { + "type": "Microsoft.Synapse/workspaces", + "apiVersion": "2019-06-01-preview", + "name": "[variables('workspaceName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "defaultDataLakeStorage": { + "accountUrl": "[reference(variables('dlsName')).primaryEndpoints.dfs]", + "filesystem": "[variables('dlsFsName')]" + }, + "sqlAdministratorLogin": "[parameters('sqlAdministratorLogin')]", + "sqlAdministratorLoginPassword": "[parameters('sqlAdministratorLoginPassword')]", + "managedVirtualNetwork": "default" + }, + "resources": [ + { + "condition": "[equals(parameters('allowAllConnections'),'true')]", + "type": "firewallrules", + "apiVersion": "2019-06-01-preview", + "name": "allowAll", + "location": "[parameters('location')]", + "dependsOn": [ "[variables('workspaceName')]" ], + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "255.255.255.255" + } + }, + { + "type": "firewallrules", + "apiVersion": "2019-06-01-preview", + "name": "AllowAllWindowsAzureIps", + "location": "[parameters('location')]", + "dependsOn": [ "[variables('workspaceName')]" ], + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "0.0.0.0" + } + }, + { + "type": "managedIdentitySqlControlSettings", + "apiVersion": "2019-06-01-preview", + "name": "default", + "location": "[parameters('location')]", + "dependsOn": [ "[variables('workspaceName')]" ], + "properties": { + "grantSqlControlToManagedIdentity": { + "desiredState": "Enabled" + } + } + } + ], + "dependsOn": [ "[variables('dlsName')]", "[variables('dlsFsName')]" ] + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2019-06-01", + "name": "[variables('dlsName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "resources": [ + { + "name": "[concat('default/', variables('dlsFsName'))]", + "type": "blobServices/containers", + "apiVersion": "2019-06-01", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ "[variables('dlsName')]" ] + } + ], + "properties": { + "accessTier": "Hot", + "supportsHttpsTrafficOnly": true, + "isHnsEnabled": true + } + }, + { + "type": "Microsoft.Storage/storageAccounts/providers/roleAssignments", + "apiVersion": "2018-09-01-preview", + "name": "[concat(variables('dlsName'), '/Microsoft.Authorization/', guid(uniqueString(variables('dlsName'))))]", + "location": "[parameters('location')]", + "dependsOn": [ "[variables('workspaceName')]" ], + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(resourceId('Microsoft.Synapse/workspaces/', variables('workspaceName')), '2019-06-01-preview', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Synapse/workspaces/sqlPools", + "apiVersion": "2019-06-01-preview", + "name": "[concat(variables('workspaceName'), '/', variables('sqlPoolName'))]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('sku')]" + }, + "dependsOn": [ "[variables('workspaceName')]" ], + "resources": [ + { + "condition": "[parameters('metadataSync')]", + "apiVersion": "2019-06-01-preview", + "dependsOn": [ "[variables('sqlPoolName')]" ], + "location": "[parameters('location')]", + "name": "config", + "properties": { + "Enabled": "[parameters('metadataSync')]" + }, + "type": "metadataSync" + } + ], + "properties": { + "createMode": "Default", + "collation": "SQL_Latin1_General_CP1_CI_AS" + } + }, + { + "condition": "[equals(parameters('sparkDeployment'),'true')]", + "type": "Microsoft.Synapse/workspaces/bigDataPools", + "apiVersion": "2019-06-01-preview", + "name": "[concat(variables('workspaceName'), '/', variables('sparkPoolName'))]", + "location": "[parameters('location')]", + "dependsOn": [ "[variables('workspaceName')]" ], + "properties": { + "nodeCount": 5, + "nodeSizeFamily": "MemoryOptimized", + "nodeSize": "[parameters('sparkNodeSize')]", + "autoScale": { + "enabled": true, + "minNodeCount": 3, + "maxNodeCount": 40 + }, + "autoPause": { + "enabled": true, + "delayInMinutes": 15 + }, + "sparkVersion": "2.4" + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "[concat('MSIRBACOnResourceGroup',copyIndex())]", + "dependsOn": [ "[variables('sqlPoolName')]" ], + "copy": { + "count": "[length(variables('logicApps'))]", + "name": "logicAppCount" + }, + "properties": { + "mode": "Incremental", + "templatelink": { + "uri": "[concat(parameters('_artifactsLocation'),'nestedtemplates/logicapproleassignments.json',parameters('_artifactsLocationSASToken'))]" + }, + "parameters": { + "logicAppName": { + "value": "[variables('logicApps')[copyIndex()].name]" + }, + "logicAppPrincipalId": { "value": "[reference(resourceId('Microsoft.Logic/workflows', variables('logicApps')[copyIndex()].name), '2019-05-01', 'full').identity.principalId]" } + } + } + } + ] +} diff --git a/101-synapse-poc/azuredeploy.parameters.json b/101-synapse-poc/azuredeploy.parameters.json new file mode 100644 index 000000000000..ff3b1c824b0f --- /dev/null +++ b/101-synapse-poc/azuredeploy.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "companyTla": { + "value": "GEN-UNIQUE-3" + }, + "sqlAdministratorLogin": { + "value": "GEN-UNIQUE-6" + }, + "sqlAdministratorLoginPassword": { + "value": "GEN-PASSWORD" + } + } +} \ No newline at end of file diff --git a/101-synapse-poc/images/1.png b/101-synapse-poc/images/1.png new file mode 100644 index 000000000000..07c7d174efc2 Binary files /dev/null and b/101-synapse-poc/images/1.png differ diff --git a/101-synapse-poc/images/10.png b/101-synapse-poc/images/10.png new file mode 100644 index 000000000000..d5c662495586 Binary files /dev/null and b/101-synapse-poc/images/10.png differ diff --git a/101-synapse-poc/images/11.png b/101-synapse-poc/images/11.png new file mode 100644 index 000000000000..4efbca96319d Binary files /dev/null and b/101-synapse-poc/images/11.png differ diff --git a/101-synapse-poc/images/12.png b/101-synapse-poc/images/12.png new file mode 100644 index 000000000000..d304eba6ccd6 Binary files /dev/null and b/101-synapse-poc/images/12.png differ diff --git a/101-synapse-poc/images/13.png b/101-synapse-poc/images/13.png new file mode 100644 index 000000000000..d5a1de34a922 Binary files /dev/null and b/101-synapse-poc/images/13.png differ diff --git a/101-synapse-poc/images/2.png b/101-synapse-poc/images/2.png new file mode 100644 index 000000000000..b1b69d106c7c Binary files /dev/null and b/101-synapse-poc/images/2.png differ diff --git a/101-synapse-poc/images/3.png b/101-synapse-poc/images/3.png new file mode 100644 index 000000000000..d8c510a9d7d7 Binary files /dev/null and b/101-synapse-poc/images/3.png differ diff --git a/101-synapse-poc/images/4.png b/101-synapse-poc/images/4.png new file mode 100644 index 000000000000..290ba694d110 Binary files /dev/null and b/101-synapse-poc/images/4.png differ diff --git a/101-synapse-poc/images/5.png b/101-synapse-poc/images/5.png new file mode 100644 index 000000000000..5d6f369fe11a Binary files /dev/null and b/101-synapse-poc/images/5.png differ diff --git a/101-synapse-poc/images/6.png b/101-synapse-poc/images/6.png new file mode 100644 index 000000000000..2f30a4844469 Binary files /dev/null and b/101-synapse-poc/images/6.png differ diff --git a/101-synapse-poc/images/7.png b/101-synapse-poc/images/7.png new file mode 100644 index 000000000000..427e99bda86f Binary files /dev/null and b/101-synapse-poc/images/7.png differ diff --git a/101-synapse-poc/images/8.png b/101-synapse-poc/images/8.png new file mode 100644 index 000000000000..cf88cb772b2b Binary files /dev/null and b/101-synapse-poc/images/8.png differ diff --git a/101-synapse-poc/images/9.png b/101-synapse-poc/images/9.png new file mode 100644 index 000000000000..7373b86427b8 Binary files /dev/null and b/101-synapse-poc/images/9.png differ diff --git a/101-synapse-poc/images/synapse1.png b/101-synapse-poc/images/synapse1.png new file mode 100644 index 000000000000..acb483bcc1ef Binary files /dev/null and b/101-synapse-poc/images/synapse1.png differ diff --git a/101-synapse-poc/metadata.JSON b/101-synapse-poc/metadata.JSON new file mode 100644 index 000000000000..42833bb898ce --- /dev/null +++ b/101-synapse-poc/metadata.JSON @@ -0,0 +1,13 @@ +{ + "$schema": "https://aka.ms/azure-quickstart-templates-metadata-schema#", + "itemDisplayName": "Azure Synapse Proof-of-Concept", + "description": "This template creates a proof of concept environment for Azure Synapse, including SQL Pools and optional Apache Spark Pools", + "summary": "Azure Synapse Proof-of-Concept", + "validationType": "Manual", + "githubUsername": "JamJarchitect", + "dateUpdated": "2020-07-24", + "type": "QuickStart", + "environments": [ + "AzureCloud" + ] + } \ No newline at end of file diff --git a/101-synapse-poc/nestedtemplates/logicapproleassignments.json b/101-synapse-poc/nestedtemplates/logicapproleassignments.json new file mode 100644 index 000000000000..18bf9d17af80 --- /dev/null +++ b/101-synapse-poc/nestedtemplates/logicapproleassignments.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "logicAppName": { + "type": "string" + }, + "logicAppPrincipalId": { + "type": "string", + "defaultValue": "[reference(resourceId('Microsoft.Logic/workflows', parameters('logicAppName')), '2019-05-01', 'full').identity.principalId]" + } + }, + "variables": { + "contributor": "b24988ac-6180-42a0-ab88-20f7382dd24c" + }, + "resources": [ + { + "type": "Microsoft.Logic/workflows/providers/roleAssignments", + "apiVersion": "2018-09-01-preview", + "name": "[concat(parameters('logicAppName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.Logic/workflows', parameters('logicAppName')), 'rotator-role-assignment'))]", + "properties": { + "principalId": "[parameters('logicAppPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('contributor'))]", + "scope": "[resourceGroup().id]", + "principalType": "ServicePrincipal" + } + + } + ] +} \ No newline at end of file diff --git a/101-synapse-poc/nestedtemplates/pausetemplate.json b/101-synapse-poc/nestedtemplates/pausetemplate.json new file mode 100644 index 000000000000..f9c5a0d86dae --- /dev/null +++ b/101-synapse-poc/nestedtemplates/pausetemplate.json @@ -0,0 +1,401 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "type": "string" + }, + "companyTla": { + "type": "string", + "metadata": { + "description": "This is a Three Letter Acronym for your company name. 'CON' for Contoso for example." + }, + "maxLength": 3 + }, + "deploymentType": { + "type": "string", + "allowedValues": [ + "devtest", + "poc", + "prod", + "shared" + ], + "defaultValue": "poc", + "metadata": { + "description": "Specify deployment type: DevTest, POC, Prod, Shared. This will also be used in the naming convention." + } + }, + "LogicAppName": { + "type": "string", + "metadata": "Name of Azure Logic app" + }, + "Frequency": { + "type": "string", + "metadata": "Choose whether to run schedule every day of the week, or only on weekdays", + "defaultValue": "Weekdays", + "allowedValues": [ + "Daily", + "Weekdays" + ] + }, + "TIME_ZONE": { + "type": "string", + "metadata": "Timezone for the schedule. Consult https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx for more information", + "defaultValue": "Pacific Standard Time", + "allowedValues": [ + "Dateline Standard Time", + "Samoa Standard Time", + "Hawaiian Standard Time", + "Alaskan Standard Time", + "Pacific Standard Time", + "Mountain Standard Time", + "Mexico Standard Time 2", + "Central Standard Time", + "Canada Central Standard Time", + "Mexico Standard Time", + "Central America Standard Time", + "Eastern Standard Time", + "Atlantic Standard Time", + "Newfoundland and Labrador Standard Time", + "E. South America Standard Time", + "S.A. Eastern Standard Time", + "Greenland Standard Time", + "Mid-Atlantic Standard Time", + "Azores Standard Time", + "Cape Verde Standard Time", + "GMT Standard Time", + "Greenwich Standard Time", + "Central Europe Standard Time", + "Central European Standard Time", + "Romance Standard Time", + "W. Europe Standard Time", + "W. Central Africa Standard Time", + "E. Europe Standard Time", + "Egypt Standard Time", + "FLE Standard Time", + "GTB Standard Time", + "Israel Standard Time", + "South Africa Standard Time", + "Russian Standard Time", + "Arab Standard Time", + "E. Africa Standard Time", + "Arabic Standard Time", + "Iran Standard Time", + "Arabian Standard Time", + "Caucasus Standard Time", + "Transitional Islamic State of Afghanistan Standard Time", + "Ekaterinburg Standard Time", + "West Asia Standard Time", + "India Standard Time", + "Nepal Standard Time", + "Central Asia Standard Time", + "Sri Lanka Standard Time", + "Myanmar Standard Time", + "North Asia Standard Time", + "China Standard Time", + "Singapore Standard Time", + "Taipei Standard Time", + "North Asia East Standard Time", + "Korea Standard Time", + "Tokyo Standard Time", + "Yakutsk Standard Time", + "Tasmania Standard Time", + "Vladivostok Standard Time", + "West Pacific Standard Time", + "Central Pacific Standard Time", + "Fiji Islands Standard Time", + "New Zealand Standard Time", + "Tonga Standard Time" + ] + }, + "PauseTime": { + "type": "string", + "metadata": "Time of day when the data warehouse will be paused", + "allowedValues": [ + "12:00 AM ( 0:00 )", + "01:00 AM ( 1:00 )", + "02:00 AM ( 2:00 )", + "03:00 AM ( 3:00 )", + "04:00 AM ( 4:00 )", + "05:00 AM ( 5:00 )", + "06:00 AM ( 6:00 )", + "07:00 AM ( 7:00 )", + "08:00 AM ( 8:00 )", + "09:00 AM ( 9:00 )", + "10:00 AM ( 10:00 )", + "11:00 AM ( 11:00 )", + "12:00 PM ( 12:00 )", + "01:00 PM ( 13:00 )", + "02:00 PM ( 14:00 )", + "03:00 PM ( 15:00 )", + "04:00 PM ( 16:00 )", + "05:00 PM ( 17:00 )", + "06:00 PM ( 18:00 )", + "07:00 PM ( 19:00 )", + "08:00 PM ( 20:00 )", + "09:00 PM ( 21:00 )", + "10:00 PM ( 22:00 )", + "11:00 PM ( 23:00 )" + ] + } + }, + "variables": { + "pauseTimeHour": "[split(substring(parameters('PauseTime'), 11, 5), ':')[0]]", + "recurrenceHours": [ "[variables('pauseTimeHour')]" ], + "recurrenceMinutes": [ 0 ], + "pauseTimeString": "[substring(parameters('PauseTime'), 0, 8)]", + "dailySchedule": [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ], + "weekdaySchedule": [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" ], + "recurrenceSchedule": "[if(equals(parameters('Frequency'), 'Weekdays'), variables('weekdaySchedule'), variables('dailySchedule'))]", + "synapseWorkspaceName": "[toLower(concat(variables('synapseName'),'ws1'))]", + "synapseName": "[toLower(concat(parameters('companyTla'),parameters('deploymentType')))]", + "synapseSQLPoolName": "[toLower(concat(variables('workspaceName'),'p1'))]", + "workspaceName": "[toLower(concat(variables('synapseName'),'ws1'))]", + "getRESTAPI": "subscriptions/@{variables('RestAPIVariables')['SubscriptionId']}/resourceGroups/@{variables('RestAPIVariables')['ResourceGroupName']}/providers/Microsoft.Synapse/workspaces/@{variables('RestAPIVariables')['workspaceName']}/sqlPools/@{variables('RestAPIVariables')['sqlPoolName']}?api-version=2019-06-01-preview", + "pauseRESTAPI": "subscriptions/@{variables('RestAPIVariables')['SubscriptionId']}/resourceGroups/@{variables('RestAPIVariables')['ResourceGroupName']}/providers/Microsoft.Synapse/workspaces/@{variables('RestAPIVariables')['workspaceName']}/sqlPools/@{variables('RestAPIVariables')['sqlPoolName']}/pause?api-version=2019-06-01-preview", + "aqcRESTAPI": "subscriptions/@{variables('RestAPIVariables')['SubscriptionId']}/resourceGroups/@{variables('RestAPIVariables')['ResourceGroupName']}/providers/Microsoft.Synapse/workspaces/@{variables('RestAPIVariables')['WorkspaceName']}/sqlpools/@{variables('RestAPIVariables')['SQLPoolName']}/dataWarehouseUserActivities/current?api-version=2019-06-01-preview", + "managementEndpoint": "[environment().resourceManager]" + }, + "resources": [ + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2019-05-01", + "name": "[parameters('LogicAppName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Initialize_API_variables": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "RestAPIVariables", + "type": "Object", + "value": { + "workspaceName": "[variables('synapseWorkspaceName')]", + "sqlPoolName": "[variables('synapseSQLPoolName')]", + "ResourceGroupName": "[resourceGroup().name]", + "SubscriptionId": "[subscription().subscriptionId]", + "TenantId": "[subscription().tenantId]", + "ScheduleTimeZone": "[parameters('TIME_ZONE')]", + "PauseTime": "[variables('pauseTimeString')]" + } + } + ] + } + }, + "Initialize_ActiveQueryCount_variable": { + "inputs": { + "variables": [ + { + "name": "ActiveQueryCount", + "type": "Integer", + "value": 1 + } + ] + }, + "runAfter": { + "Initialize_API_variables": [ + "Succeeded" + ] + }, + "type": "InitializeVariable" + }, + "Get_Synapse_state": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "[concat(variables('managementEndpoint'),variables('getRESTAPI'))]", + "authentication": { "type": "ManagedServiceIdentity" } + }, + "runAfter": { + "Initialize_ActiveQueryCount_variable": [ + "Succeeded" + ] + } + }, + "Parse_JSON": { + "inputs": { + "content": "@body('Get_Synapse_state')", + "schema": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "properties": { + "collation": { + "type": "string" + }, + "creationDate": { + "type": "string" + }, + "maxSizeBytes": { + "type": "integer" + }, + "provisioningState": { + "type": "string" + }, + "restorePointInTime": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "sku": { + "properties": { + "capacity": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "runAfter": { + "Get_Synapse_state": [ + "Succeeded" + ] + }, + "type": "ParseJson" + }, + "PauseSynapseIfOnline": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@body('Get_Synapse_state')['properties']['status']", + "Online" + ] + } + ] + }, + "actions": { + "Pause_SQL_Pool": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[concat(variables('managementEndpoint'),variables('pauseRESTAPI'))]", + "authentication": { "type": "ManagedServiceIdentity" } + }, + "runAfter": { + "Until_ZeroActiveQueries": [ + "Succeeded" + ] + } + }, + "Until_ZeroActiveQueries": { + "type": "Until", + "expression": "@equals(variables('ActiveQueryCount'), 0)", + "limit": { + "count": 3, + "timeout": "PT3H" + }, + "actions": { + "GetActiveQueryCount": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "[concat(variables('managementEndpoint'),variables('aqcRESTAPI'))]", + "authentication": { + "type": "ManagedServiceIdentity" + } + } + }, + "Update_ActiveQueryCount_variable": { + "type": "SetVariable", + "inputs": { + "name": "ActiveQueryCount", + "value": "@body('GetActiveQueryCount')['properties']['activeQueriesCount']" + }, + "runAfter": { + "GetActiveQueryCount": [ + "Succeeded" + ] + } + }, + "Wait5minsIfActiveQuery": { + "type": "If", + "actions": { + "Wait_5mins": { + "inputs": { + "interval": { + "count": 5, + "unit": "Minute" + } + }, + "type": "Wait" + } + }, + "expression": { + "and": [ + { + "greater": [ + "@variables('ActiveQueryCount')", + 0 + ] + } + ] + }, + "runAfter": { + "Update_ActiveQueryCount_variable": [ + "Succeeded" + ] + } + } + } + } + }, + "runAfter": { + "Parse_JSON": [ + "Succeeded" + ] + } + } + }, + "triggers": { + "Recurrence": { + "type": "Recurrence", + "recurrence": { + "frequency": "Week", + "interval": 1, + "timeZone": "[parameters('TIME_ZONE')]", + "startTime": "2019-01-01T00:00:00Z", + "schedule": { + "weekDays": "[variables('recurrenceSchedule')]", + "hours": "[variables('recurrenceHours')]", + "minutes": "[variables('recurrenceMinutes')]" + } + } + } + }, + "contentVersion": "1.0.0.0" + } + } + } + ] +} \ No newline at end of file diff --git a/101-synapse-poc/nestedtemplates/resumetemplate.json b/101-synapse-poc/nestedtemplates/resumetemplate.json new file mode 100644 index 000000000000..6d928a3f8902 --- /dev/null +++ b/101-synapse-poc/nestedtemplates/resumetemplate.json @@ -0,0 +1,317 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "type": "string" + }, + "companyTla": { + "type": "string", + "metadata": { + "description": "This is a Three Letter Acronym for your company name. 'CON' for Contoso for example." + }, + "maxLength": 3 + }, + "deploymentType": { + "type": "string", + "allowedValues": [ + "devtest", + "poc", + "prod", + "shared" + ], + "defaultValue": "poc", + "metadata": { + "description": "Specify deployment type: DevTest, POC, Prod, Shared. This will also be used in the naming convention." + } + }, + "LogicAppName": { + "type": "string", + "metadata": "Name of Azure Logic app" + }, + "Frequency": { + "type": "string", + "metadata": "Choose whether to run schedule every day of the week, or only on weekdays", + "defaultValue": "Weekdays", + "allowedValues": [ + "Daily", + "Weekdays" + ] + }, + "TIME_ZONE": { + "type": "string", + "metadata": "Timezone for the schedule. Consult https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx for more information", + "defaultValue": "Pacific Standard Time", + "allowedValues": [ + "Dateline Standard Time", + "Samoa Standard Time", + "Hawaiian Standard Time", + "Alaskan Standard Time", + "Pacific Standard Time", + "Mountain Standard Time", + "Mexico Standard Time 2", + "Central Standard Time", + "Canada Central Standard Time", + "Mexico Standard Time", + "Central America Standard Time", + "Eastern Standard Time", + "Atlantic Standard Time", + "Newfoundland and Labrador Standard Time", + "E. South America Standard Time", + "S.A. Eastern Standard Time", + "Greenland Standard Time", + "Mid-Atlantic Standard Time", + "Azores Standard Time", + "Cape Verde Standard Time", + "GMT Standard Time", + "Greenwich Standard Time", + "Central Europe Standard Time", + "Central European Standard Time", + "Romance Standard Time", + "W. Europe Standard Time", + "W. Central Africa Standard Time", + "E. Europe Standard Time", + "Egypt Standard Time", + "FLE Standard Time", + "GTB Standard Time", + "Israel Standard Time", + "South Africa Standard Time", + "Russian Standard Time", + "Arab Standard Time", + "E. Africa Standard Time", + "Arabic Standard Time", + "Iran Standard Time", + "Arabian Standard Time", + "Caucasus Standard Time", + "Transitional Islamic State of Afghanistan Standard Time", + "Ekaterinburg Standard Time", + "West Asia Standard Time", + "India Standard Time", + "Nepal Standard Time", + "Central Asia Standard Time", + "Sri Lanka Standard Time", + "Myanmar Standard Time", + "North Asia Standard Time", + "China Standard Time", + "Singapore Standard Time", + "Taipei Standard Time", + "North Asia East Standard Time", + "Korea Standard Time", + "Tokyo Standard Time", + "Yakutsk Standard Time", + "Tasmania Standard Time", + "Vladivostok Standard Time", + "West Pacific Standard Time", + "Central Pacific Standard Time", + "Fiji Islands Standard Time", + "New Zealand Standard Time", + "Tonga Standard Time" + ] + }, + "ResumeTime": { + "type": "string", + "metadata": "Time of day when the data warehouse will be resumed", + "allowedValues": [ + "12:00 AM ( 0:00 )", + "01:00 AM ( 1:00 )", + "02:00 AM ( 2:00 )", + "03:00 AM ( 3:00 )", + "04:00 AM ( 4:00 )", + "05:00 AM ( 5:00 )", + "06:00 AM ( 6:00 )", + "07:00 AM ( 7:00 )", + "08:00 AM ( 8:00 )", + "09:00 AM ( 9:00 )", + "10:00 AM ( 10:00 )", + "11:00 AM ( 11:00 )", + "12:00 PM ( 12:00 )", + "01:00 PM ( 13:00 )", + "02:00 PM ( 14:00 )", + "03:00 PM ( 15:00 )", + "04:00 PM ( 16:00 )", + "05:00 PM ( 17:00 )", + "06:00 PM ( 18:00 )", + "07:00 PM ( 19:00 )", + "08:00 PM ( 20:00 )", + "09:00 PM ( 21:00 )", + "10:00 PM ( 22:00 )", + "11:00 PM ( 23:00 )" + ] + } + }, + "variables": { + "resumeTimeHour": "[split(substring(parameters('resumeTime'), 11, 5), ':')[0]]", + "recurrenceHours": [ "[variables('resumeTimeHour')]" ], + "recurrenceMinutes": [ 0 ], + "dailySchedule": [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ], + "weekdaySchedule": [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" ], + "recurrenceSchedule": "[if(equals(parameters('Frequency'), 'Weekdays'), variables('weekdaySchedule'), variables('dailySchedule'))]", + "resumeTimeString": "[substring(parameters('ResumeTime'), 0, 8)]", + "synapseWorkspaceName": "[toLower(concat(variables('synapseName'),'ws1'))]", + "synapseName": "[toLower(concat(parameters('companyTla'),parameters('deploymentType')))]", + "synapseSQLPoolName": "[toLower(concat(variables('workspaceName'),'p1'))]", + "workspaceName": "[toLower(concat(variables('synapseName'),'ws1'))]", + "managementEndpoint": "[environment().resourceManager]", + "getRESTAPI": "subscriptions/@{variables('RestAPIVariables')['SubscriptionId']}/resourceGroups/@{variables('RestAPIVariables')['ResourceGroupName']}/providers/Microsoft.Synapse/workspaces/@{variables('RestAPIVariables')['workspaceName']}/sqlPools/@{variables('RestAPIVariables')['sqlPoolName']}?api-version=2019-06-01-preview", + "resumeRESTAPI": "subscriptions/@{variables('RestAPIVariables')['SubscriptionId']}/resourceGroups/@{variables('RestAPIVariables')['ResourceGroupName']}/providers/Microsoft.Synapse/workspaces/@{variables('RestAPIVariables')['workspaceName']}/sqlPools/@{variables('RestAPIVariables')['sqlPoolName']}/resume?api-version=2019-06-01-preview" + }, + "resources": [ + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2019-05-01", + "name": "[parameters('LogicAppName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Initialize_API_variables": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "RestAPIVariables", + "type": "Object", + "value": { + "workspaceName": "[variables('synapseWorkspaceName')]", + "sqlPoolName": "[variables('synapseSQLPoolName')]", + "ResourceGroupName": "[resourceGroup().name]", + "SubscriptionId": "[subscription().subscriptionId]", + "TenantId": "[subscription().tenantId]", + "ScheduleTimeZone": "[parameters('TIME_ZONE')]", + "ResumeTime": "[variables('resumeTimeString')]" + } + } + ] + } + }, + "Get_Synapse_state": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "[concat(variables('managementEndpoint'),variables('getRESTAPI'))]", + "authentication": { "type": "ManagedServiceIdentity" } + }, + "runAfter": { + "Initialize_API_Variables": [ + "Succeeded" + ] + } + }, + "Parse_JSON": { + "inputs": { + "content": "@body('Get_Synapse_state')", + "schema": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "properties": { + "collation": { + "type": "string" + }, + "creationDate": { + "type": "string" + }, + "maxSizeBytes": { + "type": "integer" + }, + "provisioningState": { + "type": "string" + }, + "restorePointInTime": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "sku": { + "properties": { + "capacity": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "runAfter": { + "Get_Synapse_state": [ + "Succeeded" + ] + }, + "type": "ParseJson" + }, + "ResumeSynapseIfOnline": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@body('Get_Synapse_state')['properties']['status']", + "Paused" + ] + } + ] + }, + "actions": { + "Resume_SQL_Pool": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[concat(variables('managementEndpoint'),variables('resumeRESTAPI'))]", + "authentication": { "type": "ManagedServiceIdentity" } + } + } + }, + "runAfter": { + "Parse_JSON": [ + "Succeeded" + ] + } + } + }, + "triggers": { + "Recurrence": { + "type": "Recurrence", + "recurrence": { + "frequency": "Week", + "interval": 1, + "timeZone": "[parameters('TIME_ZONE')]", + "startTime": "2019-01-01T00:00:00Z", + "schedule": { + "weekDays": "[variables('recurrenceSchedule')]", + "hours": "[variables('recurrenceHours')]", + "minutes": "[variables('recurrenceMinutes')]" + } + } + } + }, + "contentVersion": "1.0.0.0" + } + } + } + ] +} \ No newline at end of file