Implantação de Laravel Zero Downtime
Publicados: 2022-03-11Quando se trata de atualizar um aplicativo ativo, existem duas maneiras fundamentalmente diferentes de fazer isso.
Na primeira abordagem, fazemos alterações incrementais no estado do nosso sistema. Por exemplo, atualizamos arquivos, modificamos propriedades do ambiente, instalamos necessidades adicionais e assim por diante. Na segunda abordagem, desmontamos máquinas inteiras e reconstruímos o sistema com novas imagens e configurações declarativas (por exemplo, usando Kubernetes).
Implantação do Laravel facilitada
Este artigo abrange principalmente aplicativos relativamente pequenos, que podem não ser hospedados na nuvem, embora eu mencione como o Kubernetes pode nos ajudar muito com implantações além do cenário "sem nuvem". Também discutiremos alguns problemas gerais e dicas para realizar atualizações bem-sucedidas que podem ser aplicáveis em várias situações diferentes, não apenas com a implantação do Laravel.
Para os propósitos desta demonstração, usarei um exemplo do Laravel, mas lembre-se de que qualquer aplicativo PHP pode usar uma abordagem semelhante.
Controle de versão
Para começar, é crucial que saibamos a versão do código atualmente implantada na produção. Pode ser incluído em algum arquivo ou pelo menos no nome de uma pasta ou arquivo. Quanto à nomenclatura, se seguirmos a prática padrão de versionamento semântico, podemos incluir mais informações nele do que apenas um único número.
Observando duas versões diferentes, essas informações adicionais podem nos ajudar a entender facilmente a natureza das alterações introduzidas entre elas.
O controle de versão do lançamento começa com um sistema de controle de versão, como o Git. Digamos que preparamos uma versão para implantação, por exemplo, a versão 1.0.3. Quando se trata de organizar essas versões e fluxos de código, existem diferentes estilos de desenvolvimento, como desenvolvimento baseado em tronco e fluxo Git, que você pode escolher ou misturar com base nas preferências de sua equipe e nas especificidades do seu projeto. No final, provavelmente terminaremos com nossos lançamentos marcados de forma correspondente em nosso branch principal.
Após o commit, podemos criar uma tag simples como esta:
git tag v1.0.3
E então incluímos tags ao executar o comando push:
git push <origin> <branch> --tags
Também podemos adicionar tags a commits antigos usando seus hashes.
Obtendo Arquivos de Liberação para Seu Destino
A implantação do Laravel leva tempo, mesmo que seja simplesmente copiar arquivos. No entanto, mesmo que não demore muito, nosso objetivo é atingir zero tempo de inatividade .
Portanto, devemos evitar instalar a atualização no local e não devemos alterar os arquivos que estão sendo veiculados ao vivo. Em vez disso, devemos implantar em outro diretório e fazer a troca apenas quando a instalação estiver completamente pronta.
Na verdade, existem várias ferramentas e serviços que podem nos ajudar com implantações, como Envoyer.io (do designer do Laravel.com Jack McDade), Capistrano, Deployer, etc. Ainda não usei todos eles em produção, então não posso fazer recomendações ou escrever uma comparação abrangente, mas deixe-me mostrar a ideia por trás desses produtos. Se alguns (ou todos) não puderem atender aos seus requisitos, você sempre poderá criar seus scripts personalizados para automatizar o processo da melhor maneira que achar melhor.
Para os propósitos desta demonstração, digamos que nosso aplicativo Laravel seja servido por um servidor Nginx do seguinte caminho:
/var/www/demo/public
Primeiro, precisamos de um diretório para colocar os arquivos de lançamento sempre que fizermos uma implantação. Além disso, precisamos de um link simbólico que aponte para a versão de trabalho atual. Nesse caso, /var/www/demo
servirá como nosso link simbólico. Reatribuir o ponteiro nos permitirá alterar rapidamente as versões.
Caso estejamos lidando com um servidor Apache, podemos precisar permitir os seguintes links simbólicos na configuração:
Options +FollowSymLinks
Nossa estrutura pode ser algo assim:
/opt/demo/release/v0.1.0 /opt/demo/release/v0.1.1 /opt/demo/release/v0.1.2
Pode haver alguns arquivos que precisamos persistir em diferentes implementações, por exemplo, arquivos de log (se não estivermos usando o Logstash, obviamente). No caso da implantação do Laravel, podemos querer manter o diretório de armazenamento e o arquivo de configuração .env. Podemos mantê-los separados de outros arquivos e usar seus links simbólicos.
Para buscar nossos arquivos de lançamento do repositório Git, podemos usar comandos clone ou archive. Algumas pessoas usam git clone, mas você não pode clonar um determinado commit ou tag. Isso significa que todo o repositório é buscado e, em seguida, a tag específica é selecionada. Quando um repositório contém muitas ramificações ou um grande histórico, seu tamanho é consideravelmente maior que o arquivo de lançamento. Portanto, se você não precisar especificamente do repositório git na produção, poderá usar git archive
. Isso nos permite buscar apenas um arquivo de arquivo por uma tag específica. Outra vantagem de usar este último é que podemos ignorar alguns arquivos e pastas que não deveriam estar presentes no ambiente de produção, por exemplo, testes. Para isso, basta definir a propriedade export-ignore no .gitattributes file
. Na lista de verificação de práticas de codificação segura do OWASP, você pode encontrar a seguinte recomendação: “Remova o código de teste ou qualquer funcionalidade não destinada à produção, antes da implantação”.
Se estivermos buscando a versão do sistema de controle de versão do código-fonte, git archive e export-ignore podem nos ajudar com esse requisito.
Vamos dar uma olhada em um script simplificado (ele precisaria de um melhor tratamento de erros na produção):
deploy.sh
#!/bin/bash # Terminate execution if any command fails set -e # Get tag from a script argument TAG=$1 GIT_REMOTE_URL='here should be a remote url of the repo' BASE_DIR=/opt/demo # Create folder structure for releases if necessary RELEASE_DIR=$BASE_DIR/releases/$TAG mkdir -p $RELEASE_DIR mkdir -p $BASE_DIR/storage cd $RELEASE_DIR # Fetch the release files from git as a tar archive and unzip git archive \ --remote=$GIT_REMOTE_URL \ --format=tar \ $TAG \ | tar xf - # Install laravel dependencies with composer composer install -o --no-interaction --no-dev # Create symlinks to `storage` and `.env` ln -sf $BASE_DIR/.env ./ rm -rf storage && ln -sf $BASE_DIR/storage ./ # Run database migrations php artisan migrate --no-interaction --force # Run optimization commands for laravel php artisan optimize php artisan cache:clear php artisan route:cache php artisan view:clear php artisan config:cache # Remove existing directory or symlink for the release and create a new one. NGINX_DIR=/var/www/public mkdir -p $NGINX_DIR rm -f $NGINX_DIR/demo ln -sf $RELEASE_DIR $NGINX_DIR/demo
Para implantar nosso lançamento, poderíamos apenas executar o seguinte:

deploy.sh v1.0.3
Nota: Neste exemplo, v1.0.3 é a tag git de nossa versão.
Compositor em Produção?
Você deve ter notado que o script está invocando o Composer para instalar dependências. Embora você veja isso em muitos artigos, pode haver alguns problemas com essa abordagem. Geralmente, é uma prática recomendada criar uma compilação completa de um aplicativo e avançar essa compilação por meio de vários ambientes de teste de sua infraestrutura. No final, você teria uma compilação completamente testada, que pode ser implantada com segurança na produção. Mesmo que cada compilação deva ser reproduzível do zero, isso não significa que devemos reconstruir o aplicativo em diferentes estágios. Quando fazemos a instalação do compositor em produção, esta não é genuinamente a mesma compilação que a testada e aqui está o que pode dar errado:
- Erro de rede pode interromper o download de dependências.
- O fornecedor da biblioteca pode nem sempre seguir o SemVer.
Um erro de rede pode ser facilmente notado. Nosso script até pararia de ser executado com um erro. Mas uma alteração importante em uma biblioteca pode ser muito difícil de definir sem executar testes, o que você não pode fazer em produção. Ao instalar dependências, o Composer, o npm e outras ferramentas semelhantes dependem do versionamento semântico – major.minor.patch. Se você vir ~1.0.2 no composer.json, significa instalar a versão 1.0.2 ou a versão de patch mais recente, como 1.0.4. Se você vir ^1.0.2, significa instalar a versão 1.0.2 ou a versão secundária ou patch mais recente, como 1.1.0. Confiamos no fornecedor da biblioteca para aumentar o número principal quando qualquer alteração importante for introduzida, mas às vezes esse requisito é perdido ou não é seguido. Já houve casos assim no passado. Mesmo se você colocar versões fixas em seu composer.json, suas dependências podem ter ~ e ^ em seu composer.json.
Se for acessível, na minha opinião, uma maneira melhor seria usar um repositório de artefatos (Nexus, JFrog, etc.). A compilação de lançamento, contendo todas as dependências necessárias, seria criada uma vez, inicialmente. Esse artefato seria armazenado em um repositório e buscado para vários estágios de teste de lá. Além disso, essa seria a compilação a ser implantada na produção, em vez de reconstruir o aplicativo do Git.
Mantendo o código e o banco de dados compatíveis
A razão pela qual me apaixonei pelo Laravel à primeira vista foi como seu autor prestou muita atenção aos detalhes, pensou na conveniência dos desenvolvedores e também incorporou muitas práticas recomendadas ao framework, como migrações de banco de dados.
As migrações de banco de dados nos permitem ter nosso banco de dados e código em sincronia. Ambas as alterações podem ser incluídas em um único commit, portanto, em uma única versão. No entanto, isso não significa que qualquer alteração possa ser implantada sem tempo de inatividade. Em algum momento durante a implantação, haverá diferentes versões do aplicativo e do banco de dados em execução. Em caso de problemas, esse ponto pode até se transformar em um ponto final. Devemos sempre tentar torná-los compatíveis com as versões anteriores de seus companheiros: banco de dados antigo – novo aplicativo, novo banco de dados – aplicativo antigo.
Por exemplo, digamos que temos uma coluna de address
e precisamos dividi-la em address1
e address2
. Para manter tudo compatível, podemos precisar de vários lançamentos.
- Adicione duas novas colunas no banco de dados.
- Modifique o aplicativo para usar novos campos sempre que possível.
- Migre os dados de
address
para novas colunas e elimine-os.
Este caso também é um bom exemplo de como pequenas mudanças são muito melhores para implantação. Sua reversão também é mais fácil. Se estivermos alterando a base de código e o banco de dados por várias semanas ou meses, pode ser impossível atualizar o sistema de produção sem tempo de inatividade.
Algumas maravilhas do Kubernetes
Mesmo que a escala do nosso aplicativo não precise de nuvens, nós e Kubernetes, ainda gostaria de mencionar como são as implantações no K8s. Nesse caso, não fazemos alterações no sistema, mas declaramos o que gostaríamos de alcançar e o que deve ser executado em quantas réplicas. Em seguida, o Kubernetes garante que o estado real corresponda ao desejado.
Sempre que temos uma nova versão pronta, criamos uma imagem com novos arquivos, marcamos a imagem com a nova versão e a passamos para o K8s. O último irá rapidamente girar nossa imagem dentro de um cluster. Ele aguardará antes que o aplicativo esteja pronto com base na verificação de prontidão que fornecemos e, em seguida, redirecionará imperceptivelmente o tráfego para o novo aplicativo e eliminará o antigo. Podemos facilmente ter várias versões do nosso aplicativo em execução, o que nos permitiria realizar implantações azul/verde ou canário com apenas alguns comandos.
Se você estiver interessado, há algumas demonstrações impressionantes na palestra “9 Steps to Awesome with Kubernetes by Burr Sutter”.