Construcción de un pipeline CI/CD multilenguaje para monorepo con servicios Go y PHP en Kubernetes
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 stagingproduction— entorno de producción con políticas NetworkPolicyfeature-{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:
- 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.
- Estandariza las plantillas de Dockerfile para Go (distroless) y PHP (FPM+Nginx) — esto simplifica el mantenimiento y la incorporación de nuevos desarrolladores.
- Utiliza jobs matriciales para compilar y probar servicios del mismo tipo en paralelo — puede reducir el tiempo del pipeline entre 3 y 5 veces.
- Separa los secretos por servicio mediante External Secrets Operator y el principio de mínimo privilegio.
- Mide las métricas DORA — deployment frequency, lead time, MTTR y change failure rate — ya que reflejan la eficiencia real de tu pipeline.
- 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í →