Januar 12, 2026

CI/CD mit GitHub Actions und GCP

In diesem Beitrag wird gezeigt, wie man einen vollständigen CI/CD-Workflow für eine Python-Anwendung mithilfe von GitHub und GCP erstellt. Eine einfache Anwendung soll mit Hilfe von GitHub Actions beim Push ins Repository über Cloud Build direkt auf Cloud Run deployed werden. Grundlagen zur Nutzung von Git werden in diesem Beitrag erklärt.

Der Code zu diesem Beitrag findet sich hier.

Google Cloud einrichten

Erstelle in der Google Cloud Console ein neues Projekt.

APIs aktivieren

Die benötigten APIs lassen unter „APIs & Dienste“ → „Bibliothek“ aktivieren. Wir benötigen Cloud Run und Cloud Build, wobei die API für beides bereits aktiviert sein sollte. Außerdem benötigen wir Artifacts Registry. Suche danach und aktiviere die API.

gcloud

Lokal benötigen wir gcloud, welches wir über die Google Cloud Shell nutzen. In gcloud müssen wir uns wie immer erst anmelden und idealerweise ein Update machen:

gcloud auth login
gcloud components update

Dann wechseln wir auf unser Projekt und aktualisieren außerdem das Quota-Projekt:

gcloud config set project YOUR_PROJECT_ID
gcloud auth application-default set-quota-project YOUR_PROJECT_ID

Mein Projekt habe ich „gitcicd“ genannt. Falls euer Projekt anders heißt, das im folgenden einfach austauschen.

Service Account erzeugen

Das Erzeugen eines Service Accounts geht entweder in GCP-Konsole:

  • GCP Console → IAM & Verwaltung→ Dienstkonten
  • Erstelle Service Account, z. B. github-cloudrun-deployer
  • Rolle hinzufügen: Cloud Run Admin, Cloud Build Editor, Storage Admin, Artifact Registry Writer, Logging Viewer, Cloud Build Viewer, Service Account User
  • JSON-Schlüssel erzeugen und Inhalt als Secret GCP_SA_KEY einfügen

Alternativ lässt sich der Service Account auch mit gcloud einrichten:

gcloud iam service-accounts create github-cloudrun-deployer --description="CI/CD deploys to Cloud Run via GitHub Actions" --display-name="GitHub Cloud Run Deployer"

gcloud projects add-iam-policy-binding gitcicd --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"  --role="roles/run.admin"

gcloud projects add-iam-policy-binding gitcicd   --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"   --role="roles/cloudbuild.builds.editor"

gcloud projects add-iam-policy-binding gitcicd   --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"   --role="roles/storage.admin"

gcloud projects add-iam-policy-binding gitcicd   --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"   --role="roles/artifactregistry.writer"

gcloud projects add-iam-policy-binding gitcicd   --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"  --role="roles/logging.viewer"

gcloud projects add-iam-policy-binding gitcicd   --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"   --role="roles/cloudbuild.builds.viewer"

Kürzer geht es so:

gcloud iam service-accounts create github-cloudrun-deployer --description="CI/CD deploys to Cloud Run via GitHub Actions"

for role in run.admin cloudbuild.builds.editor storage.admin artifactregistry.writer logging.viewer cloudbuild.builds.viewer iam.serviceAccountUser
do
  gcloud projects add-iam-policy-binding gitcicd   --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"     --role="roles/$role"
done

Service Account Key generieren

Nun müssen wir für den Service Account noch einen Key generieren:

gcloud iam service-accounts keys create gcp-key.json   --iam-account=github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com

Der Key wird als Datei „gcp-key.json“ lokal erzeugt.

Storage Zugriff

Außerdem müssen wir noch dem Github Runner Zugriff auf Storage geben. Dafür benötigen wir zunächst die Projektnummer:

gcloud projects describe gitcicd --format="value(projectNumber)"

Anschließend fügen wir IAM Rechte hinzu für den Cloud Build Service Account:

gcloud projects add-iam-policy-binding gitcicd --member="serviceAccount:705737653266@cloudbuild.gserviceaccount.com"   --role="roles/storage.admin"

Dazu müssen wir noch dem github-cloudrun-deployer die entsprechenden Rechte geben:

gcloud projects add-iam-policy-binding gitcicd   --member="serviceAccount:github-cloudrun-deployer@gitcicd.iam.gserviceaccount.com"  --role="roles/iam.serviceAccountUser"

Artifact Registry einrichten

Nun müssen wir noch eine Artifact Registry einrichten. Wir nennen es „app-images“:

gcloud artifacts repositories create app-images --repository-format=docker --location=europe-west1   --description="Repo for app images"

Python Code

Wir verwenden eine sehr einfache Python-App zur Demonstration:

  • calculator.py: Enthält eine einfache Additionsfunktion.
  • api.py: Erstellt eine REST-API mit FastAPI, die die Additionsfunktion nutzt.
  • client.py: Dient zum lokalen Testen der API.

In der App „calculator.py“ werden mithilfe der Funktion „add“ zwei Zahlen addiert:

def add(a, b):
    return a + b

if __name__ == "__main__":
    print(add(2, 3))

In der Datei „api.py“ erstellen wir mit FastAPI eine einfache Rest-API um die App bereitzustellen:

from fastapi import FastAPI
from calculator import add
from pydantic import BaseModel


app = FastAPI()

class AddRequest(BaseModel):
    a: float
    b: float

@app.get("/")
def read_root():
    return {"message": "Model API is running"}

@app.post("/add")
def add_numbers(request: AddRequest):
    result = add(request.a, request.b)
    return {"result": result}

Der Server kann lokal mit folgendem Befehl gestartet werden:

uvicorn api:app --host "0.0.0.0" --port "8000"

Um die API lokal auszuprobieren können wir in „client.py“ einen einfachen Request stellen:

import requests

# Basis-URL der FastAPI
BASE_URL = "http://127.0.0.1:8000"

# GET-Anfrage an die Root-Route
def check_status():
    response = requests.get(f"{BASE_URL}/")
    print("GET / ->", response.text)
    print("Status Code:", response.status_code)

# POST-Anfrage an den /add-Endpunkt
def add_numbers(a, b):
    payload = {"a": a, "b": b}
    response = requests.post(f"{BASE_URL}/add", json=payload)
    print(f"POST /add with {a} and {b} ->", response.json())

if __name__ == "__main__":
    check_status()
    add_numbers(5, 7)

Dockerfile

Damit das Projekt später als Container auf CloudRun deployed werden kann, muss noch ein Dockerfile erzeugt werden. Die Datei heißt „Dockerfile“ ohne Endung und liegt immer im Root-Verzeichnis des Projekts. In diesem Fall ist das Dockerfile sehr einfach gehalten. Es installiert lediglich die Abhängigkeiten und startet mit „uvicorn“ die Api.

FROM python:3.13

WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8080"]

GitHub Actions

GitHub Actions ermöglichen das erstellen von CI/CD Workflows. CI/CD steht dabei für
CI (Continuous Integration): Automatisiertes Testen, Linting und Validieren von Code bei jedem Push oder Pull Request. Das Ziel ist, Fehler frühzeitig zu erkennen.
CD (Continuous Deployment/Delivery): Automatisiertes Bereitstellen der Anwendung oder des ML-Modells nach erfolgreichem Build.

Um Github Actions einzurichten benötigt man im lokalen Repository einen Ordner im Verzeichnis .github/workflows

Dort legt man eine neue Datei mit dem Namen ci.yml an. Hier ein Beispiel:

name: Python CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.9"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt


Zur Erklärung:

  • name: legt einfach den Namen der Pipeline fest
  • on: definiert wann die pipeline laufen soll. Hier also bei Pushes oder Pull Requests die jeweils auf den Main Branch zeigen
  • jobs: legt die einzelnen Schritte der Pipeline fest. Hier gibt es nur einen Job Test, dieser läuft auf ubuntu
  • steps: definiert die einzelnen Schritte eines Jobs. actions/… ruft dabei jeweils Standard-Komponenten von Github auf. Hier wird mit checkout das Repository in den Runner geladen (also die Machine auf der die Pipeline läuft), Python installiert, die requirements installiert und dann pytest ausgeführt.

Wir testen die Ausführung der Pipeline mit einem Push ins Main-Repository auf GitHub.

git add .github/workflows/ci.yml
git commit -m "Add CI pipeline"
git push origin main

Auf der GitHub Webseite kann man unter „Actions“ nun den Durchlauf des Tests sehen:

Tests ausführen

Eine häufige Nutzung von GitHub Actions ist das ausführen von Tests beim Push ins Repository. Zu diesem Zweck benötigt man pytest (Installation mit „pip install pytest“). Danach kann eine Datei test_app.py erstellt werden, in welcher die Tests definiert werden. Beispielsweise so:

from app import add


def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

Um nun die Tests als Schritt in GitHub Actions einzufügen, reicht es, diesen Step zu ergänzen:

      - name: Run tests
        run: |
          pytest

Wichtig: Da pytest benötigt wird, muss es vorher installiert werden. Am einfachsten ergänzt man es in requirements.txt, wodurch es (wie oben im ci.yml-File gezeigt) beim Ausführen der Pipeline automatisch auf dem GitHub Runner installiert wird.

Linting mit Flake8 oder Ruff

Folgender Step führt Linting mit flake8 aus. Linting dient dazu, Python-Code auf stilistische und potenzielle Fehler zu prüfen. flake8 prüft dabei unter anderem auf PEP8-Verstöße, Syntaxfehler und nicht verwendete Imports oder Variablen.

   - name: Lint with flake8
     run: flake8 model/ tests/

Alternative Tools zu flake8 sind beispielsweise black (ein auto-formatter), isort (für klare Imports), mypy (für stabilieren, typisierten Code), bandit (Sicherheitsrelevante Prüfung) oder pylint (umfangreicher als flake8). Als All-in-One Alternative wird ruff empfohlen, welches mit fast allen wichtigen Lintern kompatibel ist (bspw. flake8, isort, black und pylint).

    - name: Install Ruff
      run: pip install ruff

    - name: Run Ruff
      run: ruff check .  # prüft alle Python-Dateien im Projekt

Die Vorgaben für das Linting lassen sich über eine zentrale pyproject.toml Datei konfigurieren:

[tool.black]
line-length = 88

[tool.isort]
profile = "black"

[tool.mypy]
strict = true

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I"] # z. B. Errors, Formatting, Bugbear, Imports

GitHub Secrets

Secrets sind verschlüsselte Umgebungsvariablen, die du in deinem Repository oder GitHub-Organisation hinterlegen kannst. Sie werden in GitHub Actions Workflows automatisch eingebunden – z. B. für:

  • Login bei DockerHub, AWS, GCP, HuggingFace
  • Zugriff auf Datenbanken oder APIs
  • Deployment-Schlüssel

Sie können auf drei Ebenen gespeichert werden: Repository (nur für dieses Repo), Organisation (für alle Repos) oder Environment (für best. Umgebungen bspw. prod).

Die GitHub Secrets sind niemals öffentlich, auch nicht bei öffentlichen Repositories. Sie sind nur innerhalb von GitHub Action Workflows zugänglich. Nur Personen mit Schreibzugriff auf das Repository können die Secrets sehen (aber nur die Namen, nicht die Werte). Secrets werden in der Laufzeit als Umgebungsvariable injected und werden auch in Logs automatisch ersetzt.

Um ein Secret für ein Repository anzulegen, benötigt man folgende Schritte:

  • Gehe zu deinem Repository auf GitHub
  • Klicke auf Settings
  • Gehe links auf Secrets and variables → Actions
  • Klicke auf New repository secret
  • Gib einen Namen und den Wert ein
  • Speichern

Für dieses Beispielprojekt benötigen wir drei Variablen:

  • GCP_PROJECT (= GCP-Projektname)
  • GCP_REGION (z.B. europe-west1)
  • GCP_SA_KEY (gesamter JSON-Inhalt des Service Account Keys)

Im Workflow kann man dann auf die Secrets zugreifen:

    - name: Set up Google Cloud SDK
      uses: google-github-actions/setup-gcloud@v2
      with:
        project_id: ${{ secrets.GCP_PROJECT }}
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        export_default_credentials: true

Automatisches Deployment auf GCP

In einer zweiten GitHub Action definieren wir nun das Deployment auf GCP. Wir wollen, dass bei jedem Push in den Main-Branch ein Deployment durchgeführt wird.

Bei den Steps muss mithilfe der hinterlegten Secrets zunächst die Authentifizierung bei der Google Cloud durchgeführt werden. Anschließend wird das Projekt festgelegt und die Google Cloud SDK eingerichtet.

Im nächsten Schritt wird Docker so konfiguriert, um direkt in die Google Artifacts Registry zu pushen. Im Abschluss wird das Container Image gebaut und gepusht. Dies funktioniert unter Verwendung von Cloud Build . Das Docker-Image wird abschließend auf Cloud-Run deployed. Dabei wird über „–allow-unauthenticated“ sichergestellt, dass auch öffentliche Zugriffe ohne Authentifizierung möglich sind.

Die vollständigen GitHub Actions definieren wir in einem neuen Workflow „deploy-cloudrun.yml“:

name: Deploy to Cloud Run

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: Build & Deploy to Cloud Run
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Code
      uses: actions/checkout@v3

    - name: Authenticate to Google Cloud
      uses: google-github-actions/auth@v2
      with:
        credentials_json: '${{ secrets.GCP_SA_KEY }}'

    - name: Set up Google Cloud SDK
      uses: google-github-actions/setup-gcloud@v2
      with:
        project_id: ${{ secrets.GCP_PROJECT }}

    - name: Configure Docker for Artifact Registry
      run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev

    - name: Build & Push Docker Image
      run: |
        IMAGE=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/app-images/myapp
        gcloud builds submit --tag $IMAGE --async || echo "Ignore log streaming error"

    - name: Deploy to Cloud Run
      run: |
        IMAGE=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/app-images/myapp
        gcloud run deploy myapi \
          --image $IMAGE \
          --region ${{ secrets.GCP_REGION }} \
          --platform managed \
          --allow-unauthenticated \
          --memory 512Mi \
          --port 8080

Deployment ausführen

Um die App nun zu deployen, müssen wir lediglich den Code ins GitHub-Repository pushen.

Wichtig: Die Actions haben wir so konfiguriert, dass sie nur bei einem Push auf den main-Branch ausgeführt werden. Habt ihr also in einem Feature-Branch entwickelt, dann müsst ihr erst auf den Main-Branch pushen, damit die App deployed wird.

Anschließend ist die Instanz in der Google Cloud Console unter „Cloud Run“ zu finden:

Abschluss

Cloud Run ist so konfigurieren, dass es automatisch auf Null skaliert, wenn keine Anfragen kommen. Das kann auch manuell festgelegt werden:

gcloud run services update my-api  --region europe-west1   --min-instances 0

Testing

Um zu testen, ob die API tatsächlich funktioniert, kann wieder der client.py Code verwendet werden. Ersetze die URL mit der URL deiner Cloud Run Instanz und Teste ob eine korrekte Antwort zurückgegeben wird.

Aufräumen

Nachdem du dein Projekt erfolgreich auf Google Cloud Platform (GCP) deployt und getestet hast, möchtest du in der Regel alle Ressourcen bereinigen, um keine unnötigen Kosten zu verursachen. Dabei ist es wichtig, dass du nicht zwingend dein gesamtes GCP-Projekt löschen musst. Stattdessen kannst du gezielt einzelne Ressourcen deaktivieren oder entfernen.

Cloud Run Service löschen
gcloud run services delete myapi --region europe-west1
Container-Images aus Artifact Registry löschen
gcloud artifacts docker images delete europe-west1-docker.pkg.dev/gitcicd/app-images/myapp --delete-tags

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert