Aller au contenu

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.

Architecture 1

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:

Architecture 2

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.

RBAC

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.

pipeline CI

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 pipeline CD

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

Thanks

Written by Philippe MORISSEAU, Published on May 3, 2022.