Feature Flags mit Frühling

1. Übersicht

In diesem Artikel definieren wir kurz Feature-Flags und schlagen einen einfühlsamen und pragmatischen Ansatz vor, um sie in Spring Boot-Anwendungen zu implementieren. Dann werden wir uns mit komplexeren Iterationen befassen, die verschiedene Spring Boot-Funktionen nutzen.

Wir werden verschiedene Szenarien diskutieren, die möglicherweise das Markieren von Funktionen erfordern, und über mögliche Lösungen sprechen. Wir tun dies mit einer Bitcoin Miner-Beispielanwendung.

2. Funktionsflags

Feature-Flags - manchmal auch als Feature-Toggles bezeichnet - sind ein Mechanismus, mit dem wir bestimmte Funktionen unserer Anwendung aktivieren oder deaktivieren können, ohne den Code ändern oder im Idealfall unsere App erneut bereitstellen zu müssen.

Abhängig von der Dynamik, die für ein bestimmtes Feature-Flag erforderlich ist, müssen wir sie möglicherweise global, pro App-Instanz oder genauer konfigurieren - möglicherweise pro Benutzer oder Anforderung.

Wie in vielen Situationen des Software-Engineerings ist es wichtig, den einfachsten Ansatz zu verwenden, der das vorliegende Problem angeht, ohne unnötige Komplexität hinzuzufügen.

Feature-Flags sind ein wirksames Werkzeug, das bei kluger Verwendung Zuverlässigkeit und Stabilität in unser System bringen kann. Wenn sie jedoch missbraucht oder nicht ausreichend gewartet werden, können sie schnell zu Ursachen für Komplexität und Kopfschmerzen werden.

Es gibt viele Szenarien, in denen Feature-Flags nützlich sein könnten:

Trunk-basierte Entwicklung und nicht triviale Funktionen

In der stammbasierten Entwicklung, insbesondere wenn wir häufig weiter integrieren möchten, sind wir möglicherweise nicht bereit, eine bestimmte Funktionalität freizugeben. Feature-Flags können nützlich sein, damit wir sie weiterhin veröffentlichen können, ohne unsere Änderungen bis zum Abschluss verfügbar zu machen.

Umgebungsspezifische Konfiguration

Möglicherweise benötigen wir bestimmte Funktionen, um unsere Datenbank für eine E2E-Testumgebung zurückzusetzen.

Alternativ müssen wir möglicherweise eine andere Sicherheitskonfiguration für Nichtproduktionsumgebungen verwenden als in der Produktionsumgebung.

Daher könnten wir Feature-Flags nutzen, um das richtige Setup in der richtigen Umgebung umzuschalten.

A / B-Tests

Die Veröffentlichung mehrerer Lösungen für dasselbe Problem und die Messung der Auswirkungen ist eine überzeugende Technik, die wir mithilfe von Feature-Flags implementieren können.

Kanarische Freigabe

Wenn wir neue Funktionen bereitstellen, entscheiden wir uns möglicherweise dafür, dies schrittweise zu tun, beginnend mit einer kleinen Gruppe von Benutzern, und die Übernahme zu erweitern, wenn wir die Richtigkeit des Verhaltens überprüfen. Mit Feature-Flags können wir dies erreichen.

In den folgenden Abschnitten werden wir versuchen, einen praktischen Ansatz zur Bewältigung der oben genannten Szenarien bereitzustellen.

Lassen Sie uns verschiedene Strategien für das Markieren von Funktionen aufschlüsseln, beginnend mit dem einfachsten Szenario, um dann zu einem detaillierteren und komplexeren Setup überzugehen.

3. Feature-Flags auf Anwendungsebene

Wenn wir einen der ersten beiden Anwendungsfälle angehen müssen, sind Feature-Flags auf Anwendungsebene eine einfache Möglichkeit, die Dinge zum Laufen zu bringen.

Ein einfaches Feature-Flag umfasst normalerweise eine Eigenschaft und eine Konfiguration, die auf dem Wert dieser Eigenschaft basiert.

3.1. Feature-Flags mit Federprofilen

Im Frühjahr können wir Profile nutzen. Mit Profilen können wir bequemerweise bestimmte Beans selektiv konfigurieren. Mit ein paar Konstrukten können wir schnell eine einfache und elegante Lösung für Feature-Flags auf Anwendungsebene erstellen.

Stellen wir uns vor, wir bauen ein BitCoin-Mining-System. Unsere Software befindet sich bereits in der Produktion und wir haben die Aufgabe, einen experimentellen, verbesserten Mining-Algorithmus zu erstellen.

In unserer JavaConfig konnten wir unsere Komponenten profilieren:

@Configuration public class ProfiledMiningConfig { @Bean @Profile("!experimental-miner") public BitcoinMiner defaultMiner() { return new DefaultBitcoinMiner(); } @Bean @Profile("experimental-miner") public BitcoinMiner experimentalMiner() { return new ExperimentalBitcoinMiner(); } }

Dann mit der vorherige Konfiguration, müssen wir einfach unser Profil schließen Opt-in für unsere neue Funktionalität. Es gibt unzählige Möglichkeiten, unsere App im Allgemeinen zu konfigurieren und Profile im Besonderen zu aktivieren. Ebenso gibt es Testprogramme, die unser Leben erleichtern.

Solange unser System einfach genug ist, können wir eine umgebungsbasierte Konfiguration erstellen, um zu bestimmen, welche Feature-Flags angewendet und welche ignoriert werden sollen.

Stellen wir uns vor, wir haben eine neue Benutzeroberfläche, die auf Karten anstelle von Tabellen basiert, zusammen mit dem vorherigen experimentellen Miner.

Wir möchten beide Funktionen in unserer Akzeptanzumgebung (UAT) aktivieren. Wir könnten eine application-uat.yml- Datei erstellen:

spring: profiles: include: experimental-miner,ui-cards # More config here

Mit der vorherigen Datei müssten wir nur das UAT-Profil in der UAT-Umgebung aktivieren, um die gewünschten Funktionen zu erhalten.

Es ist auch wichtig zu verstehen, wie Sie spring.profiles.include nutzen können. Im Vergleich zu spring.profiles.active können wir mit ersteren Profile additiv einbeziehen .

In unserem Fall möchten wir, dass das UAT- Profil auch Experimental-Miner- und UI-Karten enthält .

3.2. Feature-Flags mit benutzerdefinierten Eigenschaften

Profile sind eine großartige und einfache Möglichkeit, die Arbeit zu erledigen. Möglicherweise benötigen wir jedoch Profile für andere Zwecke. Oder vielleicht möchten wir eine strukturiertere Feature-Flag-Infrastruktur aufbauen.

In diesen Szenarien sind benutzerdefinierte Eigenschaften möglicherweise eine wünschenswerte Option.

Lassen Sie uns unser vorheriges Beispiel unter Verwendung von @ConditionalOnProperty und unserem Namespace neu schreiben :

@Configuration public class CustomPropsMiningConfig { @Bean @ConditionalOnProperty( name = "features.miner.experimental", matchIfMissing = true) public BitcoinMiner defaultMiner() { return new DefaultBitcoinMiner(); } @Bean @ConditionalOnProperty( name = "features.miner.experimental") public BitcoinMiner experimentalMiner() { return new ExperimentalBitcoinMiner(); } }

Das vorherige Beispiel baut auf der bedingten Konfiguration von Spring Boot auf und konfiguriert die eine oder andere Komponente, je nachdem, ob die Eigenschaft auf true oder false gesetzt ist (oder ganz weggelassen wird).

The result is very similar to the one in 3.1, but now, we have our namespace. Having our namespace allows us to create meaningful YAML/properties files:

#[...] Some Spring config features: miner: experimental: true ui: cards: true #[...] Other feature flags

Also, this new setup allows us to prefix our feature flags – in our case, using the features prefix.

It might seem like a small detail, but as our application grows and complexity increases, this simple iteration will help us keep our feature flags under control.

Let's talk about other benefits of this approach.

3.3. Using @ConfigurationProperties

As soon as we get a prefixed set of properties, we can create a POJO decorated with @ConfigurationProperties to get a programmatic handle in our code.

Following our ongoing example:

@Component @ConfigurationProperties(prefix = "features") public class ConfigProperties { private MinerProperties miner; private UIProperties ui; // standard getters and setters public static class MinerProperties { private boolean experimental; // standard getters and setters } public static class UIProperties { private boolean cards; // standard getters and setters } }

By putting our feature flags' state in a cohesive unit, we open up new possibilities, allowing us to easily expose that information to other parts of our system, such as the UI, or to downstream systems.

3.4. Exposing Feature Configuration

Our Bitcoin mining system got a UI upgrade which is not entirely ready yet. For that reason, we decided to feature-flag it. We might have a single-page app using React, Angular, or Vue.

Regardless of the technology, we need to know what features are enabled so that we can render our page accordingly.

Let's create a simple endpoint to serve our configuration so that our UI can query the backend when needed:

@RestController public class FeaturesConfigController { private ConfigProperties properties; // constructor @GetMapping("/feature-flags") public ConfigProperties getProperties() { return properties; } }

There might be more sophisticated ways of serving this information, such as creating custom actuator endpoints. But for the sake of this guide, a controller endpoint feels like good enough a solution.

3.5. Keeping the Camp Clean

Although it might sound obvious, once we've implemented our feature flags thoughtfully, it's equally important to remain disciplined in getting rid of them once they're no longer needed.

Feature flags for the first use case – trunk-based development and non-trivial features – are typically short-lived. This means that we're going to need to make sure that our ConfigProperties, our Java configuration, and our YAML files stay clean and up-to-date.

4. More Granular Feature Flags

Sometimes we find ourselves in more complex scenarios. For A/B testing or canary releases, our previous approach is simply not enough.

To get feature flags at a more granular level, we may need to create our solution. This could involve customizing our user entity to include feature-specific information, or perhaps extending our web framework.

Polluting our users with feature flags might not be an appealing idea for everybody, however, and there are other solutions.

As an alternative, we could take advantage of some built-in tools such as Togglz. This tool adds some complexity but offers a nice out-of-the-box solution and provides first-class integration with Spring Boot.

Togglz supports different activation strategies:

  1. Username: Flags associated with specific users
  2. Gradual rollout: Flags enabled for a percentage of the user base. This is useful for Canary releases, for example, when we want to validate the behavior of our features
  3. Release date: We could schedule flags to be enabled at a certain date and time. This might be useful for a product launch, a coordinated release, or offers and discounts
  4. Client IP: Flagged features based on clients IPs. These might come in handy when applying the specific configuration to specific customers, given they have static IPs
  5. Server IP: In this case, the IP of the server is used to determine whether a feature should be enabled or not. This might be useful for canary releases too, with a slightly different approach than the gradual rollout – like when we want to assess performance impact in our instances
  6. ScriptEngine: We could enable feature flags based on arbitrary scripts. This is arguably the most flexible option
  7. System Properties: We could set certain system properties to determine the state of a feature flag. This would be quite similar to what we achieved with our most straightforward approach

5. Summary

In this article, we had a chance to talk about feature flags. Additionally, we discussed how Spring could help us achieve some of this functionality without adding new libraries.

Wir haben zunächst definiert, wie dieses Muster uns bei einigen häufigen Anwendungsfällen helfen kann.

Als Nächstes haben wir einige einfache Lösungen mit den sofort einsatzbereiten Tools Spring und Spring Boot erstellt. Damit haben wir ein einfaches, aber leistungsstarkes Feature-Flagging-Konstrukt entwickelt.

Unten haben wir einige Alternativen verglichen. Übergang von der einfacheren und weniger flexiblen Lösung zu einem komplexeren, wenn auch komplexeren Muster.

Schließlich haben wir kurz einige Richtlinien bereitgestellt, um robustere Lösungen zu entwickeln. Dies ist nützlich, wenn wir einen höheren Grad an Granularität benötigen.