Postmortem 13/04/2020: nginx-proxy & novo site do IMEsec

Métricas

  • Tempo fora do ar: 31 minutos
  • Serviços afetados: O site do imesec (https://imesec.ime.usp.br).
  • O quão foi afetado?: Queda total

O que aconteceu

Estávamos felizes em colocar o novo site em produção. No momento, estávamos em uma infra um pouco bamba: todos os containers estavam rodando em um docker-compose na torradeira, nosso servidor com 4 CPU/8 GB RAM.

Fizemos uma simples mudança no nosso ambiente de deploy. Apenas duas linhas que causaram o problema todo. Estava assim antes:

  nginx-website:
    image: richarvey/nginx-php-fpm:latest
    deploy:
      replicas: 2
      restart_policy:
        condition: any
        delay: "0"
    environment:
      GIT_EMAIL: imesec@ime.usp.br
      GIT_NAME: imesec
      GIT_REPO: https://github.com/IMEsec-USP/website.git
      VIRTUAL_HOST: imesec.ime.usp.br
      LETSENCRYPT_HOST: imesec.ime.usp.br
      WEBROOT: /var/www/html/website/

E depois da mudança:

  nginx-website:
    image: imesec/website # mudança aqui!
    deploy:
      replicas: 2
      restart_policy:
        condition: any
        delay: "0"
    environment:
      GIT_EMAIL: imesec@ime.usp.br
      GIT_NAME: imesec
      GIT_REPO: https://github.com/IMEsec-USP/website.git
      VIRTUAL_HOST: imesec.ime.usp.br
      LETSENCRYPT_HOST: imesec.ime.usp.br
      # Tiramos a variável webroot, já que não precisariamos mais dela.

Após a aplicação dessa mudança, a ada logo apitou nos seus logs:

[2020-04-13 15:33:14.018261] IMEsec Website returned 502
[2020-04-13 15:35:14.726066] IMEsec Website returned 502
[2020-04-13 15:37:18.408670] IMEsec Website returned 502
[2020-04-13 15:37:18.408754] IMEsec Website was considered HealthStatus.DOWN
[2020-04-13 15:37:18.408830] [TelegramHandler] broadcasting message in conversations [131845033, -1001284501077, 211525815]

O site estava fora, e a ada apitou sobre: nota: o log está em GMT (UTC) e o horário da mensagem está em GMT+1 (British Summer Time).

O razgrizone e o r0zbot foram tentar resolver a situação.

processo de resolução

0. O que sabíamos
  • Nossa infraestrutura desejada no momento era essa:
  • Todos esses serviços estavam declarados na stack imesec-full-stack.yml declarada aqui.
  • O Dockerfile da imagem nova era esse.
  • Tudo descrito anteriormente.
1. desescalando o problema

Primeira ideia: Rollback, depois pense. Ou seja, primeiro tente voltar para a versão anterior e resolver a crise imediata. Só depois, com tudo estável, resolveríamos o problema.

Fomos o que tentamos. Mas a situação não normalizou. O website voltou com um 404:

Nessa hora, paramos pra pensar. Em uma avaliação rápida, achamos que resolver o problema e colocar o site novo no ar seria ainda mais rápido.

2. Analisando o ponto de falha

Já que conhecíamos a infraestrutura que estávamos mexendo, a primeira coisa foi tentar identificar o ponto de falha nela. Afinal, não sabíamos se o problema estava dentro da nova imagem imesec/website (logo do serviço nginx-website no gráfico) ou se era a comunicação entre a proxy de borda e o serviço (a seta que vai do nginx-proxy até o nginx-website).

De dentro do servidor, tentamos fazer o mesmo fluxo que um navegador faria:

curl -k -H 'Host: imesec.ime.usp.br' https://localhost

Isso nos devolveu o mesmo problema: 502 Bad Gateway.

Tentamos então abrir uma porta do servidor e conectar nela diretamente, sem passar pelo proxy de borda. Fizemos isso com essa configuração:

  nginx-website:
    image: imesec/website
    deploy:
    	# ...
    environment:
    	# ...
    ports:
        - "9001:5000"

Ou seja, a porta do servidor 9001 redirecionaria para a porta 5000 dentro do container. Fizemos isso pois a porta 5000 é a que está exposta na imagem.

Fizemos isso, e o site ficou visível para todos.

O problema estava então entre a conexão entre a proxy de borda e o serviço. Tentamos então acessar o site normalmente algumas vezes pra que isso ficasse óbvio nos logs. E ficou mesmo. Apareceu isso nos logs:

nginx.1    | imesec.ime.usp.br 172.19.0.1 - - [13/Apr/2020:16:01:39 +0000] "GET / HTTP/2.0" 502 559 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.118 Safari/537.36"
nginx.1    | 2020/04/13 16:01:44 [error] 58#58: *44 connect() failed (111: Connection refused) while connecting to upstream, client: 172.19.0.1, server: imesec.ime.usp.br, request: "HEAD / HTTP/2.0", upstream: "http://172.19.0.25:80/", host: "imesec.ime.usp.br"

Tomamos atenção especial ao fato de não conseguir se conectar em http://172.19.0.25:80/. A porta estava como porta 80 e não 5000!

tentamos fazer os dois comandos:

curl -I HEAD http://172.19.0.25:80/
curl -I HEAD http://172.19.0.25:5000/

O de cima não funcionou (ele deu Connection refused), mas o segundo funcionou. Era o que esperávamos, afinal, nosso serviço está configurado pra rodar na porta 5000.

Tinhamos então que configurar a proxy pra acessar o serviço na porta 5000. Isso foi feito com uma variável de ambiente:

  nginx-website:
    # ...
    environment:
        VIRTUAL_PORT: 5000

Ao restartarmos o container do website, tudo voltou a funcionar normalmente.

Descobrindo a causa raíz

Depois de consertarmos da forma mais rápida possível, demos uma olhada mais a fundo no que tinha acontecido.

A imagem que usamos na proxy de borda utiliza o manifesto dos outros containers para automagicamente se configurar. Ela faz isso analisando as variáveis de ambiente e dockerfiles dos outros containers.

A imagem do nginx que usamos como base do nosso serviço também exporta a porta 80. Logo, nosso proxy não conseguiu identificar automaticamente qual delas teria que usar, e como fallback usou a primeira porta exportada: a porta 80, onde não tinha nada rodando.

Follow-ups

O follow up foi a resolução do problema: adicionar a porta 5000 no manifesto da nossa stack.

Nós também entendemos o porque do rollback não funcionar: a imagem antiga (richarvey/nginx-php-fpm) usa a variável de ambiente GIT_REPO: https://github.com/IMEsec-USP/website.git para clonar o repositório antes de rodar. Como a master do nosso repositório não tinha mais as pastas que ele esperava, ele não funcionou.

Uma lição aprendida é: quando possível, não usar imagens que se constroem em runtime e não em tempo de build. Uma imagem, quando funciona da primeira vez, é esperada a funcionar em todas as vezes seguintes. Como mudamos o ambiente em volta do novo container da imagem richarvey/nginx-php-fpm, ele parou de funcionar. Se tivéssemos construído uma imagem estática com o nosso site antes (e é o que temos agora), isso não teria acontecido.