GitLab CI: build/test для Go и React + Docker images
GitLab CI: build/test для Go и React + Docker images
CI (Continuous Integration) - автоматизация рутины: тесты, линтинг, сборка образов, деплой. GitLab CI запускает пайплайн при каждом пуше - робот делает скучное за тебя.
Как работает GitLab CI
Пайплайн = набор stages (этапов), выполняемых последовательно. Внутри stage - jobs (задачи), которые могут выполняться параллельно.
.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
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"
Кэширование зависимостей
Без кэша каждый 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)"], ["Скорость", "Медленнее", "Быстрее (с кэшем)"], ["Настройка", "Проще", "Сложнее первый раз"] ]} />
Переменные окружения в 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 - увидишь свой пайплайн.