Feature Flags

Skrzynka bezpieczników dla aplikacji internetowych

Rollback

i powrót do wersji v1?

Feature Flag

Mateusz Turzyński

SRE

Jak pracować z Feature Flags?

Deployment

=/=

Release

  1. Wgranie nowej wersji aplikacji
  2. Włączenie jednej funkcji
  3. Weryfikacja poprawności działania
  4. Obserwacja
  5. W razie problemów wyłączamy nową funkcję
  6. Przechodzimy do kolejnej nowej funkcji

Praca z Feature Flags

W razie problemów

zamiast rollbacku całego systemu

wyłączamy jego część

Implementacja

if (isFeatureOnline("product-reviews"))
{
    RenderReviews();
}
else
{
    RenderAlternativeContent()
}

Azure App Configuration

Feature Management

Zależności

// Program.cs

var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration
                              .GetConnectionString("AppConfig");

builder.Configuration.AddAzureAppConfiguration(options => {

    options.Connect(connectionString)
           .UseFeatureFlags();
});

//

builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement();

Konfiguracja usługi

using Microsoft.FeatureManagement;

public class ProductController : Controller
{
    private readonly IFeatureManager _featureManager;

    public ProductController(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }
    
    public IActionResult Details(int productId)
    {
        if (await featureManager.IsEnabledAsync("product-reviews"))
        {
            var reviews = FetchReviewsByProduct(productId);
        }
        ...
        return View();
    }
}

Sprawdzenie flagi

using Microsoft.FeatureManagement;

public class ProductController : Controller
{
    private readonly IFeatureManager _featureManager;

    public ProductController(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }
    
    
    [FeatureGate("product-reviews")]
    public IActionResult ProductReviews(int productId)
    {
        var reviews = FetchReviewsByProduct(productId);
        return View(reviews);
    }
}

Akcja kontrolera MVC

using Microsoft.FeatureManagement;

[FeatureGate("product")]
public class ProductController : Controller
{
    private readonly IFeatureManager _featureManager;

    public ProductController(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }
    
    
    [FeatureGate("product-reviews")]
    public IActionResult ProductReviews(int productId)
    {
        var reviews = FetchReviewsByProduct(productId);
        return View(reviews);
    }
}

Flagujemy cały kontroler

<feature name="product-review">
    <p>Opinie użytkowników</p>
    <div>
      ...
    </div>
</feature>

<feature name="product-review" negate="true">
    <p>Nie mamy żadnych opini dla tego produktu.</p>
</feature>

<feature name="product-review, product-score" requirement="All">
    <p>Opinie i oceny naszych użytkowników</p>
</feature>

<feature name="product-review, external-reviews" requirement="Any">
    <p>Opinie naszych użytkowników i partnerów</p>
</feature>

Widok MVC

using Microsoft.FeatureManagement.FeatureFilters;

IConfiguration Configuration { get; set;}

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options => {
        options.Filters.AddForFeature<CacheReviews>("product-review");
    });
}

Filtry MVC

app.UseMiddlewareForFeature<CacheReviews>("product-review");

app.UseForFeature("product-review", appBuilder => {

    appBuilder.UseMiddleware<CacheReviews>();
    ...
});

ASP.NET Core Middleware

@Controller
@ConfigurationProperties("controller")
public class HomeController {
    private FeatureManager featureManager;

    public HomeController(FeatureManager featureManager) {
        this.featureManager = featureManager;
    }
    
    @GetMapping("/")
    @FeatureGate(feature = "some-feature")
    public String index(Model model) {
        ...
    }
}

Java Spring Boot

@Component
public class FeatureFlagFilter implements Filter {

    @Autowired
    private FeatureManager featureManager;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
        FilterChain chain) throws IOException, ServletException {
            
        if(!featureManager.isEnabledAsync("fsome-feature").block()) {
            chain.doFilter(request, response);
            return;
        }
        ...
        chain.doFilter(request, response);
    }
}

Java Spring Boot

@GetMapping("/redirect")
@FeatureGate(feature = "feature-a", fallback = "/getOldFeature")
public String getNewFeature() {
    // Some New Code
}

@GetMapping("/getOldFeature")
public String getOldFeature() {
    // Some New Code
}

Java Spring Boot

import { Component } from '@angular/core';
import { AppConfigurationClient } from "@azure/app-configuration";

@Component({
  selector: 'app-product-details',
  templateUrl: './product-details.html',
  styleUrls: ['./product-details.component.css']
})
export class ProductDetailsComponent {

  showReviews = reviewFeatureAvailable();

  async reviewFeatureAvailable() {
    const connectionString = 'App Configuration Read-Only Connection String';
    const client = new AppConfigurationClient(connectionString);

    var val = await client.getConfigurationSetting({ 
        key: ".appconfig.featureflag/product-review" });

    return JSON.parse(val.value).enabled;
  }
}

<div *ngIf="(counterFeature | async)">
	<p>Opinie naszych klientów</p>
</div>

Angular

CIEKAWSZE

FLAGI

Percentage Filter

Targeting Filter

Time Window Filter

Własne Filtry

Jeszcze więcej zabawy

[FilterAlias("OnForSpecificCountry")]
public class OnForSpecificCountryFilter : IFeatureFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IRequestOriginCountryResolver _requestOriginCountryResolver;

    public OnForSpecificCountryFilter(IHttpContextAccessor httpContextAccessor, 
        IRequestOriginCountryResolver _requestOriginCountryResolver)
    {
    }
 
    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var settings = context.Parameters.Get<OnForSpecificCountryFilterSettings>();
 
        var request = _httpContextAccessor.HttpContext.Request;
        var requestCountry = _requestOriginCountryResolver.GetCountry(request);
        
        var isEnabled = requestCountry == settings.AllowedCountry;
 
        return Task.FromResult(isEnabled);
    }
}

public class OnForSpecificCountryFilterSettings
{
    public string AllowedCountry { get; set; }
}

Szczypta Konfiguracji

Praca lokalna

Bez Chmury / Offline

// appsettings.json
{
  "FeatureManagement": {
    "shopping-basket": true,
    
    "product-reviews": false,
    
    "marketing": {
      "EnabledFor": [
        {
          "Name": "Microsoft.TimeWindow",
          "Parameters": {
            "Start": "Wed, 29 Mar 2023 22:00:00 GMT",
            "End": "Thu, 30 Mar 2023 00:00:00 GMT"
          }
        }
      ]
    }
    
  }
}

Konfiguracja w JSONie

// Program.cs

builder.Configuration.AddAzureAppConfiguration(options => {

    options.Connect(connectionString)
        .UseFeatureFlags(flagsOptions => {
            flagsOptions.CacheExpirationInterval = TimeSpan.FromMinutes(5);
        });
});

Flags Cache

// Program.cs

builder.Configuration.AddAzureAppConfiguration(options => {

    options.Connect(connectionString)
        .UseFeatureFlags(flagsOptions => {
            flagsOptions.Select("ShopApp:*", LabelFilter.Null)
        });
});

Filtrowanie flag

Alternatywy

AWS AppConfig

Feature Flags

Zalety

Częstszy Release

Kod szybciej trafia na produkcję

Minimalizujemy stres

związany z wdrażaniem zmian

Release =/= Deployment

włączamy funkcje kiedy chcemy

Ograniczony dostęp

do funkcji premium

Ograniczamy

efekty uboczne

Zagrożenia

Wysokie "Verbosity"

lepiej postawić na kod deklaratywny

Łatwo o "legcy flags"

najlepiej usuwać nieużywane flagi na bieżąco

Testowanie

staje się jeszcze bardziej skomplikowane

justjoin.it/sniadanie-z-programowaniem

mateusz.turzynski@aspiresys.com

Dziękuję za uwagę :)

Feature Flags

By Mateusz Turzyński

Feature Flags

  • 142