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
- Wgranie nowej wersji aplikacji
- Włączenie jednej funkcji
- Weryfikacja poprawności działania
- Obserwacja
- W razie problemów wyłączamy nową funkcję
- 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