DevOps Intermedio - Avanzado

GitOps + Supply-Chain Security (SLSA/Nix/OCI): entrega confiable de extremo a extremo en 1 repositorio

En el ecosistema Cloud Native actual, tener un pipeline "verde" ya no es suficiente; necesitas garantizar que lo que se ejecuta en producción es legítimo, seguro y trazable. En este tutorial integral, …

Publicado: 22/11/2025 Por: Juan Felipe Orozco Cortés Duración: 30 - 45 minutos Nivel: Intermedio - Avanzado
Contenido del tutorial

Si alguna vez has pensado “mi pipeline está todo verde, pero no tengo ni idea de qué imagen terminó en Kubernetes”, este tutorial es para ti. Vamos a construir un mini-monorepo llamado gitops-supplychain-demo que une, en un solo flujo, piezas que normalmente se explican por separado: entorno reproducible con Nix, CI que firma artefactos, SBOM automático y despliegues GitOps. Tu recompensa: entender claramente qué pasa desde el primer commit hasta que tu app vive en staging y prod.

FastAPI Docker Nix SBOM + Signing Kustomize ArgoCD CI/CD

A lo largo de este tutorial verás, sin atajos ni magia, el flujo completo:

Hago un commit → CI construye, genera SBOM, escanea y firma → GitOps sincroniza staging/prod.

Desde este repo mínimo podrás copiar la estructura, adaptar la app y llevar esta filosofía de reproducibilidad + seguridad de la cadena de suministro a todos tus proyectos. Aquí no buscamos memorizar comandos; buscamos entender el mecanismo que vuelve confiable, rastreable y verificable cada despliegue.

¿En qué orden leer el repo y cada carpeta? (2 min)

Para no ahogarnos en archivos, vamos a recorrer el monorepo en un orden que tenga sentido: primero app/ (la app FastAPI y su Dockerfile), luego el entorno reproducible con flake.nix, después el pipeline de CI que construye, genera el SBOM, escanea y firma la imagen, y por último deploy/ y deploy/gitops/argocd/, donde entra en juego GitOps para llevar todo a staging y prod.

gitops-supplychain-demo
├── README.md
├── app/
│   ├── Dockerfile
│   ├── main.py
│   └── requirements.txt
├── deploy/
│   ├── README.md
│   ├── base/
│   │   ├── deployment.yaml
│   │   ├── kustomization.yaml
│   │   └── service.yaml
│   ├── gitops/
│   │   └── argocd/
│   │       ├── application-prod.yaml
│   │       └── application-staging.yaml
│   ├── prod/
│   │   ├── deployment-patch.yaml
│   │   └── kustomization.yaml
│   └── staging/
│       ├── deployment-patch.yaml
│       └── kustomization.yaml
├── flake.lock
└── flake.nix

1. app/: la aplicación mínima FastAPI

Antes de hablar de Nix, CI, SBOM o GitOps, necesitamos algo fundamental: la aplicación que vamos a entregar. En este repositorio la carpeta app/ contiene una pequeña API escrita con FastAPI, diseñada para ser simple, observable y fácil de empaquetar. Todo lo demás —los builds reproducibles, las firmas, los despliegues y las promociones automáticas— existe para mover este pequeño servicio desde un commit hasta staging y prod de forma verificable. Por eso empezamos aquí: esta app mínima es el “payload” que recorrerá toda la supply chain.

Comenzamos por app/ porque toda la cadena de suministro gira alrededor de un artefacto muy concreto: una imagen Docker construida a partir de esta aplicación FastAPI. La app expone dos endpoints esenciales: /, que devuelve un mensaje simple para validar que el servicio responde, y /health, que será consumido por Kubernetes para verificar liveness y readiness. Aunque es mínima, esta aplicación es suficiente para demostrar todo el flujo de seguridad y GitOps: es el componente que vamos a construir, escanear, firmar, verificar y desplegar en staging y producción.

1.2 Código y explicación de main.py

En esta sección vamos a mirar el archivo app/main.py, que define la aplicación FastAPI mínima del proyecto. Aquí es donde nace nuestro servicio: una API con una ruta raíz / y un endpoint de salud /health. En tu bloque de código verás el contenido completo de main.py, y en estos paneles vamos a entender por qué está escrito así y cuál es su papel dentro de toda la cadena de suministro.

from fastapi import FastAPI

app = FastAPI(title="GitOps Supply-Chain Demo")

@app.get("/")
def read_root():
    return {
        "message": "Hola desde gitops-supplychain-demo",
        "status": "ok",
    }

@app.get("/health")
def health_check():
    return {"status": "healthy"}
📦 1) ¿Qué contiene realmente main.py?

El archivo main.py hace solo tres cosas, pero muy importantes:

1. Crea una instancia de FastAPI con el título "GitOps Supply-Chain Demo", que identifica la aplicación en documentación y herramientas.
2. Define un endpoint / que devuelve un pequeño JSON con un mensaje de bienvenida y un campo status para comprobar rápidamente que el contenedor responde.
3. Define un endpoint /health que devuelve siempre {"status": "healthy"}, pensado específicamente para ser usado por los probes de Kubernetes.

➤ En tu bloque de código deberías ver exactamente eso: creación de la app, ruta / y ruta /health, sin lógica extra ni dependencias adicionales.
🧠 2) ¿Por qué esta app mínima es suficiente para el tutorial?

Aunque el código es muy pequeño, es perfecto para nuestro objetivo: mostrar el viaje completo de un servicio a través de una cadena de suministro segura. No necesitamos lógica de negocio compleja; lo que queremos ver es: quién construye la imagen, cómo se genera el SBOM, cómo se escanea, cómo se firma y cómo GitOps la lleva a staging y producción.

El endpoint /health también es clave: más adelante, en los manifests de Kubernetes, lo usaremos como livenessProbe y readinessProbe. Es una práctica estándar en servicios modernos y encaja perfecto con la idea de observabilidad desde el día 0.

La app es simple a propósito: cualquier error o comportamiento raro que veas más adelante casi siempre vendrá de la cadena de entrega (Docker, CI, firmas, GitOps), no de la lógica de negocio.
💡 3) Recomendaciones prácticas para tu propio código

✔ Para demos de supply chain security, mantén la app lo más pequeña posible: así puedes concentrarte en el flujo de entrega, no en bugs de negocio.

✔ Incluye siempre un endpoint de salud explícito (/health, /ready, etc.): te ahorra tiempo al configurar Kubernetes, balanceadores y chequeos externos.

✔ Evita añadir dependencias innecesarias en esta etapa: cuanto más limpio sea el entorno, más legible será el SBOM y más clara la salida del escáner de vulnerabilidades.

Puedes extender esta app (base de datos, autenticación, métricas, etc.) una vez tengas dominado el flujo completo de construcción, firma y despliegue. Primero domina la pipeline, luego crece la lógica.

1.3 Dependencias mínimas: explicación de requirements.txt

Antes de hablar de imágenes Docker, SBOM o escáneres de vulnerabilidades, necesitamos fijar algo básico: las dependencias exactas de la aplicación. En requirements.txt solo declaramos dos líneas: fastapi==0.115.0 y uvicorn[standard]==0.30.0. Son pocas, pero están ancladas a versiones concretas. Esto es lo que permite que el SBOM sea estable, que los escaneos de seguridad sean repetibles y que el entorno que uses tú sea el mismo que verá el pipeline de CI y cualquier otra persona que clone el repo.

fastapi==0.115.0
uvicorn[standard]==0.30.0
📦 1) ¿Qué declara exactamente requirements.txt?

En tu bloque de código deberías ver únicamente:

fastapi==0.115.0
uvicorn[standard]==0.30.0

FastAPI es el framework web que usamos para definir la API (la app que viste en main.py).
Uvicorn es el servidor ASGI que realmente expone la aplicación HTTP. El sufijo [standard] indica que instalamos Uvicorn con un conjunto de extras recomendados (por ejemplo, mejoras de rendimiento y soporte HTTP más eficiente), sin tener que listarlos uno por uno.

➤ Con solo estas dos líneas ya tienes todo lo necesario para servir la API y ejecutar el contenedor. No hay ORM, no hay librerías de autenticación, no hay “magia”: solo lo mínimo para este demo.
🧠 2) ¿Por qué fijar versiones concretas (==) en un tutorial de supply chain?

Usar == en lugar de >= no es un capricho: es una decisión de reproducibilidad. Si las versiones cambian silenciosamente, también cambia:

• El contenido del SBOM generado por Syft.
• El resultado del escaneo de vulnerabilidades con Trivy.
• El comportamiento de la app dentro del contenedor (posibles breaking changes).

Al fijar fastapi==0.115.0 y uvicorn[standard]==0.30.0 podemos decir: “si clonas este repo hoy o en seis meses, la imagen que construyas tendrá las mismas dependencias, y los análisis de seguridad serán comparables”.

Para una cadena de suministro confiable, la primera condición es que los artefactos sean reproducibles. requirements.txt es uno de los puntos donde se gana (o se pierde) esa propiedad.
💡 3) Buenas prácticas al definir dependencias para servicios productivos

✔ Mantén la lista de dependencias lo más corta posible: menos paquetes = menos superficie de ataque y SBOM más legible.

✔ Fija versiones en producción y actualiza de forma controlada (ej. por sprint), revisando los cambios de vulnerabilidades en cada bump.

✔ Evita meter librerías “por si acaso”: cada dependencia extra es un módulo más que puede aparecer mañana en un reporte de CVEs.

En este tutorial usamos solo FastAPI + Uvicorn porque queremos que el foco esté en la cadena de entrega, no en la complejidad de la app. En tus proyectos reales, puedes ampliar esta lista, pero siempre con intención y con un ojo puesto en SBOM + seguridad.

1.4 Dockerfile de la app: de script Python a imagen OCI

La carpeta app/ no solo contiene el código de la API, también define cómo se empaqueta en una imagen Docker lista para ser analizada, firmada y desplegada. Este Dockerfile toma un runtime de Python 3.12, instala exactamente las dependencias de requirements.txt, copia el código de la aplicación y arranca Uvicorn escuchando en el puerto 8000. Esta imagen es el artefacto central de todo el tutorial: es lo que construye CI, lo que aparece en el SBOM, lo que escanean las herramientas de seguridad y lo que finalmente despliega GitOps en staging y prod.

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
📦 1) Recorrido línea por línea del Dockerfile

El Dockerfile hace, en esencia, estos pasos:

1. Imagen base: FROM python:3.12-slim
Usamos una imagen oficial de Python, versión 3.12, en variante slim para reducir tamaño y superficie de ataque.

2. Configuración de entorno:
PYTHONDONTWRITEBYTECODE=1 evita que se generen archivos .pyc dentro del contenedor.
PYTHONUNBUFFERED=1 hace que los logs se envíen sin buffer, ideal para verlos en tiempo real en Docker/Kubernetes.

3. Directorio de trabajo: WORKDIR /app
A partir de aquí, todos los comandos se ejecutan dentro de /app en el contenedor.

4. Instalación de dependencias:
Primero se copia solo requirements.txt y se ejecuta:
RUN pip install --no-cache-dir -r requirements.txt
Esto instala FastAPI y Uvicorn sin dejar caché de pip, lo que mantiene la imagen más ligera.

5. Copia del código: COPY . .
Copiamos el resto de los archivos de la app dentro de /app (entre ellos, main.py).

6. Puertos y comando de arranque:
EXPOSE 8000 documenta que el servicio escucha en el puerto 8000.
El comando final:
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
lanza Uvicorn apuntando a la aplicación FastAPI definida en main.py (objeto app).

Este patrón (instalar dependencias, copiar código, exponer puerto y arrancar servidor) es el esqueleto clásico de un servicio Python listo para Kubernetes.
🧠 2) ¿Por qué este Dockerfile es adecuado para supply chain security?

Este Dockerfile está pensado para ser predecible y fácil de analizar:

• Usa una imagen base oficial y conocida (python:3.12-slim), lo que facilita identificar vulnerabilidades en el SBOM y en los escaneos.
• Separa la instalación de dependencias (requirements.txt) de la copia del código, lo que mejora la cacheabilidad de los builds en CI.
• Evita pasos innecesarios (no hay toolchains extra, compiladores, etc.), de modo que la imagen resultante es más pequeña y el SBOM más legible.

A la hora de correr Syft, Trivy o Cosign sobre la imagen, esta estructura limpia ayuda a entender rápidamente qué entra en juego: una base de Python + FastAPI + Uvicorn + tu código.

En supply chain, menos ruido = menos sorpresas. Un Dockerfile sencillo pero explícito es un gran aliado para auditar qué se está ejecutando realmente en producción.
💡 3) Consejos para tus propios Dockerfiles de servicios Python

✔ Prefiere imágenes base oficiales y con tag concreto (por ejemplo python:3.12-slim), en lugar de latest.

✔ Instala dependencias antes de copiar todo el código: esto hace que Docker pueda reutilizar capas entre builds si requirements.txt no cambia.

✔ Usa --no-cache-dir en pip para evitar residuos innecesarios dentro de la imagen.

✔ Documenta claramente el puerto con EXPOSE y arranca el servidor con un comando explícito (como hacemos con Uvicorn).

Más adelante, cuando entremos a Nix, SBOM y firmas, verás que todo gira alrededor de esta imagen. Cuanto más claro y minimalista sea tu Dockerfile, más fácil será confiar (y demostrar por qué confías) en tu cadena de entrega.

2. flake.nix: entorno reproducible con Nix

Antes de hablar de CI, firmas o GitOps, necesitamos algo crucial: un entorno de desarrollo y build que sea totalmente reproducible. Aquí es donde entra Nix. En este tutorial no usamos Nix como sistema operativo, sino como herramienta para fijar versiones exactas de Python, Git y Docker CLI, garantizando que tanto tú como el pipeline construyen siempre desde el mismo entorno.

El archivo flake.nix es el corazón de esa reproducibilidad: define un devShell consistente, declarativo y portable. Cuando ejecutas nix develop, entras en un entorno idéntico al del CI, sin importar qué sistema operativo uses, qué versiones tengas instaladas localmente o cómo esté configurado tu PATH. Esto elimina el clásico “en mi máquina funciona” y sienta las bases para una supply chain verificable.

2.1 ¿Qué hace exactamente flake.nix?

Este archivo define un entorno reproducible basado en Nix que asegura que tú, tu equipo y el pipeline CI comparten exactamente la misma versión de Python, Git y Docker CLI. Aquí no instalas nada en tu sistema: simplemente entras a un entorno aislado y determinista con nix develop. En esta sección vamos a analizar línea por línea qué declara el flake y por qué es tan importante para una cadena de suministro confiable.

{
  description = "DevShell para gitops-supplychain-demo (GitOps + supply chain)";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  };

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in {
      devShells.${system}.default = pkgs.mkShell {
        name = "gitops-supplychain-demo-shell";

        packages = with pkgs; [
          python312
          git
          docker-client
        ];

        shellHook = ''
          echo "🐚 Entorno Nix para gitops-supplychain-demo"
          echo " - python: $(python --version 2>/dev/null || echo 'no encontrado')"
          echo " - docker: $(docker --version 2>/dev/null || echo 'docker CLI no disponible o sin daemon')"
          echo ""
          echo "Sugerido: ejecutar dentro de este shell:"
          echo "  docker build -t gitops-supplychain-demo-app:dev app/"
        '';
      };
    };
}
📦 1) ¿Qué define un flake de Nix?

Un flake es un archivo declarativo que especifica inputs (dependencias) y outputs (entornos, paquetes, configuraciones). En nuestro caso, la salida principal es un devShell: un entorno listo para trabajar que contiene Python 3.12, Git y Docker CLI.

Esto garantiza que tanto tu máquina como el pipeline de CI usen exactamente las mismas versiones de herramientas, lo cual es fundamental para reproducibilidad.

➤ Piensa en Nix como un “pip + pyenv + brew + asdf + virtualenv” pero 100% reproducible y sin efectos colaterales.
🧠 2) Línea por línea: ¿qué hace este flake.nix?

inputs: Importa nixpkgs desde GitHub (canal nixos-24.05). Este canal define exactamente la versión de todas las herramientas que usaremos.

system: Declaramos x86_64-linux como arquitectura, compatible con WSL y CI.

pkgs.mkShell {...}: Aquí se construye el entorno del desarrollador. Se instalan:

  • python312 — runtime de Python para ejecutar la app
  • git — usado por CI y GitOps
  • docker-client — permite construir y subir imágenes sin instalar Docker completo

shellHook: se ejecuta automáticamente cuando entras a nix develop. Muestra versiones activas y sugiere el comando para construir la imagen de la app.

🔐 3) ¿Por qué esto mejora la seguridad de la cadena de suministro?

Nix evita variaciones entre máquinas: si tú construyes la imagen y el CI también, ambos lo hacen con el mismo Python, el mismo Docker CLI y el mismo conjunto de dependencias del sistema.

Esto reduce:

  • ❌ diferencias de compilación
  • ❌ imágenes “cocinadas” con herramientas distintas
  • ❌ inconsistencias que afectan el SBOM y los escaneos

En supply chain security, la reproducibilidad es una forma de defensa: si todos construyen con lo mismo, también pueden verificar lo mismo.
💡 4) Consejos prácticos para trabajar con Nix en proyectos reales

✔ Nunca instales dependencias globales. Entra siempre con nix develop.

✔ Si agregas herramientas al pipeline, agrégalas también aquí para mantener consistencia.

✔ Puedes crear devShells separados para frontend, backend, CI, etc., todo declarativo.

Este flake es la versión mínima viable, pero te permite construir proyectos reproducibles desde el día 1.

3. CI: pipeline de build, SBOM, escaneo, firma y verificación

Hasta ahora tenemos tres piezas clave: la app FastAPI (app/), la imagen Docker que la empaqueta y un entorno reproducible con flake.nix. El siguiente paso lógico es automatizar todo ese flujo en CI. Ahí entra .github/workflows/ci-sbom.yml: este workflow construye la imagen, la publica en un registry, genera el SBOM, ejecuta un escáner de vulnerabilidades, firma la imagen con Cosign y, por último, verifica que la firma sea válida usando identidad de GitHub Actions.

En otras palabras: este archivo convierte un simple git push en una cadena de suministro automatizada. A partir de este punto, cada commit a master produce una imagen trazable, con SBOM adjunto, analizada por Trivy y firmada de forma verificable antes de ser consumida por GitOps.

3.1 Visión general de .github/workflows/ci-sbom.yml

En tu bloque de código verás el contenido completo del workflow ci-sbom.yml. A alto nivel define dos jobs:

  • sbom: construye la imagen, la sube a GHCR, genera el SBOM, ejecuta Trivy y firma con Cosign.
  • verify-signature: verifica que la imagen esté firmada correctamente antes de continuar.

Veamos cada parte con más detalle:

name: CI - Build, SBOM, Scan and Sign

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

permissions:
  contents: read
  packages: write
  id-token: write

jobs:
  sbom:
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/gitops-supplychain-demo-app

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Build app image
        run: |
          docker build -t $IMAGE_NAME:${{ github.sha }} app/

      - name: Login to GitHub Container Registry
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

      - name: Push image to GHCR
        run: |
          docker push $IMAGE_NAME:${{ github.sha }}

      - name: Install Syft
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
            | sh -s -- -b /usr/local/bin

      - name: Generate SBOM (CycloneDX JSON)
        run: |
          syft "registry:$IMAGE_NAME:${{ github.sha }}" \
            -o cyclonedx-json=sbom-app.json

      - name: Upload SBOM artifact
        uses: actions/upload-artifact@v4
        with:
          name: sbom-app
          path: sbom-app.json

      - name: Scan image with Trivy (vulnerabilities)
        uses: aquasecurity/trivy-action@0.24.0
        with:
          image-ref: ${{ env.IMAGE_NAME }}:${{ github.sha }}
          format: table
          vuln-type: 'os,library'
          severity: 'CRITICAL,HIGH,MEDIUM,LOW'
          ignore-unfixed: true
          exit-code: '0'

      - name: Install Cosign
        uses: sigstore/cosign-installer@v3.5.0
        with:
          cosign-release: 'v2.4.0'

      - name: Sign image with Cosign (keyless)
        env:
          COSIGN_EXPERIMENTAL: "1"
        run: |
          cosign sign -y $IMAGE_NAME:${{ github.sha }}

  verify-signature:
    needs: sbom
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/gitops-supplychain-demo-app

    steps:
      - name: Install Cosign
        uses: sigstore/cosign-installer@v3.5.0
        with:
          cosign-release: 'v2.4.0'

      - name: Verify image signature (keyless)
        env:
          COSIGN_EXPERIMENTAL: "1"
        run: |
          cosign verify $IMAGE_NAME:${{ github.sha }} \
            --certificate-identity "https://github.com/${{ github.repository }}" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com"
📌 1) Disparadores y permisos del workflow

El workflow se ejecuta en dos casos:

  • on.push.branches: [ master ] — cada push a master.
  • on.pull_request.branches: [ master ] — cada PR apuntando a master.

Además, declara permisos mínimos siguiendo buenas prácticas:

  • contents: read — para leer el código del repo.
  • packages: write — necesario para subir imágenes a GHCR.
  • id-token: write — requerido por Cosign para firmar de forma keyless usando OIDC.
➤ Esta combinación convierte a GitHub Actions en una autoridad de firma: sólo los jobs legítimos del repo pueden emitir firmas válidas para tus imágenes.
⚙️ 2) Job sbom: build, publish, SBOM, scan, sign

El job sbom se ejecuta en ubuntu-latest y define una variable clave: IMAGE_NAME = ghcr.io/${'{'}{ github.repository_owner }{'}'}/gitops-supplychain-demo-app.

En los pasos principales hace lo siguiente:

  • Checkout del repo con actions/checkout@v4.
  • Build de la imagen Docker con el Dockerfile de app/.
  • Login a GitHub Container Registry usando GITHUB_TOKEN.
  • Push de la imagen a GHCR con el tag ${'{'}{ github.sha }{'}'}.
  • Instalación de Syft para generar el SBOM.
  • Generación del SBOM en formato CycloneDX JSON a partir de la imagen en el registry.
  • Upload del SBOM como artefacto descargable en la UI de Actions.
  • Escaneo de vulnerabilidades usando Trivy sobre la misma imagen.
  • Instalación de Cosign y firma keyless de la imagen.
➤ Este job ya produce todo lo que te interesa para supply chain: imagen construida, SBOM adjunto, vulnerabilidades listadas y firma asociada a la identidad del workflow.
✅ 3) Job verify-signature: asegurar que sólo usamos imágenes firmadas

El job verify-signature declara needs: sbom, lo que significa que sólo se ejecuta si el job sbom termina correctamente. Su objetivo es simple pero crítico: fallar el pipeline si la imagen no tiene una firma válida emitida por este repo.

El job:

  • Vuelve a instalar Cosign.
  • Usa cosign verify sobre la misma referencia de imagen en GHCR.
  • Verifica que el certificado de firma:
    • Tenga identidad https://github.com/${'{'}{ github.repository }{'}'}.
    • Haya sido emitido por https://token.actions.githubusercontent.com.

Si cualquiera de esas condiciones falla (otra imagen, otro repo, o firma ausente), el comando cosign verify devuelve error y el job queda en rojo.

➤ Esta verificación es lo que te permite, más adelante, exigir que sólo se desplieguen imágenes que vengan de tu CI oficial y estén firmadas correctamente.
💡 4) Cómo leer este workflow en la UI de GitHub Actions

Cuando hagas git push:

  • En la pestaña Actions verás un run asociado al commit.
  • Dentro del job sbom podrás abrir:
    • El log del build de Docker.
    • La salida de Syft (generación del SBOM).
    • La tabla de vulnerabilidades encontrada por Trivy.
    • La confirmación de firma de Cosign.
  • En el job verify-signature verás el resultado de cosign verify validando la identidad del firmante.
Te recomiendo capturar capturas de pantalla de estos logs para tu documentación o para enseñar el flujo a tu equipo.

4. Manifests de Kubernetes: base, overlays y GitOps

Hasta este punto ya tenemos una app (app/), una imagen Docker reproducible (Dockerfile + Nix) y un pipeline de CI que construye, escanea y firma artefactos. Lo siguiente es definir dónde y cómo va a ejecutarse la aplicación. Aquí entra en juego la carpeta deploy/, que contiene tres capas importantes:

  • deploy/base/ — La definición genérica de la aplicación en Kubernetes: Deployment, Service y Kustomization base.
  • deploy/staging/ y deploy/prod/ — Overlays que modifican la imagen y la capacidad según el entorno.
  • deploy/gitops/argocd/ — Las aplicaciones ArgoCD que sincronizan cada entorno con GitHub, habilitando un flujo GitOps completo.

Aquí es donde Kubernetes consume la imagen que ya fue firmada y verificada por CI. Y es también donde GitOps toma control: a partir de ahora, no hacemos kubectl apply. Los cambios entran por Git, y ArgoCD decide qué debe correr en staging y producción.

En las siguientes subsecciones explicaremos cada archivo clave de deploy/base/ y luego veremos cómo los overlays de staging/ y prod/ sustituyen la imagen (por ejemplo :staging o :prod) y ajustan réplicas u otros parámetros sin duplicar YAML. Finalmente revisaremos las aplicaciones de ArgoCD que completan el circuito GitOps del tutorial.

4.1 Deployment base: deploy/base/deployment.yaml

Este archivo define el Deployment base de Kubernetes para nuestra app. Es la plantilla mínima que describe cómo debe correr el contenedor en el cluster: cuántas réplicas hay, qué imagen usar, en qué puerto escucha y cómo sabe Kubernetes si el servicio está sano. Más adelante, los overlays de staging y prod tomarán este Deployment como base y sólo aplicarán pequeños parches (imagen y réplicas) usando Kustomize.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gitops-supplychain-demo
  labels:
    app: gitops-supplychain-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gitops-supplychain-demo
  template:
    metadata:
      labels:
        app: gitops-supplychain-demo
    spec:
      containers:
        - name: app
          image: ghcr.io/OWNER/gitops-supplychain-demo-app:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 10
            periodSeconds: 20
📌 1) Esqueleto del Deployment: apiVersion, kind, metadata, selector

La cabecera del manifiesto establece que estamos definiendo un Deployment estándar de Kubernetes:

  • apiVersion: apps/v1 — versión estable para Deployments.
  • kind: Deployment — objeto que gestiona réplicas de Pods idénticos.
  • metadata.name: gitops-supplychain-demo — nombre lógico del Deployment.
  • metadata.labels.app: gitops-supplychain-demo — etiqueta que se reutiliza en otros recursos.

En spec.selector.matchLabels.app se define el “pegamento” entre el Deployment y los Pods: cualquier Pod con la label app: gitops-supplychain-demo es gestionado por este Deployment.

➤ La consistencia de la label app: gitops-supplychain-demo entre metadata.labels, selector.matchLabels y el template es clave para que el Deployment funcione.
🧱 2) Plantilla de Pod: contenedor, imagen y puerto

Dentro de spec.template definimos cómo luce cada Pod que el Deployment va a crear:

  • template.metadata.labels.app repite la misma label para que el selector los encuentre.
  • En template.spec.containers aparece un único contenedor llamado app.

Los campos clave del contenedor son:

  • image: ghcr.io/OWNER/gitops-supplychain-demo-app:latest — imagen base que usará el entorno base.
  • imagePullPolicy: IfNotPresent — sólo descarga la imagen si no existe localmente en el nodo.
  • ports.containerPort: 8000 — puerto donde escucha la app FastAPI (coincide con Uvicorn).
➤ En los overlays de staging y prod no redefinimos todo el Deployment: sólo parcheamos la imagen (por ejemplo, :staging, :prod) y las réplicas.
🩺 3) Probes HTTP: usando /health como contrato con Kubernetes

Este Deployment aprovecha directamente el endpoint /health que definimos en main.py:

  • readinessProbe — indica cuándo el Pod está listo para recibir tráfico.
  • livenessProbe — indica si el contenedor sigue vivo o debe ser reiniciado.

Ambos probes usan:

  • httpGet.path: /health
  • httpGet.port: 8000

Y tiempos distintos:

  • readinessProbe.initialDelaySeconds: 5, periodSeconds: 10.
  • livenessProbe.initialDelaySeconds: 10, periodSeconds: 20.

Esto significa que Kubernetes:

  • Primero espera un poco a que la app levante.
  • Luego consulta periódicamente /health para saber si puede enviar tráfico al Pod.
  • Si el livenessProbe falla muchas veces, reinicia el contenedor automáticamente.
➤ Este es un buen ejemplo de diseño orientado a operaciones: el endpoint /health no es un adorno, es parte del contrato entre tu app y la plataforma.
📈 4) ¿Por qué sólo 1 réplica en el base?

En spec.replicas: 1 definimos una sola réplica en el manifiesto base. Eso mantiene el ejemplo simple y hace que los cambios de entorno sean muy claros:

  • Base: 1 réplica, imagen :latest (plantilla genérica).
  • Staging: 1 réplica, imagen :staging (patch con Kustomize).
  • Prod: 3 réplicas, imagen :prod (patch con Kustomize).

Este patrón ayuda a que el lector entienda que: los manifiestos base definen el modelo común y los overlays ajustan capacidad y estrategia de publicación por entorno.

Más adelante, si quieres añadir recursos, tolerations o límites de CPU/memoria, puedes hacerlo aquí en el base o con patches específicos por entorno.

4.2 Service base: exponer la API dentro del clúster

El Deployment por sí solo sólo arranca pods; si nadie sabe cómo llegar a ellos, tu API sigue “encerrada” dentro de Kubernetes. El archivo deploy/base/service.yaml define un objeto Service de tipo ClusterIP que agrupa los pods etiquetados como app: gitops-supplychain-demo y los expone en un puerto estable dentro del clúster. Más adelante, otras piezas (Ingress, gateways o servicios internos) hablarán siempre con este Service, sin importar cuántos pods haya ni en qué nodo estén corriendo.

apiVersion: v1
kind: Service
metadata:
  name: gitops-supplychain-demo
  labels:
    app: gitops-supplychain-demo
spec:
  type: ClusterIP
  selector:
    app: gitops-supplychain-demo
  ports:
    - name: http
      port: 80
      targetPort: 8000
📦 1) ¿Qué resuelve exactamente service.yaml?

El Service actúa como una “fachada estable” para tus pods. Aunque Kubernetes pueda mover o recrear los pods del Deployment, el Service mantiene:

1. Un nombre DNS estable dentro del clúster (gitops-supplychain-demo dentro del namespace).
2. Un selector basado en etiquetas (app: gitops-supplychain-demo) para decidir qué pods forman parte del backend.
3. Un puerto “bonito” (80) que se mapea al targetPort 8000 donde escucha Uvicorn dentro del contenedor.

En resumen: los pods pueden nacer, morir y moverse, pero el Service siempre es gitops-supplychain-demo:80 para el resto del clúster.
⚙️ 2) Campos clave: type, selector y ports

Los campos más importantes de este Service son:

spec.type: ClusterIP
Es el tipo más habitual para servicios internos. Sólo es accesible desde dentro del clúster: perfecto para entornos donde un Ingress, API Gateway o servicio de mesh se encargará de exponerlo hacia fuera.

selector.app: gitops-supplychain-demo
Debe coincidir exactamente con las etiquetas definidas en el Deployment. Si cambias el label en el Deployment pero no aquí, el Service se quedaría “sin pods” detrás.

ports.port: 80targetPort: 8000
El Service escucha en el puerto 80 (más estándar) y lo redirige al 8000 donde Uvicorn atiende dentro del contenedor. Así, cualquier cliente interno sólo necesita hablar con http://gitops-supplychain-demo:80.

Si cambias el puerto de Uvicorn en el Dockerfile, recuerda actualizar targetPort aquí y en las probes del Deployment.
🌐 3) ¿Cómo se usa este Service en un flujo GitOps real?

En el flujo GitOps de este tutorial, el Service vive en la capa base/ y es reutilizado tanto por staging como por prod a través de Kustomize. Eso significa que:

Mismo Service para ambos entornos, garantizando la misma topología de red.
✔ Lo que cambia entre staging y prod son los parches del Deployment (réplicas e imagen), no la forma en que se expone el servicio.
✔ ArgoCD sólo necesita apuntar a los overlays correctos; el Service base se aplica automáticamente como parte del conjunto de manifests.

Esta separación (base + overlays) es una de las claves para tener entornos coherentes: misma estructura, distinta configuración.
💡 4) Buenas prácticas al definir Services

✔ Usa nombres y etiquetas consistentes (app: gitops-supplychain-demo) en Deployment y Service para evitar sorpresas.
✔ Mantén ClusterIP para servicios internos y delega la exposición pública a un Ingress o gateway.
✔ Documenta claramente el port y el targetPort: son los valores que más se rompen cuando alguien cambia el runtime de la app.

Si en el futuro añades métricas o un puerto gRPC, puedes sumar más entradas en spec.ports manteniendo la misma “cara” del servicio hacia el clúster.

4.3 kustomization.yaml base: pegando Deployment + Service

Hasta ahora hemos definido dos piezas sueltas: deployment.yaml (los pods de la app) y service.yaml (el punto de entrada dentro del clúster). El archivo deploy/base/kustomization.yaml es el “orquestador” que le dice a Kustomize: “estos recursos forman un módulo base”. A partir de este módulo, los overlays de staging y prod podrán reutilizar exactamente la misma estructura, aplicando solo los cambios necesarios mediante parches.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml
  - service.yaml
📦 1) ¿Qué representa este kustomization.yaml base?

Este archivo declara una Kustomization de nivel base, es decir, un conjunto de manifests que se consideran el “mínimo común” entre todos los entornos:

✔ El Deployment con la definición genérica de pods y probes.
✔ El Service que expone la app dentro del clúster.
✔ Nada específico de staging o prod (réplicas, tags de imagen, namespaces).

Piensa en este archivo como un “módulo Kubernetes reutilizable” sobre el que los overlays construirán sus variaciones.
⚙️ 2) Campos clave: apiVersion, kind y resources

Aunque el contenido es corto, cada campo tiene un rol importante:

apiVersion: kustomize.config.k8s.io/v1beta1
Indica que estamos usando el formato de configuración de Kustomize, no un recurso “normal” de Kubernetes como Deployment o Service.

kind: Kustomization
Declara que este archivo describe cómo combinar otros manifests. No se aplica directamente al clúster; primero debe ser procesado por Kustomize.

resources:
Lista los ficheros que forman parte del módulo base: deployment.yaml y service.yaml. Cuando ejecutes kubectl kustomize deploy/base, Kustomize renderizará ambos recursos en un único stream de YAML listo para aplicar.

Si añades más piezas comunes (ConfigMaps, Roles, etc.), lo normal es ir sumándolas aquí en resources:.
🧠 3) ¿Cómo encaja esto en un flujo GitOps?

En el flujo GitOps del tutorial, los overlays de staging y prod no vuelven a redefinir el Service ni el Deployment desde cero. En vez de eso:

✔ Referencian esta base con resources: [ ../base ].
✔ Aplican patchesStrategicMerge para ajustar lo que cambia (réplicas, imagen, namespace).
✔ ArgoCD solo necesita apuntar al overlay adecuado; Kustomize se encarga de ensamblar base + parches.

Esta composición “base + overlays” es lo que permite tener entornos coherentes y auditar fácilmente qué manifiestos cambian entre staging y prod.
💡 4) Buenas prácticas al trabajar con Kustomize

✔ Mantén el kustomization.yaml base lo más genérico posible: nada de namespaces ni tags específicos de entorno.
✔ Usa rutas relativas simples en resources: facilitan que GitOps (ArgoCD/Flux) pueda apuntar a carpetas completas sin hacks.
✔ Si un recurso sólo aplica a un entorno (por ejemplo, un CronJob de limpieza en prod), colócalo directamente en el overlay correspondiente, no en la base.

Para repos de ejemplo como este, una base pequeña y clara es más valiosa que un árbol gigante con demasiadas variaciones.

5. Overlays por entorno: staging y prod

Con la base de Kubernetes definida en deploy/base/, el siguiente paso es algo que en GitOps es casi obligatorio: separar claramente los entornos. En este repositorio lo hacemos con overlays de Kustomize: deploy/staging/ y deploy/prod/. Ambos reutilizan exactamente los mismos manifests base (Deployment y Service) pero aplican cambios específicos mediante parches y metadatos (namespace, prefijos, réplicas, tags de imagen).

La idea es sencilla pero poderosa: el código de la app es uno solo, pero su configuración para staging y prod vive versionada en Git, en directorios distintos. Más adelante, cuando entremos a la parte de GitOps con ArgoCD, verás que cada Application de Argo sólo necesita apuntar al overlay correcto para desplegar el mismo servicio en el entorno adecuado.

Estás viendo solo el 60% del contenido. Hazte Premium para acceder al tutorial completo.

Comunidad

Comentarios y valoraciones

No hay comentarios aún. ¡Sé el primero en opinar!