Como implantar um aplicativo de produção em um VPS e automatizar o processo com Docker, GitHub Actions e AWS ECR.

Neste artigo, vamos aprender o que significa lançar uma aplicação em 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 VPS (Servidor Privado Virtual), um servidor dedicado ou qualquer servidor onde você tenha SSH para usá-lo. Temos que executar ações, preparar o servidor para receber e executar o app, e configurar o pipeline de implantação. Para a parte seguinte deste artigo, vou considerar que você tem um servidor Ubuntu

A primeira coisa a fazer algumas configurações de um único uso no servidor, o objetivo é prepará-lo para as próximas etapas. Para esse propósito, temos que considerar os seguintes tópicos:

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.

É importante notar que você pode alcançar o mesmo objetivo com outro repositório, se desejar, estou usando AWS aqui porque é mais simples para mim.

Já que 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 mais tarde. Ele tem o seguinte formato AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

Você também precisará configurar credenciais AWS IAM que tenham permissão para pulls/pushs 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 separadamente, pois precisaremos delas para o trabalho seguinte.

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 no aws, a secret key e o secret key id. Você também será solicitado a escolher um formato de saída, simplesmente JSON, e fornecer uma região padrão, é melhor escolher a região onde você criou o registro ECR anteriormente.

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 aplicativo, 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 um identificador, o identificador especificado será usado como argumento de linha de comando, entraremos em mais detalhes sobre isso mais tarde. Após isso, verificamos se o usuário forneceu um argumento que corresponde 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 a pasta HOME dele é chamada ubuntu, APP_FOLDER contém todo o configuração.

O下一步 consists em parar e iniciar o serviço após o qual simplesmente removemos as imagens antigas e não usadas com o comando docker image prune -fa, você pode saber mais aqui: https://docs.docker.com/reference/cli/docker/system/prune/.

Compose é o utilitário que executa todo o nosso sistema, ele precisa de um arquivo chamado docker-compose.yml onde você definirá tudo, vamos supor que nosso aplicativo precise de um serviço de redis e um serviço de 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 vai mapear o conteúdo do servidor Postgres para o disco local, então ele não pode ser perdido quando o container Docker para de funcionar. Aprenda mais sobre como executar o Postgres com docker-compose aqui https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. Eu 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 é commitado para um VCS, então não quero manter a variável de ambiente diretamente via diretiva environment no serviço. Note que nosso serviço é nomeado 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, esse arquivo simplesmente mapeia o argumento para um nome de serviço no arquivo Docker.

Nesta etapa, temos que criar um serviço Linux que vai garantir que a aplicação seja iniciada sempre que o servidor inicia ou nossa aplicação para. O seguinte script vai 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 qual serviço nossa unidade faz parte, neste caso, é o serviço Docker, isso vai garantir que nosso serviço sempre execute quando o serviço Docker também estiver em execução.

  • A seção Service descreve como executar nosso serviço, as partes interessantes são WorkingDirectory, ExecStart e o comando ExecStop, eles serão usados de acordo com o que seus nomes significam, por exemplo, se o serviço é chamado myapp quando você digitar o comando systemctl start myapp o comando ExecStart será executado. Você pode aprender mais sobre serviços Linux aqui https://www.redhat.com/sysadmin/systemd-oneshot-service. Aprenda mais sobre como executar o serviço docker com systemd 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

astleto 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 a cada reinicialização. Você pode aprender mais aqui: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

Usei o Nginx para essa tarefa, ele é 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 está etapa, a imagem do docker supostamente está em execução, vamos supor que contém um aplicativo rodando na porta 8080, e que essa porta está vinculada ao servidor através do arquivo docker-compose. Precisamos configurar uma configuração de 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;
  }
}

vamos chamar essa configuração de myapp.conf e salvá-la em um diretório onde o nginx a encontrará, esse diretório, entre outros, é chamado /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 fazer é testar e reiniciar o serviço 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

Esta configuração instruirá o nginx a ouvir o tráfego na porta 80 e com o nome de domínio [DOMÍNIO] e enviá-lo ao seu servidor de aplicativo na porta 8080 através da diretiva proxy_pass, a linha location / { simplesmente significa capturar todas as solicitações que começam com / e realizar 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.

Após configurar o servidor, agora temos que configurar o pipeline de construção, que principalmente consiste em 1 passo, escrever um arquivo de pipeline do Github Action e adicioná-lo ao projeto, vamos lá.

O GitHub action será usado 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 o 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 este 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 do GitHub actions. Para isso, simplesmente crie uma pasta .github/workflows na raiz do projeto e crie um arquivo chamado docker-build.yml, escreveremos nosso pipeline nele.

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

Existem vários passos para olhar aqui:

  • Configurar credenciais AWS: aqui o sistema carregará a chave aws que você criou anteriormente, você terá que registrá-las nos segredos da sua conta GitHub

  • Construir, tag e empurrar a imagem para o Amazon ECR: este passo executará o comando docker build e docker push para criar a imagem Docker

  • Reiniciar o serviço via SSH esta etapa conectará ao servidor e reiniciará a aplicação inteira de uma vez.

Este pipeline será executado toda vez 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, agora é 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.

Este artigo tenta descrever o processo que uso para configurar um VPS para automação quando se trata de implantação. Ele descreve como definir 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, focarei 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 em suas tarefas principais e economize dinheiro antes de explorar o vasto potencial do Mastodon.

Source:
https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr