Enmilocalfunciona

Thoughts, stories and ideas.

Implementación de feature flags con Split

Publicado por Jorge Sanz Briones el

Arquitectura de Solucionesfeature flagsSplit

Feature flags

En nuestro día a día desarrollando software, hay veces que tenemos funcionalidades terminadas, pero no queremos publicarlas inmediatamente en producción. Puede ser que hayamos terminado un desarrollo en la interfaz web, pero todavía no esté listo el servicio de backend (o viceversa), o que por alguna razón debamos esperar a que pase una fecha determinada para ofrecérselo a los usuarios. Esto nos obliga a tener una rama en nuestro repositorio, que querremos mantener actualizada con los cambios que se vayan haciendo en la rama principal hasta que finalmente decidamos entregar la funcionalidad.

Otra situación muy común es aquella en la que después de terminar y desplegar nuestro código, y a pesar de haber hecho las pruebas necesarias, aparecen errores, o finalmente resulta que lo que estábamos entregando no era exactamente lo que el cliente o los usuarios necesitaban. En ambos casos, normalmente lo que haríamos sería volver a desplegar la versión anterior mientras resolvemos los errores y completamos las funcionalidades.

En este contexto surge una técnica de desarrollo de software llamada feature flags o feature toggles, que viene a mejorar la forma en la que podemos lidiar con este tipo de situaciones, desacoplando por completo el despliegue de una funcionalidad de su activación. Esta técnica consiste en implementar condiciones en nuestro código que nos permitan activar y desactivar un comportamiento desde un sistema externo. De esta forma podríamos tener una feature mezclada en nuestra rama principal y desplegada en producción, pero que esta no se encuentre disponible para los usuarios, y de la misma manera, si queremos dar marcha atrás y deshabilitar una funcionalidad ya desplegada,  podemos hacerlo rápidamente sin tener que volver a desplegar la última versión estable.

Las feature flags facilitan la entrega continua de valor. Trabajando en proyectos en los que hacemos entregas cada pocas semanas o días, puede ser que queramos mezclar en la rama principal algunas líneas que funcionan, no tienen impacto sobre el resto del código, pero no cumplen todos los requisitos necesarios, y por lo tanto, no queremos que sea visible para los usuarios.

Haciendo uso de las feature flags se nos abren también las puertas de implementar estrategias que nos permiten hacer nuestras entregas más seguras y fáciles de gestionar. Podemos hacer A/B testing, desplegando de forma simultánea 2 variantes de la misma aplicación mientras monitorizamos el impacto de cada una de ellas en nuestro negocio. También podemos hacer canary releases para habilitar una funcionalidad a un grupo reducido de usuarios, minimizando el impacto negativo derivado de que algo no salga como estaba previsto.

Split

Split una plataforma de despliegue de features (feature delivery platform) que nos permite implementar fácilmente feature flags, de forma que podemos desplegar una release, y desde la interfaz del servidor de Split, alojado en una localización distinta de la de nuestra aplicación, podemos controlar el ciclo de vida de nuestras features.

Esta gestión puede ser de varios tipos: por un lado, podemos activar y desactivar features, por otro podemos aplicar reglas sobre las features que ya están activadas, de forma que podemos decidir a qué usuarios se las ofrecemos, pudiendo configurar porcentajes aleatorios (por ejemplo un 20% del tráfico), o en base a algún criterio (a los usuarios que utilicen un navegador distinto de internet explorer).

Split también ofrece herramientas de monitorización que nos permiten obtener métricas de los posibles errores en las nuevas funcionalidades y el comportamiento de los usuarios con las mismas, lo que nos permite tomar mejores decisiones para nuestro negocio, fundamentadas en datos, y de forma rápida y segura.

Nuestro código hará llamadas a Split a través de un SDK para obtener si una feature se encuentra activada o no en función del usuario que hace la petición y/o otros parámetros.
Podremos gestionar de forma simultánea varias features y varios entornos de ejecución, haciendo una gestión independiente de cada una de ellas.

Split tiene SDKs para la mayoría de los de lenguajes más importantes como se puede ver en la documentación. En nuestro ejemplo vamos a utilizar el SDK de Java para implementar una sencilla aplicación en la que simularemos un despliegue canary, en el que habilitaremos una funcionalidad para un pequeño grupo de usuarios. Veremos también como deshabilitar la funcionalidad o habilitarla para el conjunto total de usuarios.

Registro y configuración de Split

Lo primero que haremos será crear una cuenta en Split siguiendo el siguiente link. Una vez tengamos acceso a la plataforma, vamos a definir un segmento, que es un conjunto de usuarios para los que habilitaremos la feature flag.

En el panel de Split, seleccionamos la opción segments en el menú lateral y pulsamos en el botón create segment. Introducimos un nombre para nuestro segmento (por ejemplo canary_users), dejamos el resto de opciones como están y pulsamos en create.

Añadimos una definición pulsando en Add definition. Añadiremos varios emails de usuarios a través de la opción add individually. Podemos ver también que las configuraciones se pueden hacer para diferentes entornos, en nuestro caso dejaremos marcada la opción por defecto Prod-Default y pulsamos en save.

Ya estamos listos para crear un split, que será nuestra feature flag. Seleccionamos la opción Splits en el menú de la izquierda y pulsamos en el botón create Split. Elegimos un nombre, seleccionamos user en la opción traffic Type y pulsamos en Create.

Un split sin una especificación de las condiciones en la que tiene que aplicar no tendría mucho sentido, por lo que vamos a añadir una regla pulsando en el botón add rules.

Entre las múltiples configuraciones posibles, vamos a introducir el nombre del segmento que hemos creado anteriormente en la sección Set individual targets -> To Segments y para finalizar pulsamos en Add Target.

Como la default rule viene configurada a off por defecto, el comportamiento que obtendremos será que nuestra feature estará desactivada para todos los usuarios excepto para aquellos que se encuentren en el conjunto que hemos definido anteriormente (canary_users). Cuando hayamos terminado, pulsamos en review changes y si todo está ok pulsamos en Save.

Por último, vamos a necesitar recuperar una API key que vamos a utilizar en nuestro código para que el SDK se pueda autenticar en el servidor de Split. Pulsamos en el icono con las iniciales del workspace en la parte superior izquierda, elegimos admin settings y elegimos API keys en el menú lateral. Vemos varias opciones que podemos copiar, nosotros usaremos Server-side en entorno Prod-Default.

Uso del SDK de Split

Ahora que ya tenemos toda la configuración necesaria en la plataforma de Split, vamos a desarrollar un ejemplo sencillo con el SDK de Java, que consistirá en una API REST a la que enviaremos un nombre de usuario, y esta nos devolverá un saludo para el. Sobre esta API vamos a desarrollar una nueva feature que consistirá en incluir la fecha actual en el saludo, y estará o no habilitada en función del valor de la feature flag configurada en Split.

En esta sección trataremos de comentar los aspectos más importantes del código, pero se puede encontrar la versión completa en este enlace.

Generamos el esqueleto del proyecto desde la página de Spring Initializr con las opciones por defecto y añadiendo la dependencia Spring Web. Completamos el pom.xml con la dependencia del sdk de Split para java

<dependency>   
    <groupId>io.split.client</groupId>   
    <artifactId>java-client</artifactId>   
    <version>4.4.4</version>
</dependency>

En primer lugar incluiremos una clase donde estará la configuración del cliente de Split:

@Configuration
@ConditionalOnProperty("split.api-token")
public class SplitConfiguration {
    
    @Bean
    public SplitFactory getSplitFactory(@Value("${split.api-token}") String apiToken) throws Exception {
        
        SplitClientConfig config = SplitClientConfig.builder()
                .setBlockUntilReadyTimeout(10000)
                .build();

        SplitFactory splitFactory = SplitFactoryBuilder.build(apiToken, config);
        return splitFactory;

    }

    @Bean
    public SplitClient getSplitClient(SplitFactory splitFactory) throws Exception {
        SplitClient client = splitFactory.client();
        client.blockUntilReady();
        return client;
    }

}

Se puede observar que la clase de configuración solamente se creará en el caso de que exista una propiedad split.api-token en un fichero .properties o .yaml en nuestro directorio de resources, por lo que recuperamos el valor del API key que obtuvimos en el apartado anterior y lo añadimos a nuestro fichero de propiedades.

A continuación, añadimos un sencillo controlador que se encargará de atender las peticiones HTTP. Solo consta de un método GET para obtener un saludo para un usuario.

@RestController
public class FeaturesController {

    @Autowired
    private SayHelloService sayHelloService;

    @GetMapping(path = "/features/{user}")
    public ResponseEntity<String> features(@PathVariable String user) {
        try {
            return ResponseEntity.ok(sayHelloService.getMessage(user));
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body(e.getMessage());
        }
    }
    
}

Por último, crearemos un servicio, donde realmente se encontrará la lógica para obtener el saludo, y en donde estará también la nueva feature de añadir la fecha al saludo, y donde invocaremos al servidor de Split, que será quien nos diga si para ese usuario debe incluirse la nueva feature que hemos desarrollado:

@Service
public class SayHelloService {
    public static final String OFF = "off";
    public static final String ON = "on";
    public static final String ADD_DATE_FEATURE = "feature-1";

    @Autowired
    private SplitClient splitClient;

    public String getMessage(String user) {
        String message = "Hello %s";

        switch (splitClient.getTreatment(user, ADD_DATE_FEATURE)){
            case OFF: return sayHello(user, message);
            case ON: return  addDateToResponse(sayHello(user, message));
            default: throw new IllegalStateException();
        }
    }

    private String sayHello(String user, String message) {
        return String.format(message, user);
    }

    private String addDateToResponse(String message) {
        return message+", current date is "+ZonedDateTime.now();
    }
}

Como podemos ver, la expresión switch alberga la posibilidad de que la feature flag esté deshabilitada, en cuyo caso recibiremos la respuesta inicial, la misma que obteníamos antes de desarrollar nuestra feature. En el caso de que el servidor de Split nos devuelva que la feature flag está on, nuestro código se encarga de devolvernos una cadena de texto que incluya el saludo junto la fecha actual.

Canary release

Como comentábamos, hemos configurado el servidor de Split y nuestra aplicación para simular un despliegue de tipo canary. El nombre de esta metodología de despliegue es una analogía con la técnica que usaban los antiguos mineros para detectar gases tóxicos en las grutas en las que trabajaban. Se introducía un canario en el espacio de trabajo, y en el caso de haber algún peligro para la salud, sería el canario y no un minero el primero en morir, alertando a los trabajadores de que el entorno no sería seguro. De la misma manera, en desarrollo de software, introducimos una funcionalidad para un número pequeño de usuarios y nos aseguramos de que todo está bien antes de extenderla a todos los demás, consiguiendo minimizar el impacto en el caso de producirse comportamientos inesperados.

Entonces lo primero que tendríamos que hacer es arrancar la aplicación, y posteriormente haremos una petición para un usuario que pertenecen al segmento canary accediendo a la siguiente URL:

http://localhost:8080/say-hello/user1

obtenemos una respuesta como esta:

Hello user1, current date is 2022-09-01T18:52:22.844995445+02:00[Europe/Madrid]

mientras que para cualquier otro usuario:

http://localhost:8080/say-hello/other-user

la respuesta incluye el saludo sin la fecha:

Hello other-user

En este punto podrían darse 2 situaciones: que las pruebas realizadas estuvieran OK y pudiéramos activar la funcionalidad para el 100% de los usuarios, o que hubiera algún error y quisiéramos deshabilitarla por completo.

En el primer caso, abriríamos de nuevo el panel de Split, y en la sección splits, editaríamos la configuración anterior poniendo la default rule a ON. De esta manera podríamos repetir las pruebas anteriores observando que para cualquier usuario nuestra API nos devuelve la respuesta con la fecha.

Para terminar, vamos a ver como en cualquier momento se puede desactivar una feature con solamente pulsar un botón. Para ello entramos de nuevo en el panel y elegimos la opción KILL. Nos pide una confirmación y posteriormente vemos que, si repetimos las pruebas, nuestra feature estará desactivada para cualquier usuario y obtendremos solamente el saludo, sin la fecha.

Conclusiones

Con este sencillo ejemplo hemos conseguido entender como incluir feature flags en nuestro ciclo de desarrollo de software, y aplicando unas pocas reglas de configuración desde el panel de Split, hemos conseguido minimizar los riesgos en nuestro despliegue.

También hemos visto como activar una funcionalidad en el momento preciso, sin necesidad de hacer ningún despliegue adicional en nuestro servidor de aplicaciones, y como podemos dar marcha atrás a una funcionalidad sin más operativa que pulsar un botón, sin tener que redesplegar la última versión estable.

Además de las posibilidades vistas en este ejemplo, la herramienta ofrece muchas otras, como la configuración por diferentes atributos del usuario, la activación de las features flags mediante reglas de porcentaje de usuarios, incrementando este valor progresivamente, o hacer A/B testing definiendo métricas para evaluar 2 versiones desplegadas simultáneamente.

Autor

Jorge Sanz Briones

Arquitecto de soluciones y desarrollador de software. Trabajo desde hace tiempo con arquitecturas de microservicios basadas en eventos y desarrollo principalmente con Java, Spring y BBDD NoSql.