Como eu opero meus servidores (2022)
(blog.wesleyac.com)- 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
- Com linkagem estática
- HTML, CSS, configurações e segredos são compilados dentro do binário
- Usa rust-musl-builder e rust-embed
- 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
.backupe 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
scpoursync
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
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
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çosysusersFunçõ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
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
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
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?
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
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.shegit clone LINK && cd PROJECT && sudo make setup httpsOs targets do
makecuidam 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úblicoPelo 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
Para reduzir downtime, dá para tentar
health_uri /healthelb_try_duration 30sPor 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 subirIdealmente, 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
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
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
Pelo menos até que o número de clientes ou usuários cresça o bastante para problemas de escala realmente aparecerem
docker saveedocker importFico 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
$HOMEpara um HDD externo todos os dias às 17h, excluindo arquivos de cache, mas incluindo os dumps do banco de dadosO 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
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 segundosgit pushAdicionar 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
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
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
Em Rust, especialmente para compilação cruzada de Mac para Linux, é relativamente doloroso, enquanto em Go é trivial e vem embutido na ferramenta
goFica muito fácil eliminar atrito ao finalizar e publicar um projeto paralelo
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/
Sirvo a aplicação web a partir do mesmo serviço compilado estaticamente que fornece a API de backend
No CI, rodo
npm builde embuto o resultado, o que torna muito fácil fazer testes locais ou subir uma instância de demonstração