AWS Fargate 102: Die Patterns über die niemand spricht

Fortgeschrittene Fargate-Patterns aus dem Production-Betrieb. Von Kostenoptimierung bis zu stateful Containern - was dir die Docs nicht verraten.

In Fargate 101 haben wir die Grundlagen behandelt. Diese Woche lass uns über die Dinge sprechen, die erst auftauchen, nachdem du eine Weile Production-Workloads betrieben hast. Du weißt schon, die Sachen die dich um 2 Uhr morgens während eines Incidents "oh, so funktioniert das also" sagen lassen.

Kostenoptimierung: Die Kunst, nicht pleite zu gehen#

Erinnerst du dich, als ich sagte, dass Fargate mehr als EC2 kostet? Nun, es gibt Wege, den Schmerz zu lindern. Nachdem unsere AWS-Rechnung einen Monat fünfstellig wurde (frag nicht), wurden wir ernst mit der Optimierung.

Fargate Spot: Der 70% Rabatt, den niemand nutzt#

Fargate Spot ist wie normales Fargate, außer dass AWS deinen Container mit einer 2-Minuten-Warnung ziehen kann. Klingt beängstigend? Es ist tatsächlich für die meisten Workloads ok.

Hier ist, wie man es intelligent nutzt:

hcl
resource "aws_ecs_service" "batch_processor" {
  name            = "batch-processor"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.batch.arn
  
  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight            = 4
    base              = 0
  }
  
  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    weight            = 1
    base              = 2  # Immer 2 auf normalem Fargate behalten
  }
  
  desired_count = 10
}

Diese Konfiguration läuft 80% deiner Tasks auf Spot (~70% Ersparnis) während eine Baseline auf normalem Fargate für Stabilität bleibt. Wir nutzen das für:

  • Batch-Processing-Jobs
  • Development-Umgebungen
  • Nicht-kritische Async-Worker
  • CI/CD-Runner

Pro-Tipp: Richte CloudWatch-Alarme für Spot-Unterbrechungen ein. Wenn wir einen Spike sehen, verlagern wir temporär Traffic auf normales Fargate:

TypeScript
// Lambda-Funktion für Spot-Unterbrechungen
export const handleSpotInterruption = async (event: any) => {
  const ecs = new AWS.ECS();
  
  // Temporär normales Fargate-Gewicht erhöhen
  await ecs.putClusterCapacityProviders({
    cluster: 'production',
    capacityProviders: ['FARGATE', 'FARGATE_SPOT'],
    defaultCapacityProviderStrategy: [
      { capacityProvider: 'FARGATE', weight: 10, base: 5 },
      { capacityProvider: 'FARGATE_SPOT', weight: 1, base: 0 }
    ]
  }).promise();
  
  // Timer für Reversion nach 30 Minuten setzen
  await scheduleReversion();
};

Right-Sizing: Das Goldlöckchen-Problem#

Die meisten Leute überprovisionieren Fargate-Tasks, weil sie Angst vor OOM-Kills haben. So finden wir tatsächlich die richtige Größe:

  1. Groß starten, messen, dann verkleinern

    Bash
    # Mit großzügigen Ressourcen deployen
    CPU: 1024
    Memory: 2048
    
    # Nach einer Woche, tatsächliche Nutzung prüfen
    aws cloudwatch get-metric-statistics \
      --namespace ECS/ContainerInsights \
      --metric-name MemoryUtilized \
      --dimensions Name=ServiceName,Value=my-service \
      --statistics Average,Maximum \
      --start-time 2024-01-01T00:00:00Z \
      --end-time 2024-01-08T00:00:00Z \
      --period 3600
    
  2. Die 80%-Regel verwenden: Größe für 80% der Peak-Nutzung, nicht 100%. Diese 20% Puffer handhabt Spikes.

  3. Unterschiedliche Größen für verschiedene Umgebungen:

    hcl
    locals {
      task_sizes = {
        production = {
          cpu    = "512"
          memory = "1024"
        }
        staging = {
          cpu    = "256"
          memory = "512"
        }
        development = {
          cpu    = "256"
          memory = "512"
        }
      }
    }
    
    resource "aws_ecs_task_definition" "app" {
      cpu    = local.task_sizes[var.environment].cpu
      memory = local.task_sizes[var.environment].memory
      # ...
    }
    

ARM + Savings Plans: Der Doppel-Rabatt#

AWS Graviton (ARM) Prozessoren sind 20% günstiger und oft schneller. Kombiniere mit Savings Plans für weitere 20% Rabatt:

Dockerfile
# Multi-arch Dockerfile
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"

# Deine Build-Schritte hier...

FROM node:18-alpine
COPY --from=builder /app /app
CMD ["node", "index.js"]

Für beide Architekturen bauen:

Bash
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag myapp:latest \
  --push .

Dann in deiner Task-Definition:

JSON
{
  "runtimePlatform": {
    "cpuArchitecture": "ARM64",
    "operatingSystemFamily": "LINUX"
  }
}

Wir haben 35% gespart, indem wir unsere Node.js-Services auf Graviton verschoben haben. Das einzige was nicht funktionierte? Eine Legacy-Java-App mit x86-spezifischen JNI-Bibliotheken. Alles andere war ok.

Stateful Workloads: Ja, du kannst (Aber solltest du?)#

Jeder sagt, Container sollten stateless sein. Jeder hat meistens recht. Aber manchmal brauchst du State, und EFS ist hier dein Freundfeind.

EFS: Das Gute, Schlechte und Hässliche#

Loading diagram...

EFS mit Fargate einrichten:

hcl
resource "aws_efs_file_system" "shared" {
  creation_token = "shared-storage"
  
  performance_mode = "generalPurpose"  # oder "maxIO" für mehr Operationen
  throughput_mode  = "bursting"        # oder "provisioned" für konstante Performance
  
  lifecycle_policy {
    transition_to_ia = "AFTER_30_DAYS"  # Geld sparen bei kalten Daten
  }
}

resource "aws_ecs_task_definition" "app" {
  # ... andere Config ...
  
  volume {
    name = "shared-storage"
    
    efs_volume_configuration {
      file_system_id          = aws_efs_file_system.shared.id
      root_directory          = "/"
      transit_encryption      = "ENABLED"
      transit_encryption_port = 2999
      
      authorization_config {
        access_point_id = aws_efs_access_point.app.id
        iam             = "ENABLED"
      }
    }
  }
  
  container_definitions = jsonencode([{
    name = "app"
    # ...
    mountPoints = [{
      sourceVolume  = "shared-storage"
      containerPath = "/data"
    }]
  }])
}

Der Reality Check:

  • EFS-Latenz: 5-10ms pro Operation (vs 0.1ms für lokale SSD)
  • Durchsatz: Bursts auf 100MB/s, hält 10MB/s (außer du zahlst mehr)
  • Kosten: $0.30/GB/Monat (vs $0.10 für EBS)

Wann EFS Sinn macht:

  • Geteilte Konfigurationsdateien
  • User-Uploads die mehrere Container brauchen
  • Build-Caches (mit vorsichtigem Locking)
  • Legacy-Apps die absolut ein geteiltes Dateisystem brauchen

Wann nicht:

  • Database-Storage (verwende einfach RDS)
  • Hochfrequente Schreibvorgänge
  • Temporäre Dateien (verwende den ephemeren Storage des Containers)
  • Cache-Layer (verwende ElastiCache)

Das Session-Affinity-Pattern#

Manchmal brauchst du Sticky Sessions. So machst du es mit Fargate:

hcl
resource "aws_lb_target_group" "app" {
  name     = "app-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id
  target_type = "ip"
  
  stickiness {
    type            = "app_cookie"
    cookie_duration = 86400
    cookie_name     = "FARGATE_SESSION"
  }
  
  health_check {
    enabled = true
    path    = "/health"
    matcher = "200"
  }
}

Aber hier ist die Sache: Sticky Sessions + Auto-Scaling = Traurigkeit. Wenn eine Task stirbt, sind diese Sessions weg. Wir haben gelernt:

  1. Session-Daten in ElastiCache Redis speichern
  2. JWT-Tokens statt Server-Sessions verwenden
  3. Akzeptieren, dass einige User während Deployments ausgeloggt werden

Monitoring: Was passiert da eigentlich drin?#

Fargate ist eine Black Box, was Debugging... interessant macht. Hier ist unser Monitoring-Stack der tatsächlich funktioniert:

Die drei Säulen der Fargate-Observability#

  1. CloudWatch Container Insights (Die Basics)

    Bash
    aws ecs put-account-setting \
      --name containerInsights \
      --value enabled
    

    Das gibt dir CPU, Memory, Network und Disk-Metriken. Es ist ok für Basics, verpasst aber Application-Level-Zeug.

  2. X-Ray für Distributed Tracing (Die Verbindungen)

    JavaScript
    // Zu deiner Node.js-App hinzufügen
    const AWSXRay = require('aws-xray-sdk-core');
    const AWS = AWSXRay.captureAWS(require('aws-sdk'));
    
    // Jetzt werden alle AWS SDK Calls getrackt
    const s3 = new AWS.S3();
    await s3.getObject({ Bucket: 'my-bucket', Key: 'file.txt' }).promise();
    
  3. Custom Metrics via StatsD (Die Details)

    TypeScript
    // Einen Sidecar-Container für StatsD laufen lassen
    const taskDef = {
      containerDefinitions: [
        {
          name: "app",
          // deine App-Config
        },
        {
          name: "datadog-agent",
          image: "datadog/agent:latest",
          environment: [
            { name: "DD_API_KEY", value: process.env.DD_API_KEY },
            { name: "ECS_FARGATE", value: "true" }
          ]
        }
      ]
    };
    

Das Debug-Pattern das Stunden spart#

Kannst nicht per SSH in Fargate? Verwende ECS Exec, aber mach es nützlich:

Bash
# Das zu deinem Dockerfile hinzufügen
RUN apk add --no-cache \
    curl \
    netstat \
    ps \
    htop \
    strace \
    tcpdump

# ECS Exec in Task-Definition aktivieren
aws ecs update-service \
  --cluster production \
  --service my-service \
  --enable-execute-command

# Wie ein Profi debuggen
aws ecs execute-command \
  --cluster production \
  --task abc123 \
  --container app \
  --interactive \
  --command "/bin/sh"

# Im Container:
> netstat -tulpn  # Check was lauscht
> ps aux          # Alle Prozesse sehen
> strace -p 1     # System Calls tracen
> tcpdump -i any  # Network-Traffic beobachten

Blue-Green Deployments: Der sichere Weg#

Fargate + CodeDeploy = Zero-Downtime-Deployments. Hier ist das Setup das uns vor vielen schlechten Deploys gerettet hat:

hcl
resource "aws_codedeploy_deployment_group" "app" {
  app_name               = aws_codedeploy_app.app.name
  deployment_group_name  = "production"
  deployment_config_name = "CodeDeployDefault.ECSLinear10PercentEvery1Minutes"
  
  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
  }
  
  blue_green_deployment_config {
    terminate_blue_instances_on_deployment_success {
      action                                          = "TERMINATE"
      termination_wait_time_in_minutes               = 5
    }
    
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }
    
    green_fleet_provisioning_option {
      action = "COPY_AUTO_SCALING_GROUP"
    }
  }
  
  ecs_service {
    cluster_name = aws_ecs_cluster.main.name
    service_name = aws_ecs_service.app.name
  }
}

Das Killer-Feature? Automatisches Rollback bei CloudWatch-Alarmen:

TypeScript
const errorRateAlarm = new cloudwatch.Alarm(this, 'ErrorRate', {
  metric: new cloudwatch.Metric({
    namespace: 'MyApp',
    metricName: 'Errors',
    statistic: 'Sum'
  }),
  threshold: 10,
  evaluationPeriods: 2
});

// Mit Deployment-Group verlinken
deploymentGroup.addAlarm(errorRateAlarm);

Multi-Region Fargate: Weil Katastrophen passieren#

Fargate über Regionen zu betreiben ist nicht schwer, aber sie synchron zu halten schon. Hier ist unser Pattern:

Loading diagram...

Die Gotchas die wir entdeckt haben:

  1. Environment-Variablen pro Region: Hardcode keine Endpoints
  2. S3-Bucket-Namen müssen global einzigartig sein: Region-Suffix hinzufügen
  3. Cross-Region-Latenz: ~100ms zwischen US und EU
  4. Failover ist nicht instant: Route 53 Health Checks brauchen 30-60 Sekunden

Die Patterns die wir früher hätten kennen sollen#

Das Sidecar-Pattern#

JSON
{
  "containerDefinitions": [
    {
      "name": "app",
      "dependsOn": [{
        "containerName": "envoy",
        "condition": "HEALTHY"
      }]
    },
    {
      "name": "envoy",
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:9901/ready || exit 1"]
      }
    }
  ]
}

Das Init-Container-Pattern (Sozusagen)#

Fargate hat keine echten Init-Container, aber du kannst es faken:

Bash
# In deinem Entrypoint-Script
#!/bin/sh
echo "Initialisierung läuft..."
/app/init-db.sh
if [ $? -ne 0 ]; then
  echo "Init fehlgeschlagen, beende"
  exit 1
fi
echo "Starte Hauptanwendung..."
exec node index.js

Das Circuit-Breaker-Pattern#

TypeScript
class FargateCircuitBreaker {
  private failures = 0;
  private lastFailTime = 0;
  private readonly threshold = 5;
  private readonly timeout = 60000; // 1 Minute
  
  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.isOpen()) {
      throw new Error('Circuit Breaker ist offen');
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private isOpen(): boolean {
    return this.failures >= this.threshold && 
           Date.now() - this.lastFailTime < this.timeout;
  }
  
  private onSuccess(): void {
    this.failures = 0;
  }
  
  private onFailure(): void {
    this.failures++;
    this.lastFailTime = Date.now();
  }
}

Denk dran: Fargate ist wie ein Schweizer Taschenmesser. Unglaublich nützlich, aber du schneidest dich gelegentlich, wenn du nicht vorsichtig bist.

AWS Fargate Deep Dive Serie

Vollständiger Leitfaden zu AWS Fargate von den Grundlagen bis zur Produktion. Lerne serverlose Container, Kostenoptimierung, Debugging-Techniken und Infrastructure-as-Code-Deployment-Patterns durch reale Erfahrungen.

Fortschritt2/4 Beiträge abgeschlossen
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