Skip to content
Preview-Umgebungen pro Pull Request auf AWS, ohne Vercel
← ← Zurück zu Gedanken Cloud

Preview-Umgebungen pro Pull Request auf AWS, ohne Vercel

Code-Review hat eine Obergrenze. Ein Reviewer kann einen Diff lesen und darüber nachdenken, aber er kann nicht darauf klicken. Für ein Produkt mit UI ist der nützlichste Review-Kommentar „Ich habe es geöffnet, den Flow ausprobiert, und der zweite Schritt verliert den Formularzustand“, und diesen Kommentar bekommen Sie nur, wenn der Reviewer eine URL hat. Vercel hat auf dieser Erkenntnis ein Unternehmen gebaut. Wenn Ihre App ein statisches Frontend ist, nehmen Sie Vercel und hören Sie auf zu lesen.

Unsere ist keins. Die Plattform, die wir für einen US-Kunden betreiben, besteht aus drei Next.js-Anwendungen plus einer API-Schicht, Lambdas, DynamoDB-Tabellen und einer Aurora-Datenbank, alles in CDK definiert und auf AWS ausgerollt. Ein reines Frontend-Preview, das auf das gemeinsame Dev-Backend zeigt, ist eine Lüge: es zeigt dem Reviewer die neue UI im Gespräch mit der alten API. Wir wollten den ganzen Stack pro Pull Request, und er sollte etwa einen Dollar kosten. Hier ist, was wir gebaut haben.

Was pro Pull Request ist, und was nicht

Der Trick liegt in der Entscheidung, was dupliziert werden muss. Alles zu duplizieren ist langsam im Aufbau und teuer im Betrieb. Nichts zu duplizieren ist Vercel. Die Linie, die wir gezogen haben:

Pro Pull Request · Stack pr-123 App-Runner-Service · 0,25 vCPU / 0,5 GB Lambda-Funktionen (alle) DynamoDB-Tabellen · On-Demand · DESTROY Postgres-Datenbank pr_123 auf dem geteilten Cluster in ~6 Min. erstellt · bei Merge oder Schließen zerstört Geteilt · Dev-Konto VPC · Subnetze · das eine NAT Gateway Aurora-Serverless-v2-Cluster (Auto-Pause) ECR-Repositories · CodeBuild-Projekte Secrets Manager · KMS-Schlüssel · WAF vom PR-Stack per Name nachgeschlagen, nie von ihm erstellt

Alles mit Stundenpreis und langsamer Erstellung wird geteilt: das VPC, das NAT, der Aurora-Cluster. Alles, was im Leerlauf kostenlos und schnell erstellt ist, gibt es pro PR: App Runner in Minimalgröße, Lambdas, DynamoDB-Tabellen. Die Datenbank ist der Kompromiss. Ein eigener Cluster pro PR würde 10 Minuten dauern und an der 0,5-ACU-Untergrenze $40 im Monat kosten; eine eigene Datenbank auf dem geteilten Cluster ist ein CREATE DATABASE und kostet nichts. Migrationen laufen bei der Stack-Erstellung gegen pr_123, genau wie in Produktion, sodass ein PR, der das Schema ändert, mit seinem eigenen Schema vorschaut.

Die CDK-Seite

Die Umgebung existierte bereits als CDK-Kontextwert: dev, staging, prod. Ein Preview ist eine vierte Variante, pr, mit einer Nummer.

// bin/app.ts
const env = app.node.tryGetContext('env') ?? 'dev';
const pr = app.node.tryGetContext('pr');           // "123" oder undefined
const suffix = pr ? `-pr-${pr}` : '';

new PlatformStack(app, `platform-${env}${suffix}`, {
  env: accounts[env === 'pr' ? 'dev' : env],
  config: {
    ...configs[env === 'pr' ? 'dev' : env],
    removalPolicy: pr ? RemovalPolicy.DESTROY : configs[env].removalPolicy,
    warmInstances: pr ? 1 : configs[env].warmInstances,
    instanceSize: pr ? 'small' : configs[env].instanceSize,
    databaseName: pr ? `pr_${pr}` : 'app',
    previewToken: pr ? Secret.fromSecretNameV2(...).secretValue : undefined,
  },
});

Der Stack selbst weiß nicht, dass er ein Preview ist. Er bekommt eine Konfiguration, in der die Removal Policy DESTROY sagt, die Instanz klein ist und der Datenbankname eine Nummer enthält. Geteilte Ressourcen werden mit Vpc.fromLookup, DatabaseCluster.fromDatabaseClusterAttributes und Repository.fromRepositoryName importiert, genau wie es der Dev-Stack für Cross-Stack-Referenzen ohnehin schon tat. Der Diff für die Preview-Unterstützung lag unter hundert Zeilen, und das meiste davon war die Konfigurationsverdrahtung oben.

Die GitHub-Actions-Seite

Zwei Workflows. Der erste läuft bei pull_request mit den Typen opened, synchronize und reopened:

jobs:
  preview:
    runs-on: ubuntu-latest
    permissions: { id-token: write, contents: read, pull-requests: write }
    concurrency: preview-${{ github.event.number }}
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<dev-account>:role/github-deploy-dev
          aws-region: us-east-1
      - run: ./scripts/build-images.sh --tag pr-${{ github.event.number }}-${{ github.sha }}
      - run: npx cdk deploy platform-pr-${{ github.event.number }} --context env=pr --context pr=${{ github.event.number }} --require-approval never
      - run: ./scripts/comment-preview-url.sh ${{ github.event.number }}

Der zweite läuft bei pull_request mit dem Typ closed und besteht aus einem Schritt: cdk destroy desselben Stack-Namens, gefolgt von DROP DATABASE pr_123 über eine kleine Lambda mit Netzwerkzugang zum Cluster. Gemergt oder verworfen, der PR nimmt seine Umgebung mit.

Das Kommentar-Skript liest die App-Runner-URL aus den Stack-Outputs und postet sie einmal, dann bearbeitet es bei jedem Push denselben Kommentar, damit der PR nicht mit Bot-Rauschen vollläuft. Die URL ist der *.awsapprunner.com-Hostname. Wir erstellen keine Custom Domains für Previews: eine Zertifikatsvalidierung pro PR kostet fünf Minuten und bringt nichts.

Von Push zu URL: etwa sechs Minuten

Schritt Zeit
Checkout, Rolle annehmen 20 s
Drei Images in CodeBuild bauen, parallel ~3 Min.
cdk deploy auf einen neuen Stack, inklusive App-Runner-Service-Erstellung ~2,5 Min.
Migrationen gegen pr_123 10 s
Kommentar auf dem PR 2 s

Bei weiteren Pushes existiert der Stack, also ändert sich nur das Image-Tag und App Runner macht ein Rolling Update: etwa vier Minuten. Nicht Vercels vierzig Sekunden, aber schnell genug, dass ein Reviewer, der eine Änderung wünscht, sie in derselben Sitzung sieht.

PR geöffnetoder gepusht Images bauenTag pr-123-<sha> cdk deploy pr-123Dev-Konto · DESTROY URL auf dem PRein Kommentar, bearbeitet gemergt / geschlossendestroy + drop db jeder Push: derselbe Stack, neues Image-Tag, Rolling Update in ~4 Min. TTL-Sweeper: jeder pr-*-Stack älter als 7 Tage wird ohnehin zerstört

Die drei Dinge, die es vor dem Chaos bewahren

Ein Sweeper. Workflows schlagen fehl. Ein closed-Event geht verloren, wenn GitHub einen Ausfall hat oder jemand den Branch aus der CLI löscht. Eine geplante Lambda listet CloudFormation-Stacks namens platform-pr-*, prüft die Erstellungszeit und zerstört alles, was älter als sieben Tage ist. Sie hat viermal wirklich zugeschlagen. Ohne sie sammeln sich tote Previews zu $5 im Monat pro Stück an, still und leise.

Ein Preview-Token. Previews liegen auf öffentlichen URLs mit echt aussehenden Daten. Eine Next.js-Middleware prüft ein Cookie, das von /preview?token=… gesetzt wird, wobei das Token zur Laufzeit aus einem geteilten Secret gelesen und nicht zur Build-Zeit eingebacken wird. Reviewer klicken einmal auf den Link im PR-Kommentar, der das Token trägt, und browsen dann normal. Es ist keine Sicherheitsgrenze gegen einen entschlossenen Angreifer, aber es hält Crawler und versehentliches Teilen fern, und die Daten dahinter sind Fixtures, niemals Produktion.

Fixtures, keine Prod-Daten. Die Datenbank pro PR wird aus einer im Repo eingecheckten Fixture-Datei befüllt: eine Handvoll Konten, Bestellungen, die Zustände, die fürs Review zählen. Produktionsdaten in Previews zu kopieren ist der schnellste Weg, sie zu leaken, und es macht Previews außerdem langsam im Aufbau. Wenn ein Reviewer einen bestimmten Zustand braucht, fügt er ihn zur Fixture hinzu, und jedes zukünftige Preview hat ihn.

Was es kostet

Posten Pro PR, 3 Tage offen
App Runner, 0,25 vCPU / 0,5 GB, meist im Leerlauf ~$0,50
DynamoDB On-Demand, ein paar tausend Requests ~$0,01
Lambda ~$0,00
Datenbank auf dem geteilten Cluster $0
CodeBuild, 3 Images × ~4 Builds ~$1,20
Summe ~$1,70

Bei zwanzig Pull Requests im Monat sind das etwa $35, wovon CodeBuild der größte Teil ist. Der geteilte Aurora-Cluster in Dev existierte bereits und pausiert automatisch; die Previews halten ihn etwas länger wach, was real, aber schwer zu messen ist. Verglichen mit einem Vercel-Pro-Sitz pro Entwickler ist es günstiger, und anders als Vercel zeigt es auch die API und die Datenschicht in der Vorschau.

Wann Sie das nicht tun sollten

Wenn Ihr Backend stabil ist und Ihre PRs fast immer Frontend sind, ist ein reines Frontend-Preview gegen das geteilte Dev in Ordnung und viel einfacher. Wenn Ihr Stack zwanzig Minuten zur Erstellung braucht, weil er einen ALB, eine RDS-Instanz und einen Redis-Cluster hat, werden Umgebungen pro PR zu langsam sein, um nützlich zu sein, und Sie sollten sich Namespaces auf einem geteilten Cluster oder ephemere Datenbanken mit Branching ansehen. Und wenn Sie mehr als eine Handvoll Entwickler haben, achten Sie auf die Rate-Limits der CloudFormation-API: zwanzig gleichzeitige cdk deploy-Läufe in einem Konto werden sie treffen.

Für ein Dreierteam, das mehrere Pull Requests am Tag auf einem Stack ausliefert, der in sechs Minuten entsteht, ist es die beste Developer-Experience-Investition, die wir gemacht haben. Reviews wurden schneller, und „läuft auf meiner Maschine“ hörte auf, ein Argument zu sein, weil es dieselbe Maschine ist.

Möchten Sie Previews für Ihren eigenen Stack, auf Ihrem eigenen AWS-Konto? Sprechen Sie mit uns.