Adopt a GitOps approach with Azure App Configuration (Part 2)
In the first part we saw how to use Azure App Configuration with different applications and constraints: configuration vault, push or pull. In this second part we will see together how to use the git repos and the Azure DevOps pipelines to manage and push the configuration to Azure App Configuration.
Reminder of the GitOps approach
The GitOps approach consists of managing the configuration of applications using a source manager like git. git offers the advantage of providing a history of modifications thanks to commits and of being able to automate the deployment of the configuration thanks to CI/CD pipelines.
Each configuration change produces a git commit. This triggers a pipeline that will deploy the configuration.
To this, we will be able to add the branching model (TBD, Gitflow, github flow, ...) and the semantic versioning, to condition the configuration deployment.
Architecture (reminder)
The goal is to use Azure DevOps and Azure App Configuration together to distribute the configuration to the different elements of our distributed application.
We will push the configuration from Azure DevOps to Azure App Configuration and configure our various components of our distributed architecture to retrieve the configuration from the latter.
If you have network isolating Azure App Configuration using a Private Endpoint, you will need to be sure to use Self-hosted Azure DevOps agents to deploy your configuration (see my article). Here is an example of an architecture diagram with network isolation:
The Azure DevOps implementation
Create the Azure DevOps <-> Azure connection
If you already use Azure DevOps to deploy your applications on Azure this means that you have most certainly already configured a service connection in your project. In this case, you just have to note the Service Principal Id. Otherwise, you can follow the procedure described here.
Once your connection service is operational, you will need to give it permission to manage the configuration of your Azure App Configuration. To do this, in Azure, add the App Configuration Data Owner
(RBAC) role at the Azure App Configuration level.
Azure DevOps can now manage your configuration for you.
Manage your setup
Your configuration will have to be managed in Json or Yaml files.
Note
I recommend using the Json format. Indeed, this format is much easier to manage with Azure DevOps tasks than the Yaml format.
In order to simplify management, I advise you to separate your configuration into different files:
- a file for the "classic" parameters,
- a file for the Feature flags,
- a file for references to your Azure Keyvault.
Let's start with the "classic" settings file. Nothing's easier. For each key, you will set the value. For example in yaml you will have:
TestApp:Settings:Param1: ValueOfParam1
TestApp:Settings:Param2: ValueOfParam2
All:Settings:Param3: ValueOfParam3
For the file containing the Features flags, it will be a bit more complicated. For each Feature flags, you will have to define a node containing the parameters id, enabled and optionally description and conditions. And the key must start with .appconfig.featureflag/
.
Below is an example in json to change ;-):
{
".appconfig.featureflag/flag001": {
"id": "Flag001",
"enabled": true
},
".appconfig.featureflag/flag002": {
"id": "Flag002",
"enabled": true,
"conditions": {
"client_filters": [
{
"name": "Microsoft.TimeWindow",
"parameters": {
"Start": "Thu, 19 May 2022 22:00:00 GMT",
"End": "Sat, 21 May 2022 22:00:00 GMT"
}
}
]
}
},
".appconfig.featureflag/flag003": {
"id": "Flag003",
"description": "Lorem ipsum dolor sit amet",
"enabled": true,
"conditions": {
"client_filters": [
{
"name": "Microsoft.Targeting",
"parameters": {
"Audience": {
"Users": [],
"Groups": [
{
"Name": "groupA",
"RolloutPercentage": 0
},
{
"Name": "groupB",
"RolloutPercentage": 100
}
],
"DefaultRolloutPercentage": 50
}
}
}
]
}
}
}
Finally, for the references file to your Azure Keyvault, you will have to define a node containing the uri parameter with the url to access your secret. For example (in json):
{
"TestApp:Settings:Secret1": {
"uri":"https://[YourKeyVaultName].vault.azure.net/secrets/Secret1"
},
"TestApp:Settings:Secret2": {
"uri":"https://[YourKeyVaultName].vault.azure.net/secrets/Secret2"
},
"TestApp:Settings:Secret3": {
"uri":"https://[YourKeyVaultName].vault.azure.net/secrets/Secret3"
}
}
All that remains is to push these different files to a dedicated repository.
To manage all this, you will have to automate the deployment and above all manage the "sentinel" parameter capable of indicating to the applications that the configuration has changed! (cf. Refresh the configuration in "Pull" mode)
Sematic versioning and sentinel.
Our "sentinel" parameter must indicate a modification of the configuration in order to force the refresh of this one at the level of the applications. For that, why not use semantic versioning?
We are going to run a small script that will add a "sentinel" parameter to our "classic" configuration file. This will be evaluated with the desired version.
Below is an example of a powershell script that will modify the yaml file to add the TestApp:Settings:Sentinel
parameter:
# Arguments that get passed to the script when running it
param (
[Parameter(Position=1)]
$yamlFile,
[Parameter(Position=2)]
$version
)
# Install and import the `powershell-yaml` module
# Install module has a -Force -Verbose -Scope CurrentUser arguments which might be necessary in your CI/CD environment to install the module
Install-Module -Name powershell-yaml -Force -Verbose -Scope CurrentUser
Import-Module powershell-yaml
# LoadYml function that will read YML file and deserialize it
function LoadYml {
param (
$FileName
)
# Load file content to a string array containing all YML file lines
[string[]]$fileContent = Get-Content $FileName
$content = ''
# Convert a string array to a string
foreach ($line in $fileContent) { $content = $content + "`n" + $line }
# Deserialize a string to the PowerShell object
$yml = ConvertFrom-YAML $content
# return the object
Write-Output $yml
}
# WriteYml function that writes the YML content to a file
function WriteYml {
param (
$FileName,
$Content
)
#Serialize a PowerShell object to string
$result = ConvertTo-YAML $Content
#write to a file
Set-Content -Path $FileName -Value $result
}
# Loading yml, setting new values and writing it back to disk
$yml = LoadYml $yamlFile
$yml.'TestApp:Settings:Sentinel' = $version
WriteYml $yamlFile $yml
When setting up CI/CD pipelines, I particularly enjoy using tools like GitVersion. This utility will allow you to manage your sematic versioning from your git history: your commits, your tags, your branches.
To do this, you will need to add a GitVersion.yml file to your repository git. This file will define how to infer the version from your history.
Here is a simple example with Trunk Based Development:
mode: ContinuousDeployment
assembly-versioning-scheme: MajorMinorPatch
tag-prefix: '[vV]'
continuous-delivery-fallback-tag: ci
major-version-bump-message: '\+semver:\s?(breaking|major)'
minor-version-bump-message: '\+semver:\s?(feature|minor)'
patch-version-bump-message: '\+semver:\s?(fix|patch)'
legacy-semver-padding: 5
build-metadata-padding: 5
commits-since-version-source-padding: 5
commit-message-incrementing: Enabled
branches:
master:
mode: ContinuousDeployment
tag: unstable
increment: Minor
prevent-increment-of-merged-branch-version: true
track-merge-target: false
release:
regex: releases?[/-]
increment: Patch
tag: stable
prevent-increment-of-merged-branch-version: true
track-merge-target: false
All that remains is to automate the deployment of the configuration.
The CI/CD pipeline
Let's start with the CI. Contrary to what one might think, continuous integration will be useful for:
- Define the value of the "sentinel" parameter,
- Pack the configuration in order to be able to reproduce this configuration.
And in the case of a Azure DevOps multi-stage pipeline, this is what it can give:
stages:
- stage: Prepare
displayName: Prepare
jobs:
- job: PrepareConfiguration
displayName: Prepare configuration
pool:
vmImage: 'ubuntu-latest'
steps:
# Télécharge gitversion (s'il n'existe pas)
- task: gittools.gittools.setup-gitversion-task.gitversion/setup@0
displayName: gitversion/setup
inputs:
versionSpec: '5.*'
# Définit la version
- task: gittools.gittools.execute-gitversion-task.gitversion/execute@0
displayName: gitversion/execute
inputs:
useConfigFile: true
configFilePath: GitVersion.yml
# Définit le paramètre sentinelle
- task: PowerShell@2
displayName: setSentinelVersion
inputs:
filePath: tools/setSentinel.ps1
arguments: '-yamlFile configuration.yml -version $(GitVersion.FullSemVer)'
# Copie la configuration à insérer dans l'artefact
- task: CopyFiles@2
displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)'
inputs:
Contents: config*.yml
TargetFolder: '$(Build.ArtifactStagingDirectory)'
# Publie l'artefact
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'
Once our configuration deliverable is ready, all that remains is to deploy it on Azure App Configuration.
You can easily do this with the AzureAppConfiguration.azure-app-configuration-task-push.custom-build-release-task.AzureAppConfigurationPush@3
task.
The problem with this task is that it cannot deploy all configurations at once. Deploy by parameter type by passing a distinct value to the ContentType
parameter:
- For vault references:
application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8
- For features flags:
application/vnd.microsoft.appconfig.ff+json;charset=utf-8
And in the case of a Azure DevOps multi-stage pipeline, this can give:
- stage: Deploy
displayName: Deploy
jobs:
- deployment: DeployConfig
pool:
vmImage: 'ubuntu-latest'
environment: YOUR_ENVIRONMENT
workspace:
clean: all
strategy:
runOnce:
deploy:
steps:
# Publie les références à votre Azure Keyvault
- task: AzureAppConfiguration.azure-app-configuration-task-push.custom-build-release-task.AzureAppConfigurationPush@3
displayName: 'Azure App Configuration KeyVault'
inputs:
azureSubscription: ${{variables.azureSubscription}}
AppConfigurationEndpoint: ${{variables.AppConfigurationEndpoint}}
ConfigurationFile: $(Pipeline.Workspace)/drop/configurationKeyVault.yml
Separator: .
Depth: '1'
ContentType: 'application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8'
# Publie les features flags
- task: AzureAppConfiguration.azure-app-configuration-task-push.custom-build-release-task.AzureAppConfigurationPush@3
displayName: 'Azure App Configuration Feature Flags'
inputs:
azureSubscription: ${{variables.azureSubscription}}
AppConfigurationEndpoint: ${{variables.AppConfigurationEndpoint}}
ConfigurationFile: $(Pipeline.Workspace)/drop/configurationFeatureFlags.yml
Separator: .
Depth: '1'
ContentType: 'application/vnd.microsoft.appconfig.ff+json;charset=utf-8'
# Publie la configuration "classique"
- task: AzureAppConfiguration.azure-app-configuration-task-push.custom-build-release-task.AzureAppConfigurationPush@3
displayName: 'Azure App Configuration'
inputs:
azureSubscription: ${{variables.azureSubscription}}
AppConfigurationEndpoint: ${{variables.AppConfigurationEndpoint}}
ConfigurationFile: $(Pipeline.Workspace)/drop/configuration.yml
Separator: .
Depth: '1'
Note
The azureSubscription
and AppConfigurationEndpoint
parameters correspond respectively to the service connection configured on your Azure DevOps project and to the url of your Azure App Configuration service.
Now we have our repo. git and our CI/CD pipeline which allows us to deploy the configuration.
END !
Conclusion
Attendez un peu...
Our configurations will certainly be different depending on our environments. How to manage these distinct configurations? Modifying the configuration of the acceptance environment does not have to impact the configuration of the integration or production environment. Does that mean I have to put this in separate files? in separate repositories? or even elsewhere?
We will discuss in the third part the configuration management by environment.
To be continued...
References
- Microsoft : Azure App Configuration
- Azure DevOps : Create a service connection
- GitVersion
- Push settings to App Configuration with Azure Pipelines
Thanks
- Oussama Mouchrit : for proofreading
Written by Philippe MORISSEAU, Published on May 3, 2022.