Модуль: DevOps · Уровень: Middle+/Senior
TL;DR#
- GitHub Actions: единица — workflow (
.github/workflows/*.yml), внутри jobs, внутри steps. Шаги — это либо shell-команды (run), либо переиспользуемые actions (uses). Jobs по умолчанию параллельны, изолированы (разные раннеры), связываются черезneeds. - GitLab CI: единица —
.gitlab-ci.yml, внутри jobs, сгруппированные по stages. Stages выполняются последовательно, jobs внутри одного stage — параллельно. Артефакты передаются между stages черезartifacts/dependencies. - Кэширование обязательно для Go: кэшируем
$GOMODCACHE(~/go/pkg/mod) и build cache (~/.cache/go-build). В GH Actions —actions/cacheилиsetup-goсcache: true; в GitLab —cache:с ключом поgo.sum. - Матрицы (
strategy.matrix/parallel.matrix) гоняют один job по комбинациям (версии Go, ОС, arch). - Секреты: GH — Secrets (repo/org/environment) + OIDC для облаков без статичных ключей; GitLab — CI/CD variables (masked, protected, file). Никогда не печатать в лог.
- Концептуально оба про одно: декларативный pipeline-as-code, изолированные раннеры, кэш, артефакты, секреты, матрицы.
Теория#
GitHub Actions: модель#
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
# отмена устаревших запусков на тот же ref
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read # принцип наименьших привилегий для GITHUB_TOKEN
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true # кэширует mod cache + build cache по go.sum
- run: go vet ./...
- run: go test -race -coverprofile=cover.out ./...
- uses: actions/upload-artifact@v4
with:
name: coverage
path: cover.outon— триггеры (push, pull_request, schedule (cron), workflow_dispatch (ручной), tags).runs-on— тип раннера (GitHub-hosted или self-hosted).jobs.<id>.needs— зависимости между job (DAG).uses— переиспользуемый action (всегда пинить версию:@v4или, безопаснее, по SHA коммита).GITHUB_TOKEN— автогенерируемый токен; ограничивайте черезpermissions.
GitHub Actions: stages эмулируются через needs#
В GH нет явных stages. Последовательность задаётся графом needs:
jobs:
lint: { runs-on: ubuntu-latest, steps: [...] }
test: { runs-on: ubuntu-latest, steps: [...] }
build:
needs: [lint, test] # запустится только после успеха обоих
runs-on: ubuntu-latest
steps: [...]
deploy:
needs: build
if: github.ref == 'refs/heads/main' # только из main
environment: production # защита + approval
runs-on: ubuntu-latest
steps: [...]GitLab CI: модель#
# .gitlab-ci.yml
stages: [lint, test, build, deploy]
variables:
GOFLAGS: "-mod=readonly"
default:
image: golang:1.22
lint:
stage: lint
script:
- go vet ./...
- golangci-lint run ./...
test:
stage: test
script:
- go test -race -coverprofile=cover.out ./...
coverage: '/coverage: \d+\.\d+% of statements/'
artifacts:
paths: [cover.out]
expire_in: 1 week
build:
stage: build
script:
- go build -o app ./cmd/app
artifacts:
paths: [app]
deploy:
stage: deploy
script: ./deploy.sh
rules:
- if: '$CI_COMMIT_BRANCH == "main"' # только main
environment:
name: productionstages— порядок исполнения; job привязан к stage черезstage:.- jobs одного stage идут параллельно; следующий stage стартует после успеха всех job предыдущего.
rules(новый способ) /only/except(старый) — условия запуска job.artifacts— передаются вниз по stages автоматически (или выборочно черезdependencies/needs).needs:в GitLab позволяет строить DAG поверх stages (job стартует, как только готовы его зависимости, не дожидаясь всего stage).
Кэширование#
GitHub Actions — setup-go с cache: true достаточно в 90% случаев. Ручной вариант:
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-GitLab CI:
test:
cache:
key:
files: [go.sum] # инвалидация при изменении зависимостей
paths:
- .go/pkg/mod/
- .go-build/
variables:
GOPATH: "$CI_PROJECT_DIR/.go"
GOCACHE: "$CI_PROJECT_DIR/.go-build"Ключевая идея: ключ кэша зависит от go.sum. Изменился go.sum → новый ключ → пересборка кэша. restore-keys (GH) даёт частичное попадание по префиксу. Кэш — оптимизация, билд обязан работать и без него.
Матрицы#
GitHub Actions:
jobs:
test:
strategy:
fail-fast: false # не отменять остальные при падении одного
matrix:
go: ['1.21', '1.22']
os: [ubuntu-latest, macos-latest]
include: # добавить точечную комбинацию
- go: '1.22'
os: ubuntu-latest
coverage: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-go@v5
with: { go-version: '${{ matrix.go }}' }GitLab CI:
test:
stage: test
parallel:
matrix:
- GO_VERSION: ['1.21', '1.22']
image: golang:$GO_VERSION
script: go test ./...Секреты и безопасность#
GitHub: Secrets на уровне repo/org/environment, доступны как ${{ secrets.NAME }}. Они автоматически маскируются в логах. Для облаков — OIDC: workflow получает короткоживущий токен от облака (AWS/GCP) без хранения статичных ключей.
permissions:
id-token: write # нужно для OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/ci-deploy
aws-region: eu-central-1GitLab: CI/CD variables с флагами:
- masked — скрывается в логах;
- protected — доступна только в protected branches/tags (защита от утечки через форк/feature-ветку);
- file — значение кладётся во временный файл (для kubeconfig, JSON-ключей).
GitLab также поддерживает OIDC через id_tokens для облаков и Vault.
Типовой полный Go-пайплайн (GitHub Actions)#
name: CI/CD
on:
push: { branches: [main] }
pull_request:
permissions: { contents: read }
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.22', cache: true }
- run: gofmt -l . | tee /dev/stderr | (! read) # fail, если есть неформатированное
- run: go vet ./...
- uses: golangci/golangci-lint-action@v6
- run: go test -race -coverprofile=cover.out ./...
- run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
release:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write, id-token: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxПодводные камни / gotchas#
- Не пиннингованные actions (
uses: foo/bar@main) — supply chain риск: мейнтейнер или взлом может подменить код. Пинить по SHA коммита для критичных. - Избыточные
permissionsуGITHUB_TOKEN— по умолчанию могут быть широкими. Ставьтеcontents: readи расширяйте точечно. - Секреты недоступны в workflow из форка (
pull_requestот внешнего контрибьютора) — by design, иначе утечка. Для нужных кейсов естьpull_request_target, но он опасен (выполняет код базовой ветки с секретами) — легко открыть RCE. - GitLab: незащищённые переменные в feature-ветках — если variable не protected, её можно вытащить через push ветки с
echo. Секреты деплоя делайте protected. - Кэш не инвалидируется при смене go-версии, если ключ только по
go.sum— добавляйте версию в ключ. fail-fast: true(дефолт) в матрице отменяет все job при первом падении — теряете информацию, какие комбинации ещё сломаны. Для диагностикиfail-fast: false.- GitLab stages блокируют параллелизм: build ждёт ВСЕ test-job.
needs:(DAG) ускоряет, запуская job по готовности зависимостей. - Артефакты не передаются автоматически в GH Actions между jobs (разные раннеры) — нужен
upload/download-artifact. В GitLab внутри pipeline передаются по stages. gofmt -l .сам по себе не падает (exit 0 даже при наличии файлов) — нужен трюк, чтобы непустой вывод дал ненулевой код.
Вопросы на собеседовании#
В: Чем отличается модель выполнения GitHub Actions от GitLab CI?
О: В GitLab есть явные последовательные stages, jobs внутри stage параллельны, следующий stage ждёт весь предыдущий (ускоряется через needs/DAG). В GitHub Actions явных stages нет — порядок задаётся графом needs между jobs; jobs изолированы на разных раннерах, поэтому данные между ними передаются только через артефакты.
В: Как правильно кэшировать зависимости в Go-пайплайне?
О: Кэшируем module cache (~/go/pkg/mod) и build cache (~/.cache/go-build). Ключ кэша — хэш go.sum (+ ОС/версия Go), чтобы инвалидироваться при смене зависимостей. В GH проще всего setup-go с cache: true; в GitLab — cache.key.files: [go.sum]. Кэш должен только ускорять, билд обязан проходить и на холодную.
В: Что такое OIDC в CI и зачем он нужен? О: Вместо хранения долгоживущих ключей облака, CI получает короткоживущий токен через OpenID Connect: облако доверяет identity provider’у CI и выдаёт временные креды под конкретную роль. Это убирает статичные секреты (которые утекают и не ротируются), сужает доступ до конкретного repo/ветки/окружения.
В: Зачем ограничивать permissions у GITHUB_TOKEN?
О: По умолчанию токен может иметь широкие права на репозиторий. Скомпрометированный шаг (например, вредная transitive-зависимость в build) сможет ими воспользоваться (запушить, создать релиз). Принцип наименьших привилегий: contents: read и расширение точечно (packages: write, id-token: write) только там, где нужно.
В: Почему секреты недоступны в pull_request от форка и что такое pull_request_target?
О: Иначе любой внешний контрибьютор подсунул бы код, печатающий секреты в лог. Поэтому pull_request от форка идёт без секретов. pull_request_target запускает workflow в контексте базовой ветки (с секретами), но с кодом PR — это опасно, легко получить выполнение чужого кода с секретами; использовать крайне осторожно, не чекаутить и не запускать код PR.
В: Как сделать так, чтобы job деплоя запускался только из main?
О: GH: if: github.ref == 'refs/heads/main' + needs на CI + environment с required reviewers для approval. GitLab: rules: - if: '$CI_COMMIT_BRANCH == "main"' + environment. Дополнительно protected branches/variables, чтобы деплойные секреты были недоступны вне main.
В: Что такое матрица сборки и когда fail-fast: false?
О: Матрица гоняет один job по комбинациям параметров (версии Go, ОС, arch). fail-fast: false нужен, когда хочется увидеть все падающие комбинации сразу (диагностика совместимости), а не останавливать всё на первом фейле. Для быстрой обратной связи на PR дефолтный true экономит ресурсы.
В: Как передать собранный артефакт из одного job в другой в GitHub Actions?
О: Через actions/upload-artifact в продьюсере и actions/download-artifact в консьюмере — потому что jobs выполняются на разных изолированных раннерах и не делят файловую систему. В GitLab внутри одного pipeline артефакты передаются по stages автоматически (или выборочно через dependencies/needs).
На что копают на senior+#
- Supply chain security в CI: пиннинг actions по SHA, проверка provenance, минимальные permissions, изоляция untrusted PR.
- Reusable workflows (GH
workflow_call) / GitLabinclude+extends+ templates — DRY для десятков сервисов в организации. - Self-hosted runners: изоляция, эфемерность (один job — один раннер), безопасность (untrusted code на своих машинах), autoscaling.
- Оптимизация времени: разделение fast/slow job, path filters для монорепо, кэш Docker-слоёв (
type=gha, registry cache), параллелизм. - Стратегии деплоя из пайплайна: environments, approval-гейты, OIDC, интеграция с ArgoCD/Helm.
- Управление секретами на масштабе: external secret managers (Vault, cloud secret manager), ротация, отсутствие секретов в форках.