DevOps

Deploy Automático para Servidores com GitHub Actions Já leu

12 min de leitura

Deploy Automático para Servidores com GitHub Actions
Todo o trabalho construído nos artigos anteriores — testes, build de imagens, gerenciamento de secrets — converge em um único objetivo: colocar código novo em produção de forma confiável, rápida e repetível. O deploy é o

Todo o trabalho construído nos artigos anteriores — testes, build de imagens, gerenciamento de secrets — converge em um único objetivo: colocar código novo em produção de forma confiável, rápida e repetível. O deploy é o último metro dessa jornada, e é onde a maioria dos times sente mais ansiedade.

Essa ansiedade tem origem histórica. Quando deploys eram eventos raros e manuais, cada um carregava o peso acumulado de semanas de mudanças. O risco era proporcional ao tamanho do lote. Com CI/CD bem implementado, deploys são frequentes, pequenos e automatizados — e a ansiedade desaparece porque o risco por deploy torna-se mínimo.

Este artigo cobre as estratégias e implementações concretas de deploy automático usando GitHub Actions, do caso mais simples ao mais sofisticado.


Os Três Modelos de Deploy

Antes de escrever qualquer pipeline, é necessário entender que existem três modelos fundamentais de deploy, cada um adequado a contextos diferentes.

Push-based deployment — o pipeline de CI/CD conecta ativamente ao servidor de destino e executa os comandos de atualização. É o modelo mais direto e mais comum em infraestruturas tradicionais com servidores fixos. O pipeline "empurra" a mudança para o servidor.

Pull-based deployment — o servidor de destino monitora um registro de imagens ou um repositório e aplica as mudanças por conta própria quando detecta uma nova versão. É o modelo usado pelo ArgoCD e pelo FluxCD em ambientes Kubernetes. O servidor "puxa" a mudança do registro.

Artifact-based deployment — o pipeline gera um artefato imutável — uma imagem Docker, um pacote ZIP, um binário compilado — que é armazenado em um registro. O deploy consiste em instruir o ambiente de destino a usar aquele artefato específico. É o modelo usado pelo AWS ECS, pelo Elastic Beanstalk e pelo Heroku.

Os exemplos deste artigo cobrem principalmente o modelo push-based para servidores próprios e o artifact-based para serviços gerenciados, que são os mais relevantes para quem está começando.


Deploy Simples via SSH

O caso mais básico: um servidor Linux com Docker instalado, acessível via SSH. O pipeline conecta, baixa a nova imagem e reinicia o container.

Configuração inicial no servidor:

# No servidor de produção
# Cria o usuário de deploy sem privilégios desnecessários
sudo useradd -m -s /bin/bash deploy
sudo usermod -aG docker deploy

# Cria diretório para a aplicação
sudo mkdir -p /opt/minha-api
sudo chown deploy:deploy /opt/minha-api

# Cria o arquivo de variáveis de ambiente da aplicação
sudo tee /opt/minha-api/.env > /dev/null << 'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://app:senha@localhost:5432/producao
REDIS_URL=redis://localhost:6379
EOF
sudo chmod 600 /opt/minha-api/.env
sudo chown deploy:deploy /opt/minha-api/.env

Gerando e configurando a chave SSH para o pipeline:

# Na máquina local — gera um par de chaves dedicado para o pipeline
ssh-keygen -t ed25519 -C "github-actions-deploy" \
  -f ~/.ssh/github_actions_deploy \
  -N ""

# Copia a chave pública para o servidor
ssh-copy-id -i ~/.ssh/github_actions_deploy.pub deploy@seu-servidor.com

# Adiciona a chave privada como secret no repositório
gh secret set DEPLOY_SSH_KEY < ~/.ssh/github_actions_deploy
gh secret set DEPLOY_HOST --body "seu-servidor.com"
gh secret set DEPLOY_USER --body "deploy"

O workflow de deploy:

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

env:
  IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}

jobs:
  # ── Etapa 1: CI (reutiliza o workflow existente) ──
  ci:
    uses: ./.github/workflows/ci.yml

  # ── Etapa 2: Build e publicação da imagem ─────────
  build:
    needs: ci
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    outputs:
      image: ${{ env.IMAGE }}

    steps:
      - uses: actions/checkout@v4

      - name: Login no GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Configura Buildx
        uses: docker/setup-buildx-action@v3

      - name: Constrói e publica imagem
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ── Etapa 3: Deploy em staging ────────────────────
  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.minha-api.com

    steps:
      - name: Configura SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.STAGING_SSH_KEY }}

      - name: Registra host no known_hosts
        run: |
          ssh-keyscan -H ${{ secrets.STAGING_HOST }} >> ~/.ssh/known_hosts

      - name: Executa deploy em staging
        env:
          IMAGE: ${{ needs.build.outputs.image }}
        run: |
          ssh deploy@${{ secrets.STAGING_HOST }} bash -s << ENDSSH
            set -e

            # Autentica no registro
            echo "${{ secrets.GITHUB_TOKEN }}" | \
              docker login ghcr.io -u ${{ github.actor }} --password-stdin

            # Baixa a nova imagem
            docker pull $IMAGE

            # Para o container atual graciosamente
            docker stop minha-api-staging 2>/dev/null || true
            docker rm minha-api-staging 2>/dev/null || true

            # Sobe o novo container
            docker run -d \
              --name minha-api-staging \
              --env-file /opt/minha-api/.env \
              --network host \
              --restart unless-stopped \
              --log-driver json-file \
              --log-opt max-size=100m \
              --log-opt max-file=3 \
              $IMAGE

            # Aguarda o healthcheck
            echo "Aguardando aplicação ficar saudável..."
            for i in \$(seq 1 30); do
              STATUS=\$(curl -s -o /dev/null -w "%{http_code}" \
                http://localhost:3000/health 2>/dev/null || echo "000")

              if [ "\$STATUS" = "200" ]; then
                echo "Aplicação saudável após \${i}s"
                exit 0
              fi
              sleep 1
            done

            echo "Aplicação não ficou saudável em 30s"
            docker logs minha-api-staging --tail 50
            exit 1
          ENDSSH

  # ── Etapa 4: Smoke tests em staging ───────────────
  smoke-tests:
    needs: deploy-staging
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Executa smoke tests
        env:
          BASE_URL: https://staging.minha-api.com
        run: |
          # Verifica endpoints críticos
          check_endpoint() {
            local endpoint=$1
            local expected=$2
            local status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$endpoint")

            if [ "$status" != "$expected" ]; then
              echo "FALHA: $endpoint retornou $status, esperado $expected"
              return 1
            fi
            echo "OK: $endpoint retornou $status"
          }

          check_endpoint "/health" "200"
          check_endpoint "/api/v1/status" "200"
          check_endpoint "/api/v1/nao-existe" "404"
          check_endpoint "/api/v1/protegido" "401"

  # ── Etapa 5: Deploy em produção ───────────────────
  deploy-producao:
    needs: smoke-tests
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://api.minha-api.com

    steps:
      - name: Configura SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.PROD_SSH_KEY }}

      - name: Registra host no known_hosts
        run: |
          ssh-keyscan -H ${{ secrets.PROD_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy com zero downtime em produção
        env:
          IMAGE: ${{ needs.build.outputs.image }}
          HOST: ${{ secrets.PROD_HOST }}
        run: |
          ssh deploy@$HOST bash -s << 'ENDSSH'
            set -euo pipefail

            IMAGE="${{ env.IMAGE }}"
            CONTAINER_ATUAL="minha-api"
            CONTAINER_NOVO="minha-api-new"
            PORTA_PROD=3000
            PORTA_CANARY=3001

            echo "=== Iniciando deploy zero-downtime ==="
            echo "Imagem: $IMAGE"

            # Autentica e baixa a nova imagem ANTES de derrubar a atual
            echo "${{ secrets.GITHUB_TOKEN }}" | \
              docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker pull $IMAGE

            # Sobe o novo container em porta alternativa
            docker run -d \
              --name $CONTAINER_NOVO \
              --env-file /opt/minha-api/.env \
              --restart unless-stopped \
              -p $PORTA_CANARY:3000 \
              $IMAGE

            # Aguarda o novo container ficar saudável
            echo "Verificando saúde do novo container..."
            SAUDAVEL=false
            for i in $(seq 1 60); do
              STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
                http://localhost:$PORTA_CANARY/health 2>/dev/null || echo "000")

              if [ "$STATUS" = "200" ]; then
                echo "Novo container saudável após ${i}s"
                SAUDAVEL=true
                break
              fi
              sleep 1
            done

            if [ "$SAUDAVEL" != "true" ]; then
              echo "ERRO: Novo container não ficou saudável"
              echo "Logs do novo container:"
              docker logs $CONTAINER_NOVO --tail 100
              docker rm -f $CONTAINER_NOVO
              exit 1
            fi

            # Transfere o tráfego — atualiza o nginx para apontar para a nova porta
            # (em um cenário real, aqui seria uma atualização de upstream do nginx)
            echo "Transferindo tráfego para o novo container..."

            # Para o container antigo graciosamente
            docker stop $CONTAINER_ATUAL 2>/dev/null || true

            # Sobe o novo container na porta de produção
            docker rm $CONTAINER_ATUAL 2>/dev/null || true
            docker rm $CONTAINER_NOVO
            docker run -d \
              --name $CONTAINER_ATUAL \
              --env-file /opt/minha-api/.env \
              --restart unless-stopped \
              -p $PORTA_PROD:3000 \
              $IMAGE

            # Remove imagens antigas para liberar espaço
            docker image prune -f

            echo "=== Deploy concluído com sucesso ==="
          ENDSSH

      - name: Verifica deploy em produção
        run: |
          sleep 5
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
            https://api.minha-api.com/health)

          if [ "$STATUS" != "200" ]; then
            echo "ERRO: Produção não está saudável — status $STATUS"
            exit 1
          fi

          echo "Produção saudável — status $STATUS"

  # ── Etapa 6: Notificação ──────────────────────────
  notificar:
    needs: [deploy-producao]
    runs-on: ubuntu-latest
    if: always()

    steps:
      - name: Notifica resultado no Slack
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "${{ needs.deploy-producao.result == 'success' && '✅ *Deploy bem-sucedido*' || '❌ *Deploy falhou*' }}\n*Repositório:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* `${{ github.sha }}`\n*Autor:* ${{ github.actor }}"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Rollback Automatizado

Um deploy automatizado sem rollback automatizado está incompleto. Quando o deploy de produção falha, cada segundo conta. O rollback deve ser tão simples quanto o deploy:

# .github/workflows/rollback.yml
name: Rollback de Emergência

on:
  workflow_dispatch:
    inputs:
      versao:
        description: 'SHA do commit para o qual fazer rollback'
        required: true
        type: string
      ambiente:
        description: 'Ambiente de destino'
        required: true
        type: choice
        options:
          - staging
          - production

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: ${{ inputs.ambiente }}

    steps:
      - name: Valida que a imagem existe no registro
        run: |
          IMAGE="ghcr.io/${{ github.repository }}:${{ inputs.versao }}"
          docker manifest inspect $IMAGE > /dev/null 2>&1 || {
            echo "ERRO: Imagem $IMAGE não encontrada no registro"
            exit 1
          }
          echo "Imagem validada: $IMAGE"

      - name: Configura SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets[format('{0}_SSH_KEY', inputs.ambiente == 'production' && 'PROD' || 'STAGING')] }}

      - name: Executa rollback
        env:
          IMAGE: ghcr.io/${{ github.repository }}:${{ inputs.versao }}
          HOST: ${{ inputs.ambiente == 'production' && secrets.PROD_HOST || secrets.STAGING_HOST }}
        run: |
          ssh-keyscan -H $HOST >> ~/.ssh/known_hosts

          ssh deploy@$HOST bash -s << ENDSSH
            set -e
            echo "Iniciando rollback para ${{ inputs.versao }}..."

            echo "${{ secrets.GITHUB_TOKEN }}" | \
              docker login ghcr.io -u ${{ github.actor }} --password-stdin

            docker pull ${{ env.IMAGE }}

            docker stop minha-api 2>/dev/null || true
            docker rm minha-api 2>/dev/null || true

            docker run -d \
              --name minha-api \
              --env-file /opt/minha-api/.env \
              --restart unless-stopped \
              -p 3000:3000 \
              ${{ env.IMAGE }}

            sleep 5
            STATUS=\$(curl -s -o /dev/null -w "%{http_code}" \
              http://localhost:3000/health)

            if [ "\$STATUS" != "200" ]; then
              echo "ERRO CRÍTICO: Rollback falhou — aplicação não está saudável"
              exit 1
            fi

            echo "Rollback concluído — aplicação rodando versão ${{ inputs.versao }}"
          ENDSSH

      - name: Registra rollback no audit log
        run: |
          echo "ROLLBACK executado por ${{ github.actor }}" >> audit.log
          echo "Versão: ${{ inputs.versao }}" >> audit.log
          echo "Ambiente: ${{ inputs.ambiente }}" >> audit.log
          echo "Data: $(date -u)" >> audit.log

Deploy para AWS ECS com GitHub Actions

Para quem usa containers gerenciados na AWS, o fluxo de deploy muda: em vez de conectar via SSH a servidores, instrui-se o ECS a usar a nova imagem:

  deploy-ecs:
    needs: build
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Configura credenciais AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: us-east-1

      - name: Login no ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Faz tag e push da imagem para o ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: minha-api
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
          docker tag ghcr.io/${{ github.repository }}:${{ github.sha }} \
            $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Atualiza task definition do ECS
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: infrastructure/ecs-task-definition.json
          container-name: minha-api
          image: ${{ steps.login-ecr.outputs.registry }}/minha-api:${{ github.sha }}

      - name: Deploy no ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: minha-api-service
          cluster: producao
          wait-for-service-stability: true
          wait-for-minutes: 10

Estratégia de Promoção entre Ambientes

Em projetos maduros, o código não vai direto do repositório para produção. Ele percorre uma cadeia de ambientes, sendo promovido apenas quando aprovado em cada etapa:

Commit → CI → Build → Staging → QA → Pre-prod → Produção

O GitHub Actions modela isso com workflows encadeados e aprovações:

# A promoção para produção só acontece após aprovação explícita
# configurada em Settings → Environments → production → Required reviewers

jobs:
  deploy-staging:
    # Automático — acontece a cada merge na main
    environment: staging

  deploy-preprod:
    needs: deploy-staging
    # Automático após staging — mas requer que os testes passem
    environment: pre-production
    if: needs.deploy-staging.result == 'success'

  deploy-producao:
    needs: deploy-preprod
    # Requer aprovação manual configurada no GitHub Environment
    environment: production
    if: needs.deploy-preprod.result == 'success'

Essa estrutura garante que nenhum código chega a produção sem ter passado por todos os ambientes intermediários e sem aprovação explícita de alguém com autoridade para isso.


O Que Vem a Seguir

O próximo artigo fecha o Módulo 4 abordando notificações, logs e monitoramento do pipeline — como configurar alertas para falhas, como interpretar logs de pipeline para diagnóstico rápido e como integrar o status do pipeline com ferramentas de comunicação da equipe.


Referências para Aprofundamento

Documentação oficial - GitHub Actions — Deploying with GitHub Actions — Guia oficial com exemplos de deploy para múltiplas plataformas incluindo AWS, Azure e GCP. - AWS ECS Deploy Task Definition — GitHub — Action oficial da AWS para deploy no ECS com documentação detalhada de todos os parâmetros.

Estratégias de deploy - Deployment Strategies — Martin Fowler — Artigo clássico de Martin Fowler sobre Blue-Green Deployment, uma das estratégias de zero-downtime mais usadas. - Canary Releases — Martin Fowler — Explicação detalhada de Canary Releases com casos de uso e considerações práticas.

Ferramentas - webfactory/ssh-agent — GitHub — Action para gerenciamento de chaves SSH em runners do GitHub Actions. - slackapi/slack-github-action — GitHub — Action oficial do Slack para notificações de workflows com suporte a Block Kit para mensagens ricas.

Comentários

Mais em DevOps

Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação
Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação

A geração de senhas fortes é um dos pilares da segurança da informação. Uma s...

Artigo 26 — Seus Primeiros Recursos na AWS com Terraform
Artigo 26 — Seus Primeiros Recursos na AWS com Terraform

Artigo 26 — Seus Primeiros Recursos na AWS com Terraform Colocando Infraestru...

Compute, Storage e Redes no Azure
Compute, Storage e Redes no Azure

O artigo anterior estabeleceu a visão geral: como o Azure se organiza, como a...