PHP 7.4 → 8.4 em produção no ASG sem downtime: o processo de bake AMI que funciona (e os 2 erros que aprendemos)
Publicado em 30 de abril de 2026
O contexto: PHP em container, worker baked na AMI
A stack rodava PHP-FPM em containers Docker dentro de instâncias EC2 ARM no ASG. Três pools: pool0 (porta 9000), pool1 (9001), pool2 (9002). O worker de filas também rodava como serviço systemd dentro de uma das instâncias — tudo baked na AMI do Launch Template.
Upgrade de PHP 7.4.34 para 8.4.12. Versão major com breaking changes conhecidos. A aplicação já havia sido testada pelo time de desenvolvimento em ambiente de staging — a nossa parte era executar o upgrade em produção sem downtime.
Uma restrição importante aprendida em sessão anterior: Instance Refresh manual só pode ser executado durante o horário diurno. Com mínimo de 1 instância à noite, qualquer refresh substitui a única instância ativa — o que causa ~2 minutos de downtime. Durante o dia, com mínimo de 2 instâncias e MinHealthyPercentage=100, o ASG substitui uma por vez mantendo disponibilidade. Documentamos isso em outro post sobre o incidente de deploy noturno.
O processo de bake AMI: os 8 passos
O processo padrão para qualquer mudança de infraestrutura neste ambiente é sempre o mesmo — nunca modificar uma instância que está no ASG recebendo tráfego do ALB:
1. Obter AMI e configs do Launch Template (LT) default
2. Subir instância temporária isolada a partir dessa AMI
(fora do ASG, fora do target group do ALB)
3. Aplicar as mudanças na instância temporária e validar
4. Criar nova AMI da instância temporária (--no-reboot)
5. Terminar a instância temporária
6. Criar nova versão do Launch Template com a nova AMI
(source = versão default atual, só muda ImageId)
7. Setar nova versão como default no LT
8. Iniciar Instance Refresh no ASG
(MinHealthyPercentage=100, InstanceWarmup=120)A instância temporária nunca entra no ALB. Todas as mudanças são testadas nela antes de virar AMI. Se algo der errado, a produção não é afetada — a temporária simplesmente é terminada.
# Subir instância temporária a partir da AMI do LT default
TEMP_ID=$(aws ec2 run-instances --profile app-profile \
--image-id <ami-lt-default> \
--instance-type c6g.2xlarge \
--key-name app-key \
--security-group-ids <sg-id> \
--subnet-id <subnet-privada> \
--iam-instance-profile Arn=<iam-profile-arn> \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=app-temp-bake}]' \
--query 'Instances[0].InstanceId' --output text)
# Aguardar 2/2 health checks (não usar wait instance-running)
aws ec2 wait instance-status-ok --profile app-profile --instance-ids $TEMP_ID
# Instance Refresh (executar somente em horário diurno)
aws autoscaling start-instance-refresh --profile app-profile \
--auto-scaling-group-name app-asg \
--preferences '{"InstanceWarmup":120,"MinHealthyPercentage":100}' \
--query 'InstanceRefreshId' --output textBake 1: o upgrade principal
O operador aplicou o upgrade de PHP manualmente na instância temporária e validou os containers:
$ docker exec php-fpm-pool0 php --version
PHP 8.4.12 (cli) (built: Aug 28 2025 18:47:43) (NTS)
Zend Engine v4.4.12
with Zend OPcache v8.4.12
$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep php
php-fpm-pool2 php-fpm-custom:8.4.12 Up 3 minutes
php-fpm-pool0 php-fpm-custom:8.4.12 Up 3 minutes
php-fpm-pool1 php-fpm-custom:8.4.12 Up 3 minutesAMI criada, instância temporária terminada, nova versão do LT criada, Instance Refresh iniciado às 19:14 -03:00. Concluído às 19:22 — exatos 8 minutos. Duas instâncias produção confirmadas com PHP 8.4.12.
Mas ao verificar os logs do CloudWatch nas horas seguintes, identificamos que os logs de erro do PHP-FPM não estavam chegando. Os pools rodavam, mas o log group estava silencioso.
O erro do Bake 1: logs PHP-FPM sem permissão de escrita
A investigação mostrou o problema: o diretório '/var/log/php-fpm/' pertencia a 'apache:root' com permissão 'drwxrwx---'. O processo PHP-FPM rodava como usuário 'www-data', que estava na categoria 'others' — sem acesso de escrita ao diretório.
Os pools tinham 'php_admin_value[error_log]' configurado para '/var/log/php-fpm/pool{0,1,2}.log', mas os arquivos não conseguiam ser criados porque o diretório não era acessível. Os erros PHP eram descartados silenciosamente.
# Estado problemático (após Bake 1)
$ ls -la /var/log/ | grep php-fpm
drwxrwx--- 2 apache root 4096 Mar 28 10:00 php-fpm/
# www-data não tem acesso (está em "others" = ---)
# Correção necessária para o Bake 2
sudo chgrp www-data /var/log/php-fpm/
sudo chmod g+rwx /var/log/php-fpm/
sudo touch /var/log/php-fpm/pool0.log \
/var/log/php-fpm/pool1.log \
/var/log/php-fpm/pool2.log
sudo chown www-data:www-data /var/log/php-fpm/pool*.logEste erro não aparece em staging se o ambiente de staging usa configurações diferentes de permissão. É o tipo de problema que só aparece em produção — e só depois de um bake.
Bake 2: o ajuste de permissões e CloudWatch
Segunda instância temporária, desta vez a partir da AMI do Bake 1 (que já tinha PHP 8.4.12). As mudanças foram aplicadas em sequência:
1. Corrigir permissões do diretório /var/log/php-fpm/ (chgrp www-data + chmod g+rwx + criar arquivos pool*.log com owner www-data)
2. Atualizar configuração do CloudWatch agent para coletar os três arquivos de log dos pools
3. Validar escrita real nos arquivos antes de criar a AMI (docker exec sh -c 'echo x >> /var/log/php-fpm/pool0.log')
A configuração do CloudWatch agent foi atualizada para incluir os três pools. Um detalhe crítico: nunca usar o comando 'fetch-config' do CloudWatch agent para atualizar a configuração — ele sobrescreve o arquivo com um nome duplicado e o agente para silenciosamente. O correto é escrever o JSON diretamente no arquivo e reiniciar via systemctl.
# ERRADO: fetch-config destrói a configuração existente
# sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
# -a fetch-config -m ec2 -s -c file:/path/config.json # NAO USAR
# CORRETO: escrever direto no arquivo + reiniciar o serviço
sudo python3 -c "
import json
config = {
'logs': {
'logs_collected': {
'files': {
'collect_list': [
{
'file_path': '/var/log/php-fpm/pool0.log',
'log_group_name': 'php-fpm-errors',
'log_stream_name': '{instance_id}-pool0'
},
{
'file_path': '/var/log/php-fpm/pool1.log',
'log_group_name': 'php-fpm-errors',
'log_stream_name': '{instance_id}-pool1'
},
{
'file_path': '/var/log/php-fpm/pool2.log',
'log_group_name': 'php-fpm-errors',
'log_stream_name': '{instance_id}-pool2'
}
]
}
}
}
}
path = '/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_amazon-cloudwatch-agent.json'
open(path, 'w').write(json.dumps(config, indent=2))
print('OK')
"
sudo systemctl restart amazon-cloudwatch-agent
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a statusInstance Refresh do Bake 2 iniciado às 19:34, concluído às 19:41 — mais 7 minutos. Logs do PHP-FPM chegando no CloudWatch nos streams '{instance_id}-pool0', '{instance_id}-pool1', '{instance_id}-pool2'.
Estado final: o histórico dos Launch Templates
Ao final da sessão, o histórico do Launch Template documentava o caminho percorrido:
| Versão | Conteúdo | Status |
|--------|---------------------------------------------|-----------------|
| v606 | PHP 7.4.34 (base anterior) | Descartada |
| v607 | PHP 8.4.12 (Bake 1 — sem permissões log) | Descartada |
| v608 | PHP 8.4.12 + permissões + logs CW (Bake 2) | DEFAULT ATUAL |Instâncias de produção pós-refresh final: duas instâncias ARM rodando PHP 8.4.12 com logs de erro chegando no CloudWatch. Monitoramento ativo nas horas seguintes para detectar erros de compatibilidade PHP 8 que a aplicação pudesse gerar em produção.
Os 2 erros e o que eles ensinam
Erro 1: permissões do /var/log/php-fpm/
O diretório de logs foi criado com ownership 'apache:root' em algum momento histórico. O processo PHP-FPM (www-data) nunca conseguiu escrever. O erro era silencioso: nenhuma mensagem de falha, apenas ausência de logs.
Lição: ao configurar qualquer diretório de log para um processo que roda com usuário diferente do dono do diretório, validar explicitamente que o processo consegue escrever antes de bake. O teste é simples: 'docker exec sh -c "echo x >> arquivo.log"' e verificar se não há erro de permissão.
Erro 2: fetch-config destrói a configuração do CloudWatch agent
O comando 'amazon-cloudwatch-agent-ctl -a fetch-config' não atualiza a configuração — ele cria um arquivo novo com nome duplicado ('file_file_nome.json'), pode deixar o arquivo original com 0 bytes e o agente para silenciosamente. O status aparece como 'stopped' sem mensagem de erro clara.
Lição: para atualizar a configuração do CloudWatch agent, escrever diretamente no arquivo JSON existente e reiniciar o serviço via systemctl. Validar com '-a status' antes de criar a AMI.
Por que 2 bakes são normais, não um sinal de problema
Cada ciclo de bake revela uma camada que o anterior não antecipou. O primeiro bake cobre a mudança principal. O segundo cobre o que a validação do primeiro revelou. Em upgrades de versão major, dois ciclos é o padrão — não exceção. O processo existe justamente para absorver essas descobertas antes que afetem produção.
O custo total: dois Instance Refreshes de ~8 minutos cada, duas instâncias temporárias que existiram por menos de 20 minutos. Nenhum downtime. Nenhuma transação perdida.
