Despliegue Blue-Green a un nivel básico

En este artículo, vamos a bash, ssh, docker y nginx organizar un despliegue sin interrupciones de una aplicación web. Despliegue azul-verde es una técnica que permite actualizar una aplicación de inmediato sin rechazar ninguna solicitud. Es una de las estrategias de implementación sin tiempo de inactividad y es más adecuada para aplicaciones con una instancia única, pero que tienen la posibilidad de cargar una segunda instancia preparada.

Supongamos que tienes una aplicación web que está siendo utilizada activamente por muchos clientes y no puede parar incluso por unos segundos. Y necesitas implementar una actualización de biblioteca, corregir un error o una nueva característica emocionante. En una situación normal, tendrías que detener la aplicación, reemplazarla y volver a iniciarla. En el caso de Docker, puedes reemplazar primero y luego reiniciar, pero aún así habrá un período en el que las solicitudes a la aplicación no serán procesadas, ya que generalmente se requiere algo de tiempo para la carga inicial de la aplicación. ¿Y si arranca pero resulta no estar operativo? Así que, vamos a resolver este problema con la menor cantidad de recursos y de la manera más elegante posible.

ADVERTENCIA: La mayor parte del artículo se presenta en un formato experimental: como un registro de sesión de consola. Espero que no sea demasiado difícil de seguir, y que este código se documente suficientemente por sí mismo. Para añadir atmósfera, imagina que no son solo fragmentos de código, sino papel de un telégrafo "de hierro".

Despliegue Blue-Green a un nivel básico

Técnicas interesantes que son difíciles de encontrar simplemente leyendo el código están descritas al inicio de cada sección. Si algo no está claro, busca y verifica en explainshell (afortunadamente, está funcionando de nuevo gracias al desbloqueo de Telegram). Lo que no se puede buscar, pregúntalo en los comentarios. Estaré encantado de añadir la sección correspondiente "Técnicas interesantes".

Comencemos.

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

El servicio

Crearemos un servicio de prueba y lo colocaremos en un contenedor.

Técnicas interesantes

  • cat < file-name (Documento aquí + Redirección I/O) es un método para crear un archivo de varias líneas con un solo comando. Todo lo que bash lea de /dev/stdin después de esta línea y hasta la línea EOF se escribirá en file-name.
  • wget -qO- URL (explainshell) imprime el documento recibido por HTTP en /dev/stdout (similar a curl URL).

Impresión

Estoy rompiendo intencionadamente el snippet para incluir el resaltado para Python. Al final habrá otro fragmento similar. Consideren que en estos lugares el papel fue cortado para ser enviado al departamento de resaltado (donde se coloreaba el código manualmente con marcadores), y luego estas piezas se pegaban de nuevo.

$ cat < uptimer.py
desde http.server importar BaseHTTPRequestHandler, HTTPServer
desde time importar monotonic

version_app = 1
nombre_app = f'Uptimer v{version_app}.0'
segundos_cargando = 15 - version_app * 5

clase Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        si self.path == '':
            try:
                t = monotonic() - inicio_servidor
                si t &lt; segundos_cargando:
                    self.send_error(503)
                más:
                    self.send_response(200)
                    self.send_header(&#039;Content-Type&#039;, &#039;text/html&#039;)
                    self.end_headers()
                    respuesta = f&#039;<h2>{nombre_app} está corriendo durante {t:3.1f} segundos.</h2>n'
                    self.wfile.write(respuesta.encode('utf-8'))
            except Exception:
                self.send_error(500)
        más:
            self.send_error(404)

httpd = HTTPServer(('', 8080), Handler)
inicio_servidor = monotonic()
print(f'{nombre_app} (carga en {segundos_cargando} 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 .
Sending build context to Docker daemon  39.42kB
Step 1\/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Step 2\/4 : EXPOSE 8080
 ---&gt; Using cache
 ---&gt; cf92d174c9d3
Step 3\/4 : COPY uptimer.py app.py
 ---&gt; a7fbb33d6b7e
Step 4\/4 : CMD [ "python", "-u", ".\/app.py" ]
 ---&gt; Running in 1906b4bd9fdf
Removing intermediate container 1906b4bd9fdf
 ---&gt; c1655b996fe8
Successfully built c1655b996fe8
Successfully tagged uptimer:latest

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

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                    NAMES
8f88c944b8bf        uptimer             "python -u .\/app.py"   3 seconds ago       Up 5 seconds        0.0.0.0:8080-&gt;8080\/tcp   uptimer

$ docker logs uptimer
Uptimer v1.0 (carga en 10 seg.) iniciado.

$ wget -qSO- http://localhost:8080
  HTTP/1.0 503 Servicio No Disponible
  Server: BaseHTTP/0.6 Python/3.8.3
  Fecha: Sáb, 22 Ago 2020 19:52:40 GMT
  Conexión: cerrar
  Content-Type: text/html;charset=utf-8
  Content-Length: 484

$ wget -qSO- http://localhost:8080
  HTTP/1.0 200 OK
  Server: BaseHTTP/0.6 Python/3.8.3
  Fecha: Sáb, 22 Ago 2020 19:52:45 GMT
  Content-Type: text/html
<h2>Uptimer v1.0 está en ejecución desde hace 15.4 segundos.</h2>

$ docker rm --force uptimer
uptimer

Proxy inverso

Para que nuestra aplicación tenga la capacidad de cambiar de forma imperceptible, es necesario que haya alguna entidad delante de ella que oculta su reemplazo. Esto puede ser un servidor web nginx en en modo de proxy inverso. El proxy inverso se coloca entre el cliente y la aplicación. Toma solicitudes de los clientes y las redirige a la aplicación, mientras que las respuestas de la aplicación se envían a los clientes.

La aplicación y el proxy inverso se pueden vincular dentro de Docker utilizando docker network. De esta manera, ni siquiera es necesario abrir un puerto para el contenedor con la aplicación en el sistema anfitrión, lo que permite aislar al máximo la aplicación de amenazas externas.

Si el proxy inverso va a estar en otro host, se deberá renunciar a docker network y vincular la aplicación al proxy inverso a través de la red del host, abriendo el puerto aplicación parámetro --publish, como en el primer lanzamiento y como en el proxy inverso.

El proxy inverso se ejecutará en el puerto 80, ya que es la entidad que debería escuchar el exterior. Si el puerto 80 está ocupado en su host de prueba, cambie el parámetro --publish 80:80 en --publish ANY_FREE_PORT:80.

Técnicas interesantes

Impresión

$ 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á en ejecución desde hace 11.5 segundos.</h2>

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

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
80695a822c19        nginx:alpine        "\/docker-entrypoint.…"   hace 27 segundos       En funcionamiento 25 segundos       0.0.0.0:80-&gt;80\/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u .\/app.py"     hace aproximadamente un minuto   En funcionamiento hace aproximadamente un 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: signal process started

$ wget -qSO- http://localhost
  HTTP/1.1 200 OK
  Server: nginx/1.19.0
  Date: Sat, 22 Aug 2020 19:56:24 GMT
  Content-Type: text/html
  Transfer-Encoding: chunked
  Connection: keep-alive
<h2>Uptimer v1.0 está en ejecución desde hace 104.1 segundos.</h2>

Despliegue sin interrupciones

Lanzaremos una nueva versión de la aplicación (con un aumento del rendimiento de inicio de dos veces) y trataremos de desplegarla sin interrupciones.

Técnicas interesantes

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Escribir texto my text en el archivo /my-file.txt dentro del contenedor my-container.
  • cat > /my-file.txt — Escribir el contenido de la entrada estándar en un archivo /dev/stdin.

Impresión

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

$ docker build --tag uptimer .
Enviando contexto de construcción al demonio de Docker  39.94kB
Paso 1/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Paso 2/4 : EXPOSE 8080
 ---&gt; Usando caché
 ---&gt; cf92d174c9d3
Paso 3/4 : COPY uptimer.py app.py
 ---&gt; 3eca6a51cb2d
Paso 4/4 : CMD [ "python", "-u", ".\/app.py" ]
 ---&gt; Ejecutando en 8f13c6d3d9e7
Eliminando contenedor intermedio 8f13c6d3d9e7
 ---&gt; 1d56897841ec
Construcción exitosa 1d56897841ec
Etiqueta exitosa uptimer:latest

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

$ docker logs uptimer_BLUE
Uptimer v2.0 (se carga en 5 seg.) iniciado.

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer_BLUE:8080
<h2>Uptimer v2.0 está en funcionamiento desde hace 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
servidor {
    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: señal de proceso iniciada

$ wget -qO- http://localhost
<h2>Uptimer v2.0 está en funcionamiento desde hace 63.4 segundos.</h2>

$ docker rm -f uptimer
uptimer

$ wget -qO- http://localhost
<h2>Uptimer v2.0 está en funcionamiento desde hace 84.8 segundos.</h2>

$ docker ps
ID DEL CONTENEDOR        IMAGEN               COMANDO                  CREADO              ESTADO              PUERTOS                NOMBRES
96932d4ca97a        uptimer             "python -u .\/app.py"     Hace aproximadamente un minuto   En funcionamiento Desde hace un minuto   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        "\/docker-entrypoint.…"   Hace 8 minutos        En funcionamiento desde hace 8 minutos        0.0.0.0:80-&gt;80/tcp   reverse-proxy

En esta etapa, la imagen se construye directamente en el servidor, lo que requiere que allí se encuentren los archivos fuente de la aplicación, además de sobrecargar el servidor con trabajo adicional. El siguiente paso será separar la construcción de la imagen en una máquina diferente (por ejemplo, en un sistema CI) para luego transferirla al servidor.

Transferencia de imágenes

Desafortunadamente, no tiene sentido transferir imágenes de localhost a localhost, así que esta sección solo se puede probar teniendo a mano dos hosts con Docker. En lo básico, se ve algo así:

$ ssh production-server docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

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

$ ssh production-server docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
uptimer             latest              1d56897841ec        hace 5 minutos       78.9MB

Comando docker save guarda los datos de la imagen en un archivo .tar, es decir, pesa aproximadamente 1.5 veces más de lo que podría pesar en formato comprimido. Así que comprimámoslo en nombre de la economía de tiempo y tráficos:

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

Y además, se puede monitorear el proceso de transferencia (aunque para esto se necesita una herramienta de terceros):

$ docker image save uptimer | gzip | pv | ssh production-server 'zcat | docker image load'
25.7MiB 0:01:01 [ 425KiB/s] [                       ]
Loaded image: uptimer:latest

Consejo: Si necesita un montón de parámetros para conectarse al servidor por SSH, es posible que no esté utilizando un archivo ~/.ssh/config.

La transferencia de imágenes a través de docker image save/load es el método más minimalista, pero no el único. Hay otros:

  1. Container Registry (estándar de la industria).
  2. Conectarse al daemon de docker del servidor desde otro host:
    1. Variable de entorno DOCKER_HOST.
    2. Parámetro de línea de comandos -H o --host la herramienta docker-compose.
    3. docker context

El segundo método (con tres variantes de implementación) está bien descrito en el artículo How to deploy on remote Docker hosts with docker-compose.

deploy.sh

Ahora uniremos todo lo que hicimos manualmente en un solo script. Comencemos con la función de nivel superior y luego veamos las otras que se utilizan en él.

Técnicas interesantes

  • ${parameter?err_msg} es uno de los hechizos de la magia bash (aka sustitución de parámetros). Si el parámetro no está definido, se muestra el err_msg y se sale con código 1.
  • docker --log-driver journald — por defecto, el controlador de registros de Docker es un archivo de texto sin ninguna rotación. Con este enfoque, los registros ocupan rápidamente todo el disco, por lo que para un entorno de producción es necesario cambiar el controlador por uno más inteligente.

Script de despliegue

deploy() {
    local usage_msg="Uso: ${FUNCNAME[0]} nombre_de_imagen"
    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 "Desplegando '$NEW' en lugar de '$OLD'..."
    docker run 
        --detach 
        --restart siempre 
        --log-driver journald 
        --name $NEW 
        --network web-gateway 
        $image_name || return 3
    echo "Contenedor iniciado. Comprobando salud..."
    for i in {1..20}
    do
        sleep 1
        if get-service-status $image_name $new_slot
        then
            echo "Nuevo servicio '$NEW' parece estar OK. Cambiando..."
            sleep 2  # Asegurarse de que el servicio esté listo
            set-active-slot $image_name $new_slot || return 4
            echo "¡El servicio '$NEW' está activo!"
            sleep 2  # Asegurarse de que todas las solicitudes fueron procesadas
            echo "Eliminando '$OLD'..."
            docker rm -f $OLD
            docker image prune -f
            echo "¡Despliegue exitoso!"
            return 0
        fi
        echo "El nuevo servicio '$NEW' aún no está listo. Esperando ($i)..."
    done
    echo "El nuevo servicio '$NEW' no se levantó, eliminándolo. Falló el despliegue T_T"
    docker rm -f $NEW
    return 5
}

Funciones utilizadas:

  • ensure-reverse-proxy — Verifica que el proxy inverso esté funcionando (útil para el primer despliegue)
  • get-active-slot service_name — Determina qué ranura está activa para el servicio dado (AZUL o VERDE)
  • get-service-status service_name deployment_slot — Verifica si el servicio está listo para procesar solicitudes entrantes
  • set-active-slot service_name deployment_slot — Cambia la configuración de nginx en el contenedor del proxy inverso

En orden:

asegurar-proxy-inverso() {
    está-contenedor-activo proxy-inverso && return 0
    echo "Desplegando proxy-inverso..."
    docker red crear puerta-web
    docker ejecutar 
        --desprender 
        --reiniciar siempre 
        --controlador-registro journald 
        --nombre proxy-inverso 
        --red puerta-web 
        --publicar 80:80 
        nginx:alpine || return 1
    docker exec --interactivo proxy-inverso sh -c "> /etc/nginx/conf.d/default.conf"
    docker exec proxy-inverso nginx -s recargar
}

está-contenedor-activo() {
    local contenedor=${1?"Uso: ${FUNCNAME[0]} nombre_contenedor"}

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

obtener-slot-activo() {
    local servicio=${1?"Uso: ${FUNCNAME[0]} nombre_servicio"}

    si está-contenedor-activo ${servicio}_AZUL && está-contenedor-activo ${servicio}_VERDE; entonces
        echo "¡Colisión detectada! Deteniendo ${servicio}_VERDE..."
        docker rm -f ${servicio}_VERDE
        return 0  # AZUL
    fi
    si está-contenedor-activo ${servicio}_AZUL && ! está-contenedor-activo ${servicio}_VERDE; entonces
        return 0  # AZUL
    fi
    si ! está-contenedor-activo ${servicio}_AZUL; entonces
        return 1  # VERDE
    fi
}

obtener-estado-servicio() {
    local msg_uso="Uso: ${FUNCNAME[0]} nombre_servicio slot_despliegue"
    local servicio=${1?msg_uso}
    local slot=${2?$msg_uso}

    caso $servicio en
        # Agrega rutas de verificación de estado específicas para tus servicios aquí
        *) local ruta_verificacion_estado=":8080/" ;;
    esac
    local direccion_verificacion_estado="http://${servicio}_${slot}${ruta_verificacion_estado}"
    echo "Solicitando '$direccion_verificacion_estado' dentro de la red docker 'puerta-web':"
    docker ejecutar --rm --red puerta-web alpine 
        wget --tiempo-espera=1 --silencio --respuesta-servidor $direccion_verificacion_estado
    return $?
}

establecer-slot-activo() {
    local msg_uso="Uso: ${FUNCNAME[0]} nombre_servicio slot_despliegue"
    local servicio=${1?$msg_uso}
    local slot=${2?$msg_uso}
    [ "$slot" == AZUL ] || [ "$slot" == VERDE ] || return 1

    obtener-configuracion-nginx $servicio $slot | docker exec --interactivo proxy-inverso sh -c "cat > /etc/nginx/conf.d/$servicio.conf"
    docker exec proxy-inverso nginx -t || return 2
    docker exec proxy-inverso nginx -s recargar
}

La función obtener-slot-activo requiere una pequeña explicación:

¿Por qué devuelve un número y no imprime una cadena?

Aún así, en la función llamada verificamos el resultado de su trabajo, y comprobar el código de salida con bash es mucho más fácil que con una cadena. Además, obtener una cadena de ella es muy fácil:
obtener-slot-activo servicio && echo AZUL || echo VERDE.

¿Son suficientes tres condiciones para distinguir todos los estados?

Despliegue Blue-Green a un nivel básico

Incluso con dos es suficiente; la última aquí es simplemente para completitud, para no escribir else.

Sólo queda indefinida la función que devuelve las configuraciones de nginx: obtener-configuracion-nginx nombre_servicio slot_despliegue. De manera similar a la verificación de estado, aquí se puede establecer cualquier configuración para cualquier servicio. De lo interesante, sólo cat <<- EOF, lo que permite eliminar todas las tabulaciones al principio. Sin embargo, el precio de un formato limpio es la mezcla de tabulaciones con espacios, lo que hoy se considera de muy mal gusto. Pero bash obliga las tabulaciones, y en la configuración de nginx también sería bueno tener un formato adecuado. En resumen, aquí la mezcla de tabulaciones con espacios parece ser realmente la mejor solución de las peores. Sin embargo, en el snippet a continuación no lo verá, ya que Habr 'hace bien' al cambiar todas las tabulaciones por 4 espacios, haciendo que EOF sea no válido. Aquí se nota.

Para no tener que repetir, les contaré sobre cat << 'EOF', que aparecerá más adelante. Si simplemente se escribe cat << EOF, dentro del heredoc se realiza la interpolación de la cadena (se revelan las variables ($foo), llamadas de comandos ($(bar)) etc.), pero si se rodea el símbolo de fin de documento con comillas simples, la interpolación se desactiva y el símbolo $ se imprime tal como es. Lo que se necesita para insertar un script dentro de otro script.

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

    local container_name=${service}_${slot}
    case $service in
        # Agregue configuraciones específicas de nginx para sus servicios aquí
        *) 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
}

Este es todo el script. Y aquí el gist con este script para descargarlo a través de wget o curl.

Ejecución de scripts parametrizados en un servidor remoto

Ha llegado el momento de conectarse al servidor de destino. Esta vez localhost será adecuado:

$ ssh-copy-id localhost
/usr/bin/ssh-copy-id: INFO: intentando iniciar sesión con la(s) nueva(s) clave(s), para filtrar cualquier clave que ya esté instalada
/usr/bin/ssh-copy-id: INFO: 1 clave(s) quedan por instalar -- si se le solicita, es para instalar las nuevas claves
la contraseña de himura@localhost:

Número de clave(s) añadida(s): 1

Ahora intente iniciar sesión en la máquina con: "ssh 'localhost'"
y verifique que solo se hayan añadido las clave(s) que deseaba.

Hemos escrito un script de despliegue que transfiere una imagen compilada previamente al servidor de destino y sustituye el contenedor del servicio de manera fluida, pero ¿cómo lo ejecutamos en una máquina remota? El script tiene argumentos, ya que es versátil y puede desplegar varios servicios bajo un solo reverse proxy (los archivos de configuración de nginx pueden resolver qué servicio se manejará por qué URL). No se puede almacenar el script en el servidor, ya que en ese caso no podremos actualizarlo automáticamente (para correcciones de errores y adición de nuevos servicios), y, en general, el estado es malo.

Solución 1: Almacenar el script en el servidor, pero copiarlo cada vez a través de scp. Luego conectarse a través de ssh y ejecutar el script con los argumentos necesarios.

Desventajas:

  • Dos acciones en lugar de una.
  • El lugar donde copias puede no existir, no tener acceso, o el script puede ejecutarse en el momento de la sustitución.
  • Es recomendable limpiar (eliminar el script) después de usarlo.
  • Ya son tres acciones.

Solución 2:

  • Mantener solo las definiciones de funciones en el script y no ejecutar nada.
  • Con sed Agregar al final la llamada a la función.
  • Enviar todo esto directamente a shh a través de pipe (|)

Pros:

  • Verdaderamente sin estado.
  • Sin entidades plantillas.
  • Sintiendo genial.

Pero hagámoslo sin Ansible. Sí, ya todo está inventado. Sí, es un ciclo. Miren qué bicicleta tan simple, elegante y 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 a localhost...
¡Genial! La función 'deploy' se está ejecutando en 'hut' con el argumento 'magic-porridge-pot'.

Sin embargo, no podemos estar seguros de que en el host remoto haya un bash adecuado, así que agregaremos una pequeña verificación al principio (esto en lugar de shellbang.):

if [ "$SHELL" != "/bin/bash" ]
then
    echo "El shell '$SHELL' no es compatible con 'deploy.sh'. Establezca un shell '/bin/bash' para '$USER@$HOSTNAME'."
    exit 1
fi

Y ahora todo es real:

$ 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 la imagen comprimida 'uptimer' a 'localhost' a través de ssh...
Imagen cargada: uptimer:latest
Conectando a 'localhost' a través de ssh para desplegar 'uptimer' de manera ininterrumpida...
Desplegando 'uptimer_GREEN' en lugar de 'uptimer_BLUE'...
06f5bc70e9c4f930e7b1f826ae2ca2f536023cc01e82c2b97b2c84d68048b18a
Contenedor iniciado. Comprobando el estado...
Solicitando 'http://uptimer_GREEN:8080/' dentro de la red docker 'web-gateway':
  HTTP/1.0 503 Servicio No Disponible
wget: el servidor devolvió error: HTTP/1.0 503 Servicio No Disponible
El nuevo servicio 'uptimer_GREEN' aún no está listo. Esperando (1)...
Solicitando 'http://uptimer_GREEN:8080/' dentro de la red docker 'web-gateway':
  HTTP/1.0 503 Servicio No Disponible
wget: el servidor devolvió error: HTTP/1.0 503 Servicio No Disponible
El nuevo servicio 'uptimer_GREEN' aún no está listo. Esperando (2)...
Solicitando 'http://uptimer_GREEN:8080/' dentro de la red docker 'web-gateway':
  HTTP/1.0 200 OK
  Servidor: BaseHTTP/0.6 Python/3.8.3
  Fecha: Sab, 22 Ago 2020 20:15:50 GMT
  Tipo de Contenido: text/html

El nuevo servicio 'uptimer_GREEN' parece estar OK. Cambiando entre versiones...
nginx: el archivo de configuración /etc/nginx/nginx.conf tiene una sintaxis correcta
nginx: la prueba del archivo de configuración /etc/nginx/nginx.conf fue exitosa
2020/08/22 20:15:54 [aviso] 97#97: proceso de señal iniciado
¡El servicio 'uptimer_GREEN' está activo!
Matando 'uptimer_BLUE'...
uptimer_BLUE
Espacio total reclamado: 0B
¡Despliegue exitoso!

Ahora puedes abrir http://localhost/ en el navegador, volver a ejecutar el despliegue y asegurarte de que se realiza sin inconvenientes actualizando la página de manera continua durante el despliegue.

No olvides limpiar después de trabajar :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

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster