Eu ainda não trabalho, logo, não tenho um salário fixo, logo, não posso me comprometer a pagar um valor que eu nem sei qual é no final do mês correndo o risco de dever centenas porque tomei algum ataque ou recebi muitos acessos e a conta veio lá em cima. Quem iria querer atacar qualquer site meu, um total desconhecido? Não sei, mas tenho essa paranoia.
Por essa razão, evito hosts que cobram por uso. Outra razão é que são serverless. Não espero ter tantos acessos no meu projeto, então, isso implica que, quando finalmente houver um acesso, ele vai ser extremamente lento porque vai incluir o cold start da aplicação.
Juntando tudo isso, acabei optando por não utilizar nenhuma dessas hospedagens serverless ou muito automágicas. Encontrei uma empresa que oferece serviços de hospedagens extremamente interessante: a Square Cloud.
Eles são 100% brasileiros e a precificação é, surpreendentemente, pagável , isso é, acessível para um mero e mortal brasileiro. Eles cobram por mensalidade, não exigem cartão de crédito: aceitam pagamento via PIX mensal, semestral ou anualmente.
Problemática
É incrível, mas tem um probleminha: o processo de deploy não é mágico e entregue 100% feito e integrado com o seu sistema. Eles não estão por trás de um framework, muito pelo contrário: aceitam várias linguagens, mas uma por projeto. Isso implica que não se encarregam de descobrir como seu sistema precisa ser tratado pra ir ao ar.
O jeito que você cria uma nova aplicação neles é colocando o que é necessário pro sistema rodar dentro de um arquivo zip, configurando coisas super básicas (quantidade de memória RAM e número de vCPUs disponíveis para essa aplicação) e fim. Para atualizar, lembra muito as hosts compartilhadas. Você tem acesso à um sistema de gerenciamento de arquivos.
Eles, na sua documentação, ensinam como fazer uma integração super simplificada com o GitHub. Por meio de um WebHook, à todo push (ou release ou o gatilho que você quiser), eles puxam o código do seu repositório e rodam o comando START (definido na configuração da sua aplicação ou no arquivo squarecloud.app
).
Sabendo disso, já é possível inferir que é inteligente colocar, no comando START, múltiplos comandos que “buildam” seu sistema novamente e instalam suas dependências, e só então, rodam sua aplicação. Num sisteminha utilizando Node.js, o comando START seria algo como npm i ; npm run build ; npm run start
.
“Solução” vs. Problema
A solução supracitada cria um novo problema. Agora, você trouxe, para dentro do seu container na host, a obrigação de compilar/”buildar” seu sistema. Pode parecer banal, mas você já pode se preparar para encontrar crashes ocasionados por falta de memória ou até por falta de vCPU.
Compilar qualquer aplicação Rust é garantia de utilizar, no mínimo, toda a CPU disponível e 1gb de RAM com muita facilidade. Totalmente inverso é o cenário dessa aplicação rodando: pouquíssima memória consumida e quase nada de utilização da CPU.
Ainda sim, você precisou dedicar 1 vCPU inteira e 1gb de RAM para esse sistema só pra ele poder compilar.
A verdadeira solução
Por meses, me contentei com esse cenário e me conformei em ter que, manualmente, reiniciar a aplicação quando ela dava pau porque faltou memória e sequer pôde terminar de compilar. Mas, ontem, dia 21/12/2024, tive um pico de energia e tentei fazer algo que queria ter feito há tempos: tirar a etapa de build fora da host.
O GitHub possui muitos recursos interessantes (vários dos quais eu nem explorei, tampouco conheço, mas imagino que existam). Um deles são os GitHub Actions (vulgo workflows). A verdade é que, na documentação da Square, eles ensinam a integração mais básica (mencionada no tópico anterior) e a utilizar sua CLI, mas não é mencionado o seu GitHub Action — se foi, no mínimo deve estar escondido, pois não encontrei.
Olhando o GitHub da SquareCloud, encontrei um repositório que contém um GitHub Action que instala a CLI deles e permite utilizá-la no workflow. Com ela, você pode controlar qualquer aplicação que esteja na sua conta da Square.
Sabendo disso, fiz um repositório com uma aplicação super bobinha: um sisteminha web feito em Rust que só retorna/renderiza algumas mensagens de texto ou HTMLs com mensagens tipo “Hello World”. Esse repositório, na verdade, contém uma prova de conceito (PoC): queria tentar automatizar o processo do deploy e mandar apenas o necessário para a host.
A prova de conceito
O que é “buildar”? Depende. Num sistema front-end, é gerar um bundle: transpilar para JavaScript, se você estiver usando TypeScript; minificar seus arquivos JavaScript (cortar espaços em branco, renomear variáveis para economizar bits, etc). Se for um back-end em uma linguagem como Java ou C#, significa compilar para a linguagem (intermediária) de máquina (virtual). Se for uma linguagem compilada, significa compilar para um executável nativo. De qualquer forma, é um processo consideravelmente demorado (principalmente quando estamos falando de Rust).
Isso tudo vai gerar o executável ou os arquivos necessários para o sistema web (assets). Mas o que fazer com isso? É só jogar o executável e ele vai rodar? Vamos experimentar:
Vamos chamar minha aplicação de SED — Sistema Em Discussão. O SED é uma aplicação web em Rust e Actix-Web. Não é uma API! Utilizando Inertia, ela renderiza páginas com React com os dados enviados pelo servidor:
#[get("/")]
async fn index(req: HttpRequest) -> impl Responder {
let mut props = HashMap::new();
props.insert("message".into(), InertiaProp::Data("Hello World!".into()));
render_with_props::<Vite>(&req, "Index".into(), props).await
}
Será que basta compilar e enviar o executável para a host? Vamos testar localmente, compilando, movendo o executável para a área de trabalho e tentando executá-lo (considere que estou utilizando WSL2):
~/projetos/sed$ cargo build --release
~/projetos/sed$ mv ./target/release/sed ~/
~/projetos/sed$ cd ~/
~/$ ./sed #executando o sistema
thread 'main' panicked at src/main.rs:29:21:
Failed to open manifest at public/bundle/manifest.json: No such file or directory (os error 2)
note: run with RUST_BACKTRACE=1 environment variable to display a backtrace
O erro nos diz que ele falhou em encontrar o arquivo public/bundle/manifest.json
. O que é isso? O Rust não devia gerar um executável independente? De fato, o executável é independente. Mas o programa que ele contém depende de outros arquivos para funcionar.
Como vimos na rota índice, ela renderiza uma página React, mais tecnicamente, um componente chamado Index localizado (durante o desenvolvimento) em ./www/pages/Index.tsx
:
export default function Index({ message }: { message: string }) {
return (
<main>
<h1>{message}</h1>
</main>
)
}
Isso quer dizer que o sistema precisa ter acesso à esse e outros componentes JavaScript. Como estou utilizando Inertia e Vite, é necessário:
ter o servidor de desenvolvimento do Vite para servir esses assets (o componente Index, de fato, é um asset, bem como css e imagens);
ter o arquivo
manifest.json
, um mapa para encontrar o arquivo transpilado e minificado doIndex.tsx
.
O arquivo de manifesto é gerado pelo Vite durante o processo de build. Dessa forma, precisaremos não só compilar nosso sistema Rust, como também gerar o bundle para poder iniciar nossa aplicação em produção.
O arquivo de configuração do Vite colocará todos os assets no diretório public/bundle/assets
, enquanto o manifesto será armazenado em public/bundle/manifest.json
. Logo, tudo isso precisa estar disponível no mesmo diretório que o nosso executável.
Além disso, também é gerado um diretório dist/
contendo arquivos úteis para iniciar o servidor do Inertia, mas, não utilizaremos server-side rendering nesse simples evento e, portanto, ele será desprezado.
Até aqui, já identificamos que nosso sed tem um processo de build mais complicado, precisando:
compilar o sistema Rust (
cargo build --release
);“buildar” o front-end, nesse caso, com Vite (
npm run build
);
Vamos, então, gerar o bundle e movê-lo também para a nossa área de trabalho, e, então, tentar rodar novamente:
~/projetos/sed$ npm run build
~/projetos/sed$ mv ./public ~/
~/projetos/sed$ cd ~/
~/$ ./sed
Starting the server at 0.0.0.0:3000.
O servidor conseguiu iniciar. Por que não acessamos a nossa rota índice? Bem, fazendo isso, obtemos o seguinte erro no navegador:
Failed to open root layout at www/root.html: No such file or directory (os error 2)
Acontece que o Inertia precisa de um template raiz que contém os scripts necessários, bem como o elemento container. No nosso caso, utilizando inertia-rust, esse arquivo está em www/root.html
. Aparentemente, compilar não significa que não tem erros, hein? Vamos, novamente, copiar esse arquivo para nossa área de trabalho. Note que ele espera encontrá-lo exatamente no caminho ./www/root.html
, então não podemos só copiar o arquivo .html
, mas também o diretório www
.
~/projetos/sed$ mkdir ~/www && cp www/root.html ~/www
~/projetos/sed$ cd ~/
~/ ./sed
Starting the server at 0.0.0.0:3000.
Agora, sim. Acessando nosso índice, poderemos visualizar nossa sofisticada e elegante página:
Empacotando o sistema
Podemos concluir que o SED precisa dos seguintes artefatos ao seu dispor para funcionar corretamente:
o executável, duh;
o diretório
public/
e tudo que estiver dentro dele;nosso template
www/root.html
;todas as dependências do Node (diferentemente do Rust, elas não são achatadas num arquivo binário e ainda precisam ser baixadas na host), portanto, precisamos dos arquivos
package.json
epackage-lock.json
para instalá-las;qualquer outro arquivo do qual o sistema dependa.
Você concorda comigo que a host não precisa dos seus arquivos .tsx
ou qualquer outro a não ser os itens supracitados? Podemos, então, enviar os arquivos necessários
A integração padrão da Square, como já discutimos, baixará o repositório inteiro e fará o processo de build dentro do container na host. Já falamos sobre isso. Agora que sabemos exatamente o que eles precisam para botar o SED no ar, não seria ótimo se pudéssemos gerá-los e enviá-los para a host, deixando-a responsável somente por executar o arquivo ./sed
?
Podemos enxergar isso como um algoritmo (literalmente uma sequência de passos) que constrói nosso sistema e embala tudo num pacotinho. Podemos representá-lo por um script bash! Veja:
# ./deploy.sh
# gerando o build
cargo build --release # gera o executável em ./target/release/sed, note que sed é o arquivo executável
npm install # instala as dependências, incluindo o Vite, para que possamos buildar
npm run build # roda os comandos "vite build && vite build --ssr", descrito no package.json
# montando nosso pacote com o que é essencial!!!
mkdir -p package/www # cria ./package e ./_package/www de uma vez. Vamos colocar tudo aqui dentro!
cp target/release/sed package/ # copia (ou move, se preferir)
cp -a public package # copiamos o diretório public inteiramente pra package com a flag -a (note que ele estará disponível como /package/public)
cp www/root.html package/www/
cp package.json package/
cp package-lock.json package/
echo "console.log('foo')" > package/main.ts
Você deve ter notado uma linha que não foi discutida até o momento. Por que criar um arquivo main.ts
? A verdade é que a Square Cloud utiliza o arquivo especificado no campo MAIN do seu arquivo de configuração para descobrir qual linguagem precisa instalar no seu container para rodar a aplicação.
No nosso caso, não precisaremos mais do Rust à essa altura — ele terá feito tudo que tinha que fazer nas Actions do GitHub —, porém, ainda precisaremos do Node.js para instalar as dependências com o npm. Na verdade, se você não especificar nenhuma linguagem, eu chuto que eles não chegam nem a executar o comando START — o console fica desabilitado no painel da aplicação, no site da Square.
Para resolver isso, criamos esse arquivo TypeScript (com código TypeScript dentro dele, isso é necessário ou a Square também não considera o arquivo) para que o container instale o Node.js.
Utilizando a CLI mencionada mais cedo, podemos já enviar todos esses arquivos direto pelo terminal. Logo, podemos adicionar isso ao nosso script:
# ./deploy.sh
# gerando o build
...
# montando nosso pacote com o que é essencial!!!
...
# enviando pra square cloud
cd package # muda para o diretório package para enviar somente ele, e não o resto do repositório
squarecloud login --token="$token_login"
squarecloud commit "$application_id"
squarecloud app restart "$application_id"
O comando restart
poderia ser substituído pela flag --restart
no comando commit
, teoricamente. Na prática, não funcionou pra mim. Não imagino o motivo, então apenas fiz os comandos separadamente. Uma linha a mais, tanto fez, tanto faz, afinal.
GitHub Actions
Rodando o script deploy.sh
, a gente constrói o SED e envia o conteúdo necessário para rodar a aplicação diretamente pra host — é o que o pessoal da Square chama de commit. Não seria, ainda mais incrível, poder fazer isso automaticamente quando enviamos código novo pra branch main?
A verdade é que realizar tarefas dado algum gatilho num repositório é o trabalho dos workflows/GitHub Actions. Com um arquivo .yaml
no diretório .github/workflows
, podemos definir um workflow que será realizado toda vez que a branch main receber um novo commit. O conteúdo desse workflow é, literalmente, o que a gente acabou de montar no nosso script de bash.
A diferença é que vamos separar os passos e, antes de tudo, vamos ter que usar algumas outras actions para preparar o container — em que o GitHub estará rodando o workflow — para realizar os comandos do script:
actions/checkout@v4
: presente em, praticamente, qualquer workflow, essa action apenas faz um “git clone” do seu repositório pra dentro do container do workflow;actions/setup-node@v4
: instala e configura o Node.js;squarecloudofc/github-action@v2
: vamos utilizar essa action somente para instalar a CLI da Square Cloud no nosso workflow.
Além dessas actions, vamos realizar alguns steps (passos) adicionais para configurar, por exemplo, o próprio Rust e sua toolchain. Fora isso, o resto são os exatos mesmos comandos que a gente já descreveu no nosso script! Confira:
name: Continuous Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Faz o checkout do repositório
uses: actions/checkout@v4
- name: Configura o Rust
run: |
rustup update stable
rustup default stable
- name: Instala o Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Compila o nosso sistema (SED)
run: cargo build --release
- name: Faz o build do frontend
run: |
npm install
npm run build
- name: Monta o pacote com os arquivos importantes
run: |
mkdir -p package/www
cp target/release/sed package/
cp -a public package
cp www/root.html package/www/
cp package.json package/
cp package-lock.json package/
echo "console.log('foo')" > package/main.ts
- name: Instala a CLI da Square Cloud
uses: squarecloudofc/github-action@v2
with:
token: {{ secrets.TOKENLOGIN }}
squarecloud commit ${{ secrets.APPLICATION_ID }}
squarecloud app restart ${{ secrets.APPLICATION_ID }}
Permissão negada ?!
Provavelmente, ao copiar o arquivo, as permissões sejam perdidas. Talvez, isso aconteça no processo de commit (da Square, não do GitHub). O fato é que a Square Cloud se recusaria a executar o executável do nosso sistema.
Em suma, o arquivo de configuração da Square Cloud para o SED é algo mais ou menos assim:
MAIN=./main.ts
START=npm install ; chmod u+x ./sed ; ./sed
DISPLAY_NAME=SED
DESCRIPTION=Sistema Em Observação
VERSION=recommended
SUBDOMAIN=algum-subdominio-bem-interessante
MEMORY=512
AUTORESTART=true
A nova diretriz deles define que, para websites, o mínimo de memória RAM que podemos utilizar é 512.
Observações
Observe que utilizamos o mesmo token de login e o id da aplicação que utilizamos no script. Nesse caso, você precisará adicionar esses segredos nas configurações do seu repositório. Claro que você jamais deveria colocar essas informações “hard coded” no seu workflow, a menos que queira ter sua conta invadida ou algo do tipo.
Esse workflow rodará sempre que houver um novo commit, conforme já conversamos. Você pode ir além e garantir que ele só seja executado se algum workflow de testes garantir que todos os testes da sua aplicação tenham passado ou alguma coisa do tipo.
Eu não sou a pessoa mais experiente nesses workflows nem em bash — muito pelo contrário, sei próximo de zero —, porém, tenho a capacidade de pesquisar e encontrar o que procuro. Então, em questão de 2 dias pesquisando na documentação do próprio GitHub e em alguns outros blogposts soltos pela rede, consegui montar esse script e, depois, o workflow.
É a melhor solução? Não faço ideia. Ela funciona. Talvez, alguns passos sejam desnecessários. Ainda tem a desvantagem de ter que mexer diretamente nos scripts dentro do workflow caso eu mude os diretórios da aplicação ou haja algum novo arquivo a ser enviado também.
Mesmo assim, a parte do build fica totalmente dentro do GitHub e, à host, só resta executar a aplicação. De fato, é tudo o que ela devia fazer.
Todo o código dessa aplicação — das rotas aos arquivos de workflows — estão disponíveis no repositório deploy-experiments, no meu GitHub, então, você pode ver meus fracassos rumo ao acerto, se quiser.