Automatizando CI/CD de una API Flask en Kubernetes con GitLab y DigitalOcean

Roberto EspañaRoberto España
6 min read

Proyecto para aplicar conceptos y aprender nuevas tecnologías mediante la creación de una pipeline CI/CD completa, desde el commit hasta el despliegue


Introducción

En este proyecto muestro cómo diseñé una pipeline CI/CD completa que construye, prueba y despliega una API desarrollada con Flask. Utilicé herramientas como GitLab CI/CD, Docker, DockerHub y un clúster de Kubernetes en DigitalOcean.

El objetivo principal fue automatizar todo el proceso desde el commit hasta el despliegue en producción, garantizando que cada cambio pase por etapas definidas de:

  • Construcción de la imagen

  • Ejecución de pruebas

  • Publicación de la imagen

  • Despliegue automatizado en el clúster

🔗 Al final del artículo encontrarás el enlace al repositorio con todo el código y configuración detallada.


Contexto

Este mini proyecto te dará una pequeña mirada a la integración continua, conociendo procesos y configuraciones básicas.
Permite automatizar los commits, construir, probar y desplegar en producción sin intervención manual. Además, ayuda a familiarizarse con el flujo real de trabajo de DevOps y Kubernetes.


Tecnologías y herramientas

  • 🐍 Python 3.11 + Flask + Gunicorn

  • 🐳 Docker + DockerHub

  • 🤖 GitLab CI/CD con runners personalizados

  • ☁️ Kubernetes (2 nodos en DigitalOcean)

  • 🔄 Service tipo LoadBalancer


Arquitectura e infraestructura

La infraestructura se compone de:

  • 🖥️ VM dedicada (DigitalOcean): ejecuta los jobs de build y test, tiene un runner shell con Docker instalado.

  • ☸️ Clúster Kubernetes: con 2 nodos, donde se hace el deploy de la aplicación.

  • 🌐 Service LoadBalancer: expone la API mediante una IP pública accesible desde internet.

Diagrama de flujo

Flujo general:

  1. Push de código: El desarrollador sube cambios al repositorio GitLab.

  2. GitLab CI activa un pipeline: Usando el archivo .gitlab-ci.yml, se definen las etapas de build, test y deploy.

  3. Runner ejecuta el pipeline: El runner (basado en Ubuntu) construye la imagen Docker, corre pruebas y publica la imagen en DockerHub.

  4. Despliegue en Kubernetes: Kubernetes obtiene la imagen desde DockerHub y la despliega en sus nodos (nodo1, nodo2).

  5. Balanceo de carga: Un Service LoadBalancer distribuye las peticiones entre los nodos. El cliente accede a la aplicación vía HTTP sin preocuparse de dónde está corriendo.


Pipeline CI/CD paso a paso

🔨 Build

¿Qué hace?
Construye la imagen Docker del proyecto Flask y la sube a DockerHub, asegurando trazabilidad con el hash del commit ($CI_COMMIT_SHORT_SHA) y disponibilidad rápida con la etiqueta latest.

build_job:
  stage: build              # Etapa de construcción de imagen
  tags: 
    - test                  # Se ejecuta en el runner de test (VM)

  script:
    - echo "Build"

    # Autenticación con DockerHub usando variables protegidas
    - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USER" --password-stdin

    # Construcción de la imagen con dos tags: el hash corto del commit y latest
    - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA -t $DOCKER_IMAGE:latest .

    # Subida de ambas versiones a DockerHub
    - docker push $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
    - docker push $DOCKER_IMAGE:latest

    - echo "Imagen $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA y latest subida"

🧪 Test

¿Qué hace?
Ejecuta pruebas funcionales contra la imagen recién construida, sin exponer puertos públicamente, validando que los endpoints funcionen correctamente.

test_job:
  stage: test                   # Etapa de pruebas
  tags: 
    - test                      # Se ejecuta en el runner de test (VM DigitalOcean)

  script:
    - docker pull $DOCKER_IMAGE:latest  # Descarga la imagen desde DockerHub
    - docker run -d --name $APP_NAME $DOCKER_IMAGE:latest  # Corre el contenedor en segundo plano

    # Obtiene la IP interna del contenedor
    - IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $APP_NAME)

    # Espera a que la app esté lista
    - |
      for i in {1..10}; do
        if curl --silent --fail http://$IP:5000/; then
          echo "App lista!"
          break
        else
          echo "Esperando la app..."
          sleep 3
        fi
      done

    # Verifica endpoint raíz
    - |
      echo "Validando endpoint raíz..."
      curl --fail http://$IP:5000/ | grep "Hola" || (echo "Test falló" && exit 1)

    # Verifica endpoint con parámetro
    - |
      echo "Validando endpoint saludo..."
      curl --fail http://$IP:5000/saludo/Sady | grep "Hola, Sady" || (echo "Test falló" && exit 1)

    - echo "Test OK"

    # Limpieza
    - docker stop $APP_NAME || true 
    - docker rm $APP_NAME || true

🚀 Deploy

¿Qué hace?
Este job conecta con tu cluster Kubernetes usando un kubeconfig cargado desde una variable de CI/CD, y luego despliega tu aplicación aplicando el manifiesto deployment.yaml.

deploy_job:
  stage: deploy                 # Etapa del pipeline
  tags:
    - deploy                    # Runner asignado (Kubernetes)

  image: bitnami/kubectl:latest # Imagen con kubectl para usar en el job

  script:
    # Cargar configuración del cluster desde variable segura
    - echo "$KUBECONFIG_CONTENT" > kubeconfig
    - export KUBECONFIG=$PWD/kubeconfig

    # Aplicar el Deployment en Kubernetes
    - kubectl apply -f deployment.yaml

Kubernetes Deployment y Service

Para desplegar la app, registré previamente en DigitalOcean:

  • Una VM Ubuntu para correr el runner de build/test.

  • Un clúster Kubernetes con 2 nodos para el entorno de producción.

Antes de aplicar el deployment, configuré el Service tipo LoadBalancer para el namespace gitlab-runner.

deployment.yaml

apiVersion: apps/v1             # Versión de la API de Kubernetes para Deployments
kind: Deployment                # Tipo de recurso: Deployment

metadata:
  name: helloapp-deployment     # Nombre del Deployment
  namespace: gitlab-runner      # Namespace donde se despliega
  labels:
    app: helloapp               # Etiqueta para agrupar recursos

spec:
  replicas: 2                   # Número de réplicas (2 pods en paralelo)
  selector:
    matchLabels:
      app: helloapp             # Selección de pods por etiqueta

  template:
    metadata:
      labels:
        app: helloapp           # Etiqueta aplicada a los pods generados
    spec:
      containers:
      - name: helloapp
        image: bespana/helloapp:latest  # Imagen Docker a usar
        imagePullPolicy: Always         # Siempre intenta descargar la última versión
        ports:
        - containerPort: 5000           # Puerto expuesto por la app Flask

        # Límites y requests de recursos
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "250m"
            memory: "256Mi"

        # Probes para salud y disponibilidad
        readinessProbe:                # Verifica si el pod está listo para recibir tráfico
          httpGet:
            path: /
            port: 5000
          initialDelaySeconds: 5
          periodSeconds: 10

        livenessProbe:                 # Verifica si el pod sigue vivo
          tcpSocket:
            port: 5000
          initialDelaySeconds: 15
          periodSeconds: 20

service.yaml

apiVersion: v1                  # Versión de la API para Service
kind: Service                   # Tipo de recurso: Service

metadata:
  name: helloapp-service        # Nombre del servicio
  namespace: gitlab-runner      # Namespace donde se encuentra

spec:
  selector:
    app: helloapp               # Asocia este servicio a los pods con esta etiqueta
  type: LoadBalancer            # Expone la app con una IP pública (ideal para producción)
  ports:
  - protocol: TCP
    port: 80                    # Puerto accesible desde el exterior
    targetPort: 5000            # Puerto interno de la app Flask

Verificación del balanceo de carga

Después del despliegue, puedes hacer pruebas públicas usando la IP del LoadBalancer asignada por DigitalOcean:

kubectl get svc -n gitlab-runner

curl (ip-asignada):80

Esto muestra cómo el LoadBalancer distribuye el tráfico entre los pods.


Resultados y aprendizajes

  • Aprendí lo necesario para levantar y operar un clúster Kubernetes básico.

  • Comprendí cómo conectar un runner GitLab con un clúster K8s.

  • Practiqué cómo escribir pipelines funcionales.

  • Vi el poder del LoadBalancer en acción balanceando tráfico entre réplicas.


Conclusión

Este tipo de proyecto no toma demasiado tiempo y es ideal para aprender conceptos clave de DevOps, CI/CD y Kubernetes.
¡Te animo a que lo intentes por tu cuenta!
Empieza por algo pequeño y verás cuánto podés avanzar.


Referencias y enlaces

0
Subscribe to my newsletter

Read articles from Roberto España directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Roberto España
Roberto España

Ingeniero en Telecomunicaciones con enfoque en redes, conectividad y automatización. Actualmente estudiando AWS, DevOps y CI/CD. Comparto mi camino con proyectos prácticos y artículos técnicos desde Chile 🇨🇱. https://robertoespana.hashnode.dev/portafolio