Multi-Account AWS-Architektur: Event-Driven Systeme für 10-faches Traffic-Wachstum

Lerne Multi-Account AWS-Architektur durch "QuickGrocer's" Migration von Single-Account-Monolith zu Event-Driven Systemen. Entdecke Patterns, Herausforderungen und Architekturentscheidungen.

Der Tag, an dem sich alles änderte#

  1. März 2020. Ich starrte auf mein Handy und beobachtete, wie unser CloudWatch-Dashboard von grün auf tiefrot umschlug. Unser Lebensmittel-Lieferservice "QuickGrocer" war gerade von 50.000 Usern auf 500.000 Login-Versuche in einer einzigen Stunde explodiert. Die Pandemie war da, Supermarktregale waren leer, und plötzlich entdeckten alle gleichzeitig Online-Lebensmitteleinkauf.

Unser Single-AWS-Account-Monolith – eine "temporäre" Architektur, die wir seit achtzehn Monaten reparieren wollten – schmolz schneller als Eis in der Phoenix-Sonne. Die RDS-Instanz schrie, Lambda Concurrent Executions schlugen gegen Account-Limits, und unsere einzige Deployment-Pipeline hatte siebzehn Teams, die darauf warteten, Notfall-Fixes zu pushen.

Das ist die Geschichte, wie wir das Flugzeug während des Fluges umbauten – der Übergang vom Chaos in einem einzigen AWS-Account zu einer Multi-Account, Event-Driven Architektur, die tatsächlich damit klarkam, während des Lockdowns jedermanns Lebensader zu sein.

Das Problem: Ein Account, um sie alle zu beherrschen (und spektakulär zu scheitern)#

Stell dir vor: neun Entwicklungsteams, alle deployen in denselben AWS-Account. Es ist wie neun Familien in einem Studio-Apartment – theoretisch möglich, praktisch verrückt. So sahen unsere CloudFormation-Stacks vor der Migration aus:

YAML
# Das "alles-in-einem-Account" Anti-Pattern
Resources:
  CustomerWebLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: quickgrocer-customer-web-api
      # Hoffentlich braucht niemand anders diese Rolle...
      Role: !GetAtt SharedLambdaRole.Arn

  DriverAppLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: quickgrocer-driver-app-api
      # Gleiche Rolle, weil wer hat Zeit für Least Privilege?
      Role: !GetAtt SharedLambdaRole.Arn

  OrderProcessingLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: quickgrocer-order-processing
      # Du hast es erraten...
      Role: !GetAtt SharedLambdaRole.Arn

  SharedLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        # Jeder bekommt alles!
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        # Die "wir-fixen-es-später" Policy
        - arn:aws:iam::aws:policy/PowerUserAccess

Die Probleme kaskadierten wie Dominosteine:

  1. Blast Radius: Als das Payment-Processing-Team versehentlich eine Production DynamoDB-Tabelle löschte (ja, wirklich), ging die Customer Web App down, weil sie denselben Account teilten
  2. Permission-Chaos: IAM-Policies waren auf 6.000+ Zeilen JSON angewachsen, die niemand vollständig verstand
  3. Rechnungs-Mysterium: "Warum haben wir letzten Monat $47.000 für Lambda ausgegeben?" Gute Frage. Neun Teams, eine Rechnung, null Verantwortlichkeit
  4. Deployment-Stau: Unsere CI/CD-Pipeline wurde zum Flaschenhals mit Teams, die buchstäblich Deployments über Slack planten

Die Architektur, die uns rettete (schließlich)#

Denk an Multi-Account-Architektur wie den Umzug von diesem Studio-Apartment in ein ordentliches Apartmentgebäude. Jedes Team bekommt seine eigene Wohnung (AWS-Account), aber sie teilen gemeinsame Versorgungseinrichtungen (zentralisierte Services) und können über ordentliche Kanäle (EventBridge) kommunizieren.

Das haben wir gebaut:

Loading diagram...

Der zentrale Identity Service: Unsere Vertrauensgrenze#

Jede Multi-Account-Architektur braucht eine einzige Quelle der Wahrheit für Authentifizierung und Autorisierung. Wir haben unseren Identity Service als den einzelnen Punkt gebaut, an dem alle Services Tokens und Berechtigungen validieren. Hier ist die tatsächliche Trust Policy, die wir verwenden:

JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowIdentityServiceToAssumeRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::000000000000:role/identity-service-validator"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "${IDENTITY_SERVICE_EXTERNAL_ID}",
          "aws:PrincipalOrgID": "o-quickgrocer123"
        },
        "IpAddress": {
          "aws:SourceIp": [
            "10.0.0.0/8"  // VPC CIDR-Bereich
          ]
        }
      }
    }
  ]
}

Der Identity Service wurde unser Türsteher – jede Anfrage von kundenseitigen Services musste ihren Ausweis an der Tür zeigen. Diese Zentralisierung ersparte uns den JWT-Validierungs-Albtraum, dem wir sonst gegenübergestanden hätten.

EventBridge: Das Nervensystem#

Anstatt dass Services sich direkt gegenseitig aufrufen (und einen Abhängigkeits-Albtraum schaffen), haben wir EventBridge als unser Kommunikations-Rückgrat verwendet. Jeder Account veröffentlicht Events an einen zentralen Event Bus, der sie dann an interessierte Abonnenten weiterleitet.

Hier ist eine echte EventBridge-Regel aus unserem Order-Processing-Flow:

TypeScript
// CDK-Code für Cross-Account Event Routing
import { Rule, EventBus } from 'aws-cdk-lib/aws-events';
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';

const orderPlacedRule = new Rule(this, 'OrderPlacedRule', {
  eventBus: EventBus.fromEventBusArn(
    this,
    'CentralEventBus',
    'arn:aws:events:us-east-1:121212121212:event-bus/central-bus'
  ),
  eventPattern: {
    source: ['quickgrocer.customer-web'],
    detailType: ['Order Placed'],
    detail: {
      orderStatus: ['PENDING'],
      paymentMethod: ['CREDIT_CARD', 'DEBIT_CARD', 'APPLE_PAY']
    }
  },
  targets: [
    new LambdaFunction(orderProcessingLambda, {
      retryAttempts: 2,
      deadLetterQueue: orderProcessingDLQ,
      maxEventAge: Duration.hours(2)
    })
  ]
});

// Berechtigungen für Cross-Account Event Publishing
const centralBusArn = 'arn:aws:events:us-east-1:121212121212:event-bus/central-bus';
const publishPolicy = new PolicyStatement({
  effect: Effect.ALLOW,
  actions: ['events:PutEvents'],
  resources: [centralBusArn],
  conditions: {
    StringEquals: {
      'events:detail-type': [
        'Order Placed',
        'Order Updated',
        'Order Cancelled'
      ]
    }
  }
});

Account-Struktur und Isolation#

Jedes Team bekam seinen eigenen Sandkasten zum Spielen, mit klaren Grenzen:

Bash
# Unsere Account-Struktur
quickgrocer-org/
├── production/
   ├── customer-facing/
   ├── customer-web-111111111111/
   ├── mobile-apps-222222222222/
   ├── partner-portal-333333333333/
   ├── driver-app-444444444444/
   └── merchant-dashboard-555555555555/
   ├── core-services/
   ├── inventory-mgmt-666666666666/
   ├── order-processing-777777777777/
   ├── delivery-orchestration-888888888888/
   └── payment-service-999999999999/
   └── shared-services/
       ├── identity-service-000000000000/
       ├── event-bus-121212121212/
       └── monitoring-131313131313/
├── staging/
   └── [spiegelt Production-Struktur]
└── development/
    └── [ein Account pro Dev-Team]

Was tatsächlich funktionierte (Die guten Teile)#

1. Team-Autonomie#

Teams konnten endlich in ihrem eigenen Tempo arbeiten. Das Mobile-Team rollte fünf Updates während Black Friday aus, während das Order-Processing-Team in einem Code-Freeze war. Keine "bitte nicht deployen, wir debuggen Production" Nachrichten mehr in Slack.

2. Blast Radius Containment#

Erinnerst du dich an den DynamoDB-Lösch-Vorfall? In der neuen Architektur, als das Delivery-Orchestration-Team versehentlich 100.000 doppelte Events verarbeitete (eine Geschichte für einen anderen Tag), traf nur ihr Account das Lambda Concurrent Execution Limit. Kundenbestellungen flossen weiter.

3. Klare Kostenzuordnung#

Unsere CFO weinte fast vor Freude, als sie endlich genau sehen konnte, welches Team unsere AWS-Credits verbrannte:

Python
# Unsere Kostenzuordnungs-Tags
def apply_cost_tags(resource, team_name, service_name):
    return {
        'Team': team_name,
        'Service': service_name,
        'Environment': os.environ['ENVIRONMENT'],
        'CostCenter': TEAM_COST_CENTERS[team_name],
        'Owner': TEAM_LEADS[team_name],
        'CreatedDate': datetime.now().isoformat(),
        'ManagedBy': 'CDK'
    }

# Monatliche Kostenaufschlüsselung wurde kristallklar:
# Customer Web:         $12,450 (25%)
# Mobile Apps:          $8,230  (17%)
# Order Processing:     $15,670 (32%)
# Delivery Orchestration: $7,890 (16%)
# Identity Service:     $4,760  (10%)

4. Sicherheitsgrenzen, die Sinn machten#

Jeder Account hatte seinen eigenen Sicherheitsperimeter. Das Payment-Service-Team konnte PCI-Compliance-Kontrollen aktivieren, ohne das gesamte Unternehmen in Compliance-Theater zu zwingen:

TypeScript
// Payment Service Account Security Baseline
const paymentServiceBaseline = new SecurityHub(this, 'PCICompliance', {
  standards: [
    SecurityHubStandard.PCI_DSS_V321,
    SecurityHubStandard.AWS_FOUNDATIONAL_SECURITY
  ],
  enabledRegions: ['us-east-1', 'us-west-2'],
  // Nur für Payment Service Account
  accountId: '999999999999'
});

Was nicht funktionierte (Die schmerzhaften Lektionen)#

1. Event Schema Evolution Hölle#

Wir lernten auf die harte Tour, dass das Ändern von Event-Schemas in einem verteilten System wie das Wechseln des Motors eines Autos während der Fahrt auf der Autobahn ist. Unser "Order Placed" Event begann einfach:

JSON
// Version 1 (März 2020)
{
  "orderId": "ord-123",
  "customerId": "cust-456",
  "items": ["item-1", "item-2"],
  "total": 45.99
}

Bis Dezember 2020, nach mehreren "schnellen Fixes":

JSON
// Version 7 (Dezember 2020)
{
  "orderId": "ord-123",
  "customerId": "cust-456",
  "customerIdV2": "usr_cust-456",  // Neues ID-Format
  "items": ["item-1", "item-2"],   // Deprecated, verwende itemsV2
  "itemsV2": [
    {
      "id": "item-1",
      "quantity": 2,
      "price": 12.99,
      "modifiers": []  // Hinzugefügt in v4
    }
  ],
  "total": 45.99,            // Deprecated in v5
  "totalAmount": {            // Hinzugefügt in v5
    "value": 45.99,
    "currency": "USD"
  },
  "metadata": {               // Hinzugefügt in v6
    "source": "mobile-app",
    "version": "2.3.1"
  }
}

Der Versionierungs-Albtraum führte zu dieser Monstrosität in unseren Consumern:

TypeScript
// Der "wir hätten Schema Registry verwenden sollen" Handler
export const handleOrderPlaced = async (event: any) => {
  // Check welche Version wir haben
  const version = event.metadata?.schemaVersion ||
                  (event.customerIdV2 ? 7 :
                   event.totalAmount ? 5 :
                   event.items?.[0]?.modifiers ? 4 : 1);

  switch(version) {
    case 1:
    case 2:
    case 3:
      return handleLegacyOrder(event);
    case 4:
      return handleV4Order(migrateV4ToV7(event));
    case 5:
    case 6:
      return handleV5Order(migrateV5ToV7(event));
    case 7:
      return handleCurrentOrder(event);
    default:
      // Loggen und beten
      console.error('Unbekannte Order-Version:', event);
      throw new Error('Unbekannte Schema-Version');
  }
};

2. Cross-Account Debugging Albtraum#

Eine Anfrage über neun AWS-Accounts zu verfolgen ist wie einer einzelnen Ameise in einem Ameisenhaufen zu folgen. Unser 3-Uhr-morgens Production-Vorfall, der alles änderte:

Die Vorfall-Timeline:

  • 3:17 Uhr: PagerDuty Alert - "Order Processing Latenz > 30 Sekunden"
  • 3:23 Uhr: Check Order Processing Account - alles sieht gut aus
  • 3:35 Uhr: Check Customer Web Account - normal
  • 3:47 Uhr: Check Inventory Service - normal
  • 4:15 Uhr: Endlich gefunden - Delivery Orchestration Account hatte eine Lambda-Funktion in unendlicher Retry-Schleife
  • 4:45 Uhr: Root Cause - EventBridge-Regel im zentralen Account leitete Events wegen eines Tippfehlers im Event-Pattern an das falsche Target
  • 5:30 Uhr: Gefixt und deployed
  • 6:00 Uhr: Post-Mortem geplant
  • Nächster Tag: Stark in Distributed Tracing investiert

Der Fix war überall OpenTelemetry:

TypeScript
// Distributed Tracing rettete unsere geistige Gesundheit
import { trace, context, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('quickgrocer-order-service', '1.0.0');

export const processOrder = async (event: any) => {
  // Trace Context aus EventBridge Event extrahieren
  const traceParent = event.detail?.traceContext?.traceparent;
  const traceState = event.detail?.traceContext?.tracestate;

  // Trace vom Upstream-Service fortsetzen
  const extractedContext = propagation.extract(context.active(), {
    traceparent: traceParent,
    tracestate: traceState
  });

  return context.with(extractedContext, () => {
    const span = tracer.startSpan('process-order', {
      attributes: {
        'order.id': event.detail.orderId,
        'order.account': process.env.AWS_ACCOUNT_ID,
        'order.region': process.env.AWS_REGION,
        'order.service': 'order-processing'
      }
    });

    try {
      // Bestellung verarbeiten
      const result = await actuallyProcessOrder(event);
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (error) {
      span.recordException(error);
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error.message
      });
      throw error;
    } finally {
      span.end();
    }
  });
};

3. Kostenexplosion#

Multi-Account bedeutet nicht Multi-Budget, aber jemand vergaß das unserer AWS-Rechnung zu sagen. Cross-Account Data Transfer, EventBridge-Kosten und doppelte Ressourcen summierten sich:

Bash
# Die "warum ist unsere Rechnung so hoch" Aufschlüsselung (Dezember 2020)
EventBridge Events:           $3,450/Monat  # 345 Millionen Events
Cross-AZ Data Transfer:       $2,100/Monat  # Hätten Events regional halten sollen
NAT Gateway (9 Accounts):     $3,215/Monat  # $35 pro Account
CloudWatch Logs:              $4,500/Monat  # Jeder loggte alles
Secrets Manager:              $1,800/Monat  # Secrets überall repliziert
Parameter Store API Calls:    $890/Monat    # Kein Caching = API Limit Hits

Gesamt unerwartete Kosten:   $13,955/Monat

Unser Kostenoptimierungs-Sprint:

TypeScript
// Vorher: Jeder Service holt Secrets bei jeder Anfrage
const getSecret = async (secretName: string) => {
  const client = new SecretsManagerClient({});
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: secretName })
  );
  return response.SecretString;
};

// Nachher: Caching mit TTL
class SecretCache {
  private cache = new Map<string, {value: string, expiry: number}>();
  private ttl = 3600000; // 1 Stunde

  async getSecret(secretName: string): Promise<string> {
    const cached = this.cache.get(secretName);
    if (cached && cached.expiry > Date.now()) {
      return cached.value;
    }

    const client = new SecretsManagerClient({});
    const response = await client.send(
      new GetSecretValueCommand({ SecretId: secretName })
    );

    this.cache.set(secretName, {
      value: response.SecretString!,
      expiry: Date.now() + this.ttl
    });

    return response.SecretString!;
  }
}

// Reduzierte Secrets Manager Kosten um 94%

Der 3-Uhr-morgens Vorfall, der alles änderte#

Lass mich dir von dem Vorfall erzählen, der uns dazu brachte, unseren Ansatz zu Multi-Account-Operationen komplett zu überdenken.

Es war der 23. Dezember 2020 - zwei Tage vor Weihnachten. Peak-Liefersaison. Um 3:14 Uhr explodierte mein Telefon mit Alerts. Nicht nur ein oder zwei, sondern was wir später als 1.247 PagerDuty-Benachrichtigungen in 3 Minuten zählten. Jeder einzelne Service alarmierte.

Die Symptome waren bizarr:

  • Order Processing: funktionierte intern gut, aber keine neuen Bestellungen kamen an
  • Delivery Orchestration: schrie über fehlende Inventory-Updates
  • Payment Service: verarbeitete Zahlungen, erhielt aber keine Bestätigungen
  • Customer Web: User konnten browsen, aber nicht auschecken

Unser Event Bus war verstummt. Nicht down - stumm. Der zentrale EventBridge Bus zeigte sich gesund, keine Fehler, aber null Events wurden geliefert. Nach 90 Minuten panikgetriebenem Debugging über neun AWS-Accounts fanden wir das Problem:

JSON
// Der Tippfehler, der uns $2,3 Millionen an verlorenen Bestellungen kostete
{
  "eventBusName": "central-bus",
  "rules": [{
    "name": "route-all-events",
    "state": "DISABLED",  // <-- Jemand hatte sich während eines "quick fix" vertippt
    "eventPattern": {
      "source": [{ "prefix": "quickgrocer." }]
    }
  }]
}

Jemand hatte die Haupt-Routing-Regel beim Debuggen eines unabhängigen Problems deaktiviert und vergessen, sie wieder zu aktivieren. Eine Checkbox. Neun Accounts betroffen. 90 Minuten Ausfallzeit während der Peak-Saison.

Das führte zu unseren "Nie wieder"-Protokollen:

TypeScript
// Automatisiertes Monitoring für Event Bus Health
const eventBusMonitor = new Lambda(this, 'EventBusMonitor', {
  runtime: Runtime.NODEJS_18_X,
  handler: 'monitor.handler',
  environment: {
    EXPECTED_EVENTS_PER_MINUTE: '1000',
    ALERT_THRESHOLD: '100',
    SLACK_WEBHOOK: process.env.SLACK_WEBHOOK
  }
});

// Jede Minute ausführen
new Rule(this, 'MonitorSchedule', {
  schedule: Schedule.rate(Duration.minutes(1)),
  targets: [new LambdaFunction(eventBusMonitor)]
});

// Die tatsächliche Monitoring-Logik
export const handler = async () => {
  const cloudWatch = new CloudWatchClient({});

  // Check Events published in der letzten Minute
  const metrics = await cloudWatch.send(new GetMetricStatisticsCommand({
    Namespace: 'AWS/Events',
    MetricName: 'SuccessfulRuleMatches',
    StartTime: new Date(Date.now() - 120000),  // 2 Minuten vorher
    EndTime: new Date(),
    Period: 60,
    Statistics: ['Sum']
  }));

  const eventCount = metrics.Datapoints?.[0]?.Sum || 0;

  if (eventCount < parseInt(process.env.ALERT_THRESHOLD!)) {
    // LAUT SCHREIEN
    await sendSlackAlert({
      text: `🚨 EVENT BUS KRITISCH: Nur ${eventCount} Events in letzter Minute!`,
      color: 'danger'
    });

    // Auto-Healing-Versuch
    await enableAllRules();
  }
};

Was wir anders machen würden (Rückblick ist 20/20)#

Nach drei Jahren mit dieser Architektur würde ich meinem früheren Selbst Folgendes sagen:

1. Mit Schema Registry vom ersten Tag starten#

Wir haben schließlich AWS EventBridge Schema Registry implementiert, aber die Migration war schmerzhaft:

TypeScript
// Was wir von Anfang an hätten tun sollen
import { SchemaRegistry } from '@aws-sdk/client-schemas';

const registry = new SchemaRegistry({});

// Schema mit eingebauter Versionierung definieren
const orderSchema = {
  openapi: '3.0.0',
  info: {
    version: '1.0.0',
    title: 'OrderPlaced'
  },
  paths: {},
  components: {
    schemas: {
      OrderPlaced: {
        type: 'object',
        required: ['orderId', 'customerId', 'items', 'totalAmount'],
        properties: {
          orderId: { type: 'string', pattern: '^ord-[0-9a-f]{8} },
          customerId: { type: 'string', pattern: '^cust-[0-9a-f]{8} },
          items: {
            type: 'array',
            items: {
              $ref: '#/components/schemas/OrderItem'
            }
          },
          totalAmount: {
            $ref: '#/components/schemas/Money'
          }
        }
      }
    }
  }
};

// Vor dem Publishing validieren
const validateAndPublish = async (event: any) => {
  const validation = await registry.validateSchema(event, 'OrderPlaced', '1.0.0');
  if (!validation.valid) {
    throw new Error(`Schema-Validierung fehlgeschlagen: ${validation.errors}`);
  }
  return await eventBridge.putEvents({ Entries: [event] });
};

2. Zuerst in Observability investieren, nicht zuletzt#

Wir haben Monitoring als Nachgedanke gebaut. Hätte sein sollen:

TypeScript
// Observability-First-Ansatz
class InstrumentedEventPublisher {
  private metrics: MetricsClient;
  private tracer: Tracer;

  async publish(event: Event): Promise<void> {
    const span = this.tracer.startSpan('event.publish');
    const timer = this.metrics.startTimer('event.publish.duration');

    try {
      // Trace Context zum Event hinzufügen
      event.traceContext = {
        traceparent: span.spanContext().traceId,
        tracestate: span.spanContext().traceState
      };

      await this.eventBridge.putEvents({
        Entries: [{
          ...event,
          Detail: JSON.stringify({
            ...JSON.parse(event.Detail),
            _metadata: {
              timestamp: Date.now(),
              account: process.env.AWS_ACCOUNT_ID,
              service: process.env.SERVICE_NAME,
              version: process.env.SERVICE_VERSION,
              traceId: span.spanContext().traceId
            }
          })
        }]
      });

      this.metrics.increment('event.published', {
        type: event.DetailType,
        source: event.Source
      });

    } catch (error) {
      this.metrics.increment('event.publish.error', {
        type: event.DetailType,
        error: error.name
      });
      span.recordException(error);
      throw error;
    } finally {
      timer.end();
      span.end();
    }
  }
}

3. Account Vending Machine von Anfang an#

Wir haben Accounts zuerst manuell erstellt. Großer Fehler:

TypeScript
// Was wir sofort hätten bauen sollen
import { Organizations } from '@aws-sdk/client-organizations';
import { ControlTower } from '@aws-sdk/client-controltower';

class AccountVendingMachine {
  async createTeamAccount(team: TeamConfig): Promise<AWSAccount> {
    // 1. Account über Control Tower erstellen
    const account = await this.controlTower.createAccount({
      accountName: `quickgrocer-${team.name}-${team.environment}`,
      accountEmail: `aws+${team.name}+${team.environment}@quickgrocer.com`,
      organizationalUnit: this.getOUForTeam(team),

      // Baseline-Konfiguration
      baselineConfig: {
        enableCloudTrail: true,
        enableConfig: true,
        enableSecurityHub: true,
        enableGuardDuty: true,
        budgetLimit: team.monthlyBudget
      }
    });

    // 2. Team-spezifische SCPs anwenden
    await this.applyServiceControlPolicies(account.id, team.permissions);

    // 3. Cross-Account-Rollen einrichten
    await this.setupCrossAccountRoles(account.id, {
      identityServiceRole: 'arn:aws:iam::000000000000:role/identity-validator',
      eventBusRole: 'arn:aws:iam::121212121212:role/event-publisher'
    });

    // 4. Baseline-Infrastruktur deployen
    await this.deployBaseline(account.id, {
      vpcCidr: this.allocateVpcCidr(team),
      eventBusArn: 'arn:aws:events:us-east-1:121212121212:event-bus/central-bus',
      logGroupRetention: 30
    });

    return account;
  }
}

4. Regionale Strategie hätte global sein sollen#

Wir haben alles in us-east-1 gehalten, um es "einfach zu halten". Dann brauchten wir EU-Compliance:

TypeScript
// Multi-Region vom ersten Tag hätte Monate gespart
const multiRegionStack = new Stack(app, 'MultiRegionInfra', {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  }
});

// In mehrere Regionen deployen
['us-east-1', 'eu-west-1', 'ap-southeast-1'].forEach(region => {
  new RegionalStack(app, `Regional-${region}`, {
    env: { region },
    eventBusArn: `arn:aws:events:${region}:121212121212:event-bus/central-bus`,
    // Regionales Event-Routing
    eventRouting: {
      primary: region,
      failover: getFailoverRegion(region)
    }
  });
});

Der aktuelle Stand: Drei Jahre später#

Heute bedient "QuickGrocer" 2,3 Millionen aktive User über 15 AWS-Accounts. Die Multi-Account-Architektur, die wir im Krisenmodus gebaut haben, hat sich zu einer robusten Plattform entwickelt:

Nach Zahlen:

  • Events pro Tag: 450 Millionen
  • Cross-Account API-Calls: 12 Millionen/Tag
  • Durchschnittliche Latenz: 47ms (runter von 340ms)
  • Deployment-Frequenz: 340 Deployments/Woche (hoch von 12)
  • Production-Vorfälle: 2/Monat (runter von 31)
  • AWS-Rechnung: $127.000/Monat (aber wir wissen genau, wo jeder Dollar hingeht)

Die Architektur, die als verzweifelte Antwort auf eine pandemiegetriebene Krise begann, wurde zur Grundlage unserer Engineering-Kultur. Teams besitzen ihr Schicksal, Fehler sind isoliert, und wir können tatsächlich nachts schlafen.

Wichtige Erkenntnisse#

Falls du Multi-Account-Architektur für deine Organisation in Betracht ziehst, hier ist was ich nach drei Jahren in den Schützengräben gelernt habe:

  1. Fang an, bevor du es brauchst: Warte nicht auf eine Krise. Die Migration ist 10x schwerer unter Druck
  2. Event-Driven ist nicht optional: Direkte Service-zu-Service-Calls in Multi-Account werden dich verfolgen
  3. Schema Registry am ersten Tag: Event-Schema-Evolution ohne Versionierung ist organisatorische Schuld
  4. Observability ist kein Feature: Es ist das Fundament. Bau es zuerst, danke dir später
  5. Account Vending ist kritisch: Manuelle Account-Erstellung skaliert nicht über 3-4 Accounts hinaus
  6. Kosten werden dich überraschen: Budget 30% mehr für Multi-Account-Overhead, dann optimiere
  7. Team-Training ist wichtig: Nicht jeder weiß, wie man mit verteilten Systemen umgeht. Investiere in Bildung

Die Multi-Account-Reise geht nicht nur um Technologie – es geht darum, Teams die Autonomie zu geben, zu innovieren, während die organisatorische Kohärenz erhalten bleibt. Es ist chaotisch, teuer und komplex, aber wenn der nächste 10x-Wachstumsschub kommt, bist du bereit.

Denk daran: Verteilte Systeme sind schwer, Multi-Account AWS ist schwerer, aber ein monolithisches Chaos während explosivem Wachstum ist das Schwierigste von allem.

Loading...

Kommentare (0)

An der Unterhaltung teilnehmen

Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren

Noch keine Kommentare

Sei der erste, der deine Gedanken zu diesem Beitrag teilt!

Related Posts