Aufbau einer anbieterunabhängigen Hosting-Plattform-Architektur

Aufbau einer anbieterunabhängigen Hosting-Plattform-Architektur

Seien wir ehrlich: Eine Hosting-Plattform, die nur mit einem einzigen Cloud-Anbieter funktioniert, ist eigentlich keine Plattform. Sie ist lediglich eine Art Wrapper. Und in den meisten Fällen treten die Probleme genau dann auf, wenn ein Kunde einen zweiten Anbieter integrieren möchte. Denn die Geschäftslogik, die unbemerkt an die API-Struktur eines bestimmten Anbieters gekoppelt wurde, zieht sich plötzlich durch die gesamte Codebasis.

Hier betrachten wir einen anderen Ansatz. Die Provisionierungslogik wird von den Infrastrukturintegrationen getrennt. Das bildet die Grundlage für eine anbieterunabhängige Architektur, die auf eine langfristige Entwicklung von Hosting-Plattformen ausgelegt ist.

In den folgenden Abschnitten behandeln wir die Control Plane, ein kanonisches Ressourcenmodell, Provider-Adapter, asynchrone Provisionierung, Fehlerbehandlung und die Normalisierung der Abrechnung. Wo es sinnvoll ist, verwenden wir konkrete Codebeispiele. In anderen Fällen setzen wir auf Diagramme, da die architektonischen Abwägungen wichtiger sind als die Syntax.

Control Plane und Provider-Infrastruktur

Eine Cloud-Hosting-Plattform lässt sich in zwei Architekturebenen unterteilen. Die eine ist die sogenannte Control Plane. Sie verwaltet Kundenanforderungen, Autorisierung, Ressourcenstatus und Orchestrierung. Die andere ist die Provider-Infrastruktur: die eigentlichen VMs, Container, Storage-Volumes und Netzwerke, die von einem Hosting-Anbieter betrieben werden.

Entscheidend ist eine klare Trennung der Verantwortlichkeiten. Das bedeutet, dass die Control Plane keine providerspezifischen Details kennen sollte – beispielsweise, dass AWS eine virtuelle Maschine als „Instance“ bezeichnet, während OpenStack von einem „Server“ spricht, oder dass ein Anbieter sekundengenau und ein anderer stundenweise abrechnet. All diese Details gehören ausschließlich in die Provider-Ebene.

Diese Regel ist den meisten Teams bekannt. In der Praxis verschwimmen die Grenzen jedoch immer wieder. Ein Team benötigt beispielsweise einen schnellen Fix für einen bestimmten Anbieter und integriert ihn deshalb in einen gemeinsam genutzten Service. Solange es nur einen Anbieter gibt, funktioniert das. Sechs Monate später lässt sich derselbe Service jedoch nicht für einen zweiten Anbieter verwenden, ohne ihn umzuschreiben.

Die Plattform von Chudovo setzt diese Trennung bereits auf Interface-Ebene durch. Die Control Plane kommuniziert ausschließlich über Adapter mit den Providern und niemals direkt.

Mandantengrenzen und Isolierung von Zugangsdaten

Die Control Plane definiert außerdem die Sicherheitsgrenze zwischen Kunden und Providern. Mandantenidentität, Quoten, Rollen und Autorisierungsrichtlinien sollten innerhalb der Plattform verwaltet werden, anstatt sie uneinheitlich an die jeweiligen Infrastruktur-APIs zu delegieren. Jede Provisionierungsanfrage benötigt einen Mandantenkontext, der sie durch die Job Queue, den Adapter-Aufruf und das Audit-Log begleitet.

Für die Zugangsdaten der Provider gilt dagegen ein anderer Ansatz. Sie sollten vom Adapter nur bei Bedarf aus einem Secrets Manager abgerufen werden und außerhalb der kundenorientierten Services bleiben. In einer produktiven Architektur werden üblicherweise separate Zugangsdaten je Provider-Konto, Region oder Mandantengrenze verwendet – jeweils mit den minimalen Berechtigungen, die für die Provisionierung erforderlich sind. Dieses Muster begrenzt mögliche Schäden bei kompromittierten Zugangsdaten und ermöglicht deren Rotation, ohne das kanonische Ressourcenmodell ändern zu müssen.

Ein kanonisches Ressourcenmodell

Jede Control Plane, die anbieterunabhängig bleiben soll, benötigt ihr eigenes Vokabular: ein neutrales Ressourcenmodell, das von jedem Adapter in das jeweilige providerspezifische Modell und wieder zurück übersetzt wird.

Hier ist eine vereinfachte Version dieses Modells:

Kanonische Ressource Kernattribute Beispiel für Provider-Mapping
Compute-Instanz CPU, Arbeitsspeicher, Image, Netzwerkschnittstellen AWS-EC2-Instanz, OpenStack-Nova-Server oder virtuelle KVM-Maschine
Block Storage Größe, IOPS-Tier, Anbindungsstatus AWS-EBS-Volume, Ceph-RBD-Image
Virtuelles Netzwerk CIDR-Block, Subnetze, Routing-Regeln AWS VPC, OpenStack-Neutron-Netzwerk
Object Storage Bucket, Region, Zugriffsrichtlinie AWS-S3-Bucket, MinIO-Bucket

Die obige Tabelle erhebt keinen Anspruch auf Vollständigkeit, da reale Systeme Dutzende weitere Attribute verwalten. Entscheidend ist vielmehr, dass jedes Feld einer konkreten Eigenschaft bei mindestens einem Provider zugeordnet werden kann. Das Modell geht nicht davon aus, dass jeder Provider dieselben Funktionen unterstützt. Einige Provider bieten beispielsweise IOPS-Tiers an, andere nicht. Das Modell muss solche Unterschiede abbilden können.

Provider-Adapter und Infrastruktur-APIs

Die Aufgabe eines Adapters besteht darin, in beide Richtungen zu übersetzen:

  • Inbound: Er nimmt eine Anfrage für eine kanonische Ressource entgegen und übersetzt sie in providerspezifische API-Aufrufe.
  • Outbound: Er verarbeitet die Antworten und Statuscodes des Providers und überführt sie zurück in einen kanonischen Status.

Ein minimales Adapter-Interface könnte beispielsweise so aussehen:

interface ProviderAdapter {
  createInstance(
    spec: CanonicalInstanceSpec,
    context: OperationContext
  ): Promise<ProviderOperationRef>;

  getOperationStatus(
    ref: ProviderOperationRef
  ): Promise<CanonicalOperationStatus>;

  getInstanceStatus(
    ref: ProviderInstanceRef
  ): Promise<CanonicalStatus>;

  deleteInstance(
    ref: ProviderInstanceRef,
    context: OperationContext
  ): Promise<ProviderOperationRef>;

  listCapabilities(): Promise<CapabilitySet>;
}

Das Team von Chudovo hält die Adapter bewusst schlank. Geschäftsregeln, die festlegen, wann Ressourcen provisioniert, Vorgänge wiederholt oder Kapazitäten skaliert werden sollen, gehören nicht in den Adapter, sondern in die Control Plane.

Entscheidend ist Folgendes: Der Adapter muss lediglich wissen, wie er mit der API eines bestimmten Providers kommuniziert und dessen Status korrekt zurückmeldet. Eine Provider-API kann beispielsweise bereits einen Erfolgscode zurückgeben, obwohl die Ressource noch nicht vollständig bereit ist. Der Adapter gibt in diesem Fall den tatsächlichen Provider-Status unverändert weiter. Die Control Plane entscheidet anschließend, was „ready“ tatsächlich bedeutet.

Unterschiede bei den Provider-Funktionen

Tatsache ist, dass sich Provider in ihren Funktionen deutlich unterscheiden. Live-Migration, regionale GPU-Verfügbarkeit oder Storage-Tiers – all diese Funktionen lassen sich nicht einheitlich über verschiedene Anbieter hinweg abbilden. Eine Multi-Provider-Hosting-Plattform kann diese Unterschiede nicht einfach ignorieren. Sie muss sie transparent darstellen.

Die Adapter von Chudovo stellen dafür ein Capability Set bereit: eine strukturierte Liste der Funktionen, die eine bestimmte Provider-Integration tatsächlich unterstützt. Die Control Plane prüft diese Funktionen, bevor sie eine Provisionierungsanfrage akzeptiert – nicht erst danach. Ziel ist es, die von der Plattform akzeptierten Konfigurationen entsprechend einzuschränken. Fordert ein Kunde beispielsweise eine umfangreiche Konfiguration bei einem Provider mit begrenztem Funktionsumfang an, erhält er eine eindeutige Ablehnung statt eines stillen Fehlers. Für die meisten Kunden ist das die bessere Lösung, denn stille Fehler führen später zu zusätzlichen Supportanfragen.

Contract Testing für Provider-Adapter

Ein gemeinsames Interface ist nur dann sinnvoll, wenn alle Adapter es einheitlich interpretieren. Das Team von Chudovo überprüft dies, indem alle Adapter mit derselben Contract-Test-Suite getestet werden. Typischerweise wird dabei eine Ressource erstellt, ihre Statusübergänge werden überwacht, anschließend wird sie abgerufen und gelöscht. Außerdem wird geprüft, ob wiederholte Operationen weiterhin sicher ausgeführt werden können. Providerspezifische Tests decken zusätzlich Verhaltensweisen wie Throttling, nicht verfügbare Regionen, verzögerte Statusaktualisierungen und nicht unterstützte Funktionen ab.

Für solche Tests werden Provider-Sandboxes oder isolierte Accounts benötigt. Mock-APIs sind für deterministische Fehlerszenarien hilfreich, können jedoch nicht jedes Verhalten eines realen Providers reproduzieren. Da sich API-Antwortformate ändern können, Berechtigungen im Laufe der Zeit abweichen und dokumentierte Lifecycle-Status nicht immer dem tatsächlichen Verhalten in der Produktion entsprechen, hilft eine kleinere Anzahl regelmäßig ausgeführter Integrationstests dabei, solche Abweichungen zu erkennen, bevor Kunden davon betroffen sind.

Ein neuer Adapter ist nicht allein deshalb vollständig, weil er das Interface implementiert. Er muss dieselben Lifecycle-Garantien erfüllen wie die bereits vorhandenen Provider-Adapter.

Asynchrone Provisionierung und Statusabgleich

Provisionierung erfolgt nicht sofort. Eine VM benötigt möglicherweise fünfzehn Sekunden zum Starten, während die Konvergenz eines Netzwerks noch länger dauern kann. Ein synchrones „Create-and-Wait“-Modell funktioniert zwar für Demos, stößt unter realer Last jedoch schnell an seine Grenzen. Besonders deutlich wird dies, wenn die API eines Providers langsam reagiert oder während einer Anfrage ein Timeout auftritt.

In der Praxis sollte Provisionierung daher standardmäßig asynchron behandelt werden. Das bedeutet:

  • Eine Anfrage erstellt einen Job.
  • Der Job verfolgt die Statusübergänge: requested, pending, provisioning, ready oder failed.
  • Eine Reconciliation-Schleife im Hintergrund fragt regelmäßig den Provider-Status ab und aktualisiert den kanonischen Datensatz.

Die schwierigsten Fehler in einem solchen System treten häufig während des Statusabgleichs und nicht bei der ursprünglichen Anfrage auf. Der Status beim Provider stimmt nicht immer mit dem internen Datensatz der Plattform überein. Eine VM kann beispielsweise beim Provider existieren, aber in der Datenbank der Control Plane fehlen, weil bei einer vorherigen Anfrage ein Timeout aufgetreten ist, bevor die Antwort einging.

Erkennt die Reconciliation-Schleife eine solche Abweichung, wendet sie eine klar definierte Strategie an: Monitoring fortsetzen, die Ressource übernehmen, die Operation erneut ausführen, sie zur Überprüfung markieren oder die Ressource sicher entfernen. Dabei sollte niemals automatisch davon ausgegangen werden, dass entweder der gespeicherte Status oder der Provider-Status grundsätzlich maßgeblich ist.

Fehlerbehandlung, Wiederholungsversuche und Idempotenz

Eine gut konzipierte, anbieterunabhängige Control Plane muss Fehler verarbeiten können, ohne dabei den Ressourcenstatus zu beschädigen. Zwei Techniken sind für den Umgang mit solchen Szenarien besonders wichtig:

  • Wiederholungsversuche mit Backoff
  • Idempotenzschlüssel

Ein häufig verwendeter Ansatz besteht darin, für jede Provisionierungsanfrage einen vom Client generierten Idempotenzschlüssel mitzusenden. Kommt es bei einer Anfrage zu einem Timeout und die Control Plane startet einen erneuten Versuch, übergibt der Provider-Adapter denselben Schlüssel. Provider, die idempotente Erstellungsvorgänge unterstützen, erstellen dadurch keine doppelte Ressource.

const sleep = (milliseconds: number): Promise<void> =>
  new Promise(resolve => setTimeout(resolve, milliseconds));

async function createInstanceWithRetry(
  adapter: ProviderAdapter,
  spec: CanonicalInstanceSpec,
  idempotencyKey: string,
  maxAttempts = 3
): Promise<ProviderOperationRef> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await adapter.createInstance(spec, {
        idempotencyKey
      });
    } catch (error) {
      if (!(error instanceof ProviderTimeoutError)) {
        throw error;
      }

      if (attempt === maxAttempts - 1) {
        throw error;
      }

      const exponentialDelay = 2 ** attempt * 1000;
      const jitter = Math.random() * 1000;

      await sleep(exponentialDelay + jitter);
    }
  }

  throw new Error("Maximum retry attempts reached");
}

Einige Provider unterstützen Idempotenzschlüssel möglicherweise nicht nativ. In solchen Fällen muss die Control Plane selbst dafür sorgen, dass keine Race Condition entsteht. Dazu fügt sie unmittelbar vor dem Aufruf des Adapters einen Eintrag in ihre eigene Operationstabelle ein. Der Idempotenzschlüssel dient dabei als Unique Constraint. Bei diesem Modell übernimmt derjenige parallele Versuch die Operation, dem es zuerst gelingt, diesen Eintrag zu erstellen, und führt anschließend den Provider-Aufruf aus. Jeder weitere Versuch mit demselben Schlüssel schlägt beim Einfügen fehl, wartet entsprechend der Backoff-Strategie und fragt stattdessen den bestehenden Eintrag ab, anstatt den Provider erneut aufzurufen.

async function reserveOperation(
  db: DatabaseClient,
  idempotencyKey: string
): Promise<boolean> {
  try {
    await db.insert("provisioning_operations", {
      idempotencyKey,
      status: "in_progress"
    });

    return true; // this attempt owns the operation
  } catch (error) {
    if (isUniqueConstraintViolation(error)) {
      return false; // another attempt already claimed it
    }

    throw error;
  }
}

Observability über Provider-Grenzen hinweg

Debugging kann schnell schwierig werden, wenn jede Komponente eine Anfrage auf unterschiedliche Weise protokolliert. Eine gängige Strategie besteht darin, eine Korrelations-ID zu verwenden, die von der Plattform durchgängig weitergegeben wird – von der eingehenden API-Anfrage über den Provisionierungsjob, den Adapter und die Provider-Operation bis hin zum Reconciliation-Zyklus. Auf diese Weise lässt sich der gesamte Lifecycle anhand von Logs und Traces nachvollziehen, ohne sich ausschließlich auf Zeitstempel verlassen zu müssen.

Auch der Provider-Kontext ist für aussagekräftige Metriken wichtig. Um Kennzahlen wie Provisionierungslatenz, Fehlerrate, Throttling-Antworten, Reconciliation-Abweichungen und die Anzahl verwaister Ressourcen besser auszuwerten, empfiehlt sich eine Segmentierung nach Provider, Region, Ressourcentyp und Operation. Dadurch lässt sich leichter unterscheiden, ob ein Problem die gesamte Plattform betrifft oder lediglich ein einzelner Provider-Endpunkt beeinträchtigt ist.

Ein letzter Punkt zu Alerts: Sie sollten sich vor allem auf Auswirkungen für Kunden und festgefahrene Statusübergänge konzentrieren – nicht auf jeden temporären API-Fehler, den die Retry-Strategie automatisch beheben kann.

Providerspezifische Verbrauchserfassung und Abrechnungsnormalisierung

Provider erfassen die Nutzung unterschiedlich: sekundengenaue oder stündliche Abrechnung, gestaffelte Storage-Preise oder Egress-Gebühren, die erst ab einem bestimmten Schwellenwert anfallen. Wenn sekundengenaue AWS-Preise und stündliche Private-Cloud-Preise auf derselben Rechnung erscheinen, wird die Abrechnung unnötig kompliziert. Kunden sollten diese Unterschiede nicht selbst abgleichen müssen.

Die Hauptrechnung sollte daher normalisierte Einheiten darstellen, während providerspezifische Daten für Audit-Zwecke erhalten bleiben. Eine Billing-Normalisierungsschicht wandelt die Nutzungsdaten der einzelnen Provider in der Regel in ein kanonisches Billing Event um, das Ressourcentyp, Dauer, Menge und Kosten enthält. Dadurch erhalten Kunden ein einheitliches Rechnungsformat – unabhängig davon, welcher Provider ihre Workloads tatsächlich ausführt.

Einen neuen Provider hinzufügen, ohne die Kernlogik zu ändern

Ob dieses Architekturkonzept tatsächlich funktioniert, lässt sich anhand einer einfachen Frage überprüfen: Kann ein neuer Provider hinzugefügt werden, ohne den Code der Control Plane zu ändern?

Auf der Plattform von Chudovo bedeutet das Hinzufügen eines neuen Providers, einen neuen Adapter zu entwickeln, der das Standard-Interface implementiert. Dazu gehören die Definition des Capability Sets des Providers und die Zuordnung seiner Ressourcentypen zum kanonischen Modell.

Idealerweise bleibt der zentrale Orchestrierungsworkflow unverändert. Der Großteil der Arbeit beschränkt sich auf den neuen Adapter, Capability-Definitionen, Lifecycle-Mappings, Metering-Regeln, Zugangsdaten und Integrationstests. In einzelnen Fällen kann eine tatsächlich neue Provider-Funktion eine rückwärtskompatible Erweiterung des kanonischen Modells erforderlich machen.

Vendor Lock-in vermeiden, ohne alle Provider als identisch zu behandeln

Bei manchen Plattformen existiert die Abstraktion nur auf dem Papier. Sie werben zwar mit Multi-Provider-Unterstützung, tatsächlich sind jedoch die Annahmen eines bestimmten Providers an zahlreichen Stellen fest im Code verankert.

Bei Chudovo leisten ein Private-Cloud-Adapter und ein Public-Cloud-Adapter intern nicht denselben Arbeitsumfang – und das sollen sie auch nicht. Der gemeinsame Vertrag bleibt zwar gleich, die darunterliegende Implementierung berücksichtigt jedoch die tatsächlichen Unterschiede.

Vendor Lock-in wird nicht dadurch vermieden, dass alle Provider als austauschbar behandelt werden. Entscheidend ist vielmehr, die tatsächlichen Unterschiede im Architekturmodell abzubilden und dadurch die Kosten und den Aufwand eines Provider-Wechsels möglichst gering zu halten.

Architekturübersicht

Anbieterunabhängige Hosting-Plattform-Architektur

Die Portale bilden die oberste Ebene. Alle Anfragen durchlaufen zunächst die Control Plane und das kanonische Modell, bevor sie einen bestimmten Provider erreichen. Jeder Adapter ist dabei die einzige Codekomponente, die die tatsächliche API des jeweiligen Providers kennt.

Diese Hosting-Plattform-Architektur hält providerspezifische Abhängigkeiten unterhalb des kanonischen Modells. Die Portale befinden sich auf der obersten Ebene, und jede Provisionierungsanfrage durchläuft zunächst die Control Plane, bevor sie einen Adapter erreicht.

Fazit

In den vorherigen Abschnitten haben wir die Architektur einer Hosting-Plattform mit Multi-Provider-Unterstützung dargestellt. Die Kosten für die Unterstützung mehrerer Infrastruktur-Provider verschwinden dadurch zwar nicht, werden jedoch gezielt in die Adapter und ein Capability-Modell verlagert, anstatt sich in Form von Bedingungen über sämtliche Services zu verteilen.

Für die Unterstützung eines einzigen Providers mag dieser Ansatz zunächst überdimensioniert erscheinen. Die Integration jedes weiteren Providers wird dadurch jedoch deutlich einfacher. Anders gesagt: Den vierten Provider hinzuzufügen ist einfacher als zuvor den zweiten.

Genau darin liegt der eigentliche Vorteil dieser Architektur – und er zeigt sich nicht am ersten Tag, sondern erst im Laufe der Zeit.