AWS Fargate 103: Production-Kriegsgeschichten die dir Stunden sparen
Production-Incidents vom Fargate-Betrieb im großen Maßstab. Memory Leaks, ENI-Limits, Subnet-Ausfälle und funktionierende Debugging-Techniken.
Du kennst das Gefühl, wenn du dir bei deinem Fargate-Setup sicher bist, alles auf deinen Monitoring-Dashboards grün aussieht, und dann passiert 3 Uhr morgens? Ja, willkommen in der Production.
Nach zwei Jahren Fargate-Workloads im großen Maßstab habe ich genug Kriegsgeschichten gesammelt, um eine kleine Bibliothek zu füllen. Das ist der letzte Post unserer Fargate-Serie (101, 102) - hier sind die echten Incidents, die mir am meisten beigebracht haben, zusammen mit den Debugging-Techniken und Fixes, die tatsächlich funktioniert haben.
Die Große ENI-Dürre vom Black Friday#
Das Setup: E-Commerce-Site, Black Friday-Traffic soll 10x normal sein. Fargate Auto-Scaling konfiguriert, Load-Test bestanden, alle fühlten sich gut.
Was passierte: Um 23:47 am Thanksgiving begannen unsere Tasks mit diesem kryptischen Fehler zu scheitern:
ResourcesNotReady: The ENI allocation could not be completed
Unsere Fargate-Tasks hingen im PENDING-Zustand fest. Neue Deployments schlugen fehl. Auto-Scaling skalierte nicht. Das Dashboard zeigte alles grün, außer einem kleinen Detail: verfügbare ENIs in unserer VPC waren auf null gesunken.
Die Untersuchung#
Erinnerst du dich, wie jede Fargate-Task ihr eigenes ENI braucht? Nun, wir hatten 200 Tasks über 3 Availability Zones laufen, und AWS hat ENI-Limits pro VPC. Für eine Standard-VPC in us-east-1:
- Standard ENI-Limit: 5.000 pro VPC
- Jede Fargate-Task: 1 ENI
- Jede RDS-Instanz: 1 ENI
- Jedes Lambda in VPC: Teilt ENI-Pool
- Jeder ELB: Mehrere ENIs
Wir prüften unsere Limits:
aws ec2 describe-account-attributes \
--attribute-names max-instances
# Aber das echte Limit ist hier:
aws service-quotas get-service-quota \
--service-code ec2 \
--quota-code L-0263D0A3 # ENIs pro VPC
Wir waren bei 4.847 ENIs. Unser "Load Testing" hatte nicht alle anderen Services berücksichtigt, die ENIs verbrauchen.
Der Fix (Notfall)#
Sofortiger Fix (03:15):
# Tasks in Development-VPC zuerst killen
aws ecs update-service \
--cluster development \
--service api \
--desired-count 0
# Sofortige Quota-Erhöhung anfordern (AWS Support)
aws support create-case \
--subject "URGENT: ENI quota increase needed - production impact" \
--service-code "service-quota-increase"
Der echte Fix:
- Mehrere VPCs: Workloads auf Dev-, Staging- und Prod-VPCs aufteilen
- ENI-Monitoring: CloudWatch Custom Metric zur ENI-Nutzungsverfolgung
- Right-Sizing: Über-provisionierte Tasks reduzieren
- Lambda-Optimierung: Lambdas wo möglich aus VPC verschieben
// ENI-Monitoring Lambda
export const monitorENIs = async () => {
const ec2 = new AWS.EC2();
const cloudWatch = new AWS.CloudWatch();
const enis = await ec2.describeNetworkInterfaces().promise();
const inUse = enis.NetworkInterfaces?.length || 0;
await cloudWatch.putMetricData({
Namespace: 'Custom/VPC',
MetricData: [{
MetricName: 'ENIsInUse',
Value: inUse,
Unit: 'Count'
}]
}).promise();
};
Gelernte Lektionen:
- Load Testing sollte alle Infrastruktur-Komponenten einschließen, nicht nur deine Anwendung
- ENI-Limits sind pro VPC, nicht pro Service
- AWS Support ist überraschend schnell um 3 Uhr morgens
Das Subnet, das zum Schurken wurde#
Das Setup: Multi-AZ Fargate-Deployment über drei private Subnets. Lief monatelang reibungslos.
Was passierte: Dienstag morgen begannen 40% unserer Tasks intermittierende Verbindungsprobleme zu zeigen. Einige HTTP-Requests erfolgreich, andere nach 30 Sekunden Timeout.
Der seltsame Teil? Nur Tasks in einem spezifischen Subnet (us-east-1a) waren betroffen.
Die Untersuchungsreise#
Zuerst die offensichtlichen Checks:
# Task-Health prüfen
aws ecs list-tasks --cluster production --service-name api
aws ecs describe-tasks --cluster production --tasks task-abc123
# Network Interfaces prüfen
aws ec2 describe-network-interfaces \
--filters "Name=subnet-id,Values=subnet-12345" \
--query 'NetworkInterfaces[*].[NetworkInterfaceId,Status,PrivateIpAddress]'
Tasks sahen gesund aus. Network Interfaces waren angehängt und aktiv. Aber etwas war falsch.
Der Durchbruch: Wir aktivierten VPC Flow Logs und fanden die rauchende Waffe:
# VPC Flow Logs für das Problem-Subnet aktivieren
aws ec2 create-flow-logs \
--resource-type Subnet \
--resource-ids subnet-12345 \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /aws/vpc/flowlogs
Flow Logs zeigten, dass Pakete unser Subnet verließen, aber nie ihr Ziel erreichten. Die Rückantwort-Pakete wurden irgendwo gedroppt.
Der echte Übeltäter#
Es stellte sich heraus, dass unser Netzwerk-Team die Route-Tabelle für das Subnet früh am Morgen geändert hatte. Sie änderten die NAT Gateway-Route von 0.0.0.0/0 → nat-gateway-123
zu 0.0.0.0/0 → nat-gateway-456
, ohne zu realisieren, dass Fargate-Tasks dort liefen.
Das neue NAT Gateway war in einer anderen AZ und hatte andere Security Group-Regeln. Klassisch.
Der Fix:
# Prüfen welche Route-Tabelle mit dem Subnet assoziiert ist
aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=subnet-12345"
# Die Routes verifizieren
aws ec2 describe-route-tables --route-table-ids rtb-abc123 \
--query 'RouteTables[*].Routes[*].[DestinationCidrBlock,GatewayId,State]'
# Die Route fixen (zum ursprünglichen NAT Gateway zurück)
aws ec2 replace-route \
--route-table-id rtb-abc123 \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-gateway-123
Gelernte Lektionen:
- Teste Route-Änderungen immer zuerst in Non-Production
- VPC Flow Logs sind dein Freund für Network-Debugging
- Dokumentiere welche Route-Tabellen welche Services bedienen
- Richte Monitoring für Route-Tabellen-Änderungen ein
Das Memory Leak-Mysterium (Keine SSH-Edition)#
Das Setup: Node.js API auf Fargate, Memory-Limit auf 2GB pro Task gesetzt. Funktionierte wochenlang einwandfrei.
Was passierte: Memory-Nutzung stieg langsam über 3-4 Stunden, dann wurden Tasks OOM-gekillt. Memory-Nutzungs-Graphen sahen aus wie Skipisten.
Aber hier ist der Haken: keine Möglichkeit, per SSH in den Container zu debuggen.
Das Untersuchungs-Arsenal#
Da wir kein SSH können, müssen wir kreativ werden:
1. ECS Exec (unser Retter):
# Zuerst auf dem Service aktivieren
aws ecs update-service \
--cluster production \
--service api \
--enable-execute-command
# Dann zu laufender Task verbinden
aws ecs execute-command \
--cluster production \
--task task-abc123 \
--container api \
--interactive \
--command "/bin/bash"
# Im Container Memory-Nutzung prüfen
> ps aux --sort=-%mem | head -20
> cat /proc/meminfo
> pmap -x 1 # Memory Map von PID 1
2. Application-Level Monitoring:
// Zu deiner Node.js-App hinzufügen
const express = require('express');
const app = express();
// Memory-Monitoring-Endpoint
app.get('/debug/memory', (req, res) => {
const used = process.memoryUsage();
const stats = {
rss: Math.round(used.rss / 1024 / 1024 * 100) / 100, // MB
heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100,
heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100,
external: Math.round(used.external / 1024 / 1024 * 100) / 100,
arrayBuffers: Math.round(used.arrayBuffers / 1024 / 1024 * 100) / 100
};
res.json(stats);
});
// Heap Snapshot-Endpoint (für extremes Debugging)
app.get('/debug/heapdump', (req, res) => {
const heapdump = require('heapdump');
const filename = `/tmp/heapdump-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, (err, filename) => {
if (err) {
res.status(500).send(err.message);
} else {
res.download(filename);
}
});
});
3. Die Detektivarbeit:
Wir verwendeten ECS Exec, um Debugging-Tools zu installieren und fanden heraus, dass unser HTTP-Client Verbindungen nicht ordnungsgemäß schloss:
# Im Container
> npm install -g clinic
> clinic doctor --on-port 8080 -- node index.js &
> curl http://localhost:8080/debug/memory
# Offene File Descriptors prüfen
> ls -la /proc/1/fd | wc -l
> lsof -p 1 | grep TCP
Bingo! Tausende von TCP-Verbindungen im CLOSE_WAIT-Zustand.
Die Grundursache#
Unser Node.js HTTP-Client-Code sah harmlos genug aus:
// Der problematische Code
const axios = require('axios');
async function callExternalAPI() {
const response = await axios.get('https://api.example.com/data');
return response.data;
}
Aber wir konfigurierten Connection Pooling oder Timeouts nicht ordnungsgemäß. Jeder Request erstellte neue Verbindungen, die nicht aufgeräumt wurden.
Der Fix:
// Reparierte Version mit ordnungsgemäßer Konfiguration
const axios = require('axios');
const https = require('https');
const http = require('http');
// Connection Pooling konfigurieren
const httpAgent = new http.Agent({
keepAlive: true,
maxSockets: 50,
timeout: 5000,
});
const httpsAgent = new https.Agent({
keepAlive: true,
maxSockets: 50,
timeout: 5000,
});
const axiosInstance = axios.create({
httpAgent,
httpsAgent,
timeout: 10000, // 10 Sekunden
});
// Graceful Shutdown
process.on('SIGTERM', () => {
httpAgent.destroy();
httpsAgent.destroy();
});
async function callExternalAPI() {
const response = await axiosInstance.get('https://api.example.com/data');
return response.data;
}
Gelernte Lektionen:
- ECS Exec ist unschätzbar für containerisiertes Debugging
- Konfiguriere HTTP-Clients immer ordnungsgemäß in Production
- Überwache File Descriptors, nicht nur Memory
- Connection Pools sind wichtig, auch für "einfache" HTTP-Clients
Das 30-Sekunden Connection Timeout-Phantom#
Das Setup: Interne Service-zu-Service-Kommunikation zwischen zwei Fargate-Services. Funktionierte 99% der Zeit einwandfrei.
Was passierte: Zufällig würden etwa 1% der Requests genau 30 Sekunden hängen, dann mit einem Connection Timeout fehlschlagen.
Das Muster war völlig zufällig. Keine Korrelation mit Last, Tageszeit oder Deployment-Historie.
Die Debugging-Odyssee#
Network-Layer-Untersuchung:
# VPC Flow Logs-Analyse
aws logs filter-log-events \
--log-group-name /aws/vpc/flowlogs \
--start-time 1645564800000 \
--filter-pattern "REJECT"
# Security Group-Regeln-Audit
aws ec2 describe-security-groups \
--group-ids sg-12345 \
--query 'SecurityGroups[*].{GroupId:GroupId,IpPermissions:IpPermissions}'
Security Groups sahen in Ordnung aus. Flow Logs zeigten normale Paket-Flüsse.
Application-Layer-Untersuchung:
// Detailliertes Connection-Tracking hinzugefügt
const net = require('net');
const original_connect = net.Socket.prototype.connect;
net.Socket.prototype.connect = function(...args) {
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] Starting connection to ${args[0]?.host || args[0]?.path}`);
const result = original_connect.apply(this, args);
this.on('connect', () => {
const duration = Date.now() - startTime;
console.log(`[${new Date().toISOString()}] Connected after ${duration}ms`);
});
this.on('error', (err) => {
const duration = Date.now() - startTime;
console.log(`[${new Date().toISOString()}] Connection error after ${duration}ms:`, err.message);
});
return result;
};
Der Durchbruch#
Die Logs zeigten etwas Interessantes: erfolgreiche Verbindungen dauerten 2-5ms, aber die hängenden dauerten genau 30.000ms. Das ist nicht zufällig - das ist ein Timeout.
Dann bemerkten wir das Muster: es passierte nur, wenn beide Services in der gleichen Availability Zone waren und die Verbindung durch den Load Balancer ging.
Loading diagram...
Das Problem: AWS ALB hat eine bekannte Eigenart, bei der Verbindungen aus der gleichen AZ gelegentlich durch die Load Balancer-Infrastruktur zurückschleifen können, was Verzögerungen verursacht.
Der Fix (mehrere Strategien):
- Direkte Service-Kommunikation für gleiche AZ:
// Service Discovery mit AZ-Awareness
const AWS = require('aws-sdk');
const ecs = new AWS.ECS();
async function getServiceEndpoints() {
const tasks = await ecs.listTasks({
cluster: 'production',
serviceName: 'target-service'
}).promise();
const taskDetails = await ecs.describeTasks({
cluster: 'production',
tasks: tasks.taskArns
}).promise();
return taskDetails.tasks.map(task => ({
ip: task.attachments[0].details.find(d => d.name === 'privateIPv4Address').value,
az: task.availabilityZone,
port: 8080
}));
}
// Intelligentes Routing
async function callService(endpoint, data) {
const currentAZ = process.env.AWS_AVAILABILITY_ZONE;
const endpoints = await getServiceEndpoints();
// Zuerst gleiche AZ direkte Verbindung versuchen
const sameAZEndpoint = endpoints.find(e => e.az === currentAZ);
if (sameAZEndpoint) {
try {
return await axios.post(`http://${sameAZEndpoint.ip}:${sameAZEndpoint.port}${endpoint}`, data);
} catch (error) {
// Auf Load Balancer zurückfallen
return await axios.post(`https://internal-service.example.com${endpoint}`, data);
}
}
// Load Balancer für Cross-AZ verwenden
return await axios.post(`https://internal-service.example.com${endpoint}`, data);
}
- Connection Timeout-Tuning:
const axiosInstance = axios.create({
timeout: 5000, // Schnell fehlschlagen statt 30s warten
httpsAgent: new https.Agent({
timeout: 2000, // Connection Timeout
keepAlive: true,
})
});
Gelernte Lektionen:
- ALBs können unerwartete Latenz für gleiche AZ-Kommunikation einführen
- Service Discovery ermöglicht direkte Kommunikationsmuster
- Implementiere immer Connection Timeouts kürzer als deine SLA
- Load Balancer sind nicht immer der schnellste Pfad
Das Deployment, das sich nicht deployen ließ#
Das Setup: Standard Blue-Green-Deployment mit CodeDeploy. Funktionierte hunderte Male zuvor.
Was passierte: Neues Deployment steckte stundenlang bei 50% fest. Die Hälfte der Tasks lief die neue Version, die andere Hälfte die alte. Das CodeDeploy-Dashboard zeigte "In Progress" ohne Fehlermeldungen.
Auto-Rollback wurde nicht ausgelöst, weil technisch gesehen nichts "fehlschlug".
Die Untersuchung#
CodeDeploy-Logs waren nicht hilfreich:
aws deploy get-deployment --deployment-id d-XXXXXXXXX
# Status: InProgress, keine Fehlerinformationen
aws logs filter-log-events \
--log-group-name /aws/codedeploy-agent \
--start-time $(date -d '1 hour ago' +%s)000
ECS Service Events zeigten die echte Geschichte:
aws ecs describe-services \
--cluster production \
--services api \
--query 'services[0].events[0:10]'
Die Events zeigten:
"(service api) failed to launch a task with (error ECS was unable to assume role...)"
Die Grundursache#
Unsere Task Execution Role war von einem anderen Team für einen unverwandten Service geändert worden, und sie hatten versehentlich die Trust Relationship entfernt, die ECS erlaubt, die Rolle zu übernehmen.
Die Role Policy sah so aus:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com" // FALSCH!
},
"Action": "sts:AssumeRole"
}
]
}
Es hätte so sein sollen:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com" // RICHTIG
},
"Action": "sts:AssumeRole"
}
]
}
Der Fix:
# Role Trust Policy prüfen
aws iam get-role --role-name fargate-task-execution-role \
--query 'Role.AssumeRolePolicyDocument'
# Aktualisieren
aws iam update-assume-role-policy \
--role-name fargate-task-execution-role \
--policy-document file://trust-policy.json
Präventionsstrategie:
// Automatisierte Role-Validierung
export const validateTaskRoles = async () => {
const iam = new AWS.IAM();
const role = await iam.getRole({
RoleName: 'fargate-task-execution-role'
}).promise();
const trustPolicy = JSON.parse(decodeURIComponent(role.Role.AssumeRolePolicyDocument));
const ecsService = trustPolicy.Statement.some(statement =>
statement.Principal?.Service === 'ecs-tasks.amazonaws.com'
);
if (!ecsService) {
await sendAlert('Task execution role missing ECS trust relationship!');
return false;
}
return true;
};
Gelernte Lektionen:
- ECS Service Events sind detaillierter als CodeDeploy-Logs
- Role Trust Policies sind fragil und brauchen Monitoring
- Blue-Green-Deployments können in der Schwebe stecken bleiben
- Prüfe immer IAM, wenn Dinge mysteriös aufhören zu funktionieren
Die Debug-Toolbox, die wirklich funktioniert#
Nach all diesen Incidents ist hier das Debugging-Toolkit, das uns unzählige Stunden gespart hat:
1. Der ultimative Fargate Debug Container#
FROM node:18-alpine
RUN apk add --no-cache \
curl \
wget \
netcat-openbsd \
bind-tools \
tcpdump \
strace \
htop \
iotop \
lsof \
procps \
net-tools
# Deine App hinzufügen
COPY . /app
WORKDIR /app
# Debug-Endpoints
RUN npm install express heapdump clinic
2. Monitoring Stack#
// Health Check-Endpoint mit detaillierter Diagnose
app.get('/health/detailed', async (req, res) => {
const health = {
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
cpu: process.cpuUsage(),
connections: {
active: await getActiveConnections(),
waiting: await getWaitingConnections()
},
environment: {
nodeVersion: process.version,
availabilityZone: process.env.AWS_AVAILABILITY_ZONE || 'unknown',
region: process.env.AWS_REGION || 'unknown'
}
};
res.json(health);
});
async function getActiveConnections() {
return new Promise((resolve) => {
require('child_process').exec('netstat -an | grep ESTABLISHED | wc -l',
(error, stdout) => {
resolve(parseInt(stdout.trim()) || 0);
}
);
});
}
3. Automatisierte Incident Response#
# CloudWatch-Alarme, die tatsächlich helfen
ENIUtilizationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: High-ENI-Utilization
MetricName: ENIsInUse
Namespace: Custom/VPC
Statistic: Maximum
Period: 300
EvaluationPeriods: 2
Threshold: 4500 # 90% von 5000 Limit
ComparisonOperator: GreaterThanThreshold
AlarmActions:
- !Ref SNSTopic
MemoryUtilizationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: Fargate-Memory-High
MetricName: MemoryUtilized
Namespace: ECS/ContainerInsights
Statistic: Average
Period: 300
EvaluationPeriods: 3
Threshold: 80 # 80% Memory-Nutzung
ComparisonOperator: GreaterThanThreshold
Die universellen Gesetze des Fargate-Debuggings#
Nach all diesen Abenteuern sind hier die Muster, die zutreffen:
-
Wenn Tasks nicht starten: Prüfe ENI-Limits, Security Groups und IAM-Rollen (in dieser Reihenfolge)
-
Wenn Tasks langsam sind: Es ist meist das Netzwerk (Route-Tabellen, NAT-Gateways, DNS)
-
Wenn Memory stetig steigt: Es ist immer Connection Pooling oder Event Listeners
-
Wenn Deployments hängen: Prüfe Service Events, nicht Deployment-Logs
-
Wenn 1% der Requests fehlschlagen: Suche nach Load Balancer-Eigenarten oder Cross-AZ-Problemen
-
Wenn nichts Sinn macht: Aktiviere VPC Flow Logs und ECS Exec
Die echte Lektion? Fargate entfernt viel Infrastruktur-Komplexität, aber wenn Dinge schiefgehen, musst du die zugrundeliegenden AWS Networking- und Compute-Primitive verstehen. Die Abstraktion ist undicht, und Production findet immer die Lecks.
Behalte diese Debugging-Techniken griffbereit. Vertrau mir, du wirst sie eines Tages um 3 Uhr morgens brauchen, und wenn das passiert, wirst du für jeden Monitoring-Endpoint und jedes Diagnosetool dankbar sein, das du vorher eingerichtet hast.
Das nächste Mal, wenn jemand dir erzählt, dass serverlose Container "einstellen und vergessen" sind, zeig ihm diese Serie. Production hat andere Pläne, aber jetzt bist du darauf vorbereitet.
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.
Alle Beiträge in dieser Serie
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!
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!