Implantação Blue-Green simplificada

Neste artigo, vamos bash, ssh, docker e nginx organizar a implantação contínua de uma aplicação web. Implantação blue-green é uma técnica que permite atualizar uma aplicação instantaneamente, sem rejeitar nenhuma solicitação. É uma das estratégias de zero downtime deployment e é mais adequada para aplicações com uma única instância, mas com a possibilidade de carregar uma segunda instância, pronta para funcionar, ao lado.

Suponha que você tenha uma aplicação web que é utilizada ativamente por muitos clientes, e não pode ficar fora do ar nem por alguns segundos. E você precisa muito implantar uma atualização de biblioteca, corrigir um bug ou adicionar um novo recurso incrível. Em uma situação normal, seria necessário parar a aplicação, substituí-la e reiniciá-la. No caso do Docker, você pode primeiro substituir e, em seguida, reiniciar, mas ainda assim haverá um período em que as solicitações à aplicação não serão processadas, pois geralmente a aplicação leva um tempo para inicializar. E se ela iniciar, mas não funcionar? Vamos resolver essa tarefa com o mínimo de meios e da maneira mais elegante.

AVISO: A maior parte deste artigo é apresentada em um formato experimental — como um registro de sessão no console. Espero que isso não seja muito difícil de entender e que este código se documente suficientemente por si só. Para criar uma atmosfera, imagine que não se trata apenas de fragmentos de código, mas de papel de uma máquina de escrever ‘metálica’.

Implantação Blue-Green simplificada

Técnicas interessantes que são difíceis de encontrar apenas lendo o código estão descritas no início de cada seção. Se algo não estiver claro, pesquise e verifique em explainshell (felizmente, ele está funcionando novamente, devido à desbloqueação do Telegram). O que não pode ser encontrado, pergunte nos comentários. Farei questão de complementar a seção ‘Técnicas interessantes’.

Vamos começar.

$ mkdir blue-green-deployment && cd $_

Serviço

Vamos criar um serviço experimental e colocá-lo em um contêiner.

Técnicas interessantes

  • cat < file-name (Here Document + Redirecionamento I/O) — uma maneira de criar um arquivo múltiplo com um único comando. Tudo o que o bash ler a partir /dev/stdin desta linha e até a linha EOF será gravado em file-name.
  • wget -qO- URL (explainshell) — imprimir o documento obtido via HTTP para /dev/stdout (equivalente a curl URL).

Impressão

Eu quebro intencionalmente o snippet para incluir destaque para Python. No final, haverá mais um pedaço assim. Considere que nesses lugares o papel foi cortado para ser enviado ao departamento de destaque (onde o código foi colorido manualmente com marcadores), e depois esses pedaços foram colados de volta.

$ cat < uptimer.py
from http.server import BaseHTTPRequestHandler, HTTPServer
from time import monotonic

app_version = 1
app_name = f'Uptimer v{app_version}.0'
loading_seconds = 15 - app_version * 5

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '':
            try:
                t = monotonic() - server_start
                if t &lt; loading_seconds:
                    self.send_error(503)
                else:
                    self.send_response(200)
                    self.send_header(&#039;Content-Type&#039;, &#039;text/html&#039;)
                    self.end_headers()
                    response = f&#039;<h2>{app_name} está em execução por {t:3.1f} segundos.</h2>n'
                    self.wfile.write(response.encode('utf-8'))
            except Exception:
                self.send_error(500)
        else:
            self.send_error(404)

httpd = HTTPServer(('', 8080), Handler)
server_start = monotonic()
print(f'{app_name} (carregando em {loading_seconds} seg.) iniciado.')
httpd.serve_forever()
EOF

$ cat << EOF > Dockerfile
FROM python:alpine
EXPOSE 8080
COPY uptimer.py app.py
CMD [ "python", "-u", ".\/app.py" ]
EOF

$ docker build --tag uptimer .
Enviando contexto de construção para o daemon Docker  39.42kB
Etapa 1/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Etapa 2/4 : EXPOSE 8080
 ---&gt; Usando cache
 ---&gt; cf92d174c9d3
Etapa 3/4 : COPY uptimer.py app.py
 ---&gt; a7fbb33d6b7e
Etapa 4/4 : CMD [ "python", "-u", ".\/app.py" ]
 ---&gt; Executando em 1906b4bd9fdf
Removendo container intermediário 1906b4bd9fdf
 ---&gt; c1655b996fe8
Construído com sucesso c1655b996fe8
Tagueado com sucesso uptimer:latest

$ docker run --rm --detach --name uptimer --publish 8080:8080 uptimer
8f88c944b8bf78974a5727070a94c76aa0b9bb2b3ecf6324b784e782614b2fbf

$ docker ps
ID DO CONTÊINER        IMAGEM               COMANDO                CRIADO             STATUS              PORTAS                    NOMES
8f88c944b8bf        uptimer             "python -u .\/app.py"   há 3 segundos       Em funcionamento há 5 segundos        0.0.0.0:8080-&gt;8080\/tcp   uptimer

$ docker logs uptimer
Uptimer v1.0 (carrega em 10 seg.) iniciado.

$ wget -qSO- http:\/\/localhost:8080
  HTTP\/1.0 503 Serviço Indisponível
  Servidor: BaseHTTP\/0.6 Python\/3.8.3
  Data: Sáb, 22 Ago 2020 19:52:40 GMT
  Conexão: fechar
  Content-Type: text\/html;charset=utf-8
  Content-Length: 484

$ wget -qSO- http:\/\/localhost:8080
  HTTP\/1.0 200 OK
  Servidor: BaseHTTP\/0.6 Python\/3.8.3
  Data: Sáb, 22 Ago 2020 19:52:45 GMT
  Content-Type: text\/html
<h2>Uptimer v1.0 está em execução há 15.4 segundos.</h2>

$ docker rm --force uptimer
uptimer

Proxy reverso

Para que nosso aplicativo tenha a capacidade de mudar discretamente, é necessário que haja alguma entidade na frente dele que esconda sua troca. Isso pode ser um servidor web nginx em no modo proxy reverso. O proxy reverso é instalado entre o cliente e o aplicativo. Ele aceita solicitações dos clientes e as redireciona para o aplicativo, enquanto as respostas do aplicativo são enviadas de volta aos clientes.

O aplicativo e o proxy reverso podem ser conectados dentro do Docker usando docker network. Assim, o contêiner com o aplicativo pode nem precisar de uma porta exposta no sistema host, o que permite isolar o aplicativo ao máximo das ameaças externas.

Se o proxy reverso estiver em um host diferente, será necessário abrir mão da docker network e conectar o aplicativo ao proxy reverso através da rede do host, expondo a porta da aplicação parâmetro --publish, assim como na primeira execução e no proxy reverso.

O proxy reverso será iniciado na porta 80, pois essa é exatamente a entidade que deve escutar o exterior. Se a porta 80 estiver ocupada em seu host de teste, mude o parâmetro --publish 80:80 para --publish ANY_FREE_PORT:80.

Técnicas interessantes

Impressão

$ docker network create web-gateway
5dba128fb3b255b02ac012ded1906b7b4970b728fb7db3dbbeccc9a77a5dd7bd

$ docker run --detach --rm --name uptimer --network web-gateway uptimer
a1105f1b583dead9415e99864718cc807cc1db1c763870f40ea38bc026e2d67f

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer:8080
<h2>Uptimer v1.0 está em execução há 11,5 segundos.</h2>

$ docker run --detach --publish 80:80 --network web-gateway --name reverse-proxy nginx:alpine
80695a822c19051260c66bf60605dcb4ea66802c754037704968bc42527bf120

$ docker ps
ID DO CONTÊINER        IMAGEM               COMANDO                  CRIADO              STATUS              PORTAS                NOME
80695a822c19        nginx:alpine        "\/docker-entrypoint.…"   há 27 segundos       Em execução há 25 segundos       0.0.0.0:80-&gt;80/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u .\/app.py"     há cerca de um minuto   Em execução há cerca de um minuto   8080/tcp             uptimer

$ cat << EOF > uptimer.conf
server {
    listen 80;
    location / {
        proxy_pass http://uptimer:8080;
    }
}
EOF

$ docker cp ./uptimer.conf reverse-proxy:/etc/nginx/conf.d/default.conf

$ docker exec reverse-proxy nginx -s reload
2020/06/23 20:51:03 [notice] 31#31: processo de sinal iniciado

$ wget -qSO- http://localhost
  HTTP/1.1 200 OK
  Servidor: nginx/1.19.0
  Data: Sáb, 22 Ago 2020 19:56:24 GMT
  Content-Type: text/html
  Transfer-Encoding: chunked
  Connection: keep-alive
<h2>Uptimer v1.0 está em execução há 104,1 segundos.</h2>

Implantação sem interrupções

Vou implantar uma nova versão do aplicativo (com um aumento de desempenho de inicialização de duas vezes) e tentarei implantá-la sem interrupções.

Técnicas interessantes

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Gravar texto my text . Isso facilitará a configuração e o uso do armazenamento HashiCorp com o GitLab. /my-file.txt dentro do contêiner my-container.
  • cat > /my-file.txt — Gravar no arquivo o conteúdo da entrada padrão /dev/stdin.

Impressão

$ sed -i "s/app_version = 1/app_version = 2/" uptimer.py

$ docker build --tag uptimer .
Enviando contexto de construção para o daemon do Docker  39.94kB
Etapa 1/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Etapa 2/4 : EXPOSE 8080
 ---&gt; Usando cache
 ---&gt; cf92d174c9d3
Etapa 3/4 : COPY uptimer.py app.py
 ---&gt; 3eca6a51cb2d
Etapa 4/4 : CMD [ "python", "-u", "./app.py" ]
 ---&gt; Executando em 8f13c6d3d9e7
Removendo o container intermediário 8f13c6d3d9e7
 ---&gt; 1d56897841ec
Construído com sucesso 1d56897841ec
Marcado com sucesso uptimer:latest

$ docker run --detach --rm --name uptimer_BLUE --network web-gateway uptimer
96932d4ca97a25b1b42d1b5f0ede993b43f95fac3c064262c5c527e16c119e02

$ docker logs uptimer_BLUE
Uptimer v2.0 (carrega em 5 seg.) iniciado.

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer_BLUE:8080
<h2>Uptimer v2.0 está rodando há 23.9 segundos.</h2>

$ sed s/uptimer/uptimer_BLUE/ uptimer.conf | docker exec --interactive reverse-proxy sh -c 'cat &gt; /etc/nginx/conf.d/default.conf'

$ docker exec reverse-proxy cat /etc/nginx/conf.d/default.conf
server {
    listen 80;
    location / {
        proxy_pass http://uptimer_BLUE:8080;
    }
}

$ docker exec reverse-proxy nginx -s reload
2020/06/25 21:22:23 [notice] 68#68: sinal do processo iniciado

$ wget -qO- http://localhost
<h2>Uptimer v2.0 está rodando há 63.4 segundos.</h2>

$ docker rm -f uptimer
uptimer

$ wget -qO- http://localhost
<h2>Uptimer v2.0 está rodando há 84.8 segundos.</h2>

$ docker ps
ID DO CONTÊINER        IMAGEM               COMANDO                  CRIADO              STATUS              PORTAS                NOMES
96932d4ca97a        uptimer             "python -u ./app.py"     Há cerca de um minuto   Em funcionamento Há cerca de um minuto   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        "docker-entrypoint.…"   8 minutos atrás        Em funcionamento 8 minutos        0.0.0.0:80-&gt;80/tcp   reverse-proxy

Nesta etapa, a imagem é construída diretamente no servidor, o que requer a presença do código-fonte do aplicativo lá, além de sobrecarregar o servidor com trabalho extra. O próximo passo será a construção da imagem em uma máquina separada (por exemplo, em um sistema CI), seguida pela transmissão dela para o servidor.

Transferência de imagens

Infelizmente, não faz sentido transferir imagens de localhost para localhost, então esta seção só pode ser testada tendo em mãos dois hosts com Docker. No básico, isso se apresenta mais ou menos assim:

$ ssh production-server docker image ls
REPOSITÓRIO          TAG                 ID DA IMAGEM            CRIADO             TAMANHO

$ docker image save uptimer | ssh production-server 'docker image load'
Imagem carregada: uptimer:latest

$ ssh production-server docker image ls
REPOSITÓRIO          TAG                 ID DA IMAGEM            CRIADO             TAMANHO
uptimer             latest              1d56897841ec        há 5 minutos       78.9MB

Time docker save salva os dados da imagem em um arquivo .tar, ou seja, ele pesa aproximadamente 1,5 vezes mais do que poderia pesar em formato comprimido. Então, vamos compactá-lo em nome da economia de tempo e tráfego:

$ docker image save uptimer | gzip | ssh production-server 'zcat | docker image load'
Imagem carregada: uptimer:latest

Além disso, é possível acompanhar o processo de transferência (embora seja necessária uma ferramenta externa para isso):

$ docker image save uptimer | gzip | pv | ssh production-server 'zcat | docker image load'
25,7MiB 0:01:01 [ 425KiB/s] [                       ]
Imagem carregada: uptimer:latest

Dica: Se você precisar de muitos parâmetros para se conectar ao servidor via SSH, talvez você não esteja utilizando o arquivo ~/.ssh/config.

Transferência da imagem via docker image save/load — é o método mais minimalista, mas não o único. Existem outros:

  1. Container Registry (padrão da indústria).
  2. Conectar ao daemon Docker do servidor a partir de outro host:
    1. Variável de ambiente DOCKER_HOST.
    2. Parâmetro de linha de comando -H ou --host ferramenta docker-compose.
    3. docker context

A segunda abordagem (com três opções de implementação) está bem descrita no artigo How to deploy on remote Docker hosts with docker-compose.

deploy.sh

Agora vamos juntar tudo o que fizemos manualmente em um único script. Começaremos pela função principal e, em seguida, veremos as outras que são usadas nela.

Técnicas interessantes

  • ${parameter?err_msg} — um dos encantamentos da magia bash (aka substituição de parâmetros). Se parameter não estiver definido, exibir err_msg e sair com o código 1.
  • docker --log-driver journald — por padrão, o driver de log do Docker é um arquivo de texto sem qualquer rotação. Com essa abordagem, os logs rapidamente ocupam todo o disco, portanto, para um ambiente de produção, é necessário alterar o driver para um mais inteligente.

Script de implantação

deploy() {
    local usage_msg="Uso: ${FUNCNAME[0]} nome_da_imagem"
    local image_name=${1?$usage_msg}

    ensure-reverse-proxy || return 2
    if get-active-slot $image_name
    then
        local OLD=${image_name}_AZUL
        local new_slot=VERDE
    else
        local OLD=${image_name}_VERDE
        local new_slot=AZUL
    fi
    local NEW=${image_name}_${new_slot}
    echo "Implantando '$NEW' no lugar de '$OLD'..."
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name $NEW 
        --network web-gateway 
        $image_name || return 3
    echo "Container iniciado. Verificando saúde..."
    for i in {1..20}
    do
        sleep 1
        if get-service-status $image_name $new_slot
        then
            echo "Novo serviço '$NEW' parece estar OK. Alternando cabeçotes..."
            sleep 2  # Garantir que o serviço esteja pronto
            set-active-slot $image_name $new_slot || return 4
            echo "Serviço '$NEW' está ativo!"
            sleep 2  # Garantir que todas as solicitações foram processadas
            echo "Matando '$OLD'..."
            docker rm -f $OLD
            docker image prune -f
            echo "Implantação bem-sucedida!"
            return 0
        fi
        echo "Novo serviço '$NEW' ainda não está pronto. Aguardando ($i)..."
    done
    echo "Novo serviço '$NEW' não foi iniciado, matando-o. Falha ao implantar T_T"
    docker rm -f $NEW
    return 5
}

Funções utilizadas:

  • ensure-reverse-proxy — Garante que o reverse proxy está funcionando (útil para a primeira implantação)
  • get-active-slot nome_do_serviço — Determina qual slot está ativo atualmente para o serviço especificado (AZUL ou VERDE)
  • get-service-status nome_do_serviço slot_de_implantação — Determina se o serviço está pronto para receber solicitações
  • set-active-slot nome_do_serviço slot_de_implantação — Altera a configuração do nginx no contêiner do reverse proxy

Na ordem:

ensure-reverse-proxy() {
    is-container-up reverse-proxy && return 0
    echo "Implementando reverse-proxy..."
    docker network create web-gateway
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name reverse-proxy 
        --network web-gateway 
        --publish 80:80 
        nginx:alpine || return 1
    docker exec --interactive reverse-proxy sh -c "> /etc/nginx/conf.d/default.conf"
    docker exec reverse-proxy nginx -s reload
}

is-container-up() {
    local container=${1?"Uso: ${FUNCNAME[0]} nome_do_container"}

    [ -n "$(docker ps -f name=${container} -q)" ]
    return $?
}

get-active-slot() {
    local service=${1?"Uso: ${FUNCNAME[0]} nome_do_serviço"}

    if is-container-up ${service}_BLUE && is-container-up ${service}_GREEN; then
        echo "Colisão detectada! Parando ${service}_GREEN..."
        docker rm -f ${service}_GREEN
        return 0  # BLUE
    fi
    if is-container-up ${service}_BLUE && ! is-container-up ${service}_GREEN; then
        return 0  # BLUE
    fi
    if ! is-container-up ${service}_BLUE; then
        return 1  # GREEN
    fi
}

get-service-status() {
    local usage_msg="Uso: ${FUNCNAME[0]} nome_do_serviço slot_de_deploy"
    local service=${1?usage_msg}
    local slot=${2?$usage_msg}

    case $service in
        # Adicione caminhos específicos de healthcheck para seus serviços aqui
        *) local health_check_port_path=":8080/" ;;
    esac
    local health_check_address="http://${service}_${slot}${health_check_port_path}"
    echo "Solicitando '$health_check_address' dentro da rede docker 'web-gateway':"
    docker run --rm --network web-gateway alpine 
        wget --timeout=1 --quiet --server-response $health_check_address
    return $?
}

set-active-slot() {
    local usage_msg="Uso: ${FUNCNAME[0]} nome_do_serviço slot_de_deploy"
    local service=${1?$usage_msg}
    local slot=${2?$usage_msg}
    [ "$slot" == BLUE ] || [ "$slot" == GREEN ] || return 1

    get-nginx-config $service $slot | docker exec --interactive reverse-proxy sh -c "cat > /etc/nginx/conf.d/$service.conf"
    docker exec reverse-proxy nginx -t || return 2
    docker exec reverse-proxy nginx -s reload
}

Função get-active-slot exige algumas explicações:

Por que retorna um número em vez de imprimir uma string?

Ainda assim, na função chamadora estamos checando o resultado do seu trabalho, e verificar o código de saída com bash é muito mais fácil do que uma string. Além disso, obter uma string dela é muito simples:
get-active-slot service && echo BLUE || echo GREEN.

São três condições suficientes para distinguir todos os estados?

Implantação Blue-Green simplificada

Duas são suficientes, a última aqui é apenas para completar, para não ter que escrever else.

Apenas a função que retorna as configurações do nginx permanece indefinida: get-nginx-config nome_do_serviço slot_de_deploy. Por analogia com o healthcheck, aqui é possível definir qualquer configuração para qualquer serviço. O interessante é apenas cat <<- EOF, o que permite remover todas as guias do início. No entanto, o preço de uma formatação adequada é a mistura de guias com espaços, o que hoje é considerado um grande mau jeito. Mas o bash força guias, e no config do nginx também seria bom ter uma formatação adequada. Em resumo, aqui a mistura de guias com espaços parece realmente ser a melhor solução dentre as opções ruins. No entanto, no snippet abaixo você não verá isso, pois o habr "faz bem", trocando todas as guias por 4 espaços e tornando o EOF inválido. Aqui está evidente.

Para não fazer isso em vão, vou falar sobre cat << 'EOF', que ainda aparecerá mais adiante. Se escrevesse apenas cat << EOF, dentro do heredoc ocorre a interpolação da string (variáveis são expandidas ($foo), chamadas de comandos ($(bar)) etc.), mas se você colocar o sinal de fim do documento entre aspas simples, a interpolação é desativada e o símbolo $ é exibido tal como é. Isso é necessário para inserir um script dentro de outro script.

get-nginx-config() {
    local usage_msg="Uso: ${FUNCNAME[0]} nome_do_serviço slot_de_implantaçã"
    local service=${1?$usage_msg}
    local slot=${2?$usage_msg}
    [ "$slot" == BLUE ] || [ "$slot" == GREEN ] || return 1

    local container_name=${service}_${slot}
    case $service in
        # Adicione configurações específicas do nginx para seus serviços aqui
        *) nginx-config-simple-service $container_name:8080 ;;
    esac
}

nginx-config-simple-service() {
    local usage_msg="Uso: ${FUNCNAME[0]} proxy_pass"
    local proxy_pass=${1?$usage_msg}

cat << EOF
server {
    listen 80;
    location / {
        proxy_pass http://$proxy_pass;
    }
}
EOF
}

Esse é o script completo. E aqui está o gist com este script para download via wget ou curl.

Execução de scripts parametrizados em um servidor remoto

É hora de se conectar ao servidor de destino. Desta vez localhost funcionará bem:

$ ssh-copy-id localhost
/usr/bin/ssh-copy-id: INFO: tentando logar com a(s) nova(s) chave(s), para filtrar qualquer uma que já esteja instalada
/usr/bin/ssh-copy-id: INFO: 1 chave(s) permanece(m) para ser(em) instalada(s) -- se você for solicitado agora, é para instalar as novas chaves
senha de himura@localhost:

Número de chave(s) adicionada(s): 1

Agora tente fazer login na máquina, com: "ssh 'localhost'"
e verifique se apenas a(s) chave(s) que você queria foram adicionadas.

Criamos um script de implantação que transfere uma imagem previamente compilada para o servidor de destino e substitui o container do serviço sem interrupções, mas como executá-lo em uma máquina remota? O script tem argumentos, pois é versátil e pode implantar vários serviços sob um único reverse proxy (as configurações do nginx podem resolver qual serviço aparecerá em qual URL). O script não pode ser armazenado no servidor, pois assim não poderemos atualizá-lo automaticamente (para correção de bugs e adição de novos serviços), e, de um modo geral, estado = mal.

Solução 1: Armazenar o script no servidor, mas copiá-lo toda vez através de scp. Em seguida, conectar-se via ssh e executar o script com os argumentos necessários.

Pontos negativos:

  • Duas ações em vez de uma
  • O local para onde você copia pode não existir, ou você pode não ter acesso, ou o script pode ser executado no momento em que a substituição ocorre.
  • É desejável limpar o que sobrou (excluir o script).
  • Já são três ações.

Solução 2:

  • O script deve conter apenas definições de funções e não executar nada
  • Com sed adicionar uma chamada de função no final
  • Enviar tudo isso diretamente para o shh através de um pipe (|)

Acesso direto aos sistemas de cabos transpacíficos. Ping mínimo para a China, Japão, Austrália e costa oeste dos EUA.

  • Verdadeiramente sem estado
  • Sem entidades boilerplate
  • Sentindo-se bem

Vamos deixar isso de lado e evitar o Ansible. Sim, tudo já foi inventado. Sim, bicicleta. Veja só, que bicicleta simples, elegante e minimalista:

$ cat < deploy.sh
#!/bin/bash

usage_msg="Usage: $0 ssh_address local_image_tag"
ssh_address=${1?$usage_msg}
image_name=${2?$usage_msg}

echo "Connecting to '$ssh_address' via ssh to seamlessly deploy '$image_name'..."
( sed "$a deploy $image_name" | ssh -T $ssh_address ) << 'END_OF_SCRIPT'
deploy() {
    echo "Yay! The '${FUNCNAME[0]}' function is executing on '$(hostname)' with argument '$1'"
}
END_OF_SCRIPT
EOF

$ chmod +x deploy.sh

$ ./deploy.sh localhost magic-porridge-pot
Conectando ao localhost...
Oba! A função 'deploy' está sendo executada em 'hut' com o argumento 'magic-porridge-pot'

No entanto, não podemos ter certeza de que o host remoto tem um bash adequado, então vamos adicionar uma verificação no início (isso é em vez de shellbang):

if [ "$SHELL" != "/bin/bash" ]
then
    echo "O shell '$SHELL' não é suportado pelo 'deploy.sh'. Defina um shell '/bin/bash' para '$USER@$HOSTNAME'."
    exit 1
fi

E agora tudo de verdade:

$ docker exec reverse-proxy rm /etc/nginx/conf.d/default.conf

$ wget -qO deploy.sh https://git.io/JUURc

$ chmod +x deploy.sh

$ ./deploy.sh localhost uptimer
Enviando imagem comprimida 'uptimer' para 'localhost' via ssh...
Imagem carregada: uptimer:latest
Conectando a 'localhost' via ssh para implantar 'uptimer' de forma contínua...
Implantando 'uptimer_GREEN' no lugar de 'uptimer_BLUE'...
06f5bc70e9c4f930e7b1f826ae2ca2f536023cc01e82c2b97b2c84d68048b18a
Container iniciado. Verificando saúde...
Solicitando 'http://uptimer_GREEN:8080/' na rede docker 'web-gateway':
  HTTP/1.0 503 Serviço Indisponível
wget: servidor retornou erro: HTTP/1.0 503 Serviço Indisponível
Novo serviço 'uptimer_GREEN' ainda não está pronto. Aguardando (1)...
Solicitando 'http://uptimer_GREEN:8080/' na rede docker 'web-gateway':
  HTTP/1.0 503 Serviço Indisponível
wget: servidor retornou erro: HTTP/1.0 503 Serviço Indisponível
Novo serviço 'uptimer_GREEN' ainda não está pronto. Aguardando (2)...
Solicitando 'http://uptimer_GREEN:8080/' na rede docker 'web-gateway':
  HTTP/1.0 200 OK
  Servidor: BaseHTTP/0.6 Python/3.8.3
  Data: Sáb, 22 Ago 2020 20:15:50 GMT
  Tipo de Conteúdo: text/html

Novo serviço 'uptimer_GREEN' parece OK. Mudando cabeçotes...
nginx: o arquivo de configuração /etc/nginx/nginx.conf está ok
nginx: teste do arquivo de configuração /etc/nginx/nginx.conf foi bem-sucedido
2020/08/22 20:15:54 [aviso] 97#97: sinal de processo iniciado
O serviço 'uptimer_GREEN' está ativo!
Matando 'uptimer_BLUE'...
uptimer_BLUE
Espaço total recuperado: 0B
Implantação bem-sucedida!

Agora você pode abrir http://localhost/ no navegador, executar a implantação mais uma vez e garantir que ela ocorra sem interrupções atualizando a página rapidamente durante a execução.

Não se esqueça de limpar após o trabalho :3

$ docker rm -f uptimer_GREEN reverse-proxy 
uptimer_GREEN
reverse-proxy

$ docker network rm web-gateway 
web-gateway

$ cd ..

$ rm -r blue-green-deployment

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster