За межами базового GitHub Actions
Більшість команд починають з GitHub Actions, копіюючи workflow з якогось блогу — build, test, deploy. Це працює, і це нормально для одного репозиторію. Але коли ваша організація масштабується до десятків чи сотень репозиторіїв, ви стикаєтеся з реальними проблемами:
- Дубльовані workflow-файли в кожному репозиторії
- Непослідовні CI/CD-практики — кожна команда винаходить свої патерни
- Повільні збірки, бо кожен job виконується послідовно на runners за замовчуванням
- Зростаючі витрати на хвилини GitHub-hosted runner
- Прогалини в безпеці, бо управління секретами ad hoc
Цей посібник охоплює просунуті патерни, які вирішують ці проблеми у масштабі.
Крок 1: Створіть повторно використовувані workflows
Reusable workflows — це відповідь GitHub Actions на DRY (Don't Repeat Yourself). Ви визначаєте workflow одноразово в центральному репозиторії та викликаєте його з будь-якого іншого репо.
Створіть спільний workflow у вашому організаційному репозиторії .github:
# .github/workflows/docker-build-push.yml
# Organization: myorg/.github repository
name: Docker Build and Push
on:
workflow_call:
inputs:
image_name:
required: true
type: string
description: "Docker image name (e.g., myorg/payment-service)"
dockerfile:
required: false
type: string
default: "Dockerfile"
context:
required: false
type: string
default: "."
platforms:
required: false
type: string
default: "linux/amd64"
push:
required: false
type: boolean
default: true
secrets:
REGISTRY_USERNAME:
required: true
REGISTRY_PASSWORD:
required: true
outputs:
image_digest:
description: "The image digest"
value: ${{ jobs.build.outputs.digest }}
image_tag:
description: "The image tag"
value: ${{ jobs.build.outputs.tag }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build-push.outputs.digest }}
tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.image_name }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and Push
id: build-push
uses: docker/build-push-action@v5
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platforms }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ inputs.image_name }}@${{ steps.build-push.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
Тепер будь-який репозиторій у вашій організації викликає це з мінімальною конфігурацією:
# payment-service/.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go test ./...
build:
needs: test
uses: myorg/.github/.github/workflows/docker-build-push.yml@main
with:
image_name: myorg/payment-service
platforms: "linux/amd64,linux/arm64"
secrets:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
deploy:
needs: build
if: github.ref == 'refs/heads/main'
uses: myorg/.github/.github/workflows/deploy-k8s.yml@main
with:
service: payment-service
image_tag: ${{ needs.build.outputs.image_tag }}
environment: production
secrets: inherit
Викликаючий workflow чистий, читабельний і узгоджений у кожному сервісі вашої організації.
Крок 2: Динамічні matrix-стратегії
Matrix-збірки дозволяють тестувати у кількох конфігураціях паралельно. Справжня сила приходить від динамічних матриць, генерованих під час виконання.
Приклад статичної матриці
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [18, 20, 22]
exclude:
- os: macos-latest
node-version: 18
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Динамічна матриця зі змінених файлів
Цей патерн запускає тести лише для сервісів, що змінилися — критично у monorepo:
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.changes.outputs.matrix }}
has_changes: ${{ steps.changes.outputs.has_changes }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed services
id: changes
run: |
# Find which service directories have changes
SERVICES=""
for dir in services/*/; do
service=$(basename "$dir")
if git diff --name-only origin/main...HEAD | grep -q "^services/${service}/"; then
SERVICES="${SERVICES}\"${service}\","
fi
done
if [ -n "$SERVICES" ]; then
# Remove trailing comma and build matrix JSON
SERVICES="${SERVICES%,}"
echo "matrix={\"service\":[${SERVICES}]}" >> "$GITHUB_OUTPUT"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
test:
needs: detect-changes
if: needs.detect-changes.outputs.has_changes == 'true'
strategy:
matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/${{ matrix.service }}
steps:
- uses: actions/checkout@v4
- run: make test
- run: make lint
У monorepo з 20 сервісами це означає, що зміна в payment-service тестує лише цей сервіс, а не всі 20. Час збірки падає з 30 хвилин до 3.
Крок 3: Self-hosted runners на Kubernetes
GitHub-hosted runners зручні, але дорогі у масштабі та обмежені у налаштуванні. Self-hosted runners на Kubernetes дають вам контроль над витратами, продуктивністю та безпекою.
Actions Runner Controller (ARC) — це офіційний спосіб запускати self-hosted runners на Kubernetes:
# Install ARC using Helm
helm install arc \
--namespace arc-systems \
--create-namespace \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
# Create a GitHub App for authentication (more secure than PAT)
# Then configure the runner scale set
helm install arc-runner-set \
--namespace arc-runners \
--create-namespace \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
--set githubConfigUrl="https://github.com/myorg" \
--set githubConfigSecret.github_app_id="12345" \
--set githubConfigSecret.github_app_installation_id="67890" \
--set githubConfigSecret.github_app_private_key="$(cat private-key.pem)" \
--set maxRunners=20 \
--set minRunners=2
Для тонкого контролю визначте runner scale sets з custom resources:
# runner-scale-set-values.yaml
githubConfigUrl: "https://github.com/myorg"
maxRunners: 20
minRunners: 2
containerMode:
type: "kubernetes"
kubernetesModeWorkVolumeClaim:
accessModes: ["ReadWriteOnce"]
storageClassName: "gp3"
resources:
requests:
storage: 10Gi
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
resources:
limits:
cpu: "4"
memory: 8Gi
requests:
cpu: "2"
memory: 4Gi
nodeSelector:
workload-type: ci-runners
tolerations:
- key: "ci-runners"
operator: "Equal"
value: "true"
effect: "NoSchedule"
Оптимізація витрат за допомогою Spot-інстансів
Запускайте свої CI runners на spot/preemptible-інстансах для економії 60-90% витрат:
# eks-nodegroup-spot.yaml (for AWS EKS)
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: ci-cluster
region: us-east-1
managedNodeGroups:
- name: ci-runners-spot
instanceTypes:
- m5.xlarge
- m5a.xlarge
- m6i.xlarge
capacityType: SPOT
minSize: 0
maxSize: 30
desiredCapacity: 2
labels:
workload-type: ci-runners
taints:
- key: ci-runners
value: "true"
effect: NoSchedule
CI-навантаження ідеальні для spot-інстансів, бо вони короткоживучі, stateless і можуть переносити переривання (job просто перезапускається).
Крок 4: Composite actions для спільних кроків
Якщо reusable workflows ділять цілі конвеєри, composite actions ділять окремі кроки. Використовуйте їх для поширених патернів, як-от setup, кешування та сповіщення:
# .github/actions/setup-go-project/action.yml
name: "Setup Go Project"
description: "Checks out code, sets up Go, restores cache, and installs dependencies"
inputs:
go-version:
description: "Go version to install"
required: false
default: "1.22"
runs:
using: "composite"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ inputs.go-version }}
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ inputs.go-version }}-
- name: Download dependencies
shell: bash
run: go mod download
- name: Install tools
shell: bash
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install gotest.tools/gotestsum@latest
Використовуйте його в будь-якому workflow:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: myorg/.github/.github/actions/setup-go-project@main
with:
go-version: "1.22"
- run: gotestsum --format pkgname ./...
- run: golangci-lint run
Крок 5: Зміцнення безпеки
Просунута безпека GitHub Actions виходить за межі базового управління секретами:
Закріплюйте actions до SHA-хешів
# Bad: vulnerable to tag hijacking
- uses: actions/checkout@v4
# Good: pinned to exact commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Використовуйте Dependabot, щоб тримати закріплені SHA актуальними:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
Обмежуйте дозволи workflow
Застосовуйте принцип найменших привілеїв:
# At the workflow level
permissions:
contents: read
packages: write
# Or at the job level for more granularity
jobs:
deploy:
permissions:
contents: read
id-token: write # For OIDC authentication
steps:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: us-east-1
OIDC-автентифікація (більше жодних довгоживучих секретів)
Замініть статичні хмарні облікові дані на OIDC-федерацію:
# AWS: Create an IAM OIDC provider for GitHub Actions
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
Це повністю усуває потребу зберігати ключі доступу AWS як GitHub-секрети.
Поради щодо продуктивності
- Використовуйте кешування агресивно —
actions/cacheдля залежностей, Docker layer caching для збірок - Запускайте незалежні jobs паралельно — не ланцюгуйте jobs, що не мають реальних залежностей
- Використовуйте тригери
pathsтаpaths-ignore— пропускайте CI для змін, що стосуються лише документації - Налаштовуйте concurrency-групи — скасовуйте поточні запуски, коли пушаться нові коміти
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- "*.md"
- ".github/ISSUE_TEMPLATE/**"
Висновок
GitHub Actions значно потужніший, ніж усвідомлює більшість команд. Інвестуючи в reusable workflows, динамічні матриці, self-hosted runners та належне зміцнення безпеки, ви перетворюєте ad hoc CI/CD на масштабовану, економічно ефективну платформу, що обслуговує всю вашу інженерну організацію.
У DevOpsVibe ми допомагаємо командам проєктувати та впроваджувати GitHub Actions конвеєри, що масштабуються від стартапу до enterprise. Від міграції з Jenkins до побудови бібліотек reusable workflows і розгортання self-hosted runners на Kubernetes — ми приносимо перевірені в боях патерни, що прискорюють вашу швидкість розробки. Поговоріть з нашими CI/CD експертами.