FNSP Web Page

Trocar a imagem sem cortar ligações (e o rollback que não reverte tudo)

Começo pela parte honesta, que é a que falta em quase tudo o que está escrito sobre isto: um docker compose up -d para o contentor antigo e arranca o novo. Há um intervalo. São segundos, mas existem, e chamar-lhe «sem interrupção» é mentira.

O que um guião de deploy bem feito compra não é um intervalo de zero. É não ficar estragado — que é um problema completamente diferente, e muito mais frequente.

1. A etiqueta é fixa, nunca latest

services:
  web:
    image: gitea.exemplo.org/fnsp/fnsp_web:1.0.0
    container_name: fnsp-web
    restart: unless-stopped

Com :latest não se sabe o que está a correr, e o rollback passa a adivinhação: voltar atrás é voltar a quê? Uma etiqueta fixa transforma o deploy num número, e o guião reescreve essa linha a cada versão.

2. Verificar antes de mexer em alguma coisa

if ! docker pull "${IMAGE}:${VERSION}"; then
  echo "Falhou o pull."
  exit 1
fi

sed -i "s|${IMAGE}:[^[:space:]\"]*|${IMAGE}:${VERSION}|" "$COMPOSE_FILE"
docker compose up -d

A ordem é tudo: o pull vem antes de reescrever o ficheiro. Uma versão que não existe no registo — um erro de escrita, uma publicação que falhou — pára aqui, com o servidor exactamente como estava. Ao contrário, ficava um docker-compose.yml a apontar para uma imagem inexistente, que é a pior forma de descobrir um erro de escrita.

A falha mais comum neste passo não é a imagem: é o próprio servidor não conseguir resolver o registo. Quando o registo vive na mesma máquina, atrás do mesmo domínio público, o pedido sai para o DNS público e tenta voltar a entrar pelo router — e se o router não fizer hairpinning, o pull fica pendurado até ao timeout. A saída é o servidor resolver aquele nome para o endereço local.

3. Esperar pelo healthcheck, e não pelo arranque

Um contentor que arrancou não é um contentor que serve — é o assunto do artigo dos healthchecks. O que interessa aqui é o guião esperar pelo veredito em vez de assumir:

while ((ELAPSED < TIMEOUT_SEGUNDOS)); do
  STATUS=$(docker inspect -f '{{.State.Health.Status}}' "$CONTAINER")
  if [[ "$STATUS" == "healthy" ]]; then
    echo "Deploy concluido"
    exit 0
  fi
  sleep 3
  ELAPSED=$((ELAPSED + 3))
done

E há um parâmetro que decide se isto funciona ou se sabota tudo:

HEALTHCHECK --interval=30s --timeout=3s --start-period=25s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/healthz >/dev/null || exit 1

O --start-period=25s. Sem ele, o contentor é declarado doente enquanto ainda está a aplicar as migrações no arranque, e o guião reverte um deploy que estava perfeitamente bem. Um período de arranque curto a mais não dá um erro — dá um rollback inexplicável.

4. O rollback, e o que ele não reverte

echo "Rollback para $CURRENT"
sed -i "s|${IMAGE}:[^[:space:]\"]*|${IMAGE}:${CURRENT}|" "$COMPOSE_FILE"
docker compose up -d

Aqui está a parte que convém saber antes de confiar: o rollback repõe a imagem, não o esquema da base. As migrações são aplicadas no arranque, e voltar à imagem anterior deixa a base no estado novo.

Na prática isso divide as migrações em duas famílias. As que acrescentam — uma coluna nova com valor por omissão, uma tabela nova — são seguras: a imagem antiga não conhece a coluna e ignora-a. As que retiram ou mudam o tipo de algo que a imagem antiga usa não têm rollback nenhum, e não há guião que resolva isso. Essas fazem-se em dois deploys: primeiro acrescentar e passar a escrever nos dois sítios, só depois remover o antigo.

É uma disciplina chata, e é a diferença entre ter rollback e achar que se tem.

5. O que faltaria para não haver intervalo nenhum

Dois contentores ao mesmo tempo e o proxy a mudar de destino quando o novo ficar saudável. É fazível, e traz duas condições que não são de graça: a aplicação tem de tolerar duas instâncias a correr em simultâneo, e — no caso de quem aplica migrações no arranque — as duas vão tentar migrar ao mesmo tempo.

Para um blogue pessoal, o intervalo são segundos às três da manhã. Não vale a complexidade, e dizê-lo é mais útil do que montar uma infraestrutura de banco por hábito.

6. O pormenor sem glamour que evita a chamada de madrugada

logging:
  driver: json-file
  options:
    max-size: '10m'
    max-file: '3'

Sem isto, o registo do contentor cresce até acabar o disco — e um disco cheio não dá um erro honesto, dá o site em baixo com uma causa que não se parece nada com a causa. É a primeira das armadilhas do espaço em Linux, e custa quatro linhas.

O resumo

Etiqueta fixa para o rollback ser possível. pull antes de tocar na configuração. Esperar pelo healthcheck, com período de arranque. E saber que a base não volta atrás com a imagem.

São sessenta linhas de bash. O que elas dão não é um deploy sem intervalo — é um deploy que, quando corre mal, deixa o site de pé na versão anterior em vez de o deixar em baixo à espera de alguém.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.