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


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:
Push de código: El desarrollador sube cambios al repositorio GitLab.
GitLab CI activa un pipeline: Usando el archivo
.gitlab-ci.yml
, se definen las etapas de build, test y deploy.Runner ejecuta el pipeline: El runner (basado en Ubuntu) construye la imagen Docker, corre pruebas y publica la imagen en DockerHub.
Despliegue en Kubernetes: Kubernetes obtiene la imagen desde DockerHub y la despliega en sus nodos (
nodo1
,nodo2
).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
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