Вы запускаете новую версию приложения в Kubernetes, но после деплоя не можете быстро найти нужный под для отладки. Вы пытаетесь откатить релиз, но kubectl показывает несколько ReplicaSet с одинаковыми именами. Эти проблемы возникают из-за статичных имён подов в CI/CD пайплайне. Решение – автоматическое включение версии сборки в идентификатор ресурса. Это руководство предоставляет готовые скрипты для GitLab CI, GitHub Actions и Jenkins, которые извлекают версию из git-тегов, package.json или pom.xml и интегрируют её в процесс деплоя. Вы получите работающие конфигурации для безопасного использования динамических имён, механизмы отката и очистки старых ресурсов.
Проблема: почему статичные имена подов мешают CI/CD
Стандартный подход с именем пода "app-name-deployment" создаёт операционные сложности при частых релизах. Команда kubectl apply обновляет существующий ресурс по его имени в кластере. Когда вы меняете только образ контейнера в манифесте Deployment, имя пода в шаблоне остаётся прежним. Kubernetes создаёт новый ReplicaSet, но поды получают те же имена в своей метаданной. Это делает невозможным параллельное существование старой и новой версии приложения для blue-green или канареечного развертывания.
Типичный сценарий: разработчик запускает пайплайн для деплоя версии 1.2.0 поверх работающей 1.1.0. При возникновении ошибки требуется откат. Инженер выполняет kubectl rollout undo deployment/myapp, но не уверен, какая именно версия теперь запущена. В логах пайплайна отображается ошибка "Error from server (AlreadyExists): pods \"myapp-pod\" already exists" при попытке запустить два процесса одновременно. Поиск конкретного пода для диагностики занимает время – нужно проверять метки или описывать каждый контейнер.
Классический подход нарушает принцип идемпотентности деплоя – возможность безопасно применять одну конфигурацию многократно. С динамическими версиями каждый деплой создаёт уникальный идентификатор, что предотвращает конфликты, но требует изменения workflow. Helm с шаблоном имени релиза частично решает проблему, но при частых обновлениях создаёт множество релизных объектов в истории.
Ограничения kubectl apply и helm upgrade при частых релизах
Инструменты orchestration не "ломаются" при работе со статичными именами – они используются не по назначению в контексте CI/CD. Команда kubectl apply сравнивает текущее состояние ресурса в кластере с предоставленным манифестом и вносит минимальные изменения. Если изменился только spec.template.spec.containers[0].image, обновляется соответствующий ReplicaSet, но имя пода в spec.template.metadata.name остаётся неизменным. Для системы это один и тот же Deployment, а не две разные версии приложения.
Helm хранит историю релизов, но имя самого релиза обычно фиксировано или генерируется на основе чарта. При обновлении с новым тегом образа Helm создаёт новую ревизию, но поды внутри Deployment всё равно получают статичные имена из шаблона. Параллельный деплой двух разных версий через Helm требует создания отдельных релизов с разными именами, что усложняет управление.
Решение: динамические имена подов на основе версии сборки
Core-идея: включать уникальный идентификатор версии в метаданные пода. Это создаёт прозрачную связь между артефактом сборки и запущенным контейнером в кластере. Вы видите версию приложения по имени пода или его меткам без дополнительных запросов к API. Архитектура пайплайна становится линейной: получить версию → подставить в манифест → применить конфигурацию.
Выгоды динамического именования:
- Одновременное существование версий – запуск 1.1.0 и 1.2.0 рядом для A/B-тестирования или постепенного переноса трафика.
- Прозрачность окружения – команда
kubectl get podsсразу показывает, какая версия где работает. - Упрощенный откат – переключение селектора Service на предыдущую версию без пересоздания ресурсов.
- Автоматическая очистка – удаление подов с устаревшими версиями по label selector.
Схематичный пайплайн:
CI/CD Pipeline:
1. Сборка → Извлечь версию (git tag / package.json / CI variable)
2. Артефакт → Подставить версию в манифест (envsubst / sed / Helm values)
3. Деплой → kubectl apply с валидацией (--dry-run)
4. Верификация → Проверить health checks нового пода
5. Очистка → Удалить ресурсы старше N дней
Стратегии формирования уникального идентификатора версии
Выбор источника версии зависит от workflow проекта:
| Источник | Команда/метод | Плюсы | Минусы | Рекомендация |
|---|---|---|---|---|
| Git теги (semver) | git describe --tags --abbrev=0 |
Человекочитаемость, стандарт семантического версионирования | Требует дисциплины тегирования, только для релизов | Для production-релизов с контролем качества |
| Хэш коммита | git rev-parse --short HEAD |
Уникальность для каждого коммита, автоматическая генерация | Нечитаемость (abc123def), нет информации о мажорных версиях | Для dev/staging окружений, каждый коммит |
| Номер сборки CI | $CI_PIPELINE_IID (GitLab), $GITHUB_RUN_ID |
Уникальность в рамках CI-системы, простой инкремент | Привязка к конкретной CI, не переносимо между системами | Для внутренних нужд отслеживания в одной CI |
| Версия из package.json/pom.xml | jq -r .version package.json |
Синхронизация с версией пакета, один источник истины | Требует обновления файла перед коммитом | Для npm/Maven проектов с управлением версиями в коде |
Оптимальная стратегия – комбинированный подход: "app-1.2.3-abc123def". Основная семантическая версия из тега или package.json плюс короткий хэш коммита для уникальности. Это даёт читаемость и точную идентификацию исходного кода. Для детального сравнения стратегий управления версиями в Kubernetes, включая влияние на HPA и Service Mesh, изучите практическое сравнение хранения версии в метках или в имени пода.
Практика: готовые скрипты для извлечения версии
Каждый скрипт включает проверку ошибок и fallback-механизм. Перед использованием убедитесь, что инструменты (jq, xmllint) установлены в контейнере CI/CD.
Из git-тегов (семантическое версионирование)
Скрипт для проектов с тегированием релизов:
#!/bin/bash
set -e
# Пытаемся получить последний тег
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || true)
# Если тега нет, используем хэш коммита
if [ -z "$VERSION" ] || [ "$VERSION" = "" ]; then
VERSION="untagged-$(git rev-parse --short HEAD)"
echo "WARNING: No git tag found, using commit hash: $VERSION" >&2
else
echo "Found version from git tag: $VERSION"
fi
# Экспортируем переменную для использования в пайплайне
export APP_VERSION="${VERSION}"
echo "APP_VERSION=$APP_VERSION" >> $GITHUB_ENV # Для GitHub Actions
# Или для GitLab CI:
# echo "export APP_VERSION=\"$APP_VERSION\"" >> .env
# Валидация: версия не должна содержать пробелов или спецсимволов
if [[ ! "$APP_VERSION" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "ERROR: Invalid version format: $APP_VERSION" >&2
exit 1
fi
Из package.json (Node.js/JavaScript проекты)
Для проектов на Node.js с версией в package.json:
#!/bin/bash
set -e
# Проверяем существование файла
if [ ! -f "package.json" ]; then
echo "ERROR: package.json not found in current directory" >&2
exit 1
fi
# Извлекаем версию с помощью jq (должен быть установлен)
if command -v jq &> /dev/null; then
VERSION=$(jq -r '.version' package.json)
else
# Fallback без jq (менее надежный)
VERSION=$(grep -o '"version": "[^"]*"' package.json | head -1 | cut -d'"' -f4)
fi
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "ERROR: Could not extract version from package.json" >&2
exit 1
fi
echo "Version from package.json: $VERSION"
export APP_VERSION="${VERSION}"
Из pom.xml (Java/Maven проекты)
Для Java/Maven проектов:
#!/bin/bash
set -e
# Проверяем существование файла
if [ ! -f "pom.xml" ]; then
echo "ERROR: pom.xml not found in current directory" >&2
exit 1
fi
# Способ 1: с использованием xmllint (предпочтительный)
if command -v xmllint &> /dev/null; then
VERSION=$(xmllint --xpath '/*[local-name()="project"]/*[local-name()="version"]/text()' pom.xml 2>/dev/null || true)
fi
# Способ 2: fallback с grep/sed
if [ -z "$VERSION" ]; then
VERSION=$(grep -o '[^<]* ' pom.xml | head -1 | sed 's/\(.*\)<\/version>/\1/')
fi
# Если версия не найдена или содержит переменную Maven (${project.version})
if [ -z "$VERSION" ] || [[ "$VERSION" == \$* ]]; then
echo "WARNING: No explicit version in pom.xml, using default"
VERSION="1.0.0-SNAPSHOT"
fi
echo "Version from pom.xml: $VERSION"
export APP_VERSION="${VERSION}"
Из переменных окружения CI/CD
Универсальный скрипт с приоритетом источников:
#!/bin/bash
set -e
# Приоритет 1: Git tag из переменных CI
if [ -n "$CI_COMMIT_TAG" ]; then
VERSION="$CI_COMMIT_TAG" # GitLab CI
elif [ -n "$GITHUB_REF_NAME" ] && [[ "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
VERSION="${GITHUB_REF_NAME#v}" # GitHub Actions tag
# Приоритет 2: Номер сборки CI
elif [ -n "$CI_PIPELINE_IID" ]; then
VERSION="build-$CI_PIPELINE_IID" # GitLab CI
elif [ -n "$GITHUB_RUN_ID" ]; then
VERSION="run-$GITHUB_RUN_ID" # GitHub Actions
elif [ -n "$BUILD_NUMBER" ]; then
VERSION="jenkins-$BUILD_NUMBER" # Jenkins
# Приоритет 3: Хэш коммита
elif [ -n "$CI_COMMIT_SHORT_SHA" ]; then
VERSION="commit-$CI_COMMIT_SHORT_SHA"
elif [ -n "$GITHUB_SHA" ]; then
SHORT_SHA="${GITHUB_SHA:0:7}"
VERSION="commit-$SHORT_SHA"
else
# Последний fallback
VERSION="$(date +%Y%m%d-%H%M%S)"
echo "WARNING: Using timestamp as version: $VERSION" >&2
fi
export APP_VERSION="${VERSION}"
echo "CI/CD version: $APP_VERSION"
Интеграция: передача версии в манифесты Kubernetes
После извлечения версии её нужно передать в манифесты для деплоя. Рассмотрим два подхода: шаблонизацию raw YAML и работу с Helm.
Шаблонизация манифестов для kubectl apply
Ключевой момент: вместо изменения metadata.name пода используйте метки (labels). Это предотвращает создание новых ReplicaSet при каждом деплое и сохраняет стабильность Service селекторов. Пример deployment.yaml с плейсхолдером:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-deployment # Стабильное имя Deployment
spec:
replicas: 2
selector:
matchLabels:
app: myapp
version: "${APP_VERSION}" # Селектор по версии
template:
metadata:
labels:
app: myapp
version: "${APP_VERSION}" # Метка версии на подах
spec:
containers:
- name: myapp
image: myregistry.com/myapp:${APP_VERSION} # Тег образа
ports:
- containerPort: 8080
Скрипт подстановки с envsubst (должен быть установлен):
#!/bin/bash
set -e
# Проверяем, что переменная установлена
if [ -z "$APP_VERSION" ]; then
echo "ERROR: APP_VERSION is not set" >&2
exit 1
fi
# Подставляем переменные в манифест
export APP_VERSION
envsubst < deployment.yaml.template > deployment.yaml
# Валидация манифеста перед применением
kubectl apply -f deployment.yaml --dry-run=client
# Применение
kubectl apply -f deployment.yaml
# Проверка, что поды создались с правильными метками
kubectl get pods -l "app=myapp,version=${APP_VERSION}"
Альтернатива с sed для систем без envsubst:
sed "s/\$\{APP_VERSION\}/$APP_VERSION/g" deployment.yaml.template > deployment.yaml
Использование переменных в Helm Charts
Для Helm передайте версию через values или --set. Имя релиза Helm остаётся фиксированным, меняется только тег образа и метки. Пример values.yaml:
# values.yaml
image:
repository: myregistry.com/myapp
tag: "" # Будет переопределено в пайплайне
labels:
version: "" # Для меток пода
Шаблон Deployment в templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
selector:
matchLabels:
app: {{ .Chart.Name }}
{{- if .Values.labels.version }}
version: {{ .Values.labels.version | quote }}
{{- end }}
template:
metadata:
labels:
app: {{ .Chart.Name }}
{{- if .Values.labels.version }}
version: {{ .Values.labels.version | quote }}
{{- end }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
Команда деплоя в пайплайне:
helm upgrade --install myapp ./chart \
--set "image.tag=$APP_VERSION" \
--set "labels.version=$APP_VERSION" \
--namespace production \
--atomic # Автоматический откат при ошибке
Для комплексного понимания работы с Deployment, включая стратегии обновления и настройку ресурсов, обратитесь к полному руководству по Kubernetes Deployment.
Безопасность и эксплуатация: избегаем ловушек
Динамическое именование создаёт операционные риски, которые нужно mitigate на этапе проектирования пайплайна.
Контроль уникальности и очистка старых ресурсов
Стратегии управления историей деплоев:
- Использование меток вместо имён подов – оставляйте
metadata.nameпустым или генерируемым Kubernetes. Идентификацию версии выполняйте через labelversion: 1.2.3. Это сохраняет стабильность Service и HorizontalPodAutoscaler. - Ограничение истории ReplicaSet – настройте
spec.revisionHistoryLimitв Deployment:
apiVersion: apps/v1
kind: Deployment
spec:
revisionHistoryLimit: 3 # Хранить только 3 предыдущих ReplicaSet
# ... остальная конфигурация
- Периодическая очистка устаревших ресурсов – джоба в пайплайне или CronJob в кластере:
#!/bin/bash
# Удалить поды старше 7 дней
kubectl delete pods \
--field-selector="status.phase=Succeeded,metadata.creationTimestamp<$(date -d '7 days ago' -u +'%Y-%m-%dT%H:%M:%SZ')" \
--namespace=production
# Удалить ReplicaSet без активных подов
kubectl delete replicasets \
--selector="app=myapp" \
--field-selector="status.replicas=0" \
--namespace=production
Механизм быстрого и точного отката
С динамическими метками история ReplicaSet становится чистой и предсказуемой. Команда отката:
# Просмотреть историю деплоев
kubectl rollout history deployment/myapp-deployment
# Откат на предыдущую ревизию
kubectl rollout undo deployment/myapp-deployment
# Откат на конкретную ревизию
kubectl rollout undo deployment/myapp-deployment --to-revision=3
# Проверить, какая версия сейчас запущена
kubectl describe deployment/myapp-deployment | grep -A2 "Labels:"
При использовании меток откат сводится к обновлению селектора Service на предыдущую версию подов. Сравните с хаотичным поиском нужного ReplicaSet при статичных именах.
Обработка ошибок и валидация в пайплайне
Добавьте проверки на каждом этапе:
#!/bin/bash
set -e # Выход при первой ошибке
# 1. Проверка переменной версии
if [ -z "$APP_VERSION" ]; then
echo "ERROR: APP_VERSION is empty" >&2
exit 1
fi
# 2. Валидация формата версии (опционально)
if [[ ! "$APP_VERSION" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "ERROR: Invalid version format: $APP_VERSION" >&2
exit 1
fi
# 3. Dry-run перед реальным деплоем
kubectl apply -f deployment.yaml --dry-run=client --validate=true
# 4. Применение с таймаутом и проверкой готовности
kubectl apply -f deployment.yaml
# Ждём, пока все поды станут Ready
kubectl wait --for=condition=Ready pods -l "app=myapp,version=${APP_VERSION}" \
--timeout=300s \
--namespace=production
# 5. Проверка health checks
kubectl get pods -l "app=myapp,version=${APP_VERSION}" -o json | \
jq '.items[] | select(.status.containerStatuses[].ready != true)' | \
if [ -n "$(cat)" ]; then
echo "ERROR: Not all containers are ready" >&2
# Триггер отката
kubectl rollout undo deployment/myapp-deployment
exit 1
fi
Готовые конфигурации для вашего CI/CD
Пример для GitLab CI (.gitlab-ci.yml)
stages:
- build
- deploy
variables:
KUBE_NAMESPACE: production
APP_NAME: myapp
# Этап извлечения версии
get-version:
stage: build
script:
- |
# Приоритет: тег → хэш коммита
if [ -n "$CI_COMMIT_TAG" ]; then
export APP_VERSION="$CI_COMMIT_TAG"
else
export APP_VERSION="${CI_COMMIT_SHORT_SHA}"
fi
echo "APP_VERSION=${APP_VERSION}" > version.env
artifacts:
reports:
dotenv: version.env
deploy-to-k8s:
stage: deploy
image: bitnami/kubectl:latest
dependencies:
- get-version
script:
- source version.env
# Подстановка версии в манифест
- envsubst < deployment.yaml.template > deployment.yaml
# Dry-run валидация
- kubectl apply -f deployment.yaml --dry-run=client --namespace=$KUBE_NAMESPACE
# Реальный деплой
- kubectl apply -f deployment.yaml --namespace=$KUBE_NAMESPACE
# Ожидание готовности
- kubectl rollout status deployment/${APP_NAME} --namespace=$KUBE_NAMESPACE --timeout=300s
environment:
name: production
url: https://myapp.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_TAG
when: manual # Требует ручного подтверждения для production
Пример для GitHub Actions (.github/workflows/deploy.yml)
name: Deploy to Kubernetes
on:
push:
tags:
- 'v*' # Запуск на тегах v1.2.3
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
env:
KUBE_NAMESPACE: production
APP_NAME: myapp
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Для работы с git тегами
- name: Extract version
id: version
run: |
# Из тега или хэша коммита
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="${GITHUB_SHA:0:7}"
fi
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "APP_VERSION=${VERSION}" >> $GITHUB_ENV
- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'latest'
- name: Configure kubeconfig
run: |
echo "${{ secrets.KUBECONFIG }}" > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy to Kubernetes
run: |
# Подстановка версии
export APP_VERSION="${{ env.APP_VERSION }}"
envsubst < k8s/deployment.yaml.template > deployment.yaml
# Валидация и деплой
kubectl apply -f deployment.yaml --dry-run=client
kubectl apply -f deployment.yaml
# Проверка статуса
kubectl rollout status deployment/${{ env.APP_NAME }} \
--namespace=${{ env.KUBE_NAMESPACE }} \
--timeout=5m
- name: Cleanup old pods
if: success()
run: |
# Удалить поды старше 1 дня
kubectl delete pods \
--field-selector="status.phase=Succeeded,metadata.creationTimestamp<$(date -d '1 day ago' -u +'%Y-%m-%dT%H:%M:%SZ')" \
--namespace=${{ env.KUBE_NAMESPACE }} || true
Пример для Jenkins (Jenkinsfile, Declarative Pipeline)
pipeline {
agent any
environment {
// Извлечение версии из git
APP_VERSION = sh(
script: '''
if git describe --tags --exact-match >/dev/null 2>&1; then
git describe --tags --exact-match
else
git rev-parse --short HEAD
fi
''',
returnStdout: true
).trim()
KUBE_NAMESPACE = 'production'
APP_NAME = 'myapp'
}
stages {
stage('Build and Test') {
steps {
sh 'docker build -t myregistry.com/${APP_NAME}:${APP_VERSION} .'
sh 'docker push myregistry.com/${APP_NAME}:${APP_VERSION}'
}
}
stage('Deploy to Kubernetes') {
steps {
withCredentials([
file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')
]) {
sh '''
# Подготовка манифеста
export APP_VERSION
envsubst < k8s/deployment.yaml.template > deployment.yaml
# Валидация
kubectl apply -f deployment.yaml --dry-run=client \
--namespace=${KUBE_NAMESPACE}
# Деплой
kubectl apply -f deployment.yaml \
--namespace=${KUBE_NAMESPACE}
# Ожидание готовности
kubectl rollout status deployment/${APP_NAME} \
--namespace=${KUBE_NAMESPACE} \
--timeout=300s
'''
}
}
post {
failure {
sh '''
# Автоматический откат при ошибке
kubectl rollout undo deployment/${APP_NAME} \
--namespace=${KUBE_NAMESPACE}
'''
}
}
}
}
}
Для построения комплексных CI/CD пайплайнов с поддержкой стратегий blue-green и canary развертывания изучите полное руководство по автоматизации развертывания в production.
Ответы на частые вопросы и устранение проблем
Что делать, если мой кластер не дает создавать поды с динамическими именами?
Политики безопасности (Pod Security Policies, OPA Gatekeeper, Kyverno) могут блокировать создание подов с изменяющимися именами. Решение: не меняйте metadata.name пода, оставьте его пустым или сгенерированным Kubernetes. Идентификацию версии выполняйте через метку в metadata.labels. Deployment с обновлённой меткой версии создаст новый ReplicaSet с новой версией подов, но имена подов будут сгенерированы автоматически (например, "myapp-deployment-abc123-def456"). Это обходит ограничения политик, ориентированных на статичные имена.
Если политики проверяют метки, убедитесь, что label version соответствует разрешённому формату (например, семантическому версионированию). Для enterprise-кластеров с обязательным версионированием через политики OPA ознакомьтесь с руководством по использованию меток и селекторов в Kubernetes.
Скрипт не находит версию в package.json/pom.xml
Возможные причины и решения:
- Файл не в корне проекта – проверьте путь:
cat ./subdirectory/package.jsonили используйте поиск:find . -name "package.json" -type f. - Невалидный JSON/XML – проверьте синтаксис:
jq . package.jsonилиxmllint --format pom.xml. - Отсутствуют инструменты – установите jq или xmllint в контейнер CI/CD. Для Dockerfile:
RUN apt-get update && apt-get install -y jq libxml2-utils. - Версия определена через переменную – в Maven
<version>${project.version}</version>требует обработки properties. Используйте:mvn help:evaluate -Dexpression=project.version -q -DforceStdout.
Как это решение работает с Service и Ingress?
Service использует селектор по меткам для нахождения подов. При изменении метки version у подов есть два подхода:
- Service без версии в селекторе – селектор
app: myappнаправляет трафик на все поды приложения независимо от версии. Подходит для одновременного запуска нескольких версий с распределением через другие механизмы (например, заголовки запросов). - Service с версией в селекторе – селектор
app: myapp, version: 1.2.3направляет трафик только на конкретную версию. Для канареечного развертывания создайте второй Service с селектором на новую версию и направляйте часть трафика через Ingress.
Пример Ingress для канареечного трафика:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service-v1-2-2 # 98% трафика
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: myapp-service-v1-2-3 # 2% трафика (канареечная)
port:
number: 80
Для выбора между GitOps и традиционным Infrastructure as Code при построении таких конфигураций изучите практическое сравнение подходов для DevOps.
Автоматическое именование подов по версии устраняет операционный хаос в CI/CD пайплайнах. Начните с простого скрипта извлечения версии из git-тега, интегрируйте его в существующий пайплайн и постепенно добавляйте валидации и механизмы очистки. Используйте метки вместо изменения имён подов для совместимости с политиками безопасности и Service. Регулярно проверяйте актуальность конфигураций – технологии CI/CD и Kubernetes активно развиваются. Для экспериментов с различными подходами к интеграции ИИ в разработку рассмотрите использование агрегатора AiTunnel, который предоставляет единый интерфейс к более чем 200 моделям нейросетей, включая GPT, Gemini и Claude, с оплатой в рублях и без необходимости VPN.