DevOps, DevSecOps und Infrastructure-as-Code umsetzen GitHub Actions in der Praxis

Von Thomas Joos 6 min Lesedauer

GitHub Actions bietet umfassende Möglichkeiten zur Automatisierung von Workflows in DevOps- und DevSecOps-Umgebungen. Wir geben in diesem Beitrag einen tieferen Einblick in die Möglichkeiten und deren Umsetzung.

Mit GitHub Actions lassen sich von Deployments über Tests bis hin zur Bereitstellung von Infrastructure as Code verschiedene Workflows automatisieren.(Bild:  GitHub)
Mit GitHub Actions lassen sich von Deployments über Tests bis hin zur Bereitstellung von Infrastructure as Code verschiedene Workflows automatisieren.
(Bild: GitHub)

Im ersten Teil unseres Beitrags zu GitHub Actions haben wir die Möglichkeiten allgemein beleichtet. Im zweiten Teil gehen wir tiefgehender auf die Lösungen ein, und wie Entwickler GitHub Actions bei der Automatisierung hilfreich ist.

Einsatz von Dependency Caching zur Beschleunigung der GitHub Actions Workflows

Zur Optimierung der Workflow-Laufzeiten in GitHub Actions spielt das Caching eine entscheidende Rolle. Durch das Setzen des cache-Schlüssels in der Workflow-Konfiguration können Build-Abhängigkeiten wie Node.js node_modules oder Python pip Pakete zwischengespeichert werden.

Ein bewährter Ansatz besteht darin, actions/cache zu verwenden, um den Cache-Schlüssel dynamisch basierend auf der package-lock.json oder requirements.txt zu generieren. So ist sichergestellt, dass der Cache nur bei Änderungen der Abhängigkeiten aktualisiert wird. Dies reduziert die Installationszeit, indem vermieden wird, dass bereits geladene Abhängigkeiten erneut heruntergeladen werden müssen.

Es ist auch möglich, die Ausführung von Jobs innerhalb eines Workflows durch spezifische Bedingungen zu steuern, um Ressourcen zu sparen. Hier ein Beispiel, wie vermieden werden kann, dass Jobs ausgeführt werden, wenn nur Dokumentationsdateien im letzten Commit geändert wurden:

jobs:
  build:
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, 'skip ci')"
    steps:
    - uses: actions/checkout@v2
    - name: Build
    run: make build

In diesem Beispiel wird der build-Job nur dann ausgeführt, wenn der Commit-Message nicht „skip ci“ enthält. Dies kann sinnvoll sein, um Builds zu vermeiden, die nicht notwendig sind.

Sicherheitspraktiken bei der Verwendung von Secrets in GitHub Actions

Bei der Verwendung von GitHub Secrets ist es essenziell, den Zugriff strikt zu kontrollieren. Die Verwendung von Umgebungsvariablen, die in den GitHub Secrets definiert sind, sollte auf die absolut notwendigen Workflows beschränkt werden.

Es ist ratsam, Secrets ausschließlich in Schritten zu verwenden, in denen sie benötigt werden, und sie über Umgebungsvariablen an die entsprechenden Skripte zu übergeben. Wichtig ist auch, bei jeder Änderung der Secrets alle alten Secrets sofort zu rotieren und durch neue zu ersetzen, um die Sicherheit zu erhöhen.

Um GitHub Actions Workflows flexibler zu gestalten, kann zum Beispiel die Verwendung von Umgebungsvariablen zur dynamischen Konfiguration von Jobs vorteilhaft sein. Hier ist ein Beispiel, wie Umgebungsvariablen genutzt werden können, um unterschiedliche Verhaltensweisen basierend auf dem Kontext des Triggers zu definieren:

name: Deployment Workflow
on: [push, pull_request]
jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v2
      - name: Setup Node
      uses: actions/setup-node@v2
        with:
          node-version: '14'
      - run: npm install
      - run: npm run build
      - run: npm run deploy
        env:
          NODE_ENV: production

In diesem Workflow wird das Deployment dank des Auslösers nur ausgeführt, wenn ein push auf den main-Zweig erfolgt. Die Umgebungsvariable NODE_ENV wird auf production gesetzt, was es ermöglicht, spezifische Skripte oder Konfigurationen für die Produktionsumgebung zu aktivieren.

Die Integration automatisierter Sicherheitsprüfungen innerhalb von GitHub Actions, wie das Scannen von Abhängigkeiten mit actions/setup-node und dem automatischen Audit-Befehl von npm oder die Verwendung von GitHub CodeQL für die Analyse von Code auf Sicherheitslücken, ist essenziell. Diese Tools können so konfiguriert werden, dass sie automatisch Fixes vorschlagen oder Pull Requests für die Behebung von Schwachstellen erstellen.

GitHub Actions lässt sich so konfigurieren, dass automatisch Sicherheitsscans durchgeführt werden und bei der Entdeckung von Schwachstellen eine Information ausgegeben wird. Ein Beispiel für die Integration eines Sicherheitsscans könnte wie folgt aussehen:

jobs:
  security_scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run security scan
    uses: github/codeql-action/analyze@v1

In diesem Beispiel wird die github/codeql-action/analyze-Action verwendet, um den Code auf Sicherheitslücken und andere Qualitätsprobleme zu überprüfen. CodeQL ist ein Werkzeug, das von GitHub bereitgestellt wird, um statische Code-Analyse durchzuführen und ist nützlich, um tief verwurzelte Sicherheitsprobleme zu erkennen, die in der normalen Entwicklung möglicherweise übersehen werden.

Reduktion der Build-Zeiten durch Parallelisierung und bedingte Ausführungen

Eine effektive Methode zur Beschleunigung der GitHub Actions Workflows ist die Aufteilung langer Test-Suites in kleinere, parallel ausführbare Jobs. Die Verwendung der matrix-Funktion erlaubt es, mehrere Versionen einer Anwendung gleichzeitig zu testen. Hier ist ein Beispiel, wie eine dynamische Matrix in einen Workflows implementiert werden kann, um mehrere Node.js-Versionen und Betriebssysteme zu testen:

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu RZ- und Server-Technik

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung. Die Einwilligungserklärung bezieht sich u. a. auf die Zusendung von redaktionellen Newslettern per E-Mail und auf den Datenabgleich zu Marketingzwecken mit ausgewählten Werbepartnern (z. B. LinkedIn, Google, Meta).

Aufklappen für Details zu Ihrer Einwilligung
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
      node-version: [12, 14, 16]
    steps:
    - uses: actions/checkout@v2
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
    - name: Install dependencies
      run: npm install
    - name: Run tests
      run: npm test

In diesem Workflow-Konfigurationsbeispiel wird runs-on dynamisch gesetzt, basierend auf der matrix.os-Variable, node-version wird ebenfalls aus der Matrix gezogen. Dieser Ansatz ermöglicht es, Tests über mehrere Betriebssysteme und Node.js-Versionen hinweg zu automatisieren, was die Wahrscheinlichkeit verringert, dass plattformspezifische Bugs unbemerkt bleiben.

Zudem kann die if-Bedingung genutzt werden, um Jobs nur dann auszuführen, wenn bestimmte Bedingungen erfüllt sind, zum Beispiel Änderungen in spezifischen Verzeichnissen oder an bestimmten Dateitypen.

Durch den Einsatz sogenannter needs können Jobs so konfiguriert werden, dass sie in Abhängigkeit vom Erfolg vorheriger Jobs ausgeführt werden, was die Ausführungszeit weiter optimiert. Hier ist ein Beispiel eines Workflow-Ausschnitts, der mit einer Bedingung sicherstellt, dass Deployment-Jobs nur dann ausgeführt werden, wenn der Push auf den main-Branch erfolgt und nicht auf anderen Zweigen:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Build
      run: make build
    - name: Test
      run: make test
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
    - uses: actions/checkout@v2
    - name: Deploy
      run: ./deploy.sh

In diesem Beispiel ist der deploy-Job davon abhängig, dass der build-Job erfolgreich abgeschlossen wurde (needs: build) und wird nur auf dem Main Branch ausgeführt (if: github.ref == 'refs/heads/main'). Durch bedingte Workflows lassen sich nicht nur die CI/CD-Pipelines effizienter gestalten, sondern auch die Kontrolle und Flexibilität über den gesamten Build- und Deployment-Prozess erhöhen.

Implementierung von Compliance und Sicherheitsprüfungen durch Infrastructure as Code

Die Verwendung von Infrastructure as Code (IaC) zusammen mit GitHub Actions bietet die Möglichkeit, Sicherheits- und Compliance-Checks direkt in den Deployment-Prozess zu integrieren. Terraform- und Ansible-Scripts können genutzt werden, um Infrastrukturänderungen zu definieren und automatisiert auszurollen.

Durch die Verwendung des „terraform plan“ in einem Pull Request kann visuell überprüft werden, welche Änderungen vorgenommen werden, bevor sie tatsächlich angewendet werden. Zusätzlich kann der Einsatz von actions/labeler dabei helfen, Pull Requests basierend auf den geänderten Dateien automatisch mit Labels zu versehen, was die Compliance-Überwachung vereinfacht.

Um die Performance von GitHub Actions Workflows kontinuierlich zu verbessern, ist es sinnvoll, detaillierte Logs und Metriken der Workflow-Ausführungen zu analysieren. Die Implementierung von Custom-Logs innerhalb der Actions, welche spezifische Metriken wie Ausführungszeiten und Ressourcennutzung tracken, ermöglicht eine gezielte Analyse von Engpässen.

Optimierte Trigger-Strategien für effiziente GitHub Actions Workflows

Anstatt Workflows bei jedem Push auszuführen, können spezifische Pfade oder Dateitypen definiert werden, die einen Workflow auslösen. Dies wird durch die paths und paths-ignore Direktiven im Workflow YAML ermöglicht.

Eine solche gezielte Triggerung verhindert unnötige Builds und Tests. Durch den Einsatz von Umgebungsvariablen können Workflows dynamisch an die jeweilige Umgebung angepasst werden. Zum Beispiel kann die Anzahl der verwendeten Runner oder die Spezifikation der Serverressourcen basierend auf der Umgebung variieren.

Für komplexe Workflows kann die Steuerung von Job-Abhängigkeiten wesentlich sein, um die Ausführungsreihenfolge sicherzustellen und Ressourcen effektiv zu nutzen. Hier ist ein Beispiel, wie Abhängigkeiten zwischen Jobs definiert werden können, sodass bestimmte Jobs nur ausgeführt werden, wenn vorherige Jobs erfolgreich abgeschlossen wurden:

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      build-success: ${{ steps.build-step.outcome == 'success' }}
    steps:
      - uses: actions/checkout@v2
      - name: Build Step
        id: build-step
        run: make build
test:
    needs: build
    if: ${{ needs.build.outputs.build-success }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Test
        run: make test
  deploy:
    needs: test
    if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Deploy
        run: make deploy

In diesem Beispiel hängt der test-Job vom Erfolg des build-Jobs ab – und der deploy-Job wird wiederum nur ausgeführt, wenn der test-Job erfolgreich war und der Push auf den Hauptzweig erfolgt.

(ID:50028279)