DevOps with VSTS - The Third Way - Feature Flags

Posted on Sunday, February 19, 2017

This is part 9 of the DevOps with VSTS series

As part of The Third Way, Feature Flags provide functionality for savely experimenting in a live environment without impacting all customers.
The Feature Flag pattern does this by enabling or disabling a specific feature without the need to redeploy the application.
In essence this boils down to boolean checks inside the code.
When a new feature has been deployed for some customers and it is giving the desired value, the feature flag could then be removed entirely to prevent overcomplicating the application.

The project used for this blogpost is using Azure Active Directory for authentication.
If a feature flags is enabled for a customer, it should be stored somewhere and since the project uses AAD and AAD stores the customer/user data, storing the Feature Flag values in AAD as well would be a possible solution.

To start with the Feature Flags, an enum to specify the different Flags is added to the project.

namespace DevOpsWithVsts.Web.FeatureFlag
{
    using System.ComponentModel.DataAnnotations;

    public enum FeatureFlags
    {
        [Display(Name = "New layout")]
        NewLayout
    }
}

The next thing that's needed is a class that helps with initializing the Feature Flags and determining if a user has enabled a specific Feature Flag.
Initializing will make sure all new Feature Flags are created as AAD Groups and old Feature Flags are removed.

namespace DevOpsWithVsts.Web.FeatureFlag
{
    using DevOpsWithVsts.Web.Aad;
    using DevOpsWithVsts.Web.Authentication;
    using Microsoft.ApplicationInsights;
    using Microsoft.ApplicationInsights.DataContracts;
    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    using System;
    using System.Linq;
    using System.Threading.Tasks;

    public class FeatureFlagManager : IFeatureFlagManager
    {
        private const string AadGroupFeatureFlagPrefix = "FF_";
        private bool isInitialized;
        private object initializeLock = new object();

        private readonly IAadClient aadClient;
        private readonly IClaimsPrincipalService claimsPrincipalService;

        public FeatureFlagManager(IAadClient aadClient, IClaimsPrincipalService claimsPrincipalService)
        {
            this.aadClient = aadClient;
            this.claimsPrincipalService = claimsPrincipalService;
        }

        public async Task Initialize()
        {
            var runInitialization = false;
            lock (initializeLock)
            {
                runInitialization = !isInitialized;
                isInitialized = true;
            }

            if (runInitialization)
            {
                try
                {
                    var featureFlagNames = Enum.GetNames(typeof(FeatureFlags));
                    var existingFeatureFlags = await this.aadClient.GetGroups();

                    // First add any new featureFlag
                    foreach (var newFeatureFlag in
                        featureFlagNames
                            .Where(ff => !existingFeatureFlags.Any(eff => eff.DisplayName == $"{AadGroupFeatureFlagPrefix}{ff}")))
                    {
                        await this.aadClient.AddGroup($"{AadGroupFeatureFlagPrefix}{newFeatureFlag}");
                    }

                    // Remove old featureFlags
                    foreach (var oldFeatureFlag in
                        existingFeatureFlags
                            .Where(eff => !featureFlagNames.Any(ff => $"{AadGroupFeatureFlagPrefix}{ff}" == eff.DisplayName)))
                    {
                        await this.aadClient.RemoveGroup(oldFeatureFlag);
                    }
                }
                catch (AdalException)
                {
                    isInitialized = false;
                }
            }
        }

        public Task<bool> IsFeatureFlagEnabledForCurrentUser(FeatureFlags featureFlag)
        {
            var userId = this.claimsPrincipalService.UserId;
            return IsFeatureFlagEnabled(userId, featureFlag);
        }

        public async Task<bool> IsFeatureFlagEnabled(string userId, FeatureFlags featureFlag)
        {
            var user = await this.aadClient.GetUser(userId);
            var featureFlagGroup = await this.aadClient.GetGroupByName($"{AadGroupFeatureFlagPrefix}{Enum.GetName(typeof(FeatureFlags), featureFlag)}");
            var memberGroups = await user.GetMemberGroupsAsync(false);
            return memberGroups.Any(g => g == featureFlagGroup.ObjectId);
        }

        public Task SetFeatureFlagForCurrentUser(FeatureFlags featureFlag, bool enabled)
        {
            var userId = this.claimsPrincipalService.UserId;
            return SetFeatureFlag(userId, featureFlag, enabled);
        }

        public async Task SetFeatureFlag(string userId, FeatureFlags featureFlag, bool enabled)
        {
            var user = await this.aadClient.GetUser(userId);
            var featureFlagName = Enum.GetName(typeof(FeatureFlags), featureFlag);
            var featureFlagGroup = await this.aadClient.GetGroupByName($"{AadGroupFeatureFlagPrefix}{featureFlagName}");
            var memberGroups = await user.GetMemberGroupsAsync(false);

            if (!enabled && memberGroups.Any(g => g == featureFlagGroup.ObjectId))
            {
                await this.aadClient.RemoveUserFromGroup(userId, featureFlagGroup.ObjectId);
                var ai = new TelemetryClient();
                ai.TrackTrace($"{user.DisplayName} disabled Feature Flag {featureFlagName}", SeverityLevel.Warning);
            }

            if (enabled && !memberGroups.Any(g => g == featureFlagGroup.ObjectId))
            {
                await this.aadClient.AddUserToGroup(userId, featureFlagGroup.ObjectId);
                var ai = new TelemetryClient();
                ai.TrackTrace($"{user.DisplayName} enabled Feature Flag {featureFlagName}", SeverityLevel.Information);
            }
        }
    }
}

With this in place, it's very easy to start working with Feature Flags in the source code.
As an example, the project used for this blogpost has a Feature Flag 'New layout'.
This Feature is very simple, it uses a custom css file 'SiteNew.css' to update the color of the navigation menu.
If this Feature Flag is enabled for a user, the project uses the 'SiteNew.css' file instead of the default 'Site.css'.
For this specific use case, a boolean is set on the ViewBag which can be used by _Layout.cshtml.

ViewBag.NewLayoutEnabled = await this.featureFlagManager.IsFeatureFlagEnabledForCurrentUser(FeatureFlags.NewLayout);
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")

@if (ViewBag.NewLayoutEnabled != null && ViewBag.NewLayoutEnabled)
{
    @Styles.Render("~/Content/SiteNew.css")
}

Using the SetFeatureFlagForCurrentUser method, it is also very easy to enable users themselves to opt-in or opt-out of new features.
When a user does this, telemetry is send to AppInsights so the usage of the feature can be tracked.
As an example, if a user enabled a Feature Flag and then disables it again, a warning is send to AppInsights.
If a user disables the feature, it could be because the feature isn't working as expected for the user and this feedback is important to know.
When a user disables a feature, the application could also ask the user for the reason and send that information to AppInsights as part of the warning.

With the Feature Flags functionality set up, companies can experiment with new features more easily and using the telemetry determine if the feature is bringing enough value to release it for all customers.


Disclaimer: Any views or opinions expressed on this blog are my own personal ones and do not represent my employer in any way.