3 pontos por GN⁺ 2024-09-22 | 1 comentários | Compartilhar no WhatsApp
  • Kamal Proxy é um pequeno proxy HTTP que opera na frente de aplicações web, projetado para alternar o deploy para novas instâncias sem interromper o tráfego em andamento
  • Novas instâncias são registradas com kamal-proxy deploy, e o proxy só roteia novo tráfego para essa instância depois que ela passa em um health check HTTP
  • O comando de deploy espera até que todo o tráfego saia da instância anterior, então, depois de retornar com sucesso, é possível remover a instância antiga sem interromper requisições em andamento
  • Para operar várias aplicações em um único proxy, ele oferece roteamento baseado em host, roteamento baseado em caminho, TLS automático e especificação manual de certificados TLS
  • O Kamal Proxy foi projetado como parte da ferramenta de deploy Kamal, mas também pode ser usado de forma independente ou como componente de outras ferramentas de deploy

O que o Kamal Proxy faz

  • Kamal Proxy é um pequeno proxy HTTP para facilitar a orquestração de deploys com zero downtime
  • Ao executar uma aplicação web atrás do Kamal Proxy, é possível publicar mudanças sem interromper o tráfego em andamento
  • Não é necessária nenhuma cooperação especial por parte da aplicação para que isso funcione
  • O Kamal Proxy foi projetado para funcionar como parte do Kamal, que inclui empacotamento em contêineres e provisionamento
  • Também pode ser usado sozinho ou como parte de outras ferramentas de deploy

Execução e fluxo de deploy

  • A instância do proxy é iniciada com o comando kamal-proxy run
  • Não há arquivo de configuração; é possível informar opções quando os padrões não forem adequados para a aplicação
    • A porta HTTP padrão é 80
    • Para executar em outra porta, use kamal-proxy run --http-port 8080
    • Todas as opções podem ser consultadas em kamal-proxy help run
  • Para enviar tráfego para a aplicação web, faça o deploy da instância da aplicação no proxy
  • A instância de destino é especificada no formato hostname:port
    • Exemplo: kamal-proxy deploy service1 --target web-1:3000
    • Esse comando registra web-1:3000 com o nome de serviço service1

Como funciona a troca com zero downtime

  • Quando uma nova instância é registrada, o Kamal Proxy executa imediatamente um health check HTTP para verificar acessibilidade e funcionamento
  • Se o health check for bem-sucedido, ele começa a rotear o novo tráfego para a nova instância
  • Se a nova instância não ficar healthy dentro do tempo adequado, o comando deploy interrompe o deploy e retorna um código de saída diferente de zero
  • Cada deploy transfere todo o tráfego das instâncias implantadas anteriormente para a nova instância
  • O comando deploy só retorna depois de esperar que todo o tráfego saia da instância anterior
    • Depois de retornar com sucesso, é possível remover a instância anterior sem interromper requisições em andamento
  • Como a nova instância só recebe tráfego quando está healthy e a instância anterior só é removida depois de ser totalmente drenada, o deploy acontece com zero downtime

Configuração de health check

  • O health check padrão envia uma requisição GET para /up uma vez por segundo para cada serviço
  • Uma resposta 200 é considerada healthy
  • Para adaptar o health check à aplicação, use as flags de deploy
    • --health-check-path
    • --health-check-port
    • --health-check-timeout
    • --health-check-interval
  • Exemplo de mudança do caminho do health check:
    • kamal-proxy deploy service1 --target web-1:3000 --health-check-path web/index.html
  • Se o endpoint de health estiver exposto em uma porta diferente da do serviço principal:
    • kamal-proxy deploy service1 --target web-1:3000 --health-check-port 8080

Recursos de roteamento

  • Roteamento baseado em host

    • Com roteamento baseado em host, é possível rotear tráfego para várias aplicações no mesmo servidor com uma única instância do Kamal Proxy
    • É possível especificar um host específico no momento do deploy
    • kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com
    • Instâncias implantadas dessa forma recebem tráfego apenas do host especificado
    • Ao definir hosts separados para cada aplicação, é possível executar várias aplicações no mesmo servidor sem conflito de portas
    • Um host específico só pode rotear para um serviço por vez
    • Se service1 já estiver usando app1.example.com, será retornado um erro quando service2 for implantado com o mesmo host
    • Depois de remover service1, será possível implantar service2 no mesmo host
  • Roteamento baseado em caminho

    • O roteamento baseado em caminho é usado em aplicações que dividem o tráfego entre serviços diferentes com base no caminho da requisição
    • Os serviços podem ser montados sob diferentes prefixos de caminho
    • Exemplo de envio de requisições que começam com /api para web-1: kamal-proxy deploy service1 --target web-1:3000 --path-prefix=/api
    • Exemplo de envio do restante para web-2: kamal-proxy deploy service2 --target web-2:3000
    • Por padrão, o prefixo do caminho é removido antes de ser repassado ao upstream
    • A requisição /api/users/123 é encaminhada para web-1 como /users/123
    • Para encaminhar o caminho original sem alterações, especifique --strip-path-prefix=false

Suporte a TLS

  • O Kamal Proxy pode obter e renovar automaticamente certificados TLS para aplicações
  • O TLS automático é ativado adicionando a flag --tls no momento do deploy
    • kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tls
  • O TLS automático exige a especificação de um host
    • Essa condição existe para impedir solicitações maliciosas de certificados para nomes de host arbitrários
  • No roteamento baseado em caminho, a opção de TLS deve ser configurada na rota raiz
    • Serviços implantados em outros caminhos do mesmo host usam a configuração TLS definida na rota raiz
  • Se for necessário instalar um certificado TLS obtido manualmente, uma autoridade certificadora própria ou um Cloudflare origin certificate, é possível especificar diretamente o arquivo do certificado e o caminho da chave privada
    • --tls-certificate-path cert.pem
    • --tls-private-key-path key.pem

Variáveis de ambiente e build

  • Em ambientes como execução em contêiner Docker, as opções de run podem ser definidas como variáveis de ambiente
  • kamal-proxy run lê o valor de cada opção a partir das variáveis de ambiente, se elas estiverem definidas
    • kamal-proxy run --http-port 8080
    • HTTP_PORT=8080 kamal-proxy run
  • Se o nome da variável de ambiente entrar em conflito com o ambiente existente, é possível diferenciá-lo adicionando o prefixo KAMAL_PROXY_
    • KAMAL_PROXY_HTTP_PORT=8080 kamal-proxy run
  • O build local é feito com make em um ambiente Go funcional
  • Para fazer o build com contêiner Docker, use make docker
  • Há uma configuração Docker Compose na pasta example para testar os comandos do proxy

1 comentários

 
GN⁺ 2024-09-22
Opiniões no Hacker News
  • É estranho terem escolhido deploy como nome da ação
    É um termo já sobrecarregado, então deve gerar confusão do tipo “o app foi implantado? Não, foi implantado no proxy?”
    Poderiam ter usado algo como bind, intercept ou simplesmente proxy; não entendo por que deploy

    • “Implantado” deve ser entendido como: está em execução e registrado no proxy
    • Se o tráfego de entrada vem pelo proxy, não vejo o que deploy poderia significar além de “o tráfego do proxy começou a fluir para a nova instância do app”
  • Hoje em dia dá para fazer deploy sem downtime facilmente também com outros servidores, então é interessante a escolha de transformar isso em um app separado inteiro
    Por exemplo, com um app + proxy web que suporte sockets Unix, dá para fazer sem downtime apenas movendo um arquivo
    É atômico, e também dá para enviar requisições de aquecimento com curl
    Criar até um sistema de registro parece exagero

    • Isso é só uma parte muito pequena do Kamal(https://kamal-deploy.org)
      Kamal é uma ferramenta de deploy usada para economizar milhões de dólares ao migrar da nuvem para hardware próprio(https://basecamp.com/cloud-exit)
    • Seria bom se você pudesse explicar um pouco mais esse método
      Se houver algum texto de referência, gostaria de aprender mais
  • Isso parece ser principalmente uma tentativa de lidar com o problema fundamental de queda de tráfego ao trocar contêineres de serviço no Docker Swarm
    Passei pelo mesmo problema ao criar um servidor JAMStack na Cloud 66, usei Caddy em vez de um proxy próprio, e acho que Traefik também teria servido muito bem
    Não sei por que o Kamal escolheu Swarm em vez de k8s ou k3s, mas a complexidade precisa morar em algum lugar e não dá para escondê-la, então parece que acabaram indo para um proxy próprio
    Não usei pessoalmente, mas fico bastante cético, porque parece que terão de ficar correndo atrás de suporte a WebSockets, SSE, HTTP/3, diversos tipos de compressão e criptografia

    • O Kamal parece ter sido criado sobre a premissa de que “Kubernetes é complexo demais”, e parte dessa justificativa para recriar boa parte do que o Kubernetes faz
      Parece um exemplo de um proxy reverso que começou simples e acaba absorvendo continuamente a complexidade que tentava evitar
      Acho que sistemas de execução amplos e extensíveis precisam de concorrentes, mas o Kamal parece um trabalho que está recuando para dentro da complexidade que queria evitar
      A capacidade e a flexibilidade do Kubernetes como framework de gerenciamento de estado desejado acabam ficando ainda mais evidentes
    • Parece que você está misturando orquestração com proxy
      Dá para continuar usando Docker e ainda assim usar Caddy, Traefik ou Envoy como proxy
      O Kubernetes também acaba usando esses componentes como controladores de ingresso em muitos casos
    • Não sei onde você viu que o Kamal usa Docker Swarm
      Pelo que vejo, não parece usar Swarm
    • O Kamal usa comandos docker run comuns, e não há Swarm nem orquestração
      Basicamente é uma ferramenta simples que substitui o Capistrano
    • Se esse for o motivo, parece que você não fez nem uma pesquisa bem básica sobre o assunto
      O básico de um serviço de orquestração de contêineres é lidar com conexões e deploy blue/green por meio de um controlador de ingresso
      Mais precisamente, controlador de ingresso é o nome do papel atribuído a inúmeros proxies reversos que já lidam com esse caso de uso há anos, ou até décadas
  • Seria bom se alguém pudesse explicar rapidamente como deploy sem downtime geralmente funciona
    Imagino que as duas versões do app fiquem no ar ao mesmo tempo e que o tráfego novo seja roteado para a versão nova
    Mas, se houver um único banco de dados e a nova versão do app trouxer uma alteração de schema, o script de migração deve alterar o schema na inicialização, enquanto a versão antiga do app espera o schema antigo
    Fico curioso para saber como isso é tratado

    • O primeiro passo é separar migrações e deploy
      Muitos frameworks executam migrações ao fazer deploy do código, mas na prática é melhor controlar manualmente o momento da execução
      Cada versão do código precisa funcionar tanto com o schema atual quanto com o schema futuro pós-migração, tornando-se, na prática, código compatível com versões anteriores
      O fluxo fica: “fazer deploy do novo código que funciona tanto com o schema atual quanto com o futuro → verificar → executar a migração → verificar → remover o código que lidava com o schema desaparecido”
    • As migrações precisam ser compatíveis com versões anteriores, de modo que o schema do banco de dados consiga dar suporte às duas versões do app
      Esse é o custo extra de fazer deploy sem downtime ou rolling deploy, e empresas grandes em geral fazem assim
    • Outros comentários explicaram os métodos para quando você realmente precisa de deploy sem downtime, mas, para a maioria das organizações e na maioria das migrações, o downtime causado por migração de DB é praticamente indistinguível de zero
      Especialmente se você atende usuários em uma região específica e consegue escolher um horário tranquilo para fazer o deploy
    • Não usei pessoalmente, mas a Xata parece ter criado uma solução bem elegante para migrações de DB, pelo menos no Postgres
      Duas versões do app podem ser executadas ao mesmo tempo
      https://xata.io/blog/multi-version-schema-migrations
    • Certo, em algum momento as duas versões precisam estar em execução
      O load balancer começa a aceitar conexões no Server2 e deixa de aceitar novas conexões no Server1
      Depois, quando todas as conexões do Server1 forem fechadas, o Server1 é desconectado
      Podem ser servidores diferentes ou vários workers em um único servidor
      Durante esse período, como disseram outros comentários, as migrações precisam ser compatíveis com versões anteriores
  • O Kamal 2 está atualmente em RC (https://github.com/basecamp/kamal/releases) e, como vai oferecer suporte a SSL automático, estou animado com a possibilidade de rodar facilmente vários apps com Kamal em um único servidor

    • Ele usa o pacote autocert, que é quase uma implementação mínima
      É frágil, e os limites de emissão do Let's Encrypt e os limites de certificados simultâneos dificultam escalar horizontalmente instâncias do proxy
      Caddy/Certmagic resolve esse problema gravando dados em um armazenamento compartilhado, emitindo apenas um certificado e fazendo todas as instâncias reutilizarem e coordenarem via armazenamento
      Também não há troca de emissor, contorno de limites de emissão nem suporte a ARI
      Segurar requisições até que o upstream fique disponível também é algo que o Caddy trata bem se você configurar try_duration e try_interval em reverse_proxy
      O tratamento de cabeçalhos de proxy também não leva em conta IPs confiáveis; se ativado, alguém pode definir X-Forwarded-For e falsificar o IP
      Ainda bem que vem desativado por padrão, mas também não há nenhum aviso
      Entendo que o objetivo seja simplicidade, mas, no estado atual, parece imaturo, e há tantas opções melhores que eu não teria vontade de usar em produção
  • Não entendi bem como se usa isso e sinto que estou deixando passar alguma coisa
    O exemplo inicia 4 réplicas do serviço web
    Se é possível implantar em uma dessas réplicas, como example-web-1, para criar o serviço, não sei o que as outras 3 réplicas fazem
    Para atualizar o web e fazer uma implantação sem downtime, parece que seria preciso executar o comando de build no serviço web, iniciar esse serviço de alguma forma e então executar o deploy para o novo destino
    Mas, se você executar docker compose up --build --force-recreate web, as réplicas existentes são derrubadas, tornando tudo sem sentido

    • Quanto à primeira pergunta, as outras réplicas são balanceadas pelo Docker, que retorna vários IPs em requisições VIP ou DNS[0]
      Não verifiquei se esse proxy distribui a carga entre os vários registros retornados por uma consulta DNS, mas pelo menos com balanceamento baseado em VIP ele deve funcionar como esperado
      A parte da segunda atualização é menos clara
      Talvez a expectativa seja subir um serviço com outro nome na mesma rede e executar kamal-proxy deploy
      Pode ser algo como colocar o número da versão no nome do serviço e, se quiser reverter rapidamente, também faz sentido manter a versão anterior quente
      [0]: https://docs.docker.com/reference/compose-file/deploy/#endpo...
    • Não sei por que simplesmente não usar k8s rollout restart deployment
      Ou então alternar o DNS ou o roteador entre dois backends
  • Fico curioso se isso implementa o padrão de pausa de tráfego
    É uma abordagem em que o proxy basicamente congela o tráfego por alguns segundos; as requisições recebidas parecem demorar alguns segundos a mais que o normal, mas são concluídas após um breve atraso
    Durante esses segundos, dá para executar uma pequena migração de banco de dados ou alguma mudança de infraestrutura bloqueante um pouco mais complexa que possa terminar em até 5 segundos

    • Fico curioso se alguém já fez isso em produção de verdade
      Parece bem arriscado, e trabalhei por alguns anos em uma empresa de servidores de aplicação, mas nunca ouvi falar desse padrão
      Ainda assim, se vem de um cocriador do Django, ele provavelmente já viu muitas implantações, então é difícil simplesmente ignorar
    • Sinceramente, soa como o padrão de viver perigosamente
    • O Caddy faz isso
      Lembro de termos conversado sobre esse assunto no passado
  • Fico curioso onde foi explicado o motivo de criar um proxy próprio, em vez de usar Traefik ou alguma outra ferramenta comprovada

  • Fico curioso se há uma forma de configurar timeouts
    https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
    https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
    https://blog.cloudflare.com/exposing-go-on-the-internet/

  • É NIH. Nada mais a acrescentar