Deployment: Envoyer, Forge e estratégias zero-downtime

1. Fundamentos do Deployment Zero-Downtime em PHP

1.1. O que é zero-downtime e por que é crítico para aplicações PHP

Zero-downtime deployment é a capacidade de atualizar uma aplicação em produção sem interromper o serviço aos usuários. Para aplicações PHP, isso é especialmente crítico porque:

  • Usuários podem estar em meio a requisições HTTP
  • Jobs de fila podem estar sendo processados
  • Sessões ativas podem ser corrompidas durante a troca de versões

Uma aplicação PHP que fica indisponível por 30 segundos durante um deploy pode perder milhares de requisições e causar frustração nos usuários.

1.2. Desafios específicos do PHP: opcache, sessions e filas

O PHP apresenta desafios únicos para zero-downtime:

Opcache: O cache de bytecode do PHP armazena versões compiladas dos arquivos. Se você substituir arquivos PHP sem limpar o opcache, o servidor continuará executando código antigo.

// Script para limpar opcache programaticamente
if (function_exists('opcache_reset')) {
    opcache_reset();
    echo "Opcache limpo com sucesso.\n";
}

Sessions: Dados de sessão armazenados em arquivos podem ser dessincronizados se a estrutura mudar entre versões.

Filas: Workers processando jobs antigos podem falhar se classes ou métodos forem removidos na nova versão.

1.3. Estratégias clássicas: blue-green deployment vs. rolling updates

Blue-Green Deployment: Mantém dois ambientes completos (blue e green). Um recebe tráfego enquanto o outro é atualizado. A troca é feita via balanceador de carga.

Rolling Updates: Atualiza instâncias uma a uma, mantendo as demais ativas. Mais comum em clusters com múltiplos servidores web.

Para aplicações PHP simples, o mecanismo de symlinks (usado pelo Envoyer) é a abordagem mais prática.

2. Laravel Forge: Provisionamento e Deploy Simplificado

2.1. Configuração de servidores: Nginx, PHP-FPM e banco de dados

O Laravel Forge automatiza a configuração inicial do servidor. Você escolhe o provedor (DigitalOcean, AWS, Linode) e o Forge provisiona:

  • Nginx com configuração otimizada para PHP
  • PHP-FPM com pools configurados
  • MySQL/PostgreSQL ou MariaDB
  • Redis para cache e filas
# Exemplo de configuração Nginx gerada pelo Forge
server {
    listen 80;
    server_name exemplo.com;
    root /home/forge/exemplo.com/current/public;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

2.2. Script de deploy automático: git pull, composer install e migrations

O script de deploy do Forge pode ser personalizado. Exemplo típico:

# Deploy script do Forge
cd /home/forge/exemplo.com
git pull origin main
composer install --no-dev --prefer-dist
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

2.3. Integração com GitHub/GitLab e deploy contínuo via webhooks

Configure webhooks no repositório para disparar deploys automáticos. No GitHub, vá em Settings > Webhooks e aponte para a URL fornecida pelo Forge.

// Exemplo de verificação de webhook (para validação no servidor)
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
$payload = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $payload, env('WEBHOOK_SECRET'));

if (hash_equals($expected, $signature)) {
    // Executar deploy
}

3. Laravel Envoyer: Orquestração de Deploys com Zero-Downtime

O Envoyer implementa zero-downtime através do padrão de releases:

/home/forge/exemplo.com/
├── releases/
│   ├── 20240101-120000/   # Release atual
│   └── 20240102-100000/   # Nova release (preparada)
└── current -> releases/20240101-120000/  # Symlink ativo

Durante o deploy, uma nova release é criada em paralelo. Apenas quando tudo está pronto, o symlink current é atualizado atomicamente.

3.2. Configuração de hooks: preparação, ativação e rollback

Os hooks permitem executar scripts em momentos específicos:

// Hook de ativação (executado após symlink ser trocado)
php artisan migrate --force
php artisan queue:restart

// Hook de rollback (se algo der errado)
php artisan down --retry=10
// Reverter symlink manualmente

3.3. Gerenciamento de múltiplos servidores e balanceadores de carga

Para múltiplos servidores, o Envoyer coordena deploys sequenciais ou paralelos:

// Configuração de servidores no Envoyer (exemplo conceitual)
'servers' => [
    'web1' => ['ip' => '10.0.0.1', 'roles' => ['web']],
    'web2' => ['ip' => '10.0.0.2', 'roles' => ['web']],
    'worker' => ['ip' => '10.0.0.3', 'roles' => ['queue']],
],

4. Estratégias de Migrations e Cache sem Interrupção

4.1. Migrations seguras: separação de mudanças destrutivas em múltiplos deploys

Nunca misture mudanças destrutivas (como remover colunas) com deploys. Exemplo de migração segura:

// Primeiro deploy: adicionar nova coluna
Schema::table('users', function (Blueprint $table) {
    $table->string('email_new')->nullable();
});

// Segundo deploy (após código usar nova coluna): remover antiga
Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('email');
    $table->renameColumn('email_new', 'email');
});

4.2. Recriação de cache (config, routes, views) sem downtime

O cache deve ser recriado após o symlink ser atualizado:

// No hook de ativação do Envoyer
Artisan::call('config:cache');
Artisan::call('route:cache');
Artisan::call('view:cache');

4.3. Limpeza do opcache via PHP-FPM reload ou comando dedicado

Forçar recarga do PHP-FPM garante que todo opcache seja limpo:

sudo service php8.2-fpm reload

Ou use uma rota protegida para limpeza programática:

Route::post('/__opcache/reset', function () {
    if (opcache_reset()) {
        return response()->json(['status' => 'ok']);
    }
    return response()->json(['status' => 'error'], 500);
})->middleware('auth:api');

5. Gerenciamento de Filas e Jobs durante o Deploy

5.1. Pausa e retomada segura de workers (Horizon e Supervisord)

Com Laravel Horizon, pause workers antes do deploy:

// No hook de preparação
Artisan::call('horizon:pause');

// No hook de ativação
Artisan::call('horizon:continue');
Artisan::call('horizon:terminate');

5.2. Evitando perda de jobs: uso de filas com retry e dead-letter queues

Configure jobs para serem resilientes:

class ProcessPayment implements ShouldQueue
{
    public $tries = 5;
    public $backoff = [2, 5, 10, 30, 60];

    public function handle(): void
    {
        try {
            // Lógica do job
        } catch (\Exception $e) {
            if ($this->attempts() >= $this->tries) {
                // Enviar para dead-letter queue
                dispatch(new HandleFailedPayment($this->paymentId));
            }
            throw $e;
        }
    }
}

5.3. Sincronização entre versões antiga e nova de workers

Use versionamento de jobs para evitar incompatibilidades:

// Na nova versão, mantenha a classe antiga como alias
class_alias(\App\Jobs\V2\ProcessPayment::class, \App\Jobs\ProcessPayment::class);

6. Monitoramento e Rollback em Produção

6.1. Health checks automatizados pós-deploy (Laravel Pulse + APM)

Configure endpoints de health check:

Route::get('/health', function () {
    $checks = [
        'database' => DB::connection()->getPdo() ? 'ok' : 'fail',
        'cache' => Cache::get('health_check') ?? 'fail',
        'queue' => Queue::size() < 1000 ? 'ok' : 'warning',
    ];

    if (in_array('fail', $checks)) {
        return response()->json($checks, 500);
    }

    return response()->json($checks);
});

6.2. Estratégias de rollback rápido: ativação de release anterior

No Envoyer, o rollback é simples:

# Comando de rollback no servidor
cd /home/forge/exemplo.com
ln -sfn releases/20240101-120000 current
sudo service php8.2-fpm reload

6.3. Alertas e logs: detectando falhas no deploy em tempo real

Configure notificações no Envoyer para Slack ou email:

// Exemplo de log estruturado para monitoramento
Log::channel('deploy')->info('Deploy iniciado', [
    'release' => $releaseId,
    'server' => gethostname(),
    'timestamp' => now(),
]);

7. Segurança e Boas Práticas no Pipeline de Deploy

7.1. Variáveis de ambiente (.env) e secrets management no servidor

Nunca armazene .env no repositório. Use o gerenciador de secrets do Forge:

// No script de deploy, referencie variáveis do ambiente
DB_PASSWORD=$(forge env get DB_PASSWORD)

7.2. Permissões de diretórios e hardening do ambiente PHP

Configure permissões corretas:

# Após cada deploy
chmod -R 755 /home/forge/exemplo.com/current/storage
chmod -R 755 /home/forge/exemplo.com/current/bootstrap/cache
chown -R forge:forge /home/forge/exemplo.com

7.3. Testes automatizados antes do deploy: CI/CD integrado com Forge/Envoyer

Integre testes no pipeline:

# Exemplo de GitHub Actions
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run tests
        run: |
          composer install
          php artisan test
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Forge deploy
        run: |
          curl -X POST ${{ secrets.FORGE_DEPLOY_URL }}

Referências