Drapeaux de fonctionnalité avec ressort

1. Vue d'ensemble

Dans cet article, nous définirons brièvement les indicateurs de fonctionnalités et proposerons une approche avisée et pragmatique pour les implémenter dans les applications Spring Boot. Ensuite, nous explorerons des itérations plus sophistiquées en tirant parti des différentes fonctionnalités de Spring Boot.

Nous discuterons de divers scénarios qui pourraient nécessiter un marquage des fonctionnalités et parlerons de solutions possibles. Nous allons le faire en utilisant un exemple d'application Bitcoin Miner.

2. Drapeaux de caractéristiques

Les indicateurs de fonctionnalité - parfois appelés bascule de fonctionnalité - sont un mécanisme qui nous permet d'activer ou de désactiver des fonctionnalités spécifiques de notre application sans avoir à modifier le code ou, idéalement, à redéployer notre application.

En fonction de la dynamique requise par un indicateur de fonctionnalité donné, nous pouvons avoir besoin de les configurer globalement, par instance d'application, ou de manière plus granulaire - peut-être par utilisateur ou par demande.

Comme dans de nombreuses situations en génie logiciel, il est important d'essayer d'utiliser l'approche la plus simple qui s'attaque au problème à résoudre sans ajouter de complexité inutile.

Les indicateurs de fonctionnalités sont un outil puissant qui, lorsqu'il est utilisé à bon escient, peut apporter fiabilité et stabilité à notre système. Cependant, lorsqu'ils sont mal utilisés ou sous-entretenus, ils peuvent rapidement devenir source de complexité et de maux de tête.

Il existe de nombreux scénarios dans lesquels les indicateurs de fonctionnalité pourraient être utiles:

Développement basé sur le tronc et fonctionnalités non triviales

Dans le développement basé sur le tronc, en particulier lorsque nous souhaitons continuer à intégrer fréquemment, nous pourrions ne pas être prêts à publier une certaine fonctionnalité. Les indicateurs de fonctionnalités peuvent être utiles pour nous permettre de continuer à publier sans rendre nos modifications disponibles jusqu'à leur fin.

Configuration spécifique à l'environnement

Il se peut que nous ayons besoin de certaines fonctionnalités pour réinitialiser notre base de données pour un environnement de test E2E.

Sinon, nous pouvons avoir besoin d'utiliser une configuration de sécurité différente pour les environnements hors production de celle utilisée dans l'environnement de production.

Par conséquent, nous pourrions profiter des indicateurs de fonctionnalité pour basculer la bonne configuration dans le bon environnement.

Test A / B

La publication de plusieurs solutions pour le même problème et la mesure de l'impact est une technique convaincante que nous pourrions implémenter à l'aide d'indicateurs de fonctionnalité.

Canari libérant

Lors du déploiement de nouvelles fonctionnalités, nous pouvons décider de le faire progressivement, en commençant par un petit groupe d'utilisateurs, et en étendant son adoption à mesure que nous validons l'exactitude de son comportement. Les drapeaux de fonctionnalités nous permettent d'y parvenir.

Dans les sections suivantes, nous essaierons de fournir une approche pratique pour aborder les scénarios mentionnés ci-dessus.

Décomposons différentes stratégies pour le marquage des fonctionnalités, en commençant par le scénario le plus simple pour ensuite passer à une configuration plus granulaire et plus complexe.

3. Indicateurs de fonctionnalité au niveau de l'application

Si nous devons nous attaquer à l'un des deux premiers cas d'utilisation, les indicateurs de fonctionnalités au niveau de l'application sont un moyen simple de faire fonctionner les choses.

Un indicateur de fonctionnalité simple impliquerait généralement une propriété et une configuration basée sur la valeur de cette propriété.

3.1. Drapeaux de fonctionnalité utilisant des profils de ressort

Au printemps, nous pouvons profiter des profils. De manière pratique, les profils nous permettent de configurer certains beans de manière sélective. Avec quelques constructions autour d'eux, nous pouvons rapidement créer une solution simple et élégante pour les indicateurs de fonctionnalités au niveau de l'application.

Imaginons que nous construisions un système de minage BitCoin. Notre logiciel est déjà en production et nous sommes chargés de créer un algorithme d'exploration expérimental amélioré.

Dans notre JavaConfig, nous pourrions profiler nos composants:

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

Ensuite, avec la configuration précédente, nous devons simplement inclure notre profil pour activer notre nouvelle fonctionnalité. Il existe des tonnes de façons de configurer notre application en général et d'activer les profils en particulier. De même, il existe des utilitaires de test pour nous faciliter la vie.

As long as our system is simple enough, we could then create an environment-based configuration to determine which features flags to apply and which ones to ignore.

Let's imagine we have a new UI based on cards instead of tables, together with the previous experimental miner.

We'd like to enable both features in our acceptance environment (UAT). We could create an application-uat.yml file:

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

With the previous file in place, we'd just need to enable the UAT profile in the UAT environment to get the desired set of features.

It's also important to understand how to take advantage of spring.profiles.include. Compared to spring.profiles.active, the former enables us to include profiles in an additive manner.

In our case, we want the uat profile also to include experimental-miner and ui-cards.

3.2. Feature Flags Using Custom Properties

Profiles are a great and simple way to get the job done. However, we might require profiles for other purposes. Or perhaps, we might want to build a more structured feature flag infrastructure.

For these scenarios, custom properties might be a desirable option.

Let's rewrite our previous example taking advantage of @ConditionalOnProperty and our namespace:

@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(); } }

The previous example builds on top of Spring Boot's conditional configuration and configures one component or another, depending on whether the property is set to true or false (or omitted altogether).

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.

Nous avons commencé par définir comment ce modèle peut nous aider avec quelques cas d'utilisation courants.

Ensuite, nous avons créé quelques solutions simples à l'aide des outils prêts à l'emploi de Spring et Spring Boot. Avec cela, nous avons proposé une construction de signalisation de fonctionnalités simple mais puissante.

Ci-dessous, nous avons comparé quelques alternatives. Passer d'une solution plus simple et moins flexible à un modèle plus sophistiqué, bien que plus complexe.

Enfin, nous avons brièvement fourni quelques lignes directrices pour créer des solutions plus robustes. Ceci est utile lorsque nous avons besoin d'un degré de granularité plus élevé.