Roland Thomas Lichti

Aufsetzen des GitHub-Repositories

copyright (c) ayla87@rgbstock

In diesem Teil der Artikelserie befassen wir uns mit dem GitHub-Repository und natürlich dem Workflow für die GitHub Actions.

Außerdem verliere ich ein paar Worte über den Maven-Build und die Integration des Helmcharts in diesen Build.

Als erstes braucht man natürlich einen Account auf GitHub. Ich gehe aber mal davon aus, dass diese Hürde bereits genommen ist und in der Softwareindustrie eigentlich jeder schon über einen GitHub-Account verfügt. Aber selbst wenn dies nicht der Fall sein sollte, ist es ganz leicht, dazu braucht ihr meine Hilfe nicht.

Auch das Anlegen eines neuen Repositories ist ganz einfach.

Anlegen eines neuen Repositories. Auf das „+“ klicken und dann „New repository“ anwählen.

Danach öffnet sich eine neue Seite und man kann den gewünschten Namen eingaben. Hier kann man auch seine Lizenz aussuchen oder es auch sein lassen.

Der Software-Build mit Maven

Danach hat man sein Repository und kann die Software dorthin installieren. Ich benutze mein Repository unter https://github.com/Paladins-Inn/delphi-council als Beispiel und gehe damit von einem Maven-Build (gesteuert durch die Datei pom.xml im Hauptverzeichnis) aus. Es handelt sich um eine Spring Boot Anwendung. Außerdem nutze ich Maven, um ein paar Parameter im Helm-Chart zu ersetzen. Die Sourcen des Helm-Charts leben unterhalb von ./src/main/helm und werden durch die Maven-Konfiguration nach ./target/helm installiert:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    ...
    <build>
        ...
        <resources>
            ...
            <resource>
                <directory>src/main/helm</directory>
                <targetPath>../helm</targetPath>
                <filtering>true</filtering>
            </resource>
            ...
        </resources>
        ...
    </build>
    ...
</project>

Da es sich um ein Spring-Boot-Projekt handelt, muss man aufpassen. Normalerweise ersetzt Maven Variablen im Format „${VARIABLENNAME}“ – Spring-Boot Starter definieren es um und man muss die Notation „@VARIABLENNAME@“ nutzen. Der targetPath oben ist wichtig, da für das Maven Resource-Plugin der Standard-Ausgabepfad ./target/classes ist. Mit dem <targetPath>../helm</targetPath> wird daraus ./target/helm.

Ich nutze die Ersetzung vor allem, um die aktuelle Versionsnummer in die Chart.yaml zu bekommen. Das gleiche Spielchen mache ich übrigens auch mit dem Dockerfile, dass hier unter ./src/main/docker liegt:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    ...
    <build>
        ...
        <resources>
            ...
            <resource>
                <directory>src/main/docker</directory>
                <filtering>true</filtering>
            </resource>
            ...
        </resources>
        ...
    </build>
    ...
</project>

Hier akzeptiere ich jedoch, dass das Dockerfile nach ./target/classes kommt. Damit habe ich das Dockerfile auch im jar-Archiv und damit weiß jeder, wie die Software gebaut wurde. Das gefällt mir persönlich. Aber Ihr könnt gerne auch einen anderen targetPath konfigurieren. Bleibt aber unterhalb von ./target, damit mvn clean alles automatisch wegräumt.

Der Rest des Maven Buildfiles befasst sich mit dem Softwarebuild, der neben dem Javabuild auch einen npm-basierten Build für das Frontend umfasst. Das kommt aber so mit dem von mir verwendeten Framework Vaadin und eine nähere Besprechung würde endgültig den Rahmen dieser Artikelserie sprengen. Ihr könnt aber die Entwicklung der Software in meinen Live-Codings auf Twitch (und später auf Youtube) gerne verfolgen.

Auch die Software-Tests sollen hier während des Maven-Builds stattfinden, sodass wir uns beim Workflow nicht weiter darum kümmern müssen.

Der GitHub Workflow

Und zum Workflow kommen wir jetzt. Er versteckt sich in der Datei ./github/workflows/ci.yml. Natürlich kann man mehrere Workflows haben. Einfach eine weitere Datei daneben legen und es sind schon zwei Workflows. Aber wir werden uns die vorhande Datei mal von Oben nach unten anschauen.

## This is basic continuous integration build for your Quarkus application.

name: CI

on:
  push:
    branches: [ main ]

jobs:

Der Kopf der Datei. Hier definieren wir den Namen des Workflows, wie er uns später auch von GitHub angezeigt wird.

Außerdem definieren wir, wann der Workflow ausgeführt werden soll. Hier soll er bei jedem Push im Branch main ausgeführt werden. Damit ist dies eigentlich kein CI-Workflow mehr sondern eher ein Release-Workflow. Aber wenn man unter branches noch development eintragen würde, wäre es wieder ein CI-Workflow, ignorieren wir diese Information also erstmal. Denn jetzt kommen die Definitionen der jobs. Jobs sind die Schritte im Workflow, die jeweils einem Runner zugewiesen werden können. Dazu nutzt man den Parameter runs-on und definiert dann die einzelnen Schritte (steps), die auf diesem Runner ausgeführt werden sollen:

jobs:
  java-build:
    runs-on: [ java ]
    steps:
      ...

  container-build:
    runs-on: [ podman ]
    needs: java-build
    steps:
      ...

Dieser Workflow hat also zwei Schritte (java-build und container-build). Diese habe ich so gewählt, da ich einen runner für java (mit maven, gradle und wie oben beschrieben auch node.js) und einen Runner für buildah und podman (für Containerbuilds und Management) habe und diese so den verschiedenen Runnern zuweisen kann.

Und mittels des needs-Eintrags sorge ich dafür, dass cer container-build nicht parallel startet sondern erst nach dem java-build, da der Container natürlich die Software benötigt, die dort gebaut wird.

Der Java-Build-Teil des Workflows

Schauen wir uns den Java-Build an, zerfällt er wieder in drei Teile. Zuerst wird der Build vorbereitet, indem die Sourcen aus git ausgecheckt werden (mit der Aktion actions/checkout@v2) und die Java-Umgebung wird vorbereitet (Aktion actions/setup-java@v1). Die einzelnen Steps haben Namen und Ids. Welche Aktion genutzt wird, wird per uses definiert und mittels with werden die Aktionen mit Parametern versehen. So wähle ich per java-version hier Java11 aus.

jobs:
  java-build:
    runs-on: [ java ]
    steps:
      - name: Checkout sources
        id: checkout-sources
        uses: actions/checkout@v2

      - name: Set up JDK 11
        id: setup-java
        uses: actions/setup-java@v1
        with:
          java-version: 11

Nachdem die Umgebung vorbereitet ist, können wir jetzt den Java-Build starten. Hierzu rufen wir Maven auf:

      - name: Build
        id: build-java
        run: mvn package -B -Pproduction

Damit ist der Maven-Build gelaufen und unsere Artifakte liegen alle unterhalb des Verzeichnisses ./target. Ja, durch die oben beschriebenen Änderungen am pom.xml, nutzen wir drei Artifakte:

  1. Das jar-File mit der Anwendung ./target/delphi-council-is-@project.version@.jar (um in der Spring-Schreibweise zu bleiben)
  2. Das Dockerfile ./target/classes/Dockerfile
  3. Das Helmchart ./target/helm/delphi-council-is

Die Versionsnummer will ich nur an einer Stelle pflegen: im Maven-Buildfile unter project->version. Daher habe ich ja die Resourcen definiert, die diese Version überall hin ersetzen. Und die Version brauche ich mindestens für das Sichern der Artefakte auf GitHub. Daher extrahiere ich den APP_NAME und die APP_VERSION aus dem Dockerfile und merke mir die Variablen als IMAGE und VERSION in den $GITHUB_ENV (diese werden bei jedem step ins Environment geschrieben und machen diese als ${{ env.IMAGE }} und ${{ env.VERSION }} für die steps verfügbar. Ich gebe diese Variablen als Information aus und nutze sie zum Upload nach GitHub. Hierzu nutze ich die Aktion actions/upload-artifact@v2 mit der Liste der Dateien, die zu sichern sind. Damit wird ein Archiv mit dem konfigurierten Namen (dci) erstellt. Die Besonderheit ist, dass der gemeinsame Pfad der Dateien möglichst weit gekürzt wird. Im Archiv stehen also nicht die Dateien ./target/delphi-council-is-${{ env.VERSION }}.jar, ./target/classes/Dockerfile und ./target/helm/... – nein, im Archiv sind ./delphi-council-is-${{ env.VERSION }}.jar, ./classes/Dockerfile und ./helm/...; daran müssen wir uns erinnern, wenn wir im zweiten Teil des Flows auf diese Dateien zugreiffen wollen.

      - name: Set Image name and version
        run: |
          echo "IMAGE=$(cat target/classes/Dockerfile | grep APP_NAME= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV
          echo "VERSION=$(cat target/classes/Dockerfile | grep APP_VERSION= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV

      - name: Image name and version
        run: echo "Working on image '${{ env.IMAGE }}:${{ env.VERSION }}'."

      - name: Upload Artifact
        id: upload-jar
        uses: actions/upload-artifact@v2
        with:
          name: dci
          path: |
            target/delphi-council-is-${{ env.VERSION }}.jar
            target/classes/Dockerfile
            target/helm
          retention-days: 1

Damit ist der eigentliche Build der Software abgeschlossen und die Ergebnisse liegen im Archiv dci bei den GitHub Actions.

Hier findet man bei den Workflow-Ausführungen die erzeugte Datei. Da ich sie nur für 1 Tag aufbewahre, ist sie hier als expired markiert.

Der Container-Build-Teil des Workflows

Jetzt kommen wir zum 2. Teil des Workflows, dem Container-Build. Hier wird erstmal definiert, dass er einen Runner mit dem Tag podman nutzen soll (da ich nur dort die notwendige Software installiert habe. Außerdem wird definiert, dass dieser Schritt erst nach erfolgreichem java-build laufen darf. Das git-Repository wird wieder ausgecheckt und das Archiv dci aus dem Java-Build heruntergelanden sowie wieder die Umgebungsvariablen erzeugt (es ist ein anderer Runner, damit sind diese natürlich nicht verfügbar).

  container-build:
    runs-on: [ podman ]
    needs: java-build
    steps:
      - name: checkout sources
        id: checkout-sources
        uses: actions/checkout@v2

      - name: retrieve jar and dockerfile
        id: retrieve-jar
        uses: actions/download-artifact@v2
        with:
          name: dci

      - name: Set Image name and version
        run: |
          echo "IMAGE=$(cat classes/Dockerfile | grep APP_NAME= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV
          echo "VERSION=$(cat classes/Dockerfile | grep APP_VERSION= | head -n 1 | grep -o '".*"' | sed 's/"//g')" >> $GITHUB_ENV

Als erstes nutzen wir buildah über die Aktion redhat-actions/buildah-build@v2, um einen Dockerfile-basierten container-Build anzustoßen. ./classes/Dockerfile ist die Datei aus dem Archiv dci und der Build soll das aktuelle Verzeichnis als Basis nutzen. Außerdem braucht das Image einen Namen (image) und tags (ich tagge das Image mit drei tags): ${{ env.VERSION }} (die Version aus dem Maven-Buildfile), latest und die Git Commit-Id (${{ github.sha }}).

      - name: Buildah
        id: build-container
        uses: redhat-actions/buildah-build@v2
        with:
          image: ${{ env.IMAGE }}
          tags: ${{ env.VERSION }} latest ${{ github.sha }}
          dockerfiles: |
            ./classes/Dockerfile
          context: ./

Nach dem Build muss der erzeugte Container natürlich noch in die Container-Registry geschoben werden. Dies erledigt die Aktion redhat-actions/push-to-registry@v2, die dazu diverse Parameter braucht. Unter anderem auch Credentials für die Registry und natürlich das eigentliche Repository auf der Registry, in das das Image geschoben werden soll.

      - name: Push To quay
        id: push-to-quay
        uses: redhat-actions/push-to-registry@v2
        with:
          image: ${{ env.IMAGE }}
          tags: ${{ env.VERSION }} latest ${{ github.sha }}
          registry: ${{ secrets.QUAY_REPO }}
          username: ${{ secrets.QUAY_USER }}
          password: ${{ secrets.QUAY_TOKEN }}

Damit man nicht seinen persönlichen Account nutzen muss, legt man auf quay.io einen Robot-Account an. Dazu kommen wir im nächsten Teil. Ein solcher Account hat einen Benutzernamen und ein Token, dass hier in der Aktion als username bzw. password gesetzt werden müssen. Außerdem muss man den Pfad zum Repository angeben. Die Secrets werden im Repository definiert:

In einem normalen Repository würden QUAY_TOKEN und QUAY_USER auch unter „Repository secrets“ stehen. Da ich diese aber für eine Organisation angelegt habe, werden sie hier zwar gelistet aber bei der Organisation gepflegt. Für den Workflow macht dies keinen Unterschied, Ihr könnt sie auch direkt im Repository anlegen. Ich habe mehrere Projekte und will nur einen Quay-Account pflegen und habe daher den Weg über die Organisation gewählt.

In das QUAY_REPO müsst Ihr den kompletten Pfad angeben, bei mir ist das quay.io/klenkes74. Der QUAY_USER beinhalten den Benutzernamen für das quay-Repository, der QUAY_TOKEN das generierte Token für den Benutzer. Aber um quay.io kümmern wir uns im nächsten Teil der Artikelserie.

Sichere und unsichere Github Actions

GitHub weißt darauf hin, dass beim Einsatz eigener Runner natürlich die Sicherheit nicht vernachlässigt werden darf. Immerhin können Fremde einen Pullrequest stellen und wenn man da nicht aufpasst, kann natürlich auch der Workflow verändert werden. Daher bietet GitHub eine Konfiguration an, welche Runner erlaubt sind und welche nicht. Hier müsst Ihr auswählen, wem Ihr vertraut. Ich vertraue allen GitHub-Actions und denen von verifizierten Erstellern:

Sicherheitseinstellungen für Actions auf github.com

Damit sind wir am Ende der Github-Repository-Konfiguration angekommen. Als nächstes schauen wir uns an, wie wir an ein Quay-Repository kommen und dort einen eigenen Robot-Account für Github Actions anlegen.

Die mobile Version verlassen