Introducción

Como hemos dicho en anteriores artículos de nuestra serie Hola Docker, ejecutar manualmente procesos de builds, lanzar baterías de test y desplegar puede llegar a ser una pesadilla y no estaríamos a salvo de numerosos erróres.

  • Los errores humanos suceden con mucha facilidad.
  • Cuando los proyectos crecen, los procesos de build tienden a complicarse.
  • Lidiar con diferentes versiones entre entornos no es algo sencillo.
  • Cambiar de versiones no es un proceso fácil en muchos casos.

¿Y si...?:

  • Los procesos de build pudieran automatizarse, incluyendo indicadores que nos informen sobre si el proceso se ha completado (por ejemplo, ejecutando baterías de test).
  • Los procesos de build pudieran arrancar cuando ocurriera un merge para una rama concreta, o se realizará un push al repositorio remoto.
  • Cada proceso de build generase una imagen de Docker, y de este modo no nos tendríamos que preocupar por configuraciones en el servidor, o no tener instaladas las versiones exactas de software.
  • No ocupáramos demasiado espacio en disco ya que comenzaríamos desde una imagen base de Docker que tuviera el sistema operativo y software preinstalado.
  • Pudiéramos subirlo a un cloud hub registry, permitiendo ser consumido desde cualquier proveedor local o de cloud (Amazon, Azure, Google Cloud...)
  • Pudiéramos intercambiar fácilmente distintas versiones de build.

Todos estos son los beneficios que podemos obtener al configurar un servidor de CI/CD con GitHub Actions (CI proviene de Continuos Integration, CD significa Continuos Delivery) y mezclarlo con la tecnología de contenedores Docker.

Este es el tercer artículo de la serie Hola Docker, en estos enlaces tienes disponible la primera y segunda publicación.

TL; DR;

En este artículo usaremos GitHub Actions para activar automáticamente los siguientes procesos en cada merge to master o pull request:

  • Levantaremos una instancia linux + nodejs limpia.
  • Descargaremos el código fuente del repositorio.
  • Instalaremos las dependencias necesarias del proyecto.
  • Ejecutaremos la batería de test asociados.
  • Generaremos la imagen de docker, incluyendo la build de producción.
  • Etiquetaremos y publicaremos la imagen generada en el Docker Hub Registry.

Esta configuración la usaremos tanto para el proyecto de Front End como para el proyecto de Back End.

build-process-summary.jpg

Si quieres profundizar en los detalles sigue leyendo :)

Agenda

Los pasos para llevarlo a cabo son los siguientes:

  • Presentaremos el proyecto de ejemplo.
  • Trabajaremos de forma manual con Docker Hub.
  • Después enlazaremos nuestro proyecto de GitHub con GitHub Actions.
  • Estableceremos los pasos para un proceso de CI.
  • Comprobaremos el resultado de nuestra batería de test.
  • Verificaremos si el contenedor de Docker puede ser generado.
  • Subiremos una imagen al Docker Hub Registry (incluyendo el número de compilación).
  • Comprobaremos si nuestro proceso de Continuous Delivery ha tenido éxito para consumir nuestras imágenes desde un fichero docker compose.

Proyecto de ejemplo

Tomaremos como ejemplo una aplicación de chat. La aplicación está dividida en dos partes: el cliente (Front End) y el servidor (Back End) que serán encapsulados en contenedores Docker (a esta acción la denominaremos "dockerizar" de ahora en adelante), y desplegados mediante Docker Containers.

chat-app.gif

Ya tenemos listos un par de repositorios que juntos crean la aplicación de chat:

Siguiendo la aproximación actual de despliegue (revisar primer post de esta serie), una tercera parte será incluida también: un balanceador de carga. Su responsabilidad será direccionar el tráfico al front o back dependiendo de la url de la petición. El balanceador de carga también será "dockerizado" y desplegado usando Docker Container.

Docker Hub

En nuestro último artículo tomamos como punto de partida una imagen de Docker con Ubuntu + Nodejs; fue estupendo recuperarlas desde Docker Hub y tener el control de que versión estábamos descargando.

¿No sería genial ser capaces de subir nuestras propias imágenes a Docker Hub Registry, incluyendo la versión? Eso es lo que Docker Hub nos ofrece: puedes crear tu propia cuenta y subir las imágenes de Docker.

Ventajas de usar Docker Hub:

  • Podemos mantener varias versiones de las imágenes de nuestros contenedores (genial para soportar distintos entornos, A/B testing, canary deployment, green blue deployment, rolling back, etc...).
  • Dado que Docker se crea a partir de una serie de capas, nuestra imagen custom no ocupará más espacio del necesario, es decir, un contenedor construido sobre un contenedor linux no desplegará el SO, sólo apuntará a la imagen anterior.
  • Como la imagen del contenedor ya esta en la nube, desplegar en el proveedor de cloud es sencillo.

Docker Hub es fantástico para arrancar: podemos crear una cuenta gratis y subir nuestras imágenes Docker (la versión gratuita permite crear repositorios públicos ilimitados y uno sólo privado).

Si más tarde necesitamos usarlo para propósitos de negocio y restringir el acceso, podemos usar un registro de Docker privado, algunos proveedores son:

GitHub Actions

Aunque Docker nos ayuda a estandarizar la creación de un entorno y una configuración predeterminadas, la creación de nuevas versiones de forma manual puede convertirse en un proceso complicado y propenso a errores:

  • Tenemos que descargar el estado del código adecuado para el proceso de build.
  • Ejecutar los tests automatizados y comprobar que pasan en verde.
  • Tenemos que generar nuestra imagen de Docker.
  • Añadir el versionado adecuado (etiquetar la imagen en los términos de Docker).
  • Y por último, debemos subir la imagen manualmente al registro de Docker Hub.

Imaginemos hacer esto en cada proceso de merge to master; quedaríamos agotados con este infierno de despliegue... ¿Existe alguna manera de automatizar esto? ¡GitHub Actions al rescate!

Simplemente invirtiendo algo de tiempo en crear una configuración inicial, GitHub Actions automáticamente:

  • Notificará de cualquier PR o merge to master (se pueden establecer políticas de notificación).
  • Creará un entorno limpio (por ejemplo, en nuestro caso tomará una imagen de Ubuntu + Nodejs).
  • Descargará el estado de código adecuado desde el repositorio.
  • Ejecutará los tests.
  • Creará la imagen de Docker que contendrá la build de producción.
  • En caso de tener éxito, desplegará en el registro de Docker Hub (añadiendo la etiqueta de versión siguiendo la notación de Docker).

Una de las ventajas de GitHub Actions es que es bastante sencillo de configurar:

  • No necesitas instalar infraestructura (está basado en un entorno cloud).
  • La principal configuración se realiza mediante un fichero yml.
  • Ofrece una versión community edition donde podemos jugar con nuestros proyectos de prueba o usarlo para nuestros proyectos open source (sólo proyectos públicos).
  • Ofrece una versión enterprise para nuestros proyectos privados.

Configuración de cuentas

Fork sobre los proyectos de ejemplo

Si quieres seguir este tutorial, puedes empezar por hacer un fork de los repositorios de Front End y Back End:

fork.png

Al hacer fork de estos repositorios, una copia se creará en tu cuenta de GitHub y podrás enlazarlos a tu cuenta de GitHub Actions y configurar el proceso de CI.

Registro de Docker Hub

En nuestro artículo previo de esta serie, consumíamos una imagen de Docker Hub. Si queremos subir nuestras propias imágenes a Docker Hub (gratuito para imágenes públicas), necesitamos crear una cuenta. Puedes crear tu cuenta en el siguiente enlace.

Comencemos

En este tutorial, aplicaremos la automatización a los repositorios (los cuales hemos hecho previamente un fork para que aparezcan en nuestra cuenta de Github) de la aplicación de chat usando herramientas a disposición en GitHub Actions. GitHub Actions lanzará una tarea después de cada commit donde se realizarán lss siguientes pasos:

  • Se ejecutarán los test.
  • La aplicación será "dockerizada".
  • La imagen creada será subida al Docker Registry, en nuestro caso Docker Hub.

Subiendo una imagen de forma manual a Docker Hub

Antes de comenzar con la automatización, hagamos este proceso manualmente.

Una vez que tengamos una cuenta en Docker Hub, podemos interactuar con la plataforma desde la shell (abrir un bash terminal, o windows cmd).

Nos autentificamos contra Docker Hub.

$ docker login

Para poder subir nuestras imágenes, tienen que estar etiquetadas siguiendo el siguiente patrón:

<nombre_usuario_Docker_Hub>/<nombre_imagen>:[version]

La versión es opcional. Si no la especificamos, se aplicará latest.

$ docker tag front <nombre_usuario_Docker_Hub>/front

y finalmente la subimos

$ docker push <nombre_usuario_Docker_Hub>/front

Desde ahora, la imagen estará disponible para el uso de cualquier usuario.

$ docker pull <nombre_usuario_Docker_Hub>/front

Para crear el resto de las imágenes que necesitamos, debemos seguir exactamente los mismos pasos.

Como hemos dicho anteriormente, todas las tareas que se repiten se pueden llegar a convertir en un proceso tedioso y donde podemos cometer errores. En los siguientes pasos aprenderemos como automatizarlo usando la CI/CD de GitHub Actions.

Enlazando las credenciales de Docker Hub

Una vez hemos hecho fork de los proyectos, necesitamos entrar en cada una de las configuraciones del proyecto (Back y Front) y establecer como variables de entorno el usuario y la clave de Docker Hub.

Front

01-github-secrets.png

Back

01-github-secrets.png

Estas variables se usarán después para registrarnos en Docker Hub (nota: la primera vez que introducimos los datos, aparecen como texto plano. Una vez ya establecidas, aparecen como campos de clave).

Finalmente podemos empezar la automatización de nuestras tareas.

Configuración de GitHub Actions

Para llevar a cabo la configuración de GitHub Actions necesitamos crear un fichero llamado main.yml en la carpeta .github/workflows de tu proyecto. Es aquí donde describiremos las acciones que serán ejecutadas por GitHub Actions. Podemos tener tantos ficheros yml como quisiéramos.

Back End

Los pasos para crear el fichero yml para la aplicación de back end son los siguientes:

  1. Nombrar el flujo de trabajo a crear: en este caso, vamos a elegir Backend Chant CI/CD.
  2. Definir los triggers o desencadenadores de eventos: indicamos a GitHub Actions cuando ejecutar un flujo de trabajo específico.
  3. Seleccionar el SO: seleccionar el sistema operativo donde queremos ejecutar nuestra aplicación.
  4. Obtener el repositorio: obtener acceso a todos los archivos en el repositorio.
  5. Configuración de node: instalar nodejs con una versión específica.
  6. Instalar dependencias: como en un entorno en local, podemos ejecutar npm install.
  7. Testing: en nuestro caso ejecutaremos las pruebas unitarias que se han implementado para la aplicación.
  8. Iniciar sesión en Docker Hub: antes de enviar una imagen al Docker Hub Registry, debemos iniciar sesión en Docker Hub.
  9. Crear imagen de Docker: si las pruebas pasan en verde, simplemente creamos la imagen del contenedor (esto buscará un archivo Dockerfile en el raíz de repositorio y seguirá las instrucciones de ese fichero para crear una build de producción, almacenándola en un contenedor de imagen Docker).
  10. Etiquetar imágenes de Docker: necesitamos identificar la imagen del contenedor con una etiqueta determinada.
  11. Subir las imágenes de Docker: subir la imagen generada en el Docker Hub Registry.

Un resumen del proceso de build:

backend-flow.gif

1. Nombrar el flujo de trabajo a crear

Vamos a crear nuestro fichero main.yml en la carpeta .github/workflows de nuestro repositorio de backend:

00-main.yml-file.png

Comenzaremos indicando el nombre del flujo de trabajo.


  ./.github/workflows/main.yml

  + name: Backend Chat CI/CD
  

2. Definir los triggers o desencadenadores de eventos

Podemos activar este flujo de trabajo en varios eventos como push, pull_request, etc.

En este caso, vamos a inicial el proceso de CI/CD cada vez que haya un push to master o un pull request hacia master.


  ./.github/workflows/main.yml

  name: Backend Chat CI/CD

  + on:
  +   push:
  +     branches:
  +       - master
  +   pull_request:
  +     branches:
  +       - master
  

3. Seleccionar el SO

Seguidamente, podemos crear diferentes trabajos para builds, despliegues, etc... Comenzaremos con el trabajo de CI (build y test) y seleccionaremos el sistema operativo en el que se ejecutarán todas las pruebas. En este enlace tienes la lista de sistemas operativos disponibles.

En este caso, elegiremos una instancia de Linux (Ubuntu).


  ./.github/workflows/main.yml

  name: Backend Chat CI/CD

  on:
    push:
      branches:
      - master
    pull_request:
      branches:
        - master

  + jobs:
  +   ci:
  +     runs-on: ubuntu-latest
  

4. Obtener el repositorio

Necesitamos acceder a todos los ficheros del repositorio, por lo que debemos clonar el repositorio en este entorno. En lugar de definir manualmente todos los pasos para clonarlo desde cero, podemos usar una action ya creada, es decir, tenemos actions oficiales disponibles de los equipos de GitHub u otras compañías en el GitHub Marketplace. En este caso, utilizaremos la action checkout previamente definida para realizar el proceso de obtener nuestro repositorio de Github (descargaremos el repositorio en una carpeta determinada):


  ./.github/workflows/main.yml

  ···
  
  jobs:
    ci:
      runs-on: ubuntu-latest

  +   steps:
  +     - uses: actions/checkout@v1
  

5. Configurar node

Nosotros usaremos otra action para configurar node. Esta action está desarrollada también por el equipo de GitHub y nos permitirá poner en marcha el entorno de nodejs (incluso podremos especificar una versión):

En este caso vamos a solicitar la versión 12.x de nodejs.


  ./.github/workflows/main.yml

  ···
  
    steps:
      - uses: actions/checkout@v1
  +   - uses: actions/setup-node@v1
  +     with:
  +       node-version: '12.x'
  

6. Instalar dependencias

Comenzamos teniendo una máquina Ubuntu + nodejs levantada y en ejecución. Con la action checkout ya hemos descargado el código fuente de nuestro repositorio, así que es el momento de ejecutar npm install antes de comenzar la ejecución de las pruebas unitarias.

Para ejecutarlo, crearemos un nuevo paso con el nombre Install y ejecutaremos el comando npm install en la sección run.


  ./.github/workflows/main.yml

  ···
  
     steps:
      - uses: actions/checkout@v1
      - uses: actions/setup-node@v1
        with:
          node-version: '12.x'
  +   - name: Install
  +     run: npm install
  

7. Testing

Todo la fontanería está lista, así que vamos a agregar un comando npm test. De esta forma lanzaremos nuestra batería de test.


  ./.github/workflows/main.yml

  ···

     steps:
      - uses: actions/checkout@v1
      - uses: actions/setup-node@v1
        with:
          node-version: '12.x'
  -   - name: Install
  +   - name: Install & Tests
  -     run: npm install
  +     run: |
  +       npm install
  +       npm test
  

Fíjate, hemos incluido los scripts de install y test en un paso común (tenemos que agregar un carácter de barra inclinada para indicar que vamos a ejecutar varios scripts en el mismo paso), pero también podíamos haberlo dividirlo en pasos separados.

8. Iniciar sesión en Docker Hub

Justo después de que se hayan ejecutado todos los scripts, queremos subir la imagen de Docker que generaremos en el Docker Hub Registry.

Como GitHub Actions se ejecuta sobre el sistema operativo que habíamos definido en la sección runs_on, podemos hacer uso del software preinstalado en estos entornos virtuales, para acceder a docker sin necesidad de ejecutar un paso de instalación de Docker.

Lo primero que haremos será iniciar sesión en Docker Hub (para ello haremos uso de las variables de entorno agregadas a la sección secrets de nuestro repositorio. Revisa la sección Enlazando las credenciales de Docker Hub de este artículo).

Vamos a crear una nueva tarea, esta será la tarea de cd (Continous Delivery) y hará lo siguiente:

  • Esperará a que la tarea previa de ci (Continous Integration) se complete de forma satisfactoria.
  • Como estamos definiendo una nueva tarea, necesitamos volver a revisar el repositorio (estamos usando una instancia nueva).
  • Iniciará sesión en Docker Hub utilizando las credenciales secrets del contexto de GitHub que hemos definido.

  ./.github/workflows/main.yml

  ···

   jobs:
    ci:
      runs-on: ubuntu-latest
  
      steps:
        - uses: actions/checkout@v1
        - uses: actions/setup-node@v1
          with:
            node-version: '12.x'
        - name: Install & Tests
          run: |
            npm install
            npm test
  + cd:
  +   runs-on: ubuntu-latest
  +   needs: ci

  +   steps:
  +     - uses: actions/checkout@v1
  +     - name: Docker login
  +       run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
  

En el siguiente paso continuaremos trabajando con la tarea de cd (ahora es el momento de construir la imagen de Docker).

9. Construir la imagen de Docker

En el primer artículo de esta serie, creamos un Dockerfile configurando los pasos del proceso de build. Vamos a copiar el contenido de ese archivo y lo colocaremos en el raíz de nuestro repositorio (nombre del archivo: Dockerfile).

05-docker-file.png

./.Dockerfile

FROM node
WORKDIR /opt/back
COPY . .
RUN npm install
EXPOSE 3000
ENTRYPOINT ["npm", "start"]

A modo de recordatorio sobre esta configuración, echemos un vistazo al contenido:

  • FROM node Establecemos la imagen base de node desde Docker Hub image.
  • WORKDIR /opt/back Establecemos nuestro directorio de trabajo en /opt/back.
  • COPY . . Copiamos el contenido en el contenedor. La ruta donde será copiado el contenido será en el directorio de trabajo seleccionado, en este caso /opt/back.
  • RUN npm install Instalamos las dependencias.
  • EXPOSE 3000 Notificamos cuál es el puerto que expone nuestra aplicación.
  • ENTRYPOINT ["npm", "start"] Comando para arrancar nuestro contenedor.

Volvamos al fichero yml: dentro de la tarea cd, justo después del inicio de sesión de Docker, añadimos el comando para construir la imagen del contenedor Docker.


  ./.github/workflows/main.yml

  ···

      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
  +     - name: Build
  +       run: docker build -t back .
  

Este comando buscará el fichero Dockerfile, que creamos en la raíz del repositorio de backend, y seguirá los pasos para realizar el proceso de build de la imagen.

¡Un momento! Acabo de caer en la cuenta de que aquí hay algo extraño: estamos utilizando distintos contenedores. GitHub Actions ejecuta los tests en una instancia de linux y el Dockerfile usa otra configuración de linux / node extraída del Docker Hub Registry. Esto huele mal, ¿no?

¡Totalmente de acuerdo! Ambas configuraciones, la de node y la del fichero Dockerfile deberían arrancar desde la misma imagen. Necesitamos asegurarnos de que la prueba se ejecute con la misma configuración como si estuviéramos en producción. La mejor solución pasa por configurar la sección container dentro de una tarea para ejecutar una action desde la imagen de Docker:

jobs:
  my_job:
    container:
      image: node:10.16-jessie
      env:
        NODE_ENV: development
      ports:
        - 80

Podríamos actualizar nuestra configuración y tener algo como lo siguiente:


  ./.github/workflows/main.yml

  ···
  jobs:
    ci:
      runs-on: ubuntu-latest
  +   container:
  +     image: node
  
      steps:
        - uses: actions/checkout@v1
  -     - uses: actions/setup-node@v1
  -       with:
  -         node-version: '12.x'
  
        - name: Install & Tests
          run: |
            npm install
            npm test
    cd:
      runs-on: ubuntu-latest
      needs: ci
  
      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
        - name: Build
          run: docker build -t back .
  

Usaremos esta imagen para el paso de ci, pero para el de cd no necesitaremos configurarlo ya que sólo estamos construyendo la imagen Docker definida en el Dockerfile.

10. Etiquetar las imágenes de Docker

La imagen actual de Docker que hemos generado tiene el siguiente nombre: back. Para subirlo al Docker Hub Registry, necesitamos agregar un nombre más elaborado y único:

  • Vamos a etiquetarlo con el nombre de usuario de Docker.
  • Agregaremos un sufijo con un número de build único (en este caso el commit SHA del contexto de Github).

Por otro lado, indicaremos que la imagen actual que hemos generado es la última imagen Docker disponible.

En un proyecto real, esto puede variar según tus necesidades.


  ./.github/workflows/main.yml

  ···
      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
        - name: Build
          run: docker build -t back .
  +     - name: Tags
  +       run: |
  +         docker tag back ${{ secrets.DOCKER_USER }}/back:${{ github.sha }}
  +         docker tag back ${{ secrets.DOCKER_USER }}/back:latest
  

11. Subir las imágenes de Docker

Ahora que hemos identificado nuestra imagen con un nombre único, necesitamos subir la imagen de Docker al Docker Hub Registry. Para ello usaremos el comandos docker push.


  ./.github/workflows/main.yml

  ···
      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
        - name: Build
          run: docker build -t back .
        - name: Tags
          run: |
            docker tag back ${{ secrets.DOCKER_USER }}/back:${{ github.sha }}
            docker tag back ${{ secrets.DOCKER_USER }}/back:latest
  +     - name: Push
  +       run: |
  +         docker push ${{ secrets.DOCKER_USER }}/back:${{ github.sha }}
  +         docker push ${{ secrets.DOCKER_USER }}/back:latest    
  

Fíjate que primero estamos subiendo la imagen ${{ secrets.DOCKER_USER }}/back:${{ github.sha }}, y después la imagen ${{ secrets.DOCKER_USER }}/back:latest

¿Significa esto que la imagen será subida dos veces?... La respuesta es no.

Docker es lo suficientemente listo para identificar que la imagen es la misma, así que le asignará dos "nombres" diferentes a la misma imagen en el Docker Repository

El resultado final

El fichero main.yml debería ser parecido a esto:


  ./.github/workflows/main.yml

  name: Backend Chat CI/CD
  
  on:
    push:
      branches:
        - master
    pull_request:
      branches:
        - master
  
  jobs:
    ci:
      runs-on: ubuntu-latest
      container:
        image: node
  
      steps:
        - uses: actions/checkout@v1
        - name: Install & Tests
          run: |
            npm install
            npm test
    cd:
      runs-on: ubuntu-latest
      needs: ci
  
      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
        - name: Build
          run: docker build -t back .
        - name: Tags
          run: |
            docker tag back ${{ secrets.DOCKER_USER }}/back:${{ github.sha }}
            docker tag back ${{ secrets.DOCKER_USER }}/back:latest
        - name: Push
          run: |
            docker push ${{ secrets.DOCKER_USER }}/back:${{ github.sha }}
            docker push ${{ secrets.DOCKER_USER }}/back:latest
  

Ahora, si subimos esta configuración a GitHub automáticamente lanzará la build.

02-build-in-progress.png

Una vez finalizado, podemos comprobar si la imagen ha sido generada correctamente.

03-build-successfully.png

Y finalmente, la imagen estará disponible en tu cuenta de Docker Hub Registry.

04-published-image.png

Front End

Los pasos para crear le fichero main.yml son muy parecidos a los anteriores (backend):

  1. Nombrar el flujo de trabajo a crear.
  2. Definir los triggers de los eventos.
  3. Definir la tarea de CI (build y test).
  4. Definir la tarea de CD.
  5. Iniciar sesión en Docker Hub.
  6. Construir la imagen de Docker.
  7. Etiquetar las imágenes de Docker.
  8. Subir las imágenes de Docker.

1. Nombrar el flujo de trabajo a crear

Como hicimos con el backend, vamos a definir el flujo de trabajo en el fichero main.yml.


  ./.github/workflows/main.yml

  + name: Frontend Chat CI/CD
  

2. Definir los triggers de los eventos

Seguiremos una aproximación similar al flujo de trabajo de backend. Recuerda que podemos activar este flujo de trabajo en varios eventos como push, pull_request, etc.

En este caso, vamos a inicial el proceso de CI/CD cada vez que haya un push to master o un pull request hacia master.


  ./.github/workflows/main.yml

  name: Frontend Chat CI/CD

  + on:
  +   push:
  +     branches:
  +       - master
  +   pull_request:
  +     branches:
  +       - master
  

3. Definir la tarea de CI (build y test)

Vamos a definir nuestra tarea de ci:

  • Indicaremos que comience desde la imagen de Docker con nodejs (Ubuntu + nodejs) y ejecutará los comandos de npm install y npm test.

  ./.github/workflows/main.yml

  ···

  + jobs:
  +   ci:
  +     runs-on: ubuntu-latest
  +     container:
  +       image: node
  
  +     steps:
  +       - uses: actions/checkout@v1
  +       - name: Install & Tests
  +         run: |
  +           npm install
  +           npm test
  

4. Definir la tarea de CD

Ahora vamos a crear una tarea de cd que comenzará justo después de que la tarea de ci haya sido completada satisfactoriamente y se descargará todos los ficheros del repositorio en la instancia de ejecución.


  ./.github/workflows/main.yml

  ···

  jobs:
    ...
  + cd:
  +   runs-on: ubuntu-latest
  +   needs: ci

  +   steps:
  +     - uses: actions/checkout@v1
  

5. Iniciar sesión en Docker Hub

El siguiente paso será identificarnos en Docker Hub.


  ./.github/workflows/main.yml

  ···

  jobs:
    ...
    cd:
      runs-on: ubuntu-latest
      needs: ci
  
      steps:
        - uses: actions/checkout@v1
  +     - name: Docker login
  +       run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
  

6. Construir la imagen de Docker

Ahora vamos a indicarle que cree la imagen de Docker.


  ./.github/workflows/main.yml

  ···

  jobs:
    ...
    cd:
      runs-on: ubuntu-latest
      needs: ci
  
      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
  +     - name: Build
  +       run: docker build -t front .
  

7. Etiquetar las imágenes de Docker

Recuerda que como hicimos con la aplicación de backend, vamos a etiquetar la versión actual con el commit SHA y definirla como latest.


  ./.github/workflows/main.yml

  ···

  jobs:
    ...
    cd:
      runs-on: ubuntu-latest
      needs: ci
  
      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
        - name: Build
          run: docker build -t front .
  +     - name: Tags
  +       run: |
  +         docker tag front ${{ secrets.DOCKER_USER }}/front:${{ github.sha }}
  +         docker tag front ${{ secrets.DOCKER_USER }}/front:latest
  

8. Subir las imágenes de Docker

Ahora necesitamos subir las imágenes al Docker Hub Registry.


  ./.github/workflows/main.yml

  ···

  jobs:
    ...
    cd:
      runs-on: ubuntu-latest
      needs: ci
  
      steps:
        - uses: actions/checkout@v1
        - name: Docker login
          run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
        - name: Build
          run: docker build -t front .
        - name: Tags
          run: |
            docker tag front ${{ secrets.DOCKER_USER }}/front:${{ github.sha }}
            docker tag front ${{ secrets.DOCKER_USER }}/front:latest
  +     - name: Push
  +       run: |
  +         docker push ${{ secrets.DOCKER_USER }}/front:${{ github.sha }}
  +         docker push ${{ secrets.DOCKER_USER }}/front:latest
  

Y nuestro main.yml debería parecerse a esto:


  ./.github/workflows/main.yml

  name: Frontend Chat CI/CD
  
  on:
    push:
      branches:
        - master
    pull_request:
      branches:
        - master
  
    jobs:
      ci:
        runs-on: ubuntu-latest
        container:
          image: node
  
        steps:
          - uses: actions/checkout@v1
          - name: Install & Tests
            run: |
              npm install
              npm test
  cd:
    runs-on: ubuntu-latest
    needs: ci
  
    steps:
      - uses: actions/checkout@v1
       - name: Docker login
         run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
       - name: Build
         run: docker build -t front .
       - name: Tags
         run: |
           docker tag front ${{ secrets.DOCKER_USER }}/front:${{ github.sha }}
           docker tag front ${{ secrets.DOCKER_USER }}/front:latest
       - name: Push
         run: |
           docker push ${{ secrets.DOCKER_USER }}/front:${{ github.sha }}
           docker push ${{ secrets.DOCKER_USER }}/front:latest
  

Para finalizar, incluiremos el fichero Dockerfile al proyecto frontend como hicimos en el artículo anterior.

./.Dockerfile

FROM node AS builder
WORKDIR /opt/front
COPY . .
RUN npm install
RUN npm run build:prod

FROM nginx
WORKDIR /var/www/front
COPY --from=builder /opt/front/dist/ .
COPY nginx.conf /etc/nginx/

Además, incluir el fichero nginx.conf.

worker_processes 2;
user www-data;

events {
  use epoll;
  worker_connections 128;
}

http {
  include mime.types;
  charset utf-8;
  server {
    listen 80;
    location / {
      root /var/www/front;
    }
  }

}

Vamos a recordar qué definíamos en este fichero nginx.conf:

  • worker_processes el número de procesos dedicados.
  • user define el usuario que los worker_processes usarán.
  • events conjunto de directivas para el manejo de conexiones.
    • epoll método eficiente usado en linux 2.6+.
    • worker_connections nº máximo de conexiones simultáneas que puede abrir un worker_processes.
  • http define las directivas del servidor HTTP.
    • server en contexto http define un servidor virtual.
    • listen el puerto que escuchará el servidor virtual.
    • location la ruta de los ficheros que serán servidos. Fijaos en que /var/www/front es el sitio donde copiamos los ficheros del build de nuestra aplicación.

Ejecutando un sistema de multi contenedores

Vamos a comprobar si nuestra Configuración de CI funciona como esperamos.

Primero nos aseguramos de que GitHub Actions ha ejecutado con éxito al menos un proceso de build.

Deberíamos ver en GitHub Actions que el proceso de build ha sido lanzado para los repositorios de Front End y Back End (inicia sesión en cada repositorio de GitHub).

También deberíamos ver las imágenes disponibles en el docker registry (iniciando sesión en Docker Hub):

Como hicimos en nuestra publicación anterior, podemos lanzar todo nuestro sistema utilizando Docker Compose. Sin embargo, en este caso para el Front End y Back End vamos a consumir las imágenes que hemos subido a Docker Hub Registry.

Los cambios que vamos a introducir al fichero docker-compose.yml son los siguientes:


  ./docker-compose.yml

  version: '3.7'
  services:
    front:
  -    build: ./container-chat-front-example
  +    image: <nombre_usuario_Docker_Hub>/front:<version>
    back:
  -    build: ./container-chat-back-example
  +    image: <nombre_usuario_Docker_Hub>/back:<version>
    lb:
      build: ./container-chat-lb-example
      depends_on:
        - front
        - back
      ports:
        - '80:80'

Así queda el fichero docker-compose.yml:

version: "3.7"
  services:
    front:
      image: <nombre_usuario_Docker_Hub>/front:<version>
    back:
      image: <nombre_usuario_Docker_Hub>/back:<version>
    lb:
      build: ./container-chat-lb-example
      depends_on:
        - front
        - back
      ports:
        - "80:80"

Podemos arrancarlo utilizando:

$ docker-compose up

Entonces se descargarán las imágenes de back y front desde Docker Hub Registry (la última disponible). Podemos comprobar cómo funciona abriendo un navegador web e introduciendo http://localhost/ en la barra del navegador (más información acerca de cómo funciona en nuestro primer artículo Hola Docker)

Recursos

En Resumen

Al introducir este proceso de CI/CD, hemos obtenido múltiples ventajas:

  • El proceso de build se ha automatizado, de esta manera evitamos los errores humanos.
  • Podemos desplegar distintas versiones de la build fácilmente (como de si una máquina de discos se tratará, ver diagrama).
  • Podemos dar marcha atrás fácilmente una publicación errónea.
  • Podemos aplicar testeo A/B o tener entornos Canary.
deploy.jpg

¿Y qué hay del despliegue? En el próximo artículo de esta serie aprenderemos a crear despliegues automatizados usando Kubernetes.

Permanece atento a nuestro blog :)


Puedes seguir leyendo más artículos de esta serie:

  1. Hola Docker
  2. Hola Docker CI/CD - Travis

¿Te gusta el mundo Devops?

En Lemoncode impartimos un Bootcamp Devops Online, si quieres saber más puedes pinchar aquí para más información.