Despliegue con Gitlab CI/CD y Docker Swarm

Arturo Bermejo
Urbaner
Published in
8 min readJun 12, 2019

--

Vamos a ver el proceso de desplegar un proyecto en python a una máquina virtual en AWS usando Gitlab CI/CD y Docker Swarm.

Nota: Se asumen conocimientos básicos de Python, Docker, Git, EC2 y SSH.

Antes de comenzar es necesario lo siguiente:

El proyecto que vamos a desplegar es un simple “hello world” hecho en python. La estructura inicial del proyecto sería la siguiente.

helloapp/
- app.py
- requirements.txt

app.py

from flask import Flaskapp = Flask(__name__)@app.route("/")
def hello():
return "Hello World!"

requirements.txt

Flask==1.0.3

Si tienes python puedes crear un entorno virtual e instalar las dependencias para poder correr el proyecto y ver nuestro “hello world” en el navegador.

$ FLASK_APP=hello.py flask run

Construcción de la imagen

El primer paso es construir la imagen de nuestro proyecto. Una imagen de Docker contiene librerias de sistema y el código de una aplicación, esta imagen será usada entonces para crear contenedores los cuales tendrán la aplicación corriendo internamente.

Agregamos el siguiente archivo.

helloapp/
- docker/
- Dockefile
- app.py
- requirements.txt

Dockerfile

FROM python:3.6-alpine3.9ENV FLASK_APP app.pyRUN mkdir -p /appWORKDIR /appCOPY ./ .RUN pip install -r requirements.txtENTRYPOINT flask run -h 0.0.0.0 -p 5000

Nuestro archivo Dockerfile contiene las instrucciones necesarias para construir la imagen de nuestro proyecto. Tomamos como base una imagen de python, copiamos nuestro código, instalamos las dependencias y finalmente indicamos el comando que se ejecutará cuando se cree un contenedor basado en esta imagen el cual correra nuestro proyecto.

Para probar este proceso en local podemos ejecutar los siguientes comandos.

$ docker build -f ./docker/Dockerfile -t helloapp .
$ docker run -p 8000:5000 helloapp:latest

Con docker build construimos la imagen y la tagueamos como “helloapp”.

Con docker run corremos un contenedor de esa imagen indicando que el puerto de localhost 8000 mapee al puerto 5000 del contenedor, con lo cual ahora podemos ver de nuevo nuestro “hello world” si abrimos el navegador en localhost:8000.

Adicionalmente podemos subir nuestra imagen a un container registry lo que permitirá compartir nuestra imagen con otras personas de manera pública o privada. Este será un paso necesario también cuando necesitemos correr la imagen en nuestro servidor.

Podemos subirla por ejemplo a Docker Hub con los siguientes comandos.

$ docker login -u arturobermejo
$ docker tag flaskapp arturobermejo/flaskapp:latest
$ docker push arturobermejo/flaskapp:latest

Con docker login nos logueamos con nuestra cuenta, con docker tag retagueamos la imagen de acuerdo a lo requerido en Docker Hub y con docker push subimos nuestra imagen al registro.

Gitlab CI/CD

Gitlab es un completo sistema web de control de versiones que nos permite planificar, administrar y desplegar nuestros proyectos.

Lo primero que vamos a hacer es crear un proyecto en Gitlab para subir nuestro código. Hacemos un commit de nuestros cambios hasta el momento y luego un push.

$ cd helloapp
$ git init
$ git remote add origin git@gitlab.com:arturobermejo/helloapp.git
$ git add .
$ git commit -m "Initial commit"
$ git push -u origin master

Gitlab dispone de pipelines que nos permite definir determinados flujos y fases por los que debe pasar nuestro código en cada modificación.

Para ello vamos agregar un nuevo archivo que definirá el flujo para nuestro proyecto.

helloapp/
- docker/
- Dockefile
- app.py
- requirements.txt
- .gitlab-ci.yml

El archivo .gitlab-ci.yml debe encontrarse en la raiz de nuestro proyecto, este archivo define los jobs (tareas) que se ejecutarán cada vez que se hace una modificación en el código, y estos jobs serán ejecutadas por un Runner, veamos.

image: docker:stableservices:
- docker:dind
stages:
- build
build_image:
stage: build
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
DOCKER_HOST: tcp://docker:2375
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build --pull -t $IMAGE_TAG -f docker/Dockerfile .
- docker push $IMAGE_TAG
only:
- master

El archivo define docker como el contexto donde se correrán las jobs, con lo cual tenemos el comando docker disponible, y define por el momento solo un stage llamado build. Define un job llamado build_image la cual ejecutará los comandos necesarios para construir la imagen con nuestro código, loguearse en el container registry de Gitlab y subir la imagen generada, proceso similar al que hicimos con Docker Hub.

Nótese que tagueamos la imagen con variables de entorno que Gitlab CI/CD pone a disposición en las jobs, en este caso usamos el SHORT_SHA del último commit para crear imágenes distintas por cada cambio. Nótese además que este job solo se ejecutará para el branch master.

Hacemos un commit de nuestros cambios y lo subimos a Gitlab.

En la seccion pipelines de nuestro proyecto en Gitlab podremos ver algo como esto si se ejecutó con éxito nuestro job.

Podemos ir a la sección Container Registry de Gitlab para verificar que efectivamente se generó la imagen de nuestro proyecto.

Despliegue

El siguiente paso es desplegar nuestro proyecto, necesitamos ingresar a nuestro servidor, hacer pull de esa imagen y correr un contenedor de nuestra aplicación. Para ello vamos a definir otro stage y otro job con gitlab para automatizar ese proceso.

Preparación de la VM

Primero necesitamos una maquina virtual donde desplegar nuestro proyecto. Podemos crear una instancia manualmente en EC2 e instalar Docker y configurar puertos pero vamos a usar docker machine para que nos ayude con eso.

Asumiendo que tenemos nuestras credenciales de AWS en .aws/credentials y que tenemos un par de llaves pública y privada SSH en nuestro localhost (id_rsa, id_rsa.pub) ejecutamos lo siguiente.

$ docker-machine create \
--driver amazonec2 \
--amazonec2-open-port 8000 \
--amazonec2-region us-west-1 \
--amazonec2-ssh-keypath ~/.ssh/id_rsa \
helloaws

Podemos entrar a AWS para verificar la instancia creada. Ejecutamos el siguiente comando de docker-machine para obtener variables de entorno que necesitaremos para la conección al docker daemon, principalmente la IP.

$ docker-machine env helloaws | grep DOCKER_HOSTexport DOCKER_HOST="tcp://54.193.78.64:2376"

Es posible entrar entonces via SSH a nuestro servidor con:

$ ssh ubuntu@54.193.78.64

Agregamos el usuario ubuntu al grupo docker.

$ docker-machine ssh helloaws "sudo usermod -aG docker ubuntu"

Docker Swarm

Docker Swarm es un orquestador de contenedores que nos permite administrar contenedores de una manera mas escalable, trabaja sobre la abstracción de servicios en lugar de contenedores directamente, maneja maquinas como nodos y se encarga de repartir la ejecución de los servicios entre los nodos según los parametros o requerimientos que definamos para los servicios.

Para propositos de este tutorial vamos a usar un solo nodo. Para activar el modo swarm en nuestro servidor ejecutamos lo siguiente.

$ docker-machine ssh helloaws "docker swarm init --advertise-addr 54.193.78.64"Swarm initialized: current node (f9c6fs1i11cvy2rpxe4ix2abr) is now a manager.To add a worker to this swarm, run the following command:docker swarm join --token [TOKEN] 54.193.78.64:2377To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Esto configura nuestro servidor como nodo tipo “manager”, si queremos agregar mas nodos como “workers” ejecutamos el comando que indica como respuesta. Nótese que esto se puede hacer via SSH directamente sin usar docker-machine.

Agregamos el siguiente archivo que definirá la configuración de nuestro servicio.

helloapp/
- docker/
- Dockefile
- docker-compose.yml
- app.py
- requirements.txt
- .gitlab-ci.yml

gitlab-ci.yml

version: "3.2"services:
helloapp:
image: "${IMAGE_TAG}"
deploy:
replicas: 2
placement:
constraints: [node.role == manager]
restart_policy:
condition: on-failure
ports:
- "8000:5000"

Definimos un servicio llamado “helloapp” el cual tomará nuestra imagen la cual pasaremos como variable de entorno, se crearan dos contenedores con la restricción de que deben colocarse en un nodo tipo manager, definimos una condición de reinicio del servicio y definimos el mapping de puertos como hicimos anteriormente.

Configuración del Proyecto en Gitlab CI/CD

Para que el runner de Gitlab CI/CD pueda acceder a nuestro servidor y ejecutar cualquier comando (en nuestro caso ejecutar comandos de Docker), necesitamos darle acceso via SSH, para ello:

  • Generamos un nuevo SSH key par, en nuestro caso usaremos las llaves que generamos inicialmente.
  • Agregar la llave privada como variable en nuestro proyecto, en Setting - CI/CD - Variables, creamos una variable con el nombre SSH_PRIVATE_KEY y como valor el texto contenido en ~/.ssh/id_rsa.
  • Copiamos la llave pública al servidor que queremos tener acceso (si usamos docker-machine no es necesario este paso).
  • Insertamos la llave privada durante la ejecución del job.

Mas información sobre ese proceso aquí.

Hacemos la siguiente modificación el archivo gitlab-ci.yml para definir el job de despliegue.

...stages:
- build
- deploy
...deploy_service:
stage: deploy
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
DOCKER_HOST: ssh://ubuntu@54.193.78.64
DOCKER_STACK: myproject
before_script:
- apk update
- apk add openssh
- mkdir -p ~/.ssh
- ssh-keyscan -H 54.193.78.64 >> ~/.ssh/known_hosts
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
script:
- docker login -u $CI_DEPLOY_USER -p $CI_DEPLOY_PASSWORD $CI_REGISTRY
- docker stack deploy --with-registry-auth -c docker/docker-compose.yml $DOCKER_STACK
only:
- master

Definimos el job deploy_service, indicamos como variables de entorno la imagen que se va a usar y que vamos a pasar docker-compose.yml como mencionamos antes, indicamos el host de docker a donde nos vamos a conectar via ssh y definimos el nombre que llevara nuestro stackmyproyect”, un stack agrupa una serie de servicios, en nuestro caso solo tenemos un solo servicio.

Antes de ejecutar los comandos (before_script) inyectamos nuestra llave privada para poder conectarnos via ssh.

Y finalmente nos logueamos en el container registry de Gitlab, para ello debemos crear un Deploy Token en nuestro proyecto llamado gitlab-deploy-token y desplegamos nuestro stack definido en el archivo docker-compose.yml el cual consta de un solo servicio.

Hacemos commit de los cambios, los subimos a Gitlab y verificamos que se ejecuten correctamente los jobs, de build y de deploy.

Podemos entrar a nuestro servidor para verificar que esta corriendo nuestro servicio según lo configurado.

$ docker service ls

Nótese el prefijo de nuestro stack colocado al inicio del nombre del servicio.

Finalmente podemos ingresar a http://54.193.78.64:8000/ en el navegador para ver nuestro “hello world”.

Conclusión

Actualmente definir un pipeline para integración continua resulta muy importante para poder automatizar todo el proceso de despliegue y poder entregar y probar cambios más frecuentemente y por lo tanto con menor riesgo en nuestros proyectos, se pueden establecer muchas fases desde el inicio de un cambio hasta su pase a producción, incluyendo pruebas automatizadas, asi como estrategias de despliegue que permitan monitorear los cambios recién desplegados.

Los archivos del tutorial pueden encontrarse en aquí.

--

--