Модуль: 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.out
  • on — триггеры (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: production
  • stages — порядок исполнения; job привязан к stage через stage:.
  • jobs одного stage идут параллельно; следующий stage стартует после успеха всех job предыдущего.
  • rules (новый способ) / only/except (старый) — условия запуска job.
  • artifacts — передаются вниз по stages автоматически (или выборочно через dependencies/needs).
  • needs: в GitLab позволяет строить DAG поверх stages (job стартует, как только готовы его зависимости, не дожидаясь всего stage).

Кэширование#

GitHub Actionssetup-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-1

GitLab: 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) / GitLab include + 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), ротация, отсутствие секретов в форках.