GitLab CI и GitOps
Если GitHub Actions — оркестратор экшенов, то GitLab CI — оркестратор job’ов, описанных одним файлом. Синтаксис иной, концепции те же: стадии, кэш, артефакты, матрицы, секреты. Но главный сюжет этой главы — не синтаксис, а GitOps: способ деплоя, при котором CI теряет прямой доступ к кластерам, а правда о состоянии системы живёт в git. Это то, как деплоят системы, которым нельзя просто «не работать сегодня».
Анатомия .gitlab-ci.yml
Заголовок раздела «Анатомия .gitlab-ci.yml»Полный пример для Node.js-приложения с безопасностью и GitOps-финалом. Все ключи и их семантика — в GitLab CI/CD YAML reference:
stages: [lint, test, build, scan, deploy]
variables: IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA DOCKER_TLS_CERTDIR: "/certs" # TLS между клиентом и dind-сервисом
# ── Тесты: кэш node_modules между job'ами пайплайна ─────────────────lint:eslint: stage: lint image: node:22-alpine script: - npm ci - npm run lint cache: key: $CI_COMMIT_REF_SLUG # имя ветки: кэш общий для всех job'ов ветки paths: [node_modules/]
test:unit: stage: test image: node:22-alpine script: - npm ci - npm test -- --coverage cache: key: $CI_COMMIT_REF_SLUG paths: [node_modules/] artifacts: # отчёт покрытия гарантированно доступен дальше when: always paths: [coverage/] expire_in: 1 week
# ── Сборка образа: docker-in-docker ─────────────────────────────────build:image: stage: build image: docker:27 services: [docker:27-dind] # отдельный под-контейнер с Docker daemon script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build --pull -t $IMAGE . - docker push $IMAGE rules: - if: $CI_COMMIT_BRANCH == "main" # образы собираем только из main - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: manual # в MR — по кнопке (проверка Dockerfile)
# ── Сканирование ────────────────────────────────────────────────────scan:trivy: stage: scan image: aquasec/trivy:latest script: - trivy image --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed $IMAGE - trivy fs --severity HIGH --exit-code 1 . rules: - if: $CI_COMMIT_BRANCH == "main"
# ── GitOps: бамп тега в инфра-репозитории ───────────────────────────deploy:gitops: stage: deploy image: alpine/git:latest script: - git clone --depth 1 https://gitlab-ci-token:$INFRA_TOKEN@gitlab.darkpix.dev/infra/pet-infra.git - cd pet-infra - sed -i "s|image: .*pet-api:.*|image: $IMAGE|" apps/api/deployment.yaml - | git config user.name "ci-bot" git config user.email "ci@darkpix.dev" git commit -am "deploy(api): $CI_COMMIT_SHORT_SHA [skip ci]" git push rules: - if: $CI_COMMIT_BRANCH == "main"rules/when: дешёвый аналог if
Заголовок раздела «rules/when: дешёвый аналог if»rules — список условий, ищется первое совпадение сверху вниз; несовпадение всех — job пропущен. when: manual — пауза до нажатия, when: never — явный запрет, changes: — path-filter (только если поменялись файлы):
deploy:gitops: rules: - if: $CI_PIPELINE_SOURCE == "schedule" when: never # никогда из расписания - if: $CI_COMMIT_BRANCH == "main" changes: [apps/api/**, .gitlab-ci.yml] # только если тронули apiArtifacts против cache
Заголовок раздела «Artifacts против cache»Путаница номер один в GitLab CI. Механика разная:
| cache | artifacts | |
|---|---|---|
| Назначение | Ускорение: node_modules между job’ами и пайплайнами |
Передача результата: бинарники, отчёты, покрытие |
| Гарантия | Нет: может отсутствовать, ключ устарел | Да: скачивается needs job’ами и после прогона |
| Ключ | Свой (ветка, lockfile) | Привязан к job’у |
| Скачивание | Автоматически (если ключ совпал) | needs: [job] или вручную |
Практика: node_modules/ — кэш (если пропал — npm ci просто дольше), coverage/, dist/ — артефакты. Секреты — никогда ни в кэш, ни в артефакты: .dockerignore и .gitignore в артефактах проверяй дважды.
docker:dind против kaniko
Заголовок раздела «docker:dind против kaniko»Сборка внутри контейнера — известная боль. Три пути:
# Вариант 1: docker-in-docker — демон как сервис (как выше).# + Привычный docker build; — привилегированный режим, тяжело, требует TLS.
# Вариант 2: kaniko — сборка без демона, userspace, без привилегий.# https://github.com/GoogleContainerTools/kaniko — реализация и ограничения.build:kaniko: stage: build image: name: gcr.io/kaniko-project/executor:v1.23.2-debug entrypoint: [""] script: - mkdir -p /kaniko/.docker - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n $CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD | base64)\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context $CI_PROJECT_DIR --destination $IMAGE --cache=true # кэш слоёв в registry
# Вариант 3: buildah — аналог kaniko, стандарт OpenShift.Для shared runner’ов без privileged-режима kaniko — единственный вариант. Для своего runner’а dind удобнее и быстрее (слоёный кэш buildx лучше). Kaniko-кэш живёт в registry (--cache=true) — работает даже с чистыми runner’ами.
Runner’ы
Заголовок раздела «Runner’ы»Job’ы исполняет runner — агент, опрашивающий GitLab. Типы и executor’ы разобраны в документации GitLab Runner:
- Shared (gitLab.com) — чужие машины, ограничения по минутам, не для приватного кода с секретами.
- Specific/Group/Project runner — свой. Классика: Docker executor (каждый job — контейнер на хосте), shell executor (job’ы прямо на хосте), Kubernetes executor (job’ы — поды в кластере, автомасштабирование).
# Регистрация (docker executor)gitlab-runner register \ --url https://gitlab.darkpix.dev \ --token glrt-xxxxxxxx \ --executor docker \ --docker-image alpine:latest \ --docker-volumes /var/run/docker.sock:/var/run/docker.sockКлючевые теги в конфиге: concurrent (сколько job’ов параллельно), limit на проект, кэш через S3 ([runners.cache]) — дефолтный локальный кэш виден только одному runner’у, с S3 — всеми. Untagged job’ы: решай явно, брать ли их на runner (по умолчанию — нет, тег run_untagged = false).
Безопасность в пайплайне
Заголовок раздела «Безопасность в пайплайне»Сканирование — стадия между сборкой и деплоем. Trivy — два режима: образ (OS-пакеты + зависимости приложения) и filesystem (IaC-конфиги: Dockerfile, .gitlab-ci.yml, манифесты K8s):
scan:trivy-image: stage: scan image: aquasec/trivy:0.57.1 script: - trivy image --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed --format table $IMAGE # отчёт артефактом для аудита after_script: - trivy image --severity CRITICAL,HIGH --format json -o trivy-report.json $IMAGE || true artifacts: { when: always, paths: [trivy-report.json], expire_in: 4 weeks }Snyk (кратко): глубже по зависимостям — умеет анализировать license compliance, remediation-советы («обнови до 2.3.1, там фикс») и мониторить уже задеплоенные образы. Платный для коммерческого использования, для pet-проекта — бесплатный tier:
scan:snyk: stage: scan image: node:22-alpine script: - npm install -g snyk - snyk test --severity-threshold=high - snyk container test $IMAGE --severity-threshold=high variables: SNYK_TOKEN: $SNYK_TOKEN # masked + protected variable в настройках проектаСекреты в GitLab: CI/CD Variables с флагами masked (не показывать в логах) и protected (только на protected-ветках — т.е. в main). Правило: токены с минимальными правами (deploy-роль в registry, бранч-протекшн на main), срок жизни — короткий, аудит использования — включённый.
GitOps: pull против push
Заголовок раздела «GitOps: pull против push»Классический деплой из CI — push: пайплайн берёт креденшелы кластера и что-то меняет (kubectl apply, helm upgrade, ssh docker pull). Проблемы: креденшелы живут в CI (утечка = доступ к проду), история «что сейчас в кластере» неизвестна (кто-то мог применить манифест руками), откат — ручной.
GitOps (pull): CI лишь пушит новый тег образа в инфра-репозиторий (как job deploy:gitops выше). Агент в кластере — ArgoCD или Flux — следит за репозиторием и сам приводит кластер к состоянию из git. Креденшелов кластера в CI нет вообще: кластер тянет, а не CI толкает.
PUSH-модель PULL-модель (GitOps)┌────────┐ kubectl apply ┌──────────┐ ┌────────┐ commit ┌──────────┐│ CI │ ─────────────► │ Кластер │ │ CI │ ────────► │ Infra git││(секреты│ │(секреты! │ │(только │ │(правда!) ││ кластер│◄────────────── │ │ │ push) │ │ │└────────┘ └──────────┘ └────────┘ └────┬─────┘ │ watch+pull ┌────▼─────┐ │ ArgoCD │ │ в кластере│ └────┬─────┘ ┌────▼─────┐ │ Кластер │ └──────────┘Бонусы: откат = git revert (история деплоев — история git), дрейф конфигурации сам подсвечивается (OutOfSync в ArgoCD), кластер доступен только изнутри.
ArgoCD: установка и Application
Заголовок раздела «ArgoCD: установка и Application»Концепции ArgoCD (Application, project, sync policy) — в официальной документации.
kubectl create namespace argocdkubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlkubectl -n argocd port-forward svc/argocd-server 8080:443# пароль: kubectl -n argocd get secret argocd-initial-admin-secret \# -o jsonpath='{.data.password}' | base64 -dМинимальный Application (декларативное описание «следи за этим git, приводи этот namespace к нему»):
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: pet-api namespace: argocdspec: project: default source: repoURL: https://gitlab.darkpix.dev/infra/pet-infra.git targetRevision: main path: apps/api destination: server: https://kubernetes.default.svc # свой кластер namespace: pet syncPolicy: automated: # автосинхронизация: увидел diff — применил prune: true # удалять ресурсы, удалённые из git selfHeal: true # лечить дрейф (ручные правки в кластере) syncOptions: [CreateNamespace=true] retry: { limit: 5, backoff: { duration: 5s, factor: 2, maxDuration: 3m } }Теперь цикл деплоя: CI бампает тег в apps/api/deployment.yaml инфра-репозитория → ArgoCD видит diff (polling каждые 3 минуты или webhook) → синхронизирует. Откат плохого релиза: git revert коммита бампа — ArgoCD сам вернёт старый образ. Ручной kubectl set image теперь бессмысленен: selfHeal вернёт состояние из git за 3 минуты — а это фича, а не баг.
App-of-apps и Flux кратко
Заголовок раздела «App-of-apps и Flux кратко»Когда приложений много, Application’ы становятся boilerplate. App-of-apps: один корневой Application следит за директорией, в которой лежат Application’ы (helm-чартом или простыми манифестами) — ArgoCD рекурсивно разворачивает их (паттерн описан в документации ArgoCD):
pet-infra/├── root-application.yaml # Application на всю директорию clusters/└── clusters/prod/ ├── pet-api.yaml # Application (api) ├── pet-frontend.yaml # Application (frontend) ├── monitoring.yaml # Application (kube-prometheus-stack) └── argocd.yaml # Application (сам ArgoCD — self-management)Плюс отделение сред: ApplicationSet генерирует Application на каждый кластер/окружение из шаблона + списка сред (dev/stage/prod) — одна правда, три кластера.
Flux — альтернатива от CNCF (тот же pull, иная реализация): не GUI, а набор контроллеров (source-controller, kustomize-controller, helm-controller, notification-controller); состояние в CRD GitRepository + Kustomization/HelmRelease. Детали — в документации Flux. Выбор: ArgoCD — визуализация и управление из UI, богатые sync-стратегии; Flux — «всё кодом», нативный GitOps-нрав, легче старт в существующем кластере. Механика одна, термины разные.
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»- Артефакты вместо кэша для
node_modules. Артефакт пересобирается каждый job и перекачивается целиком — медленнее, чем честныйnpm ciбез кэша. node_modules — кэш, результаты — артефакты. - dind без TLS (
DOCKER_TLS_CERTDIR). Демон с открытым API на 2375 — любой job в сети runner’а рутовый доступ к хосту. Всегда TLS-директория, либо kaniko. - Кэш слоёв Docker «из коробки» нет. dind стартует чистым каждый раз; слои теряются. Кэшируй: kaniko
--cache=true, buildkit с registry-кэшем, илиdocker:buildxс экспортом кэша в registry. - Ручной
kubectl applyв обход ArgoCD. SelfHeal молча откатит. Изменения — только через git. Прямой доступ к кластеру для разработчиков убирай вовсе (а доступ CI — в предыдущем пункте). - CI пушит в main инфра-репозитория без проверки. Сломанный манифест уронит прод при автосинке. Включи в инфра-репо: обязательный MR +
kubeval/kube-linterв пайплайне + branch protection. - Тег
:latestв GitOps-репозитории. ArgoCD сравнивает состояние;latestне меняется при новых пушах в registry (тег перезаписывается, манифест — нет), и синхронизация не срабатывает. Всегда иммутабельные теги:$CI_COMMIT_SHORT_SHAили semver. [skip ci]забыт на бамп-коммите. Пайплайн в инфра-репе запускается, пытается бампнуть то же самое, конфликт пуша. Мелочь, а отлаживать больно — особенно ночью.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- Artifacts против cache в GitLab CI? Cache — оптимизация без гарантий (общий по ключу, может отсутствовать). Artifacts — гарантированная передача результатов job’а следующим и после прогона, с ограниченным временем жизни.
- dind против kaniko? dind — полноценный демон, быстрый, но требует privileged-режима и TLS. kaniko — userspace-сборка без демона и привилегий, кэш в registry; безопаснее на shared-инфраструктуре, чуть ограниченнее по директивам Dockerfile.
- rules против only/except?
rules— современный механизм: первое совпавшее правило побеждает, комбинирует условия (if/changes/exists/when).only/except— legacy с двусмысленной семантикой OR. В новых конфигах — только rules. - В чём суть GitOps и чем pull лучше push? Правда о состоянии в git; агент в кластере сам приводит его к git-ревизии. Креденшелов кластера в CI нет, откат = revert, дрейф виден и лечится автоматически.
- Как ArgoCD узнаёт об изменениях? Polling репозитория (по умолчанию 3 мин) или webhook из GitLab/GitHub; дальше diff текущего состояния кластера (live) с желаемым (git) и синхронизация по syncPolicy.
- Что такое app-of-apps? Паттерн: корневой Application следит за директорией, содержащей другие Application’ы — декларативное управление самими приложениями ArgoCD (GitOps для GitOps-конфигурации).
- Зачем selfHeal и prune? selfHeal возвращает ручные правки в кластере к git-состоянию (борьба с дрейфом), prune удаляет ресурсы, исчезнувшие из git. Без них кластер и git расходятся незаметно.
- ArgoCD против Flux? Одна механика (pull из git в кластер), разная реализация: ArgoCD — сервер с UI, визуальный diff, Application CRD; Flux — набор контроллеров, всё через GitRepository/Kustomization CRD, без UI из коробки.
Практика
Заголовок раздела «Практика»- Разверни GitLab Runner (docker executor) на VPS, зарегистрируй в проекте. Прогони пайплайн из главы, сравни: job с кэшем против job без (удали ключ кэша) — разница во времени.
- Перепиши
build:imageна kaniko с--cache=true. Сделай пять сборок подряд: первую после изменения кода, вторую без изменений. Объясни по логам, какие слои взяты из кэша. - Добавь Trivy: собери образ со старой версией пакета (
npm i lodash@4.17.15), убедись, что scan падает сexit-code 1и артефакт-отчёт содержит CVE. - Установи ArgoCD в k3s/kind, вынеси манифесты Deployment+Service в инфра-репозиторий, создай Application с automated+selfHeal. Сделай деплой через бамп тега из CI (или руками в git), проверь синхронизацию в UI.
- Инциент-учение: отредактируй Deployment руками (
kubectl scale), посмотри на OutOfSync в ArgoCD, дождись selfHeal. Затем удали ресурс из git — проверь prune. Наконец, откати деплойgit revertбамп-коммита.
Что почитать
Заголовок раздела «Что почитать»- GitLab CI/CD YAML reference — полный справочник ключей
- GitLab Runners — executors, кэш через S3, регистрация
- Trivy — image/fs/config сканирование
- ArgoCD Documentation — core concepts, Application, app-of-apps, ApplicationSet
- Flux CD — альтернативная реализация GitOps