API-Design für einen Cloud Service Provider: Ein praktischer Leitfaden
Wer eine API für einen Cloud Service Provider (CSP) baut, steht vor einer anderen Herausforderung als bei einer klassischen Web-API: Cloud-Ressourcen sind langlebig, Operationen dauern oft Minuten statt Millisekunden, und Kunden verlassen sich auf diese API, um produktive Infrastruktur zu steuern. Ein Fehler im Design lässt sich später kaum noch reparieren, ohne bestehende Nutzer zu brechen. Dieser Beitrag beschreibt die wichtigsten Entscheidungen, die man von Anfang an richtig treffen sollte, inklusive konkreter Beispiele.
Spec-First statt Code-First
Der wichtigste Grundsatz: Die API-Spezifikation kommt zuerst, der Code danach. Konkret heißt das, eine OpenAPI-3.1-Spezifikation zu schreiben, bevor überhaupt ein Server-Endpunkt implementiert wird.
Das bringt mehrere Vorteile:
- Mehrere Clients (Web-UI, CLI, SDKs, Terraform-Provider) können parallel gegen einen stabilen Vertrag entwickeln
- Aus der Spec lassen sich automatisch Client-SDKs generieren (z. B. mit openapi-generator)
- Mock-Server und Server-Stubs lassen sich aus derselben Spec ableiten, was Tests vor der eigentlichen Implementierung ermöglicht
- Ein Linting-Tool wie Spectral sorgt dafür, dass Endpunkte, Namenskonventionen und Fehlerformate konsistent bleiben
Praktischer Workflow
Ein bewährter Ablauf sieht so aus:
- Spec in einem Repository versionieren (ein eigenes
api-spec-Repo, getrennt vom Server-Code) - Bei jedem Pull Request automatisch gegen Spectral-Regeln linten (CI-Pipeline)
- Bei jedem Merge automatisch Client-SDKs generieren und als eigene Pakete veröffentlichen (z. B. npm, PyPI)
- Breaking Changes durch ein Diff-Tool wie
oasdiffautomatisch erkennen und den Merge blockieren, falls keine neue Major-Version vergeben wurde
Ein minimaler Auszug einer solchen Spec für eine Instance-Ressource:
paths:
/v1/instances:
post:
summary: Instanz erstellen
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateInstanceRequest'
responses:
'202':
description: Operation gestartet
content:
application/json:
schema:
$ref: '#/components/schemas/Operation'
components:
schemas:
CreateInstanceRequest:
type: object
required: [name, region, size]
properties:
name:
type: string
region:
type: string
size:
type: string
enum: [small, medium, large]
tags:
type: object
additionalProperties:
type: string
Ressourcenmodell: Was eine CSP-API typischerweise abbildet
Die Kernressourcen unterscheiden sich je nach Angebot, aber ein typisches Set sieht so aus:
- Compute: Instances/VMs, Images, Snapshots, Autoscaling-Gruppen
- Storage: Block-Storage-Volumes, Object-Storage, Backups
- Networking: VPCs, Subnets, Security-Groups/Firewalls, Load-Balancer, Floating-/Failover-IPs
- IAM: Users, Rollen, API-Keys, Service-Accounts
- Billing: Usage-Metering, Kostenaufstellungen
- Monitoring: Metriken, Logs, Health-Checks
Jede dieser Ressourcen sollte konsistenten REST-Konventionen folgen: gleiche Pluralisierung, gleiche Antwortformate, gleiches Verhalten bei Filterung und Sortierung.
Konsistenz-Checkliste für Ressourcen
Bevor eine neue Ressource in die API aufgenommen wird, lohnt sich eine kurze Checkliste:
| Aspekt | Konvention |
|---|---|
| Namensgebung | Immer Plural: /instances, nicht /instance |
| IDs | Immer als eigenständiges Feld id, niemals im Pfad als einziger Identifikator ohne Ressourcen-Präfix |
| Zeitstempel | Immer created_at / updated_at in ISO-8601, UTC |
| Soft-Delete vs. Hard-Delete | Konsistent für alle Ressourcen gleich handhaben |
| Statusfeld | Einheitliches Enum-Namensschema über alle Ressourcen (pending, active, error, deleting) |
| Filter-Parameter | Gleiche Query-Parameter-Syntax überall (?status=active®ion=eu-central) |
Asynchrone Operationen von Anfang an einplanen
Das Erstellen einer VM oder das Ziehen eines Snapshots dauert Sekunden bis Minuten. Eine CSP-API sollte das nicht wie eine synchrone Operation behandeln:
POST /v1/instances
→ 202 Accepted
{
"operation_id": "op-8f2a1c",
"status": "pending"
}
Der Client pollt anschließend den Operations-Endpunkt:
GET /v1/operations/op-8f2a1c
→ { "status": "running" }
→ { "status": "done", "resource": { ... } }
Ergänzend dazu bieten Webhooks eine push-basierte Alternative, damit nicht jeder Client aktiv pollen muss. Wer es interaktiver braucht, kann zusätzlich Server-Sent Events oder WebSockets für Live-Status anbieten.
Operation-Objekt im Detail
Ein robustes Operation-Schema enthält mehr als nur den Status:
{
"operation_id": "op-8f2a1c",
"type": "instance.create",
"status": "running",
"progress_percent": 60,
"created_at": "2026-07-27T10:15:00Z",
"started_at": "2026-07-27T10:15:02Z",
"finished_at": null,
"target_resource": {
"type": "instance",
"id": "inst-a91b2"
},
"error": null
}
Wichtig ist außerdem eine klare Antwort auf die Frage: Was passiert bei einem Fehler mitten in der Operation? Zwei gängige Strategien:
- Automatischer Rollback: Bereits angelegte Teilressourcen werden automatisch wieder entfernt
- Fail-Forward mit manuellem Cleanup: Der Fehlerzustand bleibt sichtbar (
status: error), der Nutzer entscheidet über Retry oder Löschung
Für die meisten CSP-APIs ist Fail-Forward einfacher zu implementieren und transparenter für den Nutzer, da ein automatischer Rollback selbst wieder scheitern kann.
Webhooks richtig absichern
Wer Webhooks anbietet, sollte von Anfang an:
- Jede Payload mit einer HMAC-Signatur versehen (Header z. B.
X-Signature-SHA256), damit der Empfänger die Echtheit prüfen kann - Retry mit exponentiellem Backoff implementieren, falls der Empfänger nicht antwortet (z. B. 5 Versuche über 24 Stunden)
- Eine Event-ID mitschicken, damit Empfänger doppelte Zustellungen (at-least-once delivery) selbst deduplizieren können
Authentifizierung und Autorisierung
Für eine CSP-API haben sich zwei Modelle etabliert, die sich auch kombinieren lassen:
- API-Keys für maschinelle Zugriffe (CLI, CI/CD-Pipelines, Terraform), einfach zu handhaben, aber schwerer granular einzuschränken
- OAuth2 / OIDC mit kurzlebigen Access-Tokens und Refresh-Tokens für interaktive Nutzer und Drittanbieter-Integrationen
Für die Autorisierung innerhalb eines Kontos empfiehlt sich ein RBAC-Modell (Role-Based Access Control) mit möglichst feingranularen Permissions, zum Beispiel:
instances:read
instances:create
instances:delete
billing:read
iam:manage
Rollen wie viewer, operator, admin bündeln dann Sets dieser Permissions. Wichtig: Permissions sollten von Anfang an pro Ressourcen-Typ und Aktion granular genug sein. Ein nachträgliches Aufsplitten einer zu groben Permission ist sonst ein Breaking Change für alle bestehenden Rollenzuweisungen.
Zusätzlich lohnt sich früh die Einführung von Scoped API-Keys, die nur für bestimmte Ressourcen oder Projekte gültig sind, statt eines einzigen Account-weiten Keys mit vollen Rechten.
Idempotenz ist kein Nice-to-have
Netzwerkfehler passieren. Wenn ein Client einen POST /instances-Request wiederholt, weil die Antwort nicht ankam, darf das nicht zu zwei erstellten VMs führen. Die Lösung ist ein Idempotency-Key-Header, den der Client mitschickt:
POST /v1/instances
Idempotency-Key: 6f2a-91cd-... (vom Client generierte UUID)
Der Server speichert für jeden Key das Ergebnis des ersten Requests (typischerweise 24 Stunden) und gibt bei einem identischen Wiederholungsrequest dieselbe Antwort zurück, ohne die Operation erneut auszuführen. Wichtig ist dabei:
- Der Key ist an den Request-Body-Hash gekoppelt: Derselbe Key mit unterschiedlichem Body sollte einen
409 Conflictauslösen, nicht stillschweigend den alten Request “überschreiben” - Idempotenz gilt nur für nicht-idempotente Methoden (
POST), daPUT,GETundDELETEper HTTP-Semantik ohnehin idempotent sein sollten
Dieses Muster hat sich unter anderem bei Stripes API bewährt und lässt sich direkt übernehmen.
Pagination, Rate-Limiting und Versionierung
Pagination
Cursor-basiert ist robuster als Offset-basiert, besonders wenn sich Listen zwischen zwei Requests ändern können:
GET /v1/instances?limit=50&cursor=eyJpZCI6ImluczEyMyJ9
→ {
"data": [...],
"next_cursor": "eyJpZCI6ImluczE3NSJ9",
"has_more": true
}
Bei Offset-basierter Pagination kann ein zwischenzeitlich gelöschtes Element dazu führen, dass ein Eintrag in der Liste übersprungen oder doppelt angezeigt wird. Bei Cursor-basierter Pagination passiert das nicht, da der Cursor auf eine konkrete Position im (unveränderlichen) Sortierschlüssel zeigt.
Rate-Limiting
Pro Tenant/API-Key, mit transparenten Response-Headern:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 942
X-RateLimit-Reset: 1706353200
Ein bewährter Algorithmus dafür ist Token-Bucket (erlaubt kurze Bursts, glättet aber die durchschnittliche Rate) gegenüber striktem Fixed-Window, das an Fenstergrenzen zu unfairen Spitzen führen kann. Bei 429 Too Many Requests sollte immer ein Retry-After-Header mitgeschickt werden, damit sich Clients korrekt verhalten können.
Versionierung
Pfad-basiert (/v1/...) ist für öffentliche APIs am robustesten, da Clients die Version explizit sehen und steuern können. Zusätzlich empfiehlt sich:
- Ein klares Deprecation-Header-Schema (
Sunset: Sat, 31 Jan 2027 23:59:59 GMT, RFC 8594), sobald eine Version abgekündigt wird - Eine Mindest-Übergangsfrist (z. B. 12 Monate), die öffentlich kommuniziert wird, bevor eine Version tatsächlich abgeschaltet wird
- Additive Changes (neue optionale Felder, neue Endpunkte) erfordern keine neue Version, nur Breaking Changes tun das
Fehlerformate vereinheitlichen
Ein einheitliches Fehlerschema erleichtert es Client-Entwicklern enorm, Fehler programmatisch zu behandeln. RFC 9457 (Problem Details for HTTP APIs), der aktuelle Standard, der den älteren RFC 7807 ablöst, ist ein guter Standard dafür:
{
"type": "https://api.example.com/errors/quota-exceeded",
"title": "Quota exceeded",
"status": 429,
"detail": "Instance quota of 50 reached for this account.",
"instance": "/v1/instances",
"trace_id": "7f3e-9a21-..."
}
Das Feld trace_id verdient besondere Erwähnung: Es sollte in jeder Fehlerantwort enthalten sein und sich im Server-seitigen Logging/Tracing (z. B. OpenTelemetry) wiederfinden. Das verkürzt Support-Anfragen erheblich, da Kunde und Support-Team über dieselbe eindeutige ID sprechen können.
Multi-Tenancy von Anfang an mitdenken
Mandantentrennung lässt sich nur schwer nachträglich in ein bestehendes Datenmodell einziehen. Übliche Ansätze, von strikt bis flexibel:
- Separate Datenbank pro Tenant: Maximale Isolation, aber aufwendiger im Betrieb (Migrations müssen über alle Datenbanken laufen)
- Separates Schema pro Tenant innerhalb einer Datenbank: Guter Mittelweg
- Tenant-ID als Spalte in jeder Tabelle mit Row-Level-Security: Am einfachsten zu betreiben, erfordert aber diszipliniertes Enforcement in jeder einzelnen Query
Unabhängig vom gewählten Modell gilt: Die Tenant-Zuordnung sollte niemals ausschließlich in der Applikationslogik geprüft werden, sondern zusätzlich auf Datenbankebene abgesichert sein (z. B. PostgreSQL Row-Level-Security), damit ein Bug in der Applikation nicht zu einem Datenleck zwischen Kunden führt.
Testing-Strategie
Für eine API, auf die Kunden produktive Infrastruktur aufbauen, reicht klassisches Unit-Testing nicht aus:
- Contract-Tests: Prüfen, dass die tatsächliche Server-Antwort der OpenAPI-Spec entspricht (z. B. mit Dredd oder Schemathesis)
- Property-based/Fuzz-Testing gegen die Spec, um Edge Cases in der Validierung zu finden
- Idempotenz-Tests: Automatisiert denselben Request mit gleichem Idempotency-Key mehrfach senden und prüfen, dass nur eine Ressource entsteht
- Chaos-Testing für asynchrone Operationen: Was passiert, wenn der Server während einer laufenden Operation neu startet? Bleibt der Operation-Status konsistent?
Fazit
Eine CSP-API zu bauen bedeutet, von Anfang an für Langlebigkeit zu designen: asynchrone Operationen, Idempotenz, saubere Versionierung, granulare Autorisierung und ein konsistentes Fehlerformat sind keine Details, die man “später noch ergänzt”. Wer diese Grundlagen von Tag eins an einplant, spart sich schmerzhafte Breaking Changes, sobald die ersten Kunden produktiv auf der API arbeiten.
Kommentare
Zum Anzeigen und Schreiben von Kommentaren wird die selbst gehostete Software Comentario nachgeladen. Dabei wird ein technisch notwendiges Cookie gesetzt, siehe Datenschutzerklärung.