DevOps

Construcción de un pipeline CI/CD multilenguaje para monorepo con servicios Go y PHP en Kubernetes

Ruslan Ismailov Publicado 12 min de lectura
C

Introducción: los desafíos del monorepo

El monorepo es una estrategia arquitectónica popular en la que varios servicios escritos en distintos lenguajes de programación conviven en un mismo repositorio Git. Empresas como Google, Meta y Uber llevan años adoptando este enfoque. Sin embargo, junto con la comodidad en la gestión de dependencias y un proceso unificado de code review, surgen serios problemas de CI/CD: ¿cómo evitar recompilar todos los servicios al modificar uno solo? ¿Cómo organizar pipelines paralelos para Go y PHP? ¿Cómo gestionar el despliegue en Kubernetes sin duplicar configuraciones?

En este artículo analizaremos la arquitectura práctica de un pipeline CI/CD multilenguaje para un monorepo donde coexisten microservicios en Go y PHP, desplegados en un clúster de Kubernetes. El público objetivo son ingenieros DevOps y tech leads que ya han sufrido el problema del «pipeline verde de 40 minutos por cambiar una sola línea».

Estructura del monorepo: organización de directorios

Una estructura de directorios correcta es la base de un monorepo manejable. Separa los servicios por lenguaje y dominio, y extrae los componentes comunes a directorios independientes.

monorepo/
├── services/
│   ├── go/
│   │   ├── auth-service/
│   │   │   ├── cmd/
│   │   │   ├── internal/
│   │   │   ├── Dockerfile
│   │   │   └── go.mod
│   │   └── payment-service/
│   │       ├── cmd/
│   │       ├── internal/
│   │       ├── Dockerfile
│   │       └── go.mod
│   └── php/
│       ├── api-gateway/
│       │   ├── src/
│       │   ├── Dockerfile
│       │   └── composer.json
│       └── billing-service/
│           ├── src/
│           ├── Dockerfile
│           └── composer.json
├── infra/
│   ├── helm/
│   │   ├── auth-service/
│   │   ├── payment-service/
│   │   ├── api-gateway/
│   │   └── billing-service/
│   └── kustomize/
│       ├── base/
│       └── overlays/
│           ├── staging/
│           └── production/
├── .github/
│   └── workflows/
├── scripts/
│   └── detect-changes.sh
└── Makefile

El principio clave: cada servicio es autosuficiente — contiene su propio Dockerfile, configuración de dependencias y pruebas. Las bibliotecas comunes se ubican en el directorio libs/ con versiones explícitas, de modo que un cambio en una biblioteca compartida solo dispare la recompilación de los servicios que dependen de ella.

Detección de servicios modificados: filtrado por rutas

El filtrado por rutas (path filtering) es una técnica esencial para CI/CD en monorepos. El objetivo es ejecutar el pipeline únicamente para los servicios cuyos archivos hayan cambiado en un commit o pull request.

Path filtering en GitHub Actions

GitHub Actions admite el filtro nativo paths a nivel de workflow, pero para detectar cambios de forma dinámica es preferible usar dorny/paths-filter:

name: CI Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      auth-service: ${{ steps.filter.outputs.auth-service }}
      payment-service: ${{ steps.filter.outputs.payment-service }}
      api-gateway: ${{ steps.filter.outputs.api-gateway }}
      billing-service: ${{ steps.filter.outputs.billing-service }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            auth-service:
              - 'services/go/auth-service/**'
              - 'libs/go-common/**'
            payment-service:
              - 'services/go/payment-service/**'
              - 'libs/go-common/**'
            api-gateway:
              - 'services/php/api-gateway/**'
              - 'libs/php-shared/**'
            billing-service:
              - 'services/php/billing-service/**'
              - 'libs/php-shared/**'

  build-auth-service:
    needs: detect-changes
    if: needs.detect-changes.outputs.auth-service == 'true'
    uses: ./.github/workflows/build-go-service.yml
    with:
      service: auth-service
      path: services/go/auth-service

Path filtering en GitLab CI

En GitLab CI se utiliza la directiva changes dentro de la sección rules:

build-auth-service:
  stage: build
  rules:
    - changes:
        - services/go/auth-service/**/*
        - libs/go-common/**/*
  script:
    - docker build -t $CI_REGISTRY_IMAGE/auth-service:$CI_COMMIT_SHORT_SHA
        services/go/auth-service/

build-api-gateway:
  stage: build
  rules:
    - changes:
        - services/php/api-gateway/**/*
        - libs/php-shared/**/*
  script:
    - docker build -t $CI_REGISTRY_IMAGE/api-gateway:$CI_COMMIT_SHORT_SHA
        services/php/api-gateway/

Builds Docker independientes: Go y PHP

Dockerfile multistage para servicios Go

Para los servicios Go, la estrategia óptima es un build multistage con una imagen final distroless. Esto minimiza el tamaño de la imagen y la superficie de ataque:

# services/go/auth-service/Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s -X main.version=${VERSION}" \
    -o auth-service ./cmd/server

FROM gcr.io/distroless/static-debian12 AS production
COPY --from=builder /app/auth-service /auth-service
USER nonroot:nonroot
ENTRYPOINT ["/auth-service"]

Las imágenes distroless no contienen shell, gestores de paquetes ni otras herramientas, lo que las hace significativamente más seguras para entornos de producción.

Dockerfile para servicios PHP (FPM + Nginx)

Los servicios PHP requieren dos contenedores: PHP-FPM para procesar las solicitudes y Nginx como reverse proxy. Utilizamos un build multistage con optimización de Composer:

# services/php/api-gateway/Dockerfile
FROM composer:2.7 AS composer-deps
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-scripts \
    --prefer-dist \
    --optimize-autoloader

FROM php:8.3-fpm-alpine AS production
RUN apk add --no-cache \
    redis \
    libpq-dev \
    && docker-php-ext-install pdo pgsql opcache

COPY --from=composer-deps /app/vendor /var/www/html/vendor
COPY src/ /var/www/html/src/
COPY config/ /var/www/html/config/
COPY php.ini /usr/local/etc/php/conf.d/custom.ini

FROM nginx:1.25-alpine AS nginx
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=production /var/www/html/public /var/www/html/public

Gestión de versiones de imágenes: etiquetado y versionado semántico

Una estrategia correcta de etiquetado de imágenes Docker es fundamental para la trazabilidad de los despliegues. Se recomienda usar varios tags de forma simultánea:

  • Git SHA — identificador único de cada commit: auth-service:a1b2c3d
  • Rama + SHA — para aislar feature branches: auth-service:feature-oauth-a1b2c3d
  • Versión semántica — para releases: auth-service:v1.4.2
  • latest — solo para la rama main/master en staging
#!/bin/bash
# scripts/tag-image.sh
SERVICE=$1
GIT_SHA=$(git rev-parse --short HEAD)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
REGISTRY="registry.company.com"

BASE_TAG="$REGISTRY/$SERVICE"

# Siempre etiquetamos por SHA
docker tag $BASE_TAG:build $BASE_TAG:$GIT_SHA

# Para main — etiquetamos también latest y la versión semántica
if [ "$BRANCH" = "main" ]; then
  VERSION=$(cat services/$SERVICE/VERSION)
  docker tag $BASE_TAG:build $BASE_TAG:$VERSION
  docker tag $BASE_TAG:build $BASE_TAG:latest
fi

docker push $BASE_TAG --all-tags

Pipelines paralelos: jobs matriciales y dependencias

GitHub Actions admite estrategias matriciales que permiten compilar varios servicios del mismo tipo en paralelo. Esto reduce considerablemente el tiempo total del pipeline CI/CD.

build-go-services:
  needs: detect-changes
  strategy:
    matrix:
      service:
        - name: auth-service
          changed: ${{ needs.detect-changes.outputs.auth-service }}
        - name: payment-service
          changed: ${{ needs.detect-changes.outputs.payment-service }}
    fail-fast: false
  runs-on: ubuntu-latest
  if: matrix.service.changed == 'true'
  steps:
    - uses: actions/checkout@v4
    - name: Build Go service
      run: |
        docker build \
          --build-arg VERSION=${{ github.sha }} \
          -t $REGISTRY/${{ matrix.service.name }}:${{ github.sha }} \
          services/go/${{ matrix.service.name }}/
    - name: Push image
      run: docker push $REGISTRY/${{ matrix.service.name }}:${{ github.sha }}

deploy:
  needs: [build-go-services, build-php-services, security-scan]
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  steps:
    - name: Deploy to Kubernetes
      run: ./scripts/deploy.sh

Usa fail-fast: false en la matriz para que el problema de un servicio no bloquee la compilación de los demás. Esto es especialmente importante cuando hay un gran número de microservicios independientes.

Despliegue en Kubernetes: Helm, Kustomize y estrategias de namespaces

Charts de Helm para despliegue multiservicio

Para cada servicio se crea un chart de Helm independiente con una plantilla común de values.yaml. Los valores configurables — imagen y tag — se pasan a través del pipeline CI/CD:

# infra/helm/auth-service/values.yaml
image:
  repository: registry.company.com/auth-service
  tag: latest
  pullPolicy: IfNotPresent

replicaCount: 2

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
# scripts/deploy.sh
#!/bin/bash
SERVICE=$1
ENVIRONMENT=$2
IMAGE_TAG=$3

helm upgrade --install $SERVICE \
  infra/helm/$SERVICE \
  --namespace $ENVIRONMENT \
  --create-namespace \
  --set image.tag=$IMAGE_TAG \
  --set environment=$ENVIRONMENT \
  --values infra/helm/$SERVICE/values-$ENVIRONMENT.yaml \
  --wait \
  --timeout 5m0s

Estrategia de namespaces

Estrategia recomendada de namespaces en Kubernetes para un monorepo:

  • staging — todos los servicios del entorno de staging
  • production — entorno de producción con políticas NetworkPolicy
  • feature-{branch-name} — namespaces temporales para feature branches (se eliminan tras el merge)

Kustomize resulta muy cómodo para gestionar las diferencias entre entornos sin duplicar configuraciones YAML. Los manifiestos base se ubican en infra/kustomize/base/, mientras que los directorios de overlay contienen únicamente las sobreescrituras específicas de cada entorno.

Pasos comunes del pipeline: linting, pruebas y escaneo de imágenes

Linting y pruebas

Para Go utilizamos golangci-lint con configuración en .golangci.yml; para PHP, phpstan y phpcs. Las pruebas se ejecutan en paralelo con la construcción de imágenes, lo que ahorra tiempo en el pipeline.

Escaneo de imágenes con Trivy

Integrar Trivy en el pipeline CI/CD es un paso obligatorio para un monorepo listo para producción. El escaneo se ejecuta justo después de construir la imagen, antes del despliegue:

security-scan:
  needs: [build-go-services, build-php-services]
  runs-on: ubuntu-latest
  strategy:
    matrix:
      service: [auth-service, payment-service, api-gateway, billing-service]
  steps:
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: registry.company.com/${{ matrix.service }}:${{ github.sha }}
        format: sarif
        output: trivy-results-${{ matrix.service }}.sarif
        severity: CRITICAL,HIGH
        exit-code: 1

    - name: Upload Trivy results to GitHub Security
      uses: github/codeql-action/upload-sarif@v3
      with:
        sarif_file: trivy-results-${{ matrix.service }}.sarif

Gestión de secretos en un pipeline multiservicio

La gestión de secretos es una de las tareas más delicadas en CI/CD para monorepos. Distintos servicios requieren secretos diferentes, y es fundamental evitar que los secretos de un servicio se filtren a otro.

El enfoque recomendado es el External Secrets Operator combinado con HashiCorp Vault o AWS Secrets Manager. Cada servicio accede únicamente a su propio espacio de secretos mediante Kubernetes ServiceAccount y RBAC:

# infra/helm/auth-service/templates/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: auth-service-secrets
spec:
  refreshInterval: 15m
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: auth-service-secrets
  data:
    - secretKey: JWT_SECRET
      remoteRef:
        key: services/auth-service
        property: jwt_secret
    - secretKey: DB_PASSWORD
      remoteRef:
        key: services/auth-service
        property: db_password

Para CI/CD, utiliza los secrets de GitHub Actions a nivel de environment, separando staging y producción. Nunca almacenes secretos en el repositorio, ni siquiera cifrados — solo referencias a almacenes externos.

Monitoreo del pipeline y métricas de tiempo de despliegue

Sin mediciones no es posible optimizar. Las métricas clave para un pipeline CI/CD en monorepo son:

  • Pipeline duration — tiempo total desde el commit hasta el despliegue (objetivo: menos de 10 minutos)
  • Build cache hit rate — porcentaje de uso de caché en las capas Docker (objetivo: más del 70%)
  • Deployment frequency — número de despliegues por día/semana
  • Change failure rate — porcentaje de despliegues que causaron incidentes
  • Mean time to recovery (MTTR) — tiempo medio de recuperación tras un fallo

Para la visualización de métricas, GitHub Actions se integra con Datadog o Grafana mediante plugins. GitLab CI incluye análisis de pipelines e informes sobre el tiempo de ejecución de los jobs. Para los despliegues en Kubernetes, configura el seguimiento con Argo Rollouts y rollback automático ante el deterioro de los golden signals.

Ejemplo de recopilación de métricas mediante el resumen de pasos de GitHub Actions:

- name: Report deployment metrics
  run: |
    DEPLOY_DURATION=$(($(date +%s) - $START_TIME))
    echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
    echo "| Service | Image Tag | Duration |" >> $GITHUB_STEP_SUMMARY
    echo "|---------|-----------|----------|" >> $GITHUB_STEP_SUMMARY
    echo "| $SERVICE | $IMAGE_TAG | ${DEPLOY_DURATION}s |" >> $GITHUB_STEP_SUMMARY
    
    # Envío de métrica a Datadog
    curl -X POST "https://api.datadoghq.com/api/v1/series" \
      -H "DD-API-KEY: ${{ secrets.DATADOG_API_KEY }}" \
      -d "{\"series\": [{\"metric\": \"cicd.deploy.duration\",
           \"points\": [[$START_TIME, $DEPLOY_DURATION]],
           \"tags\": [\"service:$SERVICE\", \"env:production\"]}]}"

Conclusiones y recomendaciones

Construir un pipeline CI/CD multilenguaje para un monorepo es una tarea compleja que requiere un enfoque sistemático. A continuación, las recomendaciones clave:

  1. Invierte en path filtering desde el primer día. Es el mecanismo fundamental sin el cual el CI/CD de un monorepo se convierte en un cuello de botella.
  2. Estandariza las plantillas de Dockerfile para Go (distroless) y PHP (FPM+Nginx) — esto simplifica el mantenimiento y la incorporación de nuevos desarrolladores.
  3. Utiliza jobs matriciales para compilar y probar servicios del mismo tipo en paralelo — puede reducir el tiempo del pipeline entre 3 y 5 veces.
  4. Separa los secretos por servicio mediante External Secrets Operator y el principio de mínimo privilegio.
  5. Mide las métricas DORA — deployment frequency, lead time, MTTR y change failure rate — ya que reflejan la eficiencia real de tu pipeline.
  6. Automatiza el rollback con Helm rollback o Argo Rollouts con estrategia canary para minimizar los riesgos en cada despliegue.

Un buen pipeline CI/CD para un monorepo no es solo automatización del build. Es un sistema de retroalimentación rápida que permite al equipo entregar cambios con confianza y frecuencia, independientemente del número de lenguajes y servicios en el repositorio.

La arquitectura descrita escala desde 5 hasta más de 50 servicios sin cambios fundamentales. Empieza por lo básico — configura el path filtering y los builds paralelos, y luego añade el escaneo de seguridad y las métricas. El enfoque iterativo funciona aquí tan bien como en el desarrollo de producto.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →