Loading...
Alle Artikel
CI/CD · 8 min read

Fortgeschrittene GitHub Actions: Reusable Workflows, Matrix Builds und Self-Hosted Runner

Gehen Sie über Basis-CI/CD hinaus mit fortgeschrittenen GitHub-Actions-Patterns: Reusable Workflows, dynamische Matrix-Strategien, Self-Hosted Runner auf Kubernetes und Techniken zur Kostenoptimierung von Enterprise-Pipelines.

Jenseits der Basis von GitHub Actions

Die meisten Teams starten mit GitHub Actions, indem sie einen Workflow aus einem Blogpost kopieren — build, test, deploy. Das funktioniert, und für ein einzelnes Repository ist das auch völlig in Ordnung. Doch sobald Ihre Organisation auf Dutzende oder Hunderte von Repos skaliert, stoßen Sie auf reale Probleme:

  • Duplizierte Workflow-Dateien über jedes Repository hinweg
  • Inkonsistente CI/CD-Praktiken — jedes Team erfindet eigene Patterns
  • Langsame Builds, weil jeder Job sequenziell auf den Default-Runnern läuft
  • Steigende Kosten durch GitHub-Hosted-Runner-Minuten
  • Sicherheitslücken, weil Secrets-Management ad hoc erfolgt

Dieser Leitfaden behandelt fortgeschrittene Patterns, die diese Probleme skalierbar lösen.

Schritt 1: Reusable Workflows aufbauen

Reusable Workflows sind GitHub Actions' Antwort auf DRY (Don't Repeat Yourself). Sie definieren einen Workflow einmal in einem zentralen Repository und rufen ihn aus jedem anderen Repo auf.

Erstellen Sie einen geteilten Workflow in Ihrem .github-Organisations-Repository:

# .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

Jedes Repository in Ihrer Organisation ruft diesen Workflow nun mit minimaler Konfiguration auf:

# 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

Der aufrufende Workflow ist klar, lesbar und konsistent über jeden Service in Ihrer Organisation hinweg.

Schritt 2: Dynamische Matrix-Strategien

Matrix-Builds ermöglichen es Ihnen, parallel über mehrere Konfigurationen hinweg zu testen. Die wahre Stärke entfaltet sich mit dynamischen Matrizen, die zur Laufzeit generiert werden.

Beispiel für eine statische 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

Dynamische Matrix aus geänderten Dateien

Dieses Pattern führt Tests nur für geänderte Services aus — entscheidend in Monorepos:

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

In einem Monorepo mit 20 Services bedeutet das: Eine Änderung am payment-service testet nur diesen Service, nicht alle 20. Die Build-Zeiten fallen von 30 Minuten auf 3.

Schritt 3: Self-Hosted Runner auf Kubernetes

GitHub-Hosted Runner sind bequem, aber in der Skalierung teuer und in der Anpassbarkeit eingeschränkt. Self-Hosted Runner auf Kubernetes geben Ihnen Kontrolle über Kosten, Performance und Sicherheit.

Der Actions Runner Controller (ARC) ist die offizielle Möglichkeit, Self-Hosted Runner auf Kubernetes zu betreiben:

# 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

Für feingranulare Kontrolle definieren Sie Runner Scale Sets mit 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"

Kostenoptimierung mit Spot Instances

Betreiben Sie Ihre CI-Runner auf Spot-/Preemptible-Instances und sparen Sie 60–90 % der Kosten:

# 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-Workloads sind ideal für Spot Instances, weil sie kurzlebig, zustandslos sind und Unterbrechungen tolerieren können (der Job wird einfach wiederholt).

Schritt 4: Composite Actions für gemeinsame Steps

Während Reusable Workflows ganze Pipelines teilen, teilen Composite Actions einzelne Steps. Verwenden Sie sie für gängige Patterns wie Setup, Caching und Notifications:

# .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

In jedem Workflow einsetzbar:

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

Schritt 5: Security-Hardening

Fortgeschrittene GitHub-Actions-Sicherheit geht über einfaches Secret-Management hinaus:

Actions an SHA-Hashes pinnen

# Bad: vulnerable to tag hijacking
- uses: actions/checkout@v4

# Good: pinned to exact commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Nutzen Sie Dependabot, um gepinnte SHAs aktuell zu halten:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns:
          - "*"

Workflow-Berechtigungen einschränken

Wenden Sie das Prinzip der minimalen Rechte an:

# 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-Authentifizierung (keine langlebigen Secrets mehr)

Ersetzen Sie statische Cloud-Credentials durch OIDC-Föderation:

# 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

Das beseitigt die Notwendigkeit, AWS-Access-Keys als GitHub Secrets zu speichern, vollständig.

Performance-Tipps

  • Caching aggressiv nutzenactions/cache für Abhängigkeiten, Docker-Layer-Caching für Builds
  • Unabhängige Jobs parallel laufen lassen — Verketten Sie keine Jobs, die keine echte Abhängigkeit haben
  • paths- und paths-ignore-Trigger nutzen — Überspringen Sie CI bei reinen Dokumentationsänderungen
  • Concurrency-Gruppen setzen — Laufende Runs abbrechen, wenn neue Commits gepusht werden
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  push:
    branches: [main]
    paths-ignore:
      - "docs/**"
      - "*.md"
      - ".github/ISSUE_TEMPLATE/**"

Fazit

GitHub Actions ist weitaus mächtiger, als die meisten Teams denken. Durch Investitionen in Reusable Workflows, dynamische Matrizen, Self-Hosted Runner und solides Security-Hardening verwandeln Sie Ad-hoc-CI/CD in eine skalierbare, kosteneffiziente Plattform, die Ihrer gesamten Engineering-Organisation dient.

Bei DevOpsVibe helfen wir Teams, GitHub-Actions-Pipelines zu designen und umzusetzen, die vom Start-up bis zum Enterprise skalieren. Von der Migration weg von Jenkins über den Aufbau von Reusable-Workflow-Bibliotheken bis zum Deployment von Self-Hosted Runnern auf Kubernetes bringen wir praxiserprobte Patterns mit, die Ihre Entwicklungsgeschwindigkeit beschleunigen. Sprechen Sie mit unseren CI/CD-Experten.

abgelegt unter
github-actionscicdautomationkuberneteskubernetesdevopspipelines
mit uns arbeiten

Soll unser Team Ihrer Infrastruktur helfen?

talk to an engineerFree 30-min discovery callBook
close