The other day I stumbled upon the brand new (but long awaited) ARM support for CosmosDB databases and collections. CosmosDB ARM support used to be limited to just provisioning the database account. Everything inside it, such as databases and collections, had to be provisioned using some other mechanism, such as PowerShell or Azure CLI – or heaven forbid, the portal.
But that’s over: support for ARM is finally here! It’s not all perfect yet, though. I guess Rome wasn’t build in a day either, so let’s count our blessings – and find workarounds for what’s still missing.
One of those workarounds has to do with provisioning and updating throughput (i.e. RU/s) on either a database or a collection. Provisioning throughput upon creating a new database can be done by setting an options
object with a throughput
property in the database resource for example, like so:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"type": "Microsoft.DocumentDB/databaseAccounts/apis/databases", | |
"name": "[concat(variables('databaseAccountName'), '/sql/', variables('databaseName'))]", | |
"apiVersion": "2016-03-31", | |
"dependsOn": [ "[resourceId('Microsoft.DocumentDB/databaseAccounts/', variables('databaseAccountName'))]" ], | |
"properties": { | |
"resource": { | |
"id": "[variables('databaseName')]" | |
}, | |
"options": { | |
"throughput": "400" | |
} | |
} | |
} |
But, changing that value after initial creation is not allowed. Updates to that value can instead be passed through a nested settings
resource, like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"type": "Microsoft.DocumentDB/databaseAccounts/apis/databases/settings", | |
"name": "[concat(variables('databaseAccountName'), '/sql/', variables('databaseName'), '/throughput')]", | |
"apiVersion": "2016-03-31", | |
"dependsOn": [ | |
"[resourceId('Microsoft.DocumentDB/databaseAccounts/apis/databases', variables('databaseAccountName'), 'sql', variables('databaseName'))]" | |
], | |
"properties": { | |
"resource": { | |
"throughput": "500" | |
} | |
} | |
} |
And this settings
resource, in turn, is only valid as a child to a parent resource that is itself already provisioned with throughput – so a template only holding a settings
resource with throughput in it (e.g. without the options
object) fails upon creating. So, it seems that updating throughput requires a different template than the one that was used for the initial creation. That’s not how I like my ARM templates…
So I set out to devise a workaround, where the end goal is to have a deployment pipeline that uses ARM wherever possible, and that can be run multiple times yielding the same result, i.e. is idempotent. I found that, while you need to use the options
object to initially provision the throughput, the throughput is allowed to be set to null
upon subsequent deployments. This can be leveraged by some piece of simple logic, that conditionally sets this value to either the specified throughput, or null
, depending on whether it’s an update or a create:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"type": "Microsoft.DocumentDB/databaseAccounts/apis/databases", | |
"name": "[concat(variables('databaseAccountName'), '/sql/', variables('databaseName'))]", | |
"apiVersion": "2016-03-31", | |
"dependsOn": [ "[resourceId('Microsoft.DocumentDB/databaseAccounts/', variables('databaseAccountName'))]" ], | |
"properties": { | |
"resource": { | |
"id": "[variables('databaseName')]" | |
}, | |
"options": { | |
"throughput": "[if(parameters('isUpdate'), json('null'), parameters('throughput'))]" | |
} | |
} | |
} |
Now, I just need to pass that flag to the template from the outside. For that, I can use an Azure CLI or PowerShell task to determine whether the database already exists, and pass the result of that into the ARM template as an input parameter. I won’t go into the details here, but this should be easy to implement.
Obviously, this is not ideal. I’m sacrificing the decoupling of my ARM template from other tasks in my pipeline: the template depends on another task to provide input instead of just a parameter file, and I no longer have the option to let the ARM template figure out the database name based on naming conventions or whatever, because I need to know that name up front in order to be able to pass it to the CLI / PowerShell task. But, putting it all together, I do have a single template used for creates and updates, and a single place (i.e. the parameter file) to keep track of the throughput I provisioned. To me, that’s something worth a sacrifice.
The full ARM template looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", | |
"contentVersion": "1.0.0.0", | |
"parameters": { | |
"isUpdate": { | |
"type": "bool" | |
}, | |
"throughput": { | |
"type": "int", | |
"minValue": 400, | |
"maxValue": 1000000 | |
} | |
}, | |
"variables": { | |
"databaseAccountName": "docsdbaccount", | |
"databaseName": "docsdb", | |
"docsCollectionName": "docs" | |
}, | |
"resources": [ | |
{ | |
"apiVersion": "2015-04-08", | |
"kind": "GlobalDocumentDB", | |
"location": "[resourceGroup().location]", | |
"name": "[variables('databaseAccountName')]", | |
"properties": { | |
"name": "[variables('databaseAccountName')]", | |
"databaseAccountOfferType": "Standard", | |
"locations": [ | |
{ | |
"failoverPriority": 0, | |
"locationName": "[resourceGroup().location]" | |
} | |
] | |
}, | |
"tags": { | |
"defaultExperience": "DocumentDB" | |
}, | |
"type": "Microsoft.DocumentDB/databaseAccounts" | |
}, | |
{ | |
"type": "Microsoft.DocumentDB/databaseAccounts/apis/databases", | |
"name": "[concat(variables('databaseAccountName'), '/sql/', variables('databaseName'))]", | |
"apiVersion": "2016-03-31", | |
"dependsOn": [ "[resourceId('Microsoft.DocumentDB/databaseAccounts/', variables('databaseAccountName'))]" ], | |
"properties": { | |
"resource": { | |
"id": "[variables('databaseName')]" | |
}, | |
"options": { | |
"throughput": "[if(parameters('isUpdate'), json('null'), parameters('throughput'))]" | |
} | |
}, | |
"resources": [ | |
{ | |
"type": "Microsoft.DocumentDB/databaseAccounts/apis/databases/settings", | |
"name": "[concat(variables('databaseAccountName'), '/sql/', variables('databaseName'), '/throughput')]", | |
"apiVersion": "2016-03-31", | |
"dependsOn": [ | |
"[resourceId('Microsoft.DocumentDB/databaseAccounts/apis/databases', variables('databaseAccountName'), 'sql', variables('databaseName'))]" | |
], | |
"properties": { | |
"resource": { | |
"throughput": "[parameters('throughput')]" | |
} | |
} | |
}, | |
{ | |
"type": "Microsoft.DocumentDb/databaseAccounts/apis/databases/containers", | |
"name": "[concat(variables('databaseAccountName'), '/sql/', variables('databaseName'), '/', variables('docsCollectionName'))]", | |
"apiVersion": "2016-03-31", | |
"dependsOn": [ | |
"[resourceId('Microsoft.DocumentDB/databaseAccounts/apis/databases', variables('databaseAccountName'), 'sql', variables('databaseName'))]" | |
], | |
"properties": { | |
"resource": { | |
"id": "[variables('docsCollectionName')]" | |
} | |
} | |
} | |
] | |
} | |
], | |
"outputs": { } | |
} |
Hope this helps!