Feature Flag em nodeJS com Unleash

Quem já implementou ou trabalhou em um ambiente de CI/CD, sabe da necessidade de entregar novas releases constantemente. Para conseguir isso, é necessário utilizar alguns recursos e um deles que é indispensável é o Feature Flag.

Feature Flag consiste em implantar uma feature nova, que pode ser habilitada/desabilitada a qualquer momento de acordo com os seus critérios definidos. Com esse recurso disponível, você pode mudar o comportamento da sua aplicação para um grupo de usuários durante o runtime, sem necessidade de gerar uma nova release ou executar qualquer tipo de script.

O curioso para mim, é que antes de estudar sobre essa técnica, eu já utilizava algo semelhante em muitos dos meus clientes ao longo dos anos. Vou explicar o motivo.

Como muitos de vocês, também trabalhei com clientes que não possuíam qualquer tipo de processo. Alguns até diziam utilizar um processo agile, mas de agile só tinha o nome mesmo. Alguém cria um projeto no trello e divide o projeto em sprints, mas continua entregando tudo no final e seguindo todo modelo de entregas waterfall

Cada projeto nesses clientes levavam cerca de 3 a 6 meses e quando chegava o dia do go live, a equipe cruzava os dedos e iniciava a implantação de 55 funcionalidades em 699 pacotes e 543 scripts. Mesmo sendo bastante cuidadoso, o risco era enorme.

A solução que encontrei para reduzir esse risco foi aproveitar qualquer minor deploy durante o período para correção de algum bug de produção, para incluir uma funcionalidade do projeto.

Como essa funcionalidade não podia aparecer, eu criava um parâmetro ON/OFF e condicionava a aparição da funcionalidade a esse parâmetro, então no dia do go live, a maioria das funcionalidades já estavam em produção e só precisávamos liga-las.

Feature Flag começa por ai, mas esse recurso vai muito mais alem do que a minha alternativa para não tomar porrada de cliente sem noção no dia do go live. Vamos ver alguns cenários para aplicar esta técnica

CANARY RELEASE

Canary Release é uma técnica de liberar um produto em um ambiente somente para um determinado grupo. Não importa a estratégia de seleção, pode ser por IP, critérios geográficos, perfil, range de usuários…

O importante neste caso é saber que existem dois ou mais grupos acessando a sua aplicação e que uma feature dela, vai se comportar de forma distinta para cada grupo. Conforme a nova funcionalidade vai sendo utilizada e não são identificados erros, você pode expandir esse grupo gradualmente até chegar a 100%.

O feature flag pode ser utilizado como ferramenta para criar uma canary release.

Existem outras técnicas para criar canary release como o uso de load balance para fazer balanceamento de carga, onde por exemplo num grupo de 10 maquinas, uma delas possui a nova versão e a proporção de maquinas vai aumentando gradualmente.

TESTE A/B

O teste A/B tem um princípio parecido ao do canary release, mas nesse caso queremos verificar a melhor opção entre duas apresentadas. Por exemplo, construímos uma página web utilizando um novo componente de upload e outra utilizando um tradicional.

Aplicamos ambas em um ambiente e associamos um grupo a cada página, com isso podemos acompanhar as conversões e analisar qual página funciona melhor.

Novamente utilizamos feature flag aqui.

CONTINUOUS DELIVERY

Neste último exemplo não estamos falando de teste e sim do processo de integração continua do pipeline. Neste caso o feature flag é utilizado para subir funcionalidades desligadas que somente serão ativadas após analises, testes ou decisão estratégica.

APLICANDO FEATURE FLAG

NodeJS – Unleash

Existem várias opções de componente pra implementar o feature flag, para o nodeJS eu tenho utilizado o unleash que vamos ver agora

Todo o código deste artigo para executar o unleash está disponível no gitlab em
https://gitlab.com/cateno/codechain/tree/master/nodejs

Server

O Unleash é divido em server e client, vamos começar configurando o server.

A instalação no nodeJS é bem fácil, mas é necessário ter um banco de dados postgreSQL pra executa-lo. A versão precisa ser 9.5 ou acima e somente roda em versões do nodeJS igual ou superior a 8.0.0

O unleash server pode ser executado sem necessidade de implementar código, somente com os comandos abaixo no prompt:

  • npm install unleash-server -g
  • unleash -d postgres://unleash_user:passord@localhost:5432/unleash -p 4242

Esta não é a melhor opção, pois você terá algumas restrições. O ideal é que você crie uma aplicação nodejs somente para o unleash-server e siga os passos abaixo:

Execute o comando para instalação

npm install unleash-server

Após a instalação, coloque na inicialização do seu server o código abaixo para iniciar o unleash-server.

[cc lang=”javascript”]

const unleash = require(‘unleash-server’);

unleash.start({
databaseUrl: unleashConnectionString,
port: 4242,
}).then(unleash => {
console.log(Unleash started on http://localhost:${unleash.app.get('port')},);
});

[/cc]

O código acima inicializa o unleash informando a connection string do banco de dados

No git do artigo, esse bloco de inicialização está no arquivo
https://gitlab.com/cateno/codechain/blob/master/nodejs/security/config.js, junto com o bloco de inicialização do client (veremos mais a frente). Vale destacar aqui também, que tanto server quanto client estão na mesma aplicação neste exemplo, mas numa aplicação real o ideal é ter o server isolado como uma aplicação

Pode ficar tranquilo em apontar para um banco de dados já existente, pois o unleash-server não irá afetar as suas tabelas de sistema, ele vai criar no esquema que você apontar, as tabelas abaixo necessárias para sua execução:

  • client_applications
  • client_instances
  • client_metrics
  • events
  • features
  • migrations
  • strategies

somente com o código acima, inicie a sua aplicação e se tudo estiver correto, acesso o endereço no seu navegador: http://localhost:4242

Será exibida a interface de configuração do unleash, que neste primeiro momento está vazia. Clique no botão de “+’ para criarmos uma feature para nossa aplicação

Vamos criar como exemplo uma feature para envio de e-mail e vamos deixa-la desligada

Na imagem acima, criamos uma feature com o nome F001-EnvioEmail. O item destacado em amarelo Activition strategies, indica que tipo de estratégia será utilizada para implantar a feature. A princípio , adicione a estratégia default que já vem configurada.

Após criar a feature, você será direcionado para a tela de edição dela e vai perceber algumas informações interessantes, como métrica de uso e log da feature, mas vamos ver isso mais pra frente

O mais interessante agora é ver a feature implementada em seu código para responder a configuração acima.

client

Agora vamos configurar o client começando pela instalação do pacote unleash-client executando o comando abaixo

npm install unleash-client --save

Inclua o código na sua aplicação para inicializar o unleash-client conectando ao unleash-server.

[cc lang=”javascript”]

const { initialize } = require(‘unleash-client’);

const instance = initialize({
url: ‘http://localhost:4242/api’,
appName: ‘codechain’,
instanceId: ‘codechain-001’,
refreshInterval: 5000
});
// optional events
instance.on(‘error’, console.error);
instance.on(‘warn’, console.warn);
instance.on(‘ready’, console.log);

// metrics hooks
instance.on(‘registered’, clientData => {

console.log(‘conectado no cliente’, clientData)

exports.unleash = {
isEnabled,
getVariant,
getFeatureToggleDefinition,
getFeatureToggleDefinitions,
} = require(‘unleash-client’);

console.log(‘checando status das features’);
console.log(‘isEnabled2: ‘ + isEnabled(‘F001-EnvioEmail’));

});

instance.on(‘sent’, payload => console.log(‘metrics bucket/payload sent’, payload));
instance.on(‘count’, (name, enabled) => console.log(`isEnabled(${name}) returned ${enabled}`));
[/cc]

No código cima existem alguns pontos importantes para descrever antes de prosseguirmos

no método initialize utilizamos 4 parâmetros de configuração:

  • url: este atributo é obrigatório e deve informar a url do server seguido de ‘/api’, que em nosso caso é http://localhost:4242/api
  • appName: nome da aplicação que está utilizando a feature (várias aplicações podem compartilhar do mesmo servidor de feature). Em nosso caso utilizamos codechain
  • instanceId: id da instancia da aplicação, se você tiver uma aplicação com mais de uma instancia, pode utilizar esse atributo pra identificar quem está conectado no server.
  • refreshInterval (opcional): este é o período que o seu client vai atualizar o status de cada feature no server, o seu valor padrão é 15000ms, que significa que se você desligar/ligar uma feature, a sua nova situação será visível em até 15 segundos pelo client.

Para mais detalhes sobre as outras opções disponíveis, veja na documentação oficial

Neste código também temos um exemplo do status da feature que criamos no início , a F001-EnvioEmail. Ao executar o comando isEnabled(‘F001-EnvioEmail’) recebemos true se a feature estiver ligada.

Segue abaixo resultado da inicialização da aplicação

Agora para os testes que vamos fazer a seguir, criei dois serviços que correspondem a duas features, um para envio de e-mail e outro para envio de SMS. No repositório do GIT, esses serviços estão nos arquivos api/routes.js e api/feature-flag-controller.js

routes.api

[cc lang=”javascript”]
‘use strict’;

var featureFlagController = require(‘./feature-flag-controller’);

module.exports = function(app) {
//user routes
app.route(‘/feature-flag/email’).get(featureFlagController.enviaEmail);
app.route(‘/feature-flag/sms’).get(featureFlagController.enviaSMS);
};
[/cc]

feature-flag-controller.js

[cc lang=”javascript”]
‘use strict’;

var _config = require(‘../security/config’)

var controllers = {
enviaEmail: async function (req, res) {

let user;

const runEnviaEmail = async () => {

try {

console.log(‘ — envia e-mail’);

let isEnvioEmailEnabled = _config.unleash.isEnabled(‘F001-EnvioEmail’);

if (isEnvioEmailEnabled){
res.status(200).send({msg: ’email enviado’});
}else{
res.status(400).send({msg: ‘envio de e-mail desligado’});
}

} catch (error) {
console.error(‘falha fatal’, error)
_config.formataError(error, res)
}
};

runEnviaEmail();

},
enviaSMS: async function (req, res) {

const runEnviaSMS = async () => {

try {

console.log(‘ — envia sms’);

if (user == null){
res.status(404).send({
msg: _config.msg.error_404_usuario
});
}

res.json(user);

} catch (error) {
_config.formataError(error, res)
}
};

runEnviaSMS();

}
};

module.exports = controllers;
[/cc]

iniciando testes

Se tudo estiver correto, neste momento você já possui o unleash client e server configurado, além de 2 serviços para testar no restante do artigo. Caso não tenha conseguido rodar por algum motivo, baixe a aplicação no repositório git no caminho
https://gitlab.com/cateno/codechain/tree/master/nodejs

execute o comando npm install e crie um arquivo chamado .env.staging na raiz da aplicação. Este arquivo deve ter o conteúdo abaixo substituindo os dados de acesso do banco postgreSQL.

APP_ENV=staging
APP_NAME=codechain unleash teste
DB_HOST=postgre host
DB_USER=postgre user
DB_PASS=senha do postgre
DB_PORT=5432
DB_SCHEMA=nome do esquema

Vamos prosseguir então chamando o endpoint
http://localhost:3000/feature-flag/email. A chamada pode ser pelo próprio navegador, pois o método dos serviços é o GET.

Se a sua feature F001-EnviarEmail estiver ligada, você verá o retorno {“msg”:”email enviado”}. Caso ela esteja desligada, você verá o retorno {“msg”:”envio de e-mail desligado”}

Seja qual for o resultado, retorno a interface de gerenciamento do unleash-server em http://localhost:4242 . Troque o status da feature e veja o resultado, você vai ver que o retorno do serviço vai mudar (repare também que essa mudança ocorre após os o tempo configurado em refreshInterval).

O código abaixo em feature-flag-controller.js que exibe essa saída

[cc lang=”javascript”]
let isEnvioEmailEnabled = _config.unleash.isEnabled(‘F001-EnvioEmail’);
if (isEnvioEmailEnabled){
res.status(200).send({msg: ’email enviado’});
}else{
res.status(400).send({msg: ‘envio de e-mail desligado’});
}
[/cc]

Até aqui estamos apenas ligando e desligando a feature manualmente no gerenciador. É muito útil ter esse tipo de ferramenta na sua aplicação, se bem utilizada, você pode reduzir bugs em produção rapidamente desligando recursos que estão causando problema na sua aplicação.

Agora que já sabemos o básico vamos adiante ver até aonde vai a toca do coelho

Toca do Coelho

applicationHostname

O primeiro teste mais avançado que vamos fazer é criar uma segunda FEATURE chamada F002-EnvioSMS. Esta Feature deve estar ligada e como
Activation strategies, vamos selecionar applicationHostname.

Este tipo de estratégia , quando a feature está ligada, indica que ela estará ligada somente para as maquinas onde o seu hostname, estiver na lista de configuração da feature conforme imagem abaixo

Ou seja, na imagem acima, qualquer verificação se a feature está ligada, vai retornar TRUE somente para as maquinas HOSTNAME01 e HOSTNAME02. Se a requisição vier de HOSTNAME03, a feature estará desligada.

para obter o hostname em nodeJS, importe a biblioteca do sistema operacional e use o metodo hostname() -> const { hostname } = require(‘os’); console.log(hostname());

userWithId

Esta estratégia libera a feature somente para os Ids configurados, que pode ser um número, um CPF ou um e-mail por exemplo.

Crie uma nova Feature chamada F003-EnvioWhatsApp, selecione a estratégia userWithId e inclua uma lista de e-mails que deseja liberar o acesso a feature.

Para validar o userId, é preciso passar um segundo parâmetro na função isEnabled do unleash.

[cc lang=”javascript”]
const context = {
userId: ‘cateno@codechain.com.br’,
};

let isEnvioWAEnabled = _config.unleash.isEnabled(‘F003-EnvioWhatsApp’, context);
[/cc]

Neste tipo de feature, é necessário passar o contexto no método de validação, fora esse detalhe, a implementação é semelhante as duas estratégias que vimos anteriormente.

remoteAddress

Esta estratégia é igual ao userWithId, mas ao invés do userID é passado o IP da requisição. Na verdade, você poderia utilizar uma única estratégia para userID e IP, pois nada te impede de passar como userID um IP e usar a estratégia de userWithId, mas ….

segue abaixo o código dessa validação (também presente no git)

[cc lang=”javascript”]
console.log(‘ — envia telegram’);
console.log(‘ — IP INTERNET: ‘ + req.connection.remoteAddress);
console.log(‘ — IP SERVER EM PROXY: ‘ + req.headers[‘x-forwarded-for’]);
console.log(‘ — IP SOCKET: ‘ + req.socket.remoteAddress);

const context = {
remoteAddress: req.connection.remoteAddress,
};

console.log(‘context: ‘ , context)

let isEnvioWAEnabled = _config.unleash.isEnabled(‘F004-EnviaTelegram’, context);

if (isEnvioWAEnabled){
res.status(200).send({msg: ‘telegram enviado’});
}else{
res.status(400).send({msg: ‘envio de telegram desligado’});
}
[/cc]

gradual rollout

O gradual rollout é a estratégia de liberação da feature de acordo com um percentual. Por exemplo:

Você cria uma nova feature que vai gravar em um canal do slack determinada informação de acordo com uma ação do usuário. Apos aplicar o deploy você pode criar um gradual rollout baseado em usuários iniciando em 20%.

Isso significa que somente 20% dos usuários da aplicação terão acesso a nova feature. Desta forma você reduz o risco de alguma implementação com defeito gerar um problema maior, pois se ela causar algum estrago, será somente para 20% dos usuários.

Conforme a sua percepção e dos usuários de que a feature está funcionando corretamente, você pode aumentar esse percentual para 40%, 60%, 80% e 100%, quando todos os usuários terão acesso a feature.

O Gradual Rollout é divido em 3 grupos

  • userId: utilizando um algoritmo de hash baseado no userID, somente N% dos usuariso terão acesso a feature
  • sessionID: funciona da mesma forma que o userID, mas é baseado na sessão, desta forma qualquer usuário pode ser impactado no percentual.
  • random: neste gradual rollout não existe uma métrica como base, e qualquer requisição pode ser eletiva pra uso da feature dentro do percentual

Vamos ver abaixo um exemplo do userID

Crie uma Feature chamada F005-SlackPost e selecione a estratégia gradualRolloutUserId. Defina o percentual para 20% e informe o nome do grupo grupo-slack-post

no código abaixo vamos validar se a feature está disponível para o user cateno@codechain.com.br

[cc lang=”javascript”]
const context = {
userId: ‘cateno@codechain.com.br’,
};

console.log(‘context: ‘ , context)

let isPostSlackEnabled = _config.unleash.isEnabled(‘F005-SlackPost’, context);

if (isPostSlackEnabled){
res.status(200).send({msg: ‘slack enviado’});
}else{
res.status(400).send({msg: ‘post de slack desligado’});
}
[/cc]

execute o código agora validando esta feature. O resultado será false, mas porque ?

O algoritmo de rollout utiliza o groupID e o userID pra gerar um hash. Esse hash é utilizado em um algoritmo que gera uma saída com um número de 1 a 100, no nosso caso o resultado foi 88. Com esse resultado, o usuário cateno@codechain.com.br vai enxergar a feature somente quando o rollout estiver em 88% ou mais.

Caso você troque o nome do grupo, o número gerado será diferente, desta forma o algoritmo garante que nunca serão os mesmos usuários participando de cada fase do rollout.

com isso fechamos os nossos testes aplicando o feature flag no nodeJS

Protegendo o unleash-server

como vocês devem ter percebido, o nosso unleash-server está aberto e qualquer um pode entrar e ligar ou desligar features, o que não é uma boa ideia.

Existem várias formas de implementar essa segurança, no nosso exemplo aqui, vamos utilizar a autenticação basic que é bastante simples. Instale o pacote abaixo:

npm install basic-auth -save

crie o arquivo basic-auth-hook.js conforme abaixo

[cc lang=”javascript”]
‘use strict’;

const auth = require(‘basic-auth’);

function basicAuthentication(app) {
app.use(‘/api/admin/’, (req, res, next) => {
const credentials = auth(req);

if (credentials) {
// aqui você deve validar a credential informada,
console.log(‘credential’, credentials)
next();
} else {
return res.status(‘401’).set({ ‘WWW-Authenticate’: ‘Basic realm=”codechain”‘ }).end(‘access denied’);
}
});

app.use((req, res, next) => {
// Updates active sessions every hour
req.session.nowInHours = Math.floor(Date.now() / 3600e3);
next();
});
}

module.exports = basicAuthentication;
[/cc]

na linha 12 em diante, você recebe o login e senha da credencial informada e neste momento você deve validar o usuário e senha conforme achar melhor. Em nosso caso, não estamos validando, qualquer login e senha vai liberar acesso ao unleash-server.

Na inicialização do server altere o código para incluir o basic authentication conforme abaixo

[cc lang=”javascript”]
unleash.start({
databaseUrl: unleashConnectionString,
port: 4242,
secret: ‘super-duper-secret’,
adminAuthentication: ‘custom’,
preRouterHook: basicAuthentication,
})
[/cc]

apos inicializar o unleash-server novamente será exibida a autenticação basic do navegador.

em um cenário de produção o unleash-server não deveria estar na DMZ, mas ainda assim é possível e aconselhável protege-lo com um login e senha

existe outras formas de proteger o unleash-server e o client também, porém não vou abordar todos esses recursos senão esse artigo não termina nunca. Este item sobre segurança está disponível no guia oficial.

Topicos Adicionais

Se você seguiu todos os passos do artigo você já está sabendo sobre feature flag e já implementou diversas estratégias de rollout utilizando esse recurso.

Todo o material utilizado neste artigo está disponível no repositório GIT na pasta nodejs.

Se você não conhece nodeJS é quer aplicar o conteúdo deste artigo utilizando outra linguagem, eu tive uma experiencia muito boa no java com o togglez.

Outro ponto importante sobre linguagens, é em relação ao client. O unleash possui client para várias outras linguagens, então você pode ter o unleash-server executando no nodejs que é bem simples e o client configurado em sua aplicação java ou go por exemplo.

espero que tenham gostado e que usem esse recurso nos seus próximos projetos

× Como posso te ajudar?