GitLab CI: build/test для Go и React + Docker images

GitLab CI: build/test для Go и React + Docker images

CI (Continuous Integration) - автоматизация рутины: тесты, линтинг, сборка образов, деплой. GitLab CI запускает пайплайн при каждом пуше - робот делает скучное за тебя.

Как работает GitLab CI

git push → парсинг .gitlab-ci.yml → pipeline разбрасывает stages lint/test/build веером

Пайплайн = набор stages (этапов), выполняемых последовательно. Внутри stage - jobs (задачи), которые могут выполняться параллельно.

GitLab CI pipeline: lint -> test -> build -> deploy, артефакты между stages, jobs внутри stage параллельно

.gitlab-ci.yml - базовая структура

stages:
 - lint
 - test
 - build

variables:
  GOFLAGS: "-count=1"  # без кэша тестов

# ---------- Lint ----------
backend:lint:
  image: golangci/golangci-lint:latest
  stage: lint
  script:
 - cd backend
 - golangci-lint run ./...
  only:
 - merge_requests

frontend:lint:
  image: node:20-alpine
  stage: lint
  script:
 - cd frontend
 - npm ci
 - npm run lint
  only:
 - merge_requests

# ---------- Test ----------
backend:test:
  image: golang:1.22-alpine
  stage: test
  script:
 - cd backend
 - go test ./...
  only:
 - merge_requests

frontend:test:
  image: node:20-alpine
  stage: test
  script:
 - cd frontend
 - npm ci
 - npm run test - --run
  only:
 - merge_requests
Линтинг и тесты запускаются только на [Merge Request](../git/07-mr.md) - не тратим ресурсы на каждый коммит в feature-ветку. Для `main` запускается build + deploy.

npm ci и lockfile

npm ci работает только с package-lock.json. Без него - ошибка:

npm ERR! `npm ci` can only install packages when your package.json
npm ERR! and package-lock.json are in sync.

Решение:

cd frontend
npm install              # создаёт package-lock.json
git add package-lock.json
git commit -m "chore: add lockfile"
В `.gitignore` должен быть `node_modules/`. В CI зависимости ставятся с нуля через `npm ci`. Lock-файл гарантирует одинаковые версии.

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

Без кэша каждый job скачивает зависимости заново. С кэшем - использует сохранённые:

backend:test:
  image: golang:1.22-alpine
  stage: test
  script:
 - cd backend
 - go test ./...
  cache:
    key: go-${CI_COMMIT_REF_SLUG}
    paths:
 - backend/.go/pkg/mod/
  variables:
    GOPATH: "${CI_PROJECT_DIR}/backend/.go"

frontend:test:
  image: node:20-alpine
  stage: test
  script:
 - cd frontend
 - npm ci
 - npm run test - --run
  cache:
    key: frontend-${CI_COMMIT_REF_SLUG}
    paths:
 - frontend/node_modules/

${CI_COMMIT_REF_SLUG} - имя ветки. Каждая ветка имеет свой кэш.

Сборка Docker-образов

Вариант 1: Docker-in-Docker (DinD)

build:docker:
  image: docker:24
  stage: build
  services:
 - docker:24-dind
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
  script:
 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
 - docker build -t $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA ./backend
 - docker push $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA
  only:
 - main

DinD запускает Docker daemon внутри Docker-контейнера. Работает, но медленный (нет кэша слоёв между сборками).

Вариант 2: Kaniko (без Docker daemon)

build:kaniko:
  image:
    name: gcr.io/kaniko-project/executor:v1.21.0-debug
    entrypoint: [""]
  stage: build
  script:
 - /kaniko/executor
 --context ./backend
 --dockerfile ./backend/Dockerfile
 --destination $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA
 --cache=true
  only:
 - main

<ComparisonTable title="DinD vs Kaniko" headers={["", "Docker-in-Docker", "Kaniko"]} rows={[ ["Docker daemon", "Нужен (privileged)", "Не нужен"], ["Безопасность", "Требует privileged mode", "Работает без привилегий"], ["Кэш слоёв", "Нет (по умолчанию)", "Да (--cache=true)"], ["Скорость", "Медленнее", "Быстрее (с кэшем)"], ["Настройка", "Проще", "Сложнее первый раз"] ]} />

Встроенный registry для Docker-образов. Адрес: `registry.gitlab.com/username/project`. Авторизация через CI-переменные `$CI_REGISTRY_USER` и `$CI_REGISTRY_PASSWORD` - они автоматически доступны в пайплайне.

Переменные окружения в CI

variables:
  # Глобальные переменные (доступны во всех jobs)
  GOFLAGS: "-count=1"

backend:test:
  variables:
    # Переменные конкретного job
    DATABASE_URL: "postgres://postgres:postgres@postgres:5432/test?sslmode=disable"
  services:
 - postgres:16-alpine

Секреты (пароли, токены) - через Settings → CI/CD → Variables в GitLab. Они доступны как $VARIABLE_NAME, но не видны в логах (masked).

Артефакты

Артефакты - файлы, которые передаются между jobs или доступны для скачивания:

backend:test:
  script:
 - cd backend
 - go test -coverprofile=coverage.out ./...
 - go tool cover -html=coverage.out -o coverage.html
  artifacts:
    paths:
 - backend/coverage.html
    expire_in: 7 days

Полный production .gitlab-ci.yml

stages:
 - lint
 - test
 - build
 - deploy

backend:lint:
  image: golangci/golangci-lint:latest
  stage: lint
  script:
 - cd backend && golangci-lint run ./...
  only:
 - merge_requests

backend:test:
  image: golang:1.22-alpine
  stage: test
  script:
 - cd backend && go test ./...
  only:
 - merge_requests

build:
  image:
    name: gcr.io/kaniko-project/executor:v1.21.0-debug
    entrypoint: [""]
  stage: build
  script:
 - /kaniko/executor
 --context ./backend
 --dockerfile ./backend/Dockerfile
 --destination $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA
 --cache=true
  only:
 - main

deploy:
  stage: deploy
  script:
 - ssh deploy@$SERVER "cd /opt/app && docker compose pull && docker compose up -d"
  only:
 - main
  when: manual   # ручное подтверждение деплоя

Мини-задание

  • Создай .gitlab-ci.yml с двумя stages: test и build
  • Добавь package-lock.json в репозиторий
  • Настрой кэширование node_modules и Go-модулей
  • Добавь only: merge_requests для тестов
  • Проверь пайплайн в GitLab → CI/CD → Pipelines

Итог

  • CI = автоматизация: lint → test → build → deploy при каждом пуше/MR. Как выглядит весь командный процесс вокруг этого pipeline (защита веток, MR, релизы по тегам) - в мини-проекте трека Git.
  • .gitlab-ci.yml - stages (последовательно), jobs (параллельно внутри stage).
  • npm ci + package-lock.json - детерминированные зависимости в CI.
  • Кэширование: node_modules и Go-модули - экономия минут на каждом запуске.
  • Docker-образы: DinD (просто) vs Kaniko (безопасно + быстро с кэшем).
  • Секреты - через GitLab CI Variables, не в .gitlab-ci.yml.

Типичная ошибка

Забыть package-lock.json в git - npm ci упадёт. Lock-файл должен быть в репозитории и обновляться через npm install локально.

Другая ошибка - хранить секреты (пароли, токены, SSH-ключи) в .gitlab-ci.yml. Даже если репозиторий приватный - secrets должны быть в GitLab CI Variables (Settings → CI/CD → Variables, с флагом masked).

Мини-практика (10-15 минут)

Создай минимальный .gitlab-ci.yml для Go-проекта и запусти локально через gitlab-runner:

# Установи gitlab-runner (если есть Docker)
# Или проверь на GitLab - создай тестовый репозиторий

# Минимальный .gitlab-ci.yml:
cat > .gitlab-ci.yml <<'EOF'
stages:
 - test

go:test:
  image: golang:1.22-alpine
  stage: test
  script:
 - go version
 - go test ./... -v
EOF

# Пуш в GitLab → смотри Pipelines
git add .gitlab-ci.yml
git commit -m "ci: add basic pipeline"
git push

Открой GitLab → CI/CD → Pipelines - увидишь свой пайплайн.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.