O que é Feature Flag?
Feature flag é uma técnica que permite habilitar ou desabilitar funcionalidades de forma dinâmica, sem a necessidade de alteração no código. Essencialmente, funciona como um "interruptor" (ou "toggle" ou "flag") que controla o comportamento de uma aplicação.
O uso dessa técnica facilita a gestão de novas atualizações, impacto de dependência de publicação coordenada entre times, além de permitir estratégias de experimentação de novas funcionalidades ou otimizações.
Configurando o Microsoft.FeatureManagement em Azure Functions
Dependendo do seu caso, pode ser que a velha variável de ambiente resolva ou alguma implementação customizada, contudo o pacote Microsoft.FeatureManagement atende tanto o cenário mais simples, quanto um mais complexo, pois já tem built-in várias funcionalidades como estratégia de distribuição de variantes, tempo de disponibilidade de uma funcionalidade entre outros. E também vale ressaltar sua capacidade de customização, permitindo criar a sua regra de como e quando determinada feature-flag deve estar habilitada ou não.
Nesse exemplo estarei usando uma Azure Function in-process com .Net 8 para exemplificar a configuração e uso do pacote Microsoft.FeatureManagement de forma prática. O fundamento aqui serve para outros modelos de aplicação .Net, mas o básico aqui dá para ter um pontapé inicial.
O primeiro exemplo que encontramos na documentação é usando o recurso App Configuration que centraliza o gerenciamento de configurações e feature flags da aplicação.
Para isso é necessário criar o recurso, na primeira interação precisará atribuir o papel de owner e então conseguirá editar e acessar as chaves de conexão.
Feito isso é possível configurar a conexão na aplicação
using System;
using Caramelo.FoodDispenser;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.FeatureManagement;
[assembly: FunctionsStartup(typeof(Startup))]
namespace Caramelo.FoodDispenser;
public class Startup : FunctionsStartup
{
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
builder.ConfigurationBuilder.AddAzureAppConfiguration(options =>
{
var connString = Environment.GetEnvironmentVariable("AZ_APP_CONFIG_CONN");
options
.Connect(connString)
.Select("_")
.UseFeatureFlags();
});
}
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement();
}
}
💡
O método .Select("_") é usado apenas para exemplificação — ele filtra todas as chaves começando com o prefixo _, o que na prática ignora as demais chaves do App Configuration.Mais detalhes sobre isso Aqui.
Em produção, a variável de ambiente
AZ_APP_CONFIG_CONNdeve ser configurada nosApplication Settingsdo Function App. Mais detalhe sobre isso AquiO exemplo usa Azure Functions in-process (.NET 8), mas a configuração é muito semelhante no modelo Isolated, mudando apenas a forma de injeção de dependências.
Na implementação vamos criar a function Http Trigger
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using Newtonsoft.Json;
namespace Caramelo.FoodDispenser;
public class FeedDog(IFeatureManagerSnapshot featureManager)
{
[FunctionName("FeedDog")]
public async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "feed")]
HttpRequest req,
ILogger log)
{
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonConvert.DeserializeObject<FeedingRequest>(requestBody);
int portions;
if (await featureManager.IsEnabledAsync("GreedyMode"))
{
log.LogInformation("Greedy Mode activated 🥩 — doubling portion!");
portions = data.Portions * 2;
await DispenseFood(data.DeviceId, portions);
}
else
{
log.LogInformation("Normal Mode 🍖 — regular portion.");
portions = data.Portions;
await DispenseFood(data.DeviceId, portions);
}
return new OkObjectResult(new FeedingResponse(data.DogName, data.DeviceId, portions));
}
private static Task DispenseFood(string deviceId, int portions)
{
// lógica simulada
return Task.CompletedTask;
}
}
public record FeedingRequest(string DogName, string DeviceId, int Portions);
public record FeedingResponse(string DogName, string DeviceId, int Portions);
O principal está na injeção de IFeatureManagerSnapshot e seu uso em await featureManager.IsEnabledAsync("GreedyMode").
Nesse trecho estamos verificando se a feature flag GreedyMode está habilitada e aplicando o comportamento alternativo.
💡 É possível usar o
IFeatureManagerao invés doIFeatureManagerSnapshot, contudo o segundo faz com que as configurações de features flags sejam consistentes em tempo de requisição. Ou seja, mesmo que dois serviços injetem a classe e ela troque de valor em tempo de requisição, será sempre considerado o valor inicial.
Mas para que funcione conforme esperado, antes é necessário cadastrar essa feature flag.
Cadastrando uma feature flag no App Configuration
De volta ao Portal Azure, acessando o recurso App Configuration é possível encontrar a opção Feature manager:

Clicando nela, na parte superior existe um botão Criar que ao clicar apresenta duas opções Feature Flag e Variant Feature Flag. Sem entrar muito em detalhes, ambas são muito parecidas, mas a segunda permite que tenha mais estados que On e Off, adicionando mais variações. Vamos focar na primeira mesmo nesse exemplo.
Na tela de criação é possível adicionar algumas descrições para facilitar a identificação do motivo da feature flag e efeito na aplicação, mas o importante para a aplicação é o atributo Key.
O nome da Key deve corresponder exatamente ao valor usado em IsEnabledAsync().
🐞 Não recebi nenhum erro ao criar com uma Key com nome composto separado por espaço, mas pode gerar confusão, então recomendo escolher sua convenção favorita e adotar ela. Atenção também com espaços no final da Key (Sim eu sofri com isso).
Filtros
Ainda na tela de criação existe a opção de adicionar filtros, caso marque o checkbox e clique no botão Criar irá ser exibido o menu para configuração do filtro.
A funcionalidade já possui 2 tipos de filtros built-in, Time window e Targeting.
- Time window: permite que a feature flag, se habilitada, só tenha efeito durante uma janela de tempo configurada.
- Targeting: permite que a feature flag, se habilitada, só tenha efeito para usuários ou grupos especificados.
💡 Existem outros tipos de filtro built-in não disponíveis por padrão na interface do
App Configuration.
Também é possível criar o seu próprio filtro, de forma com que a aplicação saiba interpretar e aplicar o comportamento esperado.
Para esse exemplo não será aplicado nenhum filtro
Habilitando e desabilitando uma feature flag
Criada a feature flag é possível observar o resultado da operação conforme configurado no App Configuration.
Greedy Mode activated 🥩 — doubling portion!
Desabilitando:
Normal Mode 🍖 — regular portion.
Embora o exemplo seja simples, o fundamento se aplica em outros cenários mais complexos.
Alternativas ao App Configuration
As configurações de feature flags podem ser por App Configuration, mas também podem ser configuradas no local.settings.json,appsettings.json ou a partir de outra origem customizada.
A configuração do Microsoft.FeatureManagement é a partir da interface IConfiguration, o que faz com que seja fácil prover conforme sua aplicação já o faz.
Internamente o pacote irá tentar resolver as feature flags procurando nas configurações as chaves conforme a seguinte representação:
{
"feature_management": {
"feature_flags": [
{ "id": "FeatureA", "enabled": true },
{ "id": "FeatureB", "enabled": false }
]
}
}
Contudo também é possível indicar qual a Section customizada você configurou:
services.AddFeatureManagement(configuration.GetSection("CarameloFeaturesFlags"));
local.settings.json
Caso use o local.settings.json vale lembrar que os delimitadores hierárquicos são __ ou ::, por exemplo:
{
"Values": {
"feature_management::feature_flags:0:id": "FeatureA",
"feature_management::feature_flags:0:enabled": true,
"feature_management::feature_flags:1:id": "FeatureB",
"feature_management::feature_flags:1:enabled": false
}
}
Alternativa customizada
Particularmente não gosto do padrão de delimitadores de Azure Functions, mas isso por si só não seria motivo de aumentar a complexidade.
Contudo caso precise integrar as flags com um CMS interno, banco ou painel próprio e o App Configuration não seja uma opção, é possível customizar um outro provedor das features flags.
Mas falar é fácil, vamos ao código.
Para prover uma opção customizada deve-se implementar a interface IFeatureDefinitionProvider.
Essa interface obriga implementar 2 métodos, GetAllFeatureDefinitionsAsync() que retorna todas as features flags configuradas e GetFeatureDefinitionAsync(string featureName) que obtém uma feature flag pelo seu nome.
Uma abordagem mais interessante para obter o mesmo dinamismo que o App Configuration possui, seria implementar utilizando algum recurso de armazenamento como banco de dados, dessa forma é possível alterar os valores em necessidade de alterar a aplicação ou uma nova publicação, mas para fins de exemplificação é possível implementar utilizando um arquivo json para a configuração.
{
"featureFlags": [
{
"id": "FeatureA",
"enabled": true
},
{
"id": "FeatureB",
"enabled": false
}
]
}
E implementando a interface IFeatureDefinitionProvider:
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.FeatureManagement;
using Newtonsoft.Json;
namespace Caramelo.FoodDispenser;
public class JsonFileFeatureFlagDefinitionProvider(string appRootPath) : IFeatureDefinitionProvider
{
private const string FeatureFlagFileName = "feature-flags.json";
public async Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
{
await foreach (var featureDefinition in GetAllFeatureDefinitionsAsync())
{
if (featureDefinition.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))
{
return featureDefinition;
}
}
return null;
}
public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
{
var path = Path.Combine(appRootPath, FeatureFlagFileName);
string featureFlagsJson = await File.ReadAllTextAsync(path);
var featureManagementConfig = JsonConvert.DeserializeObject<FeatureManagementConfig>(featureFlagsJson);
foreach (var featureFlag in featureManagementConfig.FeatureFlags)
{
yield return new FeatureDefinition
{
Name = featureFlag.Id,
EnabledFor = featureFlag.Enabled ? [new() { Name = "AlwaysOn", }] : [],
};
}
}
internal struct FeatureManagementConfig
{
public FeatureFlag[] FeatureFlags { get; set; }
internal struct FeatureFlag
{
public string Id { get; set; }
public bool Enabled { get; set; }
}
}
}
Manutenção e escalabilidade
O uso de feature flags é uma abordagem que pode facilitar o gerenciamento de versões de sua aplicação, mas também um aliado a experimentação e mitigação de risco.
É possível reduzir o risco de uma publicação de nova versão aplicando a técnica Canary Release, realizar experimentos a fim de obter insights e dados coletados através de Teste A/B e também ter uma rápida resposta caso alguma coisa não esteja funcionando como esperado (Graceful Degradation).
O Microsoft.FeatureManagement facilita as abordagem citadas já com seus filtros built-in(PercentageFilter, TimeWindowFilter, TargetingFilter). Vale a pena dar uma conferida também na documentação o quanto é possível customizar os filtros. Ainda usando o App configuration ao editar uma regra, existe a opção Advanced Edit que permite customizar ainda mais os filtros já existentes. As mesmas customizações são possíveis de fazer usando outro método de configuração das features flags.
Tudo isso permite lançar features de forma incremental e controlada, reduzindo riscos de publicação.
Deixando o código mais elegante:
Um ponto que particularmente me incomoda é precisar aplicar uma condicional para a feature flag, interrompendo do encadeamento de ideias ao analisar um código. Contudo para aplicações mais simples, essa estratégia se prova suficiente.
A simplicidade é o último grau de sofisticação - atribuída a Leonardo Da Vinci
Mas, conforme a aplicação cresce, pode ser necessário adotar estratégias mais sofisticadas para desacoplar a lógica de decisão da lógica de negócio, conforme abordado por Pete Hodgson no artigo
Feature Toggles, onde apresenta estratégias para desacoplar os pontos de tomada de decisão dos de lógica, levando em consideração a gestão das features flags com o crescimento saudável da aplicação.
Não vou abordar esse tema, mas cabe a você avaliar o impacto e crescimento e complexidade da aplicação, para decidir se vale ou não aplicar alguma técnica para melhorar a manutenção das features flags.





Top comments (0)