Goeleven.com

Gradual Release Using Contextual Feature flags

G

As usage of your software grows, so does the impact of releasing new features.

New features can have both a good and bad effect on the software and its users, the more users there are, the bigger the impact.

Over time the risk of deploying new features increases and as a result the organization becomes more and more risk averse.

Most organizations respond to this risk by introducing:

  • More planning overhead
  • More pre-production environments
  • More testing overhead
  • Less frequent releases

And as a result, less agility.

But did you know that there is a better way instead of trying to prevent the risk?

Gradual release

The trick is to decouple the deployment of a feature from its release.

You can limit the impact of a feature by gradually making it available to more and more people.

At ClubManagement we release features in the following order:

  • First to a handfull of canary (in the coal mine) users
  • Then to all users of the clubs that are identified as early adopters
  • Finally everyone else

Gradual release

Gradual release can be achieved by using centrally managed, contextual, feature flags.

Contextual feature flags

Contextual implies that the feature flags are on for some people, but not for others... it depends on their context.

In the past I've already discussed how clubmanagements client side framework defines features and feature flags, in the remainder of this post we'll look at how to extend this infrastructure to make those features dependent on context as well as centrally managed.

Features and toggles

But first a quick recap of the basic elements of the client side feature infrastructure.

The basic idea is that a feature is responsible for defining a set of web components.

import { GroupSelector } from "./coordination/components/group-selector.js";
import { RoleSelector } from "./coordination/components/role-selector.js";
import { SegmentSelector } from "./coordination/components/segment-selector.js";

class CoordinationSelectorFeature{
    
 constructor(){
  this.name = "Coordination Selector Feature"

  this.isActive = false;
 }    

 activate(){    
  if(this.isActive){
   customElements.define('group-selector', GroupSelector);            
   customElements.define('segment-selector', SegmentSelector);
   customElements.define('role-selector', RoleSelector);
  }
 }
}

If the feature is not active, the web components won't be defined, which will cause the browser not to attach them to the DOM and no client side code will be invoked.

Then there are feature toggles that can iterate the feature list and activate or deactivate any of the features as they see fit.

import shell from "./dish.shell.js";

class CoordinationSelectorToggle{
 async toggle(features){
  var featureName = "CoordinationSelectorFeature";
  var selectorFeature = features.find(f => f.constructor.name == featureName);
  selectorFeature.isActive = true;			
 }
}

shell.featureToggles.push(new CoordinationSelectorToggle());

Centralizing management

To centralize feature management, Microsoft offers the Azure App Configuration service.

Next to configuration settings, this service also allows you to manage feature flags.

Add feature flag

It may sound great that this service has two functions, but it really isn't.

It basically makes all of the front end SDKs unusable, as you really don't want tech savvy users of your app to be able to read all of your server side configuration settings (which may embed some sensitive data) in exchange to getting a feature flag.

This leaves you with only the option to build an api and control the feature flag information exposed.

Configure an API with the feature flag SDK

In order to integrate Azure App Configuration into your aspnet api you'll need to add the following packages:

> Install-Package Microsoft.Extensions.Configuration.AzureAppConfiguration
> Install-Package Microsoft.FeatureManagement.AspNetCore

Integrating app configuration service into the dotnet configuration infrastructure requires you to add it as part of the host builder setup in program.cs (prior to the startup logic).

A connectionstring towards the configuration service must be loaded locally without having access to the configuration infrastructure as you're still configuring it.

An environment variable will suit this purpose.

Note that you probably want to configure it to only load values labelled for a specific environment.

Care must be taken to ensure that both the regular configuration and the feature flag configuration are configured individually (this wasted quite some of my time to be honest).

public class Program
{
 public static void Main(string[] args)
 {
  CreateHostBuilder(args).Build().Run();
 }

 public static IHostBuilder CreateHostBuilder(string[] args) =>
  Host.CreateDefaultBuilder(args)
      .ConfigureWebHostDefaults(webBuilder => {
        webBuilder.ConfigureAppConfiguration((hostingContext, config) =>
  {
   var connectionString = Environment.GetEnvironmentVariable("CUSTOMCONNSTR_configurationservice");
   config.AddAzureAppConfiguration(options =>
   {
    options.Connect(connectionString)
    // Load configuration values with no label
    .Select(KeyFilter.Any, LabelFilter.Null)
    // Override with any configuration values specific to current hosting env
    .Select(KeyFilter.Any, hostingContext.HostingEnvironment.EnvironmentName)  
    // Feature flag label must match environment                              
    .UseFeatureFlags(featureFlagOptions => {
      featureFlagOptions.Label = hostingContext.HostingEnvironment.EnvironmentName;
    });
   });
  });
  webBuilder.UseStartup<Startup.Startup>();
 });
}

Aspnet requires middleware configured in Startup.cs to add configuration and feature management instances into the ioc container and make them available to the request processing pipeline.

public void ConfigureServices(IServiceCollection services)
{
 services.AddAzureAppConfiguration();
 services.AddFeatureManagement()
         // makes it contextual, more on this later
        .AddFeatureFilter<ProfilesFeatureFilter>(); 

 // ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{           
 // ...    
 app.UseAzureAppConfiguration();
 // ...
}

Expose feature flags through the api

Now that aspnet has been configured with the configuration service and feature management extensions, we can start to use them to expose the feature flags to the client app.

In this case a dedicated controller has been set up to do so.

The controller requires authorization so that it know who is requesting the flag.

The profile information is passed into the feature manager (part of the aspnet package included earlier) and out comes a boolean indicating wether the feature is enabled or not.

[Route("featureflags")]
public class FeatureFlagsController : Controller
{
 private readonly IFeatureManager _featureManager;

 public FeatureFlagsController(IFeatureManager featureManager)
 {
  this._featureManager = featureManager;
 }

 [HttpGet("{feature}")]
 [Authorize]
 public async Task<IActionResult> Get([FromRoute] string feature)
 {
  var profileId = User.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Id)?.Value;

  var isEnabled = await _featureManager.IsEnabledAsync(feature, new FeatureContext
  {
    ProfileId = profileId
  });

  return Ok(new
  {
    Enabled = isEnabled,
  });
 }
}

Client side feature flags proxy

On the client side, a proxy class is in charge to send authorized GET requests to the controller and return the result of its evaluation.

import authentication from "../clubmanagement.authentication.app.js";
import config from "./config.js";

class FeatureFlags{
    
 async get(feature){
       
  var uri = config.service + "/featureflags/" + feature;
  var request = await fetch(uri, {
      method: "GET",
      mode: 'cors',
      headers: {
       "Content-Type": "application/json",
       "Authorization": "Bearer " + authentication.getAccessToken()
      }       
    });
  return await request.json();
 }
}

export default new FeatureFlags();

Ugrading the toggle

And now we are back full circle at the feature toggle class.

It can use the proxy class to request to the API wether a specific feature is enabled or not.

If it is, it can look up the feature and activate it.

import shell from "./dish.shell.js";
import featureflags from "./featureflags.js";

class CoordinationSelectorToggle{

 async toggle(features){
  var featureName = "CoordinationSelectorFeature";
  var flag = await featureflags.get(featureName.toLowerCase());
  var selectorFeature = features.find(f => f.constructor.name == featureName);
  selectorFeature.isActive = flag.enabled;			
 }

}

shell.featureToggles.push(new CoordinationSelectorToggle());

Making feature flags contextual

Now that you've enabled the client to use feature flags, it's time to make the flag contextual.

Features are made contextual by adding feature filters to the evaluation infrastructure.

Feature filters

These filters have a name, specified in FilterAlias. When this name is present in the condition section of a feature flag definition, the filter will be called.

[FilterAlias("ProfilesFilter")]
public class ProfilesFeatureFilter : IContextualFeatureFilter<FeatureContext>
{
 public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, FeatureContext featureContext)
 {
   var settings = ConfigurationBinder.Get<ProfilesFilterSettings>(featureFilterContext.Parameters);

   return Task.FromResult(settings.EnabledFor.Contains(featureContext.ProfileId));
 }
}

The filter gets passed in a custom context object, which comes from the context object passed into the IsEnabledAsync operation of the SDK.

public class FeatureContext
{
 public string ProfileId { get; set; }
}

Next to the context it also gets access to the FeatureFilterEvaluationContext which contains the centrally configured parameters.

These parameters must be cast to a well known schema before being used to be evaluate the context.

public class ProfilesFilterSettings
{
  public string[] EnabledFor { get; set; }
}

Configuring the client filter

The settings object needs to have the same schema as the object set to the parameters property, and have the same name as the filter alias, in the client_filters condition of the feature flag.

To edit this data structure, you can to use the advanced edit feature in the azure portal.

Advanced edit

{
 "id": "coordinationselectorfeature",
 "description": "",
 "enabled": true,
 "conditions": {
  "client_filters": [
  {
   "name": "ProfilesFilter",
   "parameters": {
    "EnabledFor": [
      "39ecd2d1-91cb-48cf-b356-03291a67a9b8"
     ]
   }
  }]
 }
}

Other filters

In the code above, I'm only showing the code for a ProfilesFeatureFilter, which allows to expose the feature to the canary users.

To broaden the scope to the early adopter clubs, there is a similar OrganizationsFeatureFilter. But I've omitted that code from the article to keep it as brief as I could.

And ofcourse, to open up the feature to everyone, all that needs to be done is remove the client_filter condition.

Wrapup

Gradual release, using contextual feature flags, has become a corner stone of my development cycle.

It allows me to work on documentation and educational content, instead of responding to users, after a deployment.

While the content progresses I can enable a feature for more and more people and deal with their questions and concerns when I actually have time for it.

Next to the benefits for gradual release, this feature also allows for other pracices, such as A/B testing or feature reduction in the face of outages.

But those are topics for another day...

About the author

YVES GOELEVEN

I love to build web and cloud apps and have been doing so for the past 20 years.

My main areas of expertise are progressive web applications, event driven architecture, domain driven design, event sourcing, messaging, and Microsoft Azure.

Through this blog I hope to share some of this experience with you.

Get in touch

Liked this content? Sign up to my newsletter and never miss any.

You can unsubscribe at any time by clicking the link in the footer of your emails. I use Mailchimp as my marketing platform. By clicking subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.