< Insights

Quem É Hoje? Como os princípios de DevSecOps encurtaram nossas dailies

  • Cloud e Infraestrutura
  • Artigo

Quem está envolvido no mundo de tecnologia, provavelmente está utilizando métodos ágeis e pratica a cerimônia mais comum: a daily. Mas quem já não assistiu aquela daily que deveria durar 15 minutos se estender para 20, 30 ou até uma hora?

Isso também foi verdade aqui no time de DevOps da Objective. Mas quem disse que o ferramental de DevSecOps, sua automação e integração se limitam apenas a entrega de software?

Nesse artigo, vou compartilhar como utilizamos os princípios de DevOps para desenvolver e implantar uma ferramenta que diminuiu em quase 15% a duração das nossas dailies.

O Cenário

Primeiro, acho justo ressaltar um ponto frequentemente esquecido quando se trata de “métodos ágeis”: adaptabilidade. Adaptabilidade não só no software, mas também no comportamento do time. É por isso que o DevSecOps adotou, modificou ou eliminou conceitos de várias estruturas de gestão de projetos até que o resultado funcionasse com o time, ou seja, fosse realmente ágil. O relevante para o momento é:

  • O time tem dailies antes do almoço, para incentivar que terminem no tempo correto.
  • O time cresceu consideravelmente logo antes da duração das dailies se tornar um problema.

Segundo, a Objective trabalha em modelo remoto, assim como muitas empresas de tecnologia. Para facilitar a interação entre as pessoas, foi adotado um escritório virtual: o WorkAdventure. Pode parecer “bobo”, mas sua adoção foi muito rápida e os resultados foram muito positivos. Como qualquer escritório, o time de DevSecOps também tem… Uma sala de reuniões!

Nessa sala de reuniões o time hospeda suas cerimônias, e foi nela que passamos a ter um problema…

O Problema

Durante a daily, um membro do time, rotacionado diariamente em ordem alfabética, compartilha a tela e apresenta o quadro de tarefas.

Inicialmente, a cerimônia se iniciava às 11:45, logo antes do almoço. Conforme o time cresceu, passamos a iniciar às 11:40, para acomodar o tempo dos novos membros. E então 11:35. Podem ver onde isso está indo? Porém, o aumento do time não foi o único fator no aumento do tempo! A rotação do apresentador, etapa aparentemente inofensiva, teve um impacto significativo.

Rapidamente uma frase se tornou tão icônica que virou motivo de piada: Quem é hoje?. “Quem apresenta hoje?”, “Quem foi ontem?”, “Quem que é?”, “Sou eu hoje?” e outras variações se tornaram comuns. Com o crescimento do time era cada vez mais difícil lembrar quem apresentou a daily anterior. Pior ainda se haviam finais de semana ou feriados!

Como resultado, havia um atraso de 5 ou mais minutos. Pode parece pouco, mas pense comigo: 20 pessoas x 5 minutos x 5 dias por semana = 500 minutos, mais de 8 horas perdidas por semana! Sem contar que as pessoas estão famintas e loucas para almoçar.

Desse problema, surgiu uma ideia: já integramos, automatizamos e simplificamos o processo de tanta coisa… Por que não fazer o mesmo para descobrir quem é hoje?

Inventando o “Quem É Hoje?”

Dado o problema descrito acima, comecei o processo de planejamento de uma ferramenta, a qual precisaria:

  • Manter o controle de quem irá apresentar a daily.
  • Ser de fácil utilização, de preferência visível de dentro do WorkAdventure.
  • Conseguir tratar finais de semana e feriados.
  • Conseguir ser “sobrescrita”, trocando a pessoa que irá apresentar no dia em situações excepcionais.
  • “Conhecer” os membros do time. Ninguém quer ficar atualizando uma lista manualmente!

Dado que minha afinidade é maior com DevOps e backend, resolvi formar uma parceria com um colega com mais afinidade em UX, design e frontend em geral para trabalharmos juntos, e decidimos pelo seguinte:

  • Usar a capacidade do WorkAdventure de integrar sites externos para embutir uma “telinha” com o nome do apresentador.
  • Na “telinha” queremos ver o apresentador da daily de ontem, quem é hoje e quem será amanhã.
  • Deve ser possível, usando a telinha, pular para o próximo ou voltar, para lidar com situações especiais (exemplos: ausências médicas ou PTO).
  • O WorkAdventure torna chamadas HTTP um pouco complicadas devido ao seu sandboxing, uma medida de segurança para evitar ataques e outras brechas de segurança que requisições arbitrárias podem causar. Porém, conexões Websocket não possuem essas limitações e são extremamente fáceis de fazer. Sendo assim, usaríamos Websockets para comunicação: além de ser um protocolo bem estabelecido, fácil de integrar e sem as limitações impostas pelo sandbox do WorkAdventure, ele também permite atualizações em tempo real.
  • A lógica de rastreamento do apresentador seria implementada em um pequeno backend, o qual será hospedado em nosso cluster Kubernetes.
  • O backend e o frontend teriam um pequeno protocolo de comunicação de mensagens em JSON, onde o frontend pode mandar “comandos” e o backend manda mensagens de atualização, tudo de maneira bi-direcional e assíncrona.
  • Para garantir que a lista de apresentadores sempre reflete os membros reais do time, ela será gerada consultando um LDAP.

Decisões tomadas, o outro membro do time partiu para implementação dentro do WorkAdventure enquanto comecei a implementação do backend.

Uma pausa para falar sobre arquitetura

Vale a pena ressaltar uma decisão apresentada acima: o “Quem É Hoje?” foi separado em duas partes: o backend, responsável pela funcionalidade, empacotado em container, e o frontend, embutido no WorkAdventure. Para todos os fins e propósitos, o backend será designado como o “Quem É Hoje”, enquanto o frontend apenas como cliente.

O backend e o frontend são completamente desacoplados e agnósticos entre si. Detalhes de linguagem de programação, implementação e implantação são irrelevantes entre eles, desde que o protocolo fosse bem estabelecido. Como exemplo, um cliente em CLI foi feito para ajudar com o debug do backend, decisão que simplificou muito o desenvolvimento e os testes.


Com o planejamento inicial finalizado, fiquei livre para decidir detalhes técnicos, começando por: em qual linguagem de programação escrever o “Quem É Hoje?”?

Haviam duas escolhas óbvias: o Javascript, usando Node e Sockets.io: boa parte do trabalho estava feita, a linguagem e ferramental foram praticamente criados para esse tipo de uso; ou Java, a qual é mais familiar para mim e outros membros do time (e do resto da Objective).

Porém, como este não era um projeto crítico, sem prazo de entrega, sendo feito a parte das tarefas normais, foi decidido tentar algo mais ousado: o Quem É Hoje? será programado em Rust. Essa linguagem provavelmente não seria a primeira escolha, porém estava curioso sobre essa nova linguagem, com grandes promessas e potencial, sendo a segunda escolha mais popular para linguagens novas que desenvolvedores querem adotar em 2022 (logo atrás de Go) . Essa se tornou então uma ótima oportunidade para mergulhar de cabeça no desconhecido e testar algo novo, dado o projeto de pequeno porte e baixa criticidade.

Descobrindo os Membros do DevOps

Preferi começar pela parte de leitura de membros: era um requisito simples de integração com nossos sistemas e um bom ponto de partida. Sendo assim, imaginei como seria um “provedor de membros”. O resultado, foi algo assim:

pub trait MemberProvider {
    fn get_members(&self) -> Result<Vec<String>>;
}

Para quem é mais familiarizado com OOP, esse trecho define uma “interface” MemberProvider. Objetos que a implementam devem definir a função get_members e retornar um “Resultado” – uma lista de strings representando os membros. Essa função não recebe argumentos, exceto &self, que representa o próprio objeto e deve ser declarado explicitamente no Rust.

Como nunca havia programado em Rust, pensei em começar com o simples: implementar um “provedor de membros estático”: um objeto que, em sua construção recebe uma lista de membros e sempre retorna a mesma lista. No final, esse provedor se tornou muito útil para testes de integração (mais adiante).

O código ficou simples, um recorte da primeira versão é esse:

pub struct StaticProvider {
    member_list: Vec<String>,
}

impl StaticProvider {
    pub fn new(mut member_list: Vec<String>) -> Self {
        Self { member_list }
    }
}

impl MemberProvider for StaticProvider {
    fn get_members(&self) -> Result<Vec<String>> {
        Ok(self.member_list.clone())
    }
}

Imediatamente após essa implementação surgiu o primeiro problema: passamos por membros de maneira alfabética, sempre, logo mesmo esse provedor deveria tratar desse requisito. Também era necessário evitar duplicatas, afinal não podemos ter ambiguidade no momento que a pessoa vai apresentar! Aí surgiu o primeiro teste e ajuste de implementação:

// -- snip --
impl StaticProvider {
    pub fn new(mut member_list: Vec<String>) -> Self {
        member_list.sort_unstable();
        member_list.dedup();
        Self { member_list }
    }
}

// -- snip --

#[cfg(test)]
mod tests {
  use super::*;

  fn static_provider_should_return_unique_sorted_members() {
    let static_members = vec![
        "foo".into(),
        "bar".into(),
        "foo".into(),
        "bar".into()
    ];
    let static_member_provider = StaticProvider::new(static_members);
  
    let received_members = static_member_provider
        .get_members()
        .unwrap();
  
    let expected_members: Vec<String> = vec![
        "bar".into(),
        "foo".into()
    ];
    assert_eq!(expected_members, received_members);
  }
}

Pronto, eu já tinha programado algo “útil” em Rust! Para praticar, outros “provedores de membros”, além do desejado, foram implementados. Como não é o foco desse artigo, segue um resumo dos dois provedores mais interessantes, o LDAP Provider e o Cache Provider:

LDAP Provider“: esse foi o propósito original, ler do nosso LDAP. Foi utilizado a biblioteca ldap3 . Essa biblioteca também suporta o uso de um runtime assíncrono, casando perfeitamente com nossos requisitos O resultado resumido é esse:

// Notem que agora estamos usando async! Mais adiante.
  #[async_trait]
  impl MemberProvider for LdapProvider {
    async fn get_members(&self) -> Result> {
      // Conectamos no servidor…
      let (conn_handle, mut ldap) = self.connect().await?;
 
      // Procuramos…
      let search_result = ldap
        .search(
            &self.base_dn,
            self.scope,
            &self.filter,
            &self.attrs
        )
        .await?
        .success();
 
      // Desconectamos, afinal precisamos ser educados!
      match Self::unbind(conn_handle, ldap).await {
        Err(e) => {
          warn!(“Failed unbinding from LDAP! Error: {e}”);
        },
        Ok(_) => {},
      };
 
      let (result_set, _res) = search_result?;
 
      // Processamos os resultados e retornamos
      return Ok(Self::process_result_set(result_set));
    }
  }

Um problema dessa implementação é que ela consulta o servidor LDAP toda vez que é invocado. Esse comportamento é indesejado: são muitas requisições de rede e um ambiente mais volátil: e se o servidor LDAP ficar indisponível? E se houverem muitas requisições? Isso nos leva ao…

Cache Provider“: inicialmente pensei em manter um cache em cada provedor de membros, mas quando percebi que esse tipo de código se espalharia por todo provedor, decidi que um provedor dedicado era necessário. Apresento-os, o CacheProvider:

  pub struct CacheProvider<M: MemberProvider> {
    /* -- snip -- */
  } 

Um provedor que engloba outro provedor. A ideia é simples: ele mantém uma cópia do resultado por um certo tempo e a devolve em vez de fazer novas requisições. Como bônus, também possui uma funcionalidade de tratamento de erros: caso habilitada, uma lista de membros já esteja em cache e uma nova consulta retorne erro, o Cache Provider simplesmente retorna o que já tem salvo. Dessa forma, atingimos resiliência em relação à rede.

Websockets em Rust

Hora de implementar a comunicação em si. Já existem duas bibliotecas extremamente populares, a Tokio , um runtime assíncrono (e que já estava em uso devido ao LDAP Provider acima), e a Tungstenite , a qual possui uma implementação completa de Websockets conforme a RFC6455, ambas integradas através da Tokio Tungstenite .

Como estamos falando de comunicação, acho relevante compartilhar um pouco sobre as mensagens que o Quem É Hoje? e seus clientes trocam. Como falado anteriormente, as mensagens utilizam JSON. Este é um exemplo de mensagem que o Quem É Hoje? envia com a informação do apresentador da daily:

{
  "type": "DailyLeadInfo",
  "data": {
    "previous": "alice",
    "current": "bob",
    "next": "charlie"
  }
}

A estrutura é extremamente simples:

  • um campo type, com o tipo da mensagem;
  • e um campo data, com o conteúdo da mensagem.

Similarmente, para um cliente requisitar novamente a informação sobre o apresentador da daily, pode enviar ao backend essa mensagem:

{
  "type": "GetDailyLead",
  "data": {} // O campo data pode ser omitido quando vazio
}

E se deseja trocar o apresentador atual para o próximo ou anterior:

{
  "type": "UpdateDailyLead",
  "data": {
    "op": "Next" // ou "Previous"
  }
}

A primeira versão do Quem É Hoje? não possui autenticação ou sistema de permissões – isso está fora do escopo deste projeto – mas essa é uma feature planejada caso ele venha a se tornar algo maior.

Também existem algumas mensagens para tratamento de erros, porém são menos interessantes.

Para fazer a serialização e desserialização das mensagens, foi usada a Serde, um framework bastante popular para esse propósito. A declaração de todos os tipos de mensagens do serviço se resume a um arquivo de 67 linhas de puro Rust, com alguns macros da Serde. Por exemplo, esse é o código completo necessário para tratar a mensagem de atualização do apresentador, a qual foi exemplificada acima:

use serde::Deserialize;

#[derive(Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum IncomingMessageType {
    // A mensagem de informação, exemplificada por primeiro
    // e omitida aqui
    GetDailyLead(Option<GetDailyLeadData>), 
    UpdateDailyLead(UpdateDailyLeadData),
}

#[derive(Deserialize)]
pub enum UpdateDailyLeadOp {
    Next,
    Previous,
}

#[derive(Deserialize)]
pub struct UpdateDailyLeadData {
    pub op: UpdateDailyLeadOp,
}

Só isso! Não é necessário mais nada, a Serde já consegue tratar de tudo.

Para tratar a natureza bi-direcional assíncrona da comunicação, onde uma mensagem pode chegar a qualquer momento, sem nenhuma sequência ou ordem determinada, o Quem É Hoje? se comporta da seguinte maneira:

  • Quando um cliente conecta, o backend imediatamente envia apenas a esse cliente a informação do apresentador de hoje.
  • Se um cliente pedir uma atualização, o backend imediatamente envia apenas a esse cliente a informação do apresentador de hoje.
  • Se um cliente enviar uma ordem de atualizar o apresentador de hoje, o backend faz a troca e então envia a TODOS os clientes conectados a nova informação.
  • Para evitar problemas de concorrência, nenhuma resposta é enviada enquanto uma atualização está em progresso, várias atualizações em sequência esperam em uma fila, etc.

Rastreamento de dias úteis

O rastreamento de dias úteis é feito com uma pequena biblioteca separada que identifica se um dia é um final de semana ou feriado nacional brasileiro. O rastreamento então se torna questão de um “número de dias úteis” desde uma data arbitrária somado de um offset, incrementado ou decrementado após as mensagens UpdateDailyLead. Esse sistema funciona para maioria das datas, sendo necessária atualização manual apenas durante feriados regionais ou datas similares.

Testando a Comunicação

Escrever testes unitários para comunicação Websocket se mostrou complicado: exigiria muitos mocks, reestruturação do código de maneira não natural, etc. Então, decidi pular direto para testes de integração! As mudanças necessárias foram mínimas:

  • O conteúdo main do código foi movido para um lib.rs, transformando a aplicação em uma biblioteca. O novo main se tornou um pequeno wrapper para o código da biblioteca.
  • O cliente de CLI que eu estava usando para debug foi adaptado para permitir que fosse usado de maneira pragmática.
  • Um contador de dias úteis usado para testes, o qual sempre retorna um número fixo, foi movido para fora das seções de teste e disponibilizado como uma opção de configuração.

Feito isso, escrever os testes de integração se tornou mais simples que testes unitários. Como exemplo, o código do teste de integração que valida a funcionalidade de comunicar todos os clientes quando o apresentador é atualizado é este:

#[tokio::test]
pub async fn when_client_updates_next_daily_lead_server_updates_and_broadcasts_new_daily_lead() {
  let (mut c1, mut c2, server) = common::test_setup(
    // Lista de membros, lembram do provedor estático?
    vec!["first".into(), "second".into(), "third".into()],
    // Timeout para receber as mensagens
    Duration::from_secs(1), 
  )
  .await;
  
  let c1_actions = c1
    // O cliente 1 enfileira um envio de mensagem
    .send(SendMessageType::NextDailyLead) 
    .expect_json(json!({
      "type": "DailyLeadInfo",
      "data": {
        "previous": "first",
        "current": "second",
        "next": "third"
      }
    }))
    // Espera receber uma atualização sobre o apresentador
    .send(SendMessageType::NextDailyLead)
    // e então enfileira outra mensagem
    .expect_json(json!({
      "type": "DailyLeadInfo",
      "data": {
        "previous": "second",
        "current": "third",
        "next": "first"
      }
    }))
    // e "executa". Essa função returna uma Promise e
    // não será executada ainda.
    .execute();

  let c2_actions = c2
    // O cliente 2 espera receber a atualização do
    // apresentador como primeira mensagem,
    // consequência da primeira mensagem do cliente 1
    .expect_json(json!({
      "type": "DailyLeadInfo",
      "data": {
        "previous": "first",
        "current": "second",
        "next": "third"
      }
    }))
    // O cliente 2 espera receber a atualização do
    // apresentador como primeira mensagem,
    // consequência da segunda mensagem do cliente 1
    .expect_json(json!({
      "type": "DailyLeadInfo",
      "data": {
        "previous": "second",
        "current": "third",
        "next": "first"
      }
    }))
    .execute();

  // Aqui todas as ações são realizadas em paralelo,
  // de maneira assíncrona.
  tokio::select! {
    _ = c1_actions => (),
    _ = c2_actions => (),
    _ = server.listen() => (),
  }; 
}

No final, conseguimos realizar um teste completo, sem mocks, validando a funcionalidade completa do Quem É Hoje?, já que os testes realmente usam a comunicação através de websockets, tornando o restante da implementação mais fácil e seguro.

E o Frontend?

O frontend foi implementado em TypeScript e embutido diretamente no mapa do WorkAdventure. Sua arquitetura é baseada em eventos e sua implementação é extremamente simples, seguindo uma simples lógica:

  • Quando a pessoa clicar nesse botão, eu mando essa mensagem pro Quem É Hoje?.
  • Quando o Quem É Hoje? mandar essa mensagem, eu mostro esse conteúdo na TV.

Espera, na TV? Sim! O apresentador é exibido em uma televisão na sala de reuniões:

Nessa imagem a “televisão” está desconectada. Há uma animação enquanto a TV liga e desliga, igual às antigas TVs de tubo!

Resultado

A containerização e pipeline para build, testes automáticos, validação de merge requests e deploy da imagem gerada seguiram padrões de mercado e não são parte do escopo desse nesse artigo (assunto que poderá ser tratado em artigo futuro).

Como resumo, o resultado é um simples Pod rodando sem perturbações a muitos dias, usando apenas 6Mi de memória:

angelo@irgeshud:~ > kubectl -n quem-e-hoje get pods
NAME                          READY   STATUS    RESTARTS   AGE
quem-e-hoje-858b6bfb7-hjx2l   1/1     Running   0          28d
angelo@irgeshud:~ > kubectl -n quem-e-hoje top pod
NAME                          CPU(cores)   MEMORY(bytes)   
quem-e-hoje-858b6bfb7-hjx2l   1m           6Mi

Quando em uso, vemos isso:

Na foto, o André foi o apresentador da daily anterior, eu serei o apresentador da daily de hoje e o Bruno apresentará a próxima! Não precisamos nos preocupar em lembrar quem apresentará amanhã, a troca é automática! E “quem foi antes do feriado?” não é mais uma pergunta necessária, o Quem É Hoje? responde. As setinhas no canto inferior direito podem ser clicadas para avançar ou voltar o apresentador, caso precisemos de ajustes, e todos veem as mudanças ao vivo!

Como consequência, não perdemos mais tempo no início de nossas dailies, poupando entre 2 e 4 minutos. Hoje em dia, graças ao Quem É Hoje? (e algumas outras pequenas mudanças), nossas dailies tem duração média de 20 minutos, mesmo com um time de tamanho considerável.

Insights do nosso time

Obtenha insights do nosso time de especialistas sobre metodologias de desenvolvimento de software, linguagens, tecnologia e muito mais para apoiar o seu time na operação e estratégia de negócio.
Saiba mais sobre a Objective
Pergunte algo sobre nossa expertise, serviços e consultoria.