Dynamic feature flags in Yaml
Overview
In this article, we use Azure CLI in the build pipeline to query the app configuration instance and pass whether a feature flag should be enabled to the ARM template.
Feature Flags
Using Feature Flags within a solution provides a great mechanism for rapid releases, they enable the developer to essentially release a piece of functionality that is switched off. There are many scenarios where this is helpful but one that I have come up against most recently is the audience are not ready to use the new functionality; this could relate to the new functionality requiring training or further comms but either way, this should not hold up the release.
With the introduction of feature flags, the code can be released and the feature branch merged into master. As you would expect, Martin Fowler says it better than me so please refer to his article for a more indepth look of Feaure Flags
Azure App Configuration
Step in Azure App Configuration, it provides a way to easily manage the flags and rapidly enable functionality. It can also be used to pilot functionality with Feature Flag Filters, a bit of a mouthful but essentially its possible to target the new functionality to a specific audience.
Provisioning
Its straight forward to provision the App Configuration resource in a standard ARM template, it’s a little trickier to dynamically add feature flags to the app configuration instance.
{
"type": "Microsoft.AppConfiguration/configurationStores",
"apiVersion": "2020-06-01",
"tags": "[parameters('tags')]",
"name": "[parameters('appConfigurationName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "standard"
},
"properties": {
"encryption": {}
}
},
{
"type": "Microsoft.AppConfiguration/configurationStores/keyValues",
"apiVersion": "2020-07-01-preview",
"name": "[concat(parameters('appConfigurationName'), '/.appconfig.featureflag~2FRemoveOldAccountsEnabled')]",
"dependsOn": [
"[parameters('appConfigurationName')]"
],
"properties": {
"enabled": "false",
"contentType": "application/vnd.microsoft.appconfig.ff+json;charset=utf-8",
"value": "[concat('{\"id\":\"RemoveOldAccountsEnabled\",\"description\":\"\",\"enabled\":\"false\",\"conditions\":{\"client_filters\":[]}}')]"
}
}
NB: Its important to note that the forward slash needs to be encoded to "~2F" in the keyValues name
With the above template, the feature flag will be added in the disabled state, the problem I have noticed is that subsequent runs of the template provisioning process will disable the flag if it has been enabled since it was previously run.
Solution
To get around this, we can use Azure CLI in the build pipeline to query the app configuration instance and pass whether it should be enabled to the ARM template.
YAML
The following snippet of YAML can be integrated into your build pipeline tasks, in the scenario below I am using library variables to store the app config and subscription name.
# Gets and sets the feature flags
- task: AzureCLI@2
displayName: 'Get App Configuration Feature Flags'
inputs:
azureSubscription: '${{ parameters.azureServiceConnection }}'
scriptType: pscore
scriptLocation: inlineScript
inlineScript: |
$featureFlags = az appconfig feature list -n $(AppConfigurationName) --subscription $(AzureSubscriptionId) --only-show-errors
if ((($featureFlags | ConvertFrom-Json) | ? { $_.Key -eq 'RemoveOldAccounts'}).state -eq 'on') { $removeOldAccountsEnabled = $true } else {$removeOldAccountsEnabled = $false}
Write-Output("##vso[task.setvariable variable=RemoveOldAccountsEnabled;]$($removeOldAccountsEnabled.ToString().ToLower())")
# Replaces any tokens in the parameters file
- task: qetza.replacetokens.replacetokens-task.replacetokens@3
displayName: 'Replace Tokens'
inputs:
targetFiles: 'featureFlag-parameters.json'
tokenPrefix: '#{'
tokenSuffix: '}'
ARM
Now that we have the state of the feature flag being generated in the pipeline, we can inject it into the template as follows:
featureFlag-parameters.json
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"removeOldAccountsEnabled": {
"value": "#{RemoveOldAccountsEnabled}"
}
}
}
featureFlag.json
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"removeOldAccountsEnabled": {
"type": "string"
}
}
"variables": {},
"resources": [
{
"type": "Microsoft.AppConfiguration/configurationStores/keyValues",
"apiVersion": "2020-07-01-preview",
"name": "[concat(parameters('appConfigurationName'), '/.appconfig.featureflag~2FRemoveOldAccountsEnabled')]",
"dependsOn": [
"[parameters('appConfigurationName')]"
],
"properties": {
"enabled": "[bool(parameters('removeOldAccountsEnabled'))]",
"contentType": "application/vnd.microsoft.appconfig.ff+json;charset=utf-8",
"value": "[concat('{\"id\":\"RemoveOldAccountsEnabled\",\"description\":\"\",\"enabled\":',parameters('removeOldAccountsEnabled'),',\"conditions\":{\"client_filters\":[]}}')]"
}
}
]
}