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 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

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 é 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.

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.

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/.

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.

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ão WorkingDirectory, ExecStart e 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 o serviço 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

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

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.

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á.

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 comando docker build e docker 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.

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.

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