Canary Release Com Istio e Ambassador

Bruno Mata
#EmpiricusTech
Published in
10 min readJan 22, 2021

Um dos momentos mais esperados e emocionantes do processo de desenvolvimento de software é a publicação do projeto em produção. É nessa hora que o desenvolvedor vê, de fato, o resultado do seu trabalho.

Existem diversas formas de se publicar um projeto em produção. Você pode rodar sua aplicação em uma arquitetura on premise, através de servidores físicos alocados em um data center próprio da sua empresa, ou em uma arquitetura em cloud, como a da Amazon. Nos dois modelos, em algum momento, você vai se deparar com uma situação na qual será necessário colocar uma nova versão da sua aplicação no ar com a produção rodando.

As empresas normalmente possuem processos de qualidade (QA), cujo objetivo é reduzir o volume de defeitos antes que o código vá para produção. Porém, estas rotinas nem sempre garantem que sua aplicação seja publicada sem bugs ou com uma boa performance. Neste texto, eu vou te apresentar uma nova forma de disponibilizar aplicações em produção aumentando o padrão de segurança e qualidade, que é o Canary Release.

Um ponto interessante sobre esse nome é que ele foi inspirado em uma técnica antiga, usada por mineradores, que consistia em levar canários para dentro das minas de carvão. Os pássaros são muito mais sensíveis a gases tóxicos do que os humanos, e a morte deles representava aos mineradores que era hora de voltar.

De forma muito menos dramática, a técnica de Canary Release permite que se “avance” na publicação de uma versão de um micro-serviço, mitigando eventuais problemas não encontrados em fases de QA. Isso é garantido através de um mecanismo que possibilita que apenas um percentual do tráfego seja direcionado para a nova versão.

Expondo apenas um pequeno percentual de clients à nova versão.

Dessa forma, é possível aumentar ou diminuir o tráfego de uma rota avaliando o seu comportamento em produção em tempo real. Isso traz mais segurança no processo de entrega contínua de software, e é bastante vantajoso para sistemas críticos. Aqui na Empiricus utilizamos “release canário” para trocar a versão de um micro-serviço de pagamentos do e-commerce.

É importante dizer que, no nosso caso, nenhum pobre pássaro precisou morrer para o sucesso da estratégia.

Na nossa área de tecnologia existe uma cultura de engenharia de software bem estabelecida, que segue a risca as boas práticas sobre arquiteturas de micro-serviços. Cada projeto dentro do ecossistema tem sua função específica de negócio. É o caso da nossa loja virtual, onde temos um leque de micro-serviços, ou, unidades de negócio, que comunicam entre si para processar pedidos e renovações de assinaturas dos nossos produtos de research.

No segundo semestre de 2020 nós decidimos reescrever o micro-serviço de pagamentos, trocando a sua stack original Python Django para Java 14 com Spring WebFlux. Uma das premissas nessa empreitada era fazer uma troca transparente, ou seja, não poderíamos reajustar os outros micro-serviços, nem mesmo frontends, com novas rotas de serviço disponíveis na versão Java. Faríamos essa troca de forma gradual, sem que os clients soubessem da mudança, migrando rota por rota, e foi por isso que optamos por utilizar a técnica de canary release.

Para entender como essa técnica nos ajudou, primeiro você precisa entender um pouco da nossa arquitetura. De forma macro, temos um cluster Kubernetes fornecido e gerenciado pela Amazon Web Services, popularmente conhecido como AWS EKS, e ali estão os nossos micro-serviços. Para fazer a gestão do tráfego de entrada deste cluster (norte-sul) temos o Ambassador API Gateway e, para gerir o tráfego interno do cluster entre os micro-serviços (leste-oeste), utilizamos o service mesh Istio.

Visão simplificada de um cluster Kubernetes com Ambassador API Gateway e Istio

Um cluster Kubernetes tem a sua própria rede interna isolada fazendo com que toda comunicação interna/externa fique concentrada em componentes chamados de Ingress. Segue a definição encontrada no site oficial:

An API object that manages external access to the services in a cluster, typically HTTP.

Atuar como “Ingress” é um dos papéis do Ambassador API Gateway na nossa arquitetura, ele também atua como proxy reverso trazendo uma série de features como:

  • Load Balancing
  • TLS Termination
  • Rate Limiting
  • Authentication

Eu citei aqui apenas algumas delas, mas você pode conferir a lista completa na documentação. Além dessas features, o Ambassador também possibilita a execução de canary release.

Também é importante mencionar que o Ambassador funciona como uma camada de abstração por cima de um proxy L7 chamado Envoy. É ele o responsável por todo o trabalho operacional de gerenciar o tráfego de requisições, ficando para os outros componentes a função de gerir as configurações do usuário.

É possível, por exemplo, configurar os mapeamentos de rotas definindo regras de roteamento baseada em critérios como: prefixos de URL, verbos HTTP, dados de cabeçalhos e também os percentuais desejados de tráfego sobre o total. Mostrarei tudo isso mais abaixo, mas antes, vamos conhecer o outro componente crucial dessa arquitetura.

Para usar as palavras do meu caro amigo e colega Rodrigo Gianotto, o Istio hoje é a vanguarda da tecnologia de service mesh. Para você que não está familiarizado com o termo, esta introdução da documentação do Istio ilustra bem o assunto:

Cloud platforms provide a wealth of benefits for the organizations that use them. However, there’s no denying that adopting the cloud can put strains on DevOps teams. Developers must use microservices to architect for portability, meanwhile operators are managing extremely large hybrid and multi-cloud deployments. Istio lets you connect, secure, control, and observe services.

At a high level, Istio helps reduce the complexity of these deployments, and eases the strain on your development teams. It is a completely open source service mesh that layers transparently onto existing distributed applications. It is also a platform, including APIs that let it integrate into any logging platform, or telemetry or policy system. Istio’s diverse feature set lets you successfully, and efficiently, run a distributed microservice architecture, and provides a uniform way to secure, connect, and monitor microservices.

Como dito anteriormente, o Istio é responsável por gerenciar todo o tráfego interno do cluster Kubernetes. Ele faz isso injetando side-cars proxies dentro dos pods que, quando iniciam, configuram uma route table para interceptar toda requisição destinada ao pod.

De forma bem parecida com o Ambassador, o Istio também atua em cima do Envoy Proxy, abstraindo a parte de configuração e também trazendo todas aquelas features de controle de tráfego, inclusive canary release.

Ok Bruno, você está falando sobre canary release, citou os componentes da sua arquitetura Kubernetes, mas não está claro como isso de fato te ajudou!

Bom o melhor a gente deixa pro final, não é mesmo? Vamos lá, recapitulando, nós precisávamos trocar a versão de um micro-serviço — não só a versão como também a stack — e isso precisava ser transparente, gradual e precisávamos ter o controle sobre o tráfego entre as duas versões.

A essa altura, talvez esteja claro para você que todo o trabalho do canary é feito pelos dois componentes citados: Ambassador e Istio. O Ambassador atuando no tráfego que vem de fora do cluster Kubernetes, por exemplo, o tráfego da internet, e o Istio atuando no tráfego da rede interna do cluster.

Começando pelo API Gateway, ele fornece um modelo de configuração baseando em Mappings. Os mapeamentos nada mais são do que abstrações de configurações de roteamento que são repassadas ao Envoy. São nesses mapeamentos que você vai determinar qual a rota de API você quer dividir o tráfego e qual o percentual desejado. Segue um exemplo de mapeamento de rota:

---
apiVersion: ambassador/v2
kind: Mapping
name: my-v1-some-resource-canary-release-route
prefix: /some-resource
rewrite: ""

host: my-host.com
service: my-v1-kubernetes-service.my-namespace.svc.cluster.local
timeout_ms: 30000
weight: 90
---
apiVersion: ambassador/v2
kind: Mapping
name: my-v2-some-resource-canary-release-route
prefix: /some-resource
rewrite: ""

host: my-host.com
service: my-v2-kubernetes-service.my-namespace.svc.cluster.local
timeout_ms: 30000
weight: 10

Nota: este texto assume que você tenha um cluster Kubernetes com Ambassador API Gateway instalado, caso tenha dúvidas de como fazer isso você pode conferir a documentação oficial aqui.

Vamos abordar os principais atributos dessa configuração:

  • prefix: aqui você diz ao Ambassador que ele aplique essa regra a qualquer requisição que contenha determinado prefixo. No nosso exemplo, temos dois mapeamentos que esperam tráfego do mesmo prefixo. Neste caso, outros critérios serão usados para eleger a regra a ser aplicada.
  • rewrite: aqui você diz se será preciso reescrever o prefixo original para o informado antes de direcionar a requisição, e isso é bastante útil para abstrair rotas de versão. No nosso exemplo não utilizamos, porém, o Ambassador exige que você coloque aspas vazias para que ele entenda que deve apenas repassar o prefixo, sem altera-lo.
  • host: neste atributo é informado qual é o host esperado para aplicar esta regra. Note que é o mesmo nos dois exemplos, ou seja, terei duas regras com critérios diferentes para chamadas direcionadas ao host “my-host.com”.
  • service: diferentemente do host, este atributo especifica qual é o destino após a regra ser eleita baseada nos critérios. Como pode-se ver no exemplo, cada regra direciona para uma das versões do nosso serviço.
  • weight: este é o famigerado atributo que faz toda a mágica do canary release. É aqui onde você diz qual o percentual de tráfego você deseja para esta rota e ele funciona como mais um critério para elegibilidade da regra. No nosso exemplo, mesmo após bater o prefixo e o host de uma requisição, o Envoy Proxy ainda faz um calculo baseado no percentual informado e determina se a requisição será direcionada para o serviço v2.

Com essa configuração, todas as requisições que vierem de fora do cluster Kubernetes com destino ao domínio my-host.com e com prefixo /some-resource passarão por uma fase de decisão que vai direcionar para algum dos dois serviços. Se determinada requisição for eleita para ir para a versão 2, essa requisição será então destinada para o serviço my-v2-kubernetes-service dentro do cluster. Isso é canary release.

Certo, mas e o tráfego interno? É aí que entra a segunda estrela desse texto. No Istio, conceitualmente, fazemos a mesma coisa, porém, como dito antes, o Istio hoje é o service mesh mais completo do mercado e, como diz um amigo, com grandes poderes, vem grandes complexidades.

Devido a robustez do Istio — que também tem vida fora do ambiente Kubernetes — é necessário construir dois CRDs ou Custom Resource Definition novos no cluster. Na prática você não precisa se preocupar com isso, a instalação nativa já os configura para você. É importante ressaltar que estes recursos não são objetos pré-existentes do Kubernetes. São eles: o VirtualService e o DestinationRule.

O DestinationRule:

… defines policies that apply to traffic intended for a service after routing has occurred. These rules specify configuration for load balancing, connection pool size from the sidecar, and outlier detection settings to detect and evict unhealthy hosts from the load balancing pool.

Em resumo aqui se define sub-conjuntos de configurações usadas pelo Istio, de um serviço dentro do Kubernetes, e isso inclui as versões deste serviço.

---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-v1-kubernetes-service-destination-rule
namespace: my-namespace
spec:
host: my-v1-kubernetes-service.my-namespace.svc.cluster.local
subsets:
- labels:
version: v1
name: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-v2-kubernetes-service-destination-rule
namespace: my-namespace
spec:
host: my-v2-kubernetes-service.my-namespace.svc.cluster.local
subsets:
- labels:
version: v2
name: v2

No exemplo acima, temos dois DestinationRules, um para cada serviço. A forma com que o Istio sabe que estou trabalhando com o deployment correto do service Kubernetes é através do atributo subset. Através dele, eu digo qual é a label do deployment ao qual essa regra vai pertencer, o que neste caso é a label version. Isso porque estamos trabalhando com um modelo baseado em dois services com um deployment cada, mas a robustez do Istio permite que você trabalhe também com apenas um service e dois deployments de versões diferentes.

O VirtualService:

… defines a set of traffic routing rules to apply when a host is addressed. Each routing rule defines matching criteria for traffic of a specific protocol. If the traffic is matched, then it is sent to a named destination service (or subset/version of it) defined in the registry.

De forma bastante resumida, este cara tem o mesmo papel que os Mappings do Ambassador. Veja um exemplo:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: my-kubernetes-service-virtual-service
namespace: my-namespace
spec:
hosts:
- my-v1-kubernetes-service.my-namespace.svc.cluster.local
http:
- name: my-some-resource-canary-release-route
match:
- uri:
prefix: /some-resource
route:
- destination:
host: my-v1-kubernetes-service.my-namespace.svc.cluster.local
subset: v1
weight: 90
- destination:
host: my-v2-kubernetes-service.my-namespace.svc.cluster.local
subset: v2
weight: 10

Note que aqui, a única diferença em relação aos mappings do Ambassador são os subsets definidos anteriormente pelos DestinationRules. Na prática a configuração do Istio produz o mesmo resultado da configuração do Ambassador, porém, para o tráfego interno.

É importante lembrar que esse é um caso de uso envolvendo dois componentes que fazem canary release, mas outros cenários também são possíveis. É possível centralizar toda a configuração de tráfego no Istio, mas neste caso é necessário que o service mesh gerencie também o tráfego de entrada do cluster. Ainda com o Ambassador, também é possível centralizar a configuração no Istio. Por padrão ele direciona as requisições via Envoy diretamente para os Pods, evitando passar pelo kube-proxy nativo do Kubernetes, e isso traz a necessidade do uso dos seus Mappings. Mas é possível configurar o Ambassador para usar o kube-proxy e assim centralizar a configuração no Istio.

Diferentes cenários exigem diferentes soluções. Foi assim que conseguimos trazer mais uma camada de segurança e qualidade aos deploys de produção, enquanto desenvolvemos a nova versão do serviço. A medida que vamos concluindo as fases de testes, determinamos um percentual de tráfego para as rotas entregues com base na criticidade e então colocamos no ar. A partir daí vamos avaliando o comportamento e incrementando o percentual até chegar a 100% na nova versão.

Espero ter trazido um pouco de luz sobre o tema para aqueles que não conhecem e também mostrar que é relativamente simples fazer isso com essas ferramentas. Não é nenhum bicho de sete cabeças. Hoje praticamente todos os reverse proxies de mercado fazem canary release e talvez a grande jogada dessas ferramentas seja a abstração em torno da configuração.

--

--