GitHub Reusable Workflows: Effizienz durch standardisierte CI/CD-Pipelines

Posted on | 1145 words | ~6 mins

Über unsere Erfahrungen mit GitHub haben wir bereits in unserem Tech Blog berichtet. GitHub haben wir zur Verwaltung unseres Quellcodes und zur Implementation unserer CI/CD-Pipelines eingeführt. Aktuell läuft die Migration unserer bisherigen Lösungen in das neue System und wird im Laufe des Jahres abgeschlossen sein. Zuvor haben wir unseren Quellcode in Atlassian Bitbucket verwaltet und unsere CI-Pipelines haben wir mittels Jenkins automatisiert. Jenkins ist insbesondere im Java-Umfeld vielen ein Begriff, denn es war jahrelang “die” Open-Source-Lösung für automatisierte Builds, Tests und Deployments.

Als Dienstleister für unsere EntwicklerInnen sehen wir uns als Cloud-Plattform-Team vor der Herausforderung, die Nutzung unserer GitHub-Services so einfach und schnell nutzbar wie möglich zu gestalten. Ein relevantes Thema für uns ist dabei die zentrale Bereitstellung einheitlicher Build- und Deployment-Workflows für all unsere Entwicklungsteams und deren Repositories sowie die Versorgung mit regelmäßigen Updates.

In diesem Zuge haben wir das Thema Reusable Workflows bereits in unserem letzten CI-/CD-Post erwähnt und wollen diesen Ansatz im Folgenden näher ausführen.

Was sind Reusable Workflows?

Ein GitHub Workflow ist ein automatisierter Prozess, der über eine YAML-Datei definiert wird. Dieser Workflow führt einen oder mehrere Jobs aus – beispielsweise zum Installieren der Anwendungs-Abhängigkeiten, zum Compilieren von Quellcode, zum Erzeugen von Container-Images oder zum Deployment. Gleichartige Anwendungen folgen meist einem gleichartigen Workflow-Schema. So werden Java-Anwendungen innerhalb einer Organisation vermutlich dieselben Build- und Qualitätssicherungs-Verfahren durchlaufen.

Hier kommen Reusable Workflows in Spiel: Ein Reusable Workflow ist ein Workflow, der von einem anderen Workflow aufgerufen wird. Hierbei werden die sogenannten GitHub Action Templates genutzt: Die Logik des Reusable Workflows wird in einem zentralen Repository implementiert und kann dann von anderen Repositories verwendet werden. Die notwendige Flexibilität bei der Nutzung wird dadurch ermöglicht, dass Parameter vom aufrufenden Workflow an den Reusable Workflow übergeben werden können. Diese Parameter können dann im Reusable Workflow ausgewertet bzw. genutzt werden.

Entsprechend des DRY-Prinzips (Don’t Repeat Yourself) kann auf diese Weise eine Logik einmalig konzipiert und zentral implementiert werden. Anpassungen und Optimierungen als auch Wieder- und Weiterverwendung können in der Folge umfassend und mit geringem Aufwand umgesetzt werden.

Im Folgenden wird ein Beispiel aus Sicht eines einzelnen Anwendungs-Repositories dargestellt.

Vorher (nicht reusable)

Der nachfolgende Workflow führt verschiedene Tests auf einem NodeJS-Repository aus und veröffentlicht die Testergebnisse anschließend. Danach wird ein OpenID Connect Login auf Microsoft Azure durchgeführt, um darauffolgend eine Azure Function aufrufen zu können.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js
      uses: actions/setup-node@v3
      with:
        node-version: 16.x
    - run: npm ci
      env:
        NODE_AUTH_TOKEN: ${{ secrets.MY_REPO_SECRET }}
    - name: run lint
      run: |
        npm run lint        
    - name: run prettier
      run: |
        npm run prettier:check        
    - name: run tests
      run: |
        npm test        
    - name: Publish Unit Test Results
      uses: EnricoMi/publish-unit-test-result-action@v1.38
      if: github.actor != 'dependabot[bot]' && always()
      with:
        files: ${{ inputs.unit-test-files }}
    - name: OIDC Login to Azure
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENTID }}
        tenant-id: ${{ secrets.AZURE_TENANTID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }} 
        enable-AzPSSession: true
        allow-no-subscriptions: true
    - name: Run Azure Functions Action
      uses: Azure/functions-action@v1.4.6
      id: fa
      with:
        app-name: my-awesome-function
        package: "."

Nachher (reusable)

Nun haben wir den Workflow umgeschrieben, sodass er einen Reusable Workflow verwendet.

Die Logik des Reusable Workflows befindet sich in einem zentralen Repository namens central-workflows. In dem eigentlichen Anwendungs-Repository, in dem sich der Anwendungscode befindet, rufen wir jetzt den Workflow auf. Auf den ersten Blick ist erkennbar, dass der Workflow deutlich geschrumpft ist, was weniger Codierung, Duplizierung und Aufwand für die Maintenance der einzelnen Anwendungs-Repositories bedeutet.

Wir nutzen in unserem Beispiel einen Parameter zur Angabe der zu verwendenden NodeJS-Version, die vom aufrufenden Workflow übergeben wird (Variable node-version). So kann die Node-Version pro Repository definiert/getestet werden. Auch vertrauliche Werte, wie z.B. ein API-Token, können an einen Reusable Workflow übergeben werden.

jobs:
  build:
  uses: dvag/central-workflows/.github/workflows/build_and_deploy_azure_function_node.yml@1.0.0
  with:
    node-version: 16.x
  secrets:
    NODE_AUTH_TOKEN: ${{ secrets.MY_REPO_SECRET }}
    AZURE_CLIENTID: ${{ secrets.AZURE_CLIENTID }}
    AZURE_TENANTID: ${{ secrets.AZURE_TENANTID }}
    AZURE_SUBSCRIPTIONID: ${{ secrets.AZURE_SUBSCRIPTIONID }} 

Erst-Installation der Workflows

Unser Vorgehen zum Setup neuer bzw. nach GitHub migrierter Projekte stellt sich wie folgt dar:

Sobald der Quellcode eines Projekts nach GitHub migriert wurde, legen wir im jeweiligen GitHub-Repository die sinnvollen und notwendigen Workflows an. Hierbei bedienen wir uns einer Palette an Reusable Workflows, die wir im Laufe der Zeit implementiert haben. Dadurch können wir diese schnell und einfach den Workflows eines migrierten Projekts hinzufügen.

Unsere Reusable Worklows orientieren sich am konkreten Bedarf der nutzenden Projekte. So ergeben sich bspw. aus dem Austausch zwischen unserem Cloud-Plattform-Team und den Anwendungsentwicklungs-Teams regelmäßig neue Erkenntnisse zu implementierten Workflows, die wir dann in Reusable Workflows überführen. Unser Repertoire umfasst unter anderem:

  • Build & Deployment von Azure Functions in .NET, Java, NodeJS, PowerShell und Python
  • Deployment fertiger Docker Images in ein Kubernetes Cluster
  • Build & Testing von Java-Anwendungen mittels Maven sowie von NodeJS-Anwendungen mittels NPM

Für das Setup der Workflows benötigen wir von den jeweiligen Projekten bestimmte Eckdaten, insbesondere den Default Branch des Repositories und den Kubernetes Namespaces. Dementsprechend werden die Workflows generiert und im entsprechenden Repository wird ein Pull Request erzeugt. Wenn der Pull Request gemerged wird, ist das Projekt fertig eingerichtet und die die Workflows können gestartet. Damit kann das jeweilige Projekt schnell und einfach gebaut, getestet und deployed werden.

Wartung der Workflows

Wir haben ein zentrales Repository für die Reusable Workflows definiert. In diesem Repository arbeiten wir mit GitHub Releases, um Transparenz für die Verwender der Workflows gewährleisten zu können. In regelmäßigen Abständen werden die Reusable Workflows zentral vom Cloud-Plattform-Team aktualisiert. Da derzeit das Updaten von Reusable Workflows mit direkten GitHub Mitteln (noch) nicht möglich ist, haben wir hierfür einen Update-Workflow erstellt. Dieser Workflow wird in jedem Repository ausgeführt, in dem Reusable Workflows verwendet werden. Damit kann automatisiert geprüft werden, ob eine neue Version der verwendeten Reusable Workflows zur Verfügung steht. Wenn dies der Fall ist, werden die Änderungen mit einem Pull-Request für das Repository zur Verfügung gestellt. Künftig wäre es aus unserer Sicht wünschenswert, wenn GitHub mittels des vorhandenen Dependabots automatisiert auf neue Versionen der verwendeten Reusable Workflows hinweisen und Pull Requests erzeugen könnte.

Fazit

Mit Reusable Workflows können wir Projekte mit sehr geringem Aufwand nach GitHub migrieren und das Bauen, Deployen und Testen ermöglichen. Durch die zentrale Pflege der Reusable Workflows können wir unsere zentralen Standards definieren und unseren Entwicklerteams diese automatisiert zur Verfügung stellen.

Wir haben mehrere Checks definiert, die unser Quality Gate bilden. Dazu gehören Secrets Checks, mittels derer wir auf Secret Scanning Alerts prüfen und Vulnerability Checks, mit denen wir die Abwesenheit von Dependency Vulnerabilities sicherstellen. Wenn wir den Umfang des Quality Gates erweitern, implementieren wir diese einmal in den Reusable Workflows. Die neuen Versionen der Workflows werden per Pull Request in jedes nutzende Repository übertragen.

Die Arbeit unserer Entwicklerteams beschränkt sich hinsichtlich der Aktualisierung ihrer Workflows auf das Annehmen der entsprechenden Pull Requests. Somit können sie sich auf den Anwendungscode fokussieren. Die EntwicklerInnen haben außerdem die Möglichkeit, zum zentralen und gemeinschaftlichen Repository der Reusable Workflows beizutragen und damit eigene Ideen und Verbesserungsvorschläge einzubringen.

Welche Erfahrung habt Ihr mit Reusable Workflows gesammelt und für welche Anwendungsfälle nutzt Ihr sie? Happy to share!