DevOps

Построение мультиязычного CI/CD пайплайна для монорепозитория с Go и PHP-сервисами в Kubernetes

Ruslan Ismailov Опубликовано 12 мин чтения
П

Введение: вызовы монорепозитория

Монорепозиторий — популярная архитектурная стратегия, когда несколько сервисов на разных языках программирования живут в одном Git-репозитории. Компании вроде Google, Meta и Uber используют monorepo-подход уже много лет. Однако вместе с удобством управления зависимостями и единым code review приходят серьёзные CI/CD-проблемы: как не пересобирать все сервисы при изменении одного? Как организовать параллельные пайплайны для Go и PHP? Как управлять деплоем в Kubernetes без дублирования конфигураций?

В этой статье мы разберём практическую архитектуру мультиязычного CI/CD пайплайна для монорепозитория, где сосуществуют микросервисы на Go и PHP, деплоящиеся в Kubernetes-кластер. Целевая аудитория — DevOps-инженеры и техлиды, которые уже столкнулись с болью «зелёный пайплайн на 40 минут при изменении одной строки».

Структура монорепозитория: организация директорий

Правильная структура директорий — фундамент управляемого monorepo. Разделяйте сервисы по языкам и доменам, выносите общие компоненты в отдельные директории.

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

Ключевой принцип: каждый сервис самодостаточен — содержит собственный Dockerfile, конфигурацию зависимостей и тесты. Общие библиотеки выносятся в директорию libs/ с явными версиями, чтобы изменение общей библиотеки триггерило пересборку только зависящих от неё сервисов.

Определение изменённых сервисов: path filtering

Path filtering — ключевая техника для CI/CD монорепозитория. Цель — запускать пайплайн только для тех сервисов, файлы которых изменились в коммите или pull request.

Path filtering в GitHub Actions

GitHub Actions поддерживает нативный paths фильтр на уровне воркфлоу, но для динамического определения изменений лучше использовать 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 в GitLab CI

В GitLab CI используется директива changes в секции 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/

Независимые Docker-сборки: Go и PHP

Multistage Dockerfile для Go сервисов

Для Go-сервисов оптимальная стратегия — multistage build с финальным distroless образом. Это минимизирует размер образа и attack surface:

# 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"]

Distroless образы не содержат shell, package manager и других инструментов, что делает их значительно более безопасными для production-окружений.

Dockerfile для PHP сервисов (FPM + Nginx)

PHP-сервисы требуют двух контейнеров: PHP-FPM для обработки запросов и Nginx как reverse proxy. Используем multistage build с оптимизацией 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

Управление версиями образов: тегирование и семантическое версионирование

Правильная стратегия тегирования Docker-образов критична для трассируемости деплоев. Рекомендуем использовать несколько тегов одновременно:

  • Git SHA — уникальный идентификатор каждого коммита: auth-service:a1b2c3d
  • Ветка + SHA — для изоляции feature-веток: auth-service:feature-oauth-a1b2c3d
  • Семантическая версия — для релизов: auth-service:v1.4.2
  • latest — только для main/master ветки в 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"

# Всегда тегируем по SHA
docker tag $BASE_TAG:build $BASE_TAG:$GIT_SHA

# Для main — дополнительно тегируем latest и семантическую версию
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

Параллельные пайплайны: матричные джобы и зависимости

GitHub Actions поддерживает матричные стратегии, позволяющие параллельно собирать несколько сервисов одного типа. Это значительно сокращает общее время 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

Используйте fail-fast: false в матрице, чтобы проблема одного сервиса не блокировала сборку остальных. Это особенно важно при большом количестве независимых микросервисов.

Деплой в Kubernetes: Helm, Kustomize и namespace-стратегии

Helm-чарты для мультисервисного деплоя

Для каждого сервиса создаётся отдельный Helm-чарт с общим шаблоном values.yaml. Переопределяемые значения — образ и тег — передаются через 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

Namespace-стратегия

Рекомендуемая стратегия namespace в Kubernetes для монорепозитория:

  • staging — все сервисы staging-окружения
  • production — production-окружение с политиками NetworkPolicy
  • feature-{branch-name} — временные namespace для feature-веток (удаляются после merge)

Kustomize удобен для управления различиями между окружениями без дублирования YAML-конфигураций. Базовые манифесты лежат в infra/kustomize/base/, а overlay-директории содержат только переопределения для конкретного окружения.

Общие шаги пайплайна: линтинг, тестирование и сканирование образов

Линтинг и тесты

Для Go используем golangci-lint с конфигурацией в .golangci.yml, для PHP — phpstan и phpcs. Тесты запускаются параллельно со сборкой образов, что экономит время пайплайна.

Сканирование образов с Trivy

Интеграция Trivy в CI/CD пайплайн — обязательный шаг для production-ready монорепозитория. Сканирование запускается сразу после сборки образа, до деплоя:

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

Управление секретами в мультисервисном пайплайне

Управление секретами — одна из самых чувствительных задач в CI/CD монорепозитория. Разные сервисы требуют разных секретов, при этом важно избежать утечки секретов одного сервиса в другой.

Рекомендуемый подход — External Secrets Operator в связке с HashiCorp Vault или AWS Secrets Manager. Каждый сервис получает доступ только к своему пространству секретов через Kubernetes ServiceAccount и 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

Для CI/CD используйте GitHub Actions secrets на уровне environment, разделяя staging и production. Никогда не храните секреты в репозитории, даже зашифрованные — только ссылки на внешние хранилища.

Мониторинг пайплайна и метрики времени деплоя

Без измерений невозможно оптимизировать. Ключевые метрики для CI/CD монорепозитория:

  • Pipeline duration — общее время от коммита до деплоя (цель: менее 10 минут)
  • Build cache hit rate — процент использования кеша Docker layers (цель: более 70%)
  • Deployment frequency — сколько деплоев в день/неделю
  • Change failure rate — процент деплоев, вызвавших инциденты
  • Mean time to recovery (MTTR) — среднее время восстановления после сбоя

Для визуализации метрик GitHub Actions интегрируется с Datadog или Grafana через плагины. В GitLab CI встроены аналитика пайплайнов и отчёты о времени выполнения джобов. Для Kubernetes деплоев настройте отслеживание через Argo Rollouts с автоматическим rollback при ухудшении golden signals.

Пример сбора метрик через GitHub Actions step summary:

- 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
    
    # Отправка метрики в 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\"]}]}"

Заключение и рекомендации

Построение мультиязычного CI/CD пайплайна для монорепозитория — нетривиальная задача, требующая системного подхода. Подведём ключевые рекомендации:

  1. Инвестируйте в path filtering с первого дня. Это фундаментальный механизм, без которого CI/CD монорепозитория превращается в узкое горлышко.
  2. Стандартизируйте Dockerfile-шаблоны для Go (distroless) и PHP (FPM+Nginx) — это упрощает поддержку и онбординг новых разработчиков.
  3. Используйте матричные джобы для параллельной сборки и тестирования однотипных сервисов — это может сократить время пайплайна в 3-5 раз.
  4. Разделяйте секреты по сервисам через External Secrets Operator и принцип минимальных привилегий.
  5. Измеряйте метрики DORA — deployment frequency, lead time, MTTR и change failure rate — они покажут реальную эффективность вашего пайплайна.
  6. Автоматизируйте откат через Helm rollback или Argo Rollouts с canary-стратегией для минимизации рисков при деплое.

Хороший CI/CD пайплайн для монорепозитория — это не просто автоматизация сборки. Это система быстрой обратной связи, которая позволяет команде доставлять изменения уверенно и часто, независимо от количества языков и сервисов в репозитории.

Описанная архитектура масштабируется от 5 до 50+ сервисов без принципиальных изменений. Начните с малого — настройте path filtering и параллельные сборки, затем добавляйте сканирование безопасности и метрики. Итеративный подход здесь работает так же хорошо, как и в разработке продукта.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →