post

Preview-Umgebungen pro Pull Request

Ephemere Umgebungen pro Pull Request verkürzen das Feedback und machen Reviews konkret: Man klickt die Änderung, statt sie sich aus dem Diff vorzustellen.

≈ 9 Min. Lesezeit

Ephemere Umgebungen pro Pull Request verkürzen das Feedback und machen Reviews konkret: Man klickt die Änderung, statt sie sich aus dem Diff vorzustellen.

Das Problem hinter dem Diff

In vielen Teams, mit denen ich arbeite, sieht Review so aus: Jemand öffnet einen Pull Request, ein Kollege liest den Diff, denkt sich das Verhalten dazu und schreibt „LGTM“. Das ist ein Ratespiel. Ein Diff zeigt, welche Zeilen sich geändert haben – nicht, wie sich die Anwendung danach anfühlt. Ob der neue Checkout-Flow auf dem Smartphone bedienbar ist, ob der Ladezustand flackert, ob ein Sonderfall in der Fachlichkeit stimmt: Das steht in keinem git diff.

Die klassische Antwort war ein gemeinsames Staging. Ein Team, eine Umgebung, eine Warteschlange. Wer testen will, deployt seinen Branch, überschreibt damit den vorigen und blockiert alle anderen. Bei zwei parallelen Features ist das eine Quelle für Konflikte, bei fünf ein permanenter Stau. Das eigentliche Ziel – schnelles, konkretes Feedback – geht in der Koordination unter.

Der Stau ist dabei nur die sichtbare Seite. Die unsichtbare ist, dass ein geteiltes Staging nie einen definierten Zustand hat: Es trägt immer die Reste des letzten Deploys, halb migrierte Schemata, Testdaten aus einem fremden Feature. Wenn ein Reviewer dort etwas Merkwürdiges sieht, weiß niemand mit Sicherheit, ob das der aktuelle PR ist oder ein Artefakt von gestern. Genau dieses „liegt es an meiner Änderung oder an der Umgebung?“ frisst Zeit und Vertrauen. Eine frisch hochgezogene Instanz kennt dieses Problem nicht – sie beginnt bei jedem Lauf im selben, reproduzierbaren Nullzustand.

Die These dieses Artikels ist einfach: Jeder Pull Request bekommt seine eigene, wegwerfbare Umgebung mit eigener URL. Reviewer klicken die laufende Änderung an, statt sie sich vorzustellen. Und weil die Umgebung ephemer ist, verschwindet sie beim Merge wieder – ohne Aufräum-Ritual, ohne wachsende Kostenrechnung.

Der Outcome, nicht die Mechanik

Bevor es um app.json und CI-Pipelines geht, lohnt der Blick auf das Warum. Der Nutzen ist kein Infrastruktur-Selbstzweck, sondern eine kürzere Feedback-Schleife – und die zahlt direkt auf messbare Delivery-Leistung ein.

Accelerate (Forsgren, Humble, Kim, 2018) hat das mit Daten unterlegt: Kleine Batches, kurze Lead Time for Changes und hohe Deployment Frequency sind die Treiber leistungsfähiger Softwareauslieferung. Eine Preview-Umgebung pro Pull Request drückt genau dort. Sie belohnt kleine Änderungen – ein winziger PR ist in Sekunden deploybar und in Minuten reviewt. Sie senkt die Hemmschwelle, überhaupt zu integrieren, weil Feedback nicht auf den nächsten Staging-Slot warten muss. Das passt bruchlos zu Trunk-Based Development und zu dem, was Humble und Farley schon 2010 in Continuous Delivery beschrieben haben: kurzlebige Branches, schnelle Integration, der PR als Integrationseinheit.

Der vielleicht unterschätzteste Effekt ist sozial. Sobald es eine klickbare URL gibt, kann auch reviewen, wer keine Entwicklungsumgebung aufsetzt: QA, Produktmanagement, Design. Ein Designer, der die reale Umsetzung im Browser sieht, gibt in fünf Minuten Feedback, für das sonst ein Meeting und drei Screenshots nötig wären. Das ist der Punkt, an dem aus einem Technik-Feature ein Delivery-Vorteil wird.

Konzeptionell ist das eng verwandt mit der Build-Measure-Learn-Schleife aus Ries' Lean Startup (2011). Eine Preview ist das kleinste sinnvolle Artefakt, an dem man messen und lernen kann, bevor etwas in Produktion geht. Je kleiner der Batch, desto schneller die Schleife – und ephemere Umgebungen machen kleine Batches erst wirtschaftlich, weil das Hochfahren einer isolierten Instanz nichts mehr kostet außer ein paar Minuten Compute. In der Beratung erlebe ich immer wieder, dass Teams nicht an fehlender Technik scheitern, sondern an der Reibung, überhaupt etwas Konkretes vorzeigen zu können. Die Preview räumt genau diese Reibung weg.

Der Lebenszyklus einer ephemeren Umgebung

Die Mechanik ist bei allen etablierten Produkten dieselbe – ob Heroku „Review Apps“, GitLab „Review Apps“, Netlify „Deploy Previews“ oder Vercels „Preview Deployments“. Der PR ist der Auslöser, der Merge das Ende.

sequenceDiagram
    participant Dev as Developer
    participant PR as Pull Request
    participant CI as CI Pipeline
    participant Env as Preview Environment
    Dev->>PR: open / push commit
    PR->>CI: trigger build (HEAD commit)
    CI->>Env: deploy isolated instance (own URL + own DB)
    Env-->>PR: post preview URL as commit status
    Dev->>Env: reviewer clicks and tests
    Dev->>PR: push new commit
    PR->>CI: rebuild and redeploy
    Dev->>PR: merge or close
    PR->>Env: destroy environment

Wichtig ist das Wort isolated. Nicht ein Staging für alle, sondern n Previews parallel – jede mit eigener URL, im Idealfall mit eigener Datenschicht. Der Kontrast zum alten Modell ist der ganze Punkt:

graph LR
    subgraph Classic["One staging for all"]
        A[PR 1] --> S[Shared Staging]
        B[PR 2] --> S
        C[PR 3] --> S
        S --> Q[Queue and conflicts]
    end
    subgraph Ephemeral["Preview per PR"]
        D[PR 1] --> P1[preview-1.example.com]
        E[PR 2] --> P2[preview-2.example.com]
        F[PR 3] --> P3[preview-3.example.com]
    end

Deklarativ statt Skript-Grab

Der eleganteste Ansatz beschreibt die Umgebung deklarativ, statt sie in einer Kette von Shell-Schritten zusammenzusetzen. Heroku macht das über eine app.json vor: Was eine Review App braucht – Add-ons, Migrationen, Feature-Flags – steht an einer Stelle, versioniert im Repository.

{
  "name": "my-app",
  "environments": {
    "review": {
      "addons": ["heroku-postgresql:hobby-dev"],
      "scripts": {
        "postdeploy": "bundle exec rake db:migrate db:seed"
      },
      "env": {
        "FEATURE_FLAGS": "preview"
      }
    }
  }
}

Der Reiz liegt im postdeploy-Hook: Jede Preview bekommt eine frische Datenbank, migriert das Schema und spielt Seed-Daten ein. Die Umgebung ist damit reproduzierbar aus dem Nichts – und genau das macht sie wegwerfbar.

Wer nicht auf einer PaaS wie Heroku oder Netlify sitzt, baut dasselbe Muster mit GitHub Actions selbst. Die drei Schritte bleiben gleich: bauen, in eine isolierte Instanz deployen, die URL zurück in den PR schreiben. Wie das Deploy-Skript intern arbeitet – ob es einen Container-Host anspricht, eine App auf einer eigenen Plattform hochzieht oder ein statisches Bundle ausliefert – ist für dieses Gerüst zweitrangig; entscheidend ist der saubere Vertrag aus Name, Image und Host.

name: preview
on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Build and push image
        run: |
          docker build -t registry.example.com/app:${{ github.sha }} .
          docker push registry.example.com/app:${{ github.sha }}

      - name: Deploy preview instance
        run: |
          ./scripts/deploy-preview.sh \
            --name "pr-${{ github.event.number }}" \
            --image "registry.example.com/app:${{ github.sha }}" \
            --host "pr-${{ github.event.number }}.preview.example.com"

      - name: Comment preview URL
        run: |
          gh pr comment ${{ github.event.number }} \
            --body "Preview ready: https://pr-${{ github.event.number }}.preview.example.com"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Genauso wichtig wie das Deploy ist das Teardown. Eine Preview, die niemand abräumt, ist kein Feature, sondern eine Kostenstelle. Deshalb gehört ein zweiter Workflow dazu, der beim Schließen des PR aufräumt:

name: preview-teardown
on:
  pull_request:
    types: [closed]

jobs:
  destroy-preview:
    runs-on: ubuntu-latest
    steps:
      - name: Tear down environment
        run: |
          ./scripts/destroy-preview.sh --name "pr-${{ github.event.number }}"

Ob das Deploy-Ziel nun ein Kubernetes-Namespace, eine Heroku-App oder ein statischer Netlify-Build ist, spielt konzeptionell keine Rolle. Das Muster ist portabel: PR öffnet, Instanz entsteht, URL landet im PR, Merge räumt auf.

Ephemere Umgebungen als Prüfstand

Eine isolierte, produktionsnahe Instanz ist mehr als eine Klick-Vorschau. Sie ist ein sauberer Ort, um automatisierte Checks gegen ein echtes Deployment laufen zu lassen – nicht nur gegen Mocks. Zwei Beispiele, die sich in der Praxis bewähren:

Contract-Tests mit Pact prüfen, ob ein Service die Erwartungen seiner Konsumenten noch erfüllt. Gegen eine echte, hochgefahrene PR-Instanz verifiziert, ist das aussagekräftiger als gegen einen Stub. Und architektonische Fitness Functions, wie sie Ford, Parsons und Kua in Building Evolutionary Architecture (2017) beschreiben, lassen sich gegen die laufende Preview scharf stellen: Antwortzeiten, Abhängigkeitsregeln, Bundle-Größen. So wird die Preview zum Gate, das nicht nur „sieht gut aus“, sondern „hält die Regeln ein“ beantwortet.

Feature-Flags ergänzen das Bild. Eine Preview mit gesetztem FEATURE_FLAGS=preview zeigt den Zielzustand, während dieselbe Änderung in Produktion noch dunkel geschaltet bleibt – Review und Rollout werden entkoppelt. Wer die Flag-Idee ernst nimmt, kann eine Änderung mehrfach mergen, ohne sie freizuschalten, und sie später per Toggle live nehmen. Das reduziert lange Feature-Branches und passt zur Trunk-Based-Idee: integrieren, sichtbar machen, kontrolliert ausrollen.

Der gedankliche Bogen dahinter ist derselbe wie bei blue-green oder canary: Man trennt „deployed“ von „für alle sichtbar“. Eine Preview ist in diesem Sinn ein canary für genau einen Reviewer – die kleinste denkbare Ausrollstufe. Wer diese Trennung einmal verinnerlicht hat, denkt Deployment und Freigabe nicht mehr als denselben Schritt, und das ist eine der wirkungsvollsten Entlastungen für ein Team, das oft und ruhig ausliefern will.

Wo es wirklich wehtut

Ich wäre unehrlich, wenn ich das als reibungslos verkaufen würde. Der schwierige Teil ist nie das Deploy des Web-Prozesses – es ist alles darunter.

Am härtesten ist die Datenschicht. Teilen sich alle Previews eine Datenbank, kontaminieren sie sich gegenseitig: Ein PR migriert ein Schema, ein anderer stolpert darüber. Eine Datenbank pro PR löst das, kostet aber Provisioning-Zeit und Geld – und jede Migration muss sauber von Null durchlaufen. Das ist übrigens ein unterschätzter Nebeneffekt: Wer Previews ernst nimmt, testet damit bei jedem PR auch seinen Migrationspfad auf der grünen Wiese. Migrationen, die nur auf gewachsenen Datenbeständen funktionieren, fallen hier sofort auf, statt beim nächsten frischen Setup zu überraschen. Externe Dienste verschärfen das Bild noch: Zahlungen, E-Mail-Versand, Drittanbieter-APIs dürfen aus einer Preview niemals echt feuern. Dafür braucht es Sandbox-Modi oder Stubs, und die müssen konfiguriert sein, bevor der erste Reviewer klickt – sonst verschickt die Vorschau eines Marketing-Features versehentlich echte Mails an echte Adressen.

Der zweite Dauerbrenner ist der Lifecycle. Vergessene Umgebungen kosten Geld und driften vom aktuellen Stand weg. Das Teardown bei Merge oder Close muss zuverlässig greifen, ergänzt um eine TTL oder einen Idle-Shutdown für PRs, die wochenlang offen liegen. Ein einzelner nicht gefeuerter Close-Event – etwa weil ein PR ins Leere gelöscht wurde – darf keine verwaiste Instanz hinterlassen, deshalb gehört neben dem ereignisgetriebenen Abbau ein regelmäßiger Aufräum-Job dazu, der nach Alter oder Inaktivität kehrt. Und der Feedback-Vorteil kippt, wenn der Build zehn Minuten dauert: Kalte Starts und lange Pipelines untergraben genau das, wofür man das Ganze baut. Ein schneller, aggressiv gecachter Build ist hier kein Luxus, sondern die Voraussetzung dafür, dass die kurze Schleife überhaupt kurz bleibt.

Drittens Secrets und Zugriff. Preview-URLs sind oft öffentlich und erratbar – pr-42.preview.example.com ist keine Sicherheitsgrenze. Ungeschützt leaken solche Vorschauen unfertige Features oder echte Daten. Secrets gehören sicher injiziert, der Zugriff hinter Basic Auth, SSO oder Passwortschutz. Netlify bietet Password Protection für Deploy Previews genau aus diesem Grund.

Und schließlich der Konfig-Skew. Eine Preview nützt nur, wenn sie produktionsnah ist. Weicht sie zu stark ab, bekommt man das schlechteste aller Ergebnisse: grünes Review, roter Prod-Deploy. Dann hat die Umgebung Vertrauen erzeugt, das sie nicht decken konnte. Die Gegenmittel sind unspektakulär, aber wirksam: dasselbe Container-Image von der Preview bis in die Produktion durchreichen, Konfiguration konsequent über Umgebungsvariablen statt über abweichende Build-Pfade steuern und die wenigen bewusst gesetzten Unterschiede – Sandbox-Endpunkte, kleinere Ressourcen, aktive Feature-Flags – an einer Stelle sichtbar halten. Je näher die Preview an Produktion liegt, desto mehr ist ein grünes Review tatsächlich wert.

Der rote Faden

Preview-Umgebungen pro Pull Request sind 2021 keine exotische Idee mehr, sondern eine produktisierte Praxis – von Heroku über GitLab und Netlify bis Vercel. Der Kern ist überall gleich und in wenigen Sätzen erzählt: Der PR erzeugt eine isolierte, wegwerfbare Instanz mit eigener URL, jeder neue Commit deployt neu, der Merge räumt auf.

Der Gewinn ist kein Infrastruktur-Trick, sondern eine kürzere und ehrlichere Feedback-Schleife. Reviewer beurteilen laufendes Verhalten statt Zeilen im Diff. Nicht-Entwickler können mitreden. Kleine Batches werden belohnt, der Staging-Stau verschwindet. Das ist die Accelerate-Logik in Reinform: Reibung raus, Zyklen kürzer, Qualität früher sichtbar.

Der Rat aus der Beratung ist deshalb unspektakulär und wichtig zugleich: Fang beim Outcome an, nicht bei der Plattform. Die schwierigen Fragen sind Datenbank, Lifecycle und Zugriffsschutz – nicht das Deploy des Web-Prozesses. Wer die sauber löst, bekommt Reviews, die man anklickt statt sich vorstellt. Und das ist der Unterschied zwischen „LGTM“ und „ich habe es gesehen“.

Weiterführende Quellen

Kommentare