Neste artigo, vamos aprender o que significa empurrar uma aplicação para produção e como fazer isso automaticamente, veremos docker e GitHub action para isso. Como estamos usando docker, não vou gastar muito tempo com quais tecnologias foram usadas para escrever a aplicação, mas sim com o que fazer com a imagem docker em si. A premissa aqui significa um servidor simples, seja um simples VPS (Virtual Private Server), um servidor dedicado ou qualquer servidor onde você tenha SSH para usá-lo. Precisamos realizar ações, preparar o servidor para receber e executar o app, e configurar o pipeline de implantação. Para a próxima parte deste artigo, considerarei que você tem um servidor Ubuntu
Vamos preparar o servidor
A primeira coisa a fazer é realizar algumas configurações de um único uso no servidor, o objetivo é prepará-lo para as próximas etapas. Para isso, temos que considerar os seguintes tópicos:
Docker Compose para executar a aplicação e suas dependências
Docker Compose é uma ferramenta que executa muitas aplicações Docker como serviços e permite que elas se comuniquem entre si, além de outras funcionalidades como volumes para armazenamento de arquivos. Vamos primeiramente instalar o Docker e o Docker Compose no servidor; você pode seguir o guia de instalação oficial aqui: docs.docker.com/engine/install/ubuntu. Após isso, é importante executá-lo como um usuário não-root; aqui está o que diz a documentação oficial: docs.docker.com/engine/security/rootless.
Configure credenciais do AWS ECR & CLI.
Nota-se que você pode alcançar o mesmo objetivo com outro repositório, se desejar, estou usando AWS aqui porque é mais simples para mim.
Since estamos usando Docker, as imagens precisarão ser armazenadas e recuperadas de algum lugar. Para esse propósito, estou usando AWS ECR (Amazon Web Services Elastic Container Registry). É um repositório Docker dentro de uma conta AWS. É muito barato de usar e fácil de configurar. Você também pode usar o Docker Hub para criar um repositório privado para suas imagens. Tudo começa criando um repositório ECR privado na conta AWS. Você clicará em “Criar Repositório” e preencherá o nome do repositório.
Após criar um repositório, você pode copiar o URI do repositório e guardá-lo para uso futuro. Ele tem o seguinte formato ID_DA_CONTA_AWS.dkr.ecr.REGIAO_DA_AWS.amazonaws.com/
NOME_DO_REPOSITÓRIO
.
Você também precisará configurar credenciais AWS IAM que tenham permissão para puxar/push para/from este repositório. Vamos para o serviço IAM, clique em novo usuário e anexe a seguinte política a ele: AmazonEC2ContainerRegistryFullAccess
, você não precisa habilitar o acesso ao console AWS para ele. No final deste processo, você recebe 2 chaves da AWS, uma chave secreta
e um identificador da chave secreta
, guarde-as, pois precisaremos delas para o trabalho subsequente.
De volta no nosso servidor, precisamos instalar o AWS CLI. A maneira oficial de instalá-lo está disponível aqui. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. Após isso, você pode testar a instalação executando o comando aws --version
. Nesta etapa, você terá que executar o comando aws configure
e responder às perguntas fornecendo as chaves geradas anteriormente na aws, a chave secreta
e o id da chave secreta
. Também será solicitado que você escolha um formato de saída, simplesmente JSON, e forneça uma região padrão, é melhor escolher a região onde você criou o registro ECR anteriormente.
Configurar o script do executor da aplicação.
Em meu fluxo de trabalho, escrevi um pequeno script de shell que realiza certas ações, é a parte chave deste processo, ele faz login no registro, baixa a imagem e reinicia o serviço docker correspondente, eu simplesmente o chamo de redeploy.sh
e salvo em uma pasta de onde quero executar meu app, aqui está o conteúdo:
#!/bin/bash
# Retrieve AWS ECR login command
aws ecr get-login-password --region [SWS_REGION] | docker login --username AWS --password-stdin [AWS_REGION].dkr.ecr.us-west-2.amazonaws.com
# Associating repositories with identifiers
declare -A repositories=(
["web"]="[REGISTRY_NAME]:latest"
)
# Check if service identifier is provided as a command line argument
if [ -z "$1" ]; then
echo "Please provide a service identifier as a command line argument."
exit 1
fi
service_identifier=$1
# Check if the provided service identifier exists in the repositories array
if [ -z "${repositories[$service_identifier]}" ]; then
echo "Invalid service identifier. Available identifiers: ${!repositories[@]}"
exit 1
fi
# pull the new image from the registry
repository=${repositories[$service_identifier]}
echo "Pulling [AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
docker pull "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
# Change directory to [APP_FOLDER]
cd /home/ubuntu/[APP_FOLDER] || {
echo "Failed to change directory to /home/ubuntu/[APP_FOLDER]"
exit 1
}
# stop and restart the service, this wil force docker compose to redownload the lates image
echo "Re-running service $service_identifier"
docker compose stop "$service_identifier"
docker compose up --no-deps "$service_identifier" -d
# Remove old and un-used docker images
echo "Removing unused Docker images"
docker image prune -fa
echo "Removed Dangling Images"
A primeira etapa deste script consiste em fazer login na conta AWS com o AWS CLI para obter um token que o docker usará ao recuperar a imagem docker, lembre-se de que o registro é privado, não podemos simplesmente fazer pull sem estar autenticados.
Então declaramos uma lista de repositórios e associamos a eles algum identificador, o identificador especificado será usado como argumento de linha de comando, falaremos mais sobre isso mais tarde. Após isso, verificamos se o usuário forneceu um argumento que corresponda a um identificador de serviço existente, queremos que ele digite algo como ./redeploy web
, por exemplo, o script associará o argumento web
ao repositório web
, como no segundo passo.
Após ter o identificador do serviço, criamos a URL do repositório dinamicamente e realizamos um docker pull com ele. Isso garante que a imagem docker está sendo baixada para nosso sistema.
O script agora fará cd para a pasta do aplicativo, /home/ubuntu/[APP_FOLDER]
, isso pressupõe que você está executando tudo sob o usuário ubuntu
e que sua pasta HOME
é nomeada ubuntu
, APP_FOLDER
contém todo o configuração.
O próximo passo consiste em parar e iniciar o serviço após o qual simplesmente removemos as imagens antigas e não utilizadas com o comando docker image prune -fa
, você pode saber mais aqui: https://docs.docker.com/reference/cli/docker/system/prune/.
O arquivo Docker compose
Compose é a utilidade que executa todo nosso sistema, ele precisa de um arquivo chamado docker-compose.yml
onde você definirá tudo, vamos假设 que nosso aplicativo precisa de um serviço de redis
e postgres
para funcionar, é assim que ficará:
version: '3.9'
services:
web:
image: "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/myapp:latest"
ports:
- 8080:8080
depends_on:
- redis
- db
env_file:
- .env
redis:
image: 'redis:alpine'
ports:
- '6379:6379'
db:
image: 'postgres:14'
restart: always
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
PGDATA: /var/lib/postgresql/data/pgdata
healthcheck:
test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ]
timeout: 45s
interval: 10s
retries: 10
ports:
- '5437:5432'
volumes:
- ./opt/postgres/data:/var/lib/postgresql/data
Seu volume ./opt/postgres/data:/var/lib/postgresql/data
mapeará o conteúdo do servidor Postgres para o disco local, para que ele não se perca quando o container Docker para de rodar. Aprenda mais sobre como executar o Postgres com docker-compose aqui https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. Usei uma diretiva chamada env_file
que permite ao docker-compose ler um arquivo e carregar seu conteúdo no container Docker em tempo de execução; fiz isso porque geralmente o arquivo docker-compose é comprometido em um VCS, onde não quero manter a variável de ambiente diretamente via diretiva environment
no serviço. Note que nosso serviço é chamado web
aqui, anteriormente escrevemos um arquivo redeploy.sh
e pretendemos executá-lo assim:
./redeploy.sh web
O argumento web
está vinculado ao nome do nosso serviço, aquele arquivo simplesmente mapeia o argumento para um nome de serviço no arquivo Docker.
Configurar um serviço Linux para manter tudo em execução
Nesta etapa, temos que criar um serviço Linux que garantirá que a aplicação seja iniciada sempre que o servidor for iniciado ou nossa aplicação parar. O seguinte script ajudará você a fazer isso:
[Unit]
Description=[APP_NAME] service executed by docker compose
PartOf=docker.service
After=docker.service
After=network.target
[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/home/ubuntu/[APP_FOLDER]
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
Vamos analisá-lo !!!
-
A seção
Unit
descreve nosso serviço e especifica em qual serviço nossa unidade faz parte; neste caso, é o serviço Docker, isso garantirá que nosso serviço sempre rode quando o serviço Docker estiver em execução também. -
A seção
Service
descreve como executar nosso serviço, as partes interessantes sãoWorkingDirectory
,ExecStart
eExecStop
, eles serão usados de acordo com o que seus nomes significam, por exemplo, se o serviço é chamadomyapp
quando você digitar o comandosystemctl start myapp
o comandoExecStart
será executado. Você pode aprender mais sobre o serviço Linux aqui https://www.redhat.com/sysadmin/systemd-oneshot-service. Aprenda mais sobre como executar o serviço docker comsystemd
aqui: https://bootvar.com/systemd-service-for-docker-compose/
Este serviço precisa ser instalado de forma que o sistema o execute quando necessário, você terá que salvá-lo em um arquivo com um nome, por exemplo: myapp.service
touch myapp.service
# open it
nano myapp.service
# paste the previous scrip in it
cp myapp.service /etc/systemd/system/myapp.serivce
A partir desse ponto, ele é reconhecido como um serviço Linux, você pode executar systemctl start myapp
para iniciá-lo. O comando seguinte necessário é
systemctl enable myapp.service
Isso garantirá que o serviço seja executado automaticamente pelo servidor em cada reinicialização. Você pode aprender mais aqui: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6
O servidor web
Usei o Nginx para essa tarefa, é pequeno e poderoso, é amplamente utilizado e pode atuar como balanceador de carga, servidor de arquivos estáticos, proxy reverso e muito mais. A primeira coisa a fazer é instalá-lo.
sudo apt-get install nginx
Niba esse passo, a imagem do docker está supostamente em execução, vamos supor que contém um aplicativo rodando na porta 8080
, e essa porta está vinculada ao servidor através do arquivo docker-compose. Precisamos configurar um proxy reverso entre o Nginx e nossa porta. Aqui está a configuração necessária:
upstream app_backend {
server localhost:8080; # the appliction port
}
server {
listen 80;
server_name [DOMAIN_NAME];
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
}
}
Chamemos essa configuração de myapp.conf
e salve-a em um diretório onde o nginx a encontrará, essa pasta, entre outras, é chamada /etc/nginx/conf.d/
.
sudo touch /etc/nginx/conf.d/myapp.conf
sudo nano /etc/nginx/conf.d/myapp.conf
# paste the content there
Agora, tudo o que precisamos é testar e reiniciar o serviço do NGINX com os seguintes comandos
sudo nginx -t # test if the config is valid
sudo nginx -s reload # reload the nginx service so it will consider it
Essa configuração instruirá o nginx a ouvir o tráfego na porta 80
e com o nome de domínio [NOME_DO_DOMÍNIO]
e encaminhá-lo para o servidor do seu aplicativo na porta 8080
através da diretiva proxy_pass
, a linha location / {
simplesmente significa capturar todas as solicitações começando com /
e executar as ações escritas sob o bloco location
. Saiba mais aqui https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.
A Pipeline de Construção
Após configurar o servidor, agora temos que definir o pipeline de construção, que principalmente consiste em 1 etapa, escrever um arquivo de pipeline de Github Action e adicioná-lo ao projeto, vamos lá.
Configuração de GitHub Actions
A GitHub action será usada para construir a imagem Docker a partir do nosso código-fonte e empurrá-la para o registro de onde a imagem é puxada e executada no servidor. Vou usar um Dockerfile de exemplo para este exemplo, mas na prática, você terá que escrever seu próprio Dockerfile. Para uma aplicação express.js, o arquivo Docker seria assim:
# Fetching the minified node image on apline linux
FROM node:slim
# Declaring env
ENV NODE_ENV production
# Setting up the work directory
WORKDIR /express-docker
# Copying all the files in our project
COPY . .
# Installing dependencies
RUN npm install
# Installing pm2 globally
RUN npm install pm2 -g
# Exposing server port
EXPOSE 8080
# Starting our application
CMD pm2 start process.yml && tail -f /dev/null
Construir e executar esse Dockerfile iniciará nossa aplicação na porta 8000, mas na nossa configuração, teremos que executá-lo com docker-compose.
A próxima coisa é configurar o pipeline de GitHub actions. Para isso, simplemente crie uma pasta .github/workflows
na raiz do projeto e crie um arquivo chamado docker-build.yml
, onde escreveremos nosso pipeline.
name: Build, Push to ECS and Deploy to Server
on:
push:
branches: ['deploy/main']
jobs:
build:
name: Build Web Image
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: [AWS_REGION]
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: [REPOSITORY_NAME]
IMAGE_TAG: latest
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Restart the service via SSH
uses: appleboy/[email protected]
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: /home/ubntu/[APP_DIRECTORY]/redeploy.sh web
Aqui há várias etapas para olhar:
-
Configurar credenciais AWS
: aqui o sistema carregará a chave aws que você criou anteriormente, você precisará registrá-las nos segredos da sua conta GitHub -
Construir, tag e empurrar imagem para Amazon ECR
: esta etapa executará o comandodocker build
edocker push
para criar a imagem Docker -
Reiniciar o serviço via SSH
essa etapa fará a conexão com o servidor e reiniciará a aplicação inteira de uma vez.
Este pipeline será executado sempre que houver um pull request mesclado contra o branch deploy/main
.
on:
push:
branches: ['deploy/main']
Agora todo o sistema está em lugar e interligado, e é possível editá-lo e aplicá-lo ao seu caso específico. Em um artigo futuro, compartilharei o processo de construção da aplicação em si para produção e execução em um arquivo Docker.
Conclusão
Este artigo tenta descrever o processo que uso para configurar um VPS para automação quando se trata de implantação. Ele descreve como configurar o processo de execução da aplicação dentro do servidor e o processo de construção da aplicação, cada parte pode ser feita com outra ferramenta, por exemplo, você pode substituir o nginx pelo Treafik se desejar, e pode substituir o serviço systemd
por um programa no supervisor
e mais. Este processo não cobre coisas adicionais como fazer backup do servidor ou fechar as portas padrão nos servidores, essas serão explicadas em artigos futuros. Sinta-se à vontade para fazer uma pergunta se quiser adaptar isso ao seu fluxo. Em outro artigo, vou me concentrar em como configurar uma aplicação para estar pronta para produção em termos de implantação, essa é a parte do processo que vem antes da construção da Imagem Docker.
Espero que tenha gostado da leitura.
Estou aqui para ajudar você a implementar isso dentro da sua empresa ou equipe, permitindo que você se concentre nas suas tarefas principais e economize dinheiro antes de explorar o vasto potencial do Mastodon.