Форум программистов, компьютерный форум, киберфорум
Mr. Docker
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Тестирование Pull Request в Kubernetes с GitHub Actions и GKE

Запись от Mr. Docker размещена 02.06.2025 в 22:02
Показов 3862 Комментарии 0

Нажмите на изображение для увеличения
Название: 26c70a89-7bc1-4098-8670-6adced7569a7.jpg
Просмотров: 187
Размер:	142.1 Кб
ID:	10870
Мы все знаем, что тестирование на локальной машине или в изолированном CI-окружении — это не совсем то же самое, что тестирование в реальном кластере Kubernetes. Контекстно-зависимые ошибки, проблемы с сетевыми политиками, особенности работы с секретами и конфигурациями — все это может вылезти уже после деплоя в продакшн, если не протестировать заранее. В последние пару лет я перепробовал несколько подходов к тестированию PR в кластерах Kubernetes и пришол к определённым выводам. Поделюсь, как настроить тестирование каждого Pull Request прямо в Google Kubernetes Engine с использованием GitHub Actions — так, чтобы каждый PR получал свое собственное тестовое окружение, полностью идентичное продакшену.

В этой статье я расскажу, как создать и настроить кластер GKE, подготовить манифесты приложения с Kustomize для кастомизации, интегрировать GitHub Actions с GKE, автоматизировать сборку и хранение Docker-образов, устанавливать зависимости вроде PostgreSQL через Helm-чарты и, наконец, запускать тесты против развернутого приложения. Я не буду ходить вокруг да около — это не статья о том, как использовать Kubernetes в целом или что такое GitHub Actions. Предполагаю, что вы уже имеете базовое представление об этих технологиях. Вместо этого я сконцентрируюсь на конкретных практических аспектах интеграции этих инструментов для решения задачи тестирования PR.

Архитектура решения для тестирования PR



Давайте разберемся с общей архитектурой решения. Когда я впервые столкнулся с задачей тестирования PR в Kubernetes, я расчертил для себя схему всего процеса, чтобы понимать, что именно нужно сделать и какие компоненты взаимодействуют между собой. Итак, наша архитектура должна решать следующие задачи:

1. Создание изолированной среды для каждого PR.
2. Сборка и хранение Docker-образов для каждой версии приложения.
3. Деплой приложения и его зависимостей в кластер.
4. Запуск тестов против развернутого приложения.
5. Предоставление обратной связи в GitHub PR.

Поскольку мы используем GitHub Actions и GKE, центральным компонентом нашей архитектуры будет рабочий процесс GitHub, который взаимодействует с кластером GKE. Фактически, у нас есть два основных подхода к реализации:

Динамический подход: создавать новый кластер GKE для каждого PR.
Статический подход: использовать один предварительно настроенный кластер и развертывать приложения в разных пространствах имен.

У обоих подходов есть свои плюсы и минусы. Создание нового кластера для каждого PR обеспечивает максимальную изоляцию, но требует времени (5-7 минут на создание кластера GKE) и дополнительных затрат. Использование общего кластера быстрее и дешевле, но может привести к конфликтам ресурсов и меньшей изоляции. В моем случае я выбрал второй подход — использование одного кластера GKE для всех PR. Это компромисное решение, которое обеспечивает достаточную изоляцию при разумных затратах.

Предварительная настройка кластера GKE



Первый важный шаг в нашей архитектуре — создание и настройка кластера GKE. Для тестовой среды нам не нужен огромный кластер с множеством узлов, но и слишком маленький делать не стоит. Рекомендую создать кластер хотя бы с 2-3 узлами, чтобы иметь запас ресурсов для нескольких параллельных PR. Вот пример команды для создания минимального кластера GKE:

Bash
1
2
3
4
5
6
7
gcloud container clusters create "test-pr-cluster" \
  --project "ваш-проект" \
  --zone "europe-west3" \
  --num-nodes "2" \
  --machine-type "e2-standard-4" \
  --enable-ip-alias \
  --no-enable-basic-auth
Обратите внимание на флаг --machine-type. В моих экспериментах я начинал с e2-standard-2, но быстро понял, что при паралельном тестировании нескольких PR ресурсов не хватает. Пришлось обновить до e2-standard-4, что дало гораздо лучшие результаты. Если у вас приложение ресурсоемкое или вы ожидаете много одновременных PR, возможно, стоит выбрать еще более производительные машины.

Аутентификация в Google Cloud из GitHub Actions



Следующий ключевой компонент архитектуры — настройка аутентификации между GitHub Actions и Google Cloud. Это критически важный момент с точки зрения безопасности и функциональности. В Google Cloud есть несколько способов аутентификации, я расмотрю самый практичный и безопасный. Для интеграции GitHub Actions с GKE мы используем рабочую идентификацию через сервисный аккаунт (Workload Identity Federation through a Service Account). Этот метод позволяет GitHub Actions получать временные токены для доступа к GKE без необходимости хранения постоянных ключей доступа. Настройка такой аутентификации включает несколько шагов:

1. Создание сервисного аккаунта в Google Cloud.
2. Настройка разрешений для сервисного аккаунта.
3. Создание пула рабочей идентификации (Workload Identity Pool).
4. Настройка провайдера OIDC для GitHub Actions.
5. Связывание сервисного аккаунта с пулом идентификации.

Например, для создания сервисного аккаунта можно использовать:

Bash
1
2
gcloud iam service-accounts create github-actions \
  --display-name "GitHub Actions Service Account"
Затем необходимо предоставить этому сервисному аккаунту необходимые права для работы с GKE:

Bash
1
2
3
gcloud projects add-iam-policy-binding ваш-проект \
  --member "serviceAccount:github-actions@ваш-проект.iam.gserviceaccount.com" \
  --role "roles/container.developer"
Далее создаем пул рабочей идентификации и настраиваем его для работы с GitHub Actions:

Bash
1
2
3
4
5
6
7
8
9
10
gcloud iam workload-identity-pools create "github-actions-pool" \
  --project="ваш-проект" \
  --display-name="GitHub Actions Pool"
 
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
  --project="ваш-проект" \
  --workload-identity-pool="github-actions-pool" \
  --display-name="GitHub Provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor" \
  --issuer-uri="https://token.actions.githubusercontent.com"
И, наконец, связываем сервисный аккаунт с пулом идентификации:

Bash
1
2
3
4
5
gcloud iam service-accounts add-iam-policy-binding \
  "github-actions@ваш-проект.iam.gserviceaccount.com" \
  --project="ваш-проект" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/номер-проекта/locations/global/workloadIdentityPools/github-actions-pool/*"
Эти шаги могут показаться сложными, но они выполняются один раз при настройке инфраструктуры. После этого GitHub Actions сможет аутентифицироваться в Google Cloud без хранения долгоживущих секретов.

Управление Docker-образами



Важной частью нашей архитектуры является построение и хранение Docker-образов для каждого PR. Для этого я использую GitHub Container Registry (GHCR), который интегрирован с GitHub и позволяет хранить образы приватно, с доступом через GitHub-аутентификацию. В рамках рабочего процесса GitHub Actions мы будем:

1. Собирать Docker-образ из кода в PR,
2. Тегировать его уникальным идентификатором (например, ID рабочего процесса GitHub),
3. Отправлять образ в GHCR,
4. Использовать этот образ при деплое в GKE.

Эта часть архитектуры гарантирует, что каждый PR тестируется с соответствующей версией кода, без влияния других PR или основной ветки.

Манифесты Kubernetes и Kustomize



Для деплоя приложения в GKE нам нужны манифесты Kubernetes. Но у нас есть проблема: мы не знаем заранее, какой тег будет у Docker-образа, поскольку он генерируется во время выполнения рабочего процесса. Кроме того, разные PR должны развертываться изолированно друг от друга. Для решения этой проблемы я использую Kustomize — инструмент для кастомизации манифестов Kubernetes без использования шаблонов. С помощью Kustomize мы можем:

1. Определить базовые манифесты приложения.
2. Динамически изменять образ и теги во время выполнения рабочего процесса.
3. Создавать уникальные имена ресурсов для каждого PR.

В базовом манифесте мы указываем плейсхолдер для тега образа, который затем заменяется реальным значением в процессе деплоя.

Итак, общая архитектура решения состоит из:
  • Предварительно настроенного кластера GKE,
  • Сервисного аккаунта Google Cloud с необходимыми разрешениями,
  • Рабочего процесса GitHub Actions, который аутентифицируется в Google Cloud,
  • Docker-образов, хранящихся в GitHub Container Registry,
  • Манифестов Kubernetes с Kustomize для динамической кастомизации.

Детали манифестов и шаблонизация



Когда я впервые столкнулся с проблемой настройки тестирования PR в Kubernetes, одним из самых сложных аспектов оказалась разработка манифестов, которые бы одновременно сохраняли все характеристики продакшн-среды и позволяли гибко менять некоторые параметры для каждого PR. В моем случае, базовый манифест приложения выглядит примерно так:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vcluster-pipeline
  labels:
    type: app
    app: vcluster-pipeline
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vcluster-pipeline
  template:
    metadata:
      labels:
        type: app
        app: vcluster-pipeline
    spec:
      containers:
        - name: vcluster-pipeline
          image: ghcr.io/моя-организация/моё-приложение:latest
          envFrom:
            - configMapRef:
                name: postgres-config
      imagePullSecrets:
        - name: github-docker-registry
---
apiVersion: v1
kind: Service
metadata:
  name: vcluster-pipeline
spec:
  type: LoadBalancer
  ports:
    - port: 8080
      targetPort: 8080
  selector:
    app: vcluster-pipeline
В этом манифесте есть несколько ключевых моментов, которые нуждаются в кастомизации для каждого PR:

1. Тег Docker-образа — он должен быть уникальным для каждого PR.
2. Настройки подключения к базе данных — мы получаем их из ConfigMap.
3. Доступ к приватному реестру — используем секрет для аутентификации.
4. Имя сервиса — оно должно быть уникальным для каждого PR.
5. IP-адрес LoadBalancer — будет назначен автоматически GKE.

Kustomize позволяет решить все эти проблемы элегантным способом. В том же каталоге, что и манифест, я создаю файл kustomization.yaml:

YAML
1
2
3
4
5
6
7
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
vcluster-pipeline.yaml
images:
name: ghcr.io/моя-организация/моё-приложение
  newTag: DYNAMIC_TAG
Здесь DYNAMIC_TAG — это плейсхолдер, который будет заменен во время выполнения рабочего процесса GitHub Actions на реальный тег образа.

Проблема доступа к приватному реестру



Еще одной проблемой, которую я выявил на ранних этапах, был доступ к приватному реестру Docker-образов из кластера GKE. По умолчанию Kubernetes не может скачать образ из приватного реестра GitHub без аутентификации. Решение — создать секрет Kubernetes типа docker-registry, который содержит учетные данные для доступа к реестру. В рабочем процессе GitHub Actions это выглядит так:

YAML
1
2
3
4
5
6
7
8
name: Create Docker Registry Secret
  run: |
    kubectl create secret docker-registry github-docker-registry \
      --docker-server=${{ env.REGISTRY }} \
      --docker-email="noreply@github.com" \
      --docker-username="${{ github.actor }}" \
      --docker-password="${{ secrets.GITHUB_TOKEN }}" \
      --dry-run=client -o yaml | kubectl apply -f -
Эта команда создает секрет с учетными данными GitHub, который затем используется в поле imagePullSecrets в манифесте Deployment.

Настройка зависимостей: база данных PostgreSQL



Для полноценного тестирования обычно требуется не только само приложение, но и его зависимости, например, база данных. В моем случае это PostgreSQL. Для настройки базы данных я использую Helm — менеджер пакетов для Kubernetes.
Вот пример файла values.yaml для Helm-чарта PostgreSQL:

YAML
1
2
3
4
5
6
7
8
fullnameOverride: postgres
auth:
  user: postgres
  password: root
  postgresPassword: roottoo
primary:
  persistence:
    enabled: false
Обратите внимание на параметр persistence.enabled: false — это означает, что данные не будут сохраняться на диске. Для тестирования PR это обычно приемлемо, поскольку нам не нужно сохранять данные между запусками.
После установки Helm-чарта PostgreSQL я создаю ConfigMap с параметрами подключения к базе данных:

YAML
1
2
3
4
5
6
7
name: Set config map from values.yaml
  run: |
    kubectl create configmap postgres-config \
      --from-literal="SPRING_FLYWAY_URL=jdbc:postgresql://$(yq .fullnameOverride kubernetes/values.yaml):5432/" \
      --from-literal="SPRING_R2DBC_URL=r2dbc:postgresql://$(yq .fullnameOverride kubernetes/values.yaml):5432/" \
      --from-literal="SPRING_R2DBC_USERNAME=$(yq .auth.user kubernetes/values.yaml)" \
      --from-literal="SPRING_R2DBC_PASSWORD=$(yq .auth.password kubernetes/values.yaml)"

[Github] Почему мне следует комитить файл лицензии в отдельную ветку и делать pull request?
https://help.github.com/articles/adding-a-license-to-a-repository/ В 8 пункте сказано, что если...

Могу ли я удалить свой форк репозитория GitHub после принятия pull request
Я создал форк публичного репозитория на github с целью - исправения ошибок в основном репозитории....

GitHub и Pull Request. Как скинуть ссылку?
Вопрос такой, мне преподаватель сказал скинуть ссылку на Pull Request в GitHub. Дескопный GitHub...

New pull request на Github
Не знаю, что нажать, чтобы добавленный через браузер новый файл появился локально. Кнопка что-то не...


Практическая реализация CI/CD пайплайна



Теперь, когда я обрисовал общую архитектуру решения, погрузимся в практическую реализацию CI/CD пайплайна на GitHub Actions. Это, пожалуй, самая интересная часть всего процесса, где теоретические концепции превращаются в рабочие скрипты и автоматизацию. Наш пайплайн должен выполнять несколько критических задач: собирать и публиковать Docker-образы, аутентифицироваться в Google Cloud, развертывать приложение и его зависимости, а затем запускать тесты. И все это должно происходить автоматически при создании или обновлении Pull Request.

Сборка и публикация Docker-образа



Первый шаг нашего пайплайна — сборка Docker-образа приложения и его публикация в GitHub Container Registry. Для этого используем действия из экосистемы Docker:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    env:
      REGISTRY: ghcr.io
      IMAGE_NAME: ${{ github.repository }}
      DOCKER_BUILD_RECORD_RETENTION_DAYS: 1
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        
      - name: Log into registry ${{ env.REGISTRY }}
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=raw,value=${{github.run_id}}
            
      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          push: true
          cache-from: type=gha
          cache-to: type=gha,mode=max
Тут несколько интересных моментов:

1. permissions определяет, какие права нужны для работы с GitHub Container Registry.
2. В теге образа я использую github.run_id — уникальный идентификатор запуска GitHub Actions, что позволяет каждому PR иметь свой уникальный образ.
3. Включено кеширование через параметры cache-from и cache-to, что ускоряет сборку при повторных запусках.

Обратите внимание на DOCKER_BUILD_RECORD_RETENTION_DAYS: 1 — это ограничивает время хранения образов всего одним днем. В контексте тестирования PR это вполне разумно, поскольку нам не нужны старые образы, а также это позволяет сэкономить на хранении.

Аутентификация в Google Cloud и GKE



Следующий критический шаг — аутентификация в Google Cloud и получение доступа к кластеру GKE:

YAML
1
2
3
4
5
6
7
8
9
10
11
name: Authenticate on Google Cloud
  uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider
    service_account: [email]github-actions@ваш-проект.iam.gserviceaccount.com[/email]
 
name: Set GKE credentials
  uses: google-github-actions/get-gke-credentials@v2
  with:
    cluster_name: test-pr-cluster
    location: europe-west3
Здесь я использую действие google-github-actions/auth для аутентификации через Workload Identity Federation, как обсуждалось ранее. Затем действие get-gke-credentials настраивает kubectl для работы с нашим кластером GKE. Стоит отметить, что в реальных проектах вы, вероятно, захотите вынести идентификатор пула Workload Identity и имя сервисного аккаунта в секреты репозитория, чтобы не хардкодить их в рабочем процессе.

Создание уникального пространства имен для PR



Чтобы изолировать ресурсы каждого PR, я создаю отдельное пространство имен в Kubernetes:

YAML
1
2
3
4
5
6
name: Create namespace for PR
  run: |
    NAMESPACE="pr-${{ github.event.pull_request.number }}"
    kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
    kubectl config set-context --current --namespace=$NAMESPACE
    echo "NAMESPACE=$NAMESPACE" >> $GITHUB_ENV
Этот шаг создает пространство имен на основе номера PR и устанавливает его как текущий контекст для последующих команд kubectl. Также я сохраняю имя пространства имен в переменной среды для использования в дальнейших шагах.

Установка зависимостей: PostgreSQL



Теперь устанавливаем PostgreSQL с помощью Helm:

YAML
1
2
3
4
5
6
7
8
9
10
11
name: Install PostgreSQL
  run: |
    helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql \
    --values kubernetes/values.yaml \
    --namespace ${{ env.NAMESPACE }}
    
name: Wait for PostgreSQL to be ready
  run: |
    kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=postgresql \
    --timeout=120s \
    --namespace ${{ env.NAMESPACE }}
Я добавил дополнительный шаг ожидания готовности PostgreSQL, потому что часто встречался с ситуацией, когда следующие шаги запускались до того, как база данных была полностью инициализирована, что приводило к ошибкам.

Создание ConfigMap и Secrets



Следующий шаг — создание ConfigMap с параметрами подключения к базе данных:

YAML
1
2
3
4
5
6
7
8
9
name: Create ConfigMap for application
  run: |
    kubectl create configmap postgres-config \
      --from-literal="SPRING_FLYWAY_URL=jdbc:postgresql://postgresql:5432/" \
      --from-literal="SPRING_R2DBC_URL=r2dbc:postgresql://postgresql:5432/" \
      --from-literal="SPRING_R2DBC_USERNAME=postgres" \
      --from-literal="SPRING_R2DBC_PASSWORD=root" \
      --namespace ${{ env.NAMESPACE }} \
      --dry-run=client -o yaml | kubectl apply -f -
Также создаем секрет для доступа к приватному реестру Docker-образов:

YAML
1
2
3
4
5
6
7
8
9
name: Create Docker Registry Secret
  run: |
    kubectl create secret docker-registry github-docker-registry \
      --docker-server=${{ env.REGISTRY }} \
      --docker-email="noreply@github.com" \
      --docker-username="${{ github.actor }}" \
      --docker-password="${{ secrets.GITHUB_TOKEN }}" \
      --namespace ${{ env.NAMESPACE }} \
      --dry-run=client -o yaml | kubectl apply -f -

Деплой приложения



Теперь самое интересное — деплой нашего приложения с помощью Kustomize:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: Update image tag in Kustomization
  run: |
    cd kubernetes
    kustomize edit set image ghcr.io/моя-организация/моё-приложение=ghcr.io/моя-организация/моё-приложение:${{ github.run_id }}
    
name: Deploy application
  run: |
    kubectl apply -k kubernetes --namespace ${{ env.NAMESPACE }}
    
name: Wait for application to be ready
  run: |
    kubectl wait --for=condition=ready pod -l app=vcluster-pipeline \
    --timeout=120s \
    --namespace ${{ env.NAMESPACE }}
Первый шаг обновляет тег образа в файле kustomization.yaml, заменяя плейсхолдер на реальный тег с идентификатором запуска GitHub Actions. Затем мы применяем манифесты с помощью kubectl apply -k и ждем, пока приложение будет готово.

Заметил, что эти шаги ожидания часто игнорируются в туториалах, но в реальной жизни они критически важны для надежности пайплайна. Без них я постоянно сталкивался с ситуациями, когда тесты запускались на ещё не готовом к работе приложении.

Получение IP-адреса приложения и запуск тестов



После деплоя приложения нужно получить его внешний IP-адрес для запуска тестов. Поскольку я использую сервис типа LoadBalancer, GKE автоматически назначит внешний IP-адрес. Однако тут есть важный нюанс — этот процесс не мгновенный, и нужно подождать, пока IP будет назначен. Вот решение, которое я применил:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
name: Retrieve LoadBalancer external IP
  run: |
    for i in {1..10}; do
      EXTERNAL_IP=$(kubectl get service vcluster-pipeline -o jsonpath='{.status.loadBalancer.ingress[0].ip}' --namespace ${{ env.NAMESPACE }})
      if [ -n "$EXTERNAL_IP" ]; then
        break
      fi
      echo "Waiting for external IP... Attempt $i of 10"
      sleep 10
    done
    if [ -z "$EXTERNAL_IP" ]; then
      echo "Error: External IP not assigned to the service" >&2
      exit 1
    fi
    APP_BASE_URL="http://${EXTERNAL_IP}:8080"
    echo "APP_BASE_URL=$APP_BASE_URL" >> $GITHUB_ENV
    echo "External IP is $APP_BASE_URL"
Этот скрипт делает до 10 попыток получить IP-адрес, с интервалом в 10 секунд между попытками. Если после всех попыток IP не назначен, пайплайн завершится с ошибкой. В противном случае URL приложения сохраняется в переменной среды APP_BASE_URL для использования в тестах. Я раньше сталкивался с тем, что в некоторых инструкциях просто предлагают сразу запросить IP без проверки, что приводило к ошибкам. Или еще хуже — просто ставили фиксированную задержку в 30-60 секунд, что либо замедляло пайплайн, либо все равно иногда не работало, если провайдер облака был перегружен и назначал IP дольше обычного.

Запуск интеграционных тестов



Теперь, когда приложение развернуто и у нас есть его URL, можно запустить тесты. В моем случае это интеграционные тесты, которые проверяют работу приложения в реальной среде:

YAML
1
2
3
4
5
6
name: Run integration tests
  run: |
    export APP_BASE_URL
    ./mvnw -B verify -Dtest=SkipAll -Dit.test=ApplicationIT -Dsurefire.failIfNoSpecifiedTests=false
  env:
    APP_BASE_URL: ${{ env.APP_BASE_URL }}
Обратите внимание на параметр -Dtest=SkipAll. Это небольшая хитрость, которая позволяет пропустить выполнение всех модульных тестов и запустить только интеграционные. В Maven это можно сделать, указав шаблон, который не соответствует ни одному классу модульных тестов. Тут мы акцентируем внимание только на тестах, которые взаимодействуют с реальным развернутым приложением.

Обработка результатов тестов и обновление PR



После запуска тестов важно предоставить обратную связь в PR. Я делаю это с помощью действия github/script:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: Update PR with test results
  if: always()
  uses: actions/github-script@v6
  with:
    script: |
      const outcome = '${{ job.status }}';
      const url = '${{ env.APP_BASE_URL }}';
      
      let message = '';
      if (outcome === 'success') {
        message = `Интеграционные тесты успешно пройдены\n\nПриложение доступно по адресу: ${url}\n\nЭто окружение будет автоматически удалено через 24 часа.`;
      } else {
        message = `Интеграционные тесты завершились с ошибкой\n\nПриложение доступно по адресу: ${url} для отладки проблемы.\n\nЭто окружение будет автоматически удалено через 24 часа.`;
      }
      
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: message
      });
Директива if: always() гарантирует, что этот шаг выполнится независимо от результата тестов. Это важно, потому что мы хотим сообщить результаты даже в случае неудачи.
В сообщении я указываю URL приложения, что очень удобно для отладки проблем — разработчик может сразу перейти по ссылке и проверить, что не так с его PR.

Очистка ресурсов



В идеальном мире мы бы сразу удаляли все ресурсы после завершения тестов, но на практике я предпочитаю оставлять их на некоторое время для возможности отладки:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: Schedule cleanup
  if: always()
  run: |
    cat <<EOF | kubectl apply -f -
    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: cleanup-pr-${{ github.event.pull_request.number }}
      namespace: default
    spec:
      schedule: "0 0 * * *"  # Полночь каждый день
      successfulJobsHistoryLimit: 1
      failedJobsHistoryLimit: 1
      jobTemplate:
        spec:
          template:
            spec:
              serviceAccountName: cleanup-sa
              containers:
              - name: kubectl
                image: bitnami/kubectl
                command:
                - /bin/sh
                - -c
                - kubectl delete namespace ${{ env.NAMESPACE }} || true
              restartPolicy: OnFailure
EOF
Этот шаг создает CronJob, который удалит пространство имен PR через день. Конечно, для этого необходимо предварительно создать сервисный аккаунт cleanup-sa с соответствующими правами.

Дополнительные соображения



В процессе работы с этим пайплайном я столкнулся с несколькими проблемами, которые стоит упомянуть:

1. Параллельное выполнение. Если в вашем репозитории может быть много одновременных PR, убедитесь, что кластер GKE имеет достаточно ресурсов. Я однажды столкнулся с ситуацией, когда 10 одновременных PR полностью исчерпали ресурсы кластера.
2. Время выполнения. Весь процесс от коммита до результатов тестов занимает около 5-7 минут, что вполне приемлемо для CI/CD пайплайна. Большую часть времени занимает сборка и публикация Docker-образа.
3. Стоимость. Даже небольшой кластер GKE стоит денег. Если у вас низкая активность PR, возможно, стоит рассмотреть вариант с созданием кластера только при необходимости, несмотря на дополнительное время ожидания.
4. Безопасность. Помните, что сервисный аккаунт, используемый в GitHub Actions, имеет доступ к вашему проекту Google Cloud. Ограничивайте его права минимально необходимыми и регулярно проверяйте настройки.
5. Отладка. Иногда что-то идет не так, и может быть сложно понять причину. Я добавил в пайплайн дополнительные шаги для вывода отладочной информации:

YAML
1
2
3
4
5
6
7
8
name: Debug information
  if: always()
  run: |
    echo "Namespace: ${{ env.NAMESPACE }}"
    echo "App URL: ${{ env.APP_BASE_URL }}"
    kubectl get all --namespace ${{ env.NAMESPACE }}
    kubectl describe pods --namespace ${{ env.NAMESPACE }}
    kubectl logs -l app=vcluster-pipeline --namespace ${{ env.NAMESPACE }} --tail=100
Это помогает быстро идентифицировать проблемы, не переключаясь в консоль Google Cloud.

Инкрементальное тестирование и blue-green деплой в контексте PR



В процессе работы с тестированием PR на Kubernetes я понял, что не все тесты нужно запускать сразу. Иногда разумнее использовать инкрементальный подход. Я разделил тесты на несколько категорий:

1. Базовые тесты (проверка подключения, пинг эндпоинтов).
2. Функциональные тесты (CRUD-операции).
3. Нагрузочные тесты (только для критических PR).

Это позволяет получать быструю обратную связь и не тратить ресурсы на полное тестирование заведомо проблемных PR. Вот как можно реализовать такой подход:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: Run basic connectivity tests
  run: |
    curl -f ${{ env.APP_BASE_URL }}/health || exit 1
    curl -f ${{ env.APP_BASE_URL }}/api/v1/status || exit 1
    echo "Basic connectivity tests passed"
 
name: Run functional tests
  if: success()
  run: |
    ./mvnw -B verify -Dtest=SkipAll -Dit.test=FunctionalIT -Dsurefire.failIfNoSpecifiedTests=false
  env:
    APP_BASE_URL: ${{ env.APP_BASE_URL }}
 
name: Run load tests
  if: success() && contains(github.event.pull_request.labels.*.name, 'full-test')
  run: |
    ./mvnw -B verify -Dtest=SkipAll -Dit.test=LoadIT -Dsurefire.failIfNoSpecifiedTests=false
  env:
    APP_BASE_URL: ${{ env.APP_BASE_URL }}
Другое интересное решение, которое я внедрил - стратегия blue-green деплоя прямо в PR-тестировании. Это особенно полезно для проверки миграций баз данных или других сложных изменений. Суть в том, что мы сначала деплоим текущую версию из main, запускаем на ней тесты, а потом заменяем на версию из PR и снова запускаем тесты.

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: Deploy current main version
  run: |
    kubectl apply -k kubernetes/stable --namespace ${{ env.NAMESPACE }}
    kubectl wait --for=condition=ready pod -l app=vcluster-pipeline,version=stable --timeout=120s --namespace ${{ env.NAMESPACE }}
 
name: Run baseline tests
  run: |
    ./mvnw -B verify -Dtest=SkipAll -Dit.test=BaselineIT -Dsurefire.failIfNoSpecifiedTests=false
  env:
    APP_BASE_URL: ${{ env.STABLE_APP_URL }}
 
name: Deploy PR version
  run: |
    kubectl apply -k kubernetes/pr --namespace ${{ env.NAMESPACE }}
    kubectl wait --for=condition=ready pod -l app=vcluster-pipeline,version=pr --timeout=120s --namespace ${{ env.NAMESPACE }}
 
name: Run migration tests
  run: |
    ./mvnw -B verify -Dtest=SkipAll -Dit.test=MigrationIT -Dsurefire.failIfNoSpecifiedTests=false
  env:
    STABLE_APP_URL: ${{ env.STABLE_APP_URL }}
    PR_APP_URL: ${{ env.PR_APP_URL }}

Rollback-стратегии в контексте PR-тестирования



В PR-тестировании стратегия отката немного отличается от продакшена. Нам не нужно откатывать изменения, так как неудачный PR просто не мержится. Однако иногда полезно предусмотреть автоматический откат в рамках самого тестирования - например, если деплой приложения прошол успешно, но оно не запускается или не отвечает на запросы.
Я реализовал простую, но эффективную стратегию:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
name: Check application health
  run: |
    for i in {1..12}; do
      if curl -s -f ${{ env.APP_BASE_URL }}/health > /dev/null; then
        echo "Application is healthy"
        exit 0
      fi
      echo "Waiting for application to become healthy... Attempt $i of 12"
      sleep 10
    done
    echo "Application failed health check, performing rollback"
    kubectl rollout undo deployment/vcluster-pipeline --namespace ${{ env.NAMESPACE }}
    exit 1
Этот скрипт проверяет здоровье приложения в течение 2 минут. Если приложение не становится доступным, выполняется откат деплоймента и пайплайн завершается с ошибкой.

Работа с базами данных и состоянием



Отдельная головная боль - это миграции схемы базы данных. Тут у меня два подхода:

1. Для небольших проектов я использую встроенные механизмы миграции (Flyway, Liquibase) и позволяю приложению самостоятельно мигрировать схему при запуске.
2. Для крупных проектов предпочитаю отдельный шаг миграции в пайплайне:
YAML
1
2
3
4
name: Run database migrations
  run: |
    kubectl create job --from=cronjob/db-migrate db-migrate-${{ github.run_id }} --namespace ${{ env.NAMESPACE }}
    kubectl wait --for=condition=complete job/db-migrate-${{ github.run_id }} --timeout=180s --namespace ${{ env.NAMESPACE }}
Эта строатегия имеет ряд преимуществ:
  • Миграции выполняются до запуска приложения.
  • Легко отследить ошибки миграции.
  • Можно выполнить откат миграции в случае проблем.

В моем конкретном случае с PR-тестированием я использую упрощенный подход с Flyway, поскольку каждый PR получает свежую базу данных. Но в реальных проэктах такая стратегия может привести к проблемам, если разные PR изменяют схему базы по-разному. Еще один аспект, о котором стоит упомянуть - это использование заранее подготовленных данных для тестирования. В моей реализации я добавил дополнительный шаг для инициализации базы данных тестовыми данными:

YAML
1
2
3
name: Seed database with test data
  run: |
    kubectl exec -i $(kubectl get pod -l app.kubernetes.io/name=postgresql -o jsonpath='{.items[0].metadata.name}' --namespace ${{ env.NAMESPACE }}) -- psql -U postgres -d postgres < ./testdata/seed.sql
Это позволяет создать необходимый набор данных для тестирования и делает тесты более предсказуемыми.

Стратегии изоляции тестовых сред



Когда я начал внедрять тестирование PR в Kubernetes, одной из ключевых проблем оказалась необходимость надежной изоляции между разными тестовыми средами. Без правильной изоляции PR могут мешать друг другу, что сильно подрывает доверие к результатам тестов.

Использование пространств имен для разделения PR



Самый очевидный способ разделения ресурсов в Kubernetes - это использование отдельных пространств имен (namespaces). Именно такой подход я показал ранее, где для каждого PR создается уникальное пространство имен:

YAML
1
2
3
4
5
6
name: Create namespace for PR
  run: |
    NAMESPACE="pr-${{ github.event.pull_request.number }}"
    kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
    kubectl config set-context --current --namespace=$NAMESPACE
    echo "NAMESPACE=$NAMESPACE" >> $GITHUB_ENV
Это дает нам неплохую базовую изоляцию, но у нее есть и ограничения:
  • Разные PR все еще используют общие ресурсы кластера (CPU, память).
  • Они могут видеть пространства имен друг друга.
  • Сетевая изоляция по умолчанию отсутствует.

Для усиления изоляции я обычно добавляю сетевые политики (Network Policies):

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
name: Create network policy
  run: |
    cat <<EOF | kubectl apply -f -
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: allow-only-internal
      namespace: ${{ env.NAMESPACE }}
    spec:
      podSelector: {}
      policyTypes:
      - Ingress
      - Egress
      ingress:
      - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ${{ env.NAMESPACE }}
      egress:
      - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ${{ env.NAMESPACE }}
      - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
            - 10.0.0.0/8
            - 172.16.0.0/12
            - 192.168.0.0/16
    EOF
Эта политика запрещает подам из разных PR коммуницировать между собой, что дополнительно повышает изоляцию.

Квоты ресурсов для предотвращения конфликтов



Еще одна распостраненная проблема, которая у меня возникала - это истощение ресурсов кластера из-за "жадного" PR. Я помню ситуацию, когда один PR с нагрузочным тестированием полностью "забрал" все ресурсы кластера, из-за чего остальные тесты начали падать. Решение - установка ResourceQuota для каждого пространства имен:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name: Create resource quota
  run: |
    cat <<EOF | kubectl apply -f -
    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: pr-quota
      namespace: ${{ env.NAMESPACE }}
    spec:
      hard:
        requests.cpu: "2"
        requests.memory: 2Gi
        limits.cpu: "4"
        limits.memory: 4Gi
    EOF
Это гарантирует, что один PR не сможет захватить больше ресурсов, чем ему положено.

Динамическое создание и очистка тестовых окружений



Для эффективной работы системы очень важно не просто создавать тестовые среды, но и своевременно их уничтожать. Тут есть несколько стратегий:

1. Очистка по времени жизни - создаем CronJob, который удаляет пространства имен старше определенного возраста:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: Create cleanup job
  run: |
    cat <<EOF | kubectl apply -f -
    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: cleanup-old-prs
      namespace: default
    spec:
      schedule: "0 */6 * * *"
      jobTemplate:
        spec:
          template:
            spec:
              containers:
              - name: kubectl
                image: bitnami/kubectl
                command:
                - /bin/sh
                - -c
                - |
                  for ns in \$(kubectl get ns -l created-by=pr-test --output=jsonpath={.items[*].metadata.name}); do
                    age=\$(kubectl get ns \$ns -o go-template="{{.metadata.creationTimestamp}}")
                    now=\$(date -u +%Y-%m-%dT%H:%M:%SZ)
                    age_seconds=\$(( \$(date -d "\$now" +%s) - \$(date -d "\$age" +%s) ))
                    if [ \$age_seconds -gt 86400 ]; then
                      kubectl delete ns \$ns
                    fi
                  done
              restartPolicy: OnFailure
    EOF
2. Очистка по статусу PR - настройка вебхука, который удаляет среду, когда PR закрывается или мержится:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: Cleanup PR Environment
on:
  pull_request:
    types: [closed]
jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Authenticate with GKE
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/123456/locations/global/workloadIdentityPools/github-actions/providers/github
          service_account: [email]github-actions@project.iam.gserviceaccount.com[/email]
      
      - name: Get GKE credentials
        uses: google-github-actions/get-gke-credentials@v2
        with:
          cluster_name: test-pr-cluster
          location: europe-west3
          
      - name: Delete namespace
        run: kubectl delete namespace pr-${{ github.event.pull_request.number }} --ignore-not-found
Грамотная стратегия очистки тестовых сред не менее важна, чем их создание, особенно когда у вас активный проект с множеством PR каждый день.

Мониторинг и отладка процесса тестирования



Даже самый продуманный пайплайн для тестирования PR будет бесполезен, если вы не знаете, что происходит внутри. Когда вы запускаете тесты в Kubernetes, вы сталкиваетесь с дополнительным уровнем сложности по сравнению с локальным тестированием. Логи разбросаны по разным подам, метрики не собраны в одном месте, а отладка становится настоящим квестом. Поэтому правильная настройка мониторинга и отладки - ключевой фактор для успешного PR-тестирования.

Сбор логов из тестовой среды



Первое, что я реализовал в своем пайплайне - автоматический сбор логов со всех компонентов системы. Когда тест падает, я хочу иметь все логи под рукой, не переключаясь между разными системами. Для этого я добавил в пайплайн следующие шаги:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: Collect application logs
if: always()
run: |
  mkdir -p logs
  kubectl logs -l app=vcluster-pipeline --namespace ${{ env.NAMESPACE }} > logs/app.log || true
  kubectl logs -l app.kubernetes.io/name=postgresql --namespace ${{ env.NAMESPACE }} > logs/db.log || true
  
name: Collect pod events
if: always()
run: |
  kubectl get events --namespace ${{ env.NAMESPACE }} > logs/events.log || true
  
name: Upload logs as artifacts
if: always()
uses: actions/upload-artifact@v3
with:
  name: kubernetes-logs
  path: logs/
  retention-days: 5
Директива if: always() гарантирует, что логи собираются даже в случае сбоя предыдущих шагов, что критически важно для отладки. Особенно полезным оказалось сохранение событий Kubernetes - часто именно там скрывается причина проблемы, например, нехватка ресурсов или ошибки при получении образа. Я раньше пробовал реализовать системы централизованного логирования (ELK, Grafana Loki) для этих целей, но обнаружил, что для контекста PR-тестирования проще собирать логи напрямую и сохранять их как артефакты GitHub Actions.

Внедрение метрик производительности



Кроме логов, очень полезно собирать метрики производительности приложения. Это помогает обнаруживать регрессии производительности еще на этапе PR. Я внедрил два уровня мониторинга:

1. Базовые метрики Kubernetes - использование CPU, памяти, сети:

YAML
1
2
3
4
5
name: Collect resource metrics
if: always()
run: |
  kubectl top pods --namespace ${{ env.NAMESPACE }} > logs/resource_usage.log
  kubectl get pods -o wide --namespace ${{ env.NAMESPACE }} > logs/pods.log
2. Бизнес-метрики приложения - среднее время ответа, количество запросов в секунду, процент ошибок. Для этого я использую тесты нагрузки с помощью k6:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: Run performance test
run: |
  cat > performance.js <<EOF
  import http from 'k6/http';
  import { check, sleep } from 'k6';
  
  export default function() {
    const res = http.get('${{ env.APP_BASE_URL }}/api/v1/items');
    check(res, {
      'status is 200': (r) => r.status === 200,
      'response time < 200ms': (r) => r.timings.duration < 200
    });
    sleep(1);
  }
  EOF
  
  docker run --rm -v $(pwd):/scripts loadimpact/k6 run \
    --summary-export=logs/performance.json \
    /scripts/performance.js
Хитрость в том, что результаты этих тестов я не только сохраняю как артефакты, но и сравниваю с базовыми показателями:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
name: Compare performance with baseline
run: |
  PR_P95=$(cat logs/performance.json | jq '.metrics.http_req_duration.values."p(95)"')
  BASELINE_P95=$(cat baseline_metrics.json | jq '.metrics.http_req_duration.values."p(95)"')
  
  if (( $(echo "$PR_P95 > $BASELINE_P95 * 1.2" | bc -l) )); then
    echo "Performance degradation detected! P95 response time increased by more than 20%."
    echo "PR: $PR_P95 ms, Baseline: $BASELINE_P95 ms"
    echo "::warning::Performance degradation detected"
  else
    echo "Performance is within acceptable range"
  fi
Эта простая проверка спасла меня от множества регрессий производительности. Однажды разработчик случайно добавил N+1 запрос, который на маленьком наборе тестовых данных работал нормально, но сильно тормозил в продакшене - и именно сравнение метрик поймало эту проблему.

Интеграция результатов тестирования с PR



Результаты тестирования не должны оседать в логах CI/CD системы - они должны быть видны прямо в PR. Я использую GitHub Checks API для отображения детальной информации о результатах тестов:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
name: Report test results to PR
if: always()
uses: actions/github-script@v6
with:
  script: |
    const testResults = require('./test-results.json');
    
    const summary = {
      passed: testResults.filter(t => t.status === 'passed').length,
      failed: testResults.filter(t => t.status === 'failed').length,
      skipped: testResults.filter(t => t.status === 'skipped').length
    };
    
    const details = testResults
      .filter(t => t.status === 'failed')
      .map(t => `- ${t.name}: ${t.message}`)
      .join('\n');
    
    const conclusion = summary.failed > 0 ? 'failure' : 'success';
    
    await github.rest.checks.create({
      owner: context.repo.owner,
      repo: context.repo.repo,
      name: 'Integration Tests',
      head_sha: context.payload.pull_request.head.sha,
      status: 'completed',
      conclusion: conclusion,
      output: {
        title: `Tests: ${summary.passed} passed, ${summary.failed} failed, ${summary.skipped} skipped`,
        summary: [INLINE]### Test Results\n\n${details}[/INLINE],
        text: JSON.stringify(testResults, null, 2)
      }
    });
Кроме того, я добавляю интерактивный отчет о покрытии кода тестами прямо в PR:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
name: Generate code coverage report
run: |
  ./mvnw jacoco:report
  
name: Comment PR with coverage report
uses: actions/github-script@v6
with:
  script: |
    const fs = require('fs');
    const coverageData = JSON.parse(fs.readFileSync('./target/site/jacoco/jacoco.json', 'utf8'));
    
    const coverage = {
      instructions: coverageData.counters.find(c => c.type === 'INSTRUCTION').covered / coverageData.counters.find(c => c.type === 'INSTRUCTION').total * 100,
      branches: coverageData.counters.find(c => c.type === 'BRANCH').covered / coverageData.counters.find(c => c.type === 'BRANCH').total * 100,
      lines: coverageData.counters.find(c => c.type === 'LINE').covered / coverageData.counters.find(c => c.type === 'LINE').total * 100
    };
    
    const comment = `## Code Coverage Report
 
| Type | Coverage |
|------|----------|
| Instructions | ${coverage.instructions.toFixed(2)}% |
| Branches | ${coverage.branches.toFixed(2)}% |
| Lines | ${coverage.lines.toFixed(2)}% |
 
[View detailed report](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
    
    github.rest.issues.createComment({
      issue_number: context.issue.number,
      owner: context.repo.owner,
      repo: context.repo.repo,
      body: comment
    });
Такой подход делает процес ревью кода гораздо более эффективным - разработчики сразу видят проблемы и результаты тестов, не переключаясь между разными интерфейсами.

Профилирование приложений во время тестирования



Стандартные метрики не всегда помогают выявить узкие места в производительности. Для более глубокого анализа я внедрил профилирование JVM-приложений прямо в процесс тестирования PR. Для этого я использую Async Profiler и JFR:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: Run profiling
if: contains(github.event.pull_request.labels.*.name, 'profile')
run: |
  # Получаем PID Java-процесса
  POD_NAME=$(kubectl get pods -l app=vcluster-pipeline -o jsonpath='{.items[0].metadata.name}' --namespace ${{ env.NAMESPACE }})
  JAVA_PID=$(kubectl exec $POD_NAME --namespace ${{ env.NAMESPACE }} -- jps | grep -v Jps | cut -d ' ' -f 1)
  
  # Запускаем профилирование на 30 секунд
  kubectl exec $POD_NAME --namespace ${{ env.NAMESPACE }} -- \
    /opt/async-profiler/profiler.sh -d 30 -f /tmp/profile.html $JAVA_PID
  
  # Копируем результаты профилирования
  kubectl cp ${{ env.NAMESPACE }}/$POD_NAME:/tmp/profile.html ./logs/profile.html
  
name: Upload profile as artifact
if: contains(github.event.pull_request.labels.*.name, 'profile')
uses: actions/upload-artifact@v3
with:
  name: performance-profile
  path: logs/profile.html
  retention-days: 5
Это решение я использую выборочно, только для PR, помеченных меткой "profile", так как профилирование создает дополнительную нагрузку на систему. Один раз этот подход помог обнаружить неоптимальное использование памяти в коллекциях, которое вызывало частые сборки мусора. Проблема не проявлялась в интеграционных тестах, но приводила к значительной деградации производительности в продакшене.

Автоматизация уведомлений для команды



Помимо отображения результатов в PR, часто требуется активное оповещение разработчиков о статусе тестирования. Я внедрил систему уведомлений через Slack, которая отправляет сообщения в зависимости от результатов тестов:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: Send Slack notification
if: always()
uses: slackapi/slack-github-action@v1.24.0
with:
  payload: |
    {
      "text": "Integration Test Results for PR #${{ github.event.pull_request.number }}",
      "blocks": [
        {
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": "*PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}*\n${{ job.status == 'success' && 'Tests passed' || 'Tests failed' }}"
          }
        },
        {
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": "View PR: ${{ github.event.pull_request.html_url }}\nView workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          }
        }
      ]
    }
env:
  SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
  SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

Оптимизация производительности и затрат



Когда я начал использовать Kubernetes для тестирования PR, я быстро понял, что без оптимизации этот процесс может стать невероятно затратным как по времени, так и по деньгам. Каждый PR запускает несколько подов, использует вычислительные ресурсы и хранит данные – все это стоит денег. При активной разработке счета за облачные ресурсы могут расти как на дрожжах. Вот несколько стратегий, которые я применил для оптимизации.

Кэширование Docker образов и зависимостей



Одно из первых узких мест, которое я обнаружил – это время сборки Docker образов. При каждом новом PR приходилось заново скачивать все зависимости и собирать образ с нуля, что отнимало кучу времени. Я внедрил несколько оптимизаций:

YAML
1
2
3
4
5
6
7
8
9
10
11
name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3
 
name: Build and push Docker image
  uses: docker/build-push-action@v6
  with:
    context: .
    tags: ${{ steps.meta.outputs.tags }}
    push: true
    cache-from: type=gha                             # Использование кэша GitHub Actions
    cache-to: type=gha,mode=max
Эта настройка позволяет кэшировать слои Docker между запусками, что значительно ускоряет сборку. На моем проекте время сборки сократилось с 5-6 минут до 1-2 минут. Для Maven/Gradle проектов я также добавил кэширование зависимостей:

YAML
1
2
3
4
5
6
name: Cache Maven packages
  uses: actions/cache@v3
  with:
    path: ~/.m2
    key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
    restore-keys: ${{ runner.os }}-m2

Оптимизация размера образов



Следующий шаг – оптимизация размера образов. Меньший образ быстрее загружается в кластер и экономит место в реестре. Я начал использовать многоэтапные сборки и убрал все ненужное:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
# Стадия сборки
FROM maven:3.8.4-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
 
# Финальный образ
FROM openjdk:17-slim
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Размер образа уменьшился с 800Мб до 150Мб, что существенно ускорило его развертывание в кластере.

Стратегия предварительного прогрева кластера



Еще одна хитрость, которую я использую – "прогрев" кластера. Суть в том, чтобы заранее подготовить кластер к запуску тестов, вместо настройки всего "на лету". Я создал специальный джоб, который запускается по расписанию и делает следующее:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: Warm up cluster
  run: |
    # Предварительно скачиваем популярные образы
    kubectl create job warm-up-job --image=busybox -- echo "Warming up" || true
    kubectl create job postgres-preload --image=postgres:15 -- echo "Preloading postgres" || true
    
    # Создаем пулы подов для часто используемых компонентов
    kubectl apply -f - <<EOF
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: postgres-pool
      namespace: default
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: postgres-pool
      template:
        metadata:
          labels:
            app: postgres-pool
        spec:
          containers:
          - name: postgres
            image: postgres:15
            resources:
              requests:
                cpu: 100m
                memory: 200Mi
            command: ["sleep", "infinity"]
    EOF
Этот подход обеспечивает доступность образов на узлах кластера заранее, что ускоряет запуск подов во время тестирования PR.

Автоматическая очистка ресурсов



Забывать удалять ресурсы – верный способ получить неприятный счет в конце месяца. Я настроил автоматическую очистку по нескольким сценариям:

1. Очистка на основе TTL (Time To Live):

YAML
1
2
3
name: Set TTL for resources
  run: |
    kubectl annotate namespace ${{ env.NAMESPACE }} janitor/ttl=24h
2. Очистка на основе статуса PR – когда PR закрывается или мержится:

YAML
1
2
3
4
5
6
7
8
9
10
name: Cleanup PR Environment
on:
  pull_request:
    types: [closed]
jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Delete namespace
        run: kubectl delete namespace pr-${{ github.event.pull_request.number }} --ignore-not-found
3. Регулярная проверка и удаление "осиротевших" ресурсов:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
name: Cleanup orphaned resources
  run: |
    # Находим PR, которые уже закрыты, но их среды остались
    for ns in $(kubectl get ns -l created-by=pr-test --output=jsonpath={.items[*].metadata.name}); do
      PR_NUMBER=$(echo $ns | sed 's/pr-//')
      # Проверяем, существует ли еще PR
      if ! gh pr view $PR_NUMBER &> /dev/null; then
        kubectl delete ns $ns
      fi
    done
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Благодаря этим оптимизациям мне удалось сократить затраты на GKE примерно на 40% без снижения качества тестирования. Самый главный урок, который я извлек – оптимизируйте не только производительность, но и затраты с самого начала, иначе в конце месяца вас может ждать неприятный сюрприз.

Интеграция с системами Service Mesh для комплексного тестирования



В процессе настройки тестирования PR на Kubernetes я столкнулся с проблемой, которая заставила меня искать более продвинутое решение. Мне требовалось протестировать не только работу отдельных сервисов, но и взаимодействие между ними, включая маршрутизацию, отказоустойчивость и политики безопасности. Внедрение Service Mesh стало ключевым шагом в этом направлении.

Service Mesh — это выделенный слой инфраструктуры, который контролирует взаимодействие между сервисами. Для тестирования PR я выбрал Istio, хотя Linkerd тоже был неплохим вариантом из-за своей легковесности.
Вот как я интегрировал Istio в процесс тестирования:

YAML
1
2
3
4
5
name: Install Istio
  run: |
    curl -L [url]https://istio.io/downloadIstio[/url] | ISTIO_VERSION=1.18.2 sh -
    ./istio-1.18.2/bin/istioctl install --set profile=demo -y
    kubectl label namespace ${{ env.NAMESPACE }} istio-injection=enabled
Ключевой момент здесь — метка istio-injection=enabled, которая автоматически внедряет прокси-сайдкары Envoy во все поды в пространстве имен PR.
После этого я настроил виртуальные сервисы для тестирования различных сценариев маршрутизации:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name: Configure traffic routing
  run: |
    cat <<EOF | kubectl apply -f -
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: my-app-vs
      namespace: ${{ env.NAMESPACE }}
    spec:
      hosts:
      - "myapp.example.com"
      gateways:
      - my-gateway
      http:
      - match:
        - uri:
            prefix: /api/v1
        route:
        - destination:
            host: vcluster-pipeline
            port:
              number: 8080
    EOF
Это позволило мне тестировать сложные сценарии, такие как канареечные деплои или деплои с голубым/зеленым переключением прямо в контексте PR. Также я смог настроить политики ретраев, таймауты и цепочки вызовов между сервисами.
Одно из лучших применений Service Mesh в PR-тестировании — это симуляция сбоев:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: Setup fault injection
  run: |
    cat <<EOF | kubectl apply -f -
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: fault-injection
      namespace: ${{ env.NAMESPACE }}
    spec:
      hosts:
      - postgresql
      http:
      - fault:
          delay:
            percentage:
              value: 50
            fixedDelay: 5s
        route:
        - destination:
            host: postgresql
    EOF
Этот манифест имитирует задержки в 50% запросов к базе данных, что помогает проверить устойчивость приложения к сбоям инфраструктуры.

Интеграция с Service Mesh открыла для меня новый уровень тестирования, позволяя моделировать реалистичные условия эксплуатации и выявлять проблемы, которые иначе проявились бы только в продакшене. Однако стоит учитывать, что добавление Service Mesh увеличивает потребление ресурсов кластера, особенно при большом колличестве одновременных PR.

Заключение



Проделав весь этот путь настройки тестирования Pull Request на Kubernetes, я пришол к нескольким важным выводам. Во-первых, такой подход действительно окупается — обнаружение проблем до слияния PR экономит уйму времени и нервов всей команде. Во-вторых, хотя первоначальная настройка инфраструктуры требует усилий, дальнейшее поддержание и развитие системы становится все проще.

Конечно, решение не лишено компромисов. Нам приходится балансировать между степенью изоляции тестовых сред и затратами на инфраструктуру. Мы должны решать, насколько близко к продакшену должно быть тестовое окружение, и сколько мы готовы за это платить.

Если вы только начинаете внедрять тестирование PR в Kubernetes, я рекомендую идти поэтапно. Сначала настройте базовую инфраструктуру и простые тесты, затем добавляйте мониторинг, оптимизацию и продвинутые инструменты вроде Service Mesh по мере необходимости.

Github хочет pull request, который не нужен
В общем цвел и пах один проектик в локальном репо с несколькими ветками. Тут приспичило его...

Тесты кода на GitHub Actions
Как написать и запустить в github тесты? CodeStyle тест для html/css/js/nodejs. ...

Проблема в Docker файле, github actions
Господа, подскажите пожалуйста - почему у меня проект в github actions собирается нормально, но...

для чего в bitbucket существует Create pull request ?
Всем привет, Обьясните пожалуйста для чего в bitbucket существует Create pull request и как им...

Pull Request в иные ветки
Здравствуйте. Не разобрался с Pull Request в иные ветки репозиториев. Есть репозиторий:...

Как обновлять программы c Github(git pull)?
Собираюсь установить у себя медицинские экспертные системы. Если авторы что то поправят в...

Запуск docker образа в kubernetes
Контейнер в docker запускаю так: docker run --cap-add=SYS_ADMIN -ti -e &quot;container=docker&quot; -v...

Деплой телеграм бота на Google Kubernetes Engine через GitLab CI
Доброго времни суток. Прошу помощи у форумчан тк. сам не могу разобраться. Как задеплоить бота на...

Возможно ли поднять в kubernetes proxy
Задача. Дано: На роутере настроены 10 ip-адресов внешних от провайдера. На сервере vmware поднято...

Nginx + Kubernetes
Добрый день всем! Я решил попробовать использовать Kubernetes. Вот что я сделал на текущий...

Конфигурация ngnix для Kubernetes Deployment
Подскажите, что не так с nginx.conf переданным в ConfigMap для k8s? У меня на порту сервиса сайт не...

Где расположить БД для Kubernetes кластера в облаке
Привет. Нагуглил и разобрал пример, как разместить Spring-овый микросервис в кубернетес-кластере....

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
PowerShell Snippets
iNNOKENTIY21 11.11.2025
Модуль PowerShell 5. 1+ : Snippets. psm1 У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\ А в самом низу файла-профиля. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru