Intro
O Canary Release é uma estratégia de deploy que consiste em rotear uma parte dos seus usuários para uma nova versão, com isso você consegue monitorar qual será o comportamento dessa nova versão sem afetar a todos os seus usuários caso ocorra algum erro.
Há algumas maneiras de fazer isso, no Kubernetes isso é possível nativamente aumentando o número de réplicas dos pods novos e diminuindo os pods com a versão antiga, porém para ter uma porcentagem de 1% da nova versão, nós teríamos que ter necessariamente 1 pod com a versão nova e outros 99 com a versão antiga, o que não vai de encontro com a filosofia do Docker/Kubernetes que vieram para resolver a otimização de recursos entre outros problemas.
Outra forma de implementar essa estratégia é usando um Load Balancer nesse caso vamos usar o Nginx, com ele conseguimos manipular encaminhamento das requisições dizendo que uma porcentagem do tráfego irá para uma versão especifica, com isso podemos ter somente 1 pod com a nova versão e 1 com a antiga e ainda assim atender 1% das transações com a nova versão.
Instalação no cluster Kubernetes
Primeiro faça o clone do projeto https://github.com/fmaced1/canary-release-nginx-ingress-controller
git clone https://github.com/fmaced1/canary-release-nginx-ingress-controller
Para essa demo, iremos usar o namespace canary, mude nos arquivos yaml para o namespace que preferir.
Instale o nginx ingress controller:
kubectl apply -f nginx-ingress-controller/ingress-nginx-manifests.yaml -f nginx-ingress-controller/expose-ingress-nginx.yaml
kubectl rollout status deploy nginx-ingress-controller -n ingress-nginx -w
Se você estiver em um cluster Kubernetes que não tenha um Load Balancer, instale o MetalLB ele irá atribuir um ip público ao seu Nginx (Ingress Controller), se você estiver em um ambiente cloud provavelmente você já tem um LB do seu provider.
Ajuste o range de ips que serão alocados para o MetalLB gerenciar:
# metallb/configmap-metallb.yaml
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.x.y-192.168.x.z # ajuste aqui o range de ips
Instale o MetalLB para ter um loadbalancer:
kubectl apply -f metallb/configmap-metallb.yaml
kubectl apply -f metallb/metallb-manifests.yaml
Canary release
Faça o deploy da aplicação v1:
# app-v1.yaml contém o deploy e o svc
# ingress-v1.yaml contém a rota
kubectl apply -f nginx-canary/apps/app-v1.yaml -f nginx-canary/apps/ingress-v1.yaml
Agora faça o deploy da segunda versão:
# app-v2.yaml contém o deploy e o svc
kubectl apply -f nginx-canary/apps/app-v2.yaml
Liste o serviço ingress-nginx para saber o ip que a sua app irá responder, esse ip irá responder por k8s.local.
kubectl get svc ingress-nginx -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx LoadBalancer 10.106.17.108 192.168.x.y 80:31574/TCP 53m
Como não temos um dns, vamos colocar o nome da maquina no /etc/hosts apontando para um nome qualquer:
export EXTERNAL_IP_INGRESS_NGINX="192.168.x.y"
echo "$EXTERNAL_IP_INGRESS_NGINX k8s.local" >> /etc/hosts
Depois disso conseguimos fazer uma requisição para a rota da aplicação, deixe esse comando rodando em outro terminal:
count=0; while sleep 0.3; do let count+=1 ;echo $count - $(curl -s k8s.local); done
# output
1 - Host: my-app-v1-84ff7f48cc-kcn57, Version: v1.0.0
2 - Host: my-app-v1-84ff7f48cc-kcn57, Version: v1.0.0
3 - Host: my-app-v1-84ff7f48cc-kcn57, Version: v1.0.0
Agora vamos dividir o tráfego, 10% para o svc app-v2 e o resto continua no svc da app-v1:
kubectl apply -f nginx-canary/by-weight/ingress-v2-canary.yaml
Veja que de 300 requisições apenas 32 foram para app v2:
bash canary/nginx-canary/curl-canary.sh k8s.local
...
v1: 290 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 291 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 292 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 293 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 294 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 295 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 296 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 297 v2: 30 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 297 v2: 31 - Host: my-app-v2-dfdff8845-n6bml, Version: v2.0.0
v1: 298 v2: 31 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 299 v2: 31 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 300 v2: 31 - Host: my-app-v1-84ff7f48cc-4d9kq, Version: v1.0.0
v1: 300 v2: 32 - Host: my-app-v2-dfdff8845-n6bml, Version: v2.0.0
Quando estiver satisfeito com a app-v2, exclua o ingress-canary:
kubectl delete -f nginx-canary/by-weight/ingress-v2-canary.yaml
E vire todo o tráfego para a app-v2
kubectl apply -f nginx-canary/apps/ingress-v2.yaml