7 pontos por GN⁺ 2023-07-17 | 1 comentários | Compartilhar no WhatsApp
  • Serviços pessoais são operados diretamente em uma VM de $5/mês da DigitalOcean com Debian 10, e para serviços de pequena escala, colocar o software do servidor na VM continua sendo uma abordagem simples e razoável
  • Os apps de servidor são criados como binários estáticos em Rust, mantendo o artefato de deploy em um único arquivo, incluindo HTML, CSS, configuração e até segredos
  • O gerenciamento de execução fica com o systemd, enquanto o HTTPS é tratado por um proxy reverso nginx e por Let’s Encrypt/certbot, para que o app não precise lidar diretamente com TLS
  • Serviços que precisam de dados usam um arquivo único do SQLite, com backup diário no Tarsnap e backup em streaming para o DigitalOcean Spaces com o Litestream
  • Ao colocar vários serviços em uma mesma VM, eles são isolados com usuários Unix por serviço; se for necessária uma segurança mais forte, a escolha é uma VM separada ou systemd-nspawn e firejail

Configuração básica de servidor e deploy

  • thoughts.page, hanabi.site, cgmserver, phonebridge e outros serviços operam de forma próxima a esta configuração
  • Os apps rodam em uma VM da DigitalOcean, no plano de $5/mês, com Debian 10 como sistema operacional
    • Alguns serviços compartilham a mesma VM, e outros ficam separados em VMs diferentes
  • O software do servidor é escrito em Rust
  • Nessa abordagem, o deploy fica simplificado para copiar um único arquivo para o servidor
    • Considera que algo parecido também pode ser feito com Go, C++ e outras linguagens
    • Se a linguagem não facilitar deploy em binário único, uma alternativa é usar um contêiner Docker como artefato de build

Execução, proxy e preservação de dados

  • O systemd cuida do gerenciamento de inicialização dos serviços
    • Faz com que o binário seja executado junto com a inicialização do servidor
    • A maioria dos arquivos unit do systemd é um arquivo simples de 9 linhas
    • O próprio systemd é complexo, mas ao usá-lo apenas para iniciar o servidor no boot, quase não se entra em contato com essa complexidade
  • O deploy é feito com um script simples de deploy
    • Copia o binário para o servidor e reinicia o servidor
    • Permite rollback e garante que sempre haja uma versão válida em execução, mesmo que a conexão caia durante o deploy
  • Programas que precisam de banco de dados usam SQLite
    • Todo o estado do app fica em um único arquivo
    • Todos os dias, cria-se um backup com o comando SQLite .backup e ele é armazenado no Tarsnap
    • O script de backup é executado com cron
    • Com o Litestream, uma cópia do banco é transmitida para o armazenamento DigitalOcean Spaces em intervalos de segundos, e snapshots são criados a cada 6 horas
  • Todos os servidores rodam atrás de um proxy reverso nginx
    • O nginx faz a terminação de TLS, então o app não precisa se preocupar com HTTPS
    • Os certificados HTTPS são obtidos com Let’s Encrypt e certbot, com renovação automática
    • Um exemplo da configuração nginx do hanabi.site está em um gist separado
    • Arquivos estáticos podem ser servidos pelo nginx e copiados para o servidor com scp ou rsync

Manutenção e isolamento de serviços

  • Essa configuração busca um caminho de operação simples e robusto
    • Tirando o próprio app, o software no caminho de serving é composto por componentes testados e usados há décadas
    • Enquanto continuar pagando a DigitalOcean, operar o site essencialmente exige muito pouca manutenção
    • Os únicos problemas detectados pelo monitoramento foram questões temporárias de rede da DigitalOcean
  • Atualizações do sistema operacional e de segurança são ocasionalmente necessárias
    • As releases do Debian oferecem 5 anos de suporte, então seria preciso fazer upgrade para o Debian 11 em cerca de 2 anos e meio
    • Se acontecer de novo algo como o Heartbleed, será necessário aplicar patches
    • Esse tipo de incidente é raro
  • O custo de usar uma VM de $5/mês por serviço pode pesar, mas vários serviços podem rodar na mesma VM
    • O isolamento é feito criando uma conta de usuário Unix separada para cada serviço
    • Esse método de isolamento existe desde os primórdios do Unix e parece robusto
    • Se for necessário isolamento mais forte, pode-se usar systemd-nspawn ou firejail
    • Se a segurança realmente for crítica, paga-se mais $5/mês e roda-se em uma VM separada
  • O processo de configurar um novo projeto é curto
    • Criar um novo usuário
    • Adicionar um host virtual no nginx e emitir um certificado HTTPS com o certbot
    • Adicionar uma unit do systemd
    • Fazer commit do script de deploy no repositório e executá-lo
  • No começo há bastante coisa para aprender, mas essa infraestrutura muda muito mais devagar do que infraestrutura em nuvem
    • O formato de configuração do nginx permaneceu essencialmente quase igual nos últimos 10 anos
    • A última grande mudança na administração de sistemas Debian foi a transição para o systemd, quase 10 anos atrás
    • Há menos exposição a situações em que provedores de nuvem descontinuam serviços ou mudam silenciosamente o comportamento
    • A única dependência é o provedor de VPS, e como o servidor é um produto genérico, dá para migrar para outro provedor

1 comentários

 
GN⁺ 2023-07-17
Opiniões do Hacker News
  • Se você executa cada serviço como um usuário Unix separado para isolamento, o recurso DynamicUser do systemd pode economizar tempo
    Ele atribui um UID e cria diretórios de log/estado com as permissões corretas
    https://0pointer.net/blog/dynamic-users-with-systemd.html

    • Se o UID precisa ser persistente, também é muito fácil criar um novo usuário de sistema com systemd-sysusers
      Basta colocar em /etc/sysusers.d/ um pequeno arquivo de texto com informações como nome de usuário e diretório home, e executar o comando ou serviço sysusers
    • Usar uma conta de serviço separada para cada serviço/app é uma prática bastante padrão, e também é melhor separar os servidores
  • Funções de nuvem acionadas por HTTP são o que mais me agrada hoje em dia
    Se você tomar cuidado para evitar as armadilhas específicas de cada fornecedor, elas reduzem muito a complexidade e parecem ser a única abstração cloud native que dá a sensação de “forma final”
    Só em um app já fiz mais de 2000 deploys, e nunca precisei contatar o suporte porque o ambiente de execução quebrou nem digitar comandos obscuros no console
    O desempenho também é excelente em um plano isolado do App Service da Azure, e minha resistência ideológica à cobrança por requisição já desapareceu
    Se você for dono até do imóvel onde ficam os servidores, talvez consiga fazer mais barato, mas para implementar por conta própria atributos como compliance e auditoria que uma função HTTP simples oferece, na prática teria de criar uma empresa gigantesca e contratar um monte de gente
    O argumento de lock-in de fornecedor também já não me convence tanto. A interface que recebe uma requisição HTTP e devolve uma resposta HTTP é natural, e as diferenças entre fornecedores se limitam a coisas como a assinatura do método de trigger ou contextos adicionais como claims OIDC/SAML; então seria preciso se esforçar para criar uma arquitetura que você não conseguisse refatorar para outro fornecedor em uma semana
    Entendo que, para um blog pessoal, comprar uma VM na Hetzner e lapidá-la artesanalmente seja mais divertido. Se for uma indústria sem nenhuma regulação, também dá para defender com mais força uma análise minuciosa de margens e complexidade em vez de terceirizar completamente os servidores

    • Revirei os olhos na parte de “para implementar por conta própria, seria preciso criar uma empresa de 1 bilhão de dólares e contratar mais 1000 pessoas”
      Não sei como acham que todo mundo fazia antes da nuvem
      Hoje lidero várias equipes em uma grande empresa industrial, escalando e implantando apps inovadores na Azure, e os custos de nuvem são absurdamente altos em relação ao número de usuários
      Antes, operávamos apps com 10 vezes mais usuários por 1/100 do custo e com complexidade menor
      Como esse custo é cobrado de outros departamentos que fizeram essas escolhas duvidosas, pessoalmente não me afeta, mas acho difícil entender essa fé cega na nuvem
    • Trabalho em um setor extremamente regulado, mas não entendo a última frase
      Para nós, operar hardware próprio é a melhor opção, e a chance de entregarmos algo a um provedor de nuvem é de 0%, a ponto de nem ser permitido tirar fotos no escritório
    • Funções são um lugar razoável para hospedar APIs simples
      Mas, para um app se tornar útil, ele também precisa de outros componentes, como banco de dados, e não é aí que os custos começam a aparecer?
    • Não sei exatamente o que são essas funções de nuvem acionadas por HTTP de que estão falando aqui
      Fico na dúvida se é um handler de requisições de um servidor HTTP, computação em nuvem como serviço funcional, ou simplesmente um RPC antigo
    • O que importa é o que a função realmente faz
      Se algum script kiddie começar a enviar requisições 10 vezes por minuto, quanto tempo leva até o custo da função ficar mais caro que um VPS?
  • Uso uma configuração parecida para meu site pessoal e sites de projetos
    Uso uma VM da Linode de US$ 5 por mês, Debian GNU/Linux, e o software é escrito em Common Lisp
    Meu site pessoal ou blog é gerado como site estático por um programa em Common Lisp, e serviços online ou web apps são feitos como programas em Common Lisp que processam requisições HTTP e respondem usando Hunchentoot
    Uso arquivos de unidade do systemd para que sites/serviços subam automaticamente ao iniciar ou reiniciar a VM, e a maioria tem cerca de 10 a 15 linhas
    A configuração inicial da VM está codificada em um script de shell: https://github.com/susam/dotfiles/blob/main/linode.sh
    As configurações por projeto e por serviço são gerenciadas com Makefiles: https://github.com/susam/susam.net/blob/main/Makefile, https://github.com/susam/mathb/blob/main/Makefile
    Não uso contêineres. Esses sites já vinham sendo operados havia anos antes de contêineres virarem moda, e até agora scripts de inicialização e Makefiles têm sido suficientes
    Também uso Nginx. Ele serve arquivos estáticos e, quando há um serviço de backend, atua como proxy reverso; além da terminação TLS, oferece benefícios como limitação de taxa de requisições e configuração de lista de permissões de cabeçalhos HTTP para proteger o backend
    Meu pequeno playbook pessoal contém alguns comandos como curl LINK -o linode.sh && sh linode.sh e git clone LINK && cd PROJECT && sudo make setup https
    Os targets do make cuidam do que é necessário para colocar o site no ar, como instalar ferramentas como Nginx, certbot e sbcl, configurar o Nginx e configurar certificados; quando o comando termina, o site já fica público

    • Fico curioso para saber qual implementação de Common Lisp você usa
      Pelo Makefile, parece que você usa SBCL, e queria saber se o uso de memória já foi um problema
      Uso um VPS com 512 MB de RAM, e uma instância do SBCL consome por volta de 100 MB, então só consigo manter alguns serviços rodando ao mesmo tempo
      Já pensei em mover os serviços com menos tráfego para CLISP, mas falta um recurso que uso: aliases locais de pacote
  • Depois de continuar refinando a configuração por alguns anos, hoje me acomodei em rodar tudo em contêineres Docker
    Como orquestrador, uso docker-compose em vez de systemd; como proxy, uso Caddy em vez de nginx
    Assim como no texto original, escrevo scripts de deploy para cada projeto que precisa ser executado, então, no geral, é bem parecido
    Uma das várias vantagens do Docker é que dá para executar software de terceiros com a mesma configuração
    Uso essa configuração há alguns anos e ela é muito boa; como no texto original, é robusta, mas também tem flexibilidade para mudar como eu quiser quando preciso
    A única dor atual é deploy rolling. À medida que o software cresce, alguns segundos de downtime a cada deploy estão virando um problema, e ainda não tenho uma solução simples, mas docker swarm talvez seja o caminho certo

    • Opero de forma parecida usando Caddy
      Para reduzir downtime, dá para tentar health_uri /health e lb_try_duration 30s
      Por exemplo, se configurar algo como reverse_proxy api:8089 { health_uri /health lb_try_duration 30s }, durante o deploy de uma nova versão o Caddy faz buffer das requisições e dá 30 segundos até o novo serviço subir
      Idealmente, depois que a nova versão ficar saudável, o Caddy deveria começar a usá-la e então matar o contêiner antigo
      Dei uma olhada em https://github.com/Wowu/docker-rollout e https://github.com/lucaslorentz/caddy-docker-proxy, mas ainda não consegui priorizar isso
    • Se houver um balanceador de carga como o Caddy na frente dos pods, dá para configurá-lo para segurar as requisições enquanto um novo pod sobe: https://twitter.com/bradleyjkemp/status/1486756361845329927
      Não é perfeito, mas, em vez de o navegador receber um erro de conexão, ele apenas fica carregando por alguns segundos
      https://mrsk.dev/ também usa a mesma técnica
    • Ao criar do zero a stack de software da startup onde trabalho, já empacotei a aplicação com Docker
      Em produção começamos com compose e depois evoluímos para um pipeline de deploy contínuo que atualiza a stack automaticamente
      Conforme a empresa e a base de usuários cresceram, começou a aparecer downtime em reinicializações ou deploys, e sempre que queríamos rodar apps adicionais precisávamos preparar um novo ambiente de deploy
      Eu temia que chegasse o dia em que teríamos de usar Kubernetes, porque já tinha visto essa complexidade de perto e não queria passar a maior parte do dia apaziguando um cluster
      Então fomos para o modo Swarm, e a jornada às vezes é Jekyll e às vezes Hyde
      Há bugs que ninguém parece querer corrigir, partes da especificação do Docker que não foram implementadas mas também não avisam você disso, escolhas de implementação que fazem você arrancar os cabelos, e a sensação de que os funcionários da Docker Inc não conversam entre si ou não conseguem manter o foco até o fim
      Ainda assim, também há lados bonitos. A stack do compose funciona como está e pode crescer nos pontos necessários; quando bem configurada, deploy sem downtime, upgrades, balanceamento de carga e rollback funcionam bem
      O Raft é confiável para manter o cluster, como em outros lugares, e com um pouco de esforço dá para construir uma plataforma self-service flexível, segura e com distribuição automática por uma fração do orçamento de manutenção de K8s
      Mas é preciso se preparar para fazer os scripts de deploy direito. Gastei bastante tempo criando uma ferramenta em Python que converte um arquivo compose válido pela especificação do Docker para a especificação do Swarm, renova e limpa secrets, e expande variáveis de ambiente
      Dependendo do provedor de VPS, também é indispensável configurar corretamente o MTU da rede. Acho que isso deve ter encurtado bastante minha vida
    • Se você já tem um arquivo compose, Swarm é o caminho, mas pessoalmente concluí que não vale tanto a pena
      Pelo menos até que o número de clientes ou usuários cresça o bastante para problemas de escala realmente aparecerem
    • Montei uma configuração parecida, mas não gosto da abordagem de enviar imagens via SSH usando docker save e docker import
      Fico curioso se você opera seu próprio registry
  • No servidor físico, coloco o banco de dados PostgreSQL e o app em um Pod do Podman, rodo tudo no localhost em portas acima de 5000, e o Caddy atua como proxy reverso na 443
    Com um systemd Timer, faço dump de todos os bancos de dados em um diretório todos os dias às 16h55
    Depois, o DejaDup [1] faz backup automático de $HOME para um HDD externo todos os dias às 17h, excluindo arquivos de cache, mas incluindo os dumps do banco de dados
    O SO é Debian com GNOME Core [2], e as regras do firewalld permitem apenas 80, 443 e uma porta SSH customizada
    O SSH é baseado em chaves, e a autenticação por senha fica desativada
    É a forma mais entediante, mas simplesmente funciona bem
    1 - https://flathub.org/apps/org.gnome.DejaDup
    2 - https://packages.debian.org/bookworm/gnome-core

    • Fico curioso por que você roda GNOME em um servidor
    • Fico curioso se há algum motivo para usar systemd timers em vez de tarefas cron
  • Hoje em dia uso Dokku em um único servidor
    É fácil de administrar, os desenvolvedores podem fazer deploy só com git push, no estilo Heroku, e há muitos plugins, então adicionar um banco de dados ou configurar HTTPS leva no máximo 10 segundos

    • Uso a mesma abordagem, e é muito prático poder fazer deploy com git push
      Adicionar apps, adicionar bancos de dados, gerenciar variáveis de ambiente e gerenciar domínios é tudo muito intuitivo
  • Gosto de configurações simples, então esse jeito parece agradável de trabalhar
    Provavelmente é suficiente para 99% dos serviços
    Acho que usaria Ansible para a configuração do servidor e scripts de deploy
    Assim o servidor fica documentado e os scripts de deploy também podem ficar mais simples
    Porém, eu não evitaria acessar o servidor diretamente para depuração ou verificação

    • Eu uso Ansible, mas estou repensando se ele realmente vale a pena
      Meus scripts quase não mudaram nos últimos 7 anos, enquanto o Ansible é lento e tende a exigir a manutenção contínua de vários arquivos
      Quanto à depuração, também daria para expor os logs da aplicação via nginx em um endpoint protegido por senha
  • Às vezes parece que esquecemos que, mesmo antes da nuvem, CI/CD e uma gestão eficaz de servidores já eram práticas comuns

  • Sobre escrever software de servidor em Rust, fazer link estático e compilar HTML, CSS, configurações, segredos etc. tudo dentro do binário: tenho feito isso recentemente em Go, e gosto muito porque escrever e distribuir software que depende de arquivos estáticos fica realmente fácil

    • Ainda acho questionável incluir segredos no binário compilado
      Para segredos, usar variáveis de ambiente ou configuração é quase o padrão e, embora adicione uma etapa antes da execução, evita a situação de compartilhar o binário e esquecer que havia segredos dentro dele
      É bem fácil desempacotar o binário e procurar strings
      Incluir assets estáticos de frontend e configurações padrão junto é uma grande vantagem
    • Não gosto de Go, mas continuo usando por esse motivo
      Em Rust, especialmente para compilação cruzada de Mac para Linux, é relativamente doloroso, enquanto em Go é trivial e vem embutido na ferramenta go
      Fica muito fácil eliminar atrito ao finalizar e publicar um projeto paralelo
    • https://play.clickhouse.com/play?user=play também faz do mesmo jeito
      Mas tenho uma pergunta. No texto, ele diz que “os certificados da Let’s Encrypt são obtidos com certbot e a renovação automática também é tratada”, mas eu uso uma configuração multirregião com dois servidores e geo-DNS, então o certbot só roda no servidor dos EUA e preciso copiar manualmente o certificado para o servidor da Europa
      Existe alguma forma de resolver isso?
      A explicação do ClickHouse Playground está aqui: https://ghe.clickhouse.tech/
    • Eu também faço do mesmo jeito
      Sirvo a aplicação web a partir do mesmo serviço compilado estaticamente que fornece a API de backend
      No CI, rodo npm build e embuto o resultado, o que torna muito fácil fazer testes locais ou subir uma instância de demonstração