Despliegue en Kubernetes con CI/CD básico

Lectura
25 min~10 min lectura

Despliegue en Kubernetes con CI/CD básico: Automatizando el Pipeline de Machine Learning

Has llegado al punto culminante de nuestro viaje. Ya tienes un modelo de Machine Learning containerizado con Docker, comprendes la orquestación con Kubernetes, y ahora es el momento de unir todo en un flujo automatizado, robusto y profesional. Esta lección se centra en integrar un pipeline de CI/CD (Integración Continua y Despliegue Continuo) básico para tu aplicación de ML. No se trata solo de desplegar, sino de crear un sistema que, ante cada cambio en tu código, pruebe, construya y actualice tu modelo en producción de manera automática y confiable. Para un ingeniero de ML, esta automatización es la diferencia entre un proyecto de laboratorio y una solución industrial.

Implementaremos un pipeline utilizando GitHub Actions como nuestro servidor de CI/CD, dado su amplia adopción y integración gratuita con repositorios Git. El objetivo es claro: cada vez que hagas un push a la rama principal (main/master) de tu repositorio, un conjunto de jobs automatizados se ejecutará. Estos jobs realizarán pruebas de tu código, construirán una nueva imagen Docker, la subirán a un registro (como Docker Hub o GitHub Container Registry), y finalmente, actualizarán el despliegue en tu clúster de Kubernetes para que utilice la nueva versión de la imagen. Este proceso elimina pasos manuales propensos a errores y garantiza consistencia.

Concepto Clave: El Pipeline de CI/CD como una Línea de Ensamblaje Automatizada

Imagina una fábrica moderna de automóviles. El chasis entra en una línea de ensamblaje y, en cada estación, se realizan tareas específicas de manera automática: soldadura, pintura, instalación del motor, pruebas de calidad. Nadie mueve el coche manualmente de una estación a otra; un sistema coordinado lo hace. Un pipeline de CI/CD es la línea de ensamblaje para tu software. El chasis es tu commit de código. Las estaciones son los steps (pasos) en tu workflow: la primera estación verifica que el código compile (linting), la segunda ejecuta las pruebas unitarias, la tercera construye el contenedor Docker, la cuarta lo publica, y la quinta lo despliega en el entorno de producción (Kubernetes). Si una prueba falla en cualquier estación, la línea se detiene, evitando que un defecto llegue al final.

En el contexto de Machine Learning, esta analogía es aún más poderosa. No solo ensamblas código, sino un artefacto de modelo. Tu pipeline puede incluir estaciones adicionales para validar la precisión del modelo contra un dataset de prueba, o para ejecutar pruebas de sesgo/drift. La integración continua (CI) se encarga de la parte de "calidad" (pruebas, construcción), asegurando que cada cambio es viable. El despliegue continuo (CD) se encarga de la parte de "entrega", llevando ese cambio validado a producción de forma automática. Juntos, forman un ciclo de retroalimentación rápida que es esencial para la evolución ágil de los sistemas de ML.

Cómo funciona en la práctica: Anatomía de un Workflow de GitHub Actions para Kubernetes

El mecanismo se implementa mediante un archivo YAML ubicado en tu repositorio, en la ruta .github/workflows/deploy.yml. GitHub Actions detecta este archivo y lo ejecuta según los triggers (disparadores) que definas, como un push a una rama. El workflow contiene uno o más jobs que se ejecutan en runners (máquinas virtuales proporcionadas por GitHub). Para interactuar con Kubernetes, necesitaremos herramientas específicas en el runner y credenciales seguras para acceder al clúster.

El proceso paso a paso para nuestro proyecto integrador será: 1) Checkout del código: El job obtiene una copia de tu último commit. 2) Configuración del entorno: Se instalan Python, dependencias y se ejecutan pruebas (linting, unit tests). 3) Autenticación con Docker Registry: Usamos secrets de GitHub para loguearnos en Docker Hub. 4) Construcción y Push de la Imagen Docker: Se construye la imagen, se etiqueta con el SHA del commit y se sube al registro. 5) Autenticación con Kubernetes: Configuramos kubectl en el runner usando el archivo kubeconfig (almacenado como secret). 6) Despliegue en Kubernetes: Actualizamos la imagen en nuestro archivo de despliegue YAML y aplicamos los cambios con kubectl apply. Opcionalmente, podemos agregar un paso para verificar el estado del rollout.

Tip de Seguridad Crítico: Nunca almacenes credenciales (contraseñas, tokens, archivos kubeconfig) directamente en el código del workflow. Utiliza siempre la funcionalidad de Secrets de GitHub. Desde la configuración de tu repositorio, puedes definir secrets como DOCKER_PASSWORD o KUBE_CONFIG. Luego, en tu archivo YAML, los referencias como ${{ secrets.NOMBRE_SECRET }}. GitHub los inyecta de forma segura en el entorno de ejecución.

Código en acción: Workflow Completo de GitHub Actions

A continuación, se presenta un archivo de workflow completo y funcional. Este ejemplo asume que tu aplicación de ML es una API REST (usando Flask/FastAPI) y que tienes un Dockerfile en la raíz del proyecto. También asume que tu despliegue de Kubernetes está definido en un archivo k8s-deployment.yaml en tu repositorio, que hace referencia a una imagen con un tag específico como mi-usuario/ml-model:latest. El workflow actualizará ese tag dinámicamente.

name: CI/CD Pipeline for ML Model

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

env:
  REGISTRY: docker.io
  IMAGE_NAME: ${{ github.repository }} # Usa 'tu-usuario/tu-repo'

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.9'

      - name: Install Dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest pylint black # Herramientas de calidad

      - name: Lint with pylint
        run: |
          pylint --fail-under=7.0 app/ || true # Ejemplo, no falla el workflow

      - name: Test with pytest
        run: |
          pytest tests/ -v

      - name: Log in to Docker Hub
        if: github.event_name == 'push' # Solo en push, no en PR
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Extract metadata for Docker (tags, labels)
        if: github.event_name == 'push'
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix={{date 'YYYYMMDD'}}-,suffix=-{{sha}}
            latest

      - name: Build and push Docker image
        if: github.event_name == 'push'
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  deploy-to-kubernetes:
    needs: test-and-build # Este job depende del anterior
    if: github.event_name == 'push' # Solo despliega tras un push a main
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Configure Kubernetes
        uses: azure/setup-kubectl@v3
        with:
          version: 'latest'

      - name: Set Kubeconfig
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
          kubectl cluster-info # Verifica la conexión

      - name: Update Kubernetes Deployment Image
        run: |
          # Obtiene el tag de la imagen recién construida (el SHA)
          NEW_IMAGE_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
          # Usa 'sed' para reemplazar la imagen en el archivo YAML de despliegue
          sed -i 's|image: .*/${{ env.IMAGE_NAME }}:.*|image: '"$NEW_IMAGE_TAG"'|' k8s-deployment.yaml
          cat k8s-deployment.yaml # Opcional: ver el cambio

      - name: Deploy to Kubernetes Cluster
        run: |
          kubectl apply -f k8s-deployment.yaml
          kubectl rollout status deployment/ml-model-deployment --timeout=180s
          kubectl get pods -l app=ml-model

Este workflow define dos jobs secuenciales. El primer job, test-and-build, se ejecuta tanto en push como en pull request, asegurando calidad. El segundo job, deploy-to-kubernetes, solo se ejecuta tras un push exitoso a la rama main, y depende (needs) de que el primer job haya terminado sin errores. Nota el uso de sed para modificar dinámicamente el tag de la imagen en el archivo YAML de Kubernetes, inyectando el SHA único del commit. Esto garantiza que el clúster despliegue la imagen exacta que acaba de ser probada y construida.

Errores comunes y cómo evitarlos

Al implementar este pipeline, varios obstáculos suelen aparecer. Conocerlos de antemano te ahorrará horas de depuración.

1. Permisos insuficientes en el clúster de Kubernetes: El servicio account por defecto que usa kubectl en el runner probablemente no tenga permisos para modificar deployments. Cómo evitarlo: Crea un ServiceAccount específico en tu clúster con los roles RBAC necesarios (ej., edit o admin en el namespace correspondiente). Genera un token permanente o un kubeconfig para ese ServiceAccount y almacénalo como secret. No uses las credenciales de administrador del clúster.

2. El tag "latest" y el caching de imágenes: Si tu despliegue en Kubernetes referencia image: mi-imagen:latest, Kubernetes puede no detectar que hay una nueva imagen con ese mismo tag en el registro. Cómo evitarlo: Nunca uses "latest" para despliegues automatizados. Usa tags únicos e inmutables como el SHA del commit (como en el ejemplo). Esto garantiza que cada despliegue sea idempotente y referencie una versión específica y conocida.

3. Secrets expuestos en los logs del workflow: Si por error haces un echo ${{ secrets.PASSWORD }}, la contraseña quedará visible en los logs públicos del job. Cómo evitarlo: Sé extremadamente cuidadoso al referenciar secrets. GitHub Actions ofusca automáticamente los valores de secrets que aparecen en los logs, pero si tú los imprimes explícitamente, se mostrarán. Revisa siempre tus scripts. Usa el paso de login especializado para Docker (docker/login-action) que maneja los secrets de forma segura.

4. Falta de manejo de rollback en caso de fallo: Si el nuevo despliegue falla (por ejemplo, el contenedor no se inicia), tu aplicación puede quedar inaccesible. Cómo evitarlo: Utiliza las estrategias de rollout de Kubernetes. En tu deployment.yaml, define strategy.rollingUpdate y límites de recursos (requests/limits). El comando kubectl rollout status --timeout usado en el workflow hará que el job falle si el despliegue no se completa, pero no revierte automáticamente. Para un pipeline más avanzado, considera implementar un rollback automático usando kubectl rollout undo si las verificaciones de salud posteriores al despliegue fallan.

5. Dependencias no fijadas (requirements.txt) o contextos de construcción inconsistentes: Si tu requirements.txt no tiene versiones fijadas (numpy==1.24.0), la construcción puede ser no reproducible. Cómo evitarlo: Usa siempre pip freeze > requirements.txt en un entorno virtual controlado para generar tus dependencias. Considera el uso de herramientas como pip-tools o poetry para un manejo más robusto. Además, verifica que tu Dockerfile copie y instale las dependencias antes del código de la aplicación para aprovechar las capas cacheadas de Docker.

Checklist de dominio

Antes de considerar esta lección completa, asegúrate de poder verificar y realizar cada uno de los siguientes puntos:

  • Comprendo la diferencia entre Integración Continua (CI) y Despliegue Continuo (CD) y su valor en el ciclo de vida de un modelo de ML.
  • Puedo explicar la función de un archivo de workflow de GitHub Actions (.github/workflows/*.yaml) y sus componentes principales: triggers, jobs, steps, y uses/run.
  • He configurado secrets en un repositorio de GitHub para almacenar de forma segura credenciales de Docker Registry y de acceso a Kubernetes (kubeconfig o token).
  • Puedo escribir un workflow que construya y etiquete una imagen Docker tras un push, usando el SHA del commit como tag único e inmutable.
  • Sé cómo modificar dinámicamente un archivo de manifiesto de Kubernetes (YAML) dentro de un job para actualizar la referencia a la nueva imagen construida.
  • Puedo configurar la autenticación de kubectl en un runner de GitHub Actions y ejecutar comandos para aplicar despliegues y verificar su estado.
  • Identifico al menos tres errores comunes en pipelines de CI/CD para Kubernetes (ej., permisos, tags, secrets) y sé cómo mitigarlos.
  • He ejecutado con éxito un pipeline completo que, al hacer push a mi rama main, despliega automáticamente una nueva versión de mi modelo en un clúster de Kubernetes de prueba.
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión