Loading...
Усі статті
CI/CD · 8 min read

Просунутий GitHub Actions: повторно використовувані workflows, matrix-збірки та self-hosted runners

Вийдіть за межі базового CI/CD з просунутими патернами GitHub Actions: повторно використовувані workflows, динамічні matrix-стратегії, self-hosted runners на Kubernetes та техніки оптимізації витрат для enterprise-конвеєрів.

За межами базового 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 експертами.

у категорії
github-actionscicdautomationkuberneteskubernetesdevopspipelines
працювати з нами

Хочете, щоб наша команда допомогла з вашою інфраструктурою?

talk to an engineerFree 30-min discovery callBook
close